factrix.metrics.ts_asymmetry ¶
Long-side / short-side asymmetry test (issue #5).
Diagnostic for (COMMON, CONTINUOUS, *) and single-asset TIMESERIES
cells. Ordinary least squares (OLS) β reports a single slope and assumes the response is
symmetric around zero — β > 0 could be "rises more on positive
factor" or "falls less on negative factor", and a strategy team
needs to know which.
Two methods, both fit by OLS with Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) covariance and
tested by Wald χ² so cross-method p-values stay comparable and the
overlapping-forward-return autocorrelation is handled the same way
as ts_beta_t_nw. Welch t is intentionally avoided — its iid
assumption breaks under forward_periods > 1.
- Method A (conditional means): `r = β_long·I(f>0) + β_short·I(f<0)
- β_zero·I(f=0)
. H0:β_long + β_short = 0` — symmetric magnitude. - Method B (piecewise slopes):
r = α + β_pos·max(f,0) + β_neg·min(f,0). H0:β_pos = β_neg— slope on the positive side equals slope on the negative side.
Gates (issue #5):
- Gate B (mandatory for either method): factor must have both
positive and negative observations.
- Gate C (method B only): each side needs ≥ 2 distinct factor
values to identify a slope. Below the gate, method B is skipped
and metadata["method_b_skipped"] records the reason.
Standalone metric — does not enter the registry.
Notes
Pipeline. Per-date aggregation of factor and forward return to
a common (_f, _r) series (cross-section step), then NW HAC OLS
with sign-asymmetric slopes on the resulting time series; Wald χ²
on the slope difference.
factrix.metrics.ts_asymmetry.ts_asymmetry ¶
ts_asymmetry(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return', forward_periods: int | None = None, nw_lags: int | None = None) -> MetricOutput
Long/short asymmetry of factor → return relationship.
Reported headline:
value= method-A magnitudeβ_long + β_short(0 under perfect symmetry; positive = long side stronger)stat=value/ Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) SEmetadata["p_value"]= method-A Wald p (two-sided)
Method B (Gate C passing) populates beta_pos / beta_neg /
p_wald_slopes; otherwise method_b_skipped carries the
reason.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Long panel; aggregated to per-date |
required |
factor_col
|
str
|
Column carrying the factor. |
'factor'
|
return_col
|
str
|
Column carrying the forward return. |
'forward_return'
|
forward_periods
|
int | None
|
Overlap horizon of the forward return; used to floor the NW bandwidth so the kernel is consistent with the autocorrelation it must absorb. |
None
|
nw_lags
|
int | None
|
Override for the NW lag count. |
None
|
Returns:
| Type | Description |
|---|---|
MetricOutput
|
|
MetricOutput
|
diagnostic statistics live in |
MetricOutput
|
with a reason code when input shape is insufficient (no |
MetricOutput
|
|
MetricOutput
|
than |
MetricOutput
|
factor variation). |
Notes
Aggregate to per-date (_f, _r) then fit two NW-HAC ordinary least squares (OLS)
specifications on the resulting time series::
Method A: r_t = beta_long*I(f>0) + beta_short*I(f<0)
+ beta_zero*I(f=0)
H0: beta_long + beta_short = 0 (Wald, two-sided)
Method B: r_t = alpha + beta_pos*max(f, 0) + beta_neg*min(f, 0)
H0: beta_pos = beta_neg (Wald)
value = beta_long + beta_short (method A); 0 under perfect
symmetry, positive when the long side dominates in magnitude.
factrix runs both methods under NW HAC + Wald (not Welch t)
because forward_periods > 1 breaks the iid assumption Welch
relies on, and using one estimator family across A and B keeps
cross-method p-values comparable.
References
Newey-West 1987: HAC covariance underpinning
the Wald tests for both methods.
Andrews 1991: Bartlett growth rate T^(1/3).
Hansen-Hodrick 1980: forward_periods - 1
floor for overlapping returns.
Examples:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ts_asymmetry import ts_asymmetry
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> result = ts_asymmetry(panel)
>>> result.name
'ts_asymmetry'
Timeseries-mode conventions
FACTOR_ADF_P persistence diagnostic, plain stage-1 SE rationale,
and the forward_periods vs signal_horizon bias framing apply
here as for the rest of the TS-mode family. See
Timeseries-mode conventions.
Use cases¶
-
Decompose \(\beta\) into long-side vs short-side response
Ordinary least squares (OLS) \(\beta\) reports one slope and assumes a symmetric response — \(\beta > 0\) could be "rises more on positive factor" or "falls less on negative factor".
ts_asymmetryruns Method A (conditional means on \(\mathrm{sign}(f)\) dummies) so the long and short legs are recoverable separately. -
Symmetric-magnitude Wald test
Headline is \(\beta_{\text{long}} + \beta_{\text{short}}\) — 0 under perfect symmetry, positive when the long side dominates. Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) Wald on \(H_0: \beta_{\text{long}} + \beta_{\text{short}} = 0\) handles the autocorrelation induced by overlapping forward returns; Welch \(t\) is intentionally avoided because its iid assumption breaks under
forward_periods > 1. -
Piecewise-slope check (Method B)
When each side has \(\geq 2\) distinct factor values, Method B fits \(r = \alpha + \beta_{\text{pos}} \max(f, 0) + \beta_{\text{neg}} \min(f, 0)\) and tests \(H_0: \beta_{\text{pos}} = \beta_{\text{neg}}\). Distinguishes a magnitude asymmetry (Method A) from a slope asymmetry. Gate C below the cardinality floor records
method_b_skippedand the reason; Method A already carries the full information for categorical / binary signals.
Worked example — sign-asymmetric slopes on a common-factor panel¶
broadcast common factor → ts_asymmetry (Methods A + B)
import factrix as fx
import polars as pl
from factrix.metrics.ts_asymmetry import ts_asymmetry
from factrix.preprocess import compute_forward_return
# Build a panel whose ``factor`` is broadcast (one value per date,
# shared across all assets) — VIX / NFP-surprise style.
raw = fx.datasets.make_cs_panel(
n_assets=50, n_dates=1000, ic_target=0.08, seed=2024,
)
common = raw.group_by("date").agg(pl.col("factor").mean().alias("factor"))
panel = raw.drop("factor").join(common, on="date")
panel = compute_forward_return(panel, forward_periods=5)
out = ts_asymmetry(panel, forward_periods=5)
print(out.value, out.stat, out.metadata["p_value"])
# 0.00021 1.83 0.067 (approximate; method A magnitude)
print(out.metadata["beta_long"], out.metadata["beta_short"],
out.metadata["abs_short_over_long"])
# 0.00088 -0.00067 0.76
print(out.metadata.get("beta_pos"), out.metadata.get("beta_neg"),
out.metadata.get("p_wald_slopes"))
# 0.00121 -0.00102 0.18 (Method B; ~null if Gate C passed)
See also¶
-
ts_beta
Symmetric linear \(\beta\) on the same panel; pair when both direction and magnitude asymmetry matter.
-
ts_quantile_spread
Bucketed conditional means — strictly more flexible than the two-flavour split, at the cost of \(K\) free parameters and the
n_distinct(factor) >= n_groups * 2gate. -
event_qualityfamily
Where the input gate redirects when the factor is single-sided (no positive or no negative observations):
event_hit_rate/event_ic/profit_factor. -
Statistical methods
NW HAC SE, Andrews bandwidth, Hansen-Hodrick overlap floor, and the Wald-on-linear-restriction framing for both Method A and Method B.
-
Timeseries-mode conventions
FACTOR_ADF_P, plain stage-1 SE rationale,forward_periodsvssignal_horizonbias framing. -
Metric applicability reference
When this metric applies and the gates (Gate B: two-sided factor; Gate C: \(\geq 2\) distinct values per side for Method B).
-
Common × Continuous landing
Adjacent macro-common metrics in the same cell.