Beispiel #1
0
    def apply_action(self,
                     state: Union[date, Iterable[date]],
                     backtest: BackTest,
                     trigger_info: Optional[Union[HedgeActionInfo, Iterable[HedgeActionInfo]]] = None):
        with HistoricalPricingContext(dates=make_list(state), csa_term=self.action.csa_term, show_progress=True):
            backtest.calc_calls += 1
            backtest.calculations += len(make_list(state))
            f = Portfolio(make_list(self.action.priceable)).resolve(in_place=False)

        for create_date, portfolio in f.result().items():
            hedge = portfolio.instruments[0]
            hedge.name = f'{hedge.name}_{create_date.strftime("%Y-%m-%d")}'
            final_date = get_final_date(hedge, create_date, self.action.trade_duration)
            active_dates = [s for s in backtest.states if create_date <= s < final_date]

            if len(active_dates):
                backtest.scaling_portfolios[create_date].append(
                    ScalingPortfolio(trade=hedge, dates=active_dates, risk=self.action.risk,
                                     csa_term=self.action.csa_term, scaling_parameter=self.action.scaling_parameter))

                # add cashflows on trade entry and unwind
                backtest.cash_payments[create_date].append(
                    CashPayment(trade=hedge, effective_date=create_date, direction=-1))
                if final_date <= dt.date.today():
                    backtest.cash_payments[final_date].append(
                        CashPayment(trade=hedge, effective_date=final_date, scale_date=create_date))

        return backtest
    def apply_action(self, state: Union[date, Iterable[date]],
                     backtest: BackTest):
        with HistoricalPricingContext(dates=make_list(state),
                                      csa_term=self.action.csa_term):
            backtest.calc_calls += 1
            backtest.calculations += len(make_list(state))
            f = Portfolio(make_list(
                self.action.priceable)).resolve(in_place=False)

        for create_date, portfolio in f.result().items():
            active_dates = [
                s for s in backtest.states
                if get_final_date(portfolio.instruments[0], create_date, self.
                                  action.trade_duration) >= s >= create_date
            ]
            if len(active_dates):
                for t in portfolio:
                    t.name = f'{t.name}_{create_date.strftime("%Y-%m-%d")}'
                backtest.scaling_portfolios[create_date].append(
                    ScalingPortfolio(trade=portfolio.instruments[0],
                                     dates=active_dates,
                                     risk=self.action.risk,
                                     csa_term=self.action.csa_term))

        return backtest
    def _raise_order(
        self,
        state: Union[date, Iterable[date]],
        trigger_info: Optional[Union[AddTradeActionInfo,
                                     Iterable[AddTradeActionInfo]]] = None):
        with PricingContext(is_batch=True, show_progress=True):
            state_list = make_list(state)
            orders = {}
            if trigger_info is None or isinstance(trigger_info,
                                                  AddTradeActionInfo):
                trigger_info = [trigger_info for _ in range(len(state_list))]
            for s, ti in zip_longest(state_list, trigger_info):
                active_portfolio = self.action.dated_priceables.get(
                    s) or self.action.priceables
                with PricingContext(pricing_date=s):
                    orders[s] = (Portfolio(active_portfolio).resolve(
                        in_place=False), ti)
        final_orders = {}
        for d, p in orders.items():
            new_port = []
            for t in p[0].result():
                t.name = f'{t.name}_{d}'
                new_port.append(t)
            new_port = Portfolio(new_port)
            final_orders[d] = new_port.scale(
                None if p[1] is None else p[1].scaling, in_place=False)

        return final_orders
Beispiel #4
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
Beispiel #5
0
    def apply_action(
        self,
        state: Union[date, Iterable[date]],
        backtest: BackTest,
        trigger_info: Optional[Union[HedgeActionInfo,
                                     Iterable[HedgeActionInfo]]] = None):
        with HistoricalPricingContext(dates=make_list(state),
                                      csa_term=self.action.csa_term):
            backtest.calc_calls += 1
            backtest.calculations += len(make_list(state))
            f = Portfolio(make_list(
                self.action.priceable)).resolve(in_place=False)

        for create_date, portfolio in f.result().items():
            final_date = get_final_date(portfolio.instruments[0], create_date,
                                        self.action.trade_duration)
            if self.action.risks_on_final_day:
                next_state = list(
                    filter(lambda x: x > create_date, backtest.states))
                final_date = min(
                    final_date,
                    next_state[0]) if len(next_state) else final_date
            active_dates = [
                s for s in backtest.states if create_date <= s < final_date
            ]

            if len(active_dates):
                for t in portfolio:
                    t.name = f'{t.name}_{create_date.strftime("%Y-%m-%d")}'
                backtest.scaling_portfolios[create_date].append(
                    ScalingPortfolio(trade=portfolio.instruments[0],
                                     dates=active_dates,
                                     risk=self.action.risk,
                                     csa_term=self.action.csa_term))
                if self.action.risks_on_final_day and final_date <= dt.date.today(
                ):
                    backtest.scaling_portfolios[final_date].append(
                        ScalingPortfolio(trade=portfolio.instruments[0],
                                         dates=[final_date],
                                         risk=self.action.risk,
                                         csa_term=self.action.csa_term,
                                         unwind=True))

        return backtest
Beispiel #6
0
 def _raise_order(self,
                  state: Union[date, Iterable[date]],
                  trigger_info: Optional[Union[AddTradeActionInfo, Iterable[AddTradeActionInfo]]] = None):
     with PricingContext(is_batch=True):
         state_list = make_list(state)
         orders = {}
         if trigger_info is None or isinstance(trigger_info, AddTradeActionInfo):
             trigger_info = [trigger_info for _ in range(len(state_list))]
         for s, ti in zip_longest(state_list, trigger_info):
             active_portfolio = self.action.dated_priceables.get(s) or self.action.priceables
             with PricingContext(pricing_date=s):
                 orders[s] = (Portfolio(active_portfolio).resolve(in_place=False), ti)
     orders = {k: v[0].result().scale(None if v[1] is None else v[1].scaling, in_place=False) for k, v in
               orders.items()}
     return orders
Beispiel #7
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)

        for trigger in strategy.triggers:
            if trigger.deterministic:
                triggered_dates = [
                    date for date in dates
                    if trigger.has_triggered(date, backtest)
                ]
                for action in trigger.actions:
                    if action.deterministic:
                        action.apply_action(triggered_dates, backtest)

        with PricingContext(is_batch=True, show_progress=show_progress):
            for day, portfolio in backtest.portfolio_dict.items():
                with PricingContext(day):
                    backtest.calc_calls += 1
                    backtest.calculations += len(portfolio) * len(risks)
                    backtest.add_results(
                        day,
                        portfolio.calc(risks[0] if len(risks) ==
                                       1 else tuple(risks)))

        for trigger in strategy.triggers:
            if not trigger.deterministic:
                for date in dates:
                    if trigger.has_triggered(date, backtest):
                        for action in trigger.actions:
                            action.apply_action(date, backtest)
            else:
                for action in trigger.actions:
                    if not action.deterministic:
                        for date in dates:
                            if trigger.has_triggered(date, backtest):
                                action.apply_action(date, backtest)
        return backtest
Beispiel #8
0
 def __init__(self, trigger_requirements: Optional[TriggerRequirements],
              actions: Union[Action, Iterable[Action]]):
     self._trigger_requirements = trigger_requirements
     self._actions = make_list(actions)
     self._risks = [x.risk for x in self.actions if x.risk is not None]
     self._calc_type = CalcType.simple
    def run_backtest(self,
                     strategy,
                     start=None,
                     end=None,
                     frequency='1m',
                     states=None,
                     risks=Price,
                     show_progress=True,
                     csa_term=None,
                     visible_to_gs=False,
                     initial_value=0,
                     result_ccy=None,
                     holiday_calendar=None):
        """
        run the backtest following the triggers and actions defined in the strategy.  If states are entered run on
        those dates otherwise build a schedule from the start, end, frequency
        using gs_quant.datetime.relative_date.RelativeDateSchedule
        :param strategy: the strategy object
        :param start: a datetime
        :param end: a datetime
        :param frequency: str, default '1m'
        :param states: a list of dates will override the start, end, freq if provided
        :param risks: risks to run
        :param show_progress: boolean default true
        :param csa_term: the csa term to use
        :param visible_to_gs: are the contents of risk requests visible to GS (defaults to False)
        :param initial_value: initial cash value of strategy defaults to 0
        :param result_ccy: ccy of all risks, pvs and cash
        :param holiday_calendar for date maths - list of dates
        :return: a backtest object containing the portfolios on each day and results which show all risks on all days

        """

        logging.info(
            f'Starting Backtest: Building Date Schedule - {dt.datetime.now()}')

        strategy_pricing_dates = RelativeDateSchedule(frequency,
                                                      start,
                                                      end).apply_rule(holiday_calendar=holiday_calendar) \
            if states is None else states

        strategy_start_date = strategy_pricing_dates[0]
        strategy_end_date = strategy_pricing_dates[-1]

        risks = list(set(make_list(risks) + strategy.risks))
        if result_ccy is not None:
            risks = [(r(
                currency=result_ccy) if isinstance(r, ParameterisedRiskMeasure)
                      else raiser(f'Unparameterised risk: {r}'))
                     for r in risks]
        price_risk = Price(
            currency=result_ccy) if result_ccy is not None else Price

        backtest = BackTest(strategy, strategy_pricing_dates, risks)

        logging.info('Resolving initial portfolio')
        if len(strategy.initial_portfolio):
            for index in range(len(strategy.initial_portfolio)):
                old_name = strategy.initial_portfolio[index].name
                strategy.initial_portfolio[
                    index].name = f'{old_name}_{strategy_start_date.strftime("%Y-%m-%d")}'
                entry_payment = CashPayment(strategy.initial_portfolio[index],
                                            effective_date=strategy_start_date,
                                            direction=-1)
                backtest.cash_payments[strategy_start_date].append(
                    entry_payment)
                final_date = get_final_date(strategy.initial_portfolio[index],
                                            strategy_start_date, None)
                exit_payment = CashPayment(strategy.initial_portfolio[index],
                                           effective_date=final_date)
                backtest.cash_payments[final_date].append(exit_payment)
            init_port = Portfolio(strategy.initial_portfolio)
            with PricingContext(pricing_date=strategy_start_date,
                                csa_term=csa_term,
                                show_progress=show_progress,
                                visible_to_gs=visible_to_gs):
                init_port.resolve()
            for d in strategy_pricing_dates:
                backtest.portfolio_dict[d].append(init_port.instruments)

        logging.info(
            'Building simple and semi-deterministic triggers and actions')
        for trigger in strategy.triggers:
            if trigger.calc_type != CalcType.path_dependent:
                triggered_dates = []
                trigger_infos = defaultdict(list)
                for d in strategy_pricing_dates:
                    t_info = trigger.has_triggered(d, backtest)
                    if t_info:
                        triggered_dates.append(d)
                        if t_info.info_dict:
                            for k, v in t_info.info_dict.items():
                                trigger_infos[k].append(v)

                for action in trigger.actions:
                    if action.calc_type != CalcType.path_dependent:
                        self.get_action_handler(action).apply_action(
                            triggered_dates, backtest,
                            trigger_infos[type(action)]
                            if type(action) in trigger_infos else None)

        logging.info(
            f'Filtering strategy calculations to run from {strategy_start_date} to {strategy_end_date}'
        )
        backtest.portfolio_dict = defaultdict(
            Portfolio, {
                k: backtest.portfolio_dict[k]
                for k in backtest.portfolio_dict
                if strategy_start_date <= k <= strategy_end_date
            })
        backtest.scaling_portfolios = defaultdict(
            list, {
                k: backtest.scaling_portfolios[k]
                for k in backtest.scaling_portfolios
                if strategy_start_date <= k <= strategy_end_date
            })

        logging.info(
            'Pricing simple and semi-deterministic triggers and actions')
        with PricingContext(is_batch=True,
                            show_progress=show_progress,
                            csa_term=csa_term,
                            visible_to_gs=visible_to_gs):
            backtest.calc_calls += 1
            for day, portfolio in backtest.portfolio_dict.items():
                if isinstance(day, dt.date):
                    with PricingContext(day):
                        backtest.calculations += len(portfolio) * len(risks)
                        backtest.add_results(day, portfolio.calc(tuple(risks)))

            # semi path dependent initial calc
            for _, scaling_list in backtest.scaling_portfolios.items():
                for p in scaling_list:
                    with HistoricalPricingContext(dates=p.dates):
                        backtest.calculations += len(risks) * len(p.dates)
                        port = p.trade if isinstance(
                            p.trade, Portfolio) else Portfolio([p.trade])
                        p.results = port.calc(tuple(risks))

        logging.info(
            'Scaling semi-deterministic triggers and actions and calculating path dependent triggers '
            'and actions')
        for d in strategy_pricing_dates:
            logging.info(f'{d}: Processing triggers and actions')
            # path dependent
            for trigger in strategy.triggers:
                if trigger.calc_type == CalcType.path_dependent:
                    if trigger.has_triggered(d, backtest):
                        for action in trigger.actions:
                            self.get_action_handler(action).apply_action(
                                d, backtest)
                else:
                    for action in trigger.actions:
                        if action.calc_type == CalcType.path_dependent:
                            if trigger.has_triggered(d, backtest):
                                self.get_action_handler(action).apply_action(
                                    d, backtest)
            # test to see if new trades have been added and calc
            port = []
            for t in backtest.portfolio_dict[d]:
                if t.name not in list(backtest.results[d].to_frame().index):
                    port.append(t)

            with PricingContext(is_batch=True,
                                csa_term=csa_term,
                                show_progress=show_progress,
                                visible_to_gs=visible_to_gs):
                if len(port):
                    with PricingContext(pricing_date=d):
                        results = Portfolio(port).calc(tuple(risks))

                for sp in backtest.scaling_portfolios[d]:
                    if sp.results is None:
                        with HistoricalPricingContext(dates=sp.dates):
                            backtest.calculations += len(risks) * len(sp.dates)
                            port_sp = sp.trade if isinstance(
                                sp.trade, Portfolio) else Portfolio([sp.trade])
                            sp.results = port_sp.calc(tuple(risks))

            # results should be added outside of pricing context and not in the same call as valuating them
            if len(port):
                backtest.add_results(d, results)

            # semi path dependent scaling
            if d in backtest.scaling_portfolios:
                for p in backtest.scaling_portfolios[d]:
                    current_risk = backtest.results[d][p.risk].aggregate(
                        allow_mismatch_risk_keys=True)
                    hedge_risk = p.results[d][p.risk].aggregate()
                    if current_risk.unit != hedge_risk.unit:
                        raise RuntimeError(
                            'cannot hedge in a different currency')
                    scaling_factor = current_risk / hedge_risk
                    if isinstance(p.trade, Portfolio):
                        # Scale the portfolio by risk target
                        scaled_portfolio_position = copy.deepcopy(p.trade)
                        scaled_portfolio_position.name = f'Scaled_{scaled_portfolio_position.name}'
                        for instrument in scaled_portfolio_position.all_instruments:
                            instrument.name = f'Scaled_{instrument.name}'

                        # trade hedge in opposite direction
                        scale_direction = -1
                        scaled_portfolio_position.scale(scaling_factor *
                                                        scale_direction)

                        for day in p.dates:
                            # add scaled hedge position to portfolio for day. NOTE this adds leaves, not the portfolio
                            backtest.portfolio_dict[day] += copy.deepcopy(
                                scaled_portfolio_position)

                        # now apply scaled portfolio to cash payments
                        for d, payments in backtest.cash_payments.items():
                            for payment in payments:
                                if payment.trade == p.trade:
                                    payment.trade = copy.deepcopy(
                                        scaled_portfolio_position)
                                    payment.scale_date = None

                    else:
                        new_notional = getattr(
                            p.trade, p.scaling_parameter) * -scaling_factor
                        scaled_trade = p.trade.as_dict()
                        scaled_trade[p.scaling_parameter] = new_notional
                        scaled_trade = Instrument.from_dict(scaled_trade)
                        scaled_trade.name = p.trade.name
                        for day in p.dates:
                            backtest.add_results(
                                day, p.results[day] * -scaling_factor)
                            backtest.portfolio_dict[day] += Portfolio(
                                scaled_trade)

        logging.info('Calculating and scaling newly added portfolio positions')
        # test to see if new trades have been added and calc
        with PricingContext(is_batch=True,
                            show_progress=show_progress,
                            csa_term=csa_term,
                            visible_to_gs=visible_to_gs):
            backtest.calc_calls += 1
            leaves_by_date = {}
            for day, portfolio in backtest.portfolio_dict.items():
                results_for_date = backtest.results[day]
                if len(results_for_date) == 0:
                    continue

                trades_for_date = list(results_for_date.to_frame().index)
                leaves = []
                for leaf in portfolio:
                    if leaf.name not in trades_for_date:
                        logging.info(
                            f'{day}: new portfolio position {leaf} scheduled for calculation'
                        )
                        leaves.append(leaf)

                if len(leaves):
                    with PricingContext(pricing_date=day):
                        leaves_by_date[day] = Portfolio(leaves).calc(
                            tuple(risks))
                        backtest.calculations += len(leaves) * len(risks)

        logging.info('Processing results for newly added portfolio positions')
        for day, leaves in leaves_by_date.items():
            backtest.add_results(day, leaves)

        logging.info('Calculating prices for cash payments')
        # run any additional calcs to handle cash scaling (e.g. unwinds)
        cash_results = {}
        with PricingContext(is_batch=True,
                            show_progress=show_progress,
                            csa_term=csa_term,
                            visible_to_gs=visible_to_gs):
            backtest.calc_calls += 1
            cash_trades_by_date = defaultdict(list)
            for _, cash_payments in backtest.cash_payments.items():
                for cp in cash_payments:
                    # only calc if additional point is required
                    trades = cp.trade.all_instruments if isinstance(
                        cp.trade, Portfolio) else [cp.trade]
                    for trade in trades:
                        if cp.effective_date and cp.effective_date <= end:
                            if cp.effective_date not in backtest.results or \
                                    trade not in backtest.results[cp.effective_date]:
                                cash_trades_by_date[cp.effective_date].append(
                                    trade)
                            else:
                                cp.scale_date = None

            for cash_date, trades in cash_trades_by_date.items():
                with PricingContext(cash_date):
                    backtest.calculations += len(risks)
                    cash_results[cash_date] = Portfolio(trades).calc(
                        price_risk)

        # handle cash
        current_value = None
        for d in sorted(
                set(strategy_pricing_dates +
                    list(backtest.cash_payments.keys()))):
            if d <= end:
                if current_value is not None:
                    backtest.cash_dict[d] = current_value
                if d in backtest.cash_payments:
                    for cp in backtest.cash_payments[d]:
                        trades = cp.trade.all_instruments if isinstance(
                            cp.trade, Portfolio) else [cp.trade]
                        for trade in trades:
                            value = cash_results.get(
                                cp.effective_date,
                                {}).get(price_risk, {}).get(trade.name, {})
                            try:
                                value = backtest.results[cp.effective_date][price_risk][trade.name] \
                                    if value == {} else value
                            except (KeyError, ValueError):
                                raise RuntimeError(
                                    f'failed to get cash value for {trade.name} on '
                                    f'{cp.effective_date} received value of {value}'
                                )
                            if not isinstance(value, float):
                                raise RuntimeError(
                                    f'failed to get cash value for {trade.name} on '
                                    f'{cp.effective_date} received value of {value}'
                                )
                            ccy = next(iter(value.unit))
                            if d not in backtest.cash_dict:
                                backtest.cash_dict[d] = {ccy: initial_value}
                            if ccy not in backtest.cash_dict[d]:
                                backtest.cash_dict[d][ccy] = 0
                            if cp.scale_date:
                                scale_notional = backtest.portfolio_dict[
                                    cp.scale_date][
                                        cp.trade.name].notional_amount
                                scale_date_adj = scale_notional / cp.trade.notional_amount
                                cp.cash_paid = value * scale_date_adj * cp.direction
                                backtest.cash_dict[d][ccy] += cp.cash_paid
                            else:
                                cp.cash_paid = (
                                    0 if cp.cash_paid is None else
                                    cp.cash_paid) + value * cp.direction
                                backtest.cash_dict[d][ccy] += cp.cash_paid

                current_value = copy.deepcopy(backtest.cash_dict[d])

        logging.info(f'Finished Backtest:- {dt.datetime.now()}')
        return backtest
Beispiel #10
0
    def apply_action(
        self,
        state: Union[date, Iterable[date]],
        backtest: BackTest,
        trigger_info: Optional[Union[ExitTradeActionInfo,
                                     Iterable[ExitTradeActionInfo]]] = None):

        for s in make_list(state):

            trades_to_remove = []

            fut_dates = list(
                filter(lambda d: d >= s and type(d) is dt.date,
                       backtest.states))
            for port_date in fut_dates:
                pos_fut = list(
                    backtest.portfolio_dict[port_date].all_instruments)

                # We expect tradable names to be defined as <ActionName>_<TradeName>_<TradeDate>
                if self.action.priceable_names:
                    # List of trade names provided -> TradeDate <= exit trigger date and TradeName is present in list
                    indexes_to_remove = [
                        i for i, x in enumerate(pos_fut)
                        if dt.datetime.strptime(
                            x.name.split('_')[-1], '%Y-%m-%d').date() <= s and
                        x.name.split('_')[-2] in self.action.priceable_names
                    ]
                else:
                    # List of trade names not provided -> TradeDate <= exit trigger date
                    indexes_to_remove = [
                        i for i, x in enumerate(pos_fut)
                        if dt.datetime.strptime(
                            x.name.split('_')[-1], '%Y-%m-%d').date() <= s
                    ]

                for index in sorted(indexes_to_remove, reverse=True):
                    # Get list of trades' names that have been removed to check for their future cash flow date
                    if pos_fut[index].name not in trades_to_remove:
                        trades_to_remove.append(pos_fut[index].name)
                    del pos_fut[index]
                backtest.portfolio_dict[port_date] = Portfolio(tuple(pos_fut))

            for cp_date, cp_list in list(backtest.cash_payments.items()):
                if cp_date > s:
                    indexes_to_remove = [
                        i for i, cp in enumerate(cp_list)
                        if cp.trade.name in trades_to_remove
                    ]
                    for index in sorted(indexes_to_remove, reverse=True):
                        cp = cp_list[index]
                        prev_pos = [
                            i for i, x in enumerate(backtest.cash_payments[s])
                            if cp.trade.name == x.trade.name
                        ]
                        # If trade already exists in exit trigger date cash payments, net out the position
                        if prev_pos:
                            backtest.cash_payments[s][
                                prev_pos[0]].direction += cp.direction
                        else:
                            cp.effective_date = s
                            backtest.cash_payments[s].append(cp)
                        del backtest.cash_payments[cp_date][index]

                    if not backtest.cash_payments[cp_date]:
                        del backtest.cash_payments[cp_date]

        return backtest
Beispiel #11
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
Beispiel #12
0
 def __init__(self, trigger_requirements: Optional[TriggerRequirements], actions: Union[Action, Iterable[Action]]):
     self._trigger_requirements = trigger_requirements
     self._actions = make_list(actions)
     self._risks = [x.risk for x in self.actions if x.risk is not None]
     self._deterministic = True
Beispiel #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
Beispiel #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()}')

        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