319 lines
15 KiB
Python
319 lines
15 KiB
Python
"""Deterministic diagnostics contracts with independently checkable samples."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from math import sqrt
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
|
|
def api():
|
|
return importlib.import_module("quant_engine.factor_diagnostics")
|
|
|
|
|
|
def panel(values, *, days=1, assets=None):
|
|
assets = assets or ["A", "B", "C"]
|
|
index = pd.MultiIndex.from_product(
|
|
[pd.date_range("2026-09-21", periods=days), assets], names=["date", "asset"]
|
|
)
|
|
return pd.DataFrame(values, index=index)
|
|
|
|
|
|
def test_pairwise_matrix_keeps_complete_security_date_pairs():
|
|
frame = panel({"pe": [None, 0, 1, 2, 3, 4, 5, 6],
|
|
"pb": [1000, None, 1, 2, 3, 4, 5, 6]}, assets=list("ABCDEFGH"))
|
|
original = frame.copy()
|
|
result = api().correlation_matrix(frame)
|
|
assert result["matrix"][0][1] == pytest.approx(1)
|
|
assert result["n_pairs"] == [[7, 6], [6, 7]]
|
|
assert result["status"][0][1] == "ok"
|
|
assert result["method"]["aggregation"] == "pooled_asset_session_pairwise"
|
|
assert result["method"]["positive_semidefinite_guaranteed"] is False
|
|
pd.testing.assert_frame_equal(frame, original)
|
|
|
|
|
|
def test_matrix_empty_constant_and_short_diagonals_are_undefined():
|
|
frame = panel({"empty": [None] * 3, "constant": [0.05] * 3, "short": [1, 2, None]})
|
|
result = api().correlation_matrix(frame)
|
|
assert result["matrix"] == [[None] * 3 for _ in range(3)]
|
|
assert result["n_pairs"][0][0] == 0
|
|
assert result["status"][0][0] == "no_pairs"
|
|
assert result["status"][1][1] == "constant_both"
|
|
assert result["status"][2][2] == "insufficient_pairs"
|
|
|
|
|
|
def test_rank_correlation_uses_average_ties_after_pairing():
|
|
frame = panel({"f": [1, 1, 2, None], "r": [1, 2, 3, 999]}, assets=list("ABCD"))
|
|
result = api().correlation_matrix(frame, method="spearman")
|
|
assert result["matrix"][0][1] == pytest.approx(sqrt(3) / 2)
|
|
assert result["n_pairs"][0][1] == 3
|
|
|
|
|
|
def test_daily_ic_is_not_one_pooled_correlation():
|
|
frame = panel({"f": [1, 2, 3, 101, 102, 103]}, days=2)
|
|
returns = pd.Series([3, 2, 1, 101, 102, 103], index=frame.index)
|
|
result = api().daily_ic(frame, returns)
|
|
assert [r["ic"] for r in result["rows"]] == pytest.approx([-1, 1])
|
|
assert result["summary"][0]["ic_mean"] == pytest.approx(0)
|
|
assert result["summary"][0]["ic_std"] == pytest.approx(sqrt(2))
|
|
assert result["summary"][0]["ic_ir"] == 0
|
|
assert result["summary"][0]["ic_t"] == 0
|
|
assert result["method"]["ir_annualized"] is False
|
|
assert result["method"]["t_method"] == "naive_iid_unadjusted"
|
|
assert result["method"]["serial_correlation_adjusted"] is False
|
|
|
|
|
|
def test_daily_summary_matches_three_known_days_and_keeps_zero():
|
|
frame = panel({"f": [1, 2, 3] * 3}, days=3)
|
|
returns = pd.Series([3, 2, 1, 1, 0, 1, 1, 2, 3], index=frame.index)
|
|
result = api().daily_ic(frame, returns)
|
|
assert [r["ic"] for r in result["rows"]] == pytest.approx([-1, 0, 1])
|
|
assert [r["rank_ic"] for r in result["rows"]] == pytest.approx([-1, 0, 1])
|
|
summary = result["summary"][0]
|
|
assert (summary["n_days"], summary["ic_valid_days"], summary["rank_ic_valid_days"]) == (3, 3, 3)
|
|
assert summary["ic_std"] == pytest.approx(1)
|
|
assert summary["ic_ir"] == pytest.approx(0, abs=1e-15)
|
|
assert summary["ic_t"] == pytest.approx(0, abs=1e-15)
|
|
assert summary["rank_ic_ir"] == pytest.approx(0, abs=1e-15)
|
|
assert summary["rank_ic_t"] == pytest.approx(0, abs=1e-15)
|
|
|
|
|
|
def test_undefined_days_and_factor_observation_domain_survive_alignment():
|
|
frame = panel({"f": [1, 2, 3, None, None, None, 1, 2, 3]}, days=3)
|
|
returns = pd.Series([1, 2, 3], index=frame.index[:3])
|
|
extra = pd.Series([100], index=pd.MultiIndex.from_tuples(
|
|
[(pd.Timestamp("2026-09-25"), "D")], names=["date", "asset"]))
|
|
result = api().daily_ic(frame, pd.concat([returns, extra]))
|
|
assert len(result["rows"]) == 3
|
|
assert [r["n_pairs"] for r in result["rows"]] == [3, 0, 0]
|
|
assert [r["n_observations"] for r in result["rows"]] == [3, 3, 3]
|
|
assert [r["ic_status"] for r in result["rows"]] == ["ok", "no_pairs", "no_pairs"]
|
|
summary = result["summary"][0]
|
|
assert summary["ic_valid_days"] == 1
|
|
assert summary["ic_missing_days"] == 2
|
|
assert summary["ic_mean"] == pytest.approx(1)
|
|
assert summary["ic_std"] is summary["ic_ir"] is summary["ic_t"] is None
|
|
assert summary["ic_status"] == "insufficient_days"
|
|
assert result["method"]["extra_return_keys"] == 1
|
|
|
|
|
|
def test_daily_summary_of_constant_ic_has_no_ratio_statistics():
|
|
frame = panel({"f": [1, 2, 3] * 3}, days=3)
|
|
returns = pd.Series([1, 1, 2] * 3, index=frame.index)
|
|
summary = api().daily_ic(frame, returns)["summary"][0]
|
|
assert summary["ic_mean"] == pytest.approx(sqrt(3) / 2)
|
|
assert summary["ic_std"] == 0
|
|
assert summary["ic_ir"] is summary["ic_t"] is None
|
|
assert summary["ic_status"] == "constant_values"
|
|
assert summary["rank_ic_ir"] is summary["rank_ic_t"] is None
|
|
|
|
|
|
def test_permutation_and_input_copies_do_not_change_diagnostics():
|
|
frame = panel({"f": [1, 2, 3, 4, 5, 6]}, days=2)
|
|
returns = pd.Series([1, 3, 2, 4, 6, 5], index=frame.index)
|
|
original_frame, original_returns = frame.copy(), returns.copy()
|
|
baseline = api().daily_ic(frame, returns)
|
|
assert api().daily_ic(frame.iloc[::-1], returns.iloc[[2, 0, 4, 1, 5, 3]]) == baseline
|
|
pd.testing.assert_frame_equal(frame, original_frame)
|
|
pd.testing.assert_series_equal(returns, original_returns)
|
|
|
|
|
|
def test_large_finite_inputs_reuse_scaled_correlation_without_overflow():
|
|
frame = panel({"a": [-1e308, 0, 1e308], "b": [-1e307, 0, 1e307]})
|
|
assert api().correlation_matrix(frame)["matrix"][0][1] == pytest.approx(1)
|
|
|
|
|
|
@pytest.mark.parametrize("value", [True, "1.2", np.inf, -np.inf, object()])
|
|
def test_diagnostics_reject_invalid_numbers(value):
|
|
frame = panel({"f": [value, 2, 3]})
|
|
with pytest.raises(ValueError, match=r"Values|Infinite"):
|
|
api().correlation_matrix(frame)
|
|
|
|
|
|
@pytest.mark.parametrize("kind", ["duplicate_keys", "bad_names", "bad_date", "timezone", "empty_asset", "duplicate_factors"])
|
|
def test_diagnostics_reject_ambiguous_keys(kind):
|
|
frame = panel({"f": [1, 2, 3]})
|
|
if kind == "duplicate_keys":
|
|
frame.index = pd.MultiIndex.from_tuples([frame.index[0]] * 3, names=["date", "asset"])
|
|
elif kind == "bad_names":
|
|
frame.index.names = ["day", "asset"]
|
|
elif kind == "bad_date":
|
|
frame.index = pd.MultiIndex.from_product([["2026-09-21"], list("ABC")], names=["date", "asset"])
|
|
elif kind == "timezone":
|
|
frame.index = pd.MultiIndex.from_product([pd.date_range("2026-09-21", periods=1, tz="UTC"), list("ABC")], names=["date", "asset"])
|
|
elif kind == "empty_asset":
|
|
frame.index = pd.MultiIndex.from_product([pd.date_range("2026-09-21", periods=1), ["", "B", "C"]], names=["date", "asset"])
|
|
else:
|
|
frame = pd.concat([frame, frame], axis=1)
|
|
with pytest.raises(ValueError, match=r"keys|dates|identifiers"):
|
|
api().correlation_matrix(frame)
|
|
|
|
|
|
@pytest.mark.parametrize("minimum", [True, 0, 2, 3.1, "3"])
|
|
def test_minimum_pairs_is_explicit_and_at_least_three(minimum):
|
|
with pytest.raises(ValueError, match="min_pairs"):
|
|
api().correlation_matrix(panel({"f": [1, 2, 3]}), min_pairs=minimum)
|
|
|
|
|
|
def test_all_missing_and_empty_inputs_remain_observed_not_identity_or_zero():
|
|
frame = panel({"f": [None] * 3})
|
|
result = api().daily_ic(frame, pd.Series([None] * 3, index=frame.index))
|
|
assert result["summary"][0]["ic_mean"] is None
|
|
assert result["summary"][0]["ic_status"] == "no_valid_days"
|
|
empty = frame.iloc[:0]
|
|
assert api().correlation_matrix(empty)["matrix"] == [[None]]
|
|
assert api().daily_ic(empty, pd.Series([], index=empty.index, dtype=float))["rows"] == []
|
|
|
|
|
|
def prices(values=(100, 110, 121, 133.1)):
|
|
sessions = pd.DatetimeIndex(["2026-09-18", "2026-09-21", "2026-09-22", "2026-09-23"])
|
|
index = pd.MultiIndex.from_product([sessions, ["A"]], names=["date", "asset"])
|
|
return pd.Series(values, index=index), sessions
|
|
|
|
|
|
def forward(series, sessions, **changes):
|
|
options = {"entry_lag_sessions": 0, "holding_sessions": 2,
|
|
"price_field": "close", "price_basis": "synthetic_comparable"}
|
|
options.update(changes)
|
|
return api().forward_returns(series, sessions=sessions, **options)
|
|
|
|
|
|
def test_forward_returns_use_explicit_calendar_endpoints_and_compound_price_ratio():
|
|
series, sessions = prices()
|
|
result = forward(series, sessions)
|
|
assert result.returns.iloc[0] == pytest.approx(0.21)
|
|
assert result.intervals.iloc[0]["entry_date"] == "2026-09-18"
|
|
assert result.intervals.iloc[0]["exit_date"] == "2026-09-22"
|
|
assert result.intervals.iloc[-1]["status"] == "insufficient_calendar"
|
|
assert np.isnan(result.returns.iloc[-1])
|
|
assert result.metadata["formula"] == "exit_price / entry_price - 1"
|
|
assert result.metadata["entry_lag_sessions"] == 0
|
|
assert result.metadata["holding_sessions"] == 2
|
|
assert result.metadata["execution_eligibility"] == "not_established"
|
|
assert result.metadata["historical_availability"] == "not_established"
|
|
|
|
|
|
def test_forward_entry_lag_changes_both_endpoints():
|
|
series, sessions = prices([10, 20, 30, 80])
|
|
result = forward(series, sessions, entry_lag_sessions=1)
|
|
assert result.returns.iloc[0] == pytest.approx(3)
|
|
assert result.intervals.iloc[0]["entry_date"] == "2026-09-21"
|
|
assert result.intervals.iloc[0]["exit_date"] == "2026-09-23"
|
|
|
|
|
|
def test_forward_does_not_skip_missing_prices_or_derive_calendar_from_rows():
|
|
series, sessions = prices([100, None, 121, 133.1])
|
|
result = forward(series.drop(index=(sessions[1], "A")), sessions, holding_sessions=1)
|
|
assert len(result.returns) == 4
|
|
assert np.isnan(result.returns.iloc[0])
|
|
assert np.isnan(result.returns.iloc[1])
|
|
assert result.intervals.iloc[0]["status"] == "missing_price"
|
|
assert result.intervals.iloc[0]["exit_date"] == "2026-09-21"
|
|
assert result.returns.iloc[2] == pytest.approx(0.1)
|
|
|
|
|
|
@pytest.mark.parametrize("options", [{"holding_sessions": 0}, {"holding_sessions": True},
|
|
{"holding_sessions": 1.5}, {"entry_lag_sessions": -1},
|
|
{"entry_lag_sessions": False}, {"price_field": ""},
|
|
{"price_basis": ""}])
|
|
def test_forward_rejects_ambiguous_interval_parameters(options):
|
|
series, sessions = prices()
|
|
with pytest.raises(ValueError, match=r"sessions|price field"):
|
|
forward(series, sessions, **options)
|
|
|
|
|
|
@pytest.mark.parametrize("kind", ["duplicate", "descending", "timezone", "intraday", "outside"])
|
|
def test_forward_rejects_invalid_calendar(kind):
|
|
series, sessions = prices()
|
|
if kind == "duplicate":
|
|
sessions = sessions.insert(1, sessions[0])
|
|
elif kind == "descending":
|
|
sessions = sessions[::-1]
|
|
elif kind == "timezone":
|
|
sessions = sessions.tz_localize("UTC")
|
|
elif kind == "intraday":
|
|
sessions = sessions + pd.Timedelta(hours=1)
|
|
else:
|
|
sessions = sessions[:-1]
|
|
with pytest.raises(ValueError, match=r"sessions|dates|calendar"):
|
|
forward(series, sessions)
|
|
|
|
|
|
@pytest.mark.parametrize("value", [0, -1, True, "2", np.inf])
|
|
def test_forward_rejects_nonpositive_or_nonfinite_prices(value):
|
|
series, sessions = prices([value, 110, 121, 133.1])
|
|
with pytest.raises(ValueError, match=r"positive|Values|Infinite"):
|
|
forward(series, sessions)
|
|
|
|
|
|
def test_result_version_and_parameters_do_not_grant_source_or_decision_admission():
|
|
frame = panel({"f": [1, 2, 3]})
|
|
result = api().daily_ic(frame, pd.Series([1, 2, 3], index=frame.index))
|
|
assert result["contract_version"] == api().FACTOR_DIAGNOSTICS_VERSION
|
|
assert result["method"]["min_pairs"] == 3
|
|
assert result["method"]["min_days"] == 2
|
|
assert result["production_algorithm_version"] is None
|
|
assert result["source_admission"] == result["historical_availability"] == "not_established"
|
|
assert result["decision_eligible"] is False
|
|
|
|
|
|
def test_nonzero_daily_summary_uses_three_days_not_nine_asset_rows():
|
|
frame = panel({"f": [1, 2, 3] * 3}, days=3)
|
|
returns = pd.Series([.01, .02, .03, .02, .03, .01, .01, -.02, .01], index=frame.index)
|
|
result = api().daily_ic(frame, returns)
|
|
assert [point["ic"] for point in result["rows"]] == pytest.approx([1, -.5, 0], abs=1e-15)
|
|
summary = result["summary"][0]
|
|
for prefix in ("ic", "rank_ic"):
|
|
assert summary[f"{prefix}_valid_days"] == 3
|
|
assert summary[f"{prefix}_mean"] == pytest.approx(1 / 6)
|
|
assert summary[f"{prefix}_std"] == pytest.approx(sqrt(7 / 12))
|
|
assert summary[f"{prefix}_ir"] == pytest.approx(.2182178902359924)
|
|
assert summary[f"{prefix}_t"] == pytest.approx(.3779644730092272)
|
|
assert summary[f"{prefix}_p"] == pytest.approx(.7418011102528389)
|
|
|
|
|
|
def test_each_side_has_three_values_but_only_two_common_pairs():
|
|
frame = panel({"left": [1, 2, 3, None], "right": [None, 2, 3, 4]}, assets=list("ABCD"))
|
|
result = api().correlation_matrix(frame)
|
|
assert result["n_pairs"][0][1] == 2
|
|
assert result["matrix"][0][1] is None
|
|
assert result["status"][0][1] == "insufficient_pairs"
|
|
|
|
|
|
def test_rank_preserves_distinct_tiny_values_beside_a_large_outlier():
|
|
frame = panel({"left": [1e-308, 2e-308, 1e308], "right": [1, 2, 3]})
|
|
assert api().correlation_matrix(frame, method="spearman")["matrix"][0][1] == pytest.approx(1)
|
|
|
|
|
|
def test_daily_rank_ic_preserves_tiny_distinct_observations():
|
|
frame = panel({"f": [1e-200, 2e-200, 3e-200, 1e308]}, assets=list("ABCD"))
|
|
returns = pd.Series([4, 3, 2, 1], index=frame.index)
|
|
result = api().daily_ic(frame, returns)
|
|
assert result["rows"][0]["rank_ic"] == pytest.approx(-1)
|
|
assert result["rows"][0]["rank_ic_status"] == "ok"
|
|
|
|
|
|
def test_affine_equivalent_daily_ic_does_not_turn_roundoff_into_extreme_significance():
|
|
frame = panel({"f": [1, 2, 3, 10, 20, 30, 101, 102, 103]}, days=3)
|
|
returns = pd.Series([1, 1, 2, 101, 101, 102, 100001, 100001, 100002], index=frame.index)
|
|
result = api().daily_ic(frame, returns)
|
|
assert [point["ic"] for point in result["rows"]] == pytest.approx([sqrt(3) / 2] * 3)
|
|
summary = result["summary"][0]
|
|
assert summary["ic_mean"] == pytest.approx(sqrt(3) / 2)
|
|
assert summary["ic_valid_days"] == 3
|
|
assert summary["ic_ir"] is summary["ic_t"] is summary["ic_p"] is None
|
|
assert summary["ic_std"] <= result["method"]["minimum_ic_std_for_ratios"]
|
|
assert summary["ic_status"] in {"constant_values", "below_resolution"}
|
|
|
|
|
|
@pytest.mark.parametrize("offset,step", [(1e16, 2.0), (1e12, np.spacing(1e12))])
|
|
def test_pearson_preserves_representable_differences_beside_a_large_offset(offset, step):
|
|
frame = panel({"f": offset + np.arange(5) * step}, assets=list("ABCDE"))
|
|
returns = pd.Series([2, 5, 1, 4, 3], index=frame.index)
|
|
matrix = api().correlation_matrix(frame.assign(other=returns))
|
|
assert matrix["matrix"][0][1] == pytest.approx(.1, abs=1e-14)
|
|
daily = api().daily_ic(frame, returns)
|
|
assert daily["rows"][0]["ic"] == pytest.approx(.1, abs=1e-14)
|