factrix.metrics.trend ¶
Information coefficient (IC) trend analysis using Theil-Sen estimator.
Theil-Sen is preferred over ordinary least squares (OLS) because it has a breakdown point of 29.3%, making it robust to outliers (e.g. COVID-era IC spikes).
Notes
Pipeline. Time-series only, Theil-Sen median pairwise slope on a 1-D series; CI from the rank-based pairwise slope distribution.
Input. DataFrame with date, value (typically an IC series).
Output. Slope + confidence interval for trend detection.
factrix.metrics.trend.ic_trend ¶
ic_trend(series: DataFrame, value_col: str = 'value', overlap_periods: int = DEFAULT_FORWARD_PERIODS, *, name: str = 'ic_trend', adf_threshold: float | None = 0.1, expected_warnings: tuple[str, ...] = ()) -> MetricResult
Theil-Sen median slope of a time-indexed series.
Answers "is this factor getting better or worse over time?" - slope ≈ 0: stable - slope significantly < 0: decaying (crowding / alpha erosion) - slope significantly > 0: improving
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
DataFrame
|
DataFrame with |
required |
overlap_periods
|
int
|
Overlap horizon of the input series, used as the
non-overlapping sampling stride. The default input is a
per-period IC series built from h-period forward returns, so
consecutive observations share |
DEFAULT_FORWARD_PERIODS
|
name
|
str
|
Emitted metric name, stashed in |
'ic_trend'
|
adf_threshold
|
float | None
|
Augmented Dickey-Fuller (ADF) p-value above which the input is flagged as
unit-root suspect. Default |
0.1
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with the Theil-Sen slope as |
MetricResult
|
Hamed-Rao Mann-Kendall |
Notes
Theil-Sen median pairwise slope: slope = median{(y_j - y_i) /
(j - i) : i < j} (Sen (1968)) reports the trend's
magnitude; significance comes from the Mann-Kendall test
(Mann (1945), Kendall (1975)) on the
same ranks — stat is Kendall's tau between the sequence
index and the series. That is the standard pairing for a
non-parametric trend test: one rank-based framework supplies both
numbers. An ADF unit-root pre-check on the input flags series for
which the trend null is rejected at inflated rates regardless of
the true trend, and now carries
WarningCode.PERSISTENT_REGRESSOR rather than sitting in
metadata alone. On a trend-free IC series the Mann-Kendall p
measures 1.7-5.3% at a nominal 5% over T in 60/120/240 and
h in 1/5 (reference/inference-calibration).
Two corrections for serial dependence. Striding does ~99% of
the work; Hamed-Rao handles the residual persistence that
striding cannot reach, and is not calibrated on its own.
Mann-Kendall's null variance
Var(S) = n(n-1)(2n+5)/18 counts every pairwise comparison as
an independent draw. The default input to this metric — a
per-period IC series over h-period forward returns — is MA(h-1)
with rho_1 = 1 - 1/h (0.80 at h=5, 0.95 at h=21), so the raw
test is wholesale miscalibrated and does not improve with T.
So:
- The series is sub-sampled to non-overlapping observations at
stride
overlap_periods(the same_sample_non_overlappingmachinerypositive_rateuses) before anything is computed. This is exactly calibrated for the overlap channel. - The Mann-Kendall variance on the survivors carries the
Hamed-Rao (1998)
n/n*inflation for the residual persistence striding cannot remove (an AR factor is still autocorrelated after striding). Reported asmetadata["variance_inflation"].
Measured rejection frequency on a true null (slope 0), 600 replications per cell:
| series | raw MK | Hamed-Rao only | strided + HR |
|---|---|---|---|
| T=60, h=5 | 0.405 | 0.215 | 0.045 |
| T=240, h=5 | 0.367 | 0.162 | 0.038 |
| T=120, h=21 | 0.683 | 0.312 | 0.022 |
| T=240, h=21 | 0.673 | 0.265 | 0.033 |
| T=120, iid | 0.035 | 0.032 | 0.032 |
| T=120, AR(0.6) | 0.338 | 0.183 | 0.183 |
| T=120, AR(0.9) | 0.667 | 0.357 | 0.357 |
The last two rows are the disclosed limit: striding does nothing
for a persistent series at h = 1, and Hamed-Rao only halves
the excess. Read the other way round, in every overlap cell the
strided and strided+Hamed-Rao columns agree to within 1pp — the
stride is the fix, and Hamed-Rao earns its place only on the
h = 1 autocorrelated rows. Those runs carry
WarningCode.SERIAL_CORRELATION_DETECTED, fired off the lag-1
autocorrelation of the strided series against
PERSISTENT_SERIES_AUTOCORR.
The size fix costs power, and at long horizons costs most of it. Against a drift of 2 sd over the sample, raw Mann-Kendall versus strided + Hamed-Rao:
| cell | raw MK | strided + HR |
|---|---|---|
| T=60, h=5 | 0.865 | 0.360 |
| T=120, h=5 | 0.971 | 0.713 |
| T=240, h=5 | 0.999 | 0.952 |
| T=120, h=21 | 0.888 | 0.086 |
| T=240, h=21 | 0.951 | 0.425 |
The raw column is not a fair comparison — it is the power of a
test that rejects 37–68% under the null — but the absolute level
of the corrected column is the number to plan around. At
h = 21 the stride leaves 12 survivors at T = 120 and the
metric is effectively powerless there; it needs roughly 240+
periods before it can detect anything, and more still to be
comparable with the h = 5 cells.
Why not a t backed out of the CI. An earlier version derived
SE ≈ (ci_high - ci_low) / 2 / 1.96 from scipy's normal-based
95% rank interval and read slope / SE against t(n - 2).
Under a true null that combination rejected 2.4–3.8% at a nominal
5% for n = 10–30 — conservative, but by mixing a normal-derived
SE with a t reference rather than by design, and it made a
perfectly linear series (zero-width CI) a special case that had to
be hand-mapped to p = 0. Mann-Kendall sits at 4.6–5.4% on the
same nulls and handles the perfect line naturally (tau = ±1,
exact p).
Constant series. A flat series has no rank ordering to test:
tau is undefined, so stat / p_value are None with
WarningCode.DEGENERATE_VARIANCE, while value (the zero
slope) is still reported — the same convention every other metric
uses for a dispersion-free sample. The Theil-Sen CI is kept in
metadata as a descriptive interval; ci_excludes_zero is
informational and no longer drives the p.
factrix uses Theil-Sen rather than OLS because its 29.3% breakdown point absorbs information coefficient (IC) outliers (e.g. COVID-era spikes) that would dominate an OLS slope. The confidence interval is descriptive; Mann-Kendall supplies the inferential reference.
References
Sen 1968: Theil-Sen median pairwise slope.
Lou-Polk 2022: momentum-crowding evidence
as one suggestive economic channel for time-varying IC;
McLean-Pontiff 2016 is the cleaner
cite for post-publication IC decay specifically.
Hamed-Rao 1998: variance-inflation
correction making Mann-Kendall valid under serial correlation.
Stock-Watson 1988: practitioner
unit-root background for the ADF persistence flag.
Dickey-Fuller 1979: ADF persistence
diagnostic on the input series.
MacKinnon 1996: ADF p-value response surface
used by _adf_pvalue_interp.
Examples:
Trend on the per-period IC series produced by
:func:~factrix.metrics.ic.compute_ic:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ic import compute_ic
>>> from factrix.metrics.trend import ic_trend
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, rng=0),
... forward_periods=5,
... )
>>> ic_df = compute_ic(panel)["factor"]
>>> result = ic_trend(ic_df, value_col="ic")
>>> result.name == ""
True
Use cases¶
-
Detect alpha decay / crowding on an information coefficient (IC) series
ic_trendis a standalone series diagnostic — input is a 1-D(date, value)series, typically the per-date IC fromcompute_ic. Slope \(\approx 0\) = stable; slope significantly \(< 0\) = decaying (crowding / alpha erosion in the Lou-Polk 2022 sense); slope significantly \(> 0\) = improving. -
Outlier-robust slope via Theil-Sen
Median pairwise slope \(\mathrm{median}\{(y_j - y_i)/(j - i): i < j\}\) has a 29.3 % breakdown point — absorbs IC outliers (e.g. COVID- era spikes) that would dominate an ordinary least squares (OLS) slope. Theil-Sen supplies magnitude and a descriptive interval; Hamed-Rao Mann-Kendall supplies the test statistic and p-value.
-
Unit-root pre-check on the input
adf_threshold(default 0.10, a conventional practitioner cutoff) drives an augmented Dickey-Fuller (ADF) persistence diagnostic on the strided tested series. Above the cutoff the slope null is rejected at inflated rates regardless of the true trend; the slope value is still returned butmetadata["unit_root_suspected"] = Trueflags it for sceptical reading. Passadf_threshold=Noneto skip the check entirely. -
Reusable on any post-PANEL series
The
name=argument letsEventFactor.caar_trend/MacroPanelFactor.beta_trendpass their own primitive names so method / cache key / primitive name stay three-point unified — the same Theil-Sen primitive serves caar series, rolling-\(\beta\) series, and spread series, not just IC.
Worked example — IC series fed into ic_trend¶
compute_ic → ic_trend with ADF persistence check
import factrix as fx
from factrix.metrics.ic import compute_ic
from factrix.metrics.trend import ic_trend
from factrix.preprocess import compute_forward_return
raw = fx.datasets.make_cs_panel(
n_assets=100, n_dates=1000, ic_target=0.08, rng=2024,
)
panel = compute_forward_return(raw, forward_periods=5)
# The series diagnostic consumes (date, value); the value column on
# the compute_ic output is named ``ic``.
ic_df = compute_ic(panel)["factor"]
out = ic_trend(ic_df, value_col="ic")
print(out.value, out.stat, out.p_value)
# -3.2e-05 -0.91 0.36 (approximate; flat slope)
print(out.metadata["ci_low"], out.metadata["ci_high"],
out.metadata["ci_excludes_zero"])
# -1.0e-04 3.5e-05 False
print(out.metadata["adf_p"], out.metadata["unit_root_suspected"])
# 0.003 False (stationary; slope read is trustworthy)
See also¶
-
compute_ic/compute_spread_series
Canonical producers of the
(date, value)series this diagnostic consumes — IC series, spread series, or any other factor-mimicking- portfolio return series. -
positive_rate/oos
Sibling series diagnostics on the same input shape — sign significance and IS/out-of-sample (OOS) persistence.
-
compute_rolling_common_beta
Common-factor source of a rolling-\(\beta\) series for stability trend analysis via the same primitive.
-
Statistical methods
Theil-Sen breakdown point, ADF persistence diagnostic (MacKinnon response surface), and Hamed-Rao Mann-Kendall inference.
-
Metric applicability reference
When this metric applies and the sample-size guards that gate it (\(n \geq 10\)).
-
Series diagnostics landing
Adjacent axis-agnostic series diagnostics.