diff --git a/.github/workflows/slow-test.yml b/.github/workflows/slow-test-develop.yml similarity index 95% rename from .github/workflows/slow-test.yml rename to .github/workflows/slow-test-develop.yml index c53474d703..bf50b9c783 100644 --- a/.github/workflows/slow-test.yml +++ b/.github/workflows/slow-test-develop.yml @@ -17,6 +17,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + ref: "develop" - name: Set up Python uses: actions/setup-python@v4 diff --git a/.github/workflows/slow-test-master.yml b/.github/workflows/slow-test-master.yml new file mode 100644 index 0000000000..db0ee9e0db --- /dev/null +++ b/.github/workflows/slow-test-master.yml @@ -0,0 +1,40 @@ +name: Slow test + +on: + schedule: + - cron: '30 1 * * 0' + + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-20.04 + strategy: + matrix: + python-version: [ 3.8, 3.7 ] + + steps: + + - uses: actions/checkout@v3 + with: + ref: "master" + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install pip setuptools wheel + pip install pytest + pip install -r requirements.txt + + - name: Test with pytest + run: | + pytest --runslow --disable-warnings + + #- name: Coverage report + # run: | + # coverage html diff --git a/CHANGELOG.md b/CHANGELOG.md index cf01bf389c..a4d4b9a670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release notes +## Version 1.62 + +- Added order simulator as an optimal replacement for vectorised p&l calculation; prequisite for limit order simulation +- Replace pst logging with python logging +- ignore daily expiries for certain EUREX contracts +- Allow fixed instrument and forecast weights to be specificed as a hierarchy + ## Version 1.61 - Replaced log to database with log to file diff --git a/README.md b/README.md index 96d5d0f074..16b608ed3b 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Rob Carver [https://qoppac.blogspot.com/p/pysystemtrade.html](https://qoppac.blogspot.com/p/pysystemtrade.html) -Version 1.61 +Version 1.62 -2023-03-24 +2023-06-09 diff --git a/docs/backtesting.md b/docs/backtesting.md index f09c2db26e..dd9d226e58 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -4399,7 +4399,7 @@ Other methods exist to access logging and caching. | `positionSize.get_block_value` | Standard | `instrument_code` | D | Get value of a 1% move in the price | | `positionSize.get_instrument_currency_vol` | Standard | `instrument_code` |D | Get daily volatility in the currency of the instrument | | `positionSize.get_instrument_value_vol` | Standard | `instrument_code` |D | Get daily volatility in the currency of the trading account | -| `positionSize.get_volatility_scalar` | Standard | `instrument_code` | D |Get ratio of target volatility vs volatility of instrument in instrument's own currency | +| `positionSize.get_average_position_at_subsystem_level` | Standard | `instrument_code` | D |Get ratio of target volatility vs volatility of instrument in instrument's own currency | | `positionSize.get_subsystem_position`| Standard | `instrument_code` | D, O |Get position if we put our entire trading capital into one instrument | diff --git a/examples/introduction/simplesystem.py b/examples/introduction/simplesystem.py index 23141be605..2990c2a560 100644 --- a/examples/introduction/simplesystem.py +++ b/examples/introduction/simplesystem.py @@ -150,7 +150,7 @@ print(my_system.positionSize.get_block_value("SOFR").tail(5)) print(my_system.positionSize.get_underlying_price("SOFR")) print(my_system.positionSize.get_instrument_value_vol("SOFR").tail(5)) -print(my_system.positionSize.get_volatility_scalar("SOFR").tail(5)) +print(my_system.positionSize.get_average_position_at_subsystem_level("SOFR").tail(5)) print(my_system.positionSize.get_vol_target_dict()) print(my_system.positionSize.get_subsystem_position("SOFR").tail(5)) diff --git a/syscore/genutils.py b/syscore/genutils.py index ea00d84f71..401373104a 100755 --- a/syscore/genutils.py +++ b/syscore/genutils.py @@ -132,6 +132,10 @@ def str_of_int(x: int) -> str: return "" +def same_sign(x, y): + return sign(x) == sign(y) + + def sign(x: Union[int, float]) -> float: """ >>> sign(3) diff --git a/sysdata/production/historic_orders.py b/sysdata/production/historic_orders.py index 2ec2a799b4..075371f6f3 100644 --- a/sysdata/production/historic_orders.py +++ b/sysdata/production/historic_orders.py @@ -19,7 +19,7 @@ from sysexecution.orders.named_order_objects import missing_order from sysdata.base_data import baseData -from sysobjects.fills import listOfFills, fill_from_order +from sysobjects.fills import ListOfFills, fill_from_order from sysexecution.orders.base_orders import Order from sysexecution.orders.broker_orders import single_fill_from_broker_order from sysexecution.order_stacks.order_stack import missingOrder @@ -71,7 +71,7 @@ def get_list_of_order_ids_in_date_range( class strategyHistoricOrdersData(genericOrdersData): def get_fills_history_for_instrument_strategy( self, instrument_strategy: instrumentStrategy - ) -> listOfFills: + ) -> ListOfFills: """ :param instrument_code: str @@ -82,7 +82,7 @@ def get_fills_history_for_instrument_strategy( instrument_strategy ) order_list_as_fills = [fill_from_order(order) for order in order_list] - list_of_fills = listOfFills(order_list_as_fills) + list_of_fills = ListOfFills(order_list_as_fills) return list_of_fills @@ -115,7 +115,7 @@ class contractHistoricOrdersData(genericOrdersData): class brokerHistoricOrdersData(contractHistoricOrdersData): def get_fills_history_for_contract( self, futures_contract: futuresContract - ) -> listOfFills: + ) -> ListOfFills: """ :param instrument_code: str @@ -133,7 +133,7 @@ def get_fills_history_for_contract( for orderid in list_of_order_ids ] list_of_fills = [fill for fill in list_of_fills if fill is not missing_order] - list_of_fills = listOfFills(list_of_fills) + list_of_fills = ListOfFills(list_of_fills) return list_of_fills diff --git a/sysexecution/orders/named_order_objects.py b/sysexecution/orders/named_order_objects.py index 942ac0e96f..e3edc1980a 100644 --- a/sysexecution/orders/named_order_objects.py +++ b/sysexecution/orders/named_order_objects.py @@ -1,5 +1,6 @@ from syscore.constants import named_object + missing_order = named_object("missing order") locked_order = named_object("locked order") duplicate_order = named_object("duplicate order") diff --git a/sysobjects/fills.py b/sysobjects/fills.py index 1b99b6634b..c40401d63f 100644 --- a/sysobjects/fills.py +++ b/sysobjects/fills.py @@ -1,23 +1,47 @@ +from typing import Union import datetime -from collections import namedtuple +from dataclasses import dataclass import pandas as pd +import numpy as np -from syscore.constants import named_object -from sysexecution.orders.named_order_objects import missing_order -from sysobjects.orders import SimpleOrder, ListOfSimpleOrders +from sysexecution.orders.named_order_objects import missing_order, named_object -from sysexecution.orders.list_of_orders import listOfOrders from sysexecution.orders.base_orders import Order -Fill = namedtuple("Fill", ["date", "qty", "price"]) -NOT_FILLED = named_object("not filled") +@dataclass +class Fill: + date: datetime.datetime + qty: int + price: float + price_requires_slippage_adjustment: bool = False + + @classmethod + def zero_fill(cls, date): + return cls(date=date, qty=0, price=np.nan) + + @property + def is_unfilled(self) -> bool: + return self.qty == 0 + + +def is_empty_fill(fill: Union[named_object, Fill]) -> bool: + if fill is missing_order: + return True + if fill.is_unfilled: + return True + + return False -class listOfFills(list): +def empty_fill(date: datetime.datetime) -> Fill: + return Fill.zero_fill(date) + + +class ListOfFills(list): def __init__(self, list_of_fills): - list_of_fills = [fill for fill in list_of_fills if fill is not missing_order] + list_of_fills = [fill for fill in list_of_fills if not is_empty_fill(fill)] super().__init__(list_of_fills) def _as_dict_of_lists(self) -> dict: @@ -47,7 +71,7 @@ def from_position_series_and_prices(cls, positions: pd.Series, price: pd.Series) def _list_of_fills_from_position_series_and_prices( positions: pd.Series, price: pd.Series -) -> listOfFills: +) -> ListOfFills: ( trades_without_zeros, @@ -59,11 +83,11 @@ def _list_of_fills_from_position_series_and_prices( dates_as_list = list(prices_aligned_to_trades.index) list_of_fills_as_list = [ - Fill(date, qty, price) + Fill(date, qty, price, price_requires_slippage_adjustment=True) for date, qty, price in zip(dates_as_list, trades_as_list, prices_as_list) ] - list_of_fills = listOfFills(list_of_fills_as_list) + list_of_fills = ListOfFills(list_of_fills_as_list) return list_of_fills @@ -101,59 +125,3 @@ def fill_from_order(order: Order) -> Fill: return missing_order return Fill(fill_datetime, fill_qty, fill_price) - - -def fill_from_simple_order( - simple_order: SimpleOrder, - market_price: float, - fill_datetime: datetime.datetime, - slippage: float = 0, -) -> Fill: - if simple_order.is_zero_order: - return NOT_FILLED - - elif simple_order.is_market_order: - fill = fill_from_simple_market_order( - simple_order, - market_price=market_price, - slippage=slippage, - fill_datetime=fill_datetime, - ) - else: - ## limit order - fill = fill_from_simple_limit_order( - simple_order, market_price=market_price, fill_datetime=fill_datetime - ) - - return fill - - -def fill_from_simple_limit_order( - simple_order: SimpleOrder, market_price: float, fill_datetime: datetime.datetime -) -> Fill: - - limit_price = simple_order.limit_price - if simple_order.quantity > 0: - if limit_price > market_price: - return Fill(fill_datetime, simple_order.quantity, limit_price) - - if simple_order.quantity < 0: - if limit_price < market_price: - return Fill(fill_datetime, simple_order.quantity, limit_price) - - return NOT_FILLED - - -def fill_from_simple_market_order( - simple_order: SimpleOrder, - market_price: float, - fill_datetime: datetime.datetime, - slippage: float = 0, -) -> Fill: - - if simple_order.quantity > 0: - fill_price_with_slippage = market_price + slippage - else: - fill_price_with_slippage = market_price - slippage - - return Fill(fill_datetime, simple_order.quantity, fill_price_with_slippage) diff --git a/sysobjects/instruments.py b/sysobjects/instruments.py index 89735334a4..3d2c226140 100644 --- a/sysobjects/instruments.py +++ b/sysobjects/instruments.py @@ -328,13 +328,20 @@ def calculate_cost_percentage_terms( return cost_in_percentage_terms def calculate_cost_instrument_currency( - self, blocks_traded: float, block_price_multiplier: float, price: float + self, + blocks_traded: float, + block_price_multiplier: float, + price: float, + include_slippage: bool = True, ) -> float: value_per_block = price * block_price_multiplier - slippage = self.calculate_slippage_instrument_currency( - blocks_traded, block_price_multiplier=block_price_multiplier - ) + if include_slippage: + slippage = self.calculate_slippage_instrument_currency( + blocks_traded, block_price_multiplier=block_price_multiplier + ) + else: + slippage = 0 commission = self.calculate_total_commission( blocks_traded, value_per_block=value_per_block diff --git a/sysobjects/orders.py b/sysobjects/orders.py deleted file mode 100644 index 5b23407ab8..0000000000 --- a/sysobjects/orders.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass -from syscore.constants import named_object, arg_not_supplied - - -@dataclass() -class SimpleOrder: - ### Simple order, suitable for use in simulation, but not complex enough for production - - ## Could share code, but too complicated - quantity: int - limit_price: float = None - - @property - def is_market_order(self): - if self.limit_price is None: - return True - - @property - def is_zero_order(self) -> bool: - return self.quantity == 0 - - @classmethod - def zero_order(cls): - return cls(quantity=0) - - -zero_order = SimpleOrder.zero_order() - - -class ListOfSimpleOrders(list): - def remove_zero_orders(self): - new_list = [order for order in self if not order.is_zero_order] - return ListOfSimpleOrders(new_list) diff --git a/sysproduction/data/orders.py b/sysproduction/data/orders.py index 9ea2c63d0b..bc7caaea10 100644 --- a/sysproduction/data/orders.py +++ b/sysproduction/data/orders.py @@ -19,7 +19,7 @@ ) from sysdata.data_blob import dataBlob -from sysobjects.fills import listOfFills +from sysobjects.fills import ListOfFills from sysexecution.order_stacks.broker_order_stack import brokerOrderStackData from sysexecution.order_stacks.contract_order_stack import contractOrderStackData from sysexecution.order_stacks.instrument_order_stack import instrumentOrderStackData @@ -160,7 +160,7 @@ def get_historic_broker_order_from_order_id(self, order_id: int) -> brokerOrder: def get_fills_history_for_contract( self, futures_contract: futuresContract - ) -> listOfFills: + ) -> ListOfFills: ## We get this from broker fills, as they have leg by leg information list_of_fills = ( self.db_broker_historic_orders_data.get_fills_history_for_contract( @@ -172,7 +172,7 @@ def get_fills_history_for_contract( def get_fills_history_for_instrument_strategy( self, instrument_strategy: instrumentStrategy - ) -> listOfFills: + ) -> ListOfFills: list_of_fills = self.db_strategy_historic_orders_data.get_fills_history_for_instrument_strategy( instrument_strategy ) diff --git a/sysproduction/strategy_code/report_system_classic.py b/sysproduction/strategy_code/report_system_classic.py index 2a225bea09..ed6678ce81 100644 --- a/sysproduction/strategy_code/report_system_classic.py +++ b/sysproduction/strategy_code/report_system_classic.py @@ -304,7 +304,13 @@ def get_forecast_matrix_over_code( ) get_vol_scalar = configForMethod( - "positionSize", "get_volatility_scalar", "Vol Scalar", False, True, None, False + "positionSize", + "get_average_position_at_subsystem_level", + "Vol Scalar", + False, + True, + None, + False, ) get_subsystem_position = configForMethod( diff --git a/systems/accounts/account_buffering_subsystem.py b/systems/accounts/account_buffering_subsystem.py index a3e529022a..1556d6b57b 100644 --- a/systems/accounts/account_buffering_subsystem.py +++ b/systems/accounts/account_buffering_subsystem.py @@ -12,7 +12,9 @@ class accountBufferingSubSystemLevel(accountCosts): def subsystem_turnover(self, instrument_code: str) -> float: positions = self.get_subsystem_position(instrument_code) - average_position_for_turnover = self.get_volatility_scalar(instrument_code) + average_position_for_turnover = self.get_average_position_at_subsystem_level( + instrument_code + ) subsystem_turnover = turnover(positions, average_position_for_turnover) diff --git a/systems/accounts/account_costs.py b/systems/accounts/account_costs.py index eb72428dbd..bfb676c31e 100644 --- a/systems/accounts/account_costs.py +++ b/systems/accounts/account_costs.py @@ -344,7 +344,9 @@ def get_SR_cost_per_trade_for_instrument_percentage( @diagnostic() def _recent_average_price(self, instrument_code: str) -> float: - daily_price = self.get_instrument_prices_for_position_or_forecast(instrument_code) + daily_price = self.get_instrument_prices_for_position_or_forecast( + instrument_code + ) start_date = self._date_one_year_before_end_of_price_index(instrument_code) average_price = float(daily_price[start_date:].mean()) @@ -352,7 +354,9 @@ def _recent_average_price(self, instrument_code: str) -> float: @diagnostic() def _date_one_year_before_end_of_price_index(self, instrument_code: str): - daily_price = self.get_instrument_prices_for_position_or_forecast(instrument_code) + daily_price = self.get_instrument_prices_for_position_or_forecast( + instrument_code + ) last_date = daily_price.index[-1] start_date = last_date - pd.DateOffset(years=1) @@ -378,5 +382,5 @@ def _recent_average_daily_vol(self, instrument_code: str) -> float: return average_vol @property - def use_SR_costs(self) -> float: + def use_SR_costs(self) -> bool: return str2Bool(self.config.use_SR_costs) diff --git a/systems/accounts/account_forecast.py b/systems/accounts/account_forecast.py index 427a841858..66b37c5654 100644 --- a/systems/accounts/account_forecast.py +++ b/systems/accounts/account_forecast.py @@ -186,8 +186,9 @@ def pandl_for_instrument_forecast( forecast = self.get_capped_forecast(instrument_code, rule_variation_name) - price = self.get_instrument_prices_for_position_or_forecast(instrument_code=instrument_code, - position_or_forecast=forecast) + price = self.get_instrument_prices_for_position_or_forecast( + instrument_code=instrument_code, position_or_forecast=forecast + ) daily_returns_volatility = self.get_daily_returns_volatility(instrument_code) diff --git a/systems/accounts/account_inputs.py b/systems/accounts/account_inputs.py index 3f39397851..24ef92d7fa 100644 --- a/systems/accounts/account_inputs.py +++ b/systems/accounts/account_inputs.py @@ -98,6 +98,9 @@ def target_abs_forecast(self) -> float: def average_forecast(self) -> float: return self.config.average_absolute_forecast + def forecast_cap(self) -> float: + return self.config.forecast_cap + def get_raw_cost_data(self, instrument_code: str) -> instrumentCosts: return self.parent.data.get_raw_cost_data(instrument_code) @@ -151,7 +154,9 @@ def get_annual_risk_target(self) -> float: def get_average_position_for_instrument_at_portfolio_level( self, instrument_code: str ) -> pd.Series: - average_position_for_subsystem = self.get_volatility_scalar(instrument_code) + average_position_for_subsystem = self.get_average_position_at_subsystem_level( + instrument_code + ) scaling_factor = self.get_instrument_scaling_factor(instrument_code) scaling_factor_aligned = scaling_factor.reindex( average_position_for_subsystem.index, method="ffill" @@ -160,7 +165,9 @@ def get_average_position_for_instrument_at_portfolio_level( return average_position - def get_volatility_scalar(self, instrument_code: str) -> pd.Series: + def get_average_position_at_subsystem_level( + self, instrument_code: str + ) -> pd.Series: """ Get the volatility scalar (position with forecast of +10 using all capital) @@ -173,7 +180,9 @@ def get_volatility_scalar(self, instrument_code: str) -> pd.Series: """ - return self.parent.positionSize.get_volatility_scalar(instrument_code) + return self.parent.positionSize.get_average_position_at_subsystem_level( + instrument_code + ) def get_notional_position(self, instrument_code: str) -> pd.Series: """ diff --git a/systems/accounts/account_instruments.py b/systems/accounts/account_instruments.py index bdea58756a..4f0d1080ad 100644 --- a/systems/accounts/account_instruments.py +++ b/systems/accounts/account_instruments.py @@ -121,7 +121,9 @@ def _pandl_for_instrument_with_SR_costs( roundpositions: bool = True, ) -> accountCurve: - price = self.get_instrument_prices_for_position_or_forecast(instrument_code, position_or_forecast=positions) + price = self.get_instrument_prices_for_position_or_forecast( + instrument_code, position_or_forecast=positions + ) fx = self.get_fx_rate(instrument_code) value_of_price_point = self.get_value_of_block_price_move(instrument_code) daily_returns_volatility = self.get_daily_returns_volatility(instrument_code) @@ -162,7 +164,9 @@ def turnover_at_portfolio_level( ) -> float: ## assumes we use all capital - average_position_for_turnover = self.get_volatility_scalar(instrument_code) + average_position_for_turnover = self.get_average_position_at_subsystem_level( + instrument_code + ) ## Using actual capital positions = self.get_buffered_position( @@ -188,7 +192,9 @@ def _pandl_for_instrument_with_cash_costs( raw_costs = self.get_raw_cost_data(instrument_code) - price = self.get_instrument_prices_for_position_or_forecast(instrument_code, position_or_forecast=positions) + price = self.get_instrument_prices_for_position_or_forecast( + instrument_code, position_or_forecast=positions + ) fx = self.get_fx_rate(instrument_code) value_of_price_point = self.get_value_of_block_price_move(instrument_code) diff --git a/systems/accounts/account_subsystem.py b/systems/accounts/account_subsystem.py index 0bad50ca3f..0ec957d2ad 100644 --- a/systems/accounts/account_subsystem.py +++ b/systems/accounts/account_subsystem.py @@ -105,8 +105,25 @@ def _pandl_for_subsystem_with_SR_costs( self, instrument_code, delayfill=True, roundpositions=False ) -> accountCurve: + pandl_calculator = self._pandl_calculator_for_subsystem_with_SR_costs( + instrument_code=instrument_code, + delayfill=delayfill, + roundpositions=roundpositions, + ) + + account_curve = accountCurve(pandl_calculator) + + return account_curve + + @diagnostic(not_pickable=True) + def _pandl_calculator_for_subsystem_with_SR_costs( + self, instrument_code, delayfill=True, roundpositions=False + ) -> pandlCalculationWithSRCosts: + positions = self.get_buffered_subsystem_position(instrument_code) - price = self.get_instrument_prices_for_position_or_forecast(instrument_code, position_or_forecast=positions) + price = self.get_instrument_prices_for_position_or_forecast( + instrument_code, position_or_forecast=positions + ) fx = self.get_fx_rate(instrument_code) @@ -114,7 +131,7 @@ def _pandl_for_subsystem_with_SR_costs( daily_returns_volatility = self.get_daily_returns_volatility(instrument_code) ## following doesn't include IDM or instrument weight - average_position = self.get_volatility_scalar(instrument_code) + average_position = self.get_average_position_at_subsystem_level(instrument_code) subsystem_turnover = self.subsystem_turnover(instrument_code) annualised_SR_cost = self.get_SR_cost_given_turnover( @@ -136,19 +153,33 @@ def _pandl_for_subsystem_with_SR_costs( roundpositions=roundpositions, ) + return pandl_calculator + + @diagnostic(not_pickable=True) + def _pandl_for_subsystem_with_cash_costs( + self, instrument_code, delayfill=True, roundpositions=True + ) -> accountCurve: + + pandl_calculator = self._pandl_calculator_for_subsystem_with_cash_costs( + instrument_code=instrument_code, + delayfill=delayfill, + roundpositions=roundpositions, + ) + account_curve = accountCurve(pandl_calculator) return account_curve @diagnostic(not_pickable=True) - def _pandl_for_subsystem_with_cash_costs( + def _pandl_calculator_for_subsystem_with_cash_costs( self, instrument_code, delayfill=True, roundpositions=True - ) -> accountCurve: + ) -> pandlCalculationWithCashCostsAndFills: raw_costs = self.get_raw_cost_data(instrument_code) positions = self.get_buffered_subsystem_position(instrument_code) - price = self.get_instrument_prices_for_position_or_forecast(instrument_code, - position_or_forecast=positions) ### here! + price = self.get_instrument_prices_for_position_or_forecast( + instrument_code, position_or_forecast=positions + ) ### here! fx = self.get_fx_rate(instrument_code) @@ -172,6 +203,4 @@ def _pandl_for_subsystem_with_cash_costs( rolls_per_year=rolls_per_year, ) - account_curve = accountCurve(pandl_calculator) - - return account_curve + return pandl_calculator diff --git a/systems/accounts/order_simulator/__init__.py b/systems/accounts/order_simulator/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/systems/accounts/order_simulator/account_curve_order_simulator.py b/systems/accounts/order_simulator/account_curve_order_simulator.py new file mode 100644 index 0000000000..c504c309f1 --- /dev/null +++ b/systems/accounts/order_simulator/account_curve_order_simulator.py @@ -0,0 +1,190 @@ +import pandas as pd + +from systems.system_cache import diagnostic + +from systems.accounts.pandl_calculators.pandl_cash_costs import ( + pandlCalculationWithCashCostsAndFills, +) + +from systems.accounts.curves.account_curve import accountCurve +from systems.accounts.accounts_stage import Account +from systems.accounts.order_simulator.pandl_order_simulator import OrderSimulator + + +class AccountWithOrderSimulator(Account): + @diagnostic(not_pickable=True) + def pandl_for_subsystem( + self, instrument_code, delayfill=True, roundpositions=True + ) -> accountCurve: + + self.log.msg( + "Calculating pandl for subsystem for instrument %s" % instrument_code, + instrument_code=instrument_code, + ) + + use_SR_costs = self.use_SR_costs + _raise_exceptions( + roundpositions=roundpositions, + delayfill=delayfill, + use_SR_costs=use_SR_costs, + ) + pandl_calculator = self._pandl_calculator_for_subsystem_with_cash_costs( + instrument_code=instrument_code, + delayfill=delayfill, + roundpositions=roundpositions, + ) + account_curve = accountCurve(pandl_calculator) + + return account_curve + + @diagnostic(not_pickable=True) + def _pandl_calculator_for_subsystem_with_cash_costs( + self, instrument_code, delayfill=True, roundpositions=True + ) -> pandlCalculationWithCashCostsAndFills: + + ## Should be checked earlier, but just in case called directly + ## Order simulator doesn't work otherwise + assert delayfill + assert roundpositions + + order_simulator = self.get_order_simulator(instrument_code, is_subsystem=True) + price = order_simulator.prices() + fills = order_simulator.list_of_fills() + + raw_costs = self.get_raw_cost_data(instrument_code) + fx = self.get_fx_rate(instrument_code) + value_of_price_point = self.get_value_of_block_price_move(instrument_code) + capital = self.get_notional_capital() + vol_normalise_currency_costs = self.config.vol_normalise_currency_costs + rolls_per_year = self.get_rolls_per_year(instrument_code) + + pandl_calculator = pandlCalculationWithCashCostsAndFills( + price, + raw_costs=raw_costs, + fills=fills, + capital=capital, + value_per_point=value_of_price_point, + delayfill=delayfill, + fx=fx, + roundpositions=roundpositions, + vol_normalise_currency_costs=vol_normalise_currency_costs, + rolls_per_year=rolls_per_year, + ) + + return pandl_calculator + + @diagnostic(not_pickable=True) + def pandl_for_instrument( + self, instrument_code: str, delayfill: bool = True, roundpositions: bool = True + ) -> accountCurve: + self.log.msg( + "Calculating pandl for instrument for %s" % instrument_code, + instrument_code=instrument_code, + ) + use_SR_costs = self.use_SR_costs + _raise_exceptions( + roundpositions=roundpositions, + delayfill=delayfill, + use_SR_costs=use_SR_costs, + ) + + pandl_calculator = self._pandl_calculator_for_instrument_with_cash_costs( + instrument_code=instrument_code, + roundpositions=roundpositions, + delayfill=delayfill, + ) + account_curve = accountCurve(pandl_calculator, weighted=True) + + return account_curve + + @diagnostic(not_pickable=True) + def _pandl_calculator_for_instrument_with_cash_costs( + self, instrument_code, delayfill=True, roundpositions=True + ) -> pandlCalculationWithCashCostsAndFills: + + ## Should be checked earlier, but just in case called directly + ## Order simulator doesn't work otherwise + assert delayfill + assert roundpositions + + order_simulator = self.get_order_simulator(instrument_code, is_subsystem=False) + fills = order_simulator.list_of_fills() + price = order_simulator.prices() + + raw_costs = self.get_raw_cost_data(instrument_code) + + fx = self.get_fx_rate(instrument_code) + value_of_price_point = self.get_value_of_block_price_move(instrument_code) + + capital = self.get_notional_capital() + + vol_normalise_currency_costs = self.config.vol_normalise_currency_costs + rolls_per_year = self.get_rolls_per_year(instrument_code) + multiply_roll_costs_by = self.config.multiply_roll_costs_by + + pandl_calculator = pandlCalculationWithCashCostsAndFills( + price, + raw_costs=raw_costs, + fills=fills, + capital=capital, + value_per_point=value_of_price_point, + delayfill=delayfill, + fx=fx, + roundpositions=roundpositions, + vol_normalise_currency_costs=vol_normalise_currency_costs, + rolls_per_year=rolls_per_year, + multiply_roll_costs_by=multiply_roll_costs_by, + ) + + return pandl_calculator + + @diagnostic() + def get_buffered_position( + self, instrument_code: str, roundpositions: bool = True + ) -> pd.Series: + _raise_exceptions(roundpositions=roundpositions) + order_simulator = self.get_order_simulator(instrument_code, is_subsystem=False) + + return order_simulator.positions() + + @diagnostic() + def get_buffered_subsystem_position( + self, instrument_code: str, roundpositions: bool = True + ) -> pd.Series: + _raise_exceptions(roundpositions=roundpositions) + order_simulator = self.get_order_simulator(instrument_code, is_subsystem=True) + + return order_simulator.positions() + + def get_unrounded_subsystem_position_for_order_simulator( + self, instrument_code: str + ) -> pd.Series: + return self.get_subsystem_position(instrument_code) + + def get_unrounded_instrument_position_for_order_simulator( + self, instrument_code: str + ) -> pd.Series: + return self.get_notional_position(instrument_code) + + @diagnostic(not_pickable=True) + def get_order_simulator( + self, instrument_code, is_subsystem: bool + ) -> OrderSimulator: + return OrderSimulator( + system_accounts_stage=self, + instrument_code=instrument_code, + is_subsystem=is_subsystem, + ) + + +def _raise_exceptions( + roundpositions: bool = True, use_SR_costs: bool = False, delayfill: bool = True +): + if not roundpositions: + raise Exception("Have to round positions when using order simulator!") + if not delayfill: + raise Exception("Have to delay fills when using order simulator!") + if use_SR_costs: + raise Exception( + "Have to use cash costs not SR costs when using order simulator!" + ) diff --git a/systems/accounts/order_simulator/fills_and_orders.py b/systems/accounts/order_simulator/fills_and_orders.py new file mode 100644 index 0000000000..c84c11c974 --- /dev/null +++ b/systems/accounts/order_simulator/fills_and_orders.py @@ -0,0 +1,116 @@ +from collections import namedtuple +import datetime +from typing import Union + + +from sysobjects.fills import Fill, ListOfFills, empty_fill +from systems.accounts.order_simulator.simple_orders import ( + ListOfSimpleOrders, + SimpleOrder, + SimpleOrderWithDate, + empty_list_of_orders_with_date, +) + + +def fill_list_of_simple_orders( + list_of_orders: ListOfSimpleOrders, + fill_datetime: datetime.datetime, + market_price: float, +) -> Fill: + list_of_fills = [ + fill_from_simple_order( + simple_order=simple_order, + fill_datetime=fill_datetime, + market_price=market_price, + ) + for simple_order in list_of_orders + ] + list_of_fills = ListOfFills(list_of_fills) ## will remove unfilled + + if len(list_of_fills) == 0: + return empty_fill(fill_datetime) + elif len(list_of_fills) == 1: + return list_of_fills[0] + else: + raise Exception( + "List of orders %s has produced more than one fill %s!" + % (str(list_of_orders), str(list_of_orders)) + ) + + +def fill_from_simple_order( + simple_order: SimpleOrder, + market_price: float, + fill_datetime: datetime.datetime, +) -> Fill: + if simple_order.is_zero_order: + return empty_fill(fill_datetime) + + elif simple_order.is_market_order: + fill = fill_from_simple_market_order( + simple_order, + market_price=market_price, + fill_datetime=fill_datetime, + ) + else: + ## limit order + fill = fill_from_simple_limit_order( + simple_order, market_price=market_price, fill_datetime=fill_datetime + ) + + return fill + + +def fill_from_simple_limit_order( + simple_order: Union[SimpleOrder, SimpleOrderWithDate], + market_price: float, + fill_datetime: datetime.datetime, +) -> Fill: + + limit_price = simple_order.limit_price + if simple_order.quantity > 0: + if limit_price > market_price: + return Fill( + fill_datetime, + simple_order.quantity, + limit_price, + price_requires_slippage_adjustment=False, + ) + + if simple_order.quantity < 0: + if limit_price < market_price: + return Fill( + fill_datetime, + simple_order.quantity, + limit_price, + price_requires_slippage_adjustment=True, + ) + + return empty_fill(fill_datetime) + + +def fill_from_simple_market_order( + simple_order: Union[SimpleOrder, SimpleOrderWithDate], + market_price: float, + fill_datetime: datetime.datetime, +) -> Fill: + + return Fill( + fill_datetime, + simple_order.quantity, + market_price, + price_requires_slippage_adjustment=True, + ) + + +ListOfSimpleOrdersAndResultingFill = namedtuple( + "ListOfSimpleOrdersAndResultingFill", ["list_of_orders", "fill"] +) + + +def empty_list_of_orders_with_no_fills( + fill_datetime: datetime.datetime, +) -> ListOfSimpleOrdersAndResultingFill: + return ListOfSimpleOrdersAndResultingFill( + empty_list_of_orders_with_date(), empty_fill(fill_datetime) + ) diff --git a/systems/accounts/order_simulator/hourly_limit_orders.py b/systems/accounts/order_simulator/hourly_limit_orders.py new file mode 100644 index 0000000000..275d0fafe5 --- /dev/null +++ b/systems/accounts/order_simulator/hourly_limit_orders.py @@ -0,0 +1,79 @@ +import datetime +from typing import Tuple, Callable + +import numpy as np + + +from sysobjects.fills import Fill, empty_fill +from systems.accounts.order_simulator.fills_and_orders import ( + fill_list_of_simple_orders, + empty_list_of_orders_with_no_fills, + ListOfSimpleOrdersAndResultingFill, +) +from systems.accounts.order_simulator.simple_orders import ( + ListOfSimpleOrdersWithDate, + SimpleOrderWithDate, +) + +from systems.accounts.order_simulator.account_curve_order_simulator import ( + AccountWithOrderSimulator, +) +from systems.accounts.order_simulator.pandl_order_simulator import ( + DataAtIDXPoint, +) +from systems.accounts.order_simulator.hourly_market_orders import ( + HourlyOrderSimulatorOfMarketOrders, +) +from systems.system_cache import diagnostic + + +class HourlyOrderSimulatorOfLimitOrders(HourlyOrderSimulatorOfMarketOrders): + @property + def orders_fills_function(self) -> Callable: + return generate_order_and_fill_at_idx_point_for_limit_orders + + +def generate_order_and_fill_at_idx_point_for_limit_orders( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: DataAtIDXPoint, +) -> Tuple[ListOfSimpleOrdersWithDate, Fill]: + + current_optimal_position = data_for_idx.current_optimal_position + if np.isnan(current_optimal_position): + quantity = 0 + else: + quantity = round(current_optimal_position) - current_position + + if quantity == 0: + notional_datetime_for_empty_fill = data_for_idx.next_datetime + return empty_list_of_orders_with_no_fills( + fill_datetime=notional_datetime_for_empty_fill + ) + + simple_order = SimpleOrderWithDate( + quantity=quantity, + submit_date=current_datetime, + limit_price=data_for_idx.current_price, + ) + list_of_orders = ListOfSimpleOrdersWithDate([simple_order]) + fill = fill_list_of_simple_orders( + list_of_orders=list_of_orders, + market_price=data_for_idx.next_price, + fill_datetime=data_for_idx.next_datetime, + ) + + return ListOfSimpleOrdersAndResultingFill(list_of_orders=list_of_orders, fill=fill) + + +class AccountWithOrderSimulatorForLimitOrders(AccountWithOrderSimulator): + @diagnostic(not_pickable=True) + def get_order_simulator( + self, instrument_code, is_subsystem: bool + ) -> HourlyOrderSimulatorOfLimitOrders: + order_simulator = HourlyOrderSimulatorOfLimitOrders( + system_accounts_stage=self, + instrument_code=instrument_code, + is_subsystem=is_subsystem, + ) + return order_simulator diff --git a/systems/accounts/order_simulator/hourly_market_orders.py b/systems/accounts/order_simulator/hourly_market_orders.py new file mode 100644 index 0000000000..b91d378ce6 --- /dev/null +++ b/systems/accounts/order_simulator/hourly_market_orders.py @@ -0,0 +1,67 @@ +import pandas as pd + +from systems.accounts.order_simulator.account_curve_order_simulator import ( + AccountWithOrderSimulator, +) +from systems.accounts.order_simulator.pandl_order_simulator import ( + OrderSimulator, + OrdersSeriesData, +) +from systems.system_cache import diagnostic + + +class HourlyOrderSimulatorOfMarketOrders(OrderSimulator): + def _series_data(self) -> OrdersSeriesData: + series_data = _build_hourly_series_data_for_order_simulator( + system_accounts_stage=self.system_accounts_stage, # ignore type hint + instrument_code=self.instrument_code, + is_subsystem=self.is_subsystem, + ) + return series_data + + +def _build_hourly_series_data_for_order_simulator( + system_accounts_stage, ## no explicit type would cause circular import + instrument_code: str, + is_subsystem: bool = False, +) -> OrdersSeriesData: + + price_series = system_accounts_stage.get_hourly_prices(instrument_code) + if is_subsystem: + unrounded_positions = ( + system_accounts_stage.get_unrounded_subsystem_position_for_order_simulator( + instrument_code + ) + ) + else: + unrounded_positions = ( + system_accounts_stage.get_unrounded_instrument_position_for_order_simulator( + instrument_code + ) + ) + + price_series = price_series.sort_index() + unrounded_positions = unrounded_positions.sort_index() + + both_index = pd.concat([price_series, unrounded_positions], axis=1).index + + price_series = price_series.reindex(both_index).ffill() + unrounded_positions = unrounded_positions.reindex(both_index).ffill() + + series_data = OrdersSeriesData( + price_series=price_series, unrounded_positions=unrounded_positions + ) + return series_data + + +class AccountWithOrderSimulatorForHourlyMarketOrders(AccountWithOrderSimulator): + @diagnostic(not_pickable=True) + def get_order_simulator( + self, instrument_code, is_subsystem: bool + ) -> HourlyOrderSimulatorOfMarketOrders: + order_simulator = HourlyOrderSimulatorOfMarketOrders( + system_accounts_stage=self, + instrument_code=instrument_code, + is_subsystem=is_subsystem, + ) + return order_simulator diff --git a/systems/accounts/order_simulator/pandl_order_simulator.py b/systems/accounts/order_simulator/pandl_order_simulator.py new file mode 100644 index 0000000000..ac08abd50e --- /dev/null +++ b/systems/accounts/order_simulator/pandl_order_simulator.py @@ -0,0 +1,258 @@ +from typing import Callable +from collections import namedtuple +from dataclasses import dataclass + +import datetime +import pandas as pd +import numpy as np + + +from syscore.cache import Cache +from systems.accounts.order_simulator.simple_orders import ( + ListOfSimpleOrdersWithDate, + SimpleOrderWithDate, +) +from sysobjects.fills import ListOfFills, Fill +from systems.accounts.order_simulator.fills_and_orders import ( + ListOfSimpleOrdersAndResultingFill, + empty_list_of_orders_with_no_fills, +) + + +@dataclass +class PositionsOrdersFills: + positions: pd.Series + list_of_orders: ListOfSimpleOrdersWithDate + list_of_fills: ListOfFills + + +class OrdersSeriesData(object): + def __init__(self, price_series: pd.Series, unrounded_positions: pd.Series): + self.price_series = price_series + self.unrounded_positions = unrounded_positions + + +@dataclass +class OrderSimulator: + system_accounts_stage: object ## no explicit type as would cause circular import + instrument_code: str + is_subsystem: bool = False + + def diagnostic_df(self) -> pd.DataFrame: + return self.cache.get(self._diagnostic_df) + + def _diagnostic_df(self) -> pd.DataFrame: + position_series = self.positions() + position_df = pd.DataFrame(position_series) + + optimal_positions_series = self.optimal_positions_series() + optimal_position_df = pd.DataFrame(optimal_positions_series) + + list_of_fills = self.list_of_fills() + fills_df = list_of_fills.as_pd_df() + list_of_orders = self.list_of_orders() + orders_df = list_of_orders.as_pd_df() + df = pd.concat([optimal_position_df, orders_df, fills_df, position_df], axis=1) + df.columns = [ + "optimal_position", + "order_qty", + "limit_price", + "fill_qty", + "fill_price", + "position", + ] + + return df + + def prices(self) -> pd.Series: + return self.series_data.price_series + + def optimal_positions_series(self) -> pd.Series: + return self.series_data.unrounded_positions + + def positions(self) -> pd.Series: + positions_orders_fills = self.positions_orders_and_fills_from_series_data() + return positions_orders_fills.positions + + def list_of_fills(self) -> ListOfFills: + positions_orders_fills = self.positions_orders_and_fills_from_series_data() + return positions_orders_fills.list_of_fills + + def list_of_orders(self) -> ListOfSimpleOrdersWithDate: + positions_orders_fills = self.positions_orders_and_fills_from_series_data() + return positions_orders_fills.list_of_orders + + def positions_orders_and_fills_from_series_data(self) -> PositionsOrdersFills: + ## Because p&l with orders is path dependent, we generate everything together + return self.cache.get(self._positions_orders_and_fills_from_series_data) + + def _positions_orders_and_fills_from_series_data(self) -> PositionsOrdersFills: + series_data = self.series_data + return generate_positions_orders_and_fills_from_series_data( + series_data=series_data, + passed_orders_fills_function=self.orders_fills_function, + passed_idx_data_function=self.idx_data_function, + ) + + @property + def series_data(self) -> OrdersSeriesData: + return self.cache.get(self._series_data) + + def _series_data(self) -> OrdersSeriesData: + series_data = build_daily_series_data_for_order_simulator( + system_accounts_stage=self.system_accounts_stage, # ignore type hint + instrument_code=self.instrument_code, + is_subsystem=self.is_subsystem, + ) + return series_data + + @property + def cache(self) -> Cache: + return getattr(self, "_cache", Cache(self)) + + @property + def orders_fills_function(self) -> Callable: + return generate_order_and_fill_at_idx_point_for_market_orders + + @property + def idx_data_function(self) -> Callable: + return get_order_sim_daily_data_at_idx_point + + +def build_daily_series_data_for_order_simulator( + system_accounts_stage, ## no explicit type would cause circular import + instrument_code: str, + is_subsystem: bool = False, +) -> OrdersSeriesData: + + price_series = system_accounts_stage.get_daily_prices(instrument_code) + if is_subsystem: + unrounded_positions = ( + system_accounts_stage.get_unrounded_subsystem_position_for_order_simulator( + instrument_code + ) + ) + else: + unrounded_positions = ( + system_accounts_stage.get_unrounded_instrument_position_for_order_simulator( + instrument_code + ) + ) + + price_series = price_series.sort_index() + unrounded_positions = unrounded_positions.sort_index() + + both_index = pd.concat([price_series, unrounded_positions], axis=1).index + + price_series = price_series.reindex(both_index).ffill() + unrounded_positions = unrounded_positions.reindex(both_index).ffill() + + series_data = OrdersSeriesData( + price_series=price_series, unrounded_positions=unrounded_positions + ) + return series_data + + +def generate_positions_orders_and_fills_from_series_data( + series_data: OrdersSeriesData, + passed_idx_data_function: Callable, + passed_orders_fills_function: Callable, +) -> PositionsOrdersFills: + + master_index = series_data.price_series.index + + list_of_positions = [] + list_of_orders = [] + list_of_fills = [] + + starting_position = 0 ## doesn't do anything but makes intention clear + current_position = starting_position + + for idx, current_datetime in enumerate(master_index[:-1]): + list_of_positions.append(current_position) + data_for_idx = passed_idx_data_function(idx, series_data) + list_of_orders_and_fill = passed_orders_fills_function( + current_position=current_position, + current_datetime=current_datetime, + data_for_idx=data_for_idx, + ) + orders = list_of_orders_and_fill.list_of_orders + fill = list_of_orders_and_fill.fill + if len(orders) > 0: + list_of_orders = list_of_orders + orders + + if fill.is_unfilled: + pass + else: + list_of_fills.append(fill) + current_position = current_position + fill.qty + + ## Because we don't loop at the final point as no fill is possible, we keep our last position + ## This ensures the list of positions has the same index as the unrounded list + list_of_positions.append(current_position) + + positions = pd.Series(list_of_positions, master_index) + list_of_orders = ListOfSimpleOrdersWithDate(list_of_orders) + list_of_fills = ListOfFills(list_of_fills) + + return PositionsOrdersFills( + positions=positions, list_of_orders=list_of_orders, list_of_fills=list_of_fills + ) + + +DataAtIDXPoint = namedtuple( + "DataAtIDXPoint", + ["current_optimal_position", "current_price", "next_price", "next_datetime"], +) + + +def get_order_sim_daily_data_at_idx_point( + idx: int, series_data: OrdersSeriesData +) -> DataAtIDXPoint: + unrounded_positions = series_data.unrounded_positions + prices = series_data.price_series + + current_optimal_position = unrounded_positions[idx] + next_price = prices[idx + 1] + current_price = prices[idx] + next_datetime = unrounded_positions.index[idx + 1] + + return DataAtIDXPoint( + current_optimal_position=current_optimal_position, + next_datetime=next_datetime, + next_price=next_price, + current_price=current_price, + ) + + +def generate_order_and_fill_at_idx_point_for_market_orders( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: DataAtIDXPoint, +) -> ListOfSimpleOrdersAndResultingFill: + + current_optimal_position = data_for_idx.current_optimal_position + next_datetime = data_for_idx.next_datetime + next_price = data_for_idx.next_price + + if np.isnan(current_optimal_position): + quantity = 0 + else: + quantity = round(current_optimal_position) - current_position + + if quantity == 0: + return empty_list_of_orders_with_no_fills(fill_datetime=next_datetime) + + simple_order = SimpleOrderWithDate( + quantity=quantity, + submit_date=current_datetime, + ) + fill = Fill( + date=next_datetime, + price=next_price, + qty=simple_order.quantity, + price_requires_slippage_adjustment=True, + ) + list_of_orders = ListOfSimpleOrdersWithDate([simple_order]) + + return ListOfSimpleOrdersAndResultingFill(list_of_orders=list_of_orders, fill=fill) diff --git a/systems/accounts/order_simulator/simple_orders.py b/systems/accounts/order_simulator/simple_orders.py new file mode 100644 index 0000000000..ca7f6c9614 --- /dev/null +++ b/systems/accounts/order_simulator/simple_orders.py @@ -0,0 +1,95 @@ +from collections import namedtuple +from typing import List +import datetime +from dataclasses import dataclass + +from syscore.pandas.pdutils import make_df_from_list_of_named_tuple + + +@dataclass() +class SimpleOrder: + ### Simple order, suitable for use in simulation, but not complex enough for production + + ## Could share code, but too complicated + quantity: int + limit_price: float = None + + @property + def is_market_order(self): + if self.limit_price is None: + return True + + @property + def is_zero_order(self) -> bool: + return self.quantity == 0 + + @classmethod + def zero_order(cls): + return cls(quantity=0) + + +zero_order = SimpleOrder.zero_order() + + +class ListOfSimpleOrders(list): + def __init__(self, list_of_orders: List[SimpleOrder]): + super().__init__(list_of_orders) + + def remove_zero_orders(self): + new_list = [order for order in self if not order.is_zero_order] + return ListOfSimpleOrders(new_list) + + def contains_no_orders(self): + return len(self.remove_zero_orders()) == 0 + + +_SimpleOrderWithDateAsTuple = namedtuple( + "_SimpleOrderWithDateAsTuple", ["submit_date", "quantity", "limit_price"] +) + + +class SimpleOrderWithDate(SimpleOrder): + def __init__( + self, quantity: int, submit_date: datetime.datetime, limit_price: float = None + ): + super().__init__(quantity=quantity, limit_price=limit_price) + self.submit_date = submit_date + + def __repr__(self): + if self.limit_price is None: + limit_price_str = "MarketOrder" + else: + limit_price_str = str(self.limit_price) + return "SimpleOrderWithDate(quantity=%d, limit_price=%s, date=%s)" % ( + self.quantity, + limit_price_str, + str(self.submit_date), + ) + + @classmethod + def zero_order(cls, submit_date: datetime.datetime): + return cls(quantity=0, submit_date=submit_date) + + def _as_tuple(self): + return _SimpleOrderWithDateAsTuple( + submit_date=self.submit_date, + quantity=self.quantity, + limit_price=self.limit_price, + ) + + +class ListOfSimpleOrdersWithDate(ListOfSimpleOrders): + def __init__(self, list_of_orders: List[SimpleOrderWithDate]): + super().__init__(list_of_orders) + + def as_pd_df(self): + return make_df_from_list_of_named_tuple( + _SimpleOrderWithDateAsTuple, self._as_list_of_named_tuples() + ) + + def _as_list_of_named_tuples(self) -> list: + return [order._as_tuple() for order in self] + + +def empty_list_of_orders_with_date() -> ListOfSimpleOrdersWithDate: + return ListOfSimpleOrdersWithDate([]) diff --git a/systems/accounts/pandl_calculators/pandl_calculation.py b/systems/accounts/pandl_calculators/pandl_calculation.py index a32f17bf1e..29fc0a8661 100644 --- a/systems/accounts/pandl_calculators/pandl_calculation.py +++ b/systems/accounts/pandl_calculators/pandl_calculation.py @@ -108,23 +108,9 @@ def _pandl_in_instrument_ccy_given_points_pandl( return pandl_in_points * point_size def pandl_in_points(self) -> pd.Series: - positions = self.positions - price_returns = self.price_returns - pos_series = positions.groupby(positions.index).last() - price_returns_indexed = price_returns.reindex(pos_series.index, method="ffill") - returns = pos_series.shift(1) * price_returns_indexed - - returns[returns.isna()] = 0.0 - - return returns - - @property - def price_returns(self) -> pd.Series: - prices = self.price - price_returns = prices.ffill().diff() - - return price_returns + pandl_in_points = calculate_pandl(positions=self.positions, prices=self.price) + return pandl_in_points @property def price(self) -> pd.Series: @@ -207,3 +193,18 @@ def apply_weighting(weight: pd.Series, thing_to_weight: pd.Series) -> pd.Series: weighted_thing = thing_to_weight * aligned_weight return weighted_thing + + +def calculate_pandl(positions: pd.Series, prices: pd.Series): + pos_series = positions.groupby(positions.index).last() + both_series = pd.concat([pos_series, prices], axis=1) + both_series.columns = ["positions", "prices"] + both_series = both_series.ffill() + + price_returns = both_series.prices.diff() + + returns = both_series.positions.shift(1) * price_returns + + returns[returns.isna()] = 0.0 + + return returns diff --git a/systems/accounts/pandl_calculators/pandl_cash_costs.py b/systems/accounts/pandl_calculators/pandl_cash_costs.py index 3f23958b41..e67037286d 100644 --- a/systems/accounts/pandl_calculators/pandl_cash_costs.py +++ b/systems/accounts/pandl_calculators/pandl_cash_costs.py @@ -128,6 +128,7 @@ def _pseudo_fills_for_year(self, year: int) -> list: date=date, qty=qty * multiply_roll_costs_by, price=get_row_of_series_before_date(price_series, date), + price_requires_slippage_adjustment=True, ) for date, qty in zip(date_list, average_holding_by_period) if date <= last_date_with_positions and abs(qty) > 0 @@ -189,14 +190,8 @@ def normalise_costs_in_instrument_currency(self, costs_as_pd_series) -> pd.Serie return normalised_costs def calculate_cost_instrument_currency_for_a_fill(self, fill: Fill) -> float: - trade = fill.qty - price = fill.price - - block_price_multiplier = self.value_per_point - cost_for_trade = self.raw_costs.calculate_cost_instrument_currency( - blocks_traded=trade, - price=price, - block_price_multiplier=block_price_multiplier, + cost_for_trade = calculate_cost_from_fill_with_cost_object( + fill=fill, value_per_point=self.value_per_point, raw_costs=self.raw_costs ) return cost_for_trade @@ -231,3 +226,21 @@ def rolls_per_year(self) -> int: @property def multiply_roll_costs_by(self) -> float: return self._multiply_roll_costs_by + + +def calculate_cost_from_fill_with_cost_object( + fill: Fill, value_per_point: float, raw_costs: instrumentCosts +) -> float: + trade = fill.qty + price = fill.price + include_slippage = fill.price_requires_slippage_adjustment + + block_price_multiplier = value_per_point + cost_for_trade = raw_costs.calculate_cost_instrument_currency( + blocks_traded=trade, + price=price, + block_price_multiplier=block_price_multiplier, + include_slippage=include_slippage, + ) + + return cost_for_trade diff --git a/systems/accounts/pandl_calculators/pandl_using_fills.py b/systems/accounts/pandl_calculators/pandl_using_fills.py index 95ecb38b7a..ad20085f40 100644 --- a/systems/accounts/pandl_calculators/pandl_using_fills.py +++ b/systems/accounts/pandl_calculators/pandl_using_fills.py @@ -7,11 +7,11 @@ apply_weighting, ) -from sysobjects.fills import listOfFills, Fill +from sysobjects.fills import ListOfFills, Fill class pandlCalculationWithFills(pandlCalculation): - def __init__(self, *args, fills: listOfFills = arg_not_supplied, **kwargs): + def __init__(self, *args, fills: ListOfFills = arg_not_supplied, **kwargs): # if fills aren't supplied, can be inferred from positions super().__init__(*args, **kwargs) self._fills = fills @@ -38,7 +38,7 @@ def using_positions_and_prices_merged_from_fills( pandlCalculation, price: pd.Series, positions: pd.Series, - fills: listOfFills, + fills: ListOfFills, **kwargs, ): @@ -47,7 +47,7 @@ def using_positions_and_prices_merged_from_fills( return pandlCalculation(price=merged_prices, positions=positions, **kwargs) @property - def fills(self) -> listOfFills: + def fills(self) -> ListOfFills: fills = self._fills if fills is arg_not_supplied: # Infer from positions @@ -59,14 +59,14 @@ def fills(self) -> listOfFills: return fills - def _infer_fills_from_position(self) -> listOfFills: + def _infer_fills_from_position(self) -> ListOfFills: # positions will have delayfill and round applied to them already positions = self.positions if positions is arg_not_supplied: raise Exception("Need to pass fills or positions") - fills = listOfFills.from_position_series_and_prices( + fills = ListOfFills.from_position_series_and_prices( positions=positions, price=self.price ) return fills @@ -92,19 +92,9 @@ def _infer_position_from_fills(self) -> pd.Series: return positions - # This method is never used - def _calculate_and_set_prices_from_fills_and_input_prices(self) -> pd.Series: - - ## this will be set in the parent __init__ - passed_prices = self._price - merged_price = merge_fill_prices_with_prices(passed_prices, self.fills) - self._calculated_price = merged_price - - return merged_price - def merge_fill_prices_with_prices( - prices: pd.Series, list_of_fills: listOfFills + prices: pd.Series, list_of_fills: ListOfFills ) -> pd.Series: list_of_trades_as_pd_df = list_of_fills.as_pd_df() unique_trades_as_pd_df = unique_trades_df(list_of_trades_as_pd_df) @@ -137,7 +127,7 @@ def unique_trades_df(trade_df: pd.DataFrame) -> pd.DataFrame: return new_df -def infer_positions_from_fills(fills: listOfFills) -> pd.Series: +def infer_positions_from_fills(fills: ListOfFills) -> pd.Series: date_index = [fill.date for fill in fills] qty_trade = [fill.qty for fill in fills] trade_series = pd.Series(qty_trade, index=date_index) @@ -153,7 +143,7 @@ def infer_positions_from_fills(fills: listOfFills) -> pd.Series: @classmethod def using_fills(pandlCalculation, price: pd.Series, - fills: listOfFills, + fills: ListOfFills, **kwargs): positions = from_fills_to_positions(fills) diff --git a/systems/diagoutput.py b/systems/diagoutput.py index eb3c75bbd5..5e726dce66 100644 --- a/systems/diagoutput.py +++ b/systems/diagoutput.py @@ -299,7 +299,7 @@ def calculation_details(self, instrument_code): "positionSize.get_instrument_currency_vol", "positionSize.get_fx_rate", "positionSize.get_instrument_value_vol", - "positionSize.get_volatility_scalar", + "positionSize.get_average_position_at_subsystem_level", "positionSize.get_subsystem_position", "portfolio.get_notional_position", ] diff --git a/systems/portfolio.py b/systems/portfolio.py index 27ca08a444..a2d8902bfc 100644 --- a/systems/portfolio.py +++ b/systems/portfolio.py @@ -156,7 +156,7 @@ def get_buffers_for_position(self, instrument_code: str) -> pd.DataFrame: def get_buffers(self, instrument_code: str) -> pd.Series: position = self.get_notional_position(instrument_code) - vol_scalar = self.get_volatility_scalar(instrument_code) + vol_scalar = self.get_average_position_at_subsystem_level(instrument_code) log = self.log config = self.config idm = self.get_instrument_diversification_multiplier() @@ -257,8 +257,8 @@ def get_notional_position_without_idm(self, instrument_code: str) -> pd.Series: instrument_weight_this_code = instr_weights[instrument_code] inst_weight_this_code_reindexed = instrument_weight_this_code.reindex( - subsys_position.index - ).ffill() + subsys_position.index, method="ffill" + ) notional_position_without_idm = ( subsys_position * inst_weight_this_code_reindexed @@ -897,7 +897,9 @@ def turnover_across_subsystems(self) -> turnoverDataAcrossSubsystems: return turnovers @input - def get_volatility_scalar(self, instrument_code: str) -> pd.Series: + def get_average_position_at_subsystem_level( + self, instrument_code: str + ) -> pd.Series: """ Get the vol scalar, from a previous module @@ -915,13 +917,15 @@ def get_volatility_scalar(self, instrument_code: str) -> pd.Series: ()], data, config) >>> >>> ## from config - >>> system.portfolio.get_volatility_scalar("EDOLLAR").tail(2) + >>> system.portfolio.get_average_position_at_subsystem_level("EDOLLAR").tail(2) vol_scalar 2015-12-10 11.187869 2015-12-11 10.332930 """ - return self.position_size_stage.get_volatility_scalar(instrument_code) + return self.position_size_stage.get_average_position_at_subsystem_level( + instrument_code + ) @input def capital_multiplier(self): diff --git a/systems/positionsizing.py b/systems/positionsizing.py index 599c3280a0..b52589ffd8 100644 --- a/systems/positionsizing.py +++ b/systems/positionsizing.py @@ -71,7 +71,7 @@ def get_subsystem_buffers(self, instrument_code: str) -> pd.Series: position = self.get_subsystem_position(instrument_code) - vol_scalar = self.get_volatility_scalar(instrument_code) + vol_scalar = self.get_average_position_at_subsystem_level(instrument_code) log = self.log config = self.config @@ -123,7 +123,7 @@ def get_subsystem_position(self, instrument_code: str) -> pd.Series: """ avg_abs_forecast = self.avg_abs_forecast() - vol_scalar = self.get_volatility_scalar(instrument_code) + vol_scalar = self.get_average_position_at_subsystem_level(instrument_code) forecast = self.get_combined_forecast(instrument_code) vol_scalar = vol_scalar.reindex(forecast.index, method="ffill") @@ -164,7 +164,9 @@ def config(self) -> Config: return self.parent.config @diagnostic() - def get_volatility_scalar(self, instrument_code: str) -> pd.Series: + def get_average_position_at_subsystem_level( + self, instrument_code: str + ) -> pd.Series: """ Get ratio of required volatility vs volatility of instrument in instrument's own currency @@ -178,14 +180,14 @@ def get_volatility_scalar(self, instrument_code: str) -> pd.Series: >>> (comb, fcs, rules, rawdata, data, config)=get_test_object_futures_with_comb_forecasts() >>> system=System([rawdata, rules, fcs, comb, PositionSizing()], data, config) >>> - >>> system.positionSize.get_volatility_scalar("EDOLLAR").tail(2) + >>> system.positionSize.get_average_position_at_subsystem_level("EDOLLAR").tail(2) vol_scalar 2015-12-10 11.187869 2015-12-11 10.332930 >>> >>> ## without raw data >>> system2=System([ rules, fcs, comb, PositionSizing()], data, config) - >>> system2.positionSize.get_volatility_scalar("EDOLLAR").tail(2) + >>> system2.positionSize.get_average_position_at_subsystem_level("EDOLLAR").tail(2) vol_scalar 2015-12-10 11.180444 2015-12-11 10.344278 diff --git a/systems/provided/attenuate_vol/__init__.py b/systems/provided/attenuate_vol/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/systems/provided/rob_system/forecastScaleCap.py b/systems/provided/attenuate_vol/vol_attenuation_forecast_scale_cap.py similarity index 64% rename from systems/provided/rob_system/forecastScaleCap.py rename to systems/provided/attenuate_vol/vol_attenuation_forecast_scale_cap.py index 0b29e15935..493c097c32 100644 --- a/systems/provided/rob_system/forecastScaleCap.py +++ b/systems/provided/attenuate_vol/vol_attenuation_forecast_scale_cap.py @@ -1,4 +1,5 @@ from syscore.pandas.strategy_functions import quantile_of_points_in_data_series +from syscore.pandas.pdutils import from_scalar_values_to_ts from systems.forecast_scale_cap import * @@ -39,16 +40,37 @@ def get_raw_forecast(self, instrument_code, rule_variation_name): raw_forecast_before_atten = self.get_raw_forecast_before_attenuation( instrument_code, rule_variation_name ) + vol_attenutation_reindex = ( + self.get_attenuation_for_rule_and_instrument_indexed_to_forecast( + instrument_code=instrument_code, rule_variation_name=rule_variation_name + ) + ) + + attenuated_forecast = raw_forecast_before_atten * vol_attenutation_reindex + + return attenuated_forecast + + @diagnostic() + def get_attenuation_for_rule_and_instrument_indexed_to_forecast( + self, instrument_code, rule_variation_name + ) -> pd.Series: + + raw_forecast_before_atten = self.get_raw_forecast_before_attenuation( + instrument_code, rule_variation_name + ) + use_attenuation = self.config.get_element_or_default("use_attenuation", []) if rule_variation_name not in use_attenuation: - return raw_forecast_before_atten - else: - vol_attenutation = self.get_vol_attenuation(instrument_code) - vol_attenutation_reindex = vol_attenutation.reindex(raw_forecast_before_atten.index, method="ffill") - attenuated_forecast = raw_forecast_before_atten * vol_attenutation_reindex + forecast_ts = raw_forecast_before_atten.index + return from_scalar_values_to_ts(1.0, forecast_ts) + + vol_attenutation = self.get_vol_attenuation(instrument_code) + vol_attenutation_reindex = vol_attenutation.reindex( + raw_forecast_before_atten.index, method="ffill" + ) - return attenuated_forecast + return vol_attenutation_reindex # this is a little slow so suggestions for speeding up are welcome diff --git a/systems/provided/dynamic_small_system_optimise/accounts_stage.py b/systems/provided/dynamic_small_system_optimise/accounts_stage.py index fdde5408e9..9e71c0fff5 100644 --- a/systems/provided/dynamic_small_system_optimise/accounts_stage.py +++ b/systems/provided/dynamic_small_system_optimise/accounts_stage.py @@ -71,7 +71,9 @@ def optimised_turnover_at_portfolio_level( ) -> float: ## assumes we use all capital - average_position_for_turnover = self.get_volatility_scalar(instrument_code) + average_position_for_turnover = self.get_average_position_at_subsystem_level( + instrument_code + ) ## Using actual capital positions = self.get_optimised_position(instrument_code) diff --git a/systems/provided/example/daily_with_order_simulation.py b/systems/provided/example/daily_with_order_simulation.py new file mode 100644 index 0000000000..c1aa925e89 --- /dev/null +++ b/systems/provided/example/daily_with_order_simulation.py @@ -0,0 +1,57 @@ +### THIS IS AN EXAMPLE OF HOW TO USE A PROPER ORDER SIMULATOR RATHER THAN VECTORISED +### P&L, FOR A SIMPLE TREND SYSTEM USING DAILY DATA WITH MARKET ORDERS + +import matplotlib + +matplotlib.use("TkAgg") + +from syscore.constants import arg_not_supplied + +# from sysdata.sim.csv_futures_sim_data import csvFuturesSimData +from sysdata.sim.db_futures_sim_data import dbFuturesSimData +from sysdata.config.configdata import Config + +from systems.forecasting import Rules +from systems.basesystem import System + +from systems.rawdata import RawData +from systems.forecast_combine import ForecastCombine +from systems.forecast_scale_cap import ForecastScaleCap +from systems.positionsizing import PositionSizing +from systems.portfolio import Portfolios +from systems.accounts.order_simulator.account_curve_order_simulator import ( + AccountWithOrderSimulator, +) +from systems.accounts.accounts_stage import Account + + +def futures_system( + sim_data=arg_not_supplied, + use_vanilla_accounting: bool = False, + config_filename="systems.provided.example.daily_with_order_simulation.yaml", +): + + if sim_data is arg_not_supplied: + sim_data = dbFuturesSimData() + + config = Config(config_filename) + if use_vanilla_accounting: + account = Account() + else: + account = AccountWithOrderSimulator() + system = System( + [ + account, + Portfolios(), + PositionSizing(), + ForecastCombine(), + ForecastScaleCap(), + Rules(), + RawData(), + ], + sim_data, + config, + ) + system.set_logging_level("on") + + return system diff --git a/systems/provided/example/daily_with_order_simulation.yaml b/systems/provided/example/daily_with_order_simulation.yaml new file mode 100644 index 0000000000..1df8965ac3 --- /dev/null +++ b/systems/provided/example/daily_with_order_simulation.yaml @@ -0,0 +1,28 @@ +#YAML +percentage_vol_target: 25 +notional_trading_capital: 50000 +base_currency: "USD" +buffer_method: 'none' # not used with order sim, for consistency with vanilla +trading_rules: + ewmac8: + function: systems.provided.rules.ewmac.ewmac_forecast_with_defaults + data: rawdata.get_daily_prices + other_args: + Lfast: 8 + Lslow: 32 + forecast_scalar: 18.0 + ewmac32: + function: systems.provided.rules.ewmac.ewmac_forecast_with_defaults + data: rawdata.get_daily_prices + other_args: + Lfast: 32 + Lslow: 128 + forecast_scalar: 8.44 +forecast_weights: + ewmac8: 0.50 + ewmac32: 0.50 +forecast_div_multiplier: 1.1 +instrument_weights: + US10: .5 + SP500_micro: .5 +instrument_div_multiplier: 1.4 diff --git a/systems/provided/example/hourly_with_order_simulation.py b/systems/provided/example/hourly_with_order_simulation.py new file mode 100644 index 0000000000..bfc1d2a8e2 --- /dev/null +++ b/systems/provided/example/hourly_with_order_simulation.py @@ -0,0 +1,63 @@ +### THIS IS AN EXAMPLE OF HOW TO USE A PROPER ORDER SIMULATOR RATHER THAN VECTORISED +### P&L, FOR A SIMPLE TREND SYSTEM USING HOURLY DATA WITH MARKET ORDERS + +import matplotlib + +matplotlib.use("TkAgg") + +from syscore.constants import arg_not_supplied + +# from sysdata.sim.csv_futures_sim_data import csvFuturesSimData +from sysdata.sim.db_futures_sim_data import dbFuturesSimData +from sysdata.config.configdata import Config + +from systems.forecasting import Rules +from systems.basesystem import System + +from systems.rawdata import RawData +from systems.forecast_combine import ForecastCombine +from systems.forecast_scale_cap import ForecastScaleCap +from systems.positionsizing import PositionSizing +from systems.portfolio import Portfolios +from systems.accounts.order_simulator.hourly_market_orders import ( + AccountWithOrderSimulatorForHourlyMarketOrders, +) +from systems.accounts.order_simulator.hourly_limit_orders import ( + AccountWithOrderSimulatorForLimitOrders, +) +from systems.accounts.accounts_stage import Account + + +def futures_system( + sim_data=arg_not_supplied, + use_limit_orders: bool = False, + use_vanilla_accounting: bool = False, + config_filename="systems.provided.example.hourly_with_order_simulator.yaml", +): + + if sim_data is arg_not_supplied: + sim_data = dbFuturesSimData() + + config = Config(config_filename) + if use_vanilla_accounting: + account = Account() + elif use_limit_orders: + account = AccountWithOrderSimulatorForLimitOrders() + else: + account = AccountWithOrderSimulatorForHourlyMarketOrders() + system = System( + [ + account, + Portfolios(), + PositionSizing(), + ForecastCombine(), + ForecastScaleCap(), + Rules(), + RawData(), + ], + sim_data, + config, + ) + system.set_logging_level("on") + + return system diff --git a/systems/provided/example/hourly_with_order_simulator.yaml b/systems/provided/example/hourly_with_order_simulator.yaml new file mode 100644 index 0000000000..9e08ba8e8c --- /dev/null +++ b/systems/provided/example/hourly_with_order_simulator.yaml @@ -0,0 +1,28 @@ +#YAML +percentage_vol_target: 25 +notional_trading_capital: 50000 +base_currency: "USD" +buffer_method: 'none' # not used with order sim, for consistency with vanilla +trading_rules: + ewmac8: + function: systems.provided.rules.ewmac.ewmac_forecast_with_defaults + data: rawdata.get_hourly_prices + other_args: + Lfast: 8 + Lslow: 32 + forecast_scalar: 20.0 + ewmac32: + function: systems.provided.rules.ewmac.ewmac_forecast_with_defaults + data: rawdata.get_hourly_prices + other_args: + Lfast: 32 + Lslow: 128 + forecast_scalar: 10.0 +forecast_weights: + ewmac8: 0.50 + ewmac32: 0.50 +forecast_div_multiplier: 1.1 +instrument_weights: + US10: .5 + SP500_micro: .5 +instrument_div_multiplier: 1.4 diff --git a/systems/provided/mr/accounts.py b/systems/provided/mr/accounts.py new file mode 100644 index 0000000000..1bcb3883fa --- /dev/null +++ b/systems/provided/mr/accounts.py @@ -0,0 +1,60 @@ +import pandas as pd + +from systems.system_cache import diagnostic, input +from systems.accounts.order_simulator.account_curve_order_simulator import ( + AccountWithOrderSimulator, +) + +from systems.provided.mr.forecast_combine import MrForecastCombine +from systems.provided.attenuate_vol.vol_attenuation_forecast_scale_cap import ( + volAttenForecastScaleCap, +) +from systems.provided.mr.rawdata import MrRawData +from systems.provided.mr.mr_pandl_order_simulator import MROrderSimulator + + +class MrAccount(AccountWithOrderSimulator): + @diagnostic(not_pickable=True) + def get_order_simulator( + self, instrument_code, is_subsystem: bool + ) -> MROrderSimulator: + return MROrderSimulator( + system_accounts_stage=self, + instrument_code=instrument_code, + is_subsystem=is_subsystem, + ) + + ### The following is required to access information to do the MR positions + @input + def daily_equilibrium_price(self, instrument_code: str) -> pd.Series: + return self.raw_data_stage.daily_equilibrium_price(instrument_code) + + @input + def conditioning_forecast(self, instrument_code: str) -> pd.Series: + return self.comb_forecast_stage.conditioning_forecast(instrument_code) + + @input + def forecast_attenuation(self, instrument_code: str) -> pd.Series: + mr_rule = self.comb_forecast_stage.mr_rule_name + return self.forecast_scale_stage.get_attenuation_for_rule_and_instrument_indexed_to_forecast( + instrument_code=instrument_code, rule_variation_name=mr_rule + ) + + @input + def forecast_scalar(self, instrument_code: str) -> pd.Series: + mr_rule = self.comb_forecast_stage.mr_rule_name + return self.forecast_scale_stage.get_forecast_scalar( + instrument_code=instrument_code, rule_variation_name=mr_rule + ) + + @property + def forecast_scale_stage(self) -> volAttenForecastScaleCap: + return self.parent.forecastScaleCap + + @property + def comb_forecast_stage(self) -> MrForecastCombine: + return self.parent.combForecast + + @property + def raw_data_stage(self) -> MrRawData: + return self.parent.rawdata diff --git a/systems/provided/mr/config.yaml b/systems/provided/mr/config.yaml index d37b590e09..9dd408314d 100644 --- a/systems/provided/mr/config.yaml +++ b/systems/provided/mr/config.yaml @@ -1,4 +1,5 @@ mr: + mr_span_days: 5 conditioning_rule: momentum16 mr_rule: mean_reversion ## this rule is used for the @@ -15,10 +16,8 @@ trading_rules: function: 'systems.provided.mr.rules.mr_rule' data: - "rawdata.get_hourly_prices" - - "rawdata.get_daily_prices" - "rawdata.daily_returns_volatility" - other_args: - mr_span_days: 5 + - "rawdata.daily_equilibrium_price" forecast_scalars: momentum16: 4.1 mean_reversion: 20.0 diff --git a/systems/provided/mr/create_limit_orders.py b/systems/provided/mr/create_limit_orders.py new file mode 100644 index 0000000000..824ba1a56f --- /dev/null +++ b/systems/provided/mr/create_limit_orders.py @@ -0,0 +1,177 @@ +import datetime +from typing import Union + +from systems.accounts.order_simulator.simple_orders import ( + ListOfSimpleOrdersWithDate, + empty_list_of_orders_with_date, + SimpleOrderWithDate, +) +from systems.provided.mr.forecasting import calculate_scaled_attenuated_forecast +from systems.provided.mr.data_and_constants import ( + Mr_Limit_Types, + LOWER_LIMIT, + UPPER_LIMIT, + Mr_Trading_Flags, + REACHED_FORECAST_LIMIT, + MRDataAtIDXPoint, +) + + +def create_limit_orders_for_mr_data( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: MRDataAtIDXPoint, +) -> ListOfSimpleOrdersWithDate: + + lower_order_list = create_lower_limit_order( + data_for_idx=data_for_idx, + current_datetime=current_datetime, + current_position=current_position, + ) + upper_order_list = create_upper_limit_order( + data_for_idx=data_for_idx, + current_datetime=current_datetime, + current_position=current_position, + ) + combined_list_of_orders = lower_order_list + upper_order_list + + return ListOfSimpleOrdersWithDate(combined_list_of_orders) + + +def create_upper_limit_order( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: MRDataAtIDXPoint, +) -> ListOfSimpleOrdersWithDate: + + ## FIXME WONT' WORK WITH VERY LARGE POSITIONS AND LARGE TICK SIZES IN PRODUCTION + trade_to_upper = +1 + position_upper = current_position + trade_to_upper + upper_limit_price = limit_price_given_higher_position( + position_to_derive_for=position_upper, data_for_idx=data_for_idx + ) + if upper_limit_price is REACHED_FORECAST_LIMIT: + return empty_list_of_orders_with_date() + + upper_order = SimpleOrderWithDate( + quantity=trade_to_upper, + limit_price=upper_limit_price, + submit_date=current_datetime, + ) + + return ListOfSimpleOrdersWithDate([upper_order]) + + +def create_lower_limit_order( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: MRDataAtIDXPoint, +) -> ListOfSimpleOrdersWithDate: + + ## FIXME WONT' WORK WITH VERY LARGE POSITIONS AND LARGE TICK SIZES IN PRODUCTION + trade_to_lower = -1 + position_lower = current_position + trade_to_lower + lower_limit_price = limit_price_given_lower_position( + position_to_derive_for=position_lower, data_for_idx=data_for_idx + ) + + if lower_limit_price is REACHED_FORECAST_LIMIT: + return empty_list_of_orders_with_date() + + lower_order = SimpleOrderWithDate( + quantity=trade_to_lower, + limit_price=lower_limit_price, + submit_date=current_datetime, + ) + + return ListOfSimpleOrdersWithDate([lower_order]) + + +def limit_price_given_higher_position( + position_to_derive_for: int, + data_for_idx: MRDataAtIDXPoint, +) -> Union[float, Mr_Trading_Flags]: + ### FIXME WITH TICK SIZE DO DIFFERENTIAL ROUNDING HERE DEPENDING ON LIMIT TYPE + + return derive_limit_price_at_position_for_mean_reversion_overlay( + position_to_derive_for=position_to_derive_for, + data_for_idx=data_for_idx, + limit_type=UPPER_LIMIT, + ) + + +def limit_price_given_lower_position( + position_to_derive_for: int, + data_for_idx: MRDataAtIDXPoint, +) -> Union[float, Mr_Trading_Flags]: + ### FIXME WITH TICK SIZE DO DIFFERENTIAL ROUNDING HERE DEPENDING ON LIMIT TYPE + + return derive_limit_price_at_position_for_mean_reversion_overlay( + position_to_derive_for=position_to_derive_for, + data_for_idx=data_for_idx, + limit_type=LOWER_LIMIT, + ) + + +def derive_limit_price_at_position_for_mean_reversion_overlay( + position_to_derive_for: int, + data_for_idx: MRDataAtIDXPoint, + limit_type: Mr_Limit_Types, +) -> Union[float, Mr_Trading_Flags]: + + if is_current_forecast_beyond_limits( + data_for_idx=data_for_idx, limit_type=limit_type + ): + return REACHED_FORECAST_LIMIT + + limit_price = derive_limit_price_without_checks( + position_to_derive_for=position_to_derive_for, data_for_idx=data_for_idx + ) + + return limit_price + + +def is_current_forecast_beyond_limits( + data_for_idx: MRDataAtIDXPoint, limit_type: Mr_Limit_Types +) -> bool: + + scaled_attenuated_forecast = calculate_scaled_attenuated_forecast(data_for_idx) + abs_forecast_cap = data_for_idx.abs_forecast_cap + if limit_type is UPPER_LIMIT: + return scaled_attenuated_forecast > abs_forecast_cap + elif limit_type is LOWER_LIMIT: + return scaled_attenuated_forecast < -abs_forecast_cap + else: + raise Exception("Limit type %s not recognised!" % str(limit_type)) + + +def derive_limit_price_without_checks( + position_to_derive_for: int, + data_for_idx: MRDataAtIDXPoint, +) -> float: + """ + RAW_FORECAST = (equilibrium - current_price)/ daily_vol_hourly + [uncapped] FORECAST = forecast_atten * forecast_scalar * RAW_FORECAST + position = (average position * FORECAST ) / (avg_abs_forecast) + = (average position * forecast_atten * forecast_scalar * + (equilibrium - current_price)) / (avg_abs_forecast * daily_vol_hourly) + let FNUM = average position * forecast_atten * forecast_scalar + let FDIV = (avg_abs_forecast * daily_vol_hourly) + then + position = FNUM * (equilibrium - current_price) / FDIV + + ## solve for limit_price = current_price, given position: + + FNUM * (equilibrium - limit_price) = position * FDIV + (equilibrium - limit_price) = position * FDIV / FNUM + limit_price = equilibrium - (position * FDIV / FNUM) + limit_price = equilibrium - [(position * avg_abs_forecast * daily_vol_hourly) / + (average position * forecast_atten * forecast_scalar)] + """ + d = data_for_idx + limit_price = d.equilibrium_price - [ + (position_to_derive_for * d.avg_abs_forecast * d.hourly_vol) + / (d.average_position * d.forecast_attenuation * d.forecast_scalar) + ] + + return limit_price diff --git a/systems/provided/mr/create_orders.py b/systems/provided/mr/create_orders.py new file mode 100644 index 0000000000..48e3a242b4 --- /dev/null +++ b/systems/provided/mr/create_orders.py @@ -0,0 +1,144 @@ +import datetime +from typing import Union +from syscore.genutils import same_sign +from systems.accounts.order_simulator.fills_and_orders import ( + ListOfSimpleOrdersAndResultingFill, + fill_list_of_simple_orders, +) +from systems.accounts.order_simulator.simple_orders import ( + ListOfSimpleOrdersWithDate, + SimpleOrderWithDate, + empty_list_of_orders_with_date, +) +from systems.provided.mr.create_limit_orders import create_limit_orders_for_mr_data +from systems.provided.mr.data_and_constants import ( + Mr_Trading_Flags, + CLOSE_MR_POSITIONS, + MRDataAtIDXPoint, +) +from systems.provided.mr.forecasting import calculate_capped_scaled_attenuated_forecast + + +## FIXME - PRICE FRACTION ISSUES/ LARGE FUND ISSUES + + +def generate_mr_orders_and_fill_at_idx_point( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: MRDataAtIDXPoint, +) -> ListOfSimpleOrdersAndResultingFill: + + list_of_orders = create_orders_from_mr_data( + current_position=current_position, + current_datetime=current_datetime, + data_for_idx=data_for_idx, + ) + + next_datetime = data_for_idx.next_datetime + next_hourly_price = data_for_idx.next_hourly_price + + fill = fill_list_of_simple_orders( + list_of_orders, market_price=next_hourly_price, fill_datetime=next_datetime + ) + + return ListOfSimpleOrdersAndResultingFill(list_of_orders=list_of_orders, fill=fill) + + +def create_orders_from_mr_data( + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: MRDataAtIDXPoint, +) -> ListOfSimpleOrdersWithDate: + + optimal_unrounded_position = derive_optimal_unrounded_position( + data_for_idx=data_for_idx + ) + if optimal_unrounded_position is CLOSE_MR_POSITIONS: + ## market order to close positions + return list_with_dated_closing_market_order( + current_position=current_position, current_datetime=current_datetime + ) + list_of_orders = create_orders_for_mr_data_if_not_closing( + optimal_unrounded_position=optimal_unrounded_position, + current_datetime=current_datetime, + current_position=current_position, + data_for_idx=data_for_idx, + ) + + return list_of_orders + + +def derive_optimal_unrounded_position( + data_for_idx: MRDataAtIDXPoint, +) -> Union[int, Mr_Trading_Flags]: + + capped_scaled_attenuated_forecast = calculate_capped_scaled_attenuated_forecast( + data_for_idx=data_for_idx + ) + current_conditioner_for_forecast = data_for_idx.conditioning_forecast + if not same_sign( + capped_scaled_attenuated_forecast, current_conditioner_for_forecast + ): + ## Market order to close positions + return CLOSE_MR_POSITIONS + + avg_abs_forecast = data_for_idx.avg_abs_forecast + average_position = data_for_idx.average_position + + optimal_unrounded_position = average_position * ( + capped_scaled_attenuated_forecast / avg_abs_forecast + ) + + return optimal_unrounded_position + + +def list_with_dated_closing_market_order( + current_position: int, current_datetime: datetime.datetime +) -> ListOfSimpleOrdersWithDate: + if current_position == 0: + return empty_list_of_orders_with_date() + + closing_dated_market_order = SimpleOrderWithDate( + quantity=-current_position, submit_date=current_datetime + ) + return ListOfSimpleOrdersWithDate([closing_dated_market_order]) + + +def create_orders_for_mr_data_if_not_closing( + optimal_unrounded_position: float, + current_position: int, + current_datetime: datetime.datetime, + data_for_idx: MRDataAtIDXPoint, +) -> ListOfSimpleOrdersWithDate: + rounded_optimal_position = round(optimal_unrounded_position) + + diff_to_current = abs(rounded_optimal_position - current_position) + + if diff_to_current > 1: + ## Trade straight to optimal with market order + return list_with_single_dated_market_order_to_trade_to_optimal( + current_position=current_position, + rounded_optimal_position=rounded_optimal_position, + current_datetime=current_datetime, + ) + + list_of_orders = create_limit_orders_for_mr_data( + data_for_idx=data_for_idx, + current_datetime=current_datetime, + current_position=current_position, + ) + + return list_of_orders + + +def list_with_single_dated_market_order_to_trade_to_optimal( + rounded_optimal_position: int, + current_position: int, + current_datetime: datetime.datetime, +) -> ListOfSimpleOrdersWithDate: + + required_trade = rounded_optimal_position - current_position + dated_market_order = SimpleOrderWithDate( + quantity=required_trade, submit_date=current_datetime + ) + return ListOfSimpleOrdersWithDate([dated_market_order]) diff --git a/systems/provided/mr/data_and_constants.py b/systems/provided/mr/data_and_constants.py new file mode 100644 index 0000000000..72a79dcd72 --- /dev/null +++ b/systems/provided/mr/data_and_constants.py @@ -0,0 +1,54 @@ +from collections import namedtuple +from dataclasses import dataclass +from enum import Enum + +import pandas as pd + + +class Mr_Limit_Types(Enum): + LOWER_LIMIT = 1 + UPPER_LIMIT = 2 + + +LOWER_LIMIT = Mr_Limit_Types.LOWER_LIMIT +UPPER_LIMIT = Mr_Limit_Types.UPPER_LIMIT + + +class Mr_Trading_Flags(Enum): + CLOSE_MR_POSITIONS = 1 + REACHED_FORECAST_LIMIT = 2 + + +CLOSE_MR_POSITIONS = Mr_Trading_Flags.CLOSE_MR_POSITIONS +REACHED_FORECAST_LIMIT = Mr_Trading_Flags.REACHED_FORECAST_LIMIT + + +@dataclass +class MROrderSeriesData: + equilibrium_hourly_price_series: pd.Series + price_series: pd.Series + hourly_vol_series: pd.Series + conditioning_forecast_series: pd.Series + average_position_series: pd.Series + forecast_attenuation_series: pd.Series + forecast_scalar_series: pd.Series + avg_abs_forecast: float = 10.0 + abs_forecast_cap: float = 20.0 + + +MRDataAtIDXPoint = namedtuple( + "MRDataAtIDXPoint", + [ + "equilibrium_price", + "average_position", + "hourly_vol", + "conditioning_forecast", + "avg_abs_forecast", + "abs_forecast_cap", + "forecast_attenuation", + "current_hourly_price", + "next_hourly_price", + "next_datetime", + "forecast_scalar", + ], +) diff --git a/systems/provided/mr/forecast_combine.py b/systems/provided/mr/forecast_combine.py index 01bc4aa589..0b7001d3ba 100644 --- a/systems/provided/mr/forecast_combine.py +++ b/systems/provided/mr/forecast_combine.py @@ -5,6 +5,7 @@ from systems.system_cache import output, diagnostic from systems.forecast_combine import ForecastCombine + class MrForecastCombine(ForecastCombine): @output() def get_combined_forecast(self, instrument_code: str) -> pd.Series: @@ -14,36 +15,58 @@ def get_combined_forecast(self, instrument_code: str) -> pd.Series: ## this is already scaled and capped forecast_before_filter = self.mr_forecast(instrument_code) - forecast_after_filter = apply_forecast_filter(forecast_before_filter, conditioning_forecast) + forecast_after_filter = apply_forecast_filter( + forecast_before_filter, conditioning_forecast + ) forecast_after_filter = forecast_after_filter.ffill() return forecast_after_filter - def conditioning_forecast(self, instrument_code) -> pd.Series: - conditioning_rule_name = self.config.mr['conditioning_rule'] - return self._get_capped_individual_forecast(instrument_code=instrument_code, - rule_variation_name=conditioning_rule_name) + return self._get_capped_individual_forecast( + instrument_code=instrument_code, + rule_variation_name=self.conditioning_rule_name, + ) def mr_forecast(self, instrument_code) -> pd.Series: - mr_rule_name = self.config.mr['mr_rule'] - return self._get_capped_individual_forecast(instrument_code=instrument_code, - rule_variation_name=mr_rule_name) + return self._get_capped_individual_forecast( + instrument_code=instrument_code, rule_variation_name=self.mr_rule_name + ) + + @property + def conditioning_rule_name(self) -> str: + return self.mr_config["conditioning_rule"] + + @property + def mr_rule_name(self) -> str: + return self.mr_config["mr_rule"] + + @property + def mr_config(self) -> dict: + return self.config.mr + def apply_forecast_filter(forecast, conditioning_forecast): - conditioning_forecast = conditioning_forecast.reindex(forecast.index, method = "ffill") + conditioning_forecast = conditioning_forecast.reindex( + forecast.index, method="ffill" + ) - new_values = [forecast_overlay_for_sign(forecast_value, filter_value) - for forecast_value, filter_value - in zip(forecast.values, conditioning_forecast.values)] + new_values = [ + forecast_overlay_for_sign(forecast_value, filter_value) + for forecast_value, filter_value in zip( + forecast.values, conditioning_forecast.values + ) + ] return pd.Series(new_values, forecast.index) + def forecast_overlay_for_sign(forecast_value, filter_value): if same_sign(forecast_value, filter_value): return forecast_value else: return 0 + def same_sign(x, y): return sign(x) == sign(y) diff --git a/systems/provided/mr/forecasting.py b/systems/provided/mr/forecasting.py new file mode 100644 index 0000000000..ff46e31a88 --- /dev/null +++ b/systems/provided/mr/forecasting.py @@ -0,0 +1,82 @@ +import pandas as pd + +from systems.provided.mr.data_and_constants import MRDataAtIDXPoint + +### This rule produces a nominal forecast, but this isn't actually used by the accounting code +### that generates the orders and fills, and would do the same in production +### Only useful for a quick and dirty view of how the forecast does without limit orders + + +def mr_rule( + hourly_price: pd.Series, + daily_vol: pd.Series, + daily_equilibrium: pd.Series, +) -> pd.Series: + daily_vol_indexed_hourly = daily_vol.reindex(hourly_price.index, method="ffill") + hourly_equilibrium = daily_equilibrium.reindex(hourly_price.index, method="ffill") + + forecast_before_filter = ( + hourly_equilibrium - hourly_price + ) / daily_vol_indexed_hourly + + return forecast_before_filter + + +#### Following is the 'real' forecasting code + + +def calculate_capped_scaled_attenuated_forecast( + data_for_idx: MRDataAtIDXPoint, +) -> float: + scaled_attenuated_forecast = calculate_scaled_attenuated_forecast( + data_for_idx=data_for_idx + ) + capped_scaled_attenuated_forecast = cap_and_floor_forecast( + scaled_attenuated_forecast=scaled_attenuated_forecast, data_for_idx=data_for_idx + ) + + return capped_scaled_attenuated_forecast + + +def calculate_scaled_attenuated_forecast( + data_for_idx: MRDataAtIDXPoint, +) -> float: + scaled_forecast = calculate_scaled_forecast(data_for_idx) + forecast_attenuation = data_for_idx.forecast_attenuation + + return scaled_forecast * forecast_attenuation + + +def calculate_scaled_forecast( + data_for_idx: MRDataAtIDXPoint, +) -> float: + + forecast_scalar = data_for_idx.forecast_scalar + raw_forecast = calculate_raw_forecast(data_for_idx) + + return forecast_scalar * raw_forecast + + +def calculate_raw_forecast( + data_for_idx: MRDataAtIDXPoint, +) -> float: + equilibrium_price = data_for_idx.equilibrium_price + current_hourly_price = data_for_idx.current_hourly_price + hourly_vol = data_for_idx.hourly_vol + + return (equilibrium_price - current_hourly_price) / hourly_vol + + +def cap_and_floor_forecast( + scaled_attenuated_forecast: float, data_for_idx: MRDataAtIDXPoint +) -> float: + forecast_cap = +data_for_idx.abs_forecast_cap + forecast_floor = -data_for_idx.abs_forecast_cap + capped_scaled_attenuated_forecast = min( + [ + max([scaled_attenuated_forecast, forecast_floor]), + forecast_cap, + ] + ) + + return capped_scaled_attenuated_forecast diff --git a/systems/provided/mr/mr_pandl_order_simulator.py b/systems/provided/mr/mr_pandl_order_simulator.py new file mode 100644 index 0000000000..1989084176 --- /dev/null +++ b/systems/provided/mr/mr_pandl_order_simulator.py @@ -0,0 +1,137 @@ +from typing import Callable + +import pandas as pd + + +from systems.accounts.order_simulator.hourly_limit_orders import ( + HourlyOrderSimulatorOfLimitOrders, +) + +from systems.provided.mr.create_orders import generate_mr_orders_and_fill_at_idx_point +from systems.provided.mr.data_and_constants import MROrderSeriesData, MRDataAtIDXPoint + + +class MROrderSimulator(HourlyOrderSimulatorOfLimitOrders): + system_accounts_stage: object ## no type avoid circular import + instrument_code: str + is_subsystem: bool = False + + def _diagnostic_df(self) -> pd.DataFrame: + raise NotImplemented() + + def _series_data(self) -> MROrderSeriesData: + series_data = build_mr_series_data( + system_accounts_stage=self.system_accounts_stage, + instrument_code=self.instrument_code, + is_subsystem=self.is_subsystem, + ) + return series_data + + @property + def idx_data_function(self) -> Callable: + return get_mr_sim_hourly_data_at_idx_point + + @property + def orders_fills_function(self) -> Callable: + return generate_mr_orders_and_fill_at_idx_point + + +def build_mr_series_data( + system_accounts_stage, ## no type hint avoid circular import + instrument_code: str, + is_subsystem: bool = False, +) -> MROrderSeriesData: + + hourly_price_series = system_accounts_stage.get_hourly_prices(instrument_code) + + daily_equilibrium = system_accounts_stage.daily_equilibrium_price(instrument_code) + equilibrium_hourly_price_series = daily_equilibrium.reindex( + hourly_price_series.index, method="ffill" + ) + + daily_vol_series = system_accounts_stage.get_daily_returns_volatility( + instrument_code + ) + hourly_vol_series = daily_vol_series.reindex( + hourly_price_series.index, method="ffill" + ) + daily_conditioning_forecast_series = system_accounts_stage.conditioning_forecast( + instrument_code + ) + conditioning_forecast_series = daily_conditioning_forecast_series.reindex( + hourly_price_series.index, method="ffill" + ) + if is_subsystem: + daily_average_position_series = ( + system_accounts_stage.get_average_position_at_subsystem_level( + instrument_code + ) + ) + else: + daily_average_position_series = system_accounts_stage.get_average_position_for_instrument_at_portfolio_level( + instrument_code + ) + + average_position_series = daily_average_position_series.reindex( + hourly_price_series.index, method="ffill" + ) + + forecast_attenuation = system_accounts_stage.forecast_attenuation(instrument_code) + forecast_attenuation_series = forecast_attenuation.reindex( + hourly_price_series.index, method="ffill" + ) + + forecast_scalar = system_accounts_stage.forecast_scalar(instrument_code) + forecast_scalar_series = forecast_scalar.reindex( + hourly_price_series.index, method="ffill" + ) + + avg_abs_forecast = system_accounts_stage.average_forecast() + abs_forecast_cap = system_accounts_stage.forecast_cap() + + return MROrderSeriesData( + equilibrium_hourly_price_series=equilibrium_hourly_price_series, + average_position_series=average_position_series, + price_series=hourly_price_series, + hourly_vol_series=hourly_vol_series, + conditioning_forecast_series=conditioning_forecast_series, + avg_abs_forecast=avg_abs_forecast, + abs_forecast_cap=abs_forecast_cap, + forecast_attenuation_series=forecast_attenuation_series, + forecast_scalar_series=forecast_scalar_series, + ) + + +def get_mr_sim_hourly_data_at_idx_point( + idx: int, series_data: MROrderSeriesData +) -> MRDataAtIDXPoint: + prices = series_data.price_series + + current_hourly_price = prices[idx] + next_hourly_price = prices[idx + 1] + + average_position = series_data.average_position_series[idx] + equilibrium_price = series_data.equilibrium_hourly_price_series[idx] + conditioning_forecast = series_data.conditioning_forecast_series[idx] + forecast_attenuation = series_data.forecast_attenuation_series[idx] + hourly_vol = series_data.hourly_vol_series[idx] + forecast_scalar = series_data.forecast_scalar_series[idx] + + next_datetime = prices.index[idx + 1] + + abs_forecast_cap = series_data.abs_forecast_cap + avg_abs_forecast = series_data.avg_abs_forecast + + return MRDataAtIDXPoint( + next_hourly_price=next_hourly_price, + current_hourly_price=current_hourly_price, + conditioning_forecast=conditioning_forecast, + forecast_attenuation=forecast_attenuation, + average_position=average_position, + equilibrium_price=equilibrium_price, + abs_forecast_cap=abs_forecast_cap, + avg_abs_forecast=avg_abs_forecast, + next_datetime=next_datetime, + hourly_vol=hourly_vol, + forecast_scalar=forecast_scalar, + ) diff --git a/systems/provided/mr/rawdata.py b/systems/provided/mr/rawdata.py new file mode 100644 index 0000000000..0153511665 --- /dev/null +++ b/systems/provided/mr/rawdata.py @@ -0,0 +1,11 @@ +import pandas as pd +from systems.rawdata import RawData + + +class MrRawData(RawData): + def daily_equilibrium_price(self, instrument_code: str) -> pd.Series: + daily_price = self.get_daily_prices(instrument_code) + mr_span = self.config.mr["mr_span_days"] + daily_equilibrium = daily_price.ewm(span=mr_span).mean() + + return daily_equilibrium diff --git a/systems/provided/mr/rules.py b/systems/provided/mr/rules.py deleted file mode 100644 index f21c8ff5db..0000000000 --- a/systems/provided/mr/rules.py +++ /dev/null @@ -1,12 +0,0 @@ -import pandas as pd -def mr_rule(hourly_price: pd.Series, - daily_price: pd.Series, - daily_vol: pd.Series, - mr_span_days: int = 5) -> pd.Series: - equilibrium = daily_price.ewm(span=mr_span_days).mean() - daily_vol_hourly = daily_vol.reindex(hourly_price.index, method = "ffill") - equilibrium = equilibrium.reindex(hourly_price.index, method = "ffill") - - forecast_before_filter = (equilibrium - hourly_price)/ daily_vol_hourly - - return forecast_before_filter \ No newline at end of file diff --git a/systems/provided/mr/run_system.py b/systems/provided/mr/run_system.py index 5c0578578a..b78aacadd1 100644 --- a/systems/provided/mr/run_system.py +++ b/systems/provided/mr/run_system.py @@ -1,4 +1,5 @@ import matplotlib + matplotlib.use("TkAgg") from syscore.constants import arg_not_supplied @@ -10,11 +11,14 @@ from systems.forecasting import Rules from systems.basesystem import System from systems.provided.mr.forecast_combine import MrForecastCombine -from systems.provided.rob_system.forecastScaleCap import volAttenForecastScaleCap -from systems.provided.rob_system.rawdata import myFuturesRawData +from systems.provided.attenuate_vol.vol_attenuation_forecast_scale_cap import ( + volAttenForecastScaleCap, +) +from systems.provided.mr.rawdata import MrRawData from systems.positionsizing import PositionSizing from systems.portfolio import Portfolios -from systems.accounts.accounts_stage import Account +from systems.provided.mr.accounts import MrAccount + def futures_system( sim_data=arg_not_supplied, config_filename="systems.provided.mr.config.yaml" @@ -27,10 +31,10 @@ def futures_system( system = System( [ - Account(), + MrAccount(), Portfolios(), PositionSizing(), - myFuturesRawData(), + MrRawData(), MrForecastCombine(), volAttenForecastScaleCap(), Rules(), diff --git a/systems/provided/rob_system/run_system.py b/systems/provided/rob_system/run_system.py index df548c0ca1..eb7709bd28 100644 --- a/systems/provided/rob_system/run_system.py +++ b/systems/provided/rob_system/run_system.py @@ -11,7 +11,7 @@ from systems.forecasting import Rules from systems.basesystem import System from systems.forecast_combine import ForecastCombine -from systems.provided.rob_system.forecastScaleCap import volAttenForecastScaleCap +from systems.provided.attenuate_vol.vol_attenuation_forecast_scale_cap import volAttenForecastScaleCap from systems.provided.rob_system.rawdata import myFuturesRawData from systems.positionsizing import PositionSizing from systems.portfolio import Portfolios diff --git a/systems/provided/static_small_system_optimise/optimise_small_system.py b/systems/provided/static_small_system_optimise/optimise_small_system.py index de71e539ae..c22fff0043 100644 --- a/systems/provided/static_small_system_optimise/optimise_small_system.py +++ b/systems/provided/static_small_system_optimise/optimise_small_system.py @@ -281,7 +281,9 @@ def net_SR_for_instrument_in_system( def calculate_maximum_position(system, instrument_code, instrument_weight_idm=0.25): - pos_at_average = system.positionSize.get_volatility_scalar(instrument_code) + pos_at_average = system.positionSize.get_average_position_at_subsystem_level( + instrument_code + ) pos_at_average_in_system = pos_at_average * instrument_weight_idm forecast_multiplier = ( system.combForecast.get_forecast_cap() / system.positionSize.avg_abs_forecast() diff --git a/systems/tests/test_position_sizing.py b/systems/tests/test_position_sizing.py index 6e2c6712d9..b0676dc816 100644 --- a/systems/tests/test_position_sizing.py +++ b/systems/tests/test_position_sizing.py @@ -123,7 +123,7 @@ def test_get_instrument_value_vol(self): @unittest.SkipTest def test_get_get_volatility_scalar(self): self.assertAlmostEqual( - self.system.positionSize.get_volatility_scalar("EDOLLAR") + self.system.positionSize.get_average_position_at_subsystem_level("EDOLLAR") .ffill() .values[-1], 10.33292952955, diff --git a/tests/test_examples.py b/tests/test_examples.py index d05a6ed96c..e5f159315e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -222,7 +222,11 @@ def test_simple_system_position_sizing( print(my_system.positionSize.get_block_value("EDOLLAR").tail(5)) print(my_system.positionSize.get_underlying_price("EDOLLAR")) print(my_system.positionSize.get_instrument_value_vol("EDOLLAR").tail(5)) - print(my_system.positionSize.get_volatility_scalar("EDOLLAR").tail(5)) + print( + my_system.positionSize.get_average_position_at_subsystem_level( + "EDOLLAR" + ).tail(5) + ) print(my_system.positionSize.get_vol_target_dict()) print(my_system.positionSize.get_subsystem_position("EDOLLAR").tail(5))