Remove overlay backtesting and scoring
parent
1d9507540b
commit
76581ead04
@ -1,125 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import datetime as dt
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from apps.backtester.run import _build_merged_snapshot_store
|
|
||||||
from libs.backtest.allocator import _macro_regime_state
|
|
||||||
from libs.backtest.manifests import load_manifest, resolve_config
|
|
||||||
from libs.backtest.overlay import (
|
|
||||||
build_overlay_curve,
|
|
||||||
load_equity_curve_csv,
|
|
||||||
load_merged_store_from_snapshot_dir,
|
|
||||||
summarize_overlay_curve,
|
|
||||||
)
|
|
||||||
from libs.common.config import get_settings
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(value: str | None) -> dt.date | None:
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
return dt.date.fromisoformat(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _compute_regimes(
|
|
||||||
*,
|
|
||||||
snapshot_dir: str | Path,
|
|
||||||
split: str,
|
|
||||||
config_path: str | Path,
|
|
||||||
start_date: dt.date | None,
|
|
||||||
end_date: dt.date | None,
|
|
||||||
) -> dict[dt.date, str]:
|
|
||||||
del split # overlay regimes should cover the full requested window, not a single split
|
|
||||||
manifest = load_manifest(config_path)
|
|
||||||
config = resolve_config(manifest, config_root=".")
|
|
||||||
|
|
||||||
raw_snapshot_dir = Path(snapshot_dir)
|
|
||||||
if (raw_snapshot_dir / "train.parquet").exists() or (raw_snapshot_dir / "test.parquet").exists():
|
|
||||||
settings = get_settings()
|
|
||||||
store = load_merged_store_from_snapshot_dir(
|
|
||||||
raw_snapshot_dir,
|
|
||||||
oracle_url=settings.stock_oracle_url,
|
|
||||||
db_dsn=settings.postgres_dsn,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
store = _build_merged_snapshot_store(
|
|
||||||
manifest,
|
|
||||||
config,
|
|
||||||
snapshot_dir_override=str(raw_snapshot_dir),
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
|
||||||
store = _build_merged_snapshot_store(
|
|
||||||
manifest,
|
|
||||||
config,
|
|
||||||
snapshot_dir_override=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if start_date or end_date:
|
|
||||||
lower = start_date or dt.date.min
|
|
||||||
upper = end_date or dt.date.max
|
|
||||||
store = store.slice_by_date_range(lower, upper)
|
|
||||||
|
|
||||||
regimes: dict[dt.date, str] = {}
|
|
||||||
for date in store.all_trading_days():
|
|
||||||
regimes[date] = _macro_regime_state(config, store.get_macro_for_date(date))
|
|
||||||
return regimes
|
|
||||||
|
|
||||||
|
|
||||||
def _load_spec(path: str | Path) -> dict[str, Any]:
|
|
||||||
return json.loads(Path(path).read_text())
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="Evaluate a regime-switched overlay from book equity curves")
|
|
||||||
parser.add_argument("--spec", required=True, help="Path to overlay spec JSON")
|
|
||||||
parser.add_argument("--output-dir", required=True, help="Directory to write overlay outputs")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
spec = _load_spec(args.spec)
|
|
||||||
initial_equity = float(spec.get("initial_equity", 10_000.0))
|
|
||||||
|
|
||||||
curves = {
|
|
||||||
book["label"]: load_equity_curve_csv(book["equity_csv"])
|
|
||||||
for book in spec["books"]
|
|
||||||
}
|
|
||||||
regime_source = spec["regime_source"]
|
|
||||||
start_date = _parse_date(spec.get("start_date"))
|
|
||||||
end_date = _parse_date(spec.get("end_date"))
|
|
||||||
regimes = _compute_regimes(
|
|
||||||
snapshot_dir=regime_source["snapshot_dir"],
|
|
||||||
split=regime_source.get("split", "train"),
|
|
||||||
config_path=regime_source["config_path"],
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
)
|
|
||||||
|
|
||||||
curve = build_overlay_curve(
|
|
||||||
curves=curves,
|
|
||||||
allocations=spec["allocations"],
|
|
||||||
regimes_by_date=regimes,
|
|
||||||
initial_equity=initial_equity,
|
|
||||||
)
|
|
||||||
if start_date:
|
|
||||||
curve = curve[curve["date"] >= start_date]
|
|
||||||
if end_date:
|
|
||||||
curve = curve[curve["date"] <= end_date]
|
|
||||||
|
|
||||||
summary = summarize_overlay_curve(curve, initial_equity=initial_equity)
|
|
||||||
summary["overlay_name"] = spec.get("overlay_name", Path(args.spec).stem)
|
|
||||||
summary["books"] = [book["label"] for book in spec["books"]]
|
|
||||||
summary["allocations"] = spec["allocations"]
|
|
||||||
|
|
||||||
out_dir = Path(args.output_dir)
|
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
curve.assign(date=curve["date"].astype(str)).to_csv(out_dir / "overlay_equity.csv", index=False)
|
|
||||||
(out_dir / "overlay_summary.json").write_text(json.dumps(summary, indent=2) + "\n")
|
|
||||||
print(json.dumps(summary, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v1",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6.221_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v1b",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6.221_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v1c",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6.221_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 0.5,
|
|
||||||
"mom": 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.114.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/fallback_book_compare/return_max_long_v6.114_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"overlay_name": "return_book_overlay_v2",
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0,
|
|
||||||
"mom": 1
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0,
|
|
||||||
"mom": 1
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1,
|
|
||||||
"mom": 0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 1,
|
|
||||||
"mom": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.114.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/fallback_book_compare/return_max_long_v6.114_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"overlay_name": "return_book_overlay_v2b",
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0,
|
|
||||||
"mom": 1
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0,
|
|
||||||
"mom": 1
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1,
|
|
||||||
"mom": 0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 0,
|
|
||||||
"mom": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v3",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6.221_equity.csv",
|
|
||||||
"experiment_config": "configs/experiments/return_max_long_v6.221.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv",
|
|
||||||
"experiment_config": "configs/experiments/return_max_long_v6new.54.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {"core": 0.5, "mom": 0.5},
|
|
||||||
"neutral": {"core": 0.0, "mom": 1.0},
|
|
||||||
"risk_off": {"core": 1.0, "mom": 0.0},
|
|
||||||
"unknown": {"core": 1.0, "mom": 0.0}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v3_oot",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2020-01-02",
|
|
||||||
"end_date": "2021-12-31",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/datasets/snapshots/midlarge-liquid-long-v1-oot-2020-2021",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_oot_inputs/csv/core_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_oot_inputs/csv/mom_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0.5,
|
|
||||||
"mom": 0.5
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0.0,
|
|
||||||
"mom": 1.0
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v3b",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2022-03-03",
|
|
||||||
"end_date": "2026-03-13",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_mom_v2",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6.221_equity.csv",
|
|
||||||
"experiment_config": "configs/experiments/return_max_long_v6.221.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_v1_inputs/return_max_long_v6new.54_equity.csv",
|
|
||||||
"experiment_config": "configs/experiments/return_max_long_v6new.54.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {"core": 0.5, "mom": 0.5},
|
|
||||||
"neutral": {"core": 0.5, "mom": 0.5},
|
|
||||||
"risk_off": {"core": 1.0, "mom": 0.0},
|
|
||||||
"unknown": {"core": 1.0, "mom": 0.0}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"overlay_name": "return_book_overlay_v3b_oot",
|
|
||||||
"initial_equity": 10000,
|
|
||||||
"start_date": "2020-01-02",
|
|
||||||
"end_date": "2021-12-31",
|
|
||||||
"regime_source": {
|
|
||||||
"config_path": "configs/experiments/return_max_long_v6.221.json",
|
|
||||||
"snapshot_dir": "data/datasets/snapshots/midlarge-liquid-long-v1-oot-2020-2021",
|
|
||||||
"split": "train"
|
|
||||||
},
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"label": "core",
|
|
||||||
"equity_csv": "runs/book_overlay_oot_inputs/csv/core_equity.csv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "mom",
|
|
||||||
"equity_csv": "runs/book_overlay_oot_inputs/csv/mom_equity.csv"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"risk_on": {
|
|
||||||
"core": 0.5,
|
|
||||||
"mom": 0.5
|
|
||||||
},
|
|
||||||
"neutral": {
|
|
||||||
"core": 0.5,
|
|
||||||
"mom": 0.5
|
|
||||||
},
|
|
||||||
"risk_off": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
},
|
|
||||||
"unknown": {
|
|
||||||
"core": 1.0,
|
|
||||||
"mom": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
# Overlay Strategy Leaderboard
|
|
||||||
_Updated: 2026-03-27T03:43:33.985346+00:00_
|
|
||||||
|
|
||||||
_This board is separate from the default single-book leaderboard. Overlay rows are book-of-books evaluations and are not directly comparable to single-book `SQS` rows._
|
|
||||||
|
|
||||||
_`SQS` here is overlay official SQS: common-window overlay score with a stress OOT gate. `T.*` columns are common-window overlay metrics._
|
|
||||||
|
|
||||||
| # | Overlay | SQS | [T]Ret% | [T]Ann% | [T]DD% | Stress Ret% | Stress DD% | Stress Sharpe | Date |
|
|
||||||
|---|---------|-----|----------|----------|--------|-------------|------------|---------------|------|
|
|
||||||
| 1 | return_book_overlay_v4 | 91.4 | +185.3 | +29.7 | 5.0 | +7.4 | 5.8 | +0.80 | 2026-03-27 |
|
|
||||||
| 2 | return_book_overlay_v4b | 91.4 | +187.2 | +30.0 | 5.0 | +7.4 | 5.8 | +0.80 | 2026-03-27 |
|
|
||||||
| 3 | return_book_overlay_v3 | 90.2 | +175.6 | +28.6 | 4.5 | +6.0 | 5.4 | +0.67 | 2026-03-26 |
|
|
||||||
| 4 | return_book_overlay_v3b | 87.7 | +169.5 | +27.9 | 5.0 | +7.4 | 5.8 | +0.80 | 2026-03-26 |
|
|
||||||
| 5 | return_book_overlay_v4c | 60.7 | +232.5 | +34.8 | 5.2 | +3.1 | 3.3 | +0.43 | 2026-03-27 |
|
|
||||||
|
|
||||||
## Recent Overlay Entries
|
|
||||||
### IMP-0794 (2026-03-27) — return_book_overlay_v4b
|
|
||||||
Hypothesis: Core v6.221 plus higher-ranked v6new.207 momentum book with balanced 50/50 neutral allocation.
|
|
||||||
Verdict: **UNKNOWN** (SQS 91.4)
|
|
||||||
|
|
||||||
### IMP-0793 (2026-03-27) — return_book_overlay_v4c
|
|
||||||
Hypothesis: Core v6.221 plus top-return v6new.275 momentum book with aggressive 75/25 risk-on and full-neutral momentum allocation.
|
|
||||||
Verdict: **UNKNOWN** (SQS 60.7)
|
|
||||||
|
|
||||||
### IMP-0792 (2026-03-27) — return_book_overlay_v4
|
|
||||||
Hypothesis: Core v6.221 plus higher-ranked v6new.196 momentum book with balanced 50/50 neutral allocation.
|
|
||||||
Verdict: **UNKNOWN** (SQS 91.4)
|
|
||||||
|
|
||||||
### IMP-0738 (2026-03-26) — return_book_overlay_v3b
|
|
||||||
Hypothesis: Balanced overlay variant with 50/50 neutral allocation between v6.221 and v6new.54.
|
|
||||||
Verdict: **UNKNOWN** (SQS 87.7)
|
|
||||||
|
|
||||||
### IMP-0737 (2026-03-26) — return_book_overlay_v3
|
|
||||||
Hypothesis: Book-of-books overlay using v6.221 as stress fallback and v6new.54 as normal-regime book.
|
|
||||||
Verdict: **UNKNOWN** (SQS 90.2)
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,193 +0,0 @@
|
|||||||
"""Evaluate deterministic book overlays from daily equity curves."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime as dt
|
|
||||||
import math
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
def load_merged_store_from_snapshot_dir(
|
|
||||||
snapshot_dir: str | Path,
|
|
||||||
*,
|
|
||||||
oracle_url: str,
|
|
||||||
db_dsn: str,
|
|
||||||
):
|
|
||||||
"""Load and merge train/valid/test splits from an explicit snapshot directory."""
|
|
||||||
from libs.backtest.snapshot_store import SnapshotStore
|
|
||||||
|
|
||||||
base = Path(snapshot_dir)
|
|
||||||
stores = []
|
|
||||||
for split in ("train", "valid", "test"):
|
|
||||||
if not (base / f"{split}.parquet").exists():
|
|
||||||
continue
|
|
||||||
stores.append(
|
|
||||||
SnapshotStore.load(
|
|
||||||
snapshot_dir=base,
|
|
||||||
split_name=split,
|
|
||||||
oracle_url=oracle_url,
|
|
||||||
db_dsn=db_dsn,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not stores:
|
|
||||||
raise FileNotFoundError(f"No snapshot splits found under {base}")
|
|
||||||
|
|
||||||
merged_candidates: dict[dt.date, dict[tuple[Any, ...], dict[str, Any]]] = {}
|
|
||||||
merged_bars: dict[str, dict[dt.date, dict[str, Any]]] = {}
|
|
||||||
merged_macro: dict[dt.date, dict[str, Any]] = {}
|
|
||||||
|
|
||||||
for store in stores:
|
|
||||||
for exec_date in store.all_execution_dates():
|
|
||||||
bucket = merged_candidates.setdefault(exec_date, {})
|
|
||||||
for candidate in store.get_candidates_for_date(exec_date):
|
|
||||||
dedupe_key = (
|
|
||||||
candidate.get("event_id"),
|
|
||||||
candidate.get("symbol"),
|
|
||||||
candidate.get("execution_date"),
|
|
||||||
candidate.get("reaction_date"),
|
|
||||||
)
|
|
||||||
bucket.setdefault(dedupe_key, candidate)
|
|
||||||
for symbol, bars in store._bars.items():
|
|
||||||
merged_bars.setdefault(symbol, {}).update(bars)
|
|
||||||
for macro_date, macro_values in store._macro.items():
|
|
||||||
merged_macro.setdefault(macro_date, {}).update(macro_values)
|
|
||||||
|
|
||||||
from libs.backtest.snapshot_store import SnapshotStore
|
|
||||||
|
|
||||||
return SnapshotStore(
|
|
||||||
candidates_by_exec_date={
|
|
||||||
date: list(rows.values())
|
|
||||||
for date, rows in merged_candidates.items()
|
|
||||||
},
|
|
||||||
bars_by_symbol_date=merged_bars,
|
|
||||||
macro_by_date=merged_macro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_equity_curve_csv(path: str | Path) -> pd.DataFrame:
|
|
||||||
"""Load a paper backtest equity CSV into a normalized daily returns frame."""
|
|
||||||
csv_path = Path(path)
|
|
||||||
df = pd.read_csv(csv_path, parse_dates=["date"])
|
|
||||||
required = {"date", "equity"}
|
|
||||||
missing = required.difference(df.columns)
|
|
||||||
if missing:
|
|
||||||
raise ValueError(f"Missing columns in {csv_path}: {sorted(missing)}")
|
|
||||||
if df.empty:
|
|
||||||
raise ValueError(f"Equity CSV has no rows: {csv_path}")
|
|
||||||
|
|
||||||
df = df.sort_values("date").copy()
|
|
||||||
df["date"] = pd.to_datetime(df["date"]).dt.date
|
|
||||||
df["equity"] = df["equity"].astype(float)
|
|
||||||
df["daily_return"] = df["equity"].pct_change().fillna(0.0)
|
|
||||||
return df[["date", "equity", "daily_return"]]
|
|
||||||
|
|
||||||
|
|
||||||
def validate_allocations(
|
|
||||||
allocations: dict[str, dict[str, float]],
|
|
||||||
labels: set[str],
|
|
||||||
*,
|
|
||||||
tolerance: float = 1e-6,
|
|
||||||
) -> None:
|
|
||||||
"""Ensure each regime allocation references known labels and sums to 1."""
|
|
||||||
if not allocations:
|
|
||||||
raise ValueError("allocations must not be empty")
|
|
||||||
if "unknown" not in allocations:
|
|
||||||
raise ValueError("allocations must include an 'unknown' regime")
|
|
||||||
|
|
||||||
for regime, weights in allocations.items():
|
|
||||||
unknown = set(weights).difference(labels)
|
|
||||||
if unknown:
|
|
||||||
raise ValueError(
|
|
||||||
f"Allocation for regime '{regime}' references unknown labels: {sorted(unknown)}"
|
|
||||||
)
|
|
||||||
total = sum(float(weight) for weight in weights.values())
|
|
||||||
if abs(total - 1.0) > tolerance:
|
|
||||||
raise ValueError(
|
|
||||||
f"Allocation for regime '{regime}' must sum to 1.0, got {total:.6f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_overlay_curve(
|
|
||||||
*,
|
|
||||||
curves: dict[str, pd.DataFrame],
|
|
||||||
allocations: dict[str, dict[str, float]],
|
|
||||||
regimes_by_date: dict[dt.date, str],
|
|
||||||
initial_equity: float = 10_000.0,
|
|
||||||
) -> pd.DataFrame:
|
|
||||||
"""Combine per-book daily returns into a single overlay equity curve."""
|
|
||||||
labels = set(curves)
|
|
||||||
if not labels:
|
|
||||||
raise ValueError("curves must not be empty")
|
|
||||||
validate_allocations(allocations, labels)
|
|
||||||
|
|
||||||
merged: pd.DataFrame | None = None
|
|
||||||
for label, df in curves.items():
|
|
||||||
renamed = df.rename(
|
|
||||||
columns={
|
|
||||||
"equity": f"equity_{label}",
|
|
||||||
"daily_return": f"daily_return_{label}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
frame = renamed[["date", f"daily_return_{label}"]]
|
|
||||||
merged = frame if merged is None else merged.merge(frame, on="date", how="inner")
|
|
||||||
|
|
||||||
if merged is None or merged.empty:
|
|
||||||
raise ValueError("No overlapping dates across curves")
|
|
||||||
|
|
||||||
merged = merged.sort_values("date").copy()
|
|
||||||
merged["regime"] = merged["date"].map(regimes_by_date).fillna("unknown")
|
|
||||||
|
|
||||||
overlay_returns: list[float] = []
|
|
||||||
for row in merged.itertuples(index=False):
|
|
||||||
weights = allocations.get(row.regime, allocations["unknown"])
|
|
||||||
ret = 0.0
|
|
||||||
for label in labels:
|
|
||||||
ret += float(weights.get(label, 0.0)) * float(getattr(row, f"daily_return_{label}"))
|
|
||||||
overlay_returns.append(ret)
|
|
||||||
|
|
||||||
merged["overlay_return"] = overlay_returns
|
|
||||||
equity = initial_equity
|
|
||||||
overlay_equity: list[float] = []
|
|
||||||
for ret in overlay_returns:
|
|
||||||
equity *= 1.0 + float(ret)
|
|
||||||
overlay_equity.append(equity)
|
|
||||||
merged["overlay_equity"] = overlay_equity
|
|
||||||
return merged[["date", "regime", "overlay_return", "overlay_equity"]]
|
|
||||||
|
|
||||||
|
|
||||||
def summarize_overlay_curve(curve: pd.DataFrame, *, initial_equity: float) -> dict[str, Any]:
|
|
||||||
"""Return total return, drawdown, Sharpe, and regime counts for an overlay."""
|
|
||||||
if curve.empty:
|
|
||||||
raise ValueError("curve must not be empty")
|
|
||||||
|
|
||||||
final_equity = float(curve["overlay_equity"].iloc[-1])
|
|
||||||
total_return_pct = (final_equity / float(initial_equity) - 1.0) * 100.0
|
|
||||||
|
|
||||||
peak = float(initial_equity)
|
|
||||||
max_drawdown_pct = 0.0
|
|
||||||
for equity in curve["overlay_equity"]:
|
|
||||||
peak = max(peak, float(equity))
|
|
||||||
drawdown_pct = (peak - float(equity)) / peak * 100.0 if peak > 0 else 0.0
|
|
||||||
max_drawdown_pct = max(max_drawdown_pct, drawdown_pct)
|
|
||||||
|
|
||||||
rets = curve["overlay_return"].astype(float)
|
|
||||||
if len(rets) >= 2 and float(rets.std()) > 0:
|
|
||||||
sharpe = float(rets.mean() / rets.std() * math.sqrt(252.0))
|
|
||||||
else:
|
|
||||||
sharpe = 0.0
|
|
||||||
|
|
||||||
regime_counts = {
|
|
||||||
str(regime): int(count)
|
|
||||||
for regime, count in curve["regime"].value_counts().sort_index().items()
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"return_pct": total_return_pct,
|
|
||||||
"max_dd_pct": max_drawdown_pct,
|
|
||||||
"sharpe": sharpe,
|
|
||||||
"final_equity": final_equity,
|
|
||||||
"day_count": int(len(curve)),
|
|
||||||
"regime_day_counts": regime_counts,
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime as dt
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from libs.backtest.overlay import build_overlay_curve, summarize_overlay_curve, validate_allocations
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_allocations_requires_unknown_and_unit_sum() -> None:
|
|
||||||
try:
|
|
||||||
validate_allocations({"risk_on": {"core": 1.0}}, {"core"})
|
|
||||||
except ValueError as exc:
|
|
||||||
assert "unknown" in str(exc)
|
|
||||||
else:
|
|
||||||
raise AssertionError("expected ValueError")
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_allocations({"unknown": {"core": 0.8}}, {"core"})
|
|
||||||
except ValueError as exc:
|
|
||||||
assert "sum to 1.0" in str(exc)
|
|
||||||
else:
|
|
||||||
raise AssertionError("expected ValueError")
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_overlay_curve_switches_by_regime() -> None:
|
|
||||||
dates = [dt.date(2024, 1, 2), dt.date(2024, 1, 3), dt.date(2024, 1, 4)]
|
|
||||||
core = pd.DataFrame(
|
|
||||||
{
|
|
||||||
"date": dates,
|
|
||||||
"equity": [100.0, 110.0, 104.5],
|
|
||||||
"daily_return": [0.0, 0.10, -0.05],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
mom = pd.DataFrame(
|
|
||||||
{
|
|
||||||
"date": dates,
|
|
||||||
"equity": [100.0, 105.0, 110.25],
|
|
||||||
"daily_return": [0.0, 0.05, 0.05],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
regimes = {
|
|
||||||
dates[0]: "risk_on",
|
|
||||||
dates[1]: "risk_off",
|
|
||||||
dates[2]: "risk_on",
|
|
||||||
}
|
|
||||||
allocations = {
|
|
||||||
"risk_on": {"core": 0.0, "mom": 1.0},
|
|
||||||
"risk_off": {"core": 1.0, "mom": 0.0},
|
|
||||||
"unknown": {"core": 1.0, "mom": 0.0},
|
|
||||||
}
|
|
||||||
|
|
||||||
curve = build_overlay_curve(
|
|
||||||
curves={"core": core, "mom": mom},
|
|
||||||
allocations=allocations,
|
|
||||||
regimes_by_date=regimes,
|
|
||||||
initial_equity=100.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert curve["overlay_return"].round(6).tolist() == [0.0, 0.10, 0.05]
|
|
||||||
assert round(float(curve["overlay_equity"].iloc[-1]), 4) == 115.5
|
|
||||||
|
|
||||||
|
|
||||||
def test_summarize_overlay_curve_reports_return_drawdown_and_regimes() -> None:
|
|
||||||
curve = pd.DataFrame(
|
|
||||||
{
|
|
||||||
"date": [dt.date(2024, 1, 2), dt.date(2024, 1, 3), dt.date(2024, 1, 4)],
|
|
||||||
"regime": ["risk_on", "risk_off", "risk_on"],
|
|
||||||
"overlay_return": [0.0, -0.10, 0.05],
|
|
||||||
"overlay_equity": [100.0, 90.0, 94.5],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
summary = summarize_overlay_curve(curve, initial_equity=100.0)
|
|
||||||
assert round(summary["return_pct"], 4) == -5.5
|
|
||||||
assert round(summary["max_dd_pct"], 4) == 10.0
|
|
||||||
assert summary["regime_day_counts"] == {"risk_off": 1, "risk_on": 2}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime as dt
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from apps.paper_trader.backtest_sim import run_overlay_backtest_sync
|
|
||||||
|
|
||||||
|
|
||||||
def _write_equity_csv(path: Path, rows: list[tuple[str, float]]) -> None:
|
|
||||||
df = pd.DataFrame(rows, columns=["date", "equity"])
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
df.to_csv(path, index=False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_overlay_backtest_sync_replays_frozen_equity_csv(tmp_path: Path) -> None:
|
|
||||||
core_csv = tmp_path / "core.csv"
|
|
||||||
mom_csv = tmp_path / "mom.csv"
|
|
||||||
_write_equity_csv(core_csv, [("2025-01-02", 100.0), ("2025-01-03", 110.0), ("2025-01-06", 121.0)])
|
|
||||||
_write_equity_csv(mom_csv, [("2025-01-02", 100.0), ("2025-01-03", 90.0), ("2025-01-06", 81.0)])
|
|
||||||
|
|
||||||
spec = {
|
|
||||||
"overlay_name": "overlay_demo",
|
|
||||||
"books": [
|
|
||||||
{"label": "core", "equity_csv": str(core_csv)},
|
|
||||||
{"label": "mom", "equity_csv": str(mom_csv)},
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"unknown": {"core": 0.5, "mom": 0.5},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
spec_path = tmp_path / "overlay.json"
|
|
||||||
spec_path.write_text(json.dumps(spec))
|
|
||||||
|
|
||||||
result = run_overlay_backtest_sync(
|
|
||||||
overlay_config_path=str(spec_path),
|
|
||||||
capital=100.0,
|
|
||||||
start_date=dt.date(2025, 1, 2),
|
|
||||||
end_date=dt.date(2025, 1, 6),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["is_overlay"] is True
|
|
||||||
assert result["overlay_replay_mode"] == "frozen_equity_csv"
|
|
||||||
assert round(result["summary"]["return_pct"], 4) == 0.0
|
|
||||||
assert result["summary"]["trade_count"] == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_overlay_backtest_sync_rebases_window_from_frozen_curves(tmp_path: Path) -> None:
|
|
||||||
core_csv = tmp_path / "core.csv"
|
|
||||||
_write_equity_csv(core_csv, [("2025-01-02", 100.0), ("2025-01-03", 200.0), ("2025-01-06", 400.0)])
|
|
||||||
|
|
||||||
spec = {
|
|
||||||
"overlay_name": "overlay_demo",
|
|
||||||
"books": [
|
|
||||||
{"label": "core", "equity_csv": str(core_csv)},
|
|
||||||
],
|
|
||||||
"allocations": {
|
|
||||||
"unknown": {"core": 1.0},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
spec_path = tmp_path / "overlay.json"
|
|
||||||
spec_path.write_text(json.dumps(spec))
|
|
||||||
|
|
||||||
result = run_overlay_backtest_sync(
|
|
||||||
overlay_config_path=str(spec_path),
|
|
||||||
capital=100.0,
|
|
||||||
start_date=dt.date(2025, 1, 3),
|
|
||||||
end_date=dt.date(2025, 1, 6),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["overlay_replay_mode"] == "frozen_equity_csv"
|
|
||||||
assert round(result["summary"]["return_pct"], 4) == 100.0
|
|
||||||
assert round(result["summary"]["final_equity"], 4) == 200.0
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime as dt
|
|
||||||
|
|
||||||
from apps.tools import evaluate_book_overlay as mod
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeStore:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._dates = [dt.date(2025, 1, 2), dt.date(2025, 1, 3)]
|
|
||||||
|
|
||||||
def slice_by_date_range(self, start: dt.date, end: dt.date):
|
|
||||||
self._dates = [d for d in self._dates if start <= d <= end]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def all_trading_days(self):
|
|
||||||
return list(self._dates)
|
|
||||||
|
|
||||||
def get_macro_for_date(self, date: dt.date):
|
|
||||||
return {"state": f"regime-{date.isoformat()}"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_compute_regimes_uses_merged_store(monkeypatch) -> None:
|
|
||||||
fake_store = _FakeStore()
|
|
||||||
|
|
||||||
monkeypatch.setattr(mod, "load_manifest", lambda path: object())
|
|
||||||
monkeypatch.setattr(mod, "resolve_config", lambda manifest, config_root=".": object())
|
|
||||||
monkeypatch.setattr(mod, "_build_merged_snapshot_store", lambda manifest, config, snapshot_dir_override=None: fake_store)
|
|
||||||
monkeypatch.setattr(mod, "_macro_regime_state", lambda config, macro: macro["state"])
|
|
||||||
|
|
||||||
regimes = mod._compute_regimes(
|
|
||||||
snapshot_dir="data/parquet/example_snapshot",
|
|
||||||
split="train",
|
|
||||||
config_path="configs/experiments/example.json",
|
|
||||||
start_date=dt.date(2025, 1, 2),
|
|
||||||
end_date=dt.date(2025, 1, 3),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert regimes == {
|
|
||||||
dt.date(2025, 1, 2): "regime-2025-01-02",
|
|
||||||
dt.date(2025, 1, 3): "regime-2025-01-03",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_compute_regimes_prefers_explicit_snapshot_dir(monkeypatch, tmp_path) -> None:
|
|
||||||
snapshot_dir = tmp_path / "snap"
|
|
||||||
snapshot_dir.mkdir()
|
|
||||||
(snapshot_dir / "train.parquet").write_text("stub")
|
|
||||||
|
|
||||||
fake_store = _FakeStore()
|
|
||||||
|
|
||||||
monkeypatch.setattr(mod, "load_manifest", lambda path: object())
|
|
||||||
monkeypatch.setattr(mod, "resolve_config", lambda manifest, config_root=".": object())
|
|
||||||
monkeypatch.setattr(mod, "load_merged_store_from_snapshot_dir", lambda snapshot_dir, oracle_url, db_dsn: fake_store)
|
|
||||||
monkeypatch.setattr(mod, "_build_merged_snapshot_store", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not be called")))
|
|
||||||
monkeypatch.setattr(mod, "_macro_regime_state", lambda config, macro: macro["state"])
|
|
||||||
|
|
||||||
regimes = mod._compute_regimes(
|
|
||||||
snapshot_dir=snapshot_dir,
|
|
||||||
split="train",
|
|
||||||
config_path="configs/experiments/example.json",
|
|
||||||
start_date=dt.date(2025, 1, 2),
|
|
||||||
end_date=dt.date(2025, 1, 3),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert regimes == {
|
|
||||||
dt.date(2025, 1, 2): "regime-2025-01-02",
|
|
||||||
dt.date(2025, 1, 3): "regime-2025-01-03",
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue