Alpha signal building blocks for the optimalportfolios package.
This module provides individual alpha signal computation functions and
the AlphasData container. Each signal function is a standalone building
block that computes a cross-sectional score from asset prices — ready for
use in any portfolio optimisation workflow.
This module does not contain aggregation logic. The routing of assets
to signals, combination rules, and product-specific configurations belong
in the consuming application (e.g., a private rosaa package). This
module provides the bricks; the application builds the house.
optimalportfolios/alphas/
├── signals/
│ ├── momentum.py # compute_momentum_alpha()
│ ├── momentum_cluster.py # compute_momentum_cluster_alpha()
│ ├── low_beta.py # compute_low_beta_alpha()
│ ├── low_beta_cluster.py # compute_low_beta_cluster_alpha()
│ ├── residual_momentum.py # compute_residual_momentum_alpha()
│ ├── residual_momentum_cluster.py # compute_residual_momentum_cluster_alpha()
│ │ # + extract_rolling_clusters()
│ ├── managers_alpha.py # compute_managers_alpha()
│ ├── carry.py # compute_ra_carry_alphas()
│ ├── rolling_ewma_mean.py # estimate_rolling_ewma_means()
│ ├── utils.py # score_within_clusters()
│ └── tests/
│ └── signals_test.py
├── alpha_data.py # AlphasData container
├── backtest_alphas.py # Backtesting harness
└── README.md
The module distinguishes three stages in the alpha pipeline:
| Stage | What it is | Example |
|---|---|---|
| Raw signal | Observable quantity with units | Cumulative return, EWMA beta, regression residual |
| Score | Cross-sectional rank/z-score, dimensionless | Momentum z-score, negated beta rank |
| Alpha | Portfolio-ready signal after combination and CDF mapping | Combined score mapped to [-1, 1] |
Pipeline: raw signal → score → alpha.
Functions are named compute_*_alpha() because their primary output is
a score ready for aggregation into the final alpha vector. The raw signal
is returned as the second element for diagnostics.
The module provides 8 signal functions across two scoring modes:
| Signal | Fixed groups | Cluster-scored | Parameters |
|---|---|---|---|
| Momentum | compute_momentum_alpha |
compute_momentum_cluster_alpha |
long_span, short_span, vol_span |
| Low beta | compute_low_beta_alpha |
compute_low_beta_cluster_alpha |
beta_span |
| Residual momentum | compute_residual_momentum_alpha |
compute_residual_momentum_cluster_alpha |
beta_span, long_span, short_span, vol_span |
| Managers alpha | compute_managers_alpha |
— | alpha_span |
Fixed-group signals score assets within user-defined groups (e.g.,
"Equity", "Fixed Income") passed via group_data: pd.Series.
Cluster-scored signals score within time-varying statistical clusters extracted from the HCGL/LASSO covariance estimator. See the Cluster-Based Scoring section below.
Each signal function has the same interface:
def compute_*_alpha(
prices: pd.DataFrame,
..., # signal-specific inputs
returns_freq: Union[str, pd.Series], # single or mixed frequency
group_data: Optional[pd.Series], # for within-group scoring
**signal_params,
) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Returns (score, raw_signal)"""Cluster variants replace group_data with rolling_clusters:
def compute_*_cluster_alpha(
prices: pd.DataFrame,
...,
rolling_clusters: Dict[pd.Timestamp, pd.Series],
returns_freq: Union[str, pd.Series],
**signal_params,
) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Returns (cluster_score, raw_signal)"""Cross-sectional momentum score from EWMA-filtered risk-adjusted excess returns.
from optimalportfolios.alphas import compute_momentum_alpha
score, raw = compute_momentum_alpha(
prices=prices,
benchmark_price=benchmark,
returns_freq='ME',
long_span=12,
)| Parameter | Default | Description |
|---|---|---|
long_span |
12 | EWMA span for the long momentum signal |
short_span |
None | Short-term reversal subtraction (None = disabled) |
vol_span |
13 | EWMA span for vol normalisation (None = disabled) |
Pipeline: returns → excess returns (vs benchmark) → EWMA long/short filtered RA returns → cross-sectional score
Cross-sectional low-beta score ("betting against beta").
from optimalportfolios.alphas import compute_low_beta_alpha
score, raw_beta = compute_low_beta_alpha(
prices=prices,
benchmark_price=benchmark,
returns_freq='ME',
beta_span=12,
)| Parameter | Default | Description |
|---|---|---|
beta_span |
12 | EWMA span for beta estimation |
Pipeline: returns → EWMA regression beta → negate → cross-sectional score
Cross-sectional residual momentum score. Strips out single-benchmark beta exposure from asset returns, then applies EWMA long/short filtering (same as momentum.py). Sits between total-return momentum (no beta adjustment) and managers alpha (full MATF adjustment).
from optimalportfolios.alphas import compute_residual_momentum_alpha
score, raw_residual = compute_residual_momentum_alpha(
prices=prices,
benchmark_price=benchmark,
returns_freq='ME',
beta_span=12,
long_span=12,
vol_span=13,
)| Parameter | Default | Description |
|---|---|---|
beta_span |
12 | EWMA span for benchmark beta estimation |
long_span |
12 | EWMA span for the long momentum signal |
short_span |
None | Short-term reversal subtraction (None = disabled) |
vol_span |
13 | EWMA span for vol normalisation (None = disabled) |
Pipeline: returns → EWMA beta (lagged) → residual = r_t - β̂_{t-1}·r_bench_t → EWMA long/short filtered RA returns → cross-sectional score
References: Blitz, Huij & Martens (2011), "Residual Momentum", Journal of Empirical Finance, 18, 506-521.
Cross-sectional score from MATF factor model regression residuals.
from optimalportfolios.alphas import compute_managers_alpha
score, raw_alpha = compute_managers_alpha(
prices=asset_prices,
risk_factor_prices=factor_prices,
estimated_betas=rolling_data.get_y_betas(),
returns_freq='ME',
alpha_span=12,
)| Parameter | Default | Description |
|---|---|---|
alpha_span |
12 | EWMA span for smoothing regression residuals |
The _cluster signal variants use time-varying statistical clusters
instead of fixed user-defined groups for cross-sectional scoring.
Fixed asset-class groups (Equity, Fixed Income, Alternatives) impose a subjective structure on the cross-sectional scoring. Within-group z-scores assume assets in the same group are comparable — but a global equity ETF and a regional small-cap fund may behave very differently.
Cluster-based scoring replaces this with data-driven groups that evolve over time: assets with similar factor exposures are clustered together, and scoring happens within each cluster. This naturally handles regime shifts (e.g., a credit fund behaving more like equity during stress) and avoids the problem of heterogeneous groups inflating scores for outliers.
Clusters come from the HCGL/LASSO covariance estimator
(FactorCovarEstimator with LassoModelType.GROUP_LASSO_CLUSTERS):
- At each estimation date, the LASSO solver produces sparse factor loadings for each asset.
- Hierarchical clustering on the factor correlation matrix groups assets with similar systematic risk profiles.
- These clusters change at each rebalancing date as factor exposures evolve.
The clusters are stored in RollingFactorCovarData and extracted via
extract_rolling_clusters():
from optimalportfolios.alphas.signals.residual_momentum_cluster import extract_rolling_clusters
rolling_clusters = extract_rolling_clusters(
rolling_covar_data=taa_covar_data,
assets=universe_data.get_taa_prices().columns.tolist(),
)
# Dict[pd.Timestamp, pd.Series] → {date: pd.Series(ticker → cluster_id)}The shared score_within_clusters() function in utils.py handles the
row-by-row scoring:
from optimalportfolios.alphas.signals.utils import score_within_clusters
cluster_score = score_within_clusters(
raw_signal=raw_momentum, # T × N DataFrame
rolling_clusters=rolling_clusters,
)For each date:
- Look up the cluster assignment at the most recent estimation date.
- Score within each cluster independently (cross-sectional z-score).
- Singleton clusters (1 member) receive score 0.0.
- Dates before the first cluster estimation receive score 0.0.
- If
rolling_clustersis empty, falls back to global scoring.
from optimalportfolios.alphas import compute_momentum_cluster_alpha
score, raw = compute_momentum_cluster_alpha(
prices=prices,
benchmark_price=benchmark,
rolling_clusters=rolling_clusters,
returns_freq='ME',
long_span=12,
)In ROSAA APAC equity backtests (2005-2026), cluster-scored signals showed marginal improvement over fixed groups with approximately 5pp turnover increase (55% → 60% p.a.). The cluster variants are most useful for equity-only universes where regional groupings may not capture the true return structure; for diversified multi-asset portfolios the improvement is smaller.
All signal functions accept returns_freq as string or per-asset pd.Series:
# mixed: equities monthly, alternatives quarterly
returns_freq = pd.Series({'SPY': 'ME', 'EZU': 'ME', 'HF_Macro': 'QE', 'PE': 'QE'})
score, raw = compute_momentum_alpha(prices, returns_freq=returns_freq, ...)Mixed-frequency dispatch computes returns at each asset's native frequency, then concatenates and aligns before scoring.
When group_data is provided to the fixed-group variants, cross-sectional
scoring is computed within each group independently:
group_data = pd.Series({'SPY': 'Equity', 'TLT': 'Bonds', 'GLD': 'Alts'})
score, raw = compute_momentum_alpha(prices, group_data=group_data, ...)| Signal | Beta adjustment | Scoring | Use case |
|---|---|---|---|
| Momentum | None (benchmark-relative excess) | Fixed groups | General trend following |
| Momentum Cluster | None (benchmark-relative excess) | Data-driven clusters | Trend following with adaptive grouping |
| Low Beta | Single benchmark EWMA beta | Fixed groups | Defensive tilt |
| Low Beta Cluster | Single benchmark EWMA beta | Data-driven clusters | Defensive tilt with adaptive grouping |
| Residual Momentum | Single benchmark EWMA beta | Fixed groups | Beta-neutral momentum for multi-asset TAA |
| Residual Momentum Cluster | Single benchmark EWMA beta | Data-driven clusters | Beta-neutral momentum with adaptive grouping |
| Managers Alpha | Full MATF factor betas | Fixed groups | Fund/manager skill isolation |
Container for alpha computation results:
from optimalportfolios.alphas import AlphasData
data = AlphasData(
alpha_scores=combined_scores, # (T × N) — input to optimiser
momentum_score=mom_score, # fixed-group component scores
momentum_cluster_score=mom_cluster_score, # cluster component scores
beta_score=beta_score,
beta_cluster_score=beta_cluster_score,
managers_scores=mgr_score,
residual_momentum_score=res_score,
residual_momentum_cluster_score=res_cluster_score,
momentum=raw_momentum, # raw signals
momentum_cluster=raw_momentum_cluster,
beta=raw_beta,
beta_cluster=raw_beta_cluster,
managers_alphas=raw_alpha,
residual_momentum=raw_residual,
residual_momentum_cluster=raw_residual_cluster,
clusters=cluster_assignments, # T × N cluster IDs
)
# snapshot at a single date (all available components)
snapshot = data.get_alphas_snapshot(date=pd.Timestamp('2024-12-31'))
# export to dict (only non-None fields, safe for Excel)
output = data.to_dict()| Field | Type | Description |
|---|---|---|
alpha_scores |
DataFrame | Final combined scores — input to TAA optimiser |
momentum / momentum_score |
DataFrame | Raw / scored momentum (fixed groups) |
momentum_cluster / momentum_cluster_score |
DataFrame | Raw / scored momentum (clusters) |
beta / beta_score |
DataFrame | Raw / scored low-beta (fixed groups) |
beta_cluster / beta_cluster_score |
DataFrame | Raw / scored low-beta (clusters) |
managers_alphas / managers_scores |
DataFrame | Raw / scored managers alpha |
residual_momentum / residual_momentum_score |
DataFrame | Raw / scored residual momentum (fixed groups) |
residual_momentum_cluster / residual_momentum_cluster_score |
DataFrame | Raw / scored residual momentum (clusters) |
clusters |
DataFrame | Time-varying cluster assignments (T × N) |
All fields except alpha_scores are optional (default None).
Blitz D., Huij J., Martens M. (2011), "Residual Momentum", Journal of Empirical Finance, 18, 506-521.
Sepp A., Ossa I., and Kastenholz M. (2026), "Robust Optimization of Strategic and Tactical Asset Allocation for Multi-Asset Portfolios", The Journal of Portfolio Management, 52(4), 86-120.
Sepp A., Hansen E., and Kastenholz M. (2026), "Capital Market Assumptions and Strategic Asset Allocation Using Multi-Asset Tradable Factors", Working Paper.