factrix.metrics.k_spread ¶
Fixed-K Top-K vs Bottom-K long-short spread for small cross-sections.
Notes
Pipeline. Per non-overlapping date, select the top k and
bottom k names by factor rank, take the mean-return difference
(cross-section step), then test the per-date spread series across
time. Small cross-sections switch the headline test to a
block-bootstrap CI.
Input. DataFrame with date, asset_id, factor, forward_return.
Output. Mean spread, with the per-date cross-sectional return dispersion reported alongside.
The small-n_assets counterpart of
:func:~factrix.metrics.quantile.quantile_spread. Quantile bucketing
(n_groups=5 ⇒ quintiles) degrades when n_assets < 30: each bucket
holds only a handful of names, so the spread is dominated by
individual assets and the quintile breakpoints are unstable. Fixing
the count k per leg keeps each leg's composition stable
regardless of n_assets, and the metric reports the contemporaneous
cross-sectional dispersion so the spread can be read relative to the
typical spread of returns that period.
factrix.metrics.k_spread.k_spread ¶
k_spread(data: DataFrame, forward_periods: int = 5, k: int = 5, factor_col: str = 'factor', return_col: str = 'forward_return', rng_seed: int = 0, inference: NonOverlapping | NeweyWest = NON_OVERLAPPING) -> MetricResult
Fixed-K Top-K vs Bottom-K long-short spread.
Per non-overlapping date, the long leg is the mean forward return of
the k highest-factor names and the short leg the mean of the
k lowest; the spread is their difference. The mean spread is
tested across time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Panel with |
required |
forward_periods
|
int
|
Sampling stride for non-overlapping dates; match the forward-return horizon. |
5
|
k
|
int
|
Number of names per leg (fixed count, not a quantile
fraction). A date needs at least |
5
|
factor_col
|
str
|
Ranking column (default |
'factor'
|
return_col
|
str
|
Realised-return column (default |
'forward_return'
|
rng_seed
|
int
|
Seed for the small-N block-bootstrap branch (reproducible by default). |
0
|
inference
|
NonOverlapping | NeweyWest
|
Headline significance method. |
NON_OVERLAPPING
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with value = mean spread, |
MetricResult
|
spread series, p-value from the cross-section-aware significance |
MetricResult
|
path. |
MetricResult
|
mean per-date cross-sectional standard deviation of returns. |
Notes
Per qualifying date t (universe size n_assets_t >= 2 * k), with
\(\mathrm{top}_k\) / \(\mathrm{bot}_k\) the names ranked \(1..k\) /
n_assets_t - k + 1 .. n_assets_t by factor:
value = mean_t spread_t. The headline test follows the shared
small-cross-section policy: with n_assets < MIN_ASSETS_WARN
the per-date spread is heavy-tailed (few names per leg), so the
t-test is replaced by a block-bootstrap CI; otherwise the
non-overlapping t applies. metadata["method"] records
which ran. The contemporaneous cross-sectional dispersion
\(\mathrm{std}_i(r_{i,t})\) is averaged over dates and reported so
the spread can be judged against the period's return spread.
References
Hansen-Hodrick 1980: overlapping-return autocorrelation, motivating the non-overlap stride.
Examples:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.k_spread import k_spread
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=20, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> result = k_spread(panel, forward_periods=5, k=3)
>>> result.name == ""
True
Use cases¶
-
Small-N sibling of
quantile_spread
k_spreadis the fixed-count long-short spread: per non-overlapping date it longs thekhighest-factor names and shorts theklowest, then tests the spread across time. Quintile bucketing (quantile_spread,n_groups=5) degrades whenn_assets < 30— each bucket holds only a few names and the breakpoints are unstable. Fixing the countkkeeps each leg's composition stable regardless ofn_assets. Both name the selection convention, not a leg:quantile_spread(fraction) ↔k_spread(count). -
Cross-sectional dispersion reported alongside
metadata["cross_sectional_dispersion"]carries the mean per-date cross-sectional standard deviation of returns, so the headline spread can be read relative to the typical spread of returns that period rather than in isolation. -
Cross-section-aware significance
The headline test follows the shared small-cross-section policy (
_spread_significance): withn_assets < MIN_ASSETS_WARNthe per-date spread is heavy-tailed (few names per leg), so the non-overlappingtis replaced by a block-bootstrap CI;metadata["method"]records which ran. Dates with fewer than2·knames are dropped; if none qualify the metric short-circuits withmax_assets_per_date.
Choosing a function¶
| Goal | Function |
|---|---|
| Long-short spread by extreme quantiles (large universe) | quantile_spread |
| Long-short spread by fixed count per leg (small universe) | k_spread |
Worked example — fixed-K spread on a small universe¶
k_spread on n_assets=20
import factrix as fx
from factrix.metrics.k_spread import k_spread
from factrix.preprocess import compute_forward_return
raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=180, seed=2024)
panel = compute_forward_return(raw, forward_periods=5)
out = k_spread(panel, forward_periods=5, k=3)
print(out.value, out.metadata["method"], out.metadata["cross_sectional_dispersion"])
# 0.0018 block-bootstrap CI 0.041 (approximate)
See also¶
-
quantile_spread
The fraction-based sibling: long-short spread by extreme quantiles, the idiomatic choice for large cross-sections.
-
top_concentration
Long-leg diversification (HHI effective bets) on the top bucket.
-
Metric applicability reference
When this metric applies and the sample-size guards that gate it.