From cc003d9322bd6c4d099c46a0ce214d2895d915ee Mon Sep 17 00:00:00 2001 From: "Roberson, Martin [GBM Public]" Date: Mon, 16 Dec 2024 11:19:21 +0000 Subject: [PATCH] Chore: Make release 1.2.10 --- .../response_datatypes/backtest_datatypes.py | 15 +++ gs_quant/api/gs/data.py | 1 + gs_quant/backtests/strategy_systematic.py | 7 +- gs_quant/session.py | 2 + gs_quant/target/workflow_quote.py | 14 ++ .../timeseries/test_measures_risk_models.py | 45 +++++++ gs_quant/timeseries/measures_risk_models.py | 120 ++++++++++++++++++ 7 files changed, 202 insertions(+), 2 deletions(-) diff --git a/gs_quant/api/gs/backtests_xasset/response_datatypes/backtest_datatypes.py b/gs_quant/api/gs/backtests_xasset/response_datatypes/backtest_datatypes.py index 020cdbd8..20d30745 100644 --- a/gs_quant/api/gs/backtests_xasset/response_datatypes/backtest_datatypes.py +++ b/gs_quant/api/gs/backtests_xasset/response_datatypes/backtest_datatypes.py @@ -40,6 +40,20 @@ class TransactionDirection(Enum): Exit = 'Exit' +class RollDateMode(Enum): + OTC = 'OTC' + Listed = 'Listed' + + @classmethod + def _missing_(cls, value): + if value is None: + return None + for member in cls: + if member.value.lower() == value.lower(): + return member + return None + + @dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Transaction: @@ -103,3 +117,4 @@ class Configuration: market_data_location: Optional[PricingLocation] = None market_model: Optional[EquityMarketModel] = None cash_accrual: bool = False + roll_date_mode: Optional[RollDateMode] = None diff --git a/gs_quant/api/gs/data.py b/gs_quant/api/gs/data.py index 2b974f4e..4a7178f2 100644 --- a/gs_quant/api/gs/data.py +++ b/gs_quant/api/gs/data.py @@ -131,6 +131,7 @@ class QueryType(Enum): COVARIANCE = "Covariance" FACTOR_EXPOSURE = "Factor Exposure" FACTOR_RETURN = "Factor Return" + HISTORICAL_BETA = "Historical Beta" FACTOR_PNL = "Factor Pnl" FACTOR_PROPORTION_OF_RISK = "Factor Proportion Of Risk" DAILY_RISK = "Daily Risk" diff --git a/gs_quant/backtests/strategy_systematic.py b/gs_quant/backtests/strategy_systematic.py index 69cb944a..a7377556 100644 --- a/gs_quant/backtests/strategy_systematic.py +++ b/gs_quant/backtests/strategy_systematic.py @@ -21,7 +21,7 @@ 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 + CostPerTransaction, TransactionCostModel, Configuration, RollDateMode from gs_quant.backtests.core import Backtest, TradeInMethod from gs_quant.errors import MqValueError from gs_quant.target.backtests import * @@ -121,6 +121,8 @@ def __init__(self, self.__trades = (Trade(tuple(trade_instruments), roll_frequency, trade_buy_dates, roll_frequency, trade_exit_dates, quantity, quantity_type),) self.__delta_hedge_frequency = '1b' if delta_hedge else None + self.__xasset_bt_service_config = Configuration(roll_date_mode=RollDateMode(roll_date_mode) if + roll_date_mode is not None else None) backtest_parameters_class: Base = getattr(backtests, self.__backtest_type + 'BacktestParameters') backtest_parameter_args = { @@ -154,7 +156,8 @@ def __run_service_based_backtest(self, start: datetime.date, end: datetime.date, if not calc_measures: calc_measures = (FlowVolBacktestMeasure.PNL,) basic_bt_request = BasicBacktestRequest(date_cfg, self.__trades, calc_measures, self.__delta_hedge_frequency, - CostPerTransaction(TransactionCostModel.Fixed, 0), None) + CostPerTransaction(TransactionCostModel.Fixed, 0), + self.__xasset_bt_service_config) basic_bt_response = GsBacktestXassetApi.calculate_basic_backtest(basic_bt_request, decode_instruments=False) risks = tuple( BacktestRisk(name=k.value, diff --git a/gs_quant/session.py b/gs_quant/session.py index 92cba67e..db787eaa 100644 --- a/gs_quant/session.py +++ b/gs_quant/session.py @@ -496,6 +496,8 @@ def _headers(self): def _get_mds_domain(self): env_config = GsSession._config_for_environment(self.environment.name) current_domain = self.domain.replace('marquee.web', 'marquee') # remove .web from prod domain + if self.environment.name == Environment.QA.name: + current_domain = self.domain.replace('marquee-qa.web', 'marquee-qa') # remove .web from qa domain is_mds_web = current_domain == Domain.MDS_WEB is_env_mds_web = current_domain == env_config['MdsWebDomain'] diff --git a/gs_quant/target/workflow_quote.py b/gs_quant/target/workflow_quote.py index aa20a429..3768819f 100644 --- a/gs_quant/target/workflow_quote.py +++ b/gs_quant/target/workflow_quote.py @@ -86,6 +86,18 @@ class PriceFormat(EnumBase, Enum): Cents = 'Cents' +class RelativeExpiryType(EnumBase, Enum): + Relative = 'Relative' + Fixed = 'Fixed' + + +class RelativeStrikeType(EnumBase, Enum): + Relative_Delta = 'Relative Delta' + Relative_Spot = 'Relative Spot' + Relative_Fwd = 'Relative Fwd' + Fixed = 'Fixed' + + @dataclass class HedgeTypes(Base): pass @@ -284,6 +296,8 @@ class WorkflowPosition(Base): creator: Optional[str] = field(default=None, metadata=field_metadata) originating_system: Optional[str] = field(default=None, metadata=field_metadata) is_read_only: Optional[bool] = field(default=None, metadata=field_metadata) + strike_and_barrier_type: Optional[RelativeStrikeType] = field(default=None, metadata=field_metadata) + expiry_type: Optional[RelativeExpiryType] = field(default=None, metadata=field_metadata) name: Optional[str] = field(default=None, metadata=name_metadata) diff --git a/gs_quant/test/timeseries/test_measures_risk_models.py b/gs_quant/test/timeseries/test_measures_risk_models.py index f04cda4c..50b3b2a8 100644 --- a/gs_quant/test/timeseries/test_measures_risk_models.py +++ b/gs_quant/test/timeseries/test_measures_risk_models.py @@ -108,6 +108,51 @@ def mock_risk_model(): return actual +def test_risk_model_measure(): + replace = Replacer() + + # mock getting risk model entity() + mock = replace('gs_quant.api.gs.risk_models.GsRiskModelApi.get_risk_model', Mock()) + mock.return_value = mock_risk_model_obj + + # mock getting risk model factor entity + mock = replace('gs_quant.api.gs.risk_models.GsFactorRiskModelApi.get_risk_model_data', Mock()) + mock.return_value = mock_risk_model_data + + # mock getting risk model factor entity + mock = replace('gs_quant.api.gs.risk_models.GsFactorRiskModelApi.get_risk_model_factor_data', Mock()) + mock.return_value = mock_risk_model_factor_data + + # mock getting asset gsid + mock = replace('gs_quant.markets.securities.Asset.get_identifier', Mock()) + mock.return_value = '14593' + + # mock getting risk model dates + mock = replace('gs_quant.api.gs.risk_models.GsRiskModelApi.get_risk_model_dates', Mock()) + mock.return_value = ['2020-01-01', '2020-01-02', '2020-01-03'] + + # mock getting risk model data + mock = replace('gs_quant.models.risk_model.MarqueeRiskModel.get_data', Mock()) + mock.return_value = { + 'totalResults': 3, + 'missingDates': [], + 'results': [ + {'date': '2024-08-19', 'assetData': {'universe': ['14593'], 'bidAskSpread30d': [0.1]}}, + {'date': '2024-08-20', 'assetData': {'universe': ['14593'], 'bidAskSpread30d': [0.2]}}, + {'date': '2024-08-21', 'assetData': {'universe': ['14593'], 'bidAskSpread30d': [0.3]}} + ] + } + + with DataContext(datetime.date(2020, 1, 1), datetime.date(2020, 1, 3)): + actual = mrm.risk_model_measure(Stock(id_='id', name='Fake Asset'), 'model_id', + mrm.ModelMeasureString.BID_AKS_SPREAD_30D) + assert all(actual.values == [0.1, 0.2, 0.3]) + + with pytest.raises(AttributeError): + mrm.risk_model_measure(Stock(id_='id', name='Fake Asset'), 'model_id', 'Wrong Factor Name') + replace.restore() + + def test_factor_zscore(): replace = Replacer() diff --git a/gs_quant/timeseries/measures_risk_models.py b/gs_quant/timeseries/measures_risk_models.py index 7bec6104..dd958dcc 100644 --- a/gs_quant/timeseries/measures_risk_models.py +++ b/gs_quant/timeseries/measures_risk_models.py @@ -13,9 +13,11 @@ specific language governing permissions and limitations under the License. """ +from enum import Enum from typing import Dict, Optional import pandas as pd +import datetime as dt from pydash import decapitalize from gs_quant.api.gs.data import QueryType @@ -30,6 +32,124 @@ from gs_quant.timeseries import plot_measure_entity, plot_measure, prices from gs_quant.timeseries.measures import _extract_series_from_df +ModelMeasureStr = { + 'Asset Universe': RiskModelDataMeasure.Asset_Universe, + 'Historical Beta': RiskModelDataMeasure.Historical_Beta, + 'Total Risk': RiskModelDataMeasure.Total_Risk, + 'Specific Risk': RiskModelDataMeasure.Specific_Risk, + 'Specific Return': RiskModelDataMeasure.Specific_Return, + 'Daily Returns': RiskModelDataMeasure.Daily_Return, + 'Estimation Universe Weight': RiskModelDataMeasure.Estimation_Universe_Weight, + 'Residual Variance': RiskModelDataMeasure.Residual_Variance, + 'Predicted Beta': RiskModelDataMeasure.Predicted_Beta, + 'Global Predicted Beta': RiskModelDataMeasure.Global_Predicted_Beta, + 'Universe Factor Exposure': RiskModelDataMeasure.Universe_Factor_Exposure, + 'R Squared': RiskModelDataMeasure.R_Squared, + 'Fair Value Gap Percent': RiskModelDataMeasure.Fair_Value_Gap_Percent, + 'Fair Value Gap Standard Deviation': RiskModelDataMeasure.Fair_Value_Gap_Standard_Deviation, + 'Bid Ask Spread': RiskModelDataMeasure.Bid_Ask_Spread, + 'Bid Ask Spread 30d': RiskModelDataMeasure.Bid_Ask_Spread_30d, + 'Bid Ask Spread 60d': RiskModelDataMeasure.Bid_Ask_Spread_60d, + 'Bid Ask Spread 90d': RiskModelDataMeasure.Bid_Ask_Spread_90d, + 'Trading Volume': RiskModelDataMeasure.Trading_Volume, + 'Trading Volume 30d': RiskModelDataMeasure.Trading_Volume_30d, + 'Trading Volume 60d': RiskModelDataMeasure.Trading_Volume_60d, + 'Trading Volume 90d': RiskModelDataMeasure.Trading_Volume_90d, + 'Trading Value 30d': RiskModelDataMeasure.Traded_Value_30d, + 'Composite Volume': RiskModelDataMeasure.Composite_Volume, + 'Composite Volume 30d': RiskModelDataMeasure.Composite_Volume_30d, + 'Composite Volume 60d': RiskModelDataMeasure.Composite_Volume_60d, + 'Composite Volume 90d': RiskModelDataMeasure.Composite_Volume_90d, + 'Composite Value 30d': RiskModelDataMeasure.Composite_Value_30d, + 'Composite Issuer Market Cap': RiskModelDataMeasure.Issuer_Market_Cap, + 'Composite Price': RiskModelDataMeasure.Price, + 'Composite Model Price': RiskModelDataMeasure.Model_Price, + 'Composite Capitalization': RiskModelDataMeasure.Capitalization, + 'Composite Currency': RiskModelDataMeasure.Currency, + 'Composite Unadjusted Specific Risk': RiskModelDataMeasure.Unadjusted_Specific_Risk, + 'Dividend Yield': RiskModelDataMeasure.Dividend_Yield} + + +class ModelMeasureString(Enum): + ASSET_UNIVERSE = 'Asset Universe' + HISTORICAL_BETA = 'Historical Beta' + TOTAL_RISK = 'Total Risk' + SPECIFIC_RISK = 'Specific Risk' + SPECIFIC_RETURNS = 'Specific Return' + DAILY_RETURNS = 'Daily Returns' + ESTIMATION_UNVERSE_WEIGHT = 'Estimation Universe Weight' + RESIDUAL_VARIANCE = 'Residual Variance' + PREDICTED_BETA = 'Predicted Beta' + GLOBAL_PREDICTED_BETA = 'Global Predicted Beta' + UNIVERSE_FACTOR_EXPOSURE = 'Universe Factor Exposure' + R_SQUARED = 'R Squared' + FAIR_VALUE_GAP_PERCENT = 'Fair Value Gap Percent' + FAIR_VALUE_GAP_STANDARD_DEVIATION = 'Fair Value Gap Standard Deviation' + BID_ASK_SPREAD = 'Bid Ask Spread' + BID_AKS_SPREAD_30D = 'Bid Ask Spread 30d' + BID_AKS_SPREAD_60D = 'Bid Ask Spread 60d' + BID_AKS_SPREAD_90D = 'Bid Ask Spread 90d' + TRADING_VOLUME = 'Trading Volume' + TRADING_VOLUME_30D = 'Trading Volume 30d' + TRADING_VOLUME_60D = 'Trading Volume 60d' + TRADING_VOLUME_90D = 'Trading Volume 90d' + TRADING_VALUE_30D = 'Trading Value 30d' + COMPOSITE_VOLUME = 'Composite Volume' + COMPOSITE_VOLUME_30D = 'Composite Volume 30d' + COMPOSITE_VOLUME_60D = 'Composite Volume 60d' + COMPOSITE_VOLUME_90D = 'Composite Volume 90d' + COMPOSITE_VALUE_30d = 'Composite Value 30d' + COMPOSITE_ISSUER_MARKET_CAP = 'Composite Issuer Market Cap' + COMPOSITE_PRICE = 'Composite Price' + COMPOSITE_MODEL_PRICE = 'Composite Model Price' + COMPOSITE_CAPITALIZATION = 'Composite Capitalization' + COMPOSITE_CURRENCY = 'Composite Currency' + COMPOSITE_UNADJUSTED_SPECIFIC_RISK = 'Composite Unadjusted Specific Risk' + DIVIDEND_YIELD = 'Dividend Yield' + + +@plot_measure((AssetClass.Equity,), (AssetType.Single_Stock,), [QueryType.HISTORICAL_BETA]) +def risk_model_measure(asset: Asset, risk_model_id: str, + risk_model_measure_selected: ModelMeasureString = ModelMeasureString.HISTORICAL_BETA, + start_date: dt.date = dt.date.today(), + end_date: dt.date = dt.date.today() - dt.timedelta(7), *, + source: str = None, real_time: bool = False, request_id: Optional[str] = None) -> pd.Series: + """ + Retrieve risk model measures for a given asset. + + :param asset: Asset object loaded from security master + :param risk_model_id: ID of the risk model + :param risk_model_measure_selected: Selected risk model measure + :param start_date: Start date for the data retrieval + :param end_date: End date for the data retrieval + :param source: Name of function caller + :param real_time: Whether to retrieve intraday data instead of EOD + :param request_id: Service request ID, if any + :return: Risk model measure data for the asset + """ + model = MarqueeRiskModel.get(risk_model_id) + gsid = asset.get_identifier(AssetIdentifier.GSID) + risk_model_measure_selected = ModelMeasureStr[risk_model_measure_selected.value] + + query_results = model.get_data( + measures=[risk_model_measure_selected, + RiskModelDataMeasure.Asset_Universe], + start_date=start_date, + end_date=end_date, + assets=RiskModelDataAssetsRequest(identifier=RiskModelUniverseIdentifierRequest.gsid, universe=[gsid]), + limit_factors=False + ).get('results', []) + + measures = {} + for result in query_results: + if result: + measure_name = set(result.get('assetData', {}).keys()).difference({'universe'}) + exposures = result.get('assetData', {}).get(measure_name.pop(), []) + if exposures: + measures[result['date']] = exposures[0] + + return __format_plot_measure_results(measures, QueryType.FACTOR_EXPOSURE) + @plot_measure((AssetClass.Equity,), (AssetType.Single_Stock,)) def factor_zscore(asset: Asset, risk_model_id: str, factor_name: str, *,