Alpha signals¶
Author: Artur Sepp / First recorded: 2026-03-15
Implemented in OptimalPortfolios. Software citation: CITATION.cff.
An alpha signal is a dated characteristic used to compare assets when forming a portfolio.
The optimalportfolios.alphas layer computes raw characteristics, transforms them into
scores, and provides containers and evaluation tools; the evaluation tools are described on
signal diagnostics and alpha-rank portfolios. A score is
not automatically an expected return, a portfolio weight, or evidence of investment skill.
Overview¶
The signal constructors separate the calculation of an asset characteristic from the
choice of comparison universe. Most return (score, raw_signal). Portfolio objectives,
risk constraints, signal blends and any mapping into bounded alpha values are decisions
made by the consuming application. AlphasData stores those decisions; it does not apply
a CDF, enforce a range, or choose combination weights.
Signal Comparison¶
Family |
Raw characteristic and interpretation |
|---|---|
EWMA momentum |
Exponentially filtered returns, optionally relative to a benchmark and volatility-normalised. |
Classic momentum |
A fixed number of log returns, with an explicit hard skip of recent periods. |
Low beta |
Estimated beta to a benchmark; lower beta receives a higher score. |
Residual momentum |
Continuation in returns after subtracting a lagged single-benchmark exposure. |
Residual reversal |
Negative residual momentum with a shorter default horizon; recent residual losses receive higher scores. |
Risk-adjusted carry |
Supplied annual carry divided by annualised estimated volatility. |
Managers alpha |
Smoothed returns after subtracting supplied, dated multi-factor exposures. |
These are related signal definitions, not interchangeable replications of the published strategies in the references. In particular, the residual signal constructors use one benchmark; the cited residual-return studies use their own estimation and portfolio rules.
Inputs, notation, and assumptions¶
Convention |
This article |
|---|---|
Return basis |
Log returns at each asset’s native cadence; momentum subtracts the benchmark’s log return when one is supplied; carry is a supplied annual decimal yield |
Estimation grid |
Monthly ( |
Rebalancing grid |
None for the signals, which are formation-date values on their return grid; year ends ( |
Covariance units |
None enter the signals; volatility normalisation is per period for momentum and residual signals and annualised for carry |
Expected returns |
None; scores are cross-sectional standardisations, not return forecasts, and |
Weight state |
None: signals and scores are not weights; the rank profiler’s targets are described on signal diagnostics and alpha-rank portfolios |
Solver |
CVXPY with CLARABEL for the HCGL factor fit; the signals need no solver |
The notation follows the conventions page.
Supply positive finite adjusted price or NAV levels in a consistent currency and economic return basis. A price panel has dates in its index and asset identifiers in its columns. Group, cadence and loading labels must match those identifiers. Missing or stale prices need an explicit observation and eligibility policy before portfolio construction.
Naming Conventions¶
Object |
Convention in this package |
|---|---|
Raw signal \(x_{i,t}\) |
Method-specific units: log return, beta, filtered risk-adjusted return, or carry per unit volatility. |
Score \(z_{i,t}\) |
A cross-sectional transformation; its formula depends on the constructor and scoring mode. |
Combined alpha |
An application-supplied objective characteristic; no automatic range or return-unit calibration. |
For native observation date \(t\), the log return is \(r_{i,t}=\log(P_{i,t}/P_{i,t-1})\). A supplied benchmark produces relative log returns \(r_{i,t}-r_{b,t}\). This benchmark subtraction is different from subtracting a cash rate.
Use signals formed with information available at \(t\) for subsequent holding periods.
The EWMA signal wrappers use contemporaneous volatility weights (weight_lag=0);
they do not lag the finished signal for trading. A lagged regression beta is a separate
timing choice. Price timestamps, publication availability and execution time must all agree.
Mixed-Frequency Support¶
The paired signal constructors accept a cadence string or an asset-indexed cadence Series.
For EWMA signals, positive integer spans can also be mappings such as {"ME": 12, "QE": 4}.
They specify observation counts, not identical calendar windows. Classic momentum has separate
lookback and skip mappings. A 12-month hard lookback is distinct from an EWMA span of 12.
Fixed-group constructors compute scores within each cadence and, if supplied, each group,
then merge and forward-fill the output. Cluster constructors compute raw signals by cadence,
merge/forward-fill them, and then apply cluster scoring. Their fallback statistics can compare
assets across cadences. Managers alpha has its own residual-merge path and a scalar
alpha_span; it does not share all these grouping and span options.
The legacy carry-only score helper and estimate_rolling_ewma_means use a single cadence.
See mixed-frequency data for the observation, estimation and
rebalance clocks, and for the limits of treating filled prices as fresh information.
Within-Group Scoring (Fixed Groups)¶
group_data defines a comparison set; it does not impose portfolio allocation constraints.
An explicit benchmark keeps the regression reference common across groups. With no supplied
benchmark, low-beta and residual constructors use the equal-weight mean log return of the
assets passed into each fit. In the mixed-frequency fixed-group path, that fit can be a
cadence-by-group subset, so changing groups can also change the implicit benchmark.
EWMA momentum with benchmark_price=None uses unsubtracted asset returns. Despite the
current standard constructor’s docstring, it does not subtract an equal-weight benchmark.
Methodology¶
Signal Functions¶
Momentum¶
For a long span \(L\), the decay is \(\lambda=1-2/(L+1)\). The QIS long/short filter is normalised to unit variance for unit-variance white-noise input; it is not just an EWMA mean. With no short leg, zero initial state, and \(u_0=0\), its complete-observation form is
By default \(u_t\) is the benchmark-relative log return divided by its contemporaneous
per-period EWMA volatility, whose variance recursion starts at the first observed squared
return. Without a benchmark it is based on the asset’s own return.
vol_span=None disables this normalisation for momentum and residual signals.
A short leg subtracts another exponentially filtered component with the joint normalisation
defined in QIS’s EWMA implementation.
It does not discard a fixed number of recent returns.
Momentum defaults are long_span=12, short_span=None, vol_span=13 and
mean_adj_type=qis.MeanAdjType.NONE. The filter masks an initial warm-up period.
With volatility normalisation the raw signal is measured in units of per-period risk;
it is not an annual expected return.
Classic momentum¶
For a lookback of \(K\) observations and a skip of \(S\), the complete window is
The defaults include exactly 12 log returns and exclude the most recent one
(lookback_periods=12, skip_periods=1). There is no benchmark subtraction or
volatility scaling. An incomplete fixed window remains missing.
Low Beta¶
An EWMA regression estimates each asset’s beta to the supplied benchmark, or to the
fit’s equal-weight mean log return when the benchmark is absent. beta_span=12 and
mean_adj_type=EWMA are the defaults. The standard score is the negative of the
cross-sectional beta score; the second return value remains the unnegated beta.
The raw-beta path replaces exact zero loadings with NaN. A zero or missing score therefore needs interpretation in its scoring context. A low-beta characteristic does not itself construct a beta-neutral or leveraged betting-against-beta portfolio.
Point-in-time beta estimation: the low-beta, residual-momentum and residual-reversal
constructors, standard and cluster, call qis.EwmLinearModel.fit with
init_type=qis.InitType.X0. With mean_adj_type=EWMA, each running mean starts at its
column’s first return, including a late-starting asset’s first return after inception, and the
regression moments start at zero. A beta dated \(t\) therefore uses only returns up to \(t\), and
later observations cannot alter earlier signals, under EWMA or under
mean_adj_type=qis.MeanAdjType.NONE. NONE regresses through the origin, which changes the
estimator’s meaning. A late starter’s first return is centred to zero, so its first beta is an
exact zero and is reported missing.
Residual Momentum¶
The single-benchmark residual uses the preceding observation’s fitted beta:
The residual then enters the same QIS volatility-normalisation and long/short filter.
Default beta and long spans are 12, with vol_span=13 and no short leg.
mean_adj_type controls the beta regression; the residual-volatility leg uses NONE.
Residual reversal negates the filtered residual signal. Its default long span is 1. When all settings, including the long span, are held equal, reversal is the negative of residual momentum. Their default outputs are not negatives because their default long spans differ.
Managers Alpha¶
For factor return vector \(f_t\), let \(\tau(t)\) be the most recent supplied beta snapshot dated at or before the preceding asset-return date. The manager residual is
Factor returns are measured over the asset’s own return periods. Dates without a prior loading snapshot or a complete factor vector are skipped. A missing manager return stays missing for that asset. Supply chronologically ordered snapshot keys and aligned factor labels.
The default multiplies residuals by periods per year, then smooths with alpha_span=12.
Its score divides by the cross-sectional population standard deviation without centering:
Here \(A=12\) for monthly data; annualise=False omits that multiplication. The numerator
is an annual-scaled log-return residual, not a compounded annual forecast. The function
has no group_data argument, no cluster variant and no clipping/CDF mapping. A common
positive residual mean remains in the scores; zero dispersion can produce non-finite results.
A residual can reflect omitted risks or model error as well as skill.
Risk-adjusted carry and rolling means¶
Carry requires a separate, dated annual-decimal yield panel. For example, 0.03 means 3%
per year. The carry pair computes yield divided by annualised EWMA log-return volatility,
whose variance recursion starts at the first observed squared return, then scores that ratio. Its default return cadence is W-WED and its volatility span is 13.
Supply carry values known at formation time. This ratio does not remove all duration,
credit or other economic risk.
Unlike the momentum filter, vol_span=None in carry still estimates volatility: it passes
through to QIS’s default decay of 0.94. Use an explicit positive span for a defined horizon.
estimate_rolling_ewma_means returns EWMA log-return means, not cross-sectional scores.
Defaults are returns_freq="W-WED", span=52 and annualize=True. Annualisation multiplies
by the inferred periods per year. Requested dates between observations receive the latest
available estimate; dates before the return sample remain missing.
Cluster-Based Scoring¶
Motivation¶
Fixed groups express a supplied classification; clusters express a partition inferred from data or supplied by another model. Each chooses which assets form the comparison set. A different score does not by itself establish that one partition improves investment results.
How Clusters Are Derived¶
For HCGL, FactorLasso normally estimates an asset-response dependence matrix, clusters assets before solving the group-penalised regression, and supplies those labels with the fitted model. It does not cluster a factor correlation matrix after estimating betas. Dependence measure, clustering horizon, linkage, distance, external partitions and rolling smoothing settings can change the resulting labels.
extract_rolling_clusters reads the stored asset labels from each covariance snapshot;
it does not estimate clusters. Labels can include cadence prefixes such as ME:1.
align_rolling_clusters provides overlap-based label alignment for interpretation through
time. Relabeling a partition does not change its memberships or basic within-cluster scores.
Scoring Logic¶
Standard fixed-group scoring delegates to qis.df_to_cross_sectional_score. It clips
the raw input to [-5, 5], then uses the non-missing comparison values and population
standard deviation:
Low-beta scores reverse the sign. Raw return values are not overwritten by score clipping.
Clipping is applied before, not after, standardisation, so a score is not bounded by
either [-1, 1] or the raw-input clipping interval. A singleton or zero-dispersion fixed
group normally produces NaN.
With non-empty dated cluster assignments, score_within_clusters instead uses sample
standard deviation (ddof=1), without the standard path’s clipping:
Situation |
Basic cluster-scoring behavior |
|---|---|
Before the first assignment |
All scores are 0, including unavailable raw signals. |
Cluster size greater than |
Score using that cluster’s available raw values. |
Cluster size at most |
Use statistics of all currently assigned assets in the signal panel. |
Only one cluster or too few assigned assets |
Use the assigned-universe fallback; degenerate values become 0. |
Asset without an assignment |
Score remains 0. |
Empty assignment dictionary |
Use the standard QIS score, including clipping and |
The default threshold is 3: a three-member cluster still uses fallback statistics. Small clusters and singletons do not generally receive zero. Missing assigned raw values can remain NaN in nondegenerate calculations; zero in this table is not proof of tradability. Optional stability pooling is an explicit alternate path delegated to FactorLasso and is not exercised by the basic examples below.
Worked example¶
The fifteen Python blocks below run in order with the core dependencies. They are excerpts of
the canonical script
examples/docs/alphas_module_readme.py, which runs
them and asserts every number and property on this page against a reference computed a
different way:
python -m examples.docs.alphas_module_readme
Fixed synthetic inputs¶
Eight synthetic assets, two factors, a benchmark and annual carry inputs cover 97 monthly price dates, 2016-12-31 through 2024-12-31. The paths are deterministic and require no data files, network or random seed. All prices share one illustrative currency and total-return basis.
import numpy as np
import pandas as pd
import qis
import optimalportfolios as opt
import optimalportfolios.alphas as alphas
import optimalportfolios.alphas.signals as signals
dates = pd.date_range("2016-12-31", "2024-12-31", freq="ME")
step = np.arange(len(dates), dtype=float)
factor_changes = np.column_stack((
0.005 + 0.025 * np.sin(0.7 * step),
0.002 + 0.012 * np.cos(0.4 * step),
))
factor_prices = pd.DataFrame(
100 * np.exp(np.cumsum(factor_changes, axis=0)),
index=dates, columns=["Growth", "Rates"],
)
loadings = np.array([
[1.2, 0.1], [0.9, 0.3], [0.7, 0.2], [0.5, 0.4],
[0.2, 1.1], [0.3, 0.8], [0.4, 0.6], [0.6, 0.5],
])
specific_changes = (
0.003 * np.sin(step[:, None] * np.arange(0.9, 1.7, 0.1)[None, :])
+ np.arange(8)[None, :] * 0.0002
)
prices = pd.DataFrame(
100 * np.exp(np.cumsum(factor_changes @ loadings.T + specific_changes, axis=0)),
index=dates, columns=list("ABCDEFGH"),
)
asset_prices = prices
benchmark = factor_prices["Growth"]
carry = pd.DataFrame(
np.broadcast_to(np.linspace(0.02, 0.05, 8), prices.shape),
index=dates, columns=prices.columns,
)
Standard signal usage¶
The saved raw and score names make the components available to the later container example.
from optimalportfolios.alphas import compute_momentum_alpha
score, raw = compute_momentum_alpha(
prices=prices,
benchmark_price=benchmark,
returns_freq='ME',
long_span=12,
)
mom_score, raw_momentum = score, raw
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,
)
beta_score = score
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,
)
res_score = score
The newer families use the optimalportfolios.alphas.signals exports. The plural legacy
carry helper returns only a score; the singular helper returns the score/raw pair.
classic_score, raw_classic = signals.compute_classic_momentum_alpha(
prices, returns_freq="ME", lookback_periods=12, skip_periods=1,
)
reversal_score, raw_reversal = signals.compute_residual_reversal_alpha(
prices, benchmark_price=benchmark, returns_freq="ME", long_span=1,
)
carry_score, raw_carry = signals.compute_ra_carry_alpha(
prices, carry=carry, returns_freq="ME", vol_span=13,
)
legacy_carry_score = alphas.compute_ra_carry_alphas(
prices, carry=carry, returns_freq="ME", vol_span=13,
)
A factor model for managers’ residuals¶
This small HCGL fit supplies dated loadings and clusters. The first manager price endpoint is retained before the first beta date so manager returns can begin in January 2023.
factor_estimator = opt.FactorCovarEstimator(
rebalancing_freq="YE", factor_returns_freq="ME", factor_covar_span=36,
lasso_model=opt.LassoModel(
model_type=opt.LassoModelType.HIERARCHICAL_CLUSTER_GROUP_LASSO,
reg_lambda=1.0e-5, span=36, warmup_period=36, demean=True, solver="CLARABEL",
),
)
rolling_data = factor_estimator.fit_rolling_factor_covars(
risk_factor_prices=factor_prices,
asset_returns_dict={
"ME": qis.to_returns(prices, freq="ME", is_log_returns=True, drop_first=True)
},
assets=prices.columns,
time_period=qis.TimePeriod("2022-12-31", "2024-12-31"),
)
taa_covar_data = rolling_data
asset_prices = prices.loc["2022-11-30":]
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,
)
mgr_score = score
Cluster Signal Usage¶
The cluster-extraction example uses the same fitted collection and the explicit asset list.
from optimalportfolios.alphas.signals import extract_rolling_clusters
rolling_clusters = extract_rolling_clusters(
rolling_covar_data=taa_covar_data,
assets=prices.columns.tolist(),
)
# Dict[pd.Timestamp, pd.Series] → {date: pd.Series(ticker → cluster_id)}
The direct utility call and signal-specific wrapper use the same raw momentum:
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,
)
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,
)
mom_cluster_score, raw_momentum_cluster = score, raw
beta_cluster_score, raw_beta_cluster = signals.compute_low_beta_cluster_alpha(
prices, benchmark_price=benchmark, rolling_clusters=rolling_clusters,
)
res_cluster_score, raw_residual_cluster = signals.compute_residual_momentum_cluster_alpha(
prices, benchmark_price=benchmark, rolling_clusters=rolling_clusters,
)
classic_cluster_score, raw_classic_cluster = signals.compute_classic_momentum_cluster_alpha(
prices, rolling_clusters=rolling_clusters,
)
reversal_cluster_score, raw_reversal_cluster = signals.compute_residual_reversal_cluster_alpha(
prices, benchmark_price=benchmark, rolling_clusters=rolling_clusters,
)
carry_cluster_score, raw_carry_cluster = signals.compute_ra_carry_cluster_alpha(
prices, carry=carry, returns_freq="ME", rolling_clusters=rolling_clusters,
)
A visible scoring comparison¶
Use six deliberately simple raw values to isolate scoring from price estimation. Assets A–D form a four-member cluster; E–F form a two-member cluster. Under the default threshold, A–D use within-cluster sample statistics and E–F use the full assigned-universe sample statistics.
probe_date = pd.Timestamp("2024-12-31")
raw_probe = pd.DataFrame(
[[-4.0, -2.0, 0.0, 2.0, 4.0, 8.0]], index=[probe_date], columns=list("ABCDEF"),
)
probe_clusters = {
probe_date: pd.Series(["Large"] * 4 + ["Small"] * 2, index=raw_probe.columns),
}
standard_probe = qis.df_to_cross_sectional_score(raw_probe)
cluster_probe = signals.score_within_clusters(raw_probe, probe_clusters, min_cluster_size=3)
score_comparison = pd.DataFrame({
"Raw": raw_probe.loc[probe_date],
"Standard": standard_probe.loc[probe_date],
"Cluster": cluster_probe.loc[probe_date],
})
print(score_comparison.round(6))
Asset |
Raw |
Standard score |
Cluster score |
|---|---|---|---|
A |
-4 |
-1.517929 |
-1.161895 |
B |
-2 |
-0.889821 |
-0.387298 |
C |
0 |
-0.261712 |
0.387298 |
D |
2 |
0.366397 |
1.161895 |
E |
4 |
0.994505 |
0.617213 |
F |
8 |
1.308560 |
1.543033 |
F’s input is clipped to 5 only in the standard calculation. Its nonzero cluster score comes from fallback statistics, despite membership in a small cluster.
The figure applies the same comparison to a separate synthetic universe: nine monthly price paths with constant log drifts, five equity-like and four bond-like, so each asset’s twelve-month classic momentum equals its annual drift. Both clusters have more than three members, so each is scored with its own sample statistics: within-cluster scores have mean zero and sample standard deviation one in each cluster.

Figure: twelve-month classic momentum of a separate nine-asset synthetic universe at
31 December 2024, scored across the cross-section by compute_classic_momentum_alpha and
within an equity-like and a bond-like cluster by compute_classic_momentum_cluster_alpha.
Drawn by the exhibit function of the canonical script; the
analytics gallery lists its provenance.
Insight
Scoring within clusters removes each cluster’s average signal level before assets are compared. In the figure, the strongest bond-like asset scores -0.29 across all nine assets and ranks fifth, behind four equity-like assets; within its cluster it scores 1.07 and ranks second. The weakest equity-like asset falls from seventh to last.
Cadence and fixed-group examples¶
G and H now report only quarterly. The hard skip is one native period for each bucket.
mixed_prices = prices.copy()
mixed_prices.loc[~mixed_prices.index.is_quarter_end, ["G", "H"]] = np.nan
return_frequencies = pd.Series("ME", index=prices.columns)
return_frequencies.loc[["G", "H"]] = "QE"
mixed_score, mixed_raw = signals.compute_classic_momentum_alpha(
mixed_prices, returns_freq=return_frequencies,
lookback_periods={"ME": 12, "QE": 4}, skip_periods={"ME": 1, "QE": 1},
)
An explicit benchmark leaves the raw single-cadence momentum unchanged when fixed groups are introduced; only its comparison set changes.
group_data = pd.Series(["Group 1"] * 4 + ["Group 2"] * 4, index=prices.columns)
group_score, group_raw = signals.compute_momentum_alpha(
prices, benchmark_price=benchmark, returns_freq="ME", group_data=group_data,
)
AlphasData¶
This illustrative 50/50 blend is supplied by the example. It is not an aggregation default or a forecast calibration. The container retains all the existing component fields.
# An illustrative application-level blend; the container does not choose this rule.
combined_scores = 0.5 * mom_score + 0.5 * beta_score
cluster_assignments = pd.DataFrame.from_dict(rolling_clusters, orient="index")
cluster_assignments = cluster_assignments.reindex(prices.index, method="ffill")
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()
At 2024-12-31 the snapshot has eight asset rows and 16 populated component columns.
to_dict() returns those 16 populated fields without writing a file. The container has
no dedicated classic-momentum, reversal or carry fields; keep additional panels in a named
dictionary when evaluating those signals.
Rolling EWMA means¶
estimate_rolling_ewma_means returns annualised EWMA log-return means on requested dates, here
the quarter ends of 2023 and 2024:
mean_dates = pd.date_range("2023-03-31", "2024-12-31", freq="QE")
annual_log_means = alphas.estimate_rolling_ewma_means(
prices, rebalancing_dates=list(mean_dates), returns_freq="ME", span=12, annualize=True,
)
The result has eight requested dates and eight assets. A requested date between two observations receives the latest estimate, and a date before the return sample remains missing.
Profiling and diagnostics¶
Rank-portfolio profiles and signal diagnostics, which evaluate scores against realised future returns, are described on signal diagnostics and alpha-rank portfolios. That page checks them on a synthetic score with a known information coefficient.
Implementation in optimalportfolios¶
Architecture¶
The paired standard and cluster constructors now live together in their owning signal module.
src/optimalportfolios/alphas/
signals/
momentum.py, classic_momentum.py, low_beta.py
residual_momentum.py, residual_reversal.py, managers_alpha.py
carry.py, rolling_ewma_mean.py, utils.py
tests/ offline signal contracts
run_local/signals_run.py source-checkout diagnostics
alpha_data.py AlphasData
profile/ rank-selection backtests and reports
signal_diagnostics.py adapters for QIS signal diagnostics
backtest_alphas.py additional alpha-backtest helpers
tests/ cross-cutting contracts
Signal exports and
alpha-layer exports are the import contracts.
The development runner is python -m optimalportfolios.alphas.signals.run_local.signals_run
in a checkout; its Locals enum selects manual scenarios. It is not the offline verification
entry point for this article; the canonical script is.
Signal Matrix¶
Import the paired constructors from optimalportfolios.alphas.signals.
All 13 constructors below return (score, raw); managers alpha has no cluster counterpart.
Family |
Standard and cluster entry points |
|---|---|
EWMA momentum |
|
Classic momentum |
|
Low beta |
|
Residual momentum |
|
Residual reversal |
|
Carry |
|
Managers alpha |
|
compute_classic_momentum_from_returns returns only a raw fixed-window sum.
compute_ra_carry_alphas returns only the legacy global carry score and is also exported
from optimalportfolios.alphas. The singular carry pair is exported from the signals
subpackage; do not assume every subpackage export exists at its parent.
See the momentum, classic momentum, low-beta, residual-momentum, reversal, carry, and managers-alpha sources for individual signatures. They do not share one universal interface.
AlphasData Fields¶
Stored fields |
Contents |
|---|---|
|
Required application-supplied combined panel. |
|
Raw/scored EWMA momentum. |
|
Cluster momentum components. |
|
Raw beta and low-beta score. |
|
Cluster low-beta components. |
|
Smoothed residual and its uncentered score. |
|
Residual momentum components. |
|
Cluster residual components. |
|
Dated labels; values may be strings such as |
Only alpha_scores is required. The
container source defines the exact fields.
get_alphas_snapshot requires the requested date in alpha_scores.
Pitfall
get_alphas_snapshot is not an as-of join. If the requested date is absent from
another populated component, it takes that component’s last row, even when that row is
later than the date, so a historical snapshot can contain future values. Align every
component to the alpha_scores index before taking historical snapshots.
Evaluation entry points and verification context¶
The entry points of the rank profiler and of the signal diagnostics, and what they add to QIS, are listed in the implementation section of the signal diagnostics page. Factor fitting and cluster discovery remain in FactorLasso.
The canonical script runs the worked example and checks each signal against the page’s formulas, the scoring rules and their fallbacks, the cluster extraction, the container and the rolling means, and it changes later inputs to confirm that no earlier signal or score moves. The test suite runs it, and so does the offline examples lane of CI.
Interpretation and limitations¶
Use sufficiently long histories and distinguish missing observations from zero signals. Warm-up, zero-beta masking, degenerate group dispersion and cluster fallback have different effects. The eligibility, tie and selection rules of rank-based profiles, and their limitations, are stated on signal diagnostics and alpha-rank portfolios.
Avoid MeanAdjType.INSAMPLE in historical trading signals: it uses the full-sample mean.
The canonical script changes later prices, benchmark, carry and cluster labels and asserts
that no earlier raw signal or score moves, for all six families, standard and cluster, and
for the three beta-based families under both the default EWMA and the explicit NONE mean
adjustment. These checks cannot establish when a supplied price, carry input or factor loading
was actually available.
Changing the reporting cadence changes both the observations and the meaning of scalar spans.
Empirical Findings¶
A performance claim needs a reproducible universe, sample, data vintage, signal settings, benchmark, rebalance rule, cost convention and comparison statistic. The worked examples here verify definitions and execution. They do not establish that cluster scoring outperforms fixed groups or that residual signals identify manager skill. Use the profiling and diagnostics interfaces to evaluate a specified dataset and disclose the resulting design choices.
See also¶
Signal diagnostics and alpha-rank portfolios: information coefficients, quantile portfolios and the rank profiler.
Mixed-frequency data: native cadences, hard lookbacks and timing.
Factor covariance with HCGL and covariance estimators: factor models and annualisation.
CSV factor risk model: persisted input and loading contracts.
Rolling backtests: applying formation-date decisions to holdings.
Examples: profiling workflows and other entry points.
API reference: generated public signatures.
References¶
Blitz, D., Huij, J. and Martens, M. (2011). Residual Momentum. Journal of Empirical Finance, 18(3), 506–521.
Blitz, D., Huij, J., Lansdorp, S. and Verbeek, M. (2013). Short-Term Residual Reversal. Journal of Financial Markets, 16(3), 477–504.
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.