You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
126 lines
4.1 KiB
Python
126 lines
4.1 KiB
Python
#!/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()
|