Strategy review (2026-09-08) and a frozen, paired signal screen of residual 12-1 momentum divided by its formation-window residual volatility. Across 73 non-overlapping windows the normalized signal's IC is 0.0128 against 0.0145 for the matched control, below the frozen 0.03 floor, so the conditional portfolio replay was not run and nothing in production changes. Code-review fixes to the runner, before first commit: a missing OHLC price stops the run instead of reading as 0; the snapshot connection is closed explicitly (on Windows it otherwise kept the file locked); the paired-coverage check raises instead of asserting, so python -O cannot strip it; and source files are recorded as git blob ids with line endings normalized, so the report can be checked against history. The report was regenerated with the fixed runner; its screen section is byte-identical to the original run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
239 lines
9.6 KiB
Python
239 lines
9.6 KiB
Python
"""Economic invariants and calendar/coverage guards for normalization research."""
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import date, timedelta
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from app.services.momentum_service import compute_residual_12_1_momentum
|
|
from scripts import run_residual_normalization as study
|
|
|
|
|
|
def sessions(n=400):
|
|
days, day = [], date(2019, 1, 2)
|
|
while len(days) < n:
|
|
if day.weekday() < 5:
|
|
days.append(day)
|
|
day += timedelta(days=1)
|
|
return days
|
|
|
|
|
|
def series(beta=1.3, scale=1.0):
|
|
days = sessions()
|
|
rng = np.random.default_rng(59)
|
|
market = rng.normal(0.0003, 0.01, len(days) - 1)
|
|
specific = rng.normal(0.0008, 0.005, len(days) - 1)
|
|
spy = [100.0, *list(100 * np.cumprod(1 + market))]
|
|
stock = [100.0, *list(100 * np.cumprod(1 + beta * market + scale * specific))]
|
|
return days, stock, dict(zip(days, spy)), {d: i for i, d in enumerate(days)}
|
|
|
|
|
|
def test_numerator_is_production_and_denominator_uses_sample_variance():
|
|
days, stock, spy, index = series()
|
|
i = 300
|
|
got = study.signal_at(days, stock, i, spy, index)
|
|
assert got["paired_control"] == compute_residual_12_1_momentum(days[:i + 1], stock[:i + 1], spy)
|
|
s = np.diff(np.array(stock[i - 252:i - 21 + 1])) / np.array(stock[i - 252:i - 21])
|
|
mclose = np.array([spy[d] for d in days[i - 252:i - 21 + 1]])
|
|
m = np.diff(mclose) / mclose[:-1]
|
|
# Independent least-squares slope calculation, keeping alpha in the score.
|
|
intercept, beta = np.linalg.lstsq(np.column_stack([np.ones(len(m)), m]), s, rcond=None)[0]
|
|
assert intercept != 0
|
|
expected_sd = (s - beta * m).std(ddof=1)
|
|
assert got["residual_sd"] == pytest.approx(expected_sd)
|
|
assert got["normalized"] == pytest.approx(got["paired_control"] / expected_sd)
|
|
|
|
|
|
def test_normalization_removes_specific_return_scale_but_keeps_direction():
|
|
days, stock, spy, index = series(scale=1)
|
|
_, larger, _, _ = series(scale=3)
|
|
a = study.signal_at(days, stock, 350, spy, index)
|
|
b = study.signal_at(days, larger, 350, spy, index)
|
|
assert abs(a["paired_control"]) > 0.01
|
|
assert b["paired_control"] == pytest.approx(3 * a["paired_control"])
|
|
assert b["normalized"] == pytest.approx(a["normalized"])
|
|
assert np.sign(a["normalized"]) == np.sign(a["paired_control"])
|
|
|
|
|
|
def test_market_beta_change_does_not_become_specific_strength():
|
|
days, stock, spy, index = series(beta=0.5)
|
|
_, high_beta, _, _ = series(beta=2.5)
|
|
a = study.signal_at(days, stock, 350, spy, index)
|
|
b = study.signal_at(days, high_beta, 350, spy, index)
|
|
assert b["normalized"] == pytest.approx(a["normalized"])
|
|
|
|
|
|
def test_skipped_month_and_future_are_not_signal_inputs():
|
|
days, stock, spy, index = series()
|
|
i = 300
|
|
baseline = study.signal_at(days, stock, i, spy, index)
|
|
for k in range(i - 20, len(stock)):
|
|
stock[k] *= 1.8
|
|
spy[days[k]] *= 0.7
|
|
assert study.signal_at(days, stock, i, spy, index) == baseline
|
|
|
|
|
|
def test_missing_benchmark_pair_stays_only_in_unrestricted_control():
|
|
days, stock, spy, index = series()
|
|
del spy[days[200]]
|
|
index = {d: i for i, d in enumerate(sorted(spy))}
|
|
got = study.signal_at(days, stock, 350, spy, index)
|
|
assert got["unrestricted_control"] is not None
|
|
assert got["paired_control"] is None
|
|
assert got["normalized"] is None
|
|
|
|
|
|
def test_missing_stock_session_cannot_be_called_daily_aligned():
|
|
days, stock, spy, index = series()
|
|
del days[200]
|
|
del stock[200]
|
|
got = study.signal_at(days, stock, 349, spy, index)
|
|
assert got["unrestricted_control"] is not None
|
|
assert got["reason"] == "unaligned_formation"
|
|
|
|
|
|
def test_perfect_market_tracker_is_not_an_infinite_score():
|
|
days, _, spy, index = series()
|
|
got = study.signal_at(days, [spy[d] for d in days], 350, spy, index)
|
|
assert got["normalized"] is None
|
|
assert got["reason"] == "zero_residual_variance"
|
|
|
|
|
|
def test_nonoverlap_uses_sessions_and_permits_only_a_shared_endpoint():
|
|
# Arbitrary indices stand in for a calendar with holidays/year boundaries.
|
|
candidates = [100, 104, 109, 114, 119, 123, 128, 133, 138, 143, 148, 153, 158, 163]
|
|
assert study.nonoverlapping_indices(candidates) == [100, 133, 163]
|
|
|
|
|
|
def test_weekly_endpoint_not_moved_earlier_to_obtain_forward_data():
|
|
days = sessions(100)
|
|
got = study.weekly_indices(days)
|
|
last_per_week = {}
|
|
for i, day in enumerate(days):
|
|
last_per_week[day.isocalendar()[:2]] = i
|
|
assert all(last_per_week[days[i].isocalendar()[:2]] == i for i in got)
|
|
assert all(i + study.HORIZON < len(days) for i in got)
|
|
|
|
|
|
def test_identical_columns_have_zero_paired_bootstrap_difference():
|
|
x = np.linspace(-0.1, 0.2, 25)
|
|
matrix = np.column_stack([x, x, x])
|
|
got = study.bootstrap_means(matrix)
|
|
assert got["paired_delta_ci95"] == [0.0, 0.0]
|
|
assert got == study.bootstrap_means(matrix)
|
|
|
|
|
|
def test_screen_uses_unrounded_floor_and_requires_both_eras():
|
|
def summaries(value, pre=0.01, post=0.04):
|
|
return {era: {"windows": 20, "arms": {"normalized": {"mean_ic": mean}}}
|
|
for era, mean in (("full", value), ("pre_2021", pre), ("post_2021", post))}
|
|
assert not study.screen_pass(summaries(0.0299999))["passed"]
|
|
assert study.screen_pass(summaries(0.03))["passed"]
|
|
assert not study.screen_pass(summaries(0.04, pre=-0.001))["passed"]
|
|
|
|
|
|
def test_screen_keeps_paired_stocks_and_forward_endpoints_identical(monkeypatch):
|
|
days = sessions()
|
|
benchmark = {d: 100 + i for i, d in enumerate(days)}
|
|
prices = {}
|
|
for k in range(25):
|
|
closes = [100 + k + i * (k + 1) / 100 for i in range(len(days))]
|
|
prices[f"S{k:02}"] = ([d.toordinal() for d in days], [], [], [], closes, [])
|
|
|
|
def fake_signal(dates, closes, i, spy, index):
|
|
value = closes[0]
|
|
# One name remains in the unrestricted control but not the paired set.
|
|
excluded = value == 100
|
|
return {"unrestricted_control": value, "paired_control": None if excluded else value,
|
|
"normalized": None if excluded else value / 2,
|
|
"reason": "unaligned_formation" if excluded else "eligible"}
|
|
|
|
monkeypatch.setattr(study, "signal_at", fake_signal)
|
|
report = study.run_screen(prices, benchmark)
|
|
assert report["windows"]
|
|
index = {d.isoformat(): i for i, d in enumerate(days)}
|
|
previous_end = None
|
|
for window in report["windows"]:
|
|
assert index[window["forward_end"]] - index[window["asof"]] == 30
|
|
if previous_end:
|
|
assert window["asof"] >= previous_end
|
|
previous_end = window["forward_end"]
|
|
assert window["arms"]["unrestricted_control"]["n"] == 25
|
|
assert window["arms"]["paired_control"]["n"] == 24
|
|
assert window["arms"]["normalized"]["n"] == 24
|
|
assert window["arms"]["normalized"]["ic"] == window["arms"]["paired_control"]["ic"]
|
|
|
|
|
|
def write_snapshot(path, closes):
|
|
"""Minimal snapshot: one ticker, SPY, and a matching completion manifest."""
|
|
days = sessions(len(closes))
|
|
con = sqlite3.connect(path)
|
|
con.executescript(
|
|
"CREATE TABLE tickers (id INTEGER, symbol TEXT);"
|
|
"CREATE TABLE research_rank_only (symbol TEXT);"
|
|
"CREATE TABLE ohlcv_records (ticker_id INTEGER, date TEXT, open REAL, high REAL,"
|
|
" low REAL, close REAL, volume REAL);"
|
|
"CREATE TABLE benchmark_prices (symbol TEXT, date TEXT, close REAL);"
|
|
"INSERT INTO tickers VALUES (1, 'AAA');"
|
|
)
|
|
con.executemany(
|
|
"INSERT INTO ohlcv_records VALUES (1, ?, ?, ?, ?, ?, NULL)",
|
|
[(d.isoformat(), c, c, c, c) for d, c in zip(days, closes)],
|
|
)
|
|
con.executemany(
|
|
"INSERT INTO benchmark_prices VALUES ('SPY', ?, 100.0)", [(d.isoformat(),) for d in days]
|
|
)
|
|
con.commit()
|
|
con.close()
|
|
Path(str(path) + ".manifest.json").write_text(json.dumps({
|
|
"complete": True, "ticker_count": 1, "ohlcv_row_count": len(closes), "rank_only_count": 0,
|
|
}))
|
|
|
|
|
|
def test_missing_price_stops_the_run_instead_of_reading_as_zero(tmp_path):
|
|
path = tmp_path / "snap.sqlite"
|
|
write_snapshot(path, [100.0, None, 102.0])
|
|
with pytest.raises(ValueError, match="Missing OHLC price: AAA"):
|
|
study.read_snapshot(path)
|
|
|
|
|
|
def test_snapshot_read_keeps_missing_volume_and_releases_the_file(tmp_path):
|
|
path = tmp_path / "snap.sqlite"
|
|
write_snapshot(path, [100.0, 101.0, 102.0])
|
|
prices, benchmark, _ = study.read_snapshot(path)
|
|
assert prices["AAA"][4] == [100.0, 101.0, 102.0]
|
|
assert prices["AAA"][5] == [0.0, 0.0, 0.0]
|
|
assert len(benchmark) == 3
|
|
path.unlink() # Windows refuses while a connection still holds the file.
|
|
|
|
|
|
def test_paired_coverage_mismatch_raises_rather_than_asserts(monkeypatch):
|
|
days = sessions()
|
|
benchmark = {d: 100 + i for i, d in enumerate(days)}
|
|
prices = {
|
|
f"S{k:02}": ([d.toordinal() for d in days], [], [], [],
|
|
[100 + k + i * (k + 1) / 100 for i in range(len(days))], [])
|
|
for k in range(25)
|
|
}
|
|
|
|
def broken_pairing(dates, closes, i, spy, index):
|
|
value = closes[0]
|
|
return {"unrestricted_control": value, "paired_control": value,
|
|
"normalized": None if value == 100 else value / 2, "reason": "eligible"}
|
|
|
|
monkeypatch.setattr(study, "signal_at", broken_pairing)
|
|
with pytest.raises(AssertionError, match="Paired arms scored different stocks"):
|
|
study.run_screen(prices, benchmark)
|
|
|
|
|
|
def test_source_ids_are_git_blob_ids_whatever_the_line_endings(tmp_path):
|
|
lf, crlf = tmp_path / "lf.txt", tmp_path / "crlf.txt"
|
|
lf.write_bytes(b"hello\n")
|
|
crlf.write_bytes(b"hello\r\n")
|
|
# `printf 'hello\n' | git hash-object --stdin`
|
|
assert study.git_blob_id(lf) == "ce013625030ba8dba906f756967f9e9ca394464a"
|
|
assert study.git_blob_id(crlf) == study.git_blob_id(lf)
|