mrv.invariance – High-level invariance API¶
mrv.invariance – High-level invariance API.
Wraps Paper 1 (representation) and Paper 2 (resolution) invariance checks with functional interfaces and typed result objects.
Representation invariance (Paper 1):
from mrv.invariance import rep_invariance_validator
result = rep_invariance_validator(
model_fn=your_clustering_fn, # (features: np.ndarray) -> np.ndarray of int labels
admissible_class={ # {spec_name: feature_matrix}
"rep_a": feat_a,
"rep_b": feat_b,
"rep_c": feat_c,
},
returns=log_returns, # 1-D float array aligned with features
K=3,
)
result.summary()
print(result.ari_per_pair)
print(result.ordering_per_pair)
print(result.null_1_over_K)
Source: Paper 1 (Zheng, Low & Wang, 2026)
- ARI: Table 2 (cross-representation ARI, Adjusted Rand Index metric)
- Matching-free ordering: posthoc_rank_aligned_ordering.py, Supplement app:ordering
- 1/K null: Supplement app:ordering, text around Table 3
Resolution invariance (Paper 2):
from mrv.invariance import res_invariance_validator, ResolutionSpec
result = res_invariance_validator(
model_fn=your_regime_fn, # (prices: pd.Series) -> pd.Series of int labels
resolution_set={ # {asset_name: {freq: price_series}}
"SPY": {"5m": spy_5m, "15m": spy_15m, "1h": spy_1h, "1d": spy_1d},
"CL": {"5m": cl_5m, "15m": cl_15m, "1h": cl_1h, "1d": cl_1d},
},
spec=ResolutionSpec(), # default: 4-freq Paper 2 panel
)
result.summary()
print(result.ari_matrix["SPY"]) # cross-freq ARI DataFrame
print(result.ami_matrix["SPY"]) # cross-freq AMI DataFrame
print(result.within_intraday_excess) # {asset: intraday_ARI - overall_ARI}
print(result.perm_pvalue) # permutation p-values
Source: Paper 2 (Zheng, Low & Wang, 2026)
- Cross-frequency ARI matrix: Table 2 / Table S1
- AMI matrix: Supplement S.2 robustness tables
- within_intraday_excess: sim_dgp.py SimReplicationResult docstring
(intraday_mean_ari - overall_mean_ari)
Sub-modules¶
Representation invariance (Paper 1)¶
mrv.invariance.rep – High-level representation invariance API (Paper 1).
Wraps mrv.validator.RepValidator with a functional interface and a typed result object so callers do not need to understand the validator config layer.
- Source: Paper 1 (Zheng, Low & Wang, 2026)
ARI: Table 2 (cross-representation ARI, Adjusted Rand Index metric)
Matching-free ordering: posthoc_rank_aligned_ordering.py, Supplement app:ordering
1/K null: Supplement app:ordering, text around Table 3
- class mrv.invariance.rep.RepInvarianceResult(ari_per_pair=<factory>, ordering_per_pair=<factory>, mean_ari=<factory>, min_ari=<factory>, null_1_over_K=0.0, K=2, ari_threshold=0.65, spearman_threshold=0.85, passes_partition=<factory>, passes_ordering=<factory>)[source]¶
Bases:
objectResult of a representation-invariance check (Paper 1).
- Variables:
ari_per_pair (
dict) –{(spec_a, spec_b): float}– pairwise ARI for each specification pair per asset. Outer key is asset name.ordering_per_pair (
dict) –{(spec_a, spec_b): float}– Spearman ordering consistency per pair per asset.nanwhenreturnsis not provided.mean_ari (
dict) –{asset_name: float}– mean off-diagonal ARI per asset.min_ari (
dict) –{asset_name: float}– minimum pairwise ARI per asset.null_1_over_K (
float) –1 / K– the ordering null under random assignment of K states.K (
int) – Number of states passed by the caller.ari_threshold (
float) – Library threshold for “acceptable partition recovery” (Steinley 2004).spearman_threshold (
float) – Library threshold for stable ordinal risk ordering.passes_partition (
dict) –{asset_name: bool}– True iff mean ARI >= ari_threshold.passes_ordering (
dict) –{asset_name: bool}– True iff mean Spearman >= spearman_threshold.
- mrv.invariance.rep.rep_invariance_validator(model_fn, admissible_class, returns=None, K=2)[source]¶
Run the Paper 1 representation-invariance check.
- Parameters:
model_fn (
Callable[[ndarray],ndarray]) –(features: np.ndarray) -> np.ndarrayof integer regime labels. Called once per specification inadmissible_class.admissible_class (
Dict[str,ndarray]) –{spec_name: feature_matrix}where each feature matrix is a 2-D array of shape(n_obs, n_features). At least 2 specifications are required.returns (
Optional[ndarray]) – 1-D float array of log-returns aligned with the feature rows. When provided, ordering consistency (Spearman) is computed.K (
int) – Number of regime states. Used only to computenull_1_over_K.
- Return type:
Resolution invariance (Paper 2)¶
mrv.invariance.res – High-level resolution invariance API (Paper 2).
Wraps mrv.validator.ResValidator with a functional interface and a typed result object. Takes a model callable and a resolution-set spec; computes the cross-frequency ARI matrix, the AMI matrix, and the within-intraday excess metric introduced in Paper 2.
- Source: Paper 2 (Zheng, Low & Wang, 2026)
Cross-frequency ARI matrix: Table 2 / Table S1
AMI matrix: Supplement S.2 robustness tables
Intraday excess: sim_dgp.py intraday_mean_ari vs overall_mean_ari; SimReplicationResult fields:
overall_mean_ari(4-freq mean off-diag ARI, 5m/15m/1h/1d) andintraday_mean_ari(3-freq intraday-only, 5m/15m/1h). within_intraday_excess = intraday_mean_ari - overall_mean_ari. A positive value means intraday frequencies agree more with each other than with the daily scale; Paper 2 finds this is the dominant failure mode.
Canonical resolution-set spec¶
Paper 2 uses FREQS = (“5m”, “15m”, “1h”, “1d”) as the four-frequency panel
for each of SPY (equities), CL (WTI futures), and USDJPY (FX).
The ResolutionSpec helper class encodes this convention so callers need
not repeat the frequency labels.
- class mrv.invariance.res.ResolutionSpec(freqs=('5m', '15m', '1h', '1d'), intraday_freqs=None)[source]¶
Bases:
objectDescribes which resolution levels to test and which are intraday.
- Parameters:
freqs (
Tuple[str,...]) – Ordered frequency labels. Must have >= 2 entries. Labels must match the keys in thelabelsdict passed tores_invariance_validator().intraday_freqs (
Optional[Tuple[str,...]]) – Subset offreqsto use for the within-intraday excess metric. Defaults to all frequencies that are not"1d".
Examples
Default Paper 2 four-frequency spec:
spec = ResolutionSpec() # ("5m", "15m", "1h", "1d")
Three-frequency intraday-only spec:
spec = ResolutionSpec(freqs=("5m", "15m", "1h"), intraday_freqs=("5m", "15m", "1h"))
- class mrv.invariance.res.ResInvarianceResult(ari_matrix=<factory>, ami_matrix=<factory>, overall_mean_ari=<factory>, intraday_mean_ari=<factory>, within_intraday_excess=<factory>, passes_partition=<factory>, ari_threshold=0.65, freqs=('5m', '15m', '1h', '1d'), intraday_freqs=('5m', '15m', '1h'), perm_pvalue=<factory>, perm_null_ci=<factory>)[source]¶
Bases:
objectResult of a resolution-invariance check (Paper 2).
- Variables:
ari_matrix (
dict) –{asset_name: pd.DataFrame}– symmetric cross-frequency ARI matrix per asset. Rows and columns are frequency labels.ami_matrix (
dict) –{asset_name: pd.DataFrame}– symmetric cross-frequency AMI matrix per asset.overall_mean_ari (
dict) –{asset_name: float | None}– mean of off-diagonal ARI entries. None when the aligned label set is too short to compute.intraday_mean_ari (
dict) –{asset_name: float | None}– mean off-diagonal ARI on the intraday-only frequency subset (omits pairs involving “1d”).within_intraday_excess (
dict) –{asset_name: float | None}–intraday_mean_ari - overall_mean_ari. Positive when intraday frequencies agree more with each other than with the daily scale; Paper 2’s primary signature of resolution invariance failure.passes_partition (
dict) –{asset_name: bool}– True iff overall_mean_ari >= ARI_THRESHOLD.ari_threshold (
float) – Library threshold used forpasses_partition(Steinley 2004 = 0.65).freqs (
tupleofstr) – Frequency labels in the order they appear in the matrices.intraday_freqs (
tupleofstr) – Frequency subset used forintraday_mean_ari.perm_pvalue (
dict) –{asset_name: float | None}– permutation p-value for the overall mean off-diagonal ARI (available ifrun_permutation=True).perm_null_ci (
dict) –{asset_name: tuple[float, float] | None}– 2.5/97.5 null CI.
- mrv.invariance.res.res_invariance_validator(model_fn, resolution_set, spec=None, run_permutation=True, n_perm=500, seed=42)[source]¶
Run the Paper 2 resolution-invariance check.
- Parameters:
model_fn (
Callable[[Series],Series]) –(prices: pd.Series) -> pd.Seriesof integer regime labels. The output Series must have the same DatetimeIndex as the input. Called once per (asset, frequency) combination.When labels are already available, pass a pre-fitted callable. To supply labels directly, use
resolution_setwith pre-labelled Series and wrap them withlambda s: sas the model function.resolution_set (
Dict[str,Dict[str,Series]]) –{asset_name: {freq: pd.Series}}where each inner Series is a price (or feature) Series with a DatetimeIndex at that frequency. The model_fn is called on each inner Series to produce regime labels.Alternatively, supply integer-labelled Series directly and use
model_fn = lambda s: sto pass labels through.spec (
Optional[ResolutionSpec]) – Controls which frequencies are tested and which are considered intraday. Defaults to the Paper 2 four-frequency panel (“5m”,”15m”,”1h”,”1d”).run_permutation (
bool) – Whether to compute the permutation p-value for the mean off-diagonal ARI.n_perm (
int) – Number of permutations (Paper 2 default; Paper 2 src/core/config.py DEFAULT_PERM_N = 500).seed (
int) – Random seed for permutation test (Paper 2 DEFAULT_PERM_SEED = 42).
- Return type:
Examples
SPY demo across two frequencies with synthetic labels (mirrors
examples/paper2_res_invariance_validator_demo.py):from mrv.invariance import res_invariance_validator, ResolutionSpec import pandas as pd import numpy as np rng = np.random.default_rng(0) def make_labels(s): return pd.Series(rng.integers(0, 2, len(s)), index=s.index, dtype=int) idx = pd.date_range("2026-01-05 09:30", periods=400, freq="5min", tz="America/New_York") prices_5m = pd.Series(100 + rng.standard_normal(400).cumsum(), index=idx) prices_15m = pd.Series(100 + rng.standard_normal(400).cumsum(), index=idx) result = res_invariance_validator( model_fn=make_labels, resolution_set={"SPY": {"5m": prices_5m, "15m": prices_15m}}, spec=ResolutionSpec(freqs=("5m", "15m"), intraday_freqs=("5m", "15m")), run_permutation=False, )