def apply_action(self, state: Union[date, Iterable[date]], backtest: BackTest, trigger_info: Optional[Union[HedgeActionInfo, Iterable[HedgeActionInfo]]] = None): with HistoricalPricingContext(dates=make_list(state), csa_term=self.action.csa_term, show_progress=True): backtest.calc_calls += 1 backtest.calculations += len(make_list(state)) f = Portfolio(make_list(self.action.priceable)).resolve(in_place=False) for create_date, portfolio in f.result().items(): hedge = portfolio.instruments[0] hedge.name = f'{hedge.name}_{create_date.strftime("%Y-%m-%d")}' final_date = get_final_date(hedge, create_date, self.action.trade_duration) active_dates = [s for s in backtest.states if create_date <= s < final_date] if len(active_dates): backtest.scaling_portfolios[create_date].append( ScalingPortfolio(trade=hedge, dates=active_dates, risk=self.action.risk, csa_term=self.action.csa_term, scaling_parameter=self.action.scaling_parameter)) # add cashflows on trade entry and unwind backtest.cash_payments[create_date].append( CashPayment(trade=hedge, effective_date=create_date, direction=-1)) if final_date <= dt.date.today(): backtest.cash_payments[final_date].append( CashPayment(trade=hedge, effective_date=final_date, scale_date=create_date)) return backtest
def apply_action(self, state: Union[date, Iterable[date]], backtest: BackTest): with HistoricalPricingContext(dates=make_list(state), csa_term=self.action.csa_term): backtest.calc_calls += 1 backtest.calculations += len(make_list(state)) f = Portfolio(make_list( self.action.priceable)).resolve(in_place=False) for create_date, portfolio in f.result().items(): active_dates = [ s for s in backtest.states if get_final_date(portfolio.instruments[0], create_date, self. action.trade_duration) >= s >= create_date ] if len(active_dates): for t in portfolio: t.name = f'{t.name}_{create_date.strftime("%Y-%m-%d")}' backtest.scaling_portfolios[create_date].append( ScalingPortfolio(trade=portfolio.instruments[0], dates=active_dates, risk=self.action.risk, csa_term=self.action.csa_term)) return backtest
def _raise_order( self, state: Union[date, Iterable[date]], trigger_info: Optional[Union[AddTradeActionInfo, Iterable[AddTradeActionInfo]]] = None): with PricingContext(is_batch=True, show_progress=True): state_list = make_list(state) orders = {} if trigger_info is None or isinstance(trigger_info, AddTradeActionInfo): trigger_info = [trigger_info for _ in range(len(state_list))] for s, ti in zip_longest(state_list, trigger_info): active_portfolio = self.action.dated_priceables.get( s) or self.action.priceables with PricingContext(pricing_date=s): orders[s] = (Portfolio(active_portfolio).resolve( in_place=False), ti) final_orders = {} for d, p in orders.items(): new_port = [] for t in p[0].result(): t.name = f'{t.name}_{d}' new_port.append(t) new_port = Portfolio(new_port) final_orders[d] = new_port.scale( None if p[1] is None else p[1].scaling, in_place=False) return final_orders
def run_backtest(cls, strategy, start=None, end=None, frequency='BM', window=None, states=None, risks=Price, show_progress=True): dates = pd.date_range(start=start, end=end, freq=frequency).date.tolist() risks = make_list(risks) + strategy.risks backtest = BackTest(strategy, dates, risks) if strategy.initial_portfolio is not None: for date in dates: backtest.portfolio_dict[date].append(strategy.initial_portfolio) for trigger in strategy.triggers: if trigger.calc_type != CalcType.path_dependent: triggered_dates = [date for date in dates if trigger.has_triggered(date, backtest)] for action in trigger.actions: if action.calc_type != CalcType.path_dependent: action.apply_action(triggered_dates, backtest) with PricingContext(is_batch=True, show_progress=show_progress): for day, portfolio in backtest.portfolio_dict.items(): with PricingContext(day): backtest.calc_calls += 1 backtest.calculations += len(portfolio) * len(risks) backtest.add_results(day, portfolio.calc(tuple(risks))) # semi path dependent initial calc for _, scaling_list in backtest.scaling_portfolios.items(): for p in scaling_list: with HistoricalPricingContext(dates=p.dates): backtest.calc_calls += 1 backtest.calculations += len(p.dates) * len(risks) p.results = Portfolio([p.trade]).calc(tuple(risks)) for date in dates: # semi path dependent scaling if date in backtest.scaling_portfolios: for p in backtest.scaling_portfolios[date]: scale_date = p.dates[0] scaling_factor = backtest.results[scale_date][p.risk][0] / p.results[scale_date][p.risk][0] scaled_trade = p.trade.as_dict() scaled_trade['notional_amount'] *= -scaling_factor scaled_trade = Instrument.from_dict(scaled_trade) for day in p.dates: backtest.add_results(day, p.results[day] * -scaling_factor) backtest.portfolio_dict[day] += Portfolio(scaled_trade) # path dependent for trigger in strategy.triggers: if trigger.calc_type == CalcType.path_dependent: if trigger.has_triggered(date, backtest): for action in trigger.actions: action.apply_action(date, backtest) else: for action in trigger.actions: if action.calc_type == CalcType.path_dependent: if trigger.has_triggered(date, backtest): action.apply_action(date, backtest) return backtest
def apply_action( self, state: Union[date, Iterable[date]], backtest: BackTest, trigger_info: Optional[Union[HedgeActionInfo, Iterable[HedgeActionInfo]]] = None): with HistoricalPricingContext(dates=make_list(state), csa_term=self.action.csa_term): backtest.calc_calls += 1 backtest.calculations += len(make_list(state)) f = Portfolio(make_list( self.action.priceable)).resolve(in_place=False) for create_date, portfolio in f.result().items(): final_date = get_final_date(portfolio.instruments[0], create_date, self.action.trade_duration) if self.action.risks_on_final_day: next_state = list( filter(lambda x: x > create_date, backtest.states)) final_date = min( final_date, next_state[0]) if len(next_state) else final_date active_dates = [ s for s in backtest.states if create_date <= s < final_date ] if len(active_dates): for t in portfolio: t.name = f'{t.name}_{create_date.strftime("%Y-%m-%d")}' backtest.scaling_portfolios[create_date].append( ScalingPortfolio(trade=portfolio.instruments[0], dates=active_dates, risk=self.action.risk, csa_term=self.action.csa_term)) if self.action.risks_on_final_day and final_date <= dt.date.today( ): backtest.scaling_portfolios[final_date].append( ScalingPortfolio(trade=portfolio.instruments[0], dates=[final_date], risk=self.action.risk, csa_term=self.action.csa_term, unwind=True)) return backtest
def _raise_order(self, state: Union[date, Iterable[date]], trigger_info: Optional[Union[AddTradeActionInfo, Iterable[AddTradeActionInfo]]] = None): with PricingContext(is_batch=True): state_list = make_list(state) orders = {} if trigger_info is None or isinstance(trigger_info, AddTradeActionInfo): trigger_info = [trigger_info for _ in range(len(state_list))] for s, ti in zip_longest(state_list, trigger_info): active_portfolio = self.action.dated_priceables.get(s) or self.action.priceables with PricingContext(pricing_date=s): orders[s] = (Portfolio(active_portfolio).resolve(in_place=False), ti) orders = {k: v[0].result().scale(None if v[1] is None else v[1].scaling, in_place=False) for k, v in orders.items()} return orders
def run_backtest(cls, strategy, start=None, end=None, frequency='BM', window=None, states=None, risks=Price, show_progress=True): dates = pd.date_range(start=start, end=end, freq=frequency).date.tolist() risks = make_list(risks) + strategy.risks backtest = BackTest(strategy, dates, risks) for trigger in strategy.triggers: if trigger.deterministic: triggered_dates = [ date for date in dates if trigger.has_triggered(date, backtest) ] for action in trigger.actions: if action.deterministic: action.apply_action(triggered_dates, backtest) with PricingContext(is_batch=True, show_progress=show_progress): for day, portfolio in backtest.portfolio_dict.items(): with PricingContext(day): backtest.calc_calls += 1 backtest.calculations += len(portfolio) * len(risks) backtest.add_results( day, portfolio.calc(risks[0] if len(risks) == 1 else tuple(risks))) for trigger in strategy.triggers: if not trigger.deterministic: for date in dates: if trigger.has_triggered(date, backtest): for action in trigger.actions: action.apply_action(date, backtest) else: for action in trigger.actions: if not action.deterministic: for date in dates: if trigger.has_triggered(date, backtest): action.apply_action(date, backtest) return backtest
def __init__(self, trigger_requirements: Optional[TriggerRequirements], actions: Union[Action, Iterable[Action]]): self._trigger_requirements = trigger_requirements self._actions = make_list(actions) self._risks = [x.risk for x in self.actions if x.risk is not None] self._calc_type = CalcType.simple
def run_backtest(self, strategy, start=None, end=None, frequency='1m', states=None, risks=Price, show_progress=True, csa_term=None, visible_to_gs=False, initial_value=0, result_ccy=None, holiday_calendar=None): """ run the backtest following the triggers and actions defined in the strategy. If states are entered run on those dates otherwise build a schedule from the start, end, frequency using gs_quant.datetime.relative_date.RelativeDateSchedule :param strategy: the strategy object :param start: a datetime :param end: a datetime :param frequency: str, default '1m' :param states: a list of dates will override the start, end, freq if provided :param risks: risks to run :param show_progress: boolean default true :param csa_term: the csa term to use :param visible_to_gs: are the contents of risk requests visible to GS (defaults to False) :param initial_value: initial cash value of strategy defaults to 0 :param result_ccy: ccy of all risks, pvs and cash :param holiday_calendar for date maths - list of dates :return: a backtest object containing the portfolios on each day and results which show all risks on all days """ logging.info( f'Starting Backtest: Building Date Schedule - {dt.datetime.now()}') strategy_pricing_dates = RelativeDateSchedule(frequency, start, end).apply_rule(holiday_calendar=holiday_calendar) \ if states is None else states strategy_start_date = strategy_pricing_dates[0] strategy_end_date = strategy_pricing_dates[-1] risks = list(set(make_list(risks) + strategy.risks)) if result_ccy is not None: risks = [(r( currency=result_ccy) if isinstance(r, ParameterisedRiskMeasure) else raiser(f'Unparameterised risk: {r}')) for r in risks] price_risk = Price( currency=result_ccy) if result_ccy is not None else Price backtest = BackTest(strategy, strategy_pricing_dates, risks) logging.info('Resolving initial portfolio') if len(strategy.initial_portfolio): for index in range(len(strategy.initial_portfolio)): old_name = strategy.initial_portfolio[index].name strategy.initial_portfolio[ index].name = f'{old_name}_{strategy_start_date.strftime("%Y-%m-%d")}' entry_payment = CashPayment(strategy.initial_portfolio[index], effective_date=strategy_start_date, direction=-1) backtest.cash_payments[strategy_start_date].append( entry_payment) final_date = get_final_date(strategy.initial_portfolio[index], strategy_start_date, None) exit_payment = CashPayment(strategy.initial_portfolio[index], effective_date=final_date) backtest.cash_payments[final_date].append(exit_payment) init_port = Portfolio(strategy.initial_portfolio) with PricingContext(pricing_date=strategy_start_date, csa_term=csa_term, show_progress=show_progress, visible_to_gs=visible_to_gs): init_port.resolve() for d in strategy_pricing_dates: backtest.portfolio_dict[d].append(init_port.instruments) logging.info( 'Building simple and semi-deterministic triggers and actions') for trigger in strategy.triggers: if trigger.calc_type != CalcType.path_dependent: triggered_dates = [] trigger_infos = defaultdict(list) for d in strategy_pricing_dates: t_info = trigger.has_triggered(d, backtest) if t_info: triggered_dates.append(d) if t_info.info_dict: for k, v in t_info.info_dict.items(): trigger_infos[k].append(v) for action in trigger.actions: if action.calc_type != CalcType.path_dependent: self.get_action_handler(action).apply_action( triggered_dates, backtest, trigger_infos[type(action)] if type(action) in trigger_infos else None) logging.info( f'Filtering strategy calculations to run from {strategy_start_date} to {strategy_end_date}' ) backtest.portfolio_dict = defaultdict( Portfolio, { k: backtest.portfolio_dict[k] for k in backtest.portfolio_dict if strategy_start_date <= k <= strategy_end_date }) backtest.scaling_portfolios = defaultdict( list, { k: backtest.scaling_portfolios[k] for k in backtest.scaling_portfolios if strategy_start_date <= k <= strategy_end_date }) logging.info( 'Pricing simple and semi-deterministic triggers and actions') with PricingContext(is_batch=True, show_progress=show_progress, csa_term=csa_term, visible_to_gs=visible_to_gs): backtest.calc_calls += 1 for day, portfolio in backtest.portfolio_dict.items(): if isinstance(day, dt.date): with PricingContext(day): backtest.calculations += len(portfolio) * len(risks) backtest.add_results(day, portfolio.calc(tuple(risks))) # semi path dependent initial calc for _, scaling_list in backtest.scaling_portfolios.items(): for p in scaling_list: with HistoricalPricingContext(dates=p.dates): backtest.calculations += len(risks) * len(p.dates) port = p.trade if isinstance( p.trade, Portfolio) else Portfolio([p.trade]) p.results = port.calc(tuple(risks)) logging.info( 'Scaling semi-deterministic triggers and actions and calculating path dependent triggers ' 'and actions') for d in strategy_pricing_dates: logging.info(f'{d}: Processing triggers and actions') # path dependent for trigger in strategy.triggers: if trigger.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): for action in trigger.actions: self.get_action_handler(action).apply_action( d, backtest) else: for action in trigger.actions: if action.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): self.get_action_handler(action).apply_action( d, backtest) # test to see if new trades have been added and calc port = [] for t in backtest.portfolio_dict[d]: if t.name not in list(backtest.results[d].to_frame().index): port.append(t) with PricingContext(is_batch=True, csa_term=csa_term, show_progress=show_progress, visible_to_gs=visible_to_gs): if len(port): with PricingContext(pricing_date=d): results = Portfolio(port).calc(tuple(risks)) for sp in backtest.scaling_portfolios[d]: if sp.results is None: with HistoricalPricingContext(dates=sp.dates): backtest.calculations += len(risks) * len(sp.dates) port_sp = sp.trade if isinstance( sp.trade, Portfolio) else Portfolio([sp.trade]) sp.results = port_sp.calc(tuple(risks)) # results should be added outside of pricing context and not in the same call as valuating them if len(port): backtest.add_results(d, results) # semi path dependent scaling if d in backtest.scaling_portfolios: for p in backtest.scaling_portfolios[d]: current_risk = backtest.results[d][p.risk].aggregate( allow_mismatch_risk_keys=True) hedge_risk = p.results[d][p.risk].aggregate() if current_risk.unit != hedge_risk.unit: raise RuntimeError( 'cannot hedge in a different currency') scaling_factor = current_risk / hedge_risk if isinstance(p.trade, Portfolio): # Scale the portfolio by risk target scaled_portfolio_position = copy.deepcopy(p.trade) scaled_portfolio_position.name = f'Scaled_{scaled_portfolio_position.name}' for instrument in scaled_portfolio_position.all_instruments: instrument.name = f'Scaled_{instrument.name}' # trade hedge in opposite direction scale_direction = -1 scaled_portfolio_position.scale(scaling_factor * scale_direction) for day in p.dates: # add scaled hedge position to portfolio for day. NOTE this adds leaves, not the portfolio backtest.portfolio_dict[day] += copy.deepcopy( scaled_portfolio_position) # now apply scaled portfolio to cash payments for d, payments in backtest.cash_payments.items(): for payment in payments: if payment.trade == p.trade: payment.trade = copy.deepcopy( scaled_portfolio_position) payment.scale_date = None else: new_notional = getattr( p.trade, p.scaling_parameter) * -scaling_factor scaled_trade = p.trade.as_dict() scaled_trade[p.scaling_parameter] = new_notional scaled_trade = Instrument.from_dict(scaled_trade) scaled_trade.name = p.trade.name for day in p.dates: backtest.add_results( day, p.results[day] * -scaling_factor) backtest.portfolio_dict[day] += Portfolio( scaled_trade) logging.info('Calculating and scaling newly added portfolio positions') # test to see if new trades have been added and calc with PricingContext(is_batch=True, show_progress=show_progress, csa_term=csa_term, visible_to_gs=visible_to_gs): backtest.calc_calls += 1 leaves_by_date = {} for day, portfolio in backtest.portfolio_dict.items(): results_for_date = backtest.results[day] if len(results_for_date) == 0: continue trades_for_date = list(results_for_date.to_frame().index) leaves = [] for leaf in portfolio: if leaf.name not in trades_for_date: logging.info( f'{day}: new portfolio position {leaf} scheduled for calculation' ) leaves.append(leaf) if len(leaves): with PricingContext(pricing_date=day): leaves_by_date[day] = Portfolio(leaves).calc( tuple(risks)) backtest.calculations += len(leaves) * len(risks) logging.info('Processing results for newly added portfolio positions') for day, leaves in leaves_by_date.items(): backtest.add_results(day, leaves) logging.info('Calculating prices for cash payments') # run any additional calcs to handle cash scaling (e.g. unwinds) cash_results = {} with PricingContext(is_batch=True, show_progress=show_progress, csa_term=csa_term, visible_to_gs=visible_to_gs): backtest.calc_calls += 1 cash_trades_by_date = defaultdict(list) for _, cash_payments in backtest.cash_payments.items(): for cp in cash_payments: # only calc if additional point is required trades = cp.trade.all_instruments if isinstance( cp.trade, Portfolio) else [cp.trade] for trade in trades: if cp.effective_date and cp.effective_date <= end: if cp.effective_date not in backtest.results or \ trade not in backtest.results[cp.effective_date]: cash_trades_by_date[cp.effective_date].append( trade) else: cp.scale_date = None for cash_date, trades in cash_trades_by_date.items(): with PricingContext(cash_date): backtest.calculations += len(risks) cash_results[cash_date] = Portfolio(trades).calc( price_risk) # handle cash current_value = None for d in sorted( set(strategy_pricing_dates + list(backtest.cash_payments.keys()))): if d <= end: if current_value is not None: backtest.cash_dict[d] = current_value if d in backtest.cash_payments: for cp in backtest.cash_payments[d]: trades = cp.trade.all_instruments if isinstance( cp.trade, Portfolio) else [cp.trade] for trade in trades: value = cash_results.get( cp.effective_date, {}).get(price_risk, {}).get(trade.name, {}) try: value = backtest.results[cp.effective_date][price_risk][trade.name] \ if value == {} else value except (KeyError, ValueError): raise RuntimeError( f'failed to get cash value for {trade.name} on ' f'{cp.effective_date} received value of {value}' ) if not isinstance(value, float): raise RuntimeError( f'failed to get cash value for {trade.name} on ' f'{cp.effective_date} received value of {value}' ) ccy = next(iter(value.unit)) if d not in backtest.cash_dict: backtest.cash_dict[d] = {ccy: initial_value} if ccy not in backtest.cash_dict[d]: backtest.cash_dict[d][ccy] = 0 if cp.scale_date: scale_notional = backtest.portfolio_dict[ cp.scale_date][ cp.trade.name].notional_amount scale_date_adj = scale_notional / cp.trade.notional_amount cp.cash_paid = value * scale_date_adj * cp.direction backtest.cash_dict[d][ccy] += cp.cash_paid else: cp.cash_paid = ( 0 if cp.cash_paid is None else cp.cash_paid) + value * cp.direction backtest.cash_dict[d][ccy] += cp.cash_paid current_value = copy.deepcopy(backtest.cash_dict[d]) logging.info(f'Finished Backtest:- {dt.datetime.now()}') return backtest
def apply_action( self, state: Union[date, Iterable[date]], backtest: BackTest, trigger_info: Optional[Union[ExitTradeActionInfo, Iterable[ExitTradeActionInfo]]] = None): for s in make_list(state): trades_to_remove = [] fut_dates = list( filter(lambda d: d >= s and type(d) is dt.date, backtest.states)) for port_date in fut_dates: pos_fut = list( backtest.portfolio_dict[port_date].all_instruments) # We expect tradable names to be defined as <ActionName>_<TradeName>_<TradeDate> if self.action.priceable_names: # List of trade names provided -> TradeDate <= exit trigger date and TradeName is present in list indexes_to_remove = [ i for i, x in enumerate(pos_fut) if dt.datetime.strptime( x.name.split('_')[-1], '%Y-%m-%d').date() <= s and x.name.split('_')[-2] in self.action.priceable_names ] else: # List of trade names not provided -> TradeDate <= exit trigger date indexes_to_remove = [ i for i, x in enumerate(pos_fut) if dt.datetime.strptime( x.name.split('_')[-1], '%Y-%m-%d').date() <= s ] for index in sorted(indexes_to_remove, reverse=True): # Get list of trades' names that have been removed to check for their future cash flow date if pos_fut[index].name not in trades_to_remove: trades_to_remove.append(pos_fut[index].name) del pos_fut[index] backtest.portfolio_dict[port_date] = Portfolio(tuple(pos_fut)) for cp_date, cp_list in list(backtest.cash_payments.items()): if cp_date > s: indexes_to_remove = [ i for i, cp in enumerate(cp_list) if cp.trade.name in trades_to_remove ] for index in sorted(indexes_to_remove, reverse=True): cp = cp_list[index] prev_pos = [ i for i, x in enumerate(backtest.cash_payments[s]) if cp.trade.name == x.trade.name ] # If trade already exists in exit trigger date cash payments, net out the position if prev_pos: backtest.cash_payments[s][ prev_pos[0]].direction += cp.direction else: cp.effective_date = s backtest.cash_payments[s].append(cp) del backtest.cash_payments[cp_date][index] if not backtest.cash_payments[cp_date]: del backtest.cash_payments[cp_date] return backtest
def run_backtest(self, strategy, start=None, end=None, frequency='BM', window=None, states=None, risks=Price, show_progress=True): dates = pd.date_range(start=start, end=end, freq=frequency).date.tolist() risks = make_list(risks) + strategy.risks backtest = BackTest(strategy, dates, risks) if len(strategy.initial_portfolio): init_port = Portfolio(strategy.initial_portfolio) with PricingContext(pricing_date=dates[0]): init_port.resolve() for d in dates: backtest.portfolio_dict[d].append(init_port.instruments) for trigger in strategy.triggers: if trigger.calc_type != CalcType.path_dependent: triggered_dates = [ d for d in dates if trigger.has_triggered(d, backtest) ] for action in trigger.actions: if action.calc_type != CalcType.path_dependent: self.get_action_handler(action).apply_action( triggered_dates, backtest) with PricingContext(is_batch=True, show_progress=show_progress): for day, portfolio in backtest.portfolio_dict.items(): with PricingContext(day): backtest.calc_calls += 1 backtest.calculations += len(portfolio) * len(risks) backtest.add_results(day, portfolio.calc(tuple(risks))) # semi path dependent initial calc for _, scaling_list in backtest.scaling_portfolios.items(): for p in scaling_list: with HistoricalPricingContext(dates=p.dates): backtest.calc_calls += 1 backtest.calculations += len(p.dates) * len(risks) p.results = Portfolio([p.trade]).calc(tuple(risks)) for d in dates: # semi path dependent scaling if d in backtest.scaling_portfolios: initial_portfolio = backtest.scaling_portfolios[d][0] scale_date = initial_portfolio.dates[0] current_risk = backtest.results[scale_date][ initial_portfolio.risk].aggregate() hedge_risk = initial_portfolio.results[scale_date][ initial_portfolio.risk][0] scaling_factor = current_risk / hedge_risk for p in backtest.scaling_portfolios[d]: new_notional = p.trade.notional_amount * -scaling_factor scaled_trade = p.trade.as_dict() scaled_trade['notional_amount'] = new_notional scaled_trade = Instrument.from_dict(scaled_trade) for day in p.dates: backtest.add_results(day, p.results[day] * -scaling_factor) backtest.portfolio_dict[day] += Portfolio(scaled_trade) # path dependent for trigger in strategy.triggers: if trigger.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): for action in trigger.actions: self.get_action_handler(action).apply_action( d, backtest) else: for action in trigger.actions: if action.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): self.get_action_handler(action).apply_action( d, backtest) return backtest
def __init__(self, trigger_requirements: Optional[TriggerRequirements], actions: Union[Action, Iterable[Action]]): self._trigger_requirements = trigger_requirements self._actions = make_list(actions) self._risks = [x.risk for x in self.actions if x.risk is not None] self._deterministic = True
def run_backtest(self, strategy, start=None, end=None, frequency='BM', states=None, risks=Price, show_progress=True, csa_term=None): """ run the backtest following the triggers and actions defined in the strategy. If states are entered run on those dates otherwise build a schedule from the start, end, frequency using pd.date_range :param strategy: the strategy object :param start: a datetime :param end: a datetime :param frequency: str or DateOffset, default 'D'. Frequency strings can have multiples, e.g. '5H'. :param window: not used yet - intended for running a strategy over a series of potentially overlapping dates :param states: a list of dates will override the start, end, freq if provided :param risks: risks to run :param show_progress: boolean default true :param csa_term: the csa term to use :return: a backtest object containing the portfolios on each day and results which show all risks on all days """ dates = pd.date_range( start=start, end=end, freq=frequency).date.tolist() if states is None else states risks = make_list(risks) + strategy.risks backtest = BackTest(strategy, dates, risks) if len(strategy.initial_portfolio): init_port = Portfolio(strategy.initial_portfolio) with PricingContext(pricing_date=dates[0], csa_term=csa_term): init_port.resolve() for d in dates: backtest.portfolio_dict[d].append(init_port.instruments) for trigger in strategy.triggers: if trigger.calc_type != CalcType.path_dependent: triggered_dates = [] trigger_infos = defaultdict(list) for d in dates: t_info = trigger.has_triggered(d, backtest) if t_info: triggered_dates.append(d) if t_info.info_dict: for k, v in t_info.info_dict.items(): trigger_infos[k].append(v) for action in trigger.actions: if action.calc_type != CalcType.path_dependent: self.get_action_handler(action).apply_action( triggered_dates, backtest, trigger_infos[type(action)] if type(action) in trigger_infos else None) with PricingContext(is_batch=True, show_progress=show_progress, csa_term=csa_term): for day, portfolio in backtest.portfolio_dict.items(): with PricingContext(day): backtest.calc_calls += 1 backtest.calculations += len(portfolio) * len(risks) backtest.add_results(day, portfolio.calc(tuple(risks))) # semi path dependent initial calc for _, scaling_list in backtest.scaling_portfolios.items(): for p in scaling_list: with HistoricalPricingContext(dates=p.dates): backtest.calc_calls += 1 backtest.calculations += len(p.dates) * len(risks) p.results = Portfolio([p.trade]).calc(tuple(risks)) for d in dates: # semi path dependent scaling if d in backtest.scaling_portfolios: for p in backtest.scaling_portfolios[d]: current_risk = backtest.results[d][p.risk].aggregate( allow_mismatch_risk_keys=True) hedge_risk = p.results[d][p.risk].aggregate() scaling_factor = current_risk / hedge_risk new_notional = p.trade.notional_amount * -scaling_factor scaled_trade = p.trade.as_dict() scaled_trade['notional_amount'] = new_notional scaled_trade = Instrument.from_dict(scaled_trade) scaled_trade.name = p.trade.name for day in p.dates: backtest.add_results(day, p.results[day] * -scaling_factor) backtest.portfolio_dict[day] += Portfolio(scaled_trade) # path dependent for trigger in strategy.triggers: if trigger.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): for action in trigger.actions: self.get_action_handler(action).apply_action( d, backtest) else: for action in trigger.actions: if action.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): self.get_action_handler(action).apply_action( d, backtest) # run any additional calcs to handle cash scaling (e.g. unwinds) cash_results = defaultdict(list) with PricingContext(is_batch=True, csa_term=csa_term): for _, cash_payments in backtest.cash_payments.items(): for cp in cash_payments: # only calc if additional point is required if cp.effective_date and cp.effective_date <= end and \ cp.trade not in backtest.results[cp.effective_date]: with PricingContext(cp.effective_date): backtest.calc_calls += 1 backtest.calculations += len(risks) cash_results[cp.effective_date].append( Portfolio([cp.trade]).calc(tuple(risks))) # add cash to risk results for day, risk_results in cash_results.items(): for rr in risk_results: backtest.add_results(day, rr) # handle cash for day, cash_payments in backtest.cash_payments.items(): if day <= end: for cp in cash_payments: if cp.scale_date: scale_notional = backtest.portfolio_dict[ cp.scale_date][cp.trade.name].notional_amount scale_date_adj = scale_notional / cp.trade.notional_amount backtest.cash_dict[cp.effective_date] += \ backtest.results[cp.effective_date][Price][cp.trade] * scale_date_adj * cp.direction else: backtest.cash_dict[day] += backtest.results[day][ Price][cp.trade] * cp.direction return backtest
def run_backtest(self, strategy, start=None, end=None, frequency='1m', states=None, risks=Price, show_progress=True, csa_term=None, visible_to_gs=False, initial_value=0, result_ccy=None, holiday_calendar=None): """ run the backtest following the triggers and actions defined in the strategy. If states are entered run on those dates otherwise build a schedule from the start, end, frequency using gs_quant.datetime.relative_date.RelativeDateSchedule :param strategy: the strategy object :param start: a datetime :param end: a datetime :param frequency: str, default '1m' :param states: a list of dates will override the start, end, freq if provided :param risks: risks to run :param show_progress: boolean default true :param csa_term: the csa term to use :param visible_to_gs: are the contents of risk requests visible to GS (defaults to False) :param initial_value: initial cash value of strategy defaults to 0 :param result_ccy: ccy of all risks, pvs and cash :param holiday_calendar for date maths - list of dates :return: a backtest object containing the portfolios on each day and results which show all risks on all days """ logging.info(f'Starting Backtest: Building Date Schedule - {dt.datetime.now()}') dates = RelativeDateSchedule(frequency, start, end).apply_rule(holiday_calendar=holiday_calendar) if states is None else states risks = list(set(make_list(risks) + strategy.risks)) if result_ccy is not None: risks = [(r(currency=result_ccy) if isinstance(r, ParameterisedRiskMeasure) else raiser(f'Unparameterised risk: {r}')) for r in risks] price_risk = Price(currency=result_ccy) if result_ccy is not None else Price backtest = BackTest(strategy, dates, risks) logging.info('Resolving Initial Portfolio') if len(strategy.initial_portfolio): init_port = Portfolio(strategy.initial_portfolio) with PricingContext(pricing_date=dates[0], csa_term=csa_term, show_progress=show_progress, visible_to_gs=visible_to_gs): init_port.resolve() for d in dates: backtest.portfolio_dict[d].append(init_port.instruments) logging.info('Building Simple and Semi-deterministic triggers and actions') for trigger in strategy.triggers: if trigger.calc_type != CalcType.path_dependent: triggered_dates = [] trigger_infos = defaultdict(list) for d in dates: t_info = trigger.has_triggered(d, backtest) if t_info: triggered_dates.append(d) if t_info.info_dict: for k, v in t_info.info_dict.items(): trigger_infos[k].append(v) for action in trigger.actions: if action.calc_type != CalcType.path_dependent: self.get_action_handler(action).apply_action(triggered_dates, backtest, trigger_infos[type(action)] if type(action) in trigger_infos else None) logging.info('Pricing Simple and Semi-deterministic triggers and actions') with PricingContext(is_batch=True, show_progress=show_progress, csa_term=csa_term, visible_to_gs=visible_to_gs): backtest.calc_calls += 1 for day, portfolio in backtest.portfolio_dict.items(): if isinstance(day, dt.date): with PricingContext(day): backtest.calculations += len(portfolio) * len(risks) backtest.add_results(day, portfolio.calc(tuple(risks))) # semi path dependent initial calc for _, scaling_list in backtest.scaling_portfolios.items(): for p in scaling_list: with HistoricalPricingContext(dates=p.dates): backtest.calculations += len(risks) * len(p.dates) p.results = Portfolio([p.trade]).calc(tuple(risks)) logging.info('Scaling Semi-deterministic Triggers and Actions and Calculating Path Dependent Triggers ' 'and Actions') for d in dates: # path dependent for trigger in strategy.triggers: if trigger.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): for action in trigger.actions: self.get_action_handler(action).apply_action(d, backtest) else: for action in trigger.actions: if action.calc_type == CalcType.path_dependent: if trigger.has_triggered(d, backtest): self.get_action_handler(action).apply_action(d, backtest) # test to see if new trades have been added and calc port = [] for t in backtest.portfolio_dict[d]: if t.name not in list(backtest.results[d].to_frame().index): port.append(t) with PricingContext(csa_term=csa_term, show_progress=show_progress, visible_to_gs=visible_to_gs): if len(port) > 0: with PricingContext(pricing_date=d): backtest.add_results(d, Portfolio(port).calc(tuple(risks))) for sp in backtest.scaling_portfolios[d]: if sp.results is None: with HistoricalPricingContext(dates=sp.dates): backtest.calculations += len(risks) * len(sp.dates) sp.results = Portfolio([sp.trade]).calc(tuple(risks)) # semi path dependent scaling if d in backtest.scaling_portfolios: for p in backtest.scaling_portfolios[d]: current_risk = backtest.results[d][p.risk].aggregate(allow_mismatch_risk_keys=True) hedge_risk = p.results[d][p.risk].aggregate() if current_risk.unit != hedge_risk.unit: raise RuntimeError('cannot hedge in a different currency') scaling_factor = current_risk / hedge_risk new_notional = getattr(p.trade, p.scaling_parameter) * -scaling_factor scaled_trade = p.trade.as_dict() scaled_trade[p.scaling_parameter] = new_notional scaled_trade = Instrument.from_dict(scaled_trade) scaled_trade.name = p.trade.name for day in p.dates: backtest.add_results(day, p.results[day] * -scaling_factor) backtest.portfolio_dict[day] += Portfolio(scaled_trade) logging.info('Calculate Cash') # run any additional calcs to handle cash scaling (e.g. unwinds) cash_calcs = defaultdict(list) with PricingContext(is_batch=True, show_progress=show_progress, csa_term=csa_term, visible_to_gs=visible_to_gs): backtest.calc_calls += 1 for _, cash_payments in backtest.cash_payments.items(): for cp in cash_payments: # only calc if additional point is required if cp.effective_date and cp.effective_date <= end: if cp.effective_date not in backtest.results or \ cp.trade not in backtest.results[cp.effective_date]: with PricingContext(cp.effective_date): backtest.calculations += len(risks) cash_calcs[cp.effective_date].append(Portfolio([cp.trade]).calc(price_risk)) else: cp.scale_date = None cash_results = {} for day, risk_results in cash_calcs.items(): if day <= end: cash_results[day] = risk_results[0] if len(risk_results) > 1: for i in range(1, len(risk_results)): cash_results[day] += risk_results[i] # handle cash current_value = None for d in sorted(set(dates + list(backtest.cash_payments.keys()))): if d <= end: if current_value is not None: backtest.cash_dict[d] = current_value if d in backtest.cash_payments: for cp in backtest.cash_payments[d]: value = cash_results.get(cp.effective_date, {}).get(price_risk, {}).get(cp.trade.name, {}) try: value = backtest.results[cp.effective_date][price_risk][cp.trade.name] \ if value == {} else value except ValueError: raise RuntimeError(f'failed to get cash value for {cp.trade.name} on ' f'{cp.effective_date} received value of {value}') if not isinstance(value, float): raise RuntimeError(f'failed to get cash value for {cp.trade.name} on ' f'{cp.effective_date} received value of {value}') ccy = next(iter(value.unit)) if d not in backtest.cash_dict: backtest.cash_dict[d] = {ccy: initial_value} if ccy not in backtest.cash_dict[d]: backtest.cash_dict[d][ccy] = 0 if cp.scale_date: scale_notional = backtest.portfolio_dict[cp.scale_date][cp.trade.name].notional_amount scale_date_adj = scale_notional / cp.trade.notional_amount cp.cash_paid = value * scale_date_adj * cp.direction backtest.cash_dict[d][ccy] += cp.cash_paid else: cp.cash_paid = value * cp.direction backtest.cash_dict[d][ccy] += cp.cash_paid current_value = backtest.cash_dict[d] logging.info(f'Finished Backtest:- {dt.datetime.now()}') return backtest