diff --git a/zipline/finance/performance.py b/zipline/finance/performance.py index 351466a8..63129b31 100644 --- a/zipline/finance/performance.py +++ b/zipline/finance/performance.py @@ -40,9 +40,12 @@ Performance Tracking | | through all the events delivered to this tracker. | | | For details look at the comments for | | | :py:meth:`zipline.finance.risk.RiskMetrics.to_dict`| + +-----------------+----------------------------------------------------+ + | exceeded_max_ | True if the simulation was stopped because single | + | loss | day losses exceeded the max_drawdown stipulated in | + | | trading_environment. | +-----------------+----------------------------------------------------+ - Position Tracking ================= @@ -154,6 +157,7 @@ class PerformanceTracker(): self.result_stream = None self.last_dict = None self.order_log = [] + self.exceeded_max_loss = False # this performance period will span the entire simulation. self.cumulative_performance = PerformancePeriod( @@ -213,7 +217,7 @@ class PerformanceTracker(): 'capital_base' : self.capital_base, 'cumulative_perf' : self.cumulative_performance.to_dict(), 'daily_perf' : self.todays_performance.to_dict(), - 'cumulative_risk_metrics' : self.cumulative_risk_metrics.to_dict(), + 'cumulative_risk_metrics' : self.cumulative_risk_metrics.to_dict() } def log_order(self, order): @@ -263,11 +267,37 @@ class PerformanceTracker(): # calculate progress of test self.progress = self.day_count / self.total_days + if self.trading_environment.max_drawdown: + max_dd = -1 * self.trading_environment.max_drawdown + if self.todays_performance.returns < max_dd: + qutil.LOGGER.info("Exceeded max drawdown.") + # mark the perf period with max loss flag, + # so it shows up in the update, but don't end the test + # here. Let the update go out before stopping + self.exceeded_max_loss = True + # Output results if self.result_stream: msg = zp.PERF_FRAME(self.to_dict()) self.result_stream.send(msg) - + + if self.exceeded_max_loss: + # now that we've sent the day's update, kill this test + self.handle_simulation_end(skip_close=True) + return + + # check the day's returns versus the max drawdown + # max_drawdown is optional: + if self.trading_environment.max_drawdown: + max_dd = -1 * self.trading_environment.max_drawdown + if self.todays_performance.returns < max_dd: + qutil.LOGGER.info("Exceeded max drawdown.") + # TODO: any other information we need to relay on the + # result socket? + self.exceeded_max_loss = True + self.handle_simulation_end(skip_close=True) + return + #move the market day markers forward self.market_open = self.market_open + self.calendar_day @@ -288,7 +318,7 @@ class PerformanceTracker(): keep_transactions = True ) - def handle_simulation_end(self): + def handle_simulation_end(self, skip_close=False): """ When the simulation is complete, run the full period risk report and send it out on the result_stream. @@ -300,18 +330,21 @@ class PerformanceTracker(): # the stream will end on the last trading day, but will not trigger # an end of day, so we trigger the final market close here. - self.handle_market_close() - + # In the case of errors, we needn't close again. + if not skip_close: + self.handle_market_close() self.risk_report = risk.RiskReport( self.returns, - self.trading_environment + self.trading_environment, + exceeded_max_loss = self.exceeded_max_loss ) - + if self.result_stream: qutil.LOGGER.info("about to stream the risk report...") - report = self.risk_report.to_dict() - msg = zp.RISK_FRAME(report) + risk_dict = self.risk_report.to_dict() + + msg = zp.RISK_FRAME(risk_dict) self.result_stream.send(msg) # this signals that the simulation is complete. self.result_stream.send("DONE") diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index 98f7b7e7..248e75a7 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -315,7 +315,11 @@ class RiskMetrics(): class RiskReport(): - def __init__(self, algorithm_returns, trading_environment): + def __init__( + self, + algorithm_returns, + trading_environment, + exceeded_max_loss=False): """ algorithm_returns needs to be a list of daily_return objects sorted in date ascending order @@ -323,6 +327,7 @@ class RiskReport(): self.algorithm_returns = algorithm_returns self.trading_environment = trading_environment + self.exceeded_max_loss = exceeded_max_loss if len(self.algorithm_returns) == 0: start_date = self.trading_environment.period_start @@ -352,10 +357,11 @@ class RiskReport(): provided for each period. """ return { - 'one_month' : [x.to_dict() for x in self.month_periods], - 'three_month' : [x.to_dict() for x in self.three_month_periods], - 'six_month' : [x.to_dict() for x in self.six_month_periods], - 'twelve_month' : [x.to_dict() for x in self.year_periods] + 'one_month' : [x.to_dict() for x in self.month_periods], + 'three_month' : [x.to_dict() for x in self.three_month_periods], + 'six_month' : [x.to_dict() for x in self.six_month_periods], + 'twelve_month' : [x.to_dict() for x in self.year_periods], + 'exceeded_max_loss' : self.exceeded_max_loss } def periodsInRange(self, months_per, start, end): diff --git a/zipline/finance/trading.py b/zipline/finance/trading.py index 9c815156..b503e501 100644 --- a/zipline/finance/trading.py +++ b/zipline/finance/trading.py @@ -99,6 +99,10 @@ class TradeSimulationClient(qmsg.Component): def process_event(self, event): + if self.perf.exceeded_max_loss: + self.control_out.send(str(zp.CONTROL_PROTOCOL.SHUTDOWN)) + return + # generate transactions, if applicable txn = self.txn_sim.apply_trade_to_open_orders(event) if txn: diff --git a/zipline/protocol.py b/zipline/protocol.py index 0736d79c..c3291556 100644 --- a/zipline/protocol.py +++ b/zipline/protocol.py @@ -663,6 +663,7 @@ def convert_transactions(transactions): for txn in transactions: txn['date'] = EPOCH(txn['dt']) del(txn['dt']) + del(txn['source_id']) results.append(txn) return results