Skip to content

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', *, name: str = 'ic_trend', adf_threshold: float | None = 0.1) -> MetricOutput

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 date and value_col.

required
name str

MetricOutput.name for the returned output. Defaults to "ic_trend"; EventFactor.caar_trend / MacroPanelFactor. beta_trend pass their own names so method / cache key / primitive name stay three-point unified.

'ic_trend'
adf_threshold float | None

Augmented Dickey-Fuller (ADF) p-value above which the input is flagged as unit-root suspect. Default 0.10 is a conventional practitioner cutoff from the unit-root literature (folklore on the back of Stock-Watson (1988)'s broader review of trends in macroeconomic time series, with the specific 10% threshold closer to Stock 1994 Handbook of Econometrics §III than a direct prescription of the 1988 paper): at p > 0.10 we cannot reject I(1), so ordinary least squares (OLS) / Theil-Sen on the series reject the slope null at inflated rates regardless of the true trend. When None, the ADF check is skipped entirely and no adf_stat / adf_p / unit_root_suspected keys are written. When a float is provided it must lie in (0, 1). Detected unit roots set unit_root_suspected=True in metadata; the slope value is still returned (caller decides) but significance should be read with scepticism.

0.1

Returns:

Type Description
MetricOutput

MetricOutput with value = slope, t_stat from Theil-Sen confidence interval.

Notes

Theil-Sen median pairwise slope: slope = median{(y_j - y_i) / (j - i) : i < j}. Approximate t-stat is reconstructed from the rank-based 95% confidence interval [low, high]: SE ≈ (high - low) / 2 / 1.96 and t ≈ slope / SE. An ADF unit-root pre-check on the input flags series for which the slope null is rejected at inflated rates regardless of the true trend.

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 trade-off is the SE recovered from the rank-CI is approximate, not asymptotically exact.

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. 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-date 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, seed=0),
...     forward_periods=5,
... )
>>> ic_df = compute_ic(panel)
>>> result = ic_trend(ic_df, value_col="ic")
>>> result.name
'ic_trend'

Use cases

  • Detect alpha decay / crowding on an information coefficient (IC) series


    ic_trend is a (*, CONTINUOUS, *, TIMESERIES) diagnostic — input is a 1-D (date, value) series, typically the per-date IC from compute_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. The trade-off is the SE recovered from the rank-CI is approximate, not asymptotically exact.

  • Unit-root pre-check on the input


    adf_threshold (default 0.10, the Stock-Watson cutoff) drives an augmented Dickey-Fuller (ADF) persistence diagnostic on the input series. Above the cutoff the slope null is rejected at inflated rates regardless of the true trend; the slope value is still returned but metadata["unit_root_suspected"] = True flags it for sceptical reading. Pass adf_threshold=None to skip the check entirely.

  • Reusable on any post-PANEL series


    The name= argument lets EventFactor.caar_trend / MacroPanelFactor.beta_trend pass 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, seed=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)
out   = ic_trend(ic_df, value_col="ic")
print(out.value, out.stat, out.metadata["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.

    api/metrics/ic →

  • hit_rate / oos


    Sibling series diagnostics on the same input shape — sign significance and IS/out-of-sample (OOS) persistence.

    api/metrics/hit_rate →

  • compute_rolling_mean_beta


    Common-factor source of a rolling-\(\beta\) series for stability trend analysis via the same primitive.

    api/metrics/ts_beta →

  • Statistical methods


    Theil-Sen breakdown point, ADF persistence diagnostic (MacKinnon response surface), and the rank-CI to approximate-\(t\) recovery.

    reference/statistical-methods →

  • Metric applicability reference


    When this metric applies and the sample-size guards that gate it (\(n \geq 10\)).

    reference/metric-applicability →

  • Series diagnostics landing


    Adjacent axis-agnostic series diagnostics.

    api/metrics/series-tools →