def get_factor_risk_report(self, risk_model_id: str = None, fx_hedged: bool = None) -> FactorRiskReport: if self.positioned_entity_type in [ EntityType.PORTFOLIO, EntityType.ASSET ]: position_source_type = self.positioned_entity_type.value.capitalize( ) reports = GsReportApi.get_reports( limit=100, position_source_type=position_source_type, position_source_id=self.id, report_type=f'{position_source_type} Factor Risk') if fx_hedged: reports = [ report for report in reports if report.parameters.fx_hedged == fx_hedged ] if risk_model_id: reports = [ report for report in reports if report.parameters.risk_model == risk_model_id ] if len(reports) > 1: raise MqError( f'This {position_source_type} has more than one factor risk report that matches ' 'your parameters. Please specify the risk model ID and fxHedged value in the ' 'function parameters.') if len(reports) == 0: raise MqError( f'This {position_source_type} has no factor risk reports that match your parameters.' ) return FactorRiskReport.from_target(reports[0]) raise NotImplementedError
def poll_report(self, report_id: str, timeout: int = 600, step: int = 30) -> ReportStatus: poll = True timeout = 1800 if timeout > 1800 else timeout step = 15 if step < 15 else step end = dt.datetime.now() + dt.timedelta(seconds=timeout) while poll and dt.datetime.now() <= end: try: status = Report.get(report_id).status if status not in {ReportStatus.error, ReportStatus.cancelled, ReportStatus.done}: _logger.info(f'Report is {status} as of {dt.datetime.now().isoformat()}') time.sleep(step) else: poll = False if status == ReportStatus.error: raise MqError(f'Report {report_id} has failed for {self.id}. \ Please reach out to the Marquee team for assistance.') elif status == ReportStatus.cancelled: _logger.info(f'Report {report_id} has been cancelled. Please reach out to the \ Marquee team if you believe this is a mistake.') return status else: _logger.info(f'Report {report_id} is now complete') return status except Exception as err: raise MqError(f'Could not fetch report status with error {err}') raise MqError('The report is taking longer than expected to complete. \ Please check again later or reach out to the Marquee team for assistance.')
def _inner(self, *args, **kwargs): if has(self, '_Basket__error_messages' ) and self._Basket__error_messages is not None: if len(self._Basket__error_messages) < 1: self._Basket__finish_initialization() for error_msg in error_msgs: if error_msg in self._Basket__error_messages: raise MqError(error_msg.value) return fn(self, *args, **kwargs)
def get_performance_report(self) -> PerformanceReport: reports = GsReportApi.get_reports( limit=100, position_source_type='Portfolio', position_source_id=self.id, report_type='Portfolio Performance Analytics') if len(reports) == 0: raise MqError('This portfolio has no performance report.') return PerformanceReport.from_target(reports[0])
def get_thematic_report(self) -> ThematicReport: if self.positioned_entity_type in [EntityType.PORTFOLIO, EntityType.ASSET]: position_source_type = self.positioned_entity_type.value.capitalize() reports = GsReportApi.get_reports(limit=100, position_source_type=position_source_type, position_source_id=self.id, report_type=f'{position_source_type} Thematic Analytics') if len(reports) == 0: raise MqError(f'This {position_source_type} has no thematic analytics report.') return ThematicReport.from_target(reports[0]) raise NotImplementedError
def get_factor_risk_report(self, risk_model_id: str = None, fx_hedged: bool = None) -> FactorRiskReport: position_source_type = self.positioned_entity_type.value.capitalize() reports = self.get_factor_risk_reports(fx_hedged=fx_hedged) if risk_model_id: reports = [report for report in reports if report.parameters.risk_model == risk_model_id] if len(reports) > 1: raise MqError(f'This {position_source_type} has more than one factor risk report that matches ' 'your parameters. Please specify the risk model ID and fxHedged value in the ' 'function parameters.') return reports[0]
def get_factor_risk_reports(self, fx_hedged: bool = None) -> List[FactorRiskReport]: if self.positioned_entity_type in [EntityType.PORTFOLIO, EntityType.ASSET]: position_source_type = self.positioned_entity_type.value.capitalize() reports = GsReportApi.get_reports(limit=100, position_source_type=position_source_type, position_source_id=self.id, report_type=f'{position_source_type} Factor Risk') if fx_hedged: reports = [report for report in reports if report.parameters.fx_hedged == fx_hedged] if len(reports) == 0: raise MqError(f'This {position_source_type} has no factor risk reports that match your parameters.') return [FactorRiskReport.from_target(report) for report in reports] raise NotImplementedError
def __edit_and_rebalance(self, edit_inputs: CustomBasketsEditInputs, rebal_inputs: CustomBasketsRebalanceInputs) -> CustomBasketsResponse: """ If updates require edit and rebalance, rebal will not be scheduled until/if edit report succeeds """ _logger.info('Current update request requires multiple reports. Your rebalance request will be submitted \ once the edit report has completed. Submitting basket edits now...') response = GsIndexApi.edit(self.id, edit_inputs) report_id = response.report_id self.__latest_create_report = GsReportApi.get_report(response.report_id) report_status = self.poll_report(report_id, timeout=600, step=15) if report_status != ReportStatus.done: raise MqError(f'The basket edit report\'s status is {status}. The current rebalance request will \ not be submitted in the meantime.') _logger.info('Your basket edits have completed successfuly. Submitting rebalance request now...') response = GsIndexApi.rebalance(self.id, rebal_inputs) return response
def get_fn(self, asset): asset_class = asset.asset_class asset_type = asset.get_type() fns = self.measure_map.get(asset_class, ()) def canonicalize(word): pruned = re.sub(r"[^\w]", "", word) return pruned.casefold() canonicalized = canonicalize(asset_type.value) for fn in fns: if (fn.asset_type is None or canonicalized in map(lambda x: canonicalize(x.value), fn.asset_type)) \ and (fn.asset_type_excluded is None or canonicalized not in map(lambda x: canonicalize(x.value), fn.asset_type_excluded)): return fn raise MqError("No measure {} defined for asset class {} and type {}".format(self.display_name, asset_class, asset_type))
def __request( self, method: str, path: str, payload: Optional[Union[dict, str, Base, pd.DataFrame]] = None, request_headers: Optional[dict] = None, cls: Optional[type] = None, try_auth=True, include_version: bool = True, timeout: int = DEFAULT_TIMEOUT ) -> Union[Base, tuple, dict]: is_dataframe = isinstance(payload, pd.DataFrame) if not is_dataframe: payload = payload or {} url = '{}{}{}'.format(self.domain, '/' + self.api_version if include_version else '', path) kwargs = { 'timeout': timeout } if method in ['GET', 'DELETE']: kwargs['params'] = payload elif method in ['POST', 'PUT']: headers = self._session.headers.copy() if request_headers: headers.update({**{'Content-Type': 'application/json'}, **request_headers}) else: headers.update({'Content-Type': 'application/json'}) kwargs['headers'] = headers if is_dataframe or payload: kwargs['data'] = payload if isinstance(payload, str) else json.dumps(payload, cls=JSONEncoder) else: raise MqError('not implemented') response = self._session.request(method, url, **kwargs) if response.status_code == 401: # Expired token or other authorization issue if not try_auth: raise MqRequestError(response.status_code, response.text, context='{} {}'.format(method, url)) self._authenticate() return self.__request(method, path, payload=payload, cls=cls, try_auth=False) elif not 199 < response.status_code < 300: raise MqRequestError(response.status_code, response.text, context='{} {}'.format(method, url)) elif 'application/x-msgpack' in response.headers['content-type']: res = msgpack.unpackb(response.content, raw=False) if cls: if isinstance(res, dict) and 'results' in res: res['results'] = self.__unpack(res['results'], cls) else: res = self.__unpack(res, cls) return res elif 'application/json' in response.headers['content-type']: res = json.loads(response.text) if cls: if isinstance(res, dict) and 'results' in res: res['results'] = self.__unpack(res['results'], cls) else: res = self.__unpack(res, cls) return res else: return {'raw': response}
def normalized_performance(report_id: str, aum_source: str = None, *, source: str = None, real_time: bool = False, request_id: Optional[str] = None) -> pd.Series: """ Returns the Normalized Performance of a performance report based on AUM source :param report_id: id of performance report :param aum_source: source to normalize pnl from, default is the aum source on your portfolio, if no aum source is set on your portfolio the default is gross :param source: name of function caller :param real_time: whether to retrieve intraday data instead of EOD :param request_id: server request id :return: portfolio normalized performance **Usage** Returns the normalized performance of the portfolio based on AUM source. If :math:`aum_source` is "Custom AUM": We read AUM from custom AUM uploaded to that portfolio and normalize performance based on that exposure If :math:`aum_source` is one of: Long, Short, RiskAumSource.Net, AumSource.Gross, we take these exposures from the calculated exposures based on daily positions :math:`NP(L/S)_{t} = SUM( PNL(L/S)_{t}/ ( EXP(L/S)_{t} ) - cPNL(L/S)_{t-1) ) if ( EXP(L/S)_{t} ) - cPNL(L/S)_{t-1)) > 0 else: 1/ SUM( PNL(L/S)_{t}/ ( EXP(L/S)_{t} ) - cPNL(L/S)_{t-1) )` For each leg, short and long, then: :math:`NP_{t} = NP(L)_{t} * (EXP(L)_{t} / AUM_{t}) + NP(S)_{t} * (EXP(S)_{t} / AUM_{t}) + 1` This takes into account varying AUM and adjusts for exposure change due to PNL where :math:`cPNL(L/S)_{t-1}` is your performance reports cumulative long or short PNL at date t-1 where :math:`PNL(L/S)_{t}` is your performance reports long or short pnl at date t where :math:`AUM_{t}` is portfolio exposure on date t where :math:`EXP(L/S)_{t}` is the long or short exposure on date t """ start_date = DataContext.current.start_time - relativedelta(1) end_date = DataContext.current.end_time start_date = start_date.date() end_date = end_date.date() performance_report = PerformanceReport.get(report_id) if not aum_source: port = GsPortfolioApi.get_portfolio( performance_report.position_source_id) aum_source = port.aum_source if port.aum_source else RiskAumSource.Gross else: aum_source = RiskAumSource(aum_source) constituent_data = performance_report.get_portfolio_constituents( fields=['assetId', 'pnl', 'quantity', 'netExposure'], start_date=start_date, end_date=end_date).set_index('date') aum_col_name = aum_source.value.lower() aum_col_name = f'{aum_col_name}Exposure' if aum_col_name != 'custom aum' else 'aum' # Split into long and short and aggregate across dates long_side = _return_metrics( constituent_data[constituent_data['quantity'] > 0], list(constituent_data.index.unique()), "long") short_side = _return_metrics( constituent_data[constituent_data['quantity'] < 0], list(constituent_data.index.unique()), "short") # Get aum source data if aum_source == RiskAumSource.Custom_AUM: custom_aum = pd.DataFrame( GsPortfolioApi.get_custom_aum( performance_report.position_source_id, start_date, end_date)) if custom_aum.empty: raise MqError( f'No custom AUM for portfolio {performance_report.position_source_id} between dates {start_date},' f' {end_date}') data = pd.DataFrame.from_records(custom_aum).set_index(['date']) else: data = performance_report.get_many_measures( [aum_col_name], start_date, end_date).set_index(['date']) long_side = long_side.join(data[[f'{aum_col_name}']], how='inner') short_side = short_side.join(data[[f'{aum_col_name}']], how='inner') long_side['longRetWeighted'] = (long_side['longMetrics'] - 1) * long_side['exposure'] * \ (1 / long_side[f'{aum_col_name}']) short_side['shortRetWeighted'] = (short_side['shortMetrics'] - 1) * short_side['exposure'] *\ (1 / short_side[f'{aum_col_name}']) combined = long_side[['longRetWeighted' ]].join(short_side[['shortRetWeighted']], how='inner') combined['normalizedPerformance'] = combined['longRetWeighted'] + combined[ 'shortRetWeighted'] + 1 return pd.Series(combined['normalizedPerformance'], name="normalizedPerformance").dropna()
def __request( self, method: str, path: str, payload: Optional[Union[dict, str, bytes, Base, pd.DataFrame]] = None, request_headers: Optional[dict] = None, cls: Optional[type] = None, try_auth: Optional[bool] = True, include_version: Optional[bool] = True, timeout: Optional[int] = DEFAULT_TIMEOUT, return_request_id: Optional[bool] = False ) -> Union[Base, tuple, dict]: is_dataframe = isinstance(payload, pd.DataFrame) if not is_dataframe: payload = payload or {} url = '{}{}{}'.format( self.domain, '/' + self.api_version if include_version else '', path) kwargs = {'timeout': timeout} if method in ['GET', 'DELETE']: kwargs['params'] = payload elif method in ['POST', 'PUT']: headers = self._session.headers.copy() if request_headers: headers.update(request_headers) if 'Content-Type' not in headers: headers.update( {'Content-Type': 'application/json; charset=utf-8'}) use_msgpack = headers.get( 'Content-Type') == 'application/x-msgpack' kwargs['headers'] = headers if is_dataframe or payload: kwargs['data'] = payload if isinstance(payload, (str, bytes)) else \ msgpack.dumps(payload, default=encode_default) if use_msgpack else \ json.dumps(payload, cls=JSONEncoder) else: raise MqError('not implemented') response = self._session.request(method, url, **kwargs) request_id = response.headers.get('x-dash-requestid') if response.status_code == 401: # Expired token or other authorization issue if not try_auth: raise MqRequestError(response.status_code, response.text, context='{} {}'.format(method, url)) self._authenticate() return self.__request(method, path, payload=payload, cls=cls, try_auth=False) elif not 199 < response.status_code < 300: raise MqRequestError( response.status_code, response.text, context=f'{response.headers.get("")}: {method} {url}') elif 'Content-Type' in response.headers: if 'application/x-msgpack' in response.headers['Content-Type']: res = msgpack.unpackb(response.content, raw=False) if cls: if isinstance(res, dict) and 'results' in res: res['results'] = self.__unpack(res['results'], cls) else: res = self.__unpack(res, cls) return (res, request_id) if return_request_id else res elif 'application/json' in response.headers['Content-Type']: res = json.loads(response.text) if cls: if isinstance(res, dict) and 'results' in res: res['results'] = self.__unpack(res['results'], cls) else: res = self.__unpack(res, cls) return (res, request_id) if return_request_id else res else: ret = {'raw': response} if return_request_id: ret['request_id'] = request_id return ret
def normalized_performance(report_id: str, aum_source: str = None, *, source: str = None, real_time: bool = False, request_id: Optional[str] = None) -> pd.Series: """ Returns the Normalized Performance of a performance report based on AUM source :param report_id: id of performance report :param aum_source: source to normalize pnl from, default is the aum source on your portfolio, if no aum source is set on your portfolio the default is gross :param source: name of function caller :param real_time: whether to retrieve intraday data instead of EOD :param request_id: server request id :return: portfolio normalized performance **Usage** Returns the normalized performance of the portfolio based on AUM source. If :math:`aum_source` is "Custom AUM": We read AUM from custom AUM uploaded to that portfolio and normalize performance based on that exposure If :math:`aum_source` is one of: Long, Short, RiskAumSource.Net, AumSource.Gross, we take these exposures from the calculated exposures based on daily positions :math:`NP_{t} = SUM( PNL_{t}/ ( AUM_{t} ) - cPNL_{t-1) ) if ( AUM_{t} ) - cPNL_{t-1) ) > 0 else: 1/ SUM( PNL_{t}/ ( AUM_{t} ) - cPNL_{t-1) )` This takes into account varying AUM and adjusts for exposure change due to PNL where :math:`cPNL_{t-1}` is your performance reports cumulative PNL at date t-1 where :math:`PNL_{t}` is your performance reports pnl at date t where :math:`AUM_{t}` is portfolio exposure on date t """ start_date = DataContext.current.start_date end_date = DataContext.current.end_date ppa_report = PerformanceReport.get(report_id) if not aum_source: port = GsPortfolioApi.get_portfolio(ppa_report.position_source_id) aum_source = port.aum_source if port.aum_source else RiskAumSource.Net else: aum_source = RiskAumSource(aum_source) aum_col_name = aum_source.value.lower() aum_col_name = f'{aum_col_name}Exposure' if aum_col_name != 'custom aum' else 'aum' measures = [aum_col_name, 'pnl' ] if aum_source != RiskAumSource.Custom_AUM else ['pnl'] data = ppa_report.get_many_measures(measures, start_date, end_date) data.loc[0, 'pnl'] = 0 data['cumulativePnlT-1'] = data['pnl'].cumsum(axis=0) data = pd.DataFrame.from_records(data).set_index(['date']) if aum_source == RiskAumSource.Custom_AUM: custom_aum = pd.DataFrame( GsPortfolioApi.get_custom_aum(ppa_report.position_source_id, start_date, end_date)) if custom_aum.empty: raise MqError( f'No custom AUM for portfolio {ppa_report.position_source_id} between dates {start_date},' f' {end_date}') custom_aum = pd.DataFrame.from_records(custom_aum).set_index(['date']) data = data.join(custom_aum.loc[:, aum_col_name], how='inner') if aum_source == RiskAumSource.Short: data[f'{aum_col_name}'] = -1 * data[f'{aum_col_name}'] data['normalizedExposure'] = data[f'{aum_col_name}'] - data[ 'cumulativePnlT-1'] data[ 'pnlOverNormalizedExposure'] = data['pnl'] / data['normalizedExposure'] data['normalizedPerformance'] = data['pnlOverNormalizedExposure'].cumsum( axis=0) + 1 data.loc[data.normalizedExposure < 0, 'normalizedPerformance'] = 1 / data.loc[:, 'normalizedPerformance'] return pd.Series(data['normalizedPerformance'], name="normalizedPerformance").dropna()
def _inner(self, *args, **kwargs): if has(self, '_Basket__error_messages'): for error_msg in error_msgs: if error_msg in self._Basket__error_messages: raise MqError(error_msg.value) return fn(self, *args, **kwargs)