From c902949eb22214d133373e7928a2a004783c02b4 Mon Sep 17 00:00:00 2001 From: Frederic Fortier Date: Wed, 23 Aug 2017 14:31:10 -0400 Subject: [PATCH] Improved daily performance tracking --- catalyst/exchange/algorithm_exchange.py | 37 +++++++++++++++++++------ catalyst/exchange/exchange_utils.py | 37 +++++++++++++++++++++---- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/catalyst/exchange/algorithm_exchange.py b/catalyst/exchange/algorithm_exchange.py index 00d90b8b..0a5cad54 100644 --- a/catalyst/exchange/algorithm_exchange.py +++ b/catalyst/exchange/algorithm_exchange.py @@ -13,6 +13,7 @@ import os import signal import sys +import json from datetime import timedelta from time import sleep @@ -30,8 +31,8 @@ from catalyst.exchange.exchange_errors import ( ExchangePortfolioDataError, ExchangeTransactionError ) -from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root -from catalyst.exchange.exchange_utils import save_algo_object, get_algo_object +from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \ + save_algo_object, get_algo_object, append_algo_object from catalyst.finance.performance.period import calc_period_stats from catalyst.gens.tradesimulation import AlgorithmSimulator from catalyst.utils.api_support import ( @@ -52,7 +53,6 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): self.exchange = kwargs.pop('exchange', None) self.algo_namespace = kwargs.pop('algo_namespace', None) self.orders = {} - self.minute_perfs = None self.is_running = True self.retry_check_open_orders = 5 @@ -93,12 +93,20 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): log.info('You pressed Ctrl+C!') - stats = pd.DataFrame(self.minute_perfs) - stats.set_index('period_close', drop=True, inplace=True) + # TODO: load from daily perf pickle files? + # See: catalyst.algorithm.TradingAlgorithm#_create_daily_stats + # stats = pd.DataFrame(self.minute_perfs) + # stats.set_index('period_close', drop=True, inplace=True) - # TODO: pyfolio is going to want the daily, just resample and pick the last row - # daily_stats = stats.resample('24H').last() - self.analyze(stats) + try: + # convert perf dict to pandas dataframe + stats = self.prepare_period_stats( + start_dt=self.sim_params.start_session, + end_dt=pd.Timestamp.utcnow() + ) + self.analyze(stats) + except Exception as e: + log.warn('Unable to compute daily stats: {}'.format(e)) sys.exit(0) @@ -295,11 +303,22 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): # Performance tracker and keep only minute and cumulative self.perf_tracker.update_performance() + # TODO: save for future use? minute_stats = self.prepare_period_stats( data.current_dt, data.current_dt + timedelta(minutes=1)) log.debug('the minute performance:\n{}'.format(minute_stats)) - self.minute_perfs.append(minute_stats) + today = pd.to_datetime('today', utc=True) + daily_stats = self.prepare_period_stats( + start_dt=today, + end_dt=pd.Timestamp.utcnow() + ) + save_algo_object( + algo_name=self.algo_namespace, + key=today.strftime('%Y-%m-%d'), + obj=daily_stats, + rel_path='daily_perf' + ) except Exception as e: log.warn('unable to calculate performance: {}'.format(e)) diff --git a/catalyst/exchange/exchange_utils.py b/catalyst/exchange/exchange_utils.py index bfd3f3c4..40b8f32f 100644 --- a/catalyst/exchange/exchange_utils.py +++ b/catalyst/exchange/exchange_utils.py @@ -2,6 +2,7 @@ import json import os import pickle import urllib +from datetime import date, datetime from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \ ExchangeSymbolsNotFound @@ -75,9 +76,13 @@ def get_algo_folder(algo_name, environ=None): return algo_folder -def get_algo_object(algo_name, key, environ=None): - algo_folder = get_algo_folder(algo_name, environ) - filename = os.path.join(algo_folder, key + '.p') +def get_algo_object(algo_name, key, environ=None, rel_path=None): + folder = get_algo_folder(algo_name, environ) + + if rel_path is not None: + folder = os.path.join(folder, rel_path) + + filename = os.path.join(folder, key + '.p') if os.path.isfile(filename): try: @@ -89,11 +94,25 @@ def get_algo_object(algo_name, key, environ=None): return None -def save_algo_object(algo_name, key, obj, environ=None): +def save_algo_object(algo_name, key, obj, environ=None, rel_path=None): + folder = get_algo_folder(algo_name, environ) + + if rel_path is not None: + folder = os.path.join(folder, rel_path) + ensure_directory(folder) + + filename = os.path.join(folder, key + '.p') + + with open(filename, 'wb') as handle: + pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL) + + +def append_algo_object(algo_name, key, obj, environ=None): algo_folder = get_algo_folder(algo_name, environ) filename = os.path.join(algo_folder, key + '.p') - with open(filename, 'wb') as handle: + mode = 'a+b' if os.path.isfile(filename) else 'wb' + with open(filename, mode) as handle: pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL) @@ -104,3 +123,11 @@ def get_exchange_minute_writer_root(exchange_name, environ=None): ensure_directory(minute_data_folder) return minute_data_folder + + +def perf_serial(obj): + """JSON serializer for objects not serializable by default json code""" + + if isinstance(obj, (datetime, date)): + return obj.isoformat() + raise TypeError("Type %s not serializable" % type(obj))