def test_from_dict(): swap = IRSwap('Receive', '3m', 'USD', fixedRate=0, notionalAmount=1) properties = swap.as_dict() new_swap = Instrument.from_dict(properties) assert swap == new_swap # setting a datetime.date should work in a dictionary swap = IRSwap('Receive', dt.date(2030, 4, 11), 'USD', fixedRate='atm+5', notionalAmount=1) properties = swap.as_dict() new_swap = Instrument.from_dict(properties) assert swap == new_swap
def from_frame(cls, data: pd.DataFrame, mappings: dict = None): def get_value(this_row: pd.Series, attribute: str): value = mappings.get(attribute, attribute) return value(this_row) if callable(value) else this_row.get(value) instruments = [] mappings = mappings or {} data = data.replace({np.nan: None}) for row in (r for _, r in data.iterrows() if any(v for v in r.values if v is not None)): instrument = None for init_keys in (('asset_class', 'type'), ('$type', )): init_values = tuple( filter(None, (get_value(row, k) for k in init_keys))) if len(init_keys) == len(init_values): instrument = Instrument.from_dict( dict(zip(init_keys, init_values))) instrument = instrument.from_dict({ p: get_value(row, p) for p in instrument.properties() }) break if instrument: instruments.append(instrument) else: raise ValueError( 'Neither asset_class/type nor $type specified') return cls(instruments)
def from_frame( cls, data: pd.DataFrame, mappings: dict = None, date_formats: list = None, ): trade_list = [] attribute_map = {} mappings = mappings or {} data = data.replace({np.nan: None}) for index, row in data.iterrows(): if is_empty(row): continue try: instrument_type = get_value(row, mappings, 'type') asset_class = get_value(row, mappings, 'asset_class') except ValueError: instrument_type = '' asset_class = '' if 'tdapi' in str(instrument_type): inputs = {'$type': instrument_type[6:]} [instrument, attributes] = get_instrument(instrument_type[6:], instr_map=attribute_map, tdapi=True) else: inputs = {'asset_class': asset_class, 'type': instrument_type} [instrument, attributes] = get_instrument(instrument_type, instr_class=asset_class, instr_map=attribute_map) for attribute in attributes: if attribute == 'type' or attribute == 'asset_class': continue additional = [] prop_type = instrument.prop_type(attribute, additional) additional.append(prop_type) if prop_type is dt.date: value = get_date(row, mappings, attribute, date_formats) else: value = get_value(row, mappings, attribute) if value is not None and type(value) not in (float, int): if float in additional: value = string_to_float(value) if value is not None: if isinstance(value, str): value.strip(' ') inputs[attribute] = value trade = Instrument.from_dict(inputs) trade_list.append(trade) return cls(trade_list)
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 get_instruments_by_position_type( cls, positions_type: str, positions_id: str) -> Tuple[Instrument, ...]: root = 'deals' if positions_type == 'ETI' else 'books/' + positions_type url = '/risk-internal/{}/{}/positions'.format(root, positions_id) with GsSession.get(Environment.QA) as session: # TODO Remove this once in prod results = session._get(url) return tuple( Instrument.from_dict(p['instrument']) for p in results.get('positionSets', ({ 'positions': () }, ))[0]['positions'])
def get_instruments_by_position_type(cls, positions_type: str, positions_id: str) -> Tuple[Instrument, ...]: root = 'deals' if positions_type == 'ETI' else 'books/' + positions_type url = '/risk-internal/{}/{}/positions'.format(root, positions_id) results = GsSession.current._get(url, timeout=181) instruments = [] for position in results.get('positionSets', ({'positions': ()},))[0]['positions']: instrument_values = position['instrument'] instrument = Instrument.from_dict(instrument_values) name = instrument_values.get('name') if name: instrument.name = name instruments.append(instrument) return tuple(instruments)
def get_instruments_by_workflow_id(cls, workflow_id: str, preferInstruments: bool = False) -> Tuple[Instrument, ...]: root = 'quote' url = '/risk{}/{}/{}'.format('-internal' if not preferInstruments else '', root, workflow_id) results = GsSession.current._get(url, timeout=181) instruments = [] for position in results.get('workflowPositions').get(workflow_id)[0]['positions']: instrument_values = position['instrument'] instrument = Instrument.from_dict(instrument_values) name = instrument_values.get('name') if name: instrument.name = name instruments.append(instrument) return tuple(instruments)
def get_instruments_by_position_type( cls, positions_type: str, positions_id: str) -> Tuple[Instrument, ...]: root = 'deals' if positions_type == 'ETI' else 'books/' + positions_type url = '/risk-internal/{}/{}/positions'.format(root, positions_id) with GsSession.get(Environment.QA) as session: # TODO Remove this once in prod results = session._get(url) instruments = [] for position in results.get('positionSets', ({ 'positions': () }, ))[0]['positions']: instrument_values = position['instrument'] instrument = Instrument.from_dict(instrument_values) name = instrument_values.get('name') if name: instrument.name = name instruments.append(instrument) return tuple(instruments)
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 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 decode_instrument(value: Optional[dict]): from gs_quant.instrument import Instrument return Instrument.from_dict(value) if value else None
def scale_trade(inst, ratio): inst_dict = inst.as_dict() inst_dict['notional_amount'] = inst_dict['notional_amount'] * ratio new_inst = Instrument.from_dict(inst_dict) new_inst.name = inst.name return new_inst
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
def test_from_dict(): swap = IRSwap('Receive', '3m', 'USD', fixedRate=0, notionalAmount=1) properties = swap.as_dict() new_swap = Instrument.from_dict(properties) assert swap == new_swap