ENH: Add intraday risk and performance for minute emission.

Both risk and performance now calculate performance since inception
(cumulative) and since the open. Both periods are updated intraday
and both are reported.

Batch risk for periods starting after the end of the treasury curve
history now use most recent curve.
This commit is contained in:
fawce
2013-05-03 23:39:07 -04:00
committed by Eddie Hebert
parent 6afc85c17d
commit e6c156c50b
3 changed files with 166 additions and 54 deletions
+86 -20
View File
@@ -162,19 +162,46 @@ class PerformanceTracker(object):
self.total_days = self.sim_params.days_in_period
self.capital_base = self.sim_params.capital_base
self.emission_rate = sim_params.emission_rate
self.cumulative_risk_metrics = \
risk.RiskMetricsIterative(self.sim_params)
self.emission_rate = sim_params.emission_rate
self.perf_periods = []
if self.emission_rate == 'daily':
self.all_benchmark_returns = pd.Series(
index=trading.environment.trading_days)
self.intraday_risk_metrics = None
self.cumulative_risk_metrics = \
risk.RiskMetricsIterative(self.sim_params)
elif self.emission_rate == 'minute':
self.all_benchmark_returns = pd.Series(index=pd.date_range(
self.sim_params.first_open, self.sim_params.last_close,
freq='Min'))
self.intraday_risk_metrics = \
risk.RiskMetricsIterative(self.sim_params)
# this performance period will span the entire simulation.
self.cumulative_risk_metrics = \
risk.RiskMetricsIterative(self.sim_params)
self.cumulative_risk_metrics.initialize_daily_indices()
self.minute_performance = PerformancePeriod(
# initial cash is your capital base.
self.capital_base,
# the cumulative period will be calculated over the
# entire test.
self.period_start,
self.period_end,
# don't save the transactions for the cumulative
# period
keep_transactions=False,
keep_orders=False,
# don't serialize positions for cumualtive period
serialize_positions=False
)
self.perf_periods.append(self.minute_performance)
# this performance period will span the entire simulation from
# inception.
self.cumulative_performance = PerformancePeriod(
# initial cash is your capital base.
self.capital_base,
@@ -188,6 +215,7 @@ class PerformanceTracker(object):
# don't serialize positions for cumualtive period
serialize_positions=False
)
self.perf_periods.append(self.cumulative_performance)
# this performance period will span just the current market day
self.todays_performance = PerformancePeriod(
@@ -200,6 +228,7 @@ class PerformanceTracker(object):
keep_orders=True,
serialize_positions=True
)
self.perf_periods.append(self.todays_performance)
self.saved_dt = self.period_start
self.returns = []
@@ -255,9 +284,11 @@ class PerformanceTracker(object):
# Naming as intraday to make clear that these results are
# being updated per minute
_dict['intraday_risk_metrics'] = \
self.cumulative_risk_metrics.to_dict()
self.intraday_risk_metrics.to_dict()
_dict['intraday_perf'] = self.todays_performance.to_dict(
self.saved_dt)
_dict['cumulative_risk_metrics'] = \
self.cumulative_risk_metrics.to_dict()
return _dict
@@ -267,26 +298,24 @@ class PerformanceTracker(object):
if event.type == zp.DATASOURCE_TYPE.TRADE:
#update last sale
self.cumulative_performance.update_last_sale(event)
self.todays_performance.update_last_sale(event)
for perf_period in self.perf_periods:
perf_period.update_last_sale(event)
elif event.type == zp.DATASOURCE_TYPE.TRANSACTION:
# Trade simulation always follows a transaction with the
# TRADE event that was used to simulate it, so we don't
# check for end of day rollover messages here.
self.txn_count += 1
self.cumulative_performance.execute_transaction(
event
)
self.todays_performance.execute_transaction(event)
for perf_period in self.perf_periods:
perf_period.execute_transaction(event)
elif event.type == zp.DATASOURCE_TYPE.DIVIDEND:
self.cumulative_performance.add_dividend(event)
self.todays_performance.add_dividend(event)
for perf_period in self.perf_periods:
perf_period.add_dividend(event)
elif event.type == zp.DATASOURCE_TYPE.ORDER:
self.cumulative_performance.record_order(event)
self.todays_performance.record_order(event)
for perf_period in self.perf_periods:
perf_period.record_order(event)
elif event.type == zp.DATASOURCE_TYPE.CUSTOM:
pass
@@ -294,14 +323,51 @@ class PerformanceTracker(object):
self.all_benchmark_returns[event.dt] = event.returns
#calculate performance as of last trade
self.cumulative_performance.calculate_performance()
self.todays_performance.calculate_performance()
for perf_period in self.perf_periods:
perf_period.calculate_performance()
def handle_minute_close(self, dt):
#update risk metrics for cumulative performance
self.cumulative_risk_metrics.update(dt,
self.todays_performance.returns,
self.all_benchmark_returns[dt])
todays_date = self.market_close.replace(hour=0, minute=0, second=0,
microsecond=0)
minute_returns = self.minute_performance.returns
self.minute_performance.rollover()
algo_minute_returns = pd.Series({dt: minute_returns})
bench_minute_returns = pd.Series({dt: self.all_benchmark_returns[dt]})
# the intraday risk is calculated on top of minute performance
# returns for the bench and the algo
self.intraday_risk_metrics.update(dt,
algo_minute_returns,
bench_minute_returns)
# the intraday risk metrics compound the minutely returns of the
# benchmark.
bench_since_open = self.intraday_risk_metrics.benchmark_returns[-1]
benchmark_returns = pd.Series({dt: bench_since_open})
# if we've reached market close, check on dividends
if dt == self.market_close:
for perf_period in self.perf_periods:
perf_period.update_dividends(todays_date)
algorithm_returns = pd.Series({dt: self.todays_performance.returns})
self.intraday_risk_metrics.update(dt,
algorithm_returns,
benchmark_returns)
self.cumulative_risk_metrics.update(todays_date,
algorithm_returns,
benchmark_returns)
# if this is the close, save the returns objects for cumulative
# risk calculations
if dt == self.market_close:
todays_return_obj = zp.DailyReturn(
todays_date,
self.todays_performance.returns
)
self.returns.append(todays_return_obj)
def handle_market_close(self):
# add the return results from today to the list of DailyReturn objects.
+78 -34
View File
@@ -284,37 +284,56 @@ that date doesn't exceed treasury history range."
class RiskMetricsBase(object):
def __init__(self, start_date, end_date, returns):
def __init__(self, start_date, end_date, returns,
benchmark_returns=None):
treasury_curves = trading.environment.treasury_curves
mask = ((treasury_curves.index >= start_date) &
(treasury_curves.index <= end_date))
if treasury_curves.index[-1] >= start_date:
mask = ((treasury_curves.index >= start_date) &
(treasury_curves.index <= end_date))
self.treasury_curves = treasury_curves[mask]
self.treasury_curves = treasury_curves[mask]
else:
# our test is beyond the treasury curve history
# so we'll use the last available treasury curve
self.treasury_curves = treasury_curves[-1:]
self.start_date = start_date
self.end_date = end_date
self.algorithm_period_returns, self.algorithm_returns = \
self.calculate_period_returns(returns)
benchmark_returns = [
x for x in trading.environment.benchmark_returns
if x.date >= returns[0].date and x.date <= returns[-1].date
]
if not benchmark_returns:
benchmark_returns = [
x for x in trading.environment.benchmark_returns
if x.date >= returns[0].date and
x.date <= returns[-1].date
]
self.benchmark_period_returns, self.benchmark_returns = \
self.calculate_period_returns(benchmark_returns)
self.runonce = True
self.algorithm_returns = self.mask_returns_to_period(returns)
self.benchmark_returns = self.mask_returns_to_period(benchmark_returns)
self.calculate_metrics()
if(len(self.benchmark_returns) != len(self.algorithm_returns)):
def calculate_metrics(self):
self.benchmark_period_returns = \
self.calculate_period_returns(self.benchmark_returns)
self.algorithm_period_returns = \
self.calculate_period_returns(self.algorithm_returns)
if not self.algorithm_returns.index.equals(
self.benchmark_returns.index
):
message = "Mismatch between benchmark_returns ({bm_count}) and \
algorithm_returns ({algo_count}) in range {start} : {end}"
message = message.format(
bm_count=len(self.benchmark_returns),
algo_count=len(self.algorithm_returns),
start=start_date,
end=end_date
start=self.start_date,
end=self.end_date
)
raise Exception(message)
self.runonce = False
self.num_trading_days = len(self.benchmark_returns)
self.benchmark_volatility = self.calculate_volatility(
@@ -399,7 +418,7 @@ class RiskMetricsBase(object):
return '\n'.join(statements)
def calculate_period_returns(self, daily_returns):
def mask_returns_to_period(self, daily_returns):
returns = pd.Series([x.returns for x in daily_returns],
index=[x.date for x in daily_returns])
@@ -410,9 +429,11 @@ class RiskMetricsBase(object):
(returns.index <= self.end_date) & trade_day_mask)
returns = returns[mask]
period_returns = (1. + returns).prod() - 1
return returns
return period_returns, returns
def calculate_period_returns(self, returns):
period_returns = (1. + returns).prod() - 1
return period_returns
def calculate_volatility(self, daily_returns):
return np.std(daily_returns, ddof=1) * math.sqrt(self.num_trading_days)
@@ -536,21 +557,18 @@ class RiskMetricsIterative(RiskMetricsBase):
(all_trading_days <= self.end_date))
self.trading_days = all_trading_days[mask]
if sim_params.period_end not in self.trading_days:
last_day = pd.tseries.index.DatetimeIndex(
[sim_params.period_end]
)
self.trading_days = self.trading_days.append(last_day)
self.sim_params = sim_params
if sim_params.emission_rate == 'daily':
self.algorithm_returns_cont = pd.Series(index=self.trading_days)
self.benchmark_returns_cont = pd.Series(index=self.trading_days)
self.initialize_daily_indices()
elif sim_params.emission_rate == 'minute':
self.algorithm_returns_cont = pd.Series(index=pd.date_range(
sim_params.first_open, sim_params.last_close,
freq="Min"))
self.benchmark_returns_cont = pd.Series(index=pd.date_range(
sim_params.first_open, sim_params.last_close,
freq="Min"))
self.initialize_minute_indices(sim_params)
self.algorithm_returns = None
self.benchmark_returns = None
@@ -576,6 +594,19 @@ class RiskMetricsIterative(RiskMetricsBase):
self.max_drawdown = 0
self.current_max = -np.inf
self.excess_returns = []
self.daily_treasury = {}
def initialize_minute_indices(self, sim_params):
self.algorithm_returns_cont = pd.Series(index=pd.date_range(
sim_params.first_open, sim_params.last_close,
freq="Min"))
self.benchmark_returns_cont = pd.Series(index=pd.date_range(
sim_params.first_open, sim_params.last_close,
freq="Min"))
def initialize_daily_indices(self):
self.algorithm_returns_cont = pd.Series(index=self.trading_days)
self.benchmark_returns_cont = pd.Series(index=self.trading_days)
@property
def last_return_date(self):
@@ -597,7 +628,9 @@ class RiskMetricsIterative(RiskMetricsBase):
self.benchmark_period_returns.append(
self.calculate_period_returns(self.benchmark_returns))
if(len(self.benchmark_returns) != len(self.algorithm_returns)):
if not self.algorithm_returns.index.equals(
self.benchmark_returns.index
):
message = "Mismatch between benchmark_returns ({bm_count}) and \
algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
message = message.format(
@@ -614,11 +647,22 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
self.calculate_volatility(self.benchmark_returns))
self.algorithm_volatility.append(
self.calculate_volatility(self.algorithm_returns))
self.treasury_period_return = choose_treasury(
self.treasury_curves,
self.start_date,
self.algorithm_returns.index[-1]
)
# caching the treasury rates for the live case is a
# big speedup, because it avoids searching the treasury
# curves on every minute.
treasury_end = self.algorithm_returns.index[-1].replace(
hour=0, minute=0)
if treasury_end not in self.daily_treasury:
treasury_period_return = choose_treasury(
self.treasury_curves,
self.start_date,
self.algorithm_returns.index[-1]
)
self.daily_treasury[treasury_end] =\
treasury_period_return
self.treasury_period_return = \
self.daily_treasury[treasury_end]
self.excess_returns.append(
self.algorithm_period_returns[-1] - self.treasury_period_return)
self.beta.append(self.calculate_beta()[0])
+2
View File
@@ -88,6 +88,8 @@ class TradingEnvironment(object):
load(self.bm_symbol)
self.treasury_curves = pd.Series(treasury_curves_map)
if max_date:
self.treasury_curves = self.treasury_curves[:max_date]
self._period_trading_days = None
self._trading_days_series = None