Factor Model#
Abstract interface and concrete implementations for the factor-model attribution methodology (see Attribution Methodology for the conceptual background), plus the factor-returns loaders and synthetic exposure generator that feed it.
- class FactorModelAttributionInterface(portfolio_returns: DataFrame, benchmark_returns: DataFrame, daily_portfolio_weights: DataFrame, by_factor_factor_returns: DataFrame, asset_returns: DataFrame)[source]#
Bases:
object
- class FactorModelAttribution(portfolio_returns: DataFrame, benchmark_returns: DataFrame, daily_portfolio_weights: DataFrame, by_factor_factor_returns: DataFrame, asset_returns: DataFrame)[source]#
Bases:
FactorModelAttributionInterface- last_day_decomposition(title)[source]#
Plots positive and negative factor contributions as a diverging bar chart.
- Parameters:
series (pd.Series) – Index = factor names Values = contribution percentages (can be negative)
- multifactor_attribution()[source]#
Calculate the CAPM attribution model for a given date.
- Parameters:
date – The date for which to calculate the attribution.
- Returns:
A tuple containing the detailed attribution DataFrame and a totals DataFrame.
- plot_portfolio_weights_area(tickers=None, figsize=(14, 7), title='Portfolio Weights Over Time', y_label='Weight', date_fmt='%Y-%m')[source]#
Area plot of portfolio weights (long and short) over time.
- Parameters:
pct_df_returns (pd.DataFrame) – Index: dates (daily, or any time index) Columns: tickers Values: portfolio weights (can be positive or negative)
tickers (list, optional) – Subset of tickers to plot. If None, all columns are used.
figsize (tuple) – Figure size (width, height).
title (str) – Plot title.
y_label (str) – Label for the y-axis.
date_fmt (str) – Date format string for x-axis tick labels.
- Returns:
fig, ax
- Return type:
matplotlib Figure and Axes
- plot_stacked_weights(figsize=(14, 7), title='Return Attribution by Factor Over Time', y_label='Share of Total Return', date_fmt='%Y-%m', legend_loc='upper left')[source]#
Plot a stacked area chart of return attribution by factor over time, using absolute values normalized to total absolute return.
Each column’s contribution is expressed as abs(col) / sum(abs(all cols)), preserving the sign information via color-coded positive/negative stacking.
- Parameters:
figsize (tuple) – Size of the figure.
title (str) – Figure title.
y_label (str) – Label for y-axis.
date_fmt (str) – Date format string for x-axis.
legend_loc (str) – Location of the legend.
- plot_stacked_weights_abs_norm(figsize=(14, 7), title='Return Attribution by Factor Over Time', y_label='Share of Total Return', date_fmt='%Y-%m', legend_loc='upper left', days_of_smoothing=5)[source]#
Plot a stacked area chart of return attribution by factor over time. Each factor’s contribution = signed_value / sum(abs(all cols)). Positive factors stack above zero, negative factors stack below zero. The net area always sums to +100% or -100% depending on overall return direction.
- Parameters:
figsize (tuple) – Size of the figure.
title (str) – Figure title.
y_label (str) – Label for y-axis.
date_fmt (str) – Date format string for x-axis.
legend_loc (str) – Location of the legend.
days_of_smoothing (int) – Rolling window (in days) for smoothing the percentage shares.
- plot_stacked_weights_neg(figsize=(14, 7), title='Stacked Portfolio Weights Over Time', y_label='Weight', date_fmt='%Y-%m', legend_loc='upper left')[source]#
Plot a stacked area chart of portfolio weights (long & short) over time.
- Parameters:
pct_df_returns (pd.DataFrame) – Index: dates (DatetimeIndex or convertible to datetime) Columns: tickers Values: portfolio weights (can be positive or negative).
figsize (tuple) – Size of the figure.
title (str) – Figure title.
y_label (str) – Label for y-axis.
date_fmt (str) – Date format string for x-axis.
legend_loc (str) – Location of the legend.
- plot_stacked_weights_normalized(figsize=(14, 7), title='Return Attribution by Factor Over Time', y_label='Share of Total Return', date_fmt='%Y-%m', legend_loc='upper left', days_of_smoothing=5)[source]#
Plot a stacked area chart of return attribution by factor over time, using absolute values normalized to total absolute return (always sums to 100%).
- Parameters:
figsize (tuple) – Size of the figure.
title (str) – Figure title.
y_label (str) – Label for y-axis.
date_fmt (str) – Date format string for x-axis.
legend_loc (str) – Location of the legend.
days_of_smoothing (int) – Rolling window (in days) for smoothing the percentage shares.
- weighted_std(values, weights=None, hl=None)[source]#
Return the weighted average and standard deviation. values, weights – Numpy ndarrays with the same shape.
IF weights is None, then it will be calculated using WEXP the half_life
- wexp(num_periods, half_life)[source]#
Correr funcion con num_periods fija y varias half lifes parece que esta en dias
- class FactorModelArrowAttributionInterface(daily_portfolio_weights: Table, by_factor_factor_returns: dict[str, Table], asset_returns: Table, portfolio_returns: Table | None = None, benchmark_returns: Table | None = None)[source]#
Bases:
objectAbstract interface for multi-factor model attribution using PyArrow Tables.
Defines the contract for decomposing portfolio returns into contributions from individual risk factors (style, industry, etc.) using factor return data and portfolio weights.
- Parameters:
daily_portfolio_weights (pa.Table) – Daily portfolio weights with a date column and one float64 column per asset ticker.
by_factor_factor_returns (dict[str, pa.Table]) – Mapping of factor name to a
pa.Tableof per-asset factor returns. Each table has a date column and one float64 column per asset.asset_returns (pa.Table) – Total asset returns with a date column and one float64 column per asset ticker.
portfolio_returns (pa.Table, optional) – Pre-computed portfolio-level returns with columns
dateandportfolio_returns. IfNone, computed internally from weights and asset returns.benchmark_returns (pa.Table, optional) – Benchmark-level returns with a date column and a returns column.
- abstractmethod multifactor_attribution() None[source]#
Calculate the multi-factor attribution model across all dates.
For each date, computes the weighted sum of each factor’s per-asset returns using portfolio weights, producing a time series of daily factor contributions.
- Returns:
Results are stored in
self.portfolio_attribution_tsas apa.Tablewith a date column and one float64 column per factor.- Return type:
None
- class FactorModelArrowAttribution(daily_portfolio_weights: Table, by_factor_factor_returns: dict[str, Table], asset_returns: Table, portfolio_returns: Table | None = None, benchmark_returns: Table | None = None, date_column: str = 'date')[source]#
Bases:
FactorModelArrowAttributionInterfaceMulti-factor model attribution implementation using PyArrow.
Decomposes portfolio returns into contributions from style factors, industry factors, and an idiosyncratic residual using portfolio weights and per-asset factor return tables.
- Parameters:
daily_portfolio_weights (pa.Table) – Daily portfolio weights with a
date_columnand one float64 column per asset ticker.by_factor_factor_returns (dict[str, pa.Table]) – Mapping of factor name (e.g.
"f_momentum") to apa.Tableof per-asset factor returns, each with adate_columnand one float64 column per asset.asset_returns (pa.Table) – Total asset returns with a
date_columnand one float64 column per asset ticker.portfolio_returns (pa.Table, optional) – Pre-computed portfolio returns with columns
dateandportfolio_returns. Computed from weights and asset returns if not provided.benchmark_returns (pa.Table, optional) – Benchmark returns table.
date_column (str, default
"date") – Name of the date column common to all input tables.
- portfolio_attribution_ts#
Daily factor contribution time series after
multifactor_attribution.- Type:
pa.Table
- simulated_rets#
Cumulative factor contributions after
time_series_calculation.- Type:
pa.Table
- pct_df_returns#
Daily percentage decomposition after
calc_pct_area.- Type:
pa.Table
- attribution_plots(title: str | None = None) None[source]#
Plot cumulative multi-factor attribution in a 3-panel figure.
Panels show: (1) total / factor / idiosyncratic cumulative returns, (2) style factor cumulative returns, (3) industry factor cumulative returns. Requires
time_series_calculationto have been run.- Parameters:
title (str, optional) – Overall figure title. Currently unused by the plot layout.
- calc_pct_area() None[source]#
Compute daily percentage decomposition of returns by factor.
Divides each factor’s daily contribution by the total portfolio return to get the fraction attributable to that factor. An idiosyncratic residual column (
f_idio_returns) captures the portion not explained by named factors. NaN and Inf values are forward-filled.Sets Attributes#
- pct_df_returnspa.Table
Daily percentage attribution with the same date column and one float64 column per factor plus
f_idio_returns.
- cummulative_pct_decomp() dict[str, float][source]#
Compute the cumulative percentage decomposition at the final date.
Takes the last row of
simulated_retsand expresses each factor’s cumulative contribution as a fraction of total excess returns, including an idiosyncratic residual.- Returns:
Mapping of factor name (plus
"idio_returns") to its percentage share of total cumulative excess return.- Return type:
dict[str, float]
- last_day_decomposition(title: str) None[source]#
Plot a horizontal bar chart of cumulative factor decomposition.
Shows each factor’s percentage contribution to total cumulative return at the final date, colored green (positive) or red (negative).
- Parameters:
title (str) – Chart title. Currently overridden internally to
"Factor Contributions to Strategy Returns".
- multifactor_attribution() None[source]#
Calculate multi-factor attribution across all dates.
For each date in
daily_portfolio_weights, computes the portfolio-weighted sum of each factor’s per-asset returns. Logs average factor coverage across the portfolio.Sets Attributes#
- portfolio_attribution_tspa.Table
Daily factor contributions with a date column and one float64 column per factor.
- plot_stacked_weights_normalized(figsize: tuple[int, int] = (14, 7), title: str = 'Return Attribution by Factor Over Time', y_label: str = 'Share of Total Return', date_fmt: str = '%Y-%m', legend_loc: str = 'upper left', days_of_smoothing: int = 5) None[source]#
Plot a normalized stacked area chart of factor contributions.
Each factor’s absolute contribution is divided by the sum of all absolute contributions to show relative importance over time, smoothed with a rolling window.
- Parameters:
figsize (tuple[int, int], default
(14, 7)) – Matplotlib figure size in inches.title (str, default
"Return Attribution by Factor Over Time") – Chart title. A rolling-window suffix is appended automatically.y_label (str, default
"Share of Total Return") – Y-axis label.date_fmt (str, default
"%Y-%m") – Date format string for the x-axis.legend_loc (str, default
"upper left") – Matplotlib legend location.days_of_smoothing (int, default
5) – Rolling window size in trading days for smoothing.
Loaders for per-factor returns files, keyed by factor name.
Provides a pandas-based loader (load_by_factor_returns_csv) and a PyArrow-based loader (load_by_factor_returns_arrow) that read one CSV file per factor from a directory, restrict columns to the portfolio’s assets, and optionally slice the result to a date range.
- load_by_factor_returns_arrow(path: str, available_factors: list[str], portfolio_weights: PortfolioWeights, start_date: date | None = None, end_date: date | None = None) dict[str, FactorReturns][source]#
Load per-factor returns CSV files into a dict of FactorReturns.
Arrow-based equivalent of load_by_factor_returns_csv. Each file in available_factors is read from path with only the date column and the portfolio’s asset columns retained, dates are parsed (trying
%Y-%m-%dthen%d/%m/%Y), and the result is sliced from the portfolio’s first weighted day onward before being validated and wrapped in a FactorReturns entity.- Parameters:
path (str) – Directory containing one CSV file per factor.
available_factors (list[str]) – File names (with extension) to load from path. Entries equal to
".gitkeep"are ignored.portfolio_weights (PortfolioWeights) – Portfolio weights entity; its first date lower-bounds the returned data and its asset names determine which factor-return columns are read and validated against.
start_date (datetime.date or None) – If given, the earliest date to keep. Raises if earlier than the data’s minimum date.
end_date (datetime.date or None) – If given, the latest date to keep. Silently clamped to the data’s maximum date if later.
- Returns:
Mapping of factor name (derived from the file name) to its FactorReturns entity.
- Return type:
dict[str, FactorReturns]
- Raises:
DateRangeError – If start_date is before the earliest available date in a factor’s file.
DataValidationError – If a factor’s asset columns are not a subset of the portfolio’s tickers, or the resulting table is empty.
MissingColumnError – If a factor’s file is missing its date column.
EntityTypeError – If a factor’s date or asset columns have an unexpected type.
- load_by_factor_returns_csv(path: str, available_factors: list[str], portfolio_weights: DataFrame, start_date: date | None = None, end_date: date | None = None) dict[str, DataFrame][source]#
Load per-factor returns CSV files into a dict of DataFrames.
Each file in available_factors is read from path, restricted to the columns present in portfolio_weights, and sliced from the portfolio’s first weighted day onward.
- Parameters:
path (str) – Directory containing one CSV file per factor.
available_factors (list[str]) – File names (with extension) to load from path. Entries equal to
".gitkeep"are ignored.portfolio_weights (pd.DataFrame) – Portfolio weights, dates as index and assets as columns; the columns determine which factor-return columns are read, and the index’s first date lower-bounds the returned data.
start_date (datetime.date or None) – If given, the earliest date to keep. Raises if earlier than the data’s minimum date.
end_date (datetime.date or None) – If given, the latest date to keep. Silently clamped to the data’s maximum date if later.
- Returns:
Mapping of factor name (derived from the file name) to its returns DataFrame, indexed by date with one column per asset.
- Return type:
dict[str, pd.DataFrame]
- Raises:
DateRangeError – If start_date is before the earliest available date in a factor’s file.
- generate_synthetic_exposure_dict(portfolio_weights: DataFrame, universe_returns: DataFrame, date_list: list[str] | None = None, factor_index: Collection[str] = ('ci_intercept', 'nc_beta', 'nc_log_market_cap', 'nc_momentum', 'nc_resid_vol', 'nc_value'), seed: int | None = 42, dtype: str = 'float32', na_percentage: float = 0.0) dict[date, DataFrame][source]#
Create an in-memory synthetic exposure dictionary that mirrors the real data shape, with an optional percentage of random NaN values injected.
- Parameters:
portfolio_weights (pd.DataFrame) – DataFrame with dates as index and assets as columns. Columns are used for synthetic exposure columns.
universe_returns (pd.DataFrame) – DataFrame containing universe assets (used for column reference).
date_list (list[str] | None) – Dates to generate (format ‘%Y-%m-%d’). If None, use all dates from portfolio_weights.index.
factor_index (iterable[str]) – Row index (factors) for each synthetic exposure matrix.
seed (int | None) – RNG seed for reproducibility. Use None for non-deterministic draws.
dtype (str) – Numpy/pandas dtype for the generated values.
na_percentage (float) – Fraction (0-1) of entries to replace with NaN randomly. Default = 0 (no missing data).
- Returns:
Keys are datetime.date objects, values are DataFrames with index=factor_index and columns=portfolio_weights.columns.
- Return type:
dict[datetime.date, pd.DataFrame]
- stock_factor_returns_loader(f_rets_by_stock_path: str, files_to_load: list[str], first_day_weights: Timestamp) dict[str, DataFrame][source]#
Load per-stock factor-returns CSV files into a dict of DataFrames.
- Parameters:
f_rets_by_stock_path (str) – Directory containing one CSV file per stock.
files_to_load (list[str]) – File names (with extension) to load from f_rets_by_stock_path. The stock name is derived from each file name (text before the first
.).first_day_weights (pd.Timestamp) – Earliest date to keep; each file is sliced from this date onward.
- Returns:
Mapping of stock name to its factor-returns DataFrame, indexed by date. Files listed in files_to_load that are not found are logged as a warning and omitted from the result.
- Return type:
dict[str, pd.DataFrame]
Immutable entity for per-asset factor return time-series.
This module provides FactorReturns, a frozen dataclass that
wraps a pa.Table of per-asset returns for a single risk factor
(e.g. momentum, size, an industry sector). Schema validation runs on
construction; an additional validate_asset_coverage() method
verifies that all asset columns are a subset of the portfolio’s tickers.
- class FactorReturns(table: Table, factor_name: str)[source]#
Bases:
objectImmutable wrapper around a PyArrow table of per-asset factor returns.
The expected schema is:
[date: timestamp(ns), ASSET1: float64, ASSET2: float64, ...]
Asset columns must be a subset of the portfolio’s tickers — partial coverage is allowed because the factor-model attribution engine handles missing assets via its daily-coverage logic. Return values may be negative.
Validation performed automatically on construction:
The table must contain at least one row.
A
"date"column of typetimestamp(ns)must be present.Every non-date column must be
float64.
- Parameters:
table (pa.Table) – PyArrow table whose first column is a
timestamp(ns)date column named"date"and whose remaining columns hold per-asset factor returns asfloat64.factor_name (str) – Human-readable name of the factor (e.g.
"f_momentum"). Used in error messages and logging.
- Raises:
DataValidationError – If the table is empty.
MissingColumnError – If the
"date"column is absent.EntityTypeError – If the
"date"column is not a timestamp or any asset column is notfloat64.
Examples
>>> import pyarrow as pa >>> tbl = pa.table({ ... "date": pa.array(["2024-01-02", "2024-01-03"]).cast(pa.timestamp("ns")), ... "AAPL": pa.array([0.003, -0.001], type=pa.float64()), ... }) >>> fr = FactorReturns(table=tbl, factor_name="f_momentum") >>> fr.asset_names ['AAPL']
- property asset_names: list[str]#
Return the list of asset column names (excludes
"date").- Returns:
Ordered list of asset ticker names present in the table.
- Return type:
list[str]
- factor_name: str#
- table: Table#
- validate_asset_coverage(portfolio_tickers: set[str]) None[source]#
Validate that all asset columns exist in the portfolio’s ticker set.
This check ensures that the factor file does not contain columns for assets that are absent from the portfolio. It does not require that every portfolio ticker is covered — partial coverage is acceptable and handled by the factor-model attribution engine.
- Parameters:
portfolio_tickers (set[str]) – Set of ticker symbols present in the portfolio.
- Raises:
DataValidationError – If any asset column name is not found in
portfolio_tickers.