Skip to content

Commit

Permalink
Risk result handling, backtesting and tracing enhancements (#276)
Browse files Browse the repository at this point in the history
Co-authored-by: Tan, Rachel Wei Swin [GBM Public] <rachelweiswin.tan@ln.email.gs.com>
  • Loading branch information
rtsscy and Tan, Rachel Wei Swin [GBM Public] committed Aug 15, 2023
1 parent e45dbec commit 5bb32bb
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 144 deletions.
1 change: 1 addition & 0 deletions gs_quant/backtests/action_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def apply_action(
self,
state: Union[date, Iterable[date]],
backtest: TBaseBacktest,
trigger_info: Any,
) -> Any:
pass

Expand Down
298 changes: 171 additions & 127 deletions gs_quant/backtests/generic_engine.py

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions gs_quant/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
under the License.
"""

import datetime as dt

from gs_quant.target.common import *
from gs_quant.target.common import PayReceive as _PayReceive
from gs_quant.target.common import RiskMeasure as __RiskMeasure
from gs_quant.target.common import RiskMeasureType, AssetClass
from gs_quant.target.workflow_quote import HedgeTypes


class PositionType(Enum):
Expand All @@ -35,14 +34,12 @@ class PositionType(Enum):


class DateLimit(Enum):

""" Datetime date constants """

LOW_LIMIT = dt.date(1952, 1, 1)


class PayReceive(EnumBase, Enum):

"""Pay or receive fixed"""

Pay = 'Pay'
Expand Down Expand Up @@ -107,3 +104,7 @@ def __repr__(self):
def parameter_is_empty(self):
return self.parameters is None


global_config.decoders[Optional[HedgeTypes]] = decode_hedge_type
global_config.decoders[HedgeTypes] = decode_hedge_type
global_config.decoders[Optional[Tuple[HedgeTypes, ...]]] = decode_hedge_types
2 changes: 2 additions & 0 deletions gs_quant/markets/securities.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ def get_data_series(self,
coordinate = self.get_data_coordinate(measure, dimensions, frequency)
if coordinate is None:
raise MqValueError(f"No data coordinate found for parameters: {measure, dimensions, frequency}")
elif coordinate.dataset_id is None:
raise MqValueError(f"Measure '{measure.value}' not found for asset: {self.__id}")
return coordinate.get_series(start=start, end=end, dates=dates, operator=operator)

def get_latest_close_price(self) -> float:
Expand Down
2 changes: 1 addition & 1 deletion gs_quant/models/risk_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def get_asset_universe(self,
universe = [data.get('assetData').get('universe') for data in results]
dates_to_universe = dict(zip(dates, universe))
if format == ReturnFormat.DATA_FRAME:
dates_to_universe = pd.DataFrame(dates_to_universe)
return pd.DataFrame.from_dict(dates_to_universe, orient='index').T
return dates_to_universe

def get_factor(self, name: str, start_date: dt.date = None, end_date: dt.date = None) -> Factor:
Expand Down
4 changes: 3 additions & 1 deletion gs_quant/risk/result_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ def mmapi_pca_table_handler(result: dict, risk_key: RiskKey, _instrument: Instru
r['coordinate'].update({'layer3': r['layer3']})
r['coordinate'].update({'level': r['level']})
r['coordinate'].update({'sensitivity': r['sensitivity']})
r['coordinate'].update({'irDelta': r['irDelta']})
coordinates.append(r['coordinate'])

mappings = (('mkt_type', 'type'),
Expand All @@ -337,7 +338,8 @@ def mmapi_pca_table_handler(result: dict, risk_key: RiskKey, _instrument: Instru
('layer2', 'layer2'),
('layer3', 'layer3'),
('level', 'level'),
('sensitivity', 'sensitivity'))
('sensitivity', 'sensitivity'),
('irDelta', 'irDelta'))

return __dataframe_handler(coordinates, mappings, risk_key, request_id=request_id)

Expand Down
4 changes: 2 additions & 2 deletions gs_quant/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,11 @@ def __request(
return_request_id: Optional[bool] = False,
use_body: bool = False
) -> Union[Base, tuple, dict]:
kwargs, url = self._build_request_params(method, path, payload, request_headers, include_version, timeout,
use_body, "data")
span = Tracer.get_instance().active_span
tracer = Tracer(f'http:/{path}') if span else contextlib.nullcontext()
with tracer as scope:
kwargs, url = self._build_request_params(method, path, payload, request_headers, include_version, timeout,
use_body, "data")
if scope:
scope.span.set_tag('path', path)
scope.span.set_tag('timeout', timeout)
Expand Down
6 changes: 4 additions & 2 deletions gs_quant/timeseries/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,9 +900,11 @@ def percentiles(x: pd.Series, y: Optional[pd.Series] = None, w: Union[Window, in
return pd.Series(dtype=float)

res = pd.Series(dtype=np.dtype(float))
convert_to_date = not isinstance(x.index, pd.DatetimeIndex)

for idx, val in y.items():
sample = x.loc[(x.index > (idx - w.w).date()) & (x.index <= idx)] if isinstance(w.w, pd.DateOffset) \
else x[:idx][-w.w:]
sample = x.loc[(x.index > ((idx - w.w).date() if convert_to_date else idx - w.w)) & (x.index <= idx)] if \
isinstance(w.w, pd.DateOffset) else x[:idx][-w.w:]
res.loc[idx] = percentileofscore(sample, val, kind='mean')

if isinstance(w.r, pd.DateOffset):
Expand Down
101 changes: 94 additions & 7 deletions gs_quant/tracing/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
specific language governing permissions and limitations
under the License.
"""
import datetime as dt
import logging
import traceback
from contextlib import ContextDecorator
from typing import Tuple, Optional

import pandas as pd
from opentracing import Span, UnsupportedFormatException
from opentracing import Tracer as OpenTracer
from opentracing.mocktracer import MockTracer
Expand Down Expand Up @@ -101,18 +104,65 @@ def get_spans():
return Tracer.get_instance().finished_spans()

@staticmethod
def gather_data():
def plot(reset=False):
try:
import plotly.express as px
except ImportError:
_logger.warning('Package "plotly" required to visualise the trace, printing instead')
Tracer.print(reset)
return
color_list = ('#2b76f7', '#5a8efb', '#79aaff', '#89bbff', '#cdddff')
error_color = 'rgb(244, 127, 114)'
ordered_spans, _ = Tracer.gather_data(False)
span_df = pd.DataFrame.from_records([(
f'#{i}',
f'{s.operation_name} {int(1000 * (s.finish_time - s.start_time)):,.0f}ms',
dt.datetime.fromtimestamp(s.start_time),
dt.datetime.fromtimestamp(s.finish_time),
'\n '.join([f'{k}={v}' for k, v in s.tags.items()]) if s.tags else '',
) for i, (depth, s) in enumerate(ordered_spans)], columns=['id', 'operation', 'start', 'end', 'tags'])
color_map = {f'#{i}': error_color if 'error' in s.tags else color_list[depth % len(color_list)] for
i, (depth, s) in enumerate(ordered_spans)}
fig = px.timeline(
data_frame=span_df,
width=1000,
height=40 + 30 * len(span_df),
x_start="start",
x_end="end",
y="id",
hover_data='tags',
text='operation',
color='id',
color_discrete_map=color_map
)
fig.update_layout(
showlegend=False,
yaxis_visible=False,
yaxis_showticklabels=False,
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
margin={'l': 0, 't': 0, 'b': 0, 'r': 0}
)
if reset:
Tracer.reset()
fig.show()

@staticmethod
def gather_data(as_string: bool = True):
spans = Tracer.get_spans()
spans_by_parent = {}

for span in reversed(spans):
spans_by_parent.setdefault(span.parent_id, []).append(span)

def _build_tree(parent_span, depth):
name = f'{"* " * depth}{parent_span.operation_name}'
elapsed = (parent_span.finish_time - parent_span.start_time) * 1000 if parent_span.finished else 'N/A'
error = " [Error]" if parent_span.tags.get('error', False) else ""
lines.append(f'{name:<50}{elapsed:>8.1f} ms{error}')
if as_string:
name = f'{"* " * depth}{parent_span.operation_name}'
elapsed = (parent_span.finish_time - parent_span.start_time) * 1000 if parent_span.finished else 'N/A'
error = " [Error]" if parent_span.tags.get('error', False) else ""
lines.append(f'{name:<50}{elapsed:>8.1f} ms{error}')
else:
lines.append((depth, parent_span))
for child_span in reversed(spans_by_parent.get(parent_span.context.span_id, [])):
_build_tree(child_span, depth + 1)

Expand All @@ -122,8 +172,11 @@ def _build_tree(parent_span, depth):
_build_tree(span, 0)
total += (span.finish_time - span.start_time) * 1000

tracing_str = '\n'.join(lines)
return tracing_str, total
if as_string:
tracing_str = '\n'.join(lines)
return tracing_str, total
else:
return lines, total

@staticmethod
def print(reset=True):
Expand All @@ -132,3 +185,37 @@ def print(reset=True):
if reset:
Tracer.reset()
return tracing_str, total


def parse_tracing_line_args(line: str) -> Tuple[Optional[str], bool]:
stripped = tuple(s for s in line.split(' ') if s != '')
if len(stripped) > 0 and stripped[0] in ('chart', 'plot', 'graph'):
return tuple(stripped[1:]) if len(stripped[1:]) else None, True
return stripped if len(stripped) else None, False


try:
# Attempt to import/register some jupyter magic
import gs_quant_internal.tracing.jupyter # noqa
except ImportError:
try:
from IPython.core.magic import register_cell_magic
from IPython import get_ipython

@register_cell_magic("trace")
def trace_ipython_cell(line, cell):
"""Wraps the execution of a cell in a tracer call and prints"""
span_name, show_chart = parse_tracing_line_args(line)
if cell is None:
return line
with Tracer(label=span_name):
res = get_ipython().run_cell(cell)
if res.error_in_exec:
Tracer.record_exception(res.error_in_exec)
if show_chart:
Tracer.plot(True)
else:
Tracer.print(True)
return None
except Exception:
pass

0 comments on commit 5bb32bb

Please sign in to comment.