Exemple #1
0
 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
Exemple #2
0
    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.')
Exemple #3
0
 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)
Exemple #4
0
 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])
Exemple #5
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
Exemple #6
0
 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]
Exemple #7
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
Exemple #8
0
 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
Exemple #9
0
    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))
Exemple #10
0
    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}
Exemple #11
0
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()
Exemple #12
0
    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()
Exemple #14
0
 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)