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
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
    def get_or_create_asset_from_instrument(instrument: Instrument) -> str:
        asset = GsAsset(asset_class=instrument.asset_class,
                        type_=instrument.type,
                        name=instrument.name or '',
                        parameters=instrument.as_dict(as_camel_case=True))

        results = GsSession.current._post('/assets', asset)
        return results['id']
Example #5
0
    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
Example #6
0
    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'])
Example #7
0
    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)
Example #8
0
    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)
Example #9
0
    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)
Example #10
0
def get_instrument(instr_type, instr_class=None, instr_map=None, tdapi=False):
    if instr_map is None:
        instr_map = {}
    if tdapi:
        if instr_type in instr_map:
            instr = instr_map[instr_type][0]
            attri = instr_map[instr_type][1]
        else:
            instr = Instrument().from_dict({'$type': instr_type})
            attri = instr.properties()
            instr_map[instr_type] = (instr, attri)
    else:
        if (instr_class, instr_type) in instr_map:
            instr = instr_map[(instr_class, instr_type)][0]
            attri = instr_map[(instr_class, instr_type)][1]
        else:
            instr = Instrument().from_dict({
                'asset_class': instr_class,
                'type': instr_type
            })
            attri = instr.properties()
            instr_map[(instr_class, instr_type)] = (instr, attri)
    return [instr, attri]
Example #11
0
    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
Example #13
0
    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
Example #14
0
    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
Example #15
0
    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
Example #16
0
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
Example #17
0
def decode_instrument(value: Optional[dict]):
    from gs_quant.instrument import Instrument
    return Instrument.from_dict(value) if value else None