367 lines
12 KiB
Python
367 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import hyperscalper_feature_gap_analysis_v1 as runner
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def test_read_only_runner_writes_711_map_and_honest_deq_blocker(tmp_path, monkeypatch):
|
|
rows = [
|
|
{"request_id": f"r{index}", "indicator_id": index, "period": 10, "p1": 0.0}
|
|
for index in range(711)
|
|
]
|
|
request = tmp_path / "request.json"
|
|
request.write_text(json.dumps({"requests": rows}), encoding="utf-8")
|
|
checkpoint = tmp_path / "checkpoint.npz"
|
|
values = {f"r{index}": np.arange(8, dtype=float) + index for index in range(711)}
|
|
values["r710"] = np.full(8, np.nan)
|
|
np.savez(checkpoint, **values)
|
|
manifest = tmp_path / "manifest.json"
|
|
manifest.write_text(
|
|
json.dumps({"coverage": {"feature_versions": [f"r{index}" for index in range(711)]}}),
|
|
encoding="utf-8",
|
|
)
|
|
for name in ("usage.parquet", "lineage.parquet"):
|
|
(tmp_path / name).write_bytes(b"not-read-without-pyarrow")
|
|
output = tmp_path / "out"
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
[
|
|
"runner",
|
|
"--oracle-request",
|
|
str(request),
|
|
"--oracle-checkpoint",
|
|
str(checkpoint),
|
|
"--acceptance-manifest",
|
|
str(manifest),
|
|
"--historical-usage",
|
|
str(tmp_path / "usage.parquet"),
|
|
"--lineage",
|
|
str(tmp_path / "lineage.parquet"),
|
|
"--output-dir",
|
|
str(output),
|
|
"--sample-rows",
|
|
"8",
|
|
],
|
|
)
|
|
runner.main()
|
|
result = json.loads((output / "hyperscalper_feature_gap_analysis_v1.json").read_text())
|
|
assert result["read_only"] is True
|
|
assert len(result["information_map"]["features"]) == 711
|
|
assert result["information_map"]["features"][-1]["redundancy_status"] == (
|
|
"unusable_no_finite_observations"
|
|
)
|
|
assert result["redundancy"]["map"]["format"] == "parquet"
|
|
assert result["decision_equivalence"]["status"] == "BLOCKED"
|
|
|
|
|
|
def test_checkpoint_directory_maps_hs22_filenames_to_engineering_primitives(tmp_path):
|
|
rows = [
|
|
{"indicator_id": index, "period": index + 10, "p1": float(index % 3)}
|
|
for index in range(711)
|
|
]
|
|
checkpoint_dir = tmp_path / "checkpoints"
|
|
checkpoint_dir.mkdir()
|
|
for row in reversed(rows):
|
|
np.save(
|
|
checkpoint_dir
|
|
/ f"hs22_{row['indicator_id']}_{row['period']}_{row['p1']:g}.npy",
|
|
np.array([row["indicator_id"], row["period"]], dtype=float),
|
|
allow_pickle=False,
|
|
)
|
|
|
|
matrix, names = runner.checkpoint_directory_columns(
|
|
checkpoint_dir,
|
|
[
|
|
{
|
|
**row,
|
|
"feature_key": f"{row['indicator_id']}:{row['period']}:{row['p1']:g}",
|
|
"request_id": None,
|
|
}
|
|
for row in rows
|
|
],
|
|
)
|
|
|
|
assert matrix.shape == (2, 711)
|
|
assert np.array_equal(matrix[:, 0], [0.0, 10.0])
|
|
assert np.array_equal(matrix[:, -1], [710.0, 720.0])
|
|
assert names[0] == "hs22_0_10_0.npy"
|
|
|
|
|
|
def test_pairwise_redundancy_uses_staggered_finite_observations_for_clustering():
|
|
sample = np.array(
|
|
[
|
|
[0.0, 0.0, np.nan, np.nan],
|
|
[1.0, 2.0, np.nan, np.nan],
|
|
[2.0, 4.0, np.nan, np.nan],
|
|
[np.nan, np.nan, 5.0, np.nan],
|
|
[np.nan, np.nan, 6.0, np.nan],
|
|
]
|
|
)
|
|
|
|
pearson, spearman, counts = runner.pairwise_redundancy(sample, min_pair_samples=3)
|
|
|
|
assert counts[0, 1] == 3
|
|
assert pearson[0, 1] == pytest.approx(1.0)
|
|
assert spearman[0, 1] == pytest.approx(1.0)
|
|
assert np.isnan(pearson[0, 2])
|
|
assert runner.clusters(pearson) == [[0, 1]]
|
|
|
|
|
|
def test_pairwise_redundancy_precomputes_ranks_once_per_column_with_bounded_runtime(monkeypatch):
|
|
sample = np.arange(512 * 64, dtype=float).reshape(512, 64)
|
|
sample[::17, ::7] = np.nan
|
|
calls = 0
|
|
original = runner.rank_finite_columns
|
|
|
|
def counted_ranks(values):
|
|
nonlocal calls
|
|
calls += 1
|
|
return original(values)
|
|
|
|
monkeypatch.setattr(runner, "rank_finite_columns", counted_ranks)
|
|
started = time.perf_counter()
|
|
pearson, spearman, counts = runner.pairwise_redundancy(sample, min_pair_samples=2)
|
|
|
|
assert time.perf_counter() - started < 3.0
|
|
assert calls == 1
|
|
assert counts[0, 1] > 2
|
|
assert pearson[0, 1] == pytest.approx(1.0)
|
|
assert spearman[0, 1] == pytest.approx(1.0, abs=1e-5)
|
|
|
|
|
|
def test_type_aware_redundancy_uses_agreement_and_jaccard_for_detection_outputs():
|
|
sample = np.array(
|
|
[
|
|
[0.0, 1.0, 1.0, 0.0],
|
|
[1.0, 1.0, 1.0, 1.0],
|
|
[1.0, 0.0, 0.0, 1.0],
|
|
[0.0, 0.0, 0.0, 0.0],
|
|
]
|
|
)
|
|
|
|
pearson, spearman, agreement, jaccard, counts = runner.type_aware_redundancy(
|
|
sample, ["state", "state", "event", "event"], min_pair_samples=2
|
|
)
|
|
primary = runner.primary_redundancy_matrix(
|
|
pearson, agreement, jaccard, ["state", "state", "event", "event"]
|
|
)
|
|
|
|
assert counts[0, 1] == 4
|
|
assert agreement[0, 1] == pytest.approx(0.5)
|
|
assert jaccard[2, 3] == pytest.approx(1 / 3)
|
|
assert np.isnan(pearson[0, 1])
|
|
assert primary[0, 1] == pytest.approx(0.5)
|
|
assert primary[2, 3] == pytest.approx(1 / 3)
|
|
|
|
|
|
def test_semantic_types_derives_type_and_domain_from_primitive_map(tmp_path):
|
|
semantic_map = tmp_path / "semantic-map.json"
|
|
semantic_map.write_text(
|
|
json.dumps(
|
|
{
|
|
"primitives": [
|
|
{"indicator_id": 1, "period": 10, "p1": 0, "output_type": "boolean", "engineering_family": "regime"},
|
|
{"indicator_id": 2, "period": 20, "p1": 1, "output_type": "event", "domain": "trigger"},
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
rows = [
|
|
{"feature_key": "1:10:0", "indicator_id": 1, "period": 10, "p1": 0.0},
|
|
{"feature_key": "2:20:1", "indicator_id": 2, "period": 20, "p1": 1.0},
|
|
]
|
|
|
|
types, blocker = runner.semantic_types(semantic_map, rows)
|
|
|
|
assert blocker is None
|
|
assert types == [
|
|
{"output_type": "state", "domain": "regime"},
|
|
{"output_type": "event", "domain": "trigger"},
|
|
]
|
|
|
|
|
|
def test_lineage_combo_rejects_incomplete_data():
|
|
with pytest.raises(ValueError, match="at least five triples"):
|
|
runner.lineage_combo([1, 10, 0.0])
|
|
with pytest.raises(ValueError, match="at least five triples"):
|
|
runner.lineage_combo([[1, 10, 0.0]] * 5)
|
|
|
|
|
|
def test_lineage_combo_reads_only_the_five_primitives_from_real_flat_encoding():
|
|
# Flat lineage rows append tp, sl, confirmation bounds, and volume threshold.
|
|
combo = [
|
|
17, 20, 2.0,
|
|
31, 14, 0.0,
|
|
47, 9, 1.5,
|
|
62, 30, 0.0,
|
|
70, 20, 0.0,
|
|
0.015, 0.01, 5, 20, 5, 20, 75,
|
|
]
|
|
|
|
assert runner.lineage_combo(json.dumps(combo)) == (
|
|
(17, 20, 2.0),
|
|
(31, 14, 0.0),
|
|
(47, 9, 1.5),
|
|
(62, 30, 0.0),
|
|
(70, 20, 0.0),
|
|
)
|
|
|
|
|
|
def test_lineage_usage_parses_combo_json_role_triples(tmp_path):
|
|
pa = pytest.importorskip("pyarrow")
|
|
pq = pytest.importorskip("pyarrow.parquet")
|
|
lineage = tmp_path / "lineage.parquet"
|
|
combo = [1, 10, 0.0, 2, 11, 1.5, 1, 10, 0.0, 4, 13, 2.0, 5, 14, 3.0]
|
|
pq.write_table(
|
|
pa.Table.from_pylist(
|
|
[
|
|
{"strategy_id": 100, "combo_json": json.dumps(combo)},
|
|
{"strategy_id": 101, "combo_json": json.dumps(combo + [0.01, 0.02, 3, 6, 3, 6, 75])},
|
|
]
|
|
),
|
|
lineage,
|
|
)
|
|
|
|
counts, evidence, blocker = runner.lineage_usage(lineage)
|
|
|
|
assert blocker is None
|
|
assert counts == {"1:10:0": 4, "2:11:1.5": 2, "4:13:2": 2, "5:14:3": 2}
|
|
assert evidence["role_counts"] == {
|
|
"confirm": 2,
|
|
"signal": 2,
|
|
"trend": 2,
|
|
"trigger": 2,
|
|
"vol": 2,
|
|
}
|
|
assert evidence["strategy_ids_observed"] == 2
|
|
assert evidence["combo_counts"][0]["usage_count"] == 2
|
|
|
|
|
|
def test_cohort_deq_evidence_aggregates_exported_ledger_associatively(tmp_path):
|
|
deq_path = tmp_path / "cohort-deq.json"
|
|
sample = {
|
|
"deq": {
|
|
"return_1_bars_bps": 10.0,
|
|
"return_2_bars_bps": -5.0,
|
|
"return_3_bars_bps": None,
|
|
"return_5_bars_bps": 0.0,
|
|
"return_10_bars_bps": 20.0,
|
|
"mfe_bps": 25.0,
|
|
"mae_bps": -4.0,
|
|
"time_to_positive_bars": 2,
|
|
}
|
|
}
|
|
deq_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"contract": "Cohort001-DEQ-ledger-export-v1",
|
|
"strategies": [
|
|
{
|
|
"strategy_version_id": "strategy-a",
|
|
"feature_triples": [
|
|
{"indicator_id": 7, "period": 10, "p1": 1.0},
|
|
{"indicator_id": 7, "period": 20, "p1": 2.0},
|
|
],
|
|
"folds": [
|
|
{
|
|
"fold": "Fold 1",
|
|
"scenarios": [
|
|
{"scenario": "BASE", "deq_samples": [sample]},
|
|
{"scenario": "STRESS", "deq_samples": [sample]},
|
|
],
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
evidence, parquet_rows, blocker = runner.cohort_deq_evidence(deq_path)
|
|
|
|
assert blocker is None
|
|
assert evidence["status"] == "AVAILABLE"
|
|
assert evidence["attribution"] == "ASSOCIATIVE_NOT_CAUSAL"
|
|
assert evidence["strategy_summaries"][0]["trade_count"] == 2
|
|
assert evidence["strategy_summaries"][0]["scenario_coverage"] == ["BASE", "STRESS"]
|
|
returns = evidence["feature_summaries"][0]["returns"]
|
|
assert returns["return_1_bars_bps"]["mean"] == 10.0
|
|
assert returns["return_2_bars_bps"]["directional_incorrect_count"] == 2
|
|
assert evidence["family_summaries"][0]["trade_count"] == 4
|
|
assert {row["entity_type"] for row in parquet_rows} == {"strategy", "feature", "family"}
|
|
|
|
|
|
def test_cohort_deq_evidence_uses_genome_combo_when_exported_triples_are_null(tmp_path):
|
|
deq_path = tmp_path / "cohort-deq.json"
|
|
deq_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"contract": "Cohort001-DEQ-ledger-export-v1",
|
|
"strategies": [
|
|
{
|
|
"strategy_version_id": "strategy-a",
|
|
"feature_triples": None,
|
|
"genome": {
|
|
"combo": [
|
|
7, 10, 1.0,
|
|
8, 11, 2.0,
|
|
9, 12, 3.0,
|
|
10, 13, 4.0,
|
|
11, 14, 5.0,
|
|
0.01, 0.02, 3, 6, 3, 6, 75,
|
|
]
|
|
},
|
|
"folds": [
|
|
{
|
|
"fold": "Fold 1",
|
|
"scenarios": [
|
|
{
|
|
"scenario": "BASE",
|
|
"deq_samples": [{"deq": {"return_1_bars_bps": 10.0}}],
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
evidence, _, blocker = runner.cohort_deq_evidence(deq_path)
|
|
|
|
assert blocker is None
|
|
assert [item["feature_key"] for item in evidence["feature_summaries"]] == [
|
|
"10:13:4",
|
|
"11:14:5",
|
|
"7:10:1",
|
|
"8:11:2",
|
|
"9:12:3",
|
|
]
|
|
|
|
|
|
def test_deq_strategy_feature_keys_falls_back_when_exported_triples_are_invalid():
|
|
strategy = {
|
|
"feature_triples": [{"indicator_id": "not-an-id"}],
|
|
"genome": {"combo": [7, 10, 1.0, 8, 11, 2.0, 9, 12, 3.0, 10, 13, 4.0, 11, 14, 5.0]},
|
|
}
|
|
|
|
assert runner.deq_strategy_feature_keys(strategy) == [
|
|
("7:10:1", "7"),
|
|
("8:11:2", "8"),
|
|
("9:12:3", "9"),
|
|
("10:13:4", "10"),
|
|
("11:14:5", "11"),
|
|
]
|