def apply_action( self, state: Union[date, Iterable[date]], backtest: BackTest, trigger_info: Optional[Union[AddTradeActionInfo, Iterable[AddTradeActionInfo]]] = None): orders = self._raise_order(state, trigger_info) # record entry and unwind cashflows for create_date, portfolio in orders.items(): for inst in portfolio.all_instruments: backtest.cash_payments[create_date].append( CashPayment(inst, effective_date=create_date, direction=-1)) final_date = get_final_date(inst, create_date, self.action.trade_duration) backtest.cash_payments[final_date].append( CashPayment(inst, effective_date=final_date)) for s in backtest.states: pos = [] for create_date, portfolio in orders.items(): pos += [ inst for inst in portfolio.instruments if get_final_date(inst, create_date, self.action. trade_duration) > s >= create_date ] if len(pos): backtest.portfolio_dict[s].append(pos) 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, 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 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