fix: unify backtest execution and add forward validation candidates

Share point-in-time signals and a cash-funded order ledger across backtests and trading. Correct eligibility, missing-price fills, quality factors, regime inputs, warmup windows, and cash-injection accounting. Add reproducible registered-strategy comparisons and paired forward candidates with a 20% resize band.

Validation: 125 Python tests, 23 web tests, TypeScript/Vite build, 87 strategy causality checks, and real-data target/order/equity parity for 10 strategies.
This commit is contained in:
2026-10-01 02:24:01 -07:00
parent 761725d2c8
commit 722cade974
45 changed files with 1561 additions and 1021 deletions

View File

@@ -27,7 +27,7 @@ from trader import (
LIVE_EXCLUDED_STRATEGIES,
MARKET_FEE_TIERED,
MONITOR_CAPITAL,
STRATEGY_REGISTRY,
STRATEGY_REGISTRY, EXECUTION_VARIANTS,
book_dir,
book_strategies,
convert_quote_currency,
@@ -482,6 +482,11 @@ STRATEGY_META = {
}
for _name, (_base, _market) in EXECUTION_VARIANTS.items():
STRATEGY_META[_name] = {**STRATEGY_META[_base], "markets": [_market],
"label": STRATEGY_META[_base]["label"] + " · 20%再平衡带(前瞻候选)"}
def _log(msg: str) -> None:
print(msg, file=sys.stderr)
@@ -740,7 +745,8 @@ def cmd_backtest(args: argparse.Namespace) -> None:
raw = run_market(market, known, years, capital, tx_cost, fixed_fee,
**MARKET_FEE_TIERED.get(market, {}))
results = [
_build_strategy_result(run["name"], run["equity"], run["trades"], injection_schedule, capital)
_build_strategy_result(run["name"], run["equity"], run["trades"], injection_schedule, capital,
execution_plan=run.get("_plan"), fee_params=run.get("_fees"))
for run in (raw["strategy_runs"] if raw else [])
]
benchmark_label = raw["benchmark_label"] if raw else "SPY"
@@ -749,13 +755,18 @@ def cmd_backtest(args: argparse.Namespace) -> None:
output = {
"meta": {
"market": market,
"years": years,
"years": raw.get("actual_years", years) if raw else years,
"requestedYears": years,
"initialCapital": capital,
"startDate": results[0]["equityCurve"][0]["date"] if results else None,
"endDate": results[0]["equityCurve"][-1]["date"] if results else None,
"txCost": tx_cost,
"fixedFee": fixed_fee,
"injectionSchedule": injection_schedule,
"engineVersion": raw.get("engine_version") if raw else None,
"warnings": raw.get("warnings", []) if raw else [],
"coverage": raw.get("coverage") if raw else None,
"errors": raw.get("errors", []) if raw else [],
},
"strategies": results,
"benchmark": {
@@ -773,21 +784,29 @@ def _build_strategy_result(
trade_events: list[dict],
injection_schedule: dict | None,
initial_capital: float,
*, execution_plan: dict | None = None, fee_params: dict | None = None,
) -> dict:
"""Build a strategy result dict with optional injection applied.
Return and risk metrics, drawdowns and periodic returns are time-weighted
(computed on the curve without injections), so injected cash never counts
as performance. ``capitalFlows`` reports the money view: total invested,
(computed from cash-flow-adjusted NAV), so injected cash never counts
as performance. The orders and fees are replayed at the actual capital. ``capitalFlows`` reports the money view: total invested,
final value and profit of the injected curve.
"""
injection_events = []
equity_with_inj = None
metric_equity = equity
if injection_schedule:
returns = equity.pct_change().fillna(0.0)
contributions, injection_events = build_injection_schedule(equity, injection_schedule)
equity_with_inj = equity_with_cashflows(returns, contributions, initial_capital)
if execution_plan is None or fee_params is None:
raise ValueError("Cash injections require an order replay; scaling a return curve is not supported")
from backtest_runner import replay_prepared
days = execution_plan["prices"].index
contributions, injection_events = build_injection_schedule(equity.reindex(days), injection_schedule)
replay = replay_prepared(execution_plan, initial_capital, contributions=contributions, **fee_params)
equity_with_inj = replay["equity"]
metric_equity = replay["unit_equity"]
trade_events = replay["trades"]
# Curve shown to the user: the money curve when cash is injected.
final_equity = equity_with_inj if equity_with_inj is not None else equity
@@ -804,14 +823,10 @@ def _build_strategy_result(
base_curve = [{"date": d.strftime("%Y-%m-%d"), "value": round(float(v), 2)}
for d, v in equity.items()]
# Time-weighted metrics on the curve without injections
strat_metrics = metrics.raw_summary(equity)
strat_metrics["rebalanceWinRate"] = compute_rebalance_win_rate(
equity, trade_events,
)
drawdowns = compute_drawdown_periods(equity)
monthly_ret, yearly_ret = compute_periodic_returns(equity)
strat_metrics = metrics.raw_summary(metric_equity)
strat_metrics["rebalanceWinRate"] = compute_rebalance_win_rate(metric_equity, trade_events)
drawdowns = compute_drawdown_periods(metric_equity)
monthly_ret, yearly_ret = compute_periodic_returns(metric_equity)
meta = STRATEGY_META.get(name, {"label": name, "category": "other"})
@@ -1060,7 +1075,9 @@ def cmd_books() -> None:
"strategies": {m: book_strategies(None, m) for m in ("us", "cn")}}]
for book_id, cfg in LIVE_BOOKS.items():
books.append({"id": book_id, "label": cfg["label"], "startDate": cfg["start"],
"capital": cfg["capital"], "feePerOrder": cfg["fee_per_order"],
"capital": cfg["capital"],
"feePerOrder": None if "fee_schedule" in cfg else cfg["fee_per_order"],
"feeSchedule": cfg.get("fee_schedule"),
"strategies": {m: book_strategies(book_id, m) for m in ("us", "cn")}})
json.dump(books, sys.stdout)