class PredefinedAssetEngine(BacktestBaseEngine): def get_action_handler(self, action: Action) -> Action: handler_factory = PredefinedAssetEngineActionFactory( self.action_impl_map) return handler_factory.get_action_handler(action) def supports_strategy(self, strategy): all_actions = reduce(lambda x, y: x + y, (map(lambda x: x.actions, strategy.triggers))) try: for x in all_actions: self.get_action_handler(x) except RuntimeError: return False return True def __init__(self, data_mgr: DataManager = DataManager(), calendars: Union[str, Tuple[str, ...]] = 'Weekend', tz: timezone = timezone('America/New_York'), valuation_method: ValuationMethod = ValuationMethod( ValuationFixingType.PRICE), action_impl_map=None): if action_impl_map is None: action_impl_map = {Action: SubmitOrderActionImpl} self.action_impl_map = action_impl_map self.calendars = calendars self.tz = tz self.data_handler = DataHandler(data_mgr, tz=tz) self.valuation_method = valuation_method self.execution_engine = None def _eod_valuation_time(self): if self.valuation_method.window: return self.valuation_method.window.end else: return dt.time(23) def _timer(self, strategy, start, end, frequency, states=None): dates = list(map(lambda x: x.date(), to_datetime(bdate_range(start=start, end=end, freq=frequency)))) \ if states is None else states all_times = [] times = list() for trigger in strategy.triggers: if hasattr(trigger, 'get_trigger_times'): for t in trigger.get_trigger_times(): # allow user to define their trigger times as a time, in which case add that time to every date # or as a datetime itself in which case just add it to the timer if isinstance(t, dt.datetime): all_times.append(t) else: times.append(t) times.append(self._eod_valuation_time()) times = list(dict.fromkeys(times)) for d in dates: if isinstance(d, dt.datetime): if self.calendars == 'Weekend' or is_business_day( d.date(), self.calendars): all_times.append(d) for t in times: if d.tzinfo is not None and d.tzinfo.utcoffset( d) is not None: all_times.append( d.tzinfo.localize( dt.datetime.combine(d.date(), t))) else: if self.calendars == 'Weekend' or is_business_day( d, self.calendars): for t in times: all_times.append(dt.datetime.combine(d, t)) all_times = list(set(all_times)) all_times.sort() return all_times def _adjust_date(self, date): date = (date + BDay(1) - BDay(1)).date() # 1st move to latest weekday. if self.calendars == 'Weekend' or is_business_day( date, self.calendars): return date else: return prev_business_date(date, self.calendars) def run_backtest(self, strategy, start, end, frequency="B", states=None, initial_value=100): # initialize backtest object self.data_handler.reset_clock() backtest = PredefinedAssetBacktest(self.data_handler, initial_value) # initialize execution engine self.execution_engine = SimulatedExecutionEngine(self.data_handler) if states is not None: timer = self._timer(strategy, start, end, frequency, states) else: # if start is a holiday, go back to the previous day on the backtest calendar adjusted_start = self._adjust_date(start) backtest.set_start_date(adjusted_start) # create timer timer_start = (adjusted_start + BDay(1)).date() if self.calendars == 'Weekend' \ else business_day_offset(adjusted_start, 1, roll='forward', calendars=self.calendars) timer_end = self._adjust_date(end) timer = self._timer(strategy, timer_start, timer_end, frequency) self._run(strategy, timer, backtest) return backtest def _run(self, strategy, timer, backtest: PredefinedAssetBacktest): events = deque() for state in tqdm(timer): # update to latest data self.data_handler.update(state) # see if any submitted orders have been executed fills = self.execution_engine.ping(state) events.extend(fills) # generate a market event events.append(MarketEvent()) # create valuation event when it's due for daily valuation if state.time() == self._eod_valuation_time(): events.append(ValuationEvent()) while events: event = events.popleft() if event.type == 'Market': # market event (new mkt data coming in) for trigger in strategy.triggers: trigger_info = trigger.has_triggered(state, backtest) if trigger_info.triggered: for action in trigger.actions: info_dict = trigger_info.info_dict info = info_dict[type( action)] if info_dict and type( action) in info_dict else None orders = self.get_action_handler( action).apply_action( state, backtest, info) backtest.record_orders(orders) events.extend([OrderEvent(o) for o in orders]) elif event.type == 'Order': # order event (submit the order to execution engine) self.execution_engine.submit_order(event) elif event.type == 'Fill': # fill event (update backtest with the fill results) backtest.update_fill(event) elif event.type == 'Valuation': # valuation event (calculate daily level) backtest.mark_to_market(state, self.valuation_method) return backtest
class PredefinedAssetEngine(BacktestBaseEngine): def get_action_handler(self, action: Action) -> Action: handler_factory = PredefinedAssetEngineActionFactory(self.action_impl_map) return handler_factory.get_action_handler(action) @classmethod def supports_strategy(cls, strategy): all_actions = reduce(lambda x, y: x + y, (map(lambda x: x.actions, strategy.triggers))) try: for x in all_actions: cls.get_action_handler(x) except RuntimeError: return False return True def __init__(self, data_mgr: DataManager, calendars: Union[str, Tuple[str, ...]], tz: timezone, valuation_method: ValuationMethod, action_impl_map={}): self.action_impl_map = action_impl_map self.calendars = calendars self.tz = tz self.data_handler = DataHandler(data_mgr, tz=tz) self.valuation_method = valuation_method self.execution_engine = None def _eod_valuation_time(self): if self.valuation_method.window: return self.valuation_method.window.end else: return dt.time(23) def _timer(self, strategy, start, end): dates = date_range(start, end, calendars=self.calendars) times = list() for trigger in strategy.triggers: times.extend(trigger.get_trigger_times()) times.append(self._eod_valuation_time()) times = list(dict.fromkeys(times)) all_times = [] for d in dates: for t in times: all_times.append(dt.datetime.combine(d, t)) return all_times def run_backtest(self, strategy, start, end): # initialize backtest object backtest = PredefinedAssetBacktest(self.data_handler) backtest.set_start_date(start) # initialize execution engine self.execution_engine = SimulatedExecutionEngine(self.data_handler) # create timer timer = self._timer(strategy, start, end) self._run(strategy, timer, backtest) return backtest def _run(self, strategy, timer, backtest: PredefinedAssetBacktest): events = deque() for state in tqdm(timer): # update to latest data self.data_handler.update(state) # see if any submitted orders have been executed fills = self.execution_engine.ping(state) events.extend(fills) # generate a market event events.append(MarketEvent()) # create valuation event when it's due for daily valuation if state.time() == self._eod_valuation_time(): events.append(ValuationEvent()) while events: event = events.popleft() if event.type == 'Market': # market event (new mkt data coming in) for trigger in strategy.triggers: trigger_info = trigger.has_triggered(state, backtest) if trigger_info.triggered: for action in trigger.actions: info_dict = trigger_info.info_dict info = info_dict[type(action)] if info_dict and type(action) in info_dict else None orders = self.get_action_handler(action).apply_action(state, backtest, info) backtest.record_orders(orders) events.extend([OrderEvent(o) for o in orders]) elif event.type == 'Order': # order event (submit the order to execution engine) self.execution_engine.submit_order(event) elif event.type == 'Fill': # fill event (update backtest with the fill results) backtest.update_fill(event) elif event.type == 'Valuation': # valuation event (calculate daily level) backtest.mark_to_market(state, self.valuation_method) return backtest