From d0d75a6476c88cb8ddb96931602ef6a0635520b8 Mon Sep 17 00:00:00 2001 From: "Roberson, Martin [GBM Public]" Date: Tue, 9 Jul 2024 10:16:47 +0000 Subject: [PATCH] Chore: Make release 1.0.96 --- .../json_encoders/response_encoders.py | 2 +- gs_quant/api/gs/backtests_xasset/request.py | 2 +- .../generic_backtest_datatypes.py | 27 +++++++ gs_quant/api/gs/portfolios.py | 29 ++++---- gs_quant/backtests/strategy.py | 1 - gs_quant/backtests/strategy_systematic.py | 70 +++++++++++++------ .../backtests/strategy_systematic_factory.py | 53 -------------- 7 files changed, 91 insertions(+), 93 deletions(-) create mode 100644 gs_quant/api/gs/backtests_xasset/response_datatypes/generic_backtest_datatypes.py delete mode 100644 gs_quant/backtests/strategy_systematic_factory.py diff --git a/gs_quant/api/gs/backtests_xasset/json_encoders/response_encoders.py b/gs_quant/api/gs/backtests_xasset/json_encoders/response_encoders.py index 339ba176..fae168c7 100644 --- a/gs_quant/api/gs/backtests_xasset/json_encoders/response_encoders.py +++ b/gs_quant/api/gs/backtests_xasset/json_encoders/response_encoders.py @@ -27,10 +27,10 @@ decode_risk_result_with_data from gs_quant.api.gs.backtests_xasset.response_datatypes.backtest_datatypes import Transaction from gs_quant.api.gs.backtests_xasset.response_datatypes.risk_result_datatypes import RiskResultWithData -from gs_quant.backtests import FlowVolBacktestMeasure from gs_quant.common import Currency, CurrencyName, RiskMeasure from gs_quant.json_convertors_common import encode_risk_measure, decode_risk_measure from gs_quant.priceable import PriceableImpl +from gs_quant.target.backtests import FlowVolBacktestMeasure def encode_response_obj(data: Any) -> Dict: diff --git a/gs_quant/api/gs/backtests_xasset/request.py b/gs_quant/api/gs/backtests_xasset/request.py index da9b9482..260dbab1 100644 --- a/gs_quant/api/gs/backtests_xasset/request.py +++ b/gs_quant/api/gs/backtests_xasset/request.py @@ -24,7 +24,7 @@ from gs_quant.api.gs.backtests_xasset.json_encoders.request_encoders import legs_encoder, legs_decoder from gs_quant.api.gs.backtests_xasset.response_datatypes.backtest_datatypes import DateConfig, Trade, \ CostPerTransaction, Configuration -from gs_quant.backtests.strategy import Strategy +from gs_quant.api.gs.backtests_xasset.response_datatypes.generic_backtest_datatypes import Strategy from gs_quant.common import RiskMeasure from gs_quant.json_convertors import decode_optional_date, decode_date_tuple, encode_date_tuple from gs_quant.json_convertors_common import encode_risk_measure_tuple, decode_risk_measure_tuple diff --git a/gs_quant/api/gs/backtests_xasset/response_datatypes/generic_backtest_datatypes.py b/gs_quant/api/gs/backtests_xasset/response_datatypes/generic_backtest_datatypes.py new file mode 100644 index 00000000..14835312 --- /dev/null +++ b/gs_quant/api/gs/backtests_xasset/response_datatypes/generic_backtest_datatypes.py @@ -0,0 +1,27 @@ +""" +Copyright 2019 Goldman Sachs. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +""" + +from dataclasses import dataclass +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class Strategy(object): + """ + A strategy object on which one may run a backtest + """ + pass diff --git a/gs_quant/api/gs/portfolios.py b/gs_quant/api/gs/portfolios.py index fe5ffe6c..35b1af8b 100644 --- a/gs_quant/api/gs/portfolios.py +++ b/gs_quant/api/gs/portfolios.py @@ -23,6 +23,7 @@ from gs_quant.common import PositionType from gs_quant.common import RiskRequest, Currency from gs_quant.instrument import Instrument +from gs_quant.session import GsSession from gs_quant.target.portfolios import Portfolio, Position, PositionSet from gs_quant.target.reports import Report from gs_quant.target.risk_models import RiskModelTerm as Term @@ -54,11 +55,11 @@ def get_portfolios(cls, url += f'&{k}={i}' else: url += f'&{k}={v}' - return cls.get_session()._get(f'{url}&limit={limit}', cls=Portfolio)['results'] + return GsSession.current._get(f'{url}&limit={limit}', cls=Portfolio)['results'] @classmethod def get_portfolio(cls, portfolio_id: str) -> Portfolio: - return cls.get_session()._get('/portfolios/{id}'.format(id=portfolio_id), cls=Portfolio) + return GsSession.current._get('/portfolios/{id}'.format(id=portfolio_id), cls=Portfolio) @classmethod def get_portfolio_by_name(cls, name: str) -> Portfolio: @@ -74,15 +75,15 @@ def get_portfolio_by_name(cls, name: str) -> Portfolio: @classmethod def create_portfolio(cls, portfolio: Portfolio) -> Portfolio: - return cls.get_session()._post('/portfolios', portfolio, cls=Portfolio) + return GsSession.current._post('/portfolios', portfolio, cls=Portfolio) @classmethod def update_portfolio(cls, portfolio: Portfolio): - return cls.get_session()._put('/portfolios/{id}'.format(id=portfolio.id), portfolio, cls=Portfolio) + return GsSession.current._put('/portfolios/{id}'.format(id=portfolio.id), portfolio, cls=Portfolio) @classmethod def delete_portfolio(cls, portfolio_id: str) -> dict: - return cls.get_session()._delete('/portfolios/{id}'.format(id=portfolio_id)) + return GsSession.current._delete('/portfolios/{id}'.format(id=portfolio_id)) # manage portfolio positions @@ -95,7 +96,7 @@ def get_positions(cls, portfolio_id: str, start_date: dt.date = None, end_date: if end_date is not None: url += '&endDate={sd}'.format(sd=end_date.isoformat()) - res = cls.get_session()._get(url) + res = GsSession.current._get(url) return tuple(PositionSet.from_dict(v) for v in res.get('positionSets', ())) @classmethod @@ -103,7 +104,7 @@ def get_positions_for_date(cls, portfolio_id: str, position_date: dt.date, position_type: str = 'close') -> PositionSet: url = '/portfolios/{id}/positions/{date}?type={ptype}'.format( id=portfolio_id, date=position_date.isoformat(), ptype=position_type) - position_sets = cls.get_session()._get(url, cls=PositionSet)['results'] + position_sets = GsSession.current._get(url, cls=PositionSet)['results'] return position_sets[0] if len(position_sets) > 0 else None @classmethod @@ -150,7 +151,7 @@ def get_instruments_by_position_type(cls, positions_type: str, @classmethod def get_latest_positions(cls, portfolio_id: str, position_type: str = 'close') -> Union[PositionSet, dict]: url = '/portfolios/{id}/positions/last?type={ptype}'.format(id=portfolio_id, ptype=position_type) - results = cls.get_session()._get(url)['results'] + results = GsSession.current._get(url)['results'] # Annoyingly, different types are returned depending on position_type @@ -190,7 +191,7 @@ def update_positions(cls, position_sets: List[PositionSet], net_positions: bool = True) -> float: url = f'/portfolios/{portfolio_id}/positions?netPositions={str(net_positions).lower()}' - return cls.get_session()._put(url, position_sets) + return GsSession.current._put(url, position_sets) @classmethod def get_positions_data(cls, @@ -213,7 +214,7 @@ def get_positions_data(cls, if include_all_business_days: url += '&includeAllBusinessDays=true' - return cls.get_session()._get(url)['results'] + return GsSession.current._get(url)['results'] @classmethod def update_quote(cls, quote_id: str, request: RiskRequest): @@ -322,7 +323,7 @@ def get_custom_aum(cls, url += f"&startDate={start_date.strftime('%Y-%m-%d')}" if end_date: url += f"&endDate={end_date.strftime('%Y-%m-%d')}" - return cls.get_session()._get(url)['data'] + return GsSession.current._get(url)['data'] @classmethod @deprecation.deprecated(deprecated_in='1.0.10', @@ -336,11 +337,11 @@ def upload_custom_aum(cls, payload = {'data': aum_data} if clear_existing_data: url += '?clearExistingData=true' - return cls.get_session()._post(url, payload) + return GsSession.current._post(url, payload) @classmethod def update_portfolio_tree(cls, portfolio_id: str): - return cls.get_session()._post(f'/portfolios/{portfolio_id}/tree', {}) + return GsSession.current._post(f'/portfolios/{portfolio_id}/tree', {}) @classmethod def get_attribution(cls, @@ -358,4 +359,4 @@ def get_attribution(cls, url += f"¤cy={currency.value}" if performance_report_id: url += f'&reportId={performance_report_id}' - return cls.get_session()._get(url)['results'] + return GsSession.current._get(url)['results'] diff --git a/gs_quant/backtests/strategy.py b/gs_quant/backtests/strategy.py index ffdddb7e..372fd99f 100644 --- a/gs_quant/backtests/strategy.py +++ b/gs_quant/backtests/strategy.py @@ -25,7 +25,6 @@ from gs_quant.base import Priceable from gs_quant.json_convertors import decode_named_instrument, encode_named_instrument, dc_decode - backtest_engines = [GenericEngine(), PredefinedAssetEngine(), EquityVolEngine()] diff --git a/gs_quant/backtests/strategy_systematic.py b/gs_quant/backtests/strategy_systematic.py index 5a1f0bc4..17b07104 100644 --- a/gs_quant/backtests/strategy_systematic.py +++ b/gs_quant/backtests/strategy_systematic.py @@ -18,11 +18,15 @@ import gs_quant.target.backtests as backtests from gs_quant.api.gs.backtests import GsBacktestApi -from gs_quant.base import get_enum_value +from gs_quant.api.gs.backtests_xasset.apis import GsBacktestXassetApi +from gs_quant.api.gs.backtests_xasset.request import BasicBacktestRequest +from gs_quant.api.gs.backtests_xasset.response_datatypes.backtest_datatypes import DateConfig, Trade, \ + CostPerTransaction, TransactionCostModel from gs_quant.backtests.core import Backtest, TradeInMethod from gs_quant.errors import MqValueError from gs_quant.target.backtests import * -from gs_quant.instrument import EqOption, EqVarianceSwap +from gs_quant.instrument import EqOption, EqVarianceSwap, Instrument +from gs_quant.target.instrument import FXOption, FXBinary _logger = logging.getLogger(__name__) @@ -31,24 +35,14 @@ ISO_FORMAT = r"^([0-9]{4})-([0-9]{2})-([0-9]{2})$" -class StrategySystematicBase(metaclass=ABCMeta): - @abstractmethod - def backtest( - self, - start: datetime.date = None, - end: datetime.date = datetime.date.today() - datetime.timedelta(days=1), - is_async: bool = False, - measures: Iterable[FlowVolBacktestMeasure] = (FlowVolBacktestMeasure.ALL_MEASURES,), - correlation_id: str = None - ) -> Union[Backtest, BacktestResult]: - ... - - -class StrategySystematic(StrategySystematicBase): +class StrategySystematic: """Equity back testing systematic strategy""" + _supported_eq_instruments = (EqOption, EqVarianceSwap) + _supported_fx_instruments = (FXOption, FXBinary) + _supported_instruments = _supported_eq_instruments + _supported_fx_instruments def __init__(self, - underliers: Union[EqOption, Iterable[EqOption], EqVarianceSwap, Iterable[EqVarianceSwap]], + underliers: Union[Instrument, Iterable[Instrument]], quantity: float = 1, quantity_type: Union[BacktestTradingQuantityType, str] = BacktestTradingQuantityType.notional, trade_in_method: Union[TradeInMethod, str] = TradeInMethod.FixedRoll, @@ -85,11 +79,12 @@ def __init__(self, ) self.__underliers = [] - - if isinstance(underliers, (EqOption, EqVarianceSwap)): + trade_instruments = [] + if isinstance(underliers, self._supported_instruments): instrument = underliers notional_percentage = 100 instrument = self.check_underlier_fields(instrument) + trade_instruments.append(instrument) self.__underliers.append(BacktestStrategyUnderlier( instrument=instrument, notional_percentage=notional_percentage, @@ -105,16 +100,22 @@ def __init__(self, instrument = underlier notional_percentage = 100 - if not isinstance(instrument, (EqOption, EqVarianceSwap)): + if not isinstance(instrument, self._supported_instruments): raise MqValueError('The format of the backtest asset is incorrect.') + elif isinstance(instrument, self._supported_fx_instruments): + instrument.notional_amount *= notional_percentage / 100 instrument = self.check_underlier_fields(instrument) + trade_instruments.append(instrument) self.__underliers.append(BacktestStrategyUnderlier( instrument=instrument, notional_percentage=notional_percentage, hedge=BacktestStrategyUnderlierHedge(risk_details=delta_hedge), market_model=market_model, expiry_date_mode=expiry_date_mode)) + # xasset backtesting service fields + self.__trades = (Trade(tuple(trade_instruments), roll_frequency, roll_frequency, quantity, quantity_type),) + self.__delta_hedge_frequency = '1b' if delta_hedge else None backtest_parameters_class: Base = getattr(backtests, self.__backtest_type + 'BacktestParameters') backtest_parameter_args = { @@ -125,17 +126,39 @@ def __init__(self, 'index_initial_value': index_initial_value } self.__backtest_parameters = backtest_parameters_class.from_dict(backtest_parameter_args) + all_eq = all(isinstance(i, self._supported_eq_instruments) for i in trade_instruments) + all_fx = all(isinstance(i, self._supported_fx_instruments) for i in trade_instruments) + if not (all_eq or all_fx): + raise MqValueError('Cannot run backtests for different asset classes.') + self.__use_xasset_backtesting_service = all_fx @staticmethod def check_underlier_fields( - underlier: Union[EqOption, EqVarianceSwap] - ) -> Union[EqOption, EqVarianceSwap]: + underlier: Instrument + ) -> Instrument: if isinstance(underlier, EqOption): underlier.number_of_options = None return underlier + def __run_service_based_backtest(self, start: datetime.date, end: datetime.date, + measures: Iterable[FlowVolBacktestMeasure]) -> BacktestResult: + date_cfg = DateConfig(start, end) + measures = tuple(m for m in measures if m != FlowVolBacktestMeasure.portfolio) + if not measures: + measures = (FlowVolBacktestMeasure.PNL,) + basic_bt_request = BasicBacktestRequest(date_cfg, self.__trades, measures, self.__delta_hedge_frequency, + CostPerTransaction(TransactionCostModel.Fixed, 0), None) + basic_bt_response = GsBacktestXassetApi.calculate_basic_backtest(basic_bt_request) + risks = tuple( + BacktestRisk(name=k.value, + timeseries=tuple(FieldValueMap(date=d, value=r.result) for d, r in v.items())) + for k, v in basic_bt_response.measures.items() + ) + portfolio = None + return BacktestResult(risks=risks, portfolio=portfolio) + def backtest( self, start: datetime.date = None, @@ -144,7 +167,8 @@ def backtest( measures: Iterable[FlowVolBacktestMeasure] = (FlowVolBacktestMeasure.ALL_MEASURES,), correlation_id: str = None ) -> Union[Backtest, BacktestResult]: - + if self.__use_xasset_backtesting_service: + return self.__run_service_based_backtest(start, end, measures) params_dict = self.__backtest_parameters.as_dict() params_dict['measures'] = [m.value for m in measures] backtest_parameters_class: Base = getattr(backtests, self.__backtest_type + 'BacktestParameters') diff --git a/gs_quant/backtests/strategy_systematic_factory.py b/gs_quant/backtests/strategy_systematic_factory.py deleted file mode 100644 index ae26c449..00000000 --- a/gs_quant/backtests/strategy_systematic_factory.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Copyright 2019 Goldman Sachs. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. -""" - -from typing import Union, Iterable, Tuple - -from gs_quant.backtests import TradeInMethod, StrategySystematic -from gs_quant.common import Currency -from gs_quant.instrument import EqOption, EqVarianceSwap, Instrument -from gs_quant.target.backtests import BacktestTradingQuantityType, DeltaHedgeParameters, BacktestSignalSeriesItem, \ - EquityMarketModel - - -class StrategySystematicFactory: - @staticmethod - def get(underliers: Union[Instrument, Iterable[Instrument]], - quantity: float = 1, - quantity_type: Union[BacktestTradingQuantityType, str] = BacktestTradingQuantityType.notional, - trade_in_method: Union[TradeInMethod, str] = TradeInMethod.FixedRoll, - roll_frequency: str = None, - scaling_method: str = None, - index_initial_value: float = 0.0, - delta_hedge: DeltaHedgeParameters = None, - name: str = None, - cost_netting: bool = False, - currency: Union[Currency, str] = Currency.USD, - trade_in_signals: Tuple[BacktestSignalSeriesItem, ...] = None, - trade_out_signals: Tuple[BacktestSignalSeriesItem, ...] = None, - market_model: Union[EquityMarketModel, str] = EquityMarketModel.SFK, - roll_date_mode: str = None, - expiry_date_mode: str = None, - cash_accrual: bool = True): - supported_eq_inst = (EqOption, EqVarianceSwap) - if (isinstance(underliers, Instrument) and isinstance(underliers, supported_eq_inst)) or \ - isinstance(underliers, Iterable) and all(isinstance(u, supported_eq_inst) for u in underliers): - return StrategySystematic(underliers, quantity, quantity_type, trade_in_method, roll_frequency, - scaling_method, index_initial_value, delta_hedge, name, cost_netting, - currency, trade_in_signals, trade_out_signals, market_model, roll_date_mode, - expiry_date_mode, cash_accrual) - else: - raise NotImplementedError('StrategySystematic only implemented for equity underliers')