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: object

Result 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. nan when returns is 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.

ari_per_pair: Dict[str, Dict[tuple[str, str], float]]
ordering_per_pair: Dict[str, Dict[tuple[str, str], float]]
mean_ari: Dict[str, float]
min_ari: Dict[str, float]
null_1_over_K: float = 0.0
K: int = 2
ari_threshold: float = 0.65
spearman_threshold: float = 0.85
passes_partition: Dict[str, bool]
passes_ordering: Dict[str, bool]
summary()[source]

Return a short text summary.

Return type:

str

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.ndarray of integer regime labels. Called once per specification in admissible_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 compute null_1_over_K.

Return type:

RepInvarianceResult

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) and intraday_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: object

Describes 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 the labels dict passed to res_invariance_validator().

  • intraday_freqs (Optional[Tuple[str, ...]]) – Subset of freqs to 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"))
freqs: Tuple[str, ...] = ('5m', '15m', '1h', '1d')
intraday_freqs: Tuple[str, ...] | None = None
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: object

Result 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 for passes_partition (Steinley 2004 = 0.65).

  • freqs (tuple of str) – Frequency labels in the order they appear in the matrices.

  • intraday_freqs (tuple of str) – Frequency subset used for intraday_mean_ari.

  • perm_pvalue (dict) – {asset_name: float | None} – permutation p-value for the overall mean off-diagonal ARI (available if run_permutation=True).

  • perm_null_ci (dict) – {asset_name: tuple[float, float] | None} – 2.5/97.5 null CI.

ari_matrix: Dict[str, DataFrame]
ami_matrix: Dict[str, DataFrame]
overall_mean_ari: Dict[str, float | None]
intraday_mean_ari: Dict[str, float | None]
within_intraday_excess: Dict[str, float | None]
passes_partition: Dict[str, bool]
ari_threshold: float = 0.65
freqs: Tuple[str, ...] = ('5m', '15m', '1h', '1d')
intraday_freqs: Tuple[str, ...] = ('5m', '15m', '1h')
perm_pvalue: Dict[str, float | None]
perm_null_ci: Dict[str, Tuple[float, float] | None]
summary()[source]

Return a short text summary.

Return type:

str

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.Series of 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_set with pre-labelled Series and wrap them with lambda s: s as 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: s to 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:

ResInvarianceResult

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,
)