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:
51
bridge.py
51
bridge.py
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user