Skip to content

Commit

Permalink
Chore: Make release 1.0.106
Browse files Browse the repository at this point in the history
  • Loading branch information
martinroberson authored and Vanden Bon, David V [GBM Public] committed Aug 5, 2024
1 parent 8285cc6 commit 654f9c4
Show file tree
Hide file tree
Showing 22 changed files with 118 additions and 175 deletions.
5 changes: 3 additions & 2 deletions gs_quant/api/gs/backtests_xasset/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def calculate_risk(cls, risk_request: RiskRequest) -> RiskResponse:
return result

@classmethod
def calculate_basic_backtest(cls, backtest_request: BasicBacktestRequest) -> BasicBacktestResponse:
def calculate_basic_backtest(cls, backtest_request: BasicBacktestRequest, decode_instruments: bool = True) -> \
BasicBacktestResponse:
response = GsSession.current._post('/backtests/xasset/strategy/basic', backtest_request.to_json())
result = BasicBacktestResponse.from_dict(response)
result = BasicBacktestResponse.from_dict_custom(response, decode_instruments)
return result
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ def decode_inst_tuple(t: tuple) -> Tuple[Instrument, ...]:
return tuple(decode_inst(i) for i in t)


def decode_daily_portfolio(results: dict) -> Dict[dt.date, Tuple[Instrument, ...]]:
return {dt.date.fromisoformat(k): decode_inst_tuple(v) for k, v in results.items()}
def decode_daily_portfolio(results: dict, decode_instruments: bool = True) -> Dict[dt.date, Tuple[Instrument, ...]]:
return {dt.date.fromisoformat(k): decode_inst_tuple(v) if decode_instruments else v for k, v in results.items()}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def decode_basic_bt_measure_dict(results: dict) -> Dict[FlowVolBacktestMeasure,
for k, v in results.items()}


def decode_basic_bt_transactions(results: dict) -> Dict[dt.date, Tuple[Transaction, ...]]:
def decode_basic_bt_transactions(results: dict, decode_instruments: bool = True) -> \
Dict[dt.date, Tuple[Transaction, ...]]:
def to_ccy(s: str) -> Union[Currency, CurrencyName, str]:
if s in [x.value for x in Currency]:
return Currency(s)
Expand All @@ -70,6 +71,7 @@ def to_ccy(s: str) -> Union[Currency, CurrencyName, str]:
return s

return {dt.date.fromisoformat(k): tuple(
Transaction(decode_inst_tuple(t['portfolio']), t['portfolio_price'], t['cost'], to_ccy(t['currency']),
Transaction(decode_inst_tuple(t['portfolio']) if decode_instruments else t['portfolio'],
t['portfolio_price'], t['cost'], to_ccy(t['currency']),
TransactionDirection(t['direction']) if t['direction'] else None)
for t in v) for k, v in results.items()}
12 changes: 11 additions & 1 deletion gs_quant/api/gs/backtests_xasset/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import datetime as dt

from dataclasses import dataclass, field
from typing import Dict, Optional, Tuple
from typing import Dict, Optional, Tuple, Any

from dataclasses_json import dataclass_json, LetterCase, config

Expand Down Expand Up @@ -53,6 +53,16 @@ class BasicBacktestResponse:
= field(default=None, metadata=config(decoder=decode_basic_bt_transactions))
additional_results: Optional[AdditionalResults] = field(default=None)

@classmethod
def from_dict_custom(cls, data: Any, decode_instruments: bool = True):
if decode_instruments:
return cls.from_dict(data)
return BasicBacktestResponse(measures=decode_basic_bt_measure_dict(data['measures']),
portfolio=decode_daily_portfolio(data['portfolio'], decode_instruments),
transactions=decode_basic_bt_transactions(data['transactions'],
decode_instruments),
additional_results=AdditionalResults.from_dict_custom(data['additional_results']))


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Tuple, Dict, Union
from typing import Optional, Tuple, Dict, Union, Any

from dataclasses_json import dataclass_json, LetterCase, config

Expand Down Expand Up @@ -58,6 +58,14 @@ class AdditionalResults:
hedge_pnl: Optional[Dict[dt.date, float]] = None
no_of_calculations: Optional[int] = None

@classmethod
def from_dict_custom(cls, data: Any, decode_instruments: bool = True):
if decode_instruments:
return cls.from_dict(data)
return AdditionalResults(hedges=decode_daily_portfolio(data['hedges'], decode_instruments),
hedge_pnl=data['hedge_pnl'],
no_of_calculations=data['no_of_calculations'])


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass(unsafe_hash=True, repr=False)
Expand Down
127 changes: 61 additions & 66 deletions gs_quant/backtests/generic_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from gs_quant.backtests.backtest_engine import BacktestBaseEngine
from gs_quant.backtests.backtest_objects import BackTest, ScalingPortfolio, CashPayment, Hedge
from gs_quant.backtests.backtest_utils import make_list, CalcType, get_final_date
from gs_quant.common import ParameterisedRiskMeasure
from gs_quant.common import ParameterisedRiskMeasure, RiskMeasure
from gs_quant.context_base import nullcontext
from gs_quant.datetime.relative_date import RelativeDateSchedule
from gs_quant.instrument import Instrument
Expand Down Expand Up @@ -114,7 +114,7 @@ class AddScaledTradeActionImpl(ActionHandler):
def __init__(self, action: AddScaledTradeAction):
super().__init__(action)

def _nav_scale_orders(self, orders):
def _nav_scale_orders(self, orders, price_measure):
sorted_order_days = sorted(make_list(orders.keys()))
final_days_orders = {}

Expand All @@ -126,23 +126,27 @@ def _nav_scale_orders(self, orders):
final_days_orders[d] = []
final_days_orders[d].append(inst)

unscaled_prices_by_day = {}
unscaled_unwind_prices_by_day = {}
with PricingContext(is_async=True):
for day, portfolio in orders.items():
with PricingContext(pricing_date=day):
unscaled_prices_by_day[day] = portfolio.calc(price_measure)
for unwind_day, unwind_instruments in final_days_orders.items():
if unwind_day <= dt.date.today():
with PricingContext(pricing_date=unwind_day):
unscaled_unwind_prices_by_day[unwind_day] = Portfolio(unwind_instruments).calc(price_measure)

# Start with first_quantity, then only use proceeds from selling instruments
available_cash = self.action.scaling_level

scaling_factors_by_inst = {}
# Go through each order day of the strategy in sorted order
for idx, cur_day in enumerate(sorted_order_days):
scale_factor = available_cash / unscaled_prices_by_day[cur_day].aggregate()
portfolio = orders[cur_day]

# Scale portfolio price to available cash - remove orders if no cash left
if available_cash == 0:
del orders[cur_day]
continue
else:
with PricingContext(pricing_date=cur_day):
cur_order_price = portfolio.calc(risk.Price)

scale_factor = available_cash / cur_order_price.aggregate()
portfolio.scale(scale_factor)
portfolio.scale(scale_factor)
for inst in portfolio:
scaling_factors_by_inst[inst] = scale_factor

available_cash = 0

Expand All @@ -151,27 +155,19 @@ def _nav_scale_orders(self, orders):
else:
break

# Only consider final days between current order date and the next in an iteration
unwind_days = {d: p for d, p in final_days_orders.items() if cur_day < d <= next_day}

# Price the instruments sold in between these order dates
# At this point all past orders have been scaled, so these instruments will be scaled too
unwind_vals = []
with PricingContext():
for unwind_day, inst_list in unwind_days.items():
with PricingContext(pricing_date=unwind_day):
unwind_vals += [i.calc(risk.Price) for i in inst_list]

# Cash received from unwinds is the cash available for the next order
for val in unwind_vals:
available_cash += val.result()
for d, p in final_days_orders.items():
# Only consider final days between current order date and the next in an iteration
if cur_day < d <= next_day:
available_cash += sum(unscaled_unwind_prices_by_day[d][inst] * scaling_factors_by_inst[inst] for
inst in p)

def _scale_order(self, orders, daily_risk):
def _scale_order(self, orders, daily_risk, price_measure):
if self.action.scaling_type == ScalingActionType.size:
for _, portfolio in orders.items():
portfolio.scale(self.action.scaling_level)
elif self.action.scaling_type == ScalingActionType.NAV:
self._nav_scale_orders(orders)
self._nav_scale_orders(orders, price_measure)
elif self.action.scaling_type == ScalingActionType.risk_measure:
for day, portfolio in orders.items():
scaling_factor = self.action.scaling_level / daily_risk[day]
Expand All @@ -180,7 +176,8 @@ def _scale_order(self, orders, daily_risk):
raise RuntimeError(f'Scaling Type {self.action.scaling_type} not supported by engine')

def _raise_order(self,
state: Union[date, Iterable[date]]):
state: Union[date, Iterable[date]],
price_measure: RiskMeasure):
state_list = make_list(state)
orders = {}
order_valuations = (ResolvedInstrumentValues,)
Expand All @@ -204,7 +201,7 @@ def _raise_order(self,
daily_risk = {d: res[self.action.scaling_risk].aggregate() for d, res in orders.items()} if \
self.action.scaling_type == ScalingActionType.risk_measure else None

self._scale_order(final_orders, daily_risk)
self._scale_order(final_orders, daily_risk, price_measure)

return final_orders

Expand All @@ -214,7 +211,7 @@ def apply_action(self,
trigger_info: Optional[Union[EnterPositionQuantityScaledActionInfo,
Iterable[EnterPositionQuantityScaledActionInfo]]] = None):

orders = self._raise_order(state)
orders = self._raise_order(state, backtest.price_measure)

# record entry and unwind cashflows
for create_date, portfolio in orders.items():
Expand Down Expand Up @@ -254,7 +251,7 @@ def _quantity_type_to_risk_measure(quantity_type: BacktestTradingQuantityType):

return map[quantity_type]

def _nav_scale_orders(self, orders, first_quantity):
def _nav_scale_orders(self, orders, first_quantity, price_measure):
sorted_order_days = sorted(make_list(orders.keys()))
final_days_orders = {}

Expand All @@ -266,23 +263,27 @@ def _nav_scale_orders(self, orders, first_quantity):
final_days_orders[d] = []
final_days_orders[d].append(inst)

unscaled_prices_by_day = {}
unscaled_unwind_prices_by_day = {}
with PricingContext(is_async=True):
for day, portfolio in orders.items():
with PricingContext(pricing_date=day):
unscaled_prices_by_day[day] = portfolio.calc(price_measure)
for unwind_day, unwind_instruments in final_days_orders.items():
if unwind_day <= dt.date.today():
with PricingContext(pricing_date=unwind_day):
unscaled_unwind_prices_by_day[unwind_day] = Portfolio(unwind_instruments).calc(price_measure)

# Start with first_quantity, then only use proceeds from selling instruments
available_cash = first_quantity

scaling_factors_by_inst = {}
# Go through each order day of the strategy in sorted order
for idx, cur_day in enumerate(sorted_order_days):
scale_factor = available_cash / unscaled_prices_by_day[cur_day].aggregate()
portfolio = orders[cur_day]

# Scale portfolio price to available cash - remove portfolio if no cash left
if available_cash == 0:
del orders[cur_day]
continue
else:
with PricingContext(pricing_date=cur_day):
cur_order_price = portfolio.calc(risk.Price)

scale_factor = available_cash / cur_order_price.aggregate()
portfolio.scale(scale_factor)
portfolio.scale(scale_factor)
for inst in portfolio:
scaling_factors_by_inst[inst] = scale_factor

available_cash = 0

Expand All @@ -291,22 +292,14 @@ def _nav_scale_orders(self, orders, first_quantity):
else:
break

# Only consider final days between current order date and the next in an iteration
unwind_days = {d: p for d, p in final_days_orders.items() if cur_day < d <= next_day}

# Price the instruments sold in between these order dates
# At this point all past orders have been scaled, so these instruments will be scaled too
unwind_vals = []
with PricingContext():
for unwind_day, inst_list in unwind_days.items():
with PricingContext(pricing_date=unwind_day):
unwind_vals += [i.calc(risk.Price) for i in inst_list]

# Cash received from unwinds is the cash available for the next order
for val in unwind_vals:
available_cash += val.result()
for d, p in final_days_orders.items():
# Only consider final days between current order date and the next in an iteration
if cur_day < d <= next_day:
available_cash += sum(unscaled_unwind_prices_by_day[d][inst] * scaling_factors_by_inst[inst] for
inst in p)

def _scale_order(self, orders):
def _scale_order(self, orders, price_measure):
quantity_type = self.action.trade_quantity_type
quantity = self.action.trade_quantity

Expand All @@ -317,7 +310,7 @@ def _scale_order(self, orders):
orders_risk = {}
if quantity_type == BacktestTradingQuantityType.NAV:
# Scale separately if strategy is NAV
self._nav_scale_orders(orders, quantity)
self._nav_scale_orders(orders, quantity, price_measure)
else:
# Scale risk daily risk to specified value otherwise
risk_measure = EnterPositionQuantityScaledActionImpl._quantity_type_to_risk_measure(quantity_type)
Expand All @@ -332,7 +325,8 @@ def _scale_order(self, orders):
portfolio.scale(scaling_factor)

def _raise_order(self,
state: Union[date, Iterable[date]]):
state: Union[date, Iterable[date]],
price_measure: RiskMeasure):
state_list = make_list(state)
orders = {}

Expand All @@ -350,7 +344,7 @@ def _raise_order(self,
new_port.append(t)
final_orders[d] = Portfolio(new_port)

self._scale_order(final_orders)
self._scale_order(final_orders, price_measure)

return final_orders

Expand All @@ -360,7 +354,7 @@ def apply_action(self,
trigger_info: Optional[Union[EnterPositionQuantityScaledActionInfo,
Iterable[EnterPositionQuantityScaledActionInfo]]] = None):

orders = self._raise_order(state)
orders = self._raise_order(state, backtest.price_measure)

# record entry and unwind cashflows
for create_date, portfolio in orders.items():
Expand Down Expand Up @@ -929,11 +923,12 @@ def _handle_cash(self, backtest, risks, price_risk, strategy_pricing_dates, stra
for _, cash_payments in backtest.cash_payments.items():
for cp in cash_payments:
# only calc if additional point is required
cp_day_results = backtest.results[cp.effective_date]
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 <= strategy_end_date:
if cp.effective_date not in backtest.results or \
trade not in backtest.results[cp.effective_date]:
trade not in cp_day_results:
cash_trades_by_date[cp.effective_date].append(trade)
else:
cp.scale_date = None
Expand All @@ -953,12 +948,12 @@ def _handle_cash(self, backtest, risks, price_risk, strategy_pricing_dates, stra
backtest.cash_dict[d] = current_value
if d in backtest.cash_payments:
for cp in backtest.cash_payments[d]:
cp_day_risk_results = backtest.results[cp.effective_date][price_risk]
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
value = cp_day_risk_results[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}')
Expand Down
6 changes: 3 additions & 3 deletions gs_quant/backtests/strategy_systematic.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def __run_service_based_backtest(self, start: datetime.date, end: datetime.date,
calc_measures = (FlowVolBacktestMeasure.PNL,)
basic_bt_request = BasicBacktestRequest(date_cfg, self.__trades, calc_measures, self.__delta_hedge_frequency,
CostPerTransaction(TransactionCostModel.Fixed, 0), None)
basic_bt_response = GsBacktestXassetApi.calculate_basic_backtest(basic_bt_request)
basic_bt_response = GsBacktestXassetApi.calculate_basic_backtest(basic_bt_request, decode_instruments=False)
risks = tuple(
BacktestRisk(name=k.value,
timeseries=tuple(FieldValueMap(date=d, value=r.result) for d, r in v.items()))
Expand All @@ -160,14 +160,14 @@ def __run_service_based_backtest(self, start: datetime.date, end: datetime.date,
if FlowVolBacktestMeasure.portfolio in measures:
for d in sorted(set().union(basic_bt_response.portfolio.keys(), basic_bt_response.transactions.keys())):
if d in basic_bt_response.portfolio:
positions = [{'instrument': i.to_dict() if i is not None else {}} for
positions = [{'instrument': i if i is not None else {}} for
i in basic_bt_response.portfolio[d]]
else:
positions = []
transactions = []
if d in basic_bt_response.transactions:
for t in basic_bt_response.transactions[d]:
trades = [{'instrument': i.to_dict() if i is not None else {},
trades = [{'instrument': i if i is not None else {},
'price': t.portfolio_price}
for i in t.portfolio] if t.portfolio is not None else []
transactions.append({'type': t.direction.value, 'trades': trades})
Expand Down
1 change: 0 additions & 1 deletion gs_quant/markets/portfolio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ def get_performance_report(self, tags: Dict = None) -> PerformanceReport:
position_source_id=self.id,
report_type='Portfolio Performance Analytics',
tags=tags)
reports = [report for report in reports if report.parameters.tags == tags]
if len(reports) == 0:
raise MqError('No performance report found.')
return PerformanceReport.from_target(reports[0])
Expand Down
Loading

0 comments on commit 654f9c4

Please sign in to comment.