Factor Attribution

Contents

Factor Attribution#

In this tutorial we are going to show how to run a factor-based attribution using the Reporting API. This API is currently under active development.

The steps are as follows:

  • Uploading a set of portfolios

  • Organizing the portfolios into a hierarchy

  • Creating a factor model

  • Running a factor attribution report

  • Pulling out different aggregations and drill-downs

Imports & Setup#

For this tutorial notebook, you will need to import the following packages.

import datetime as dt
import polars as pl

from bayesline.apiclient import BayeslineApiClient
from bayesline.api.equity import (
    FactorRiskModelSettings,
    UniverseSettings,
    ExposureSettings,
    ContinuousExposureGroupSettings,
    CategoricalExposureGroupSettings,
    ModelConstructionSettings,
    PortfolioHierarchySettings,
    ReportSettings,
    FactorAttributionDrilldownReportSettings,
)

We will also need to have a Bayesline API client configured.

bln = BayeslineApiClient.new_client(
    endpoint="https://[ENDPOINT]",
    api_key="[API-KEY]",
)

We will first upload a portfolio and set a hierarchy. The steps followed here are the same as in the Portfolio Hierarchies Tutorial.

uploader = bln.equity.uploaders.get_data_type("portfolios").get_or_create_dataset("portfolio-attribution-demo")
portfolio_df = pl.DataFrame({
    "portfolio_id": [
        "AGTHX", "AGTHX", "AGTHX", "AGTHX",
        "FCNTX", "FCNTX",
        "VADGX", "VADGX", "VADGX", "VADGX", "VADGX", "VADGX",
        "SPX", "SPX", "SPX", "SPX", "SPX", "SPX", "SPX", "SPX",
    ],
    "asset_id": [
        # AGTHX
        "02079K305", "02079K305", 
        "2588173", "2588173",

        # FCNTX
        "67066G10", "67066G10",

        # VADGX
        "02079K305", "02079K305", 
        "2588173", "2588173", 
        "67066G10", "67066G10",

        # SPX
        "02079K305", "02079K305", 
        "2588173", "2588173",
        "67066G10", "67066G10",
        "85371710", "85371710",

    ],  
    "asset_id_type": [
        # AGTHX
        "cusip9", "cusip9", "sedol7", "sedol7",

        # FCNTX
        "cusip8", "cusip8",

        # VADGX
        "cusip9", "cusip9", "sedol7", "sedol7", "cusip8", "cusip8",

        # SPX
        "cusip9", "cusip9", "sedol7", "sedol7", "cusip8", "cusip8", "cusip8", "cusip8",
        
    ],
    "date": [
        # AGTHX
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        dt.date(2025, 1, 1), dt.date(2025, 1, 31),

        # FCNTX
        dt.date(2025, 1, 1), dt.date(2025, 1, 31),

        # VADGX
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        
        # SPX
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
        dt.date(2025, 1, 1), dt.date(2025, 1, 31), 
    ],
    "value": [
        # AGTHX
        0.5, 0.55, 
        0.5, 0.45,

        # FCNTX
        1.0, 1.0,

        # VADGX
        0.3, 0.5, 
        0.4, 0.2,
        0.3, 0.25,

        # SPX
        0.3, 0.3, 
        0.4, 0.2,
        0.3, 0.25,
        0.3, 0.25,
    ],
})

portfolio_df
shape: (20, 5)
portfolio_idasset_idasset_id_typedatevalue
strstrstrdatef64
"AGTHX""02079K305""cusip9"2025-01-010.5
"AGTHX""02079K305""cusip9"2025-01-310.55
"AGTHX""2588173""sedol7"2025-01-010.5
"AGTHX""2588173""sedol7"2025-01-310.45
"FCNTX""67066G10""cusip8"2025-01-011.0
"SPX""2588173""sedol7"2025-01-310.2
"SPX""67066G10""cusip8"2025-01-010.3
"SPX""67066G10""cusip8"2025-01-310.25
"SPX""85371710""cusip8"2025-01-010.3
"SPX""85371710""cusip8"2025-01-310.25
uploader.fast_commit(portfolio_df, mode="append")
UploadCommitResult(version=1, committed_names=[])
portfolio_hierarchy_df = pl.DataFrame(
    {
        "fund": ["some_fund", "some_fund", "some_fund", "some_fund"],
        "book": ["book_1", "book_1", "book_2", "book_2"],
        "ticker": ["AGTHX", "FCNTX", "VADGX", "SPX"],
        "portfolio_id": ["AGTHX", "FCNTX", "VADGX", "SPX"],
    }
)
portfoliohierarchy_settings = PortfolioHierarchySettings.from_polars(
    portfolio_hierarchy_df, portfolio_source="portfolio-attribution-demo"
)
portfoliohierarchy_settings.to_polars()
shape: (4, 5)
fundbooktickerportfolio_idbenchmark_id
strstrstrstrnull
"some_fund""book_1""AGTHX""AGTHX"null
"some_fund""book_1""FCNTX""FCNTX"null
"some_fund""book_2""VADGX""VADGX"null
"some_fund""book_2""SPX""SPX"null

Now that we have the portfolio hiearachy in place, we define a basic factor model.

factorriskmodel_settings = FactorRiskModelSettings(
    universe=UniverseSettings(dataset="Bayesline-US-All-1y"),
    exposures=ExposureSettings(
        exposures=[
            ContinuousExposureGroupSettings(hierarchy="market"),
            CategoricalExposureGroupSettings(hierarchy="trbc"),
            ContinuousExposureGroupSettings(hierarchy="style"),
        ]
    ),
    modelconstruction=ModelConstructionSettings(
        zero_sum_constraints={"trbc": "mcap_weighted"}
    ),
)

We then define and load the report. We have different multi-period aggregation settings available, which we do not go into here.

# construct the report
report_settings = ReportSettings(
    report=FactorAttributionDrilldownReportSettings(),
    risk_model=factorriskmodel_settings,
)
report_engine = bln.equity.portfolioreport.load(
    report_settings,
    hierarchy_ref_or_settings=portfoliohierarchy_settings,
)

The order defines the axes over which we can aggregate. For example by putting fund, book and ticker in our portfolio axis, we can filter and aggregate along this dimension at a later stage. The concepts of fund, book, and ticker were defined through the PortfolioHierarchySettings above.

order = {
    "date": ["date"],
    "portfolio": ["fund", "book", "ticker"],
    "factor_and_idio": ["type", "factor", "factor_group"],
    "asset": ["input_asset_id"],
}
report_accessor = report_engine.get_report(
    order,
    date_start="2025-01-02",
    date_end="2025-01-31",
    subtotals=["date", "portfolio", "factor_and_idio", "asset"],
)

Let’s first look at the return attribution from all sources at the ticker level (recall that ticker is defined in the portfolio hierarchy). For a specific ticker, we want to know the return attribution to different assets at each date.

# for a specific ticker, the return attribution across dates and assets
(
    report_accessor.get_data(
        [("ticker", "AGTHX")],  # filters: [ticker=AGTHX]
        expand=("date", "input_asset_id",), # combinations define the rows
        value_cols=("Portfolio",),  # exclude benchmark / active
    )
    .select("ticker", "date", "input_asset_id", "Portfolio")
)
shape: (60, 4)
tickerdateinput_asset_idPortfolio
strstrstrf32
"AGTHX""2025-01-02""02079K305"0.000345
"AGTHX""2025-01-02""2588173"-0.00345
"AGTHX""2025-01-02""67066G10"0.0
"AGTHX""2025-01-03""02079K305"0.006256
"AGTHX""2025-01-03""2588173"0.005673
"AGTHX""2025-01-30""2588173"-0.029748
"AGTHX""2025-01-30""67066G10"0.0
"AGTHX""2025-01-31""02079K305"0.008625
"AGTHX""2025-01-31""2588173"0.000076
"AGTHX""2025-01-31""67066G10"0.0

Using different settings, we can split out the returns across all sources (factors and idio) at a different level. Below we show the returns on a specific date, for a specific book, split out by factor.

# for a specific book on a specific date, the factor return attribution across assets
(
    report_accessor.get_data(
        [("date", "2025-01-02"), ("book", "book_1")],  # filters
        expand=("type", "factor_group", "factor",),
        value_cols=("Portfolio",),
    )
    .select("date", "book", "type", "factor_group", "factor", "Portfolio")
)
shape: (22, 6)
datebooktypefactor_groupfactorPortfolio
strstrstrstrstrf32
"2025-01-02""book_1""Factors""market""Market"-0.000255
"2025-01-02""book_1""Factors""trbc""Energy"0.0
"2025-01-02""book_1""Factors""trbc""Basic Materials"0.0
"2025-01-02""book_1""Factors""trbc""Industrials"0.0
"2025-01-02""book_1""Factors""trbc""Consumer Cyclicals"0.0
"2025-01-02""book_1""Factors""style""Volatility"0.007958
"2025-01-02""book_1""Factors""style""Momentum"0.001951
"2025-01-02""book_1""Factors""style""Dividend"0.000043
"2025-01-02""book_1""Factors""style""Leverage"0.000005
"2025-01-02""book_1""Idiosyncratic"""""0.010206

We can drill down further by including the asset dimension. This decomposes the returns of the assets in a specific book, on a specific date.

# for a specific book on a specific date, the factor return attribution by asset
(
    report_accessor.get_data(
        [("date", "2025-01-02"), ("book", "book_1")],  # filters
        expand=("type", "factor_group", "factor", "input_asset_id"),
        value_cols=("Portfolio",),
    )
    .select("date", "book", "type", "factor_group", "factor", "input_asset_id", "Portfolio")
)
shape: (66, 7)
datebooktypefactor_groupfactorinput_asset_idPortfolio
strstrstrstrstrstrf32
"2025-01-02""book_1""Factors""market""Market""02079K305"-0.000063
"2025-01-02""book_1""Factors""market""Market""2588173"-0.000062
"2025-01-02""book_1""Factors""market""Market""67066G10"-0.000129
"2025-01-02""book_1""Factors""trbc""Energy""02079K305"0.0
"2025-01-02""book_1""Factors""trbc""Energy""2588173"0.0
"2025-01-02""book_1""Factors""style""Leverage""2588173"0.000002
"2025-01-02""book_1""Factors""style""Leverage""67066G10"0.000002
"2025-01-02""book_1""Idiosyncratic""""""02079K305"0.000308
"2025-01-02""book_1""Idiosyncratic""""""2588173"-0.001452
"2025-01-02""book_1""Idiosyncratic""""""67066G10"0.01135

By using the filters, we can also isolate the specific return attribution.

# the idiosyncratic returns in a specific fund
(
    report_accessor.get_data(
        [("fund", "some_fund"), ("type", "Idiosyncratic")],  # filters
        expand=("date", "input_asset_id",),
        value_cols=("Portfolio",),
    )
    .select("fund", "date", "input_asset_id", "Portfolio")
)
shape: (60, 4)
funddateinput_asset_idPortfolio
strstrstrf32
"some_fund""2025-01-02""02079K305"0.00034
"some_fund""2025-01-02""2588173"-0.001895
"some_fund""2025-01-02""67066G10"0.009112
"some_fund""2025-01-03""02079K305"-0.000533
"some_fund""2025-01-03""2588173"-0.00056
"some_fund""2025-01-30""2588173"-0.021893
"some_fund""2025-01-30""67066G10"-0.002691
"some_fund""2025-01-31""02079K305"0.006192
"some_fund""2025-01-31""2588173"0.000446
"some_fund""2025-01-31""67066G10"-0.01452