Skip to content

Commit

Permalink
Added target allocations output via JSONStatistics.
Browse files Browse the repository at this point in the history
  • Loading branch information
mhallsmoore committed Dec 10, 2019
1 parent c4d2831 commit e36eb52
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 8 deletions.
14 changes: 12 additions & 2 deletions qstrader/portcon/pcm.py
Expand Up @@ -230,7 +230,7 @@ def _create_zero_target_weights_vector(self, dt):
assets = self.universe.get_assets(dt)
return {asset: 0.0 for asset in assets}

def __call__(self, dt):
def __call__(self, dt, stats=None):
"""
Execute the portfolio construction process at a particular
provided date-time.
Expand All @@ -242,7 +242,11 @@ def __call__(self, dt):
Parameters
----------
dt : `pd.Timestamp`
The date-time used to for Asset list determination and weight generation.
The date-time used to for Asset list determination and
weight generation.
stats : `dict`, optional
An optional statistics dictionary to append values to
throughout the simulation lifetime.
Returns
-------
Expand All @@ -267,6 +271,12 @@ def __call__(self, dt):
full_zero_weights, optimised_weights
)

# TODO: Improve this with a full statistics logging handler
if stats is not None:
alloc_dict = {'Date': dt}
alloc_dict.update(full_weights)
stats['target_allocations'].append(alloc_dict)

# Calculate target portfolio in notional
target_portfolio = self._generate_target_portfolio(dt, full_weights)

Expand Down
42 changes: 42 additions & 0 deletions qstrader/statistics/json_statistics.py
Expand Up @@ -39,6 +39,7 @@ class JSONStatistics(object):
def __init__(
self,
equity_curve,
target_allocations,
strategy_id=None,
strategy_name=None,
benchmark_curve=None,
Expand All @@ -48,6 +49,7 @@ def __init__(
output_filename='statistics.json'
):
self.equity_curve = equity_curve
self.target_allocations = target_allocations
self.strategy_id = strategy_id
self.strategy_name = strategy_name
self.benchmark_curve = benchmark_curve
Expand Down Expand Up @@ -84,6 +86,38 @@ def _series_to_tuple_list(series):
for k, v in series.to_dict().items()
]

@staticmethod
def _dataframe_to_column_list(df):
"""
Converts Pandas DataFrame indexed by date-time into
list of tuples indexed by milliseconds since epoch.
Parameters
----------
df : `pd.DataFrame`
The Pandas DataFrame to be converted.
Returns
-------
`list[tuple]`
The list of epoch-indexed tuple values.
"""
col_list = []
for k, v in df.to_dict().items():
name = k.replace('EQ:', '')
date_val_tups = [
(
int(
datetime.datetime.combine(
date_key, datetime.datetime.min.time()
).timestamp() * 1000.0
), date_val
)
for date_key, date_val in v.items()
]
col_list.append({'name': name, 'data': date_val_tups})
return col_list

@staticmethod
def _calculate_returns(curve):
"""
Expand Down Expand Up @@ -139,6 +173,11 @@ def _calculate_statistics(self, curve):

return stats

def _calculate_allocations(self, allocations):
"""
"""
return JSONStatistics._dataframe_to_column_list(allocations)

def _create_full_statistics(self):
"""
Create the 'full' statistics dictionary, which has an entry for the
Expand All @@ -153,6 +192,9 @@ def _create_full_statistics(self):

JSONStatistics._calculate_returns(self.equity_curve)
full_stats['strategy'] = self._calculate_statistics(self.equity_curve)
full_stats['strategy']['target_allocations'] = self._calculate_allocations(
self.target_allocations
)

if self.benchmark_curve is not None:
JSONStatistics._calculate_returns(self.benchmark_curve)
Expand Down
7 changes: 5 additions & 2 deletions qstrader/system/qts.py
Expand Up @@ -98,7 +98,7 @@ def _initialise_models(self):
data_handler=self.data_handler
)

def __call__(self, dt):
def __call__(self, dt, stats=None):
"""
Construct the portfolio and (optionally) execute the orders
with the broker.
Expand All @@ -107,13 +107,16 @@ def __call__(self, dt):
----------
dt : `pd.Timestamp`
The current time.
stats : `dict`, optional
An optional statistics dictionary to append values to
throughout the simulation lifetime.
Returns
-------
`None`
"""
# Construct the target portfolio
rebalance_orders = self.portfolio_construction_model(dt)
rebalance_orders = self.portfolio_construction_model(dt, stats=stats)

# Execute the orders
self.execution_handler(dt, rebalance_orders)
30 changes: 26 additions & 4 deletions qstrader/trading/backtest.py
Expand Up @@ -110,6 +110,7 @@ def __init__(

self.qts = self._create_quant_trading_system()
self.equity_curve = []
self.target_allocations = []

def _is_rebalance_event(self, dt):
"""
Expand Down Expand Up @@ -255,8 +256,6 @@ def _create_rebalance_event_times(self):
Creates the list of rebalance timestamps used to determine when
to execute the quant trading strategy throughout the backtest.
TODO: Currently supports only weekly rebalances.
Returns
-------
`List[pd.Timestamp]`
Expand Down Expand Up @@ -325,7 +324,7 @@ def output_holdings(self):

def get_equity_curve(self):
"""
Returns the Equity Curve as a Pandas DataFrame.
Returns the equity curve as a Pandas DataFrame.
Returns
-------
Expand All @@ -338,12 +337,33 @@ def get_equity_curve(self):
equity_df.index = equity_df.index.date
return equity_df

def get_target_allocations(self):
"""
Returns the target allocations as a Pandas DataFrame
utilising the same index as the equity curve with
forward-filled dates.
Returns
-------
`pd.DataFrame`
The datetime-indexed target allocations of the strategy.
"""
equity_curve = self.get_equity_curve()
alloc_df = pd.DataFrame(self.target_allocations).set_index('Date')
alloc_df.index = alloc_df.index.date
alloc_df = alloc_df.reindex(index=equity_curve.index, method='ffill')
if self.burn_in_dt is not None:
alloc_df = alloc_df[self.burn_in_dt:]
return alloc_df

def run(self, results=True):
"""
Execute the simulation engine by iterating over all
simulation events, rebalancing the quant trading
system at the appropriate schedule.
"""
stats = {'target_allocations': []}

for event in self.sim_engine:
# Output the system event and timestamp
dt = event.ts
Expand All @@ -356,7 +376,7 @@ def run(self, results=True):
# out a full run of the quant trading system
if self._is_rebalance_event(dt):
print(event.ts, "REBALANCE")
self.qts(dt)
self.qts(dt, stats=stats)

# Out of market hours we want a daily
# performance update, but only if we
Expand All @@ -368,6 +388,8 @@ def run(self, results=True):
else:
self._update_equity_curve(dt)

self.target_allocations = stats['target_allocations']

# At the end of the simulation output the
# holdings and plot the tearsheet
if results:
Expand Down

0 comments on commit e36eb52

Please sign in to comment.