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

@@ -261,9 +261,12 @@ export function LivePage() {
function emptyHint(book: LiveBook | undefined, market: StrategyMarket): string {
if (!book?.startDate) return "暂无在线策略。运行 quant 的 trader.py auto 后再刷新。";
const currency = currencyForMarket(market);
const fee = book.feePerOrder ? ` · 每笔 ${formatMoney(book.feePerOrder[market], currency)}` : "";
const schedule = book.feeSchedule?.[market];
const fee = schedule
? ` · 佣金最低 ${formatMoney(schedule.fixed_fee, currency)},比例成本 ${(schedule.tx_cost * 100).toFixed(2)}%`
: book.feePerOrder ? ` · 每单佣金 ${formatMoney(book.feePerOrder[market], currency)}` : "";
return `${book.label}:${book.strategies[market].length} 个策略,各 ${formatMoney(book.capital[market], currency)}${fee},`
+ `${book.startDate[market]} 收盘后出现首日记录。`;
+ `${book.startDate[market]} 起首个交易日收盘后出现记录。`;
}
function LiveStrategyDetail({ strategy }: { strategy: LiveStrategyState }) {

View File

@@ -594,6 +594,15 @@ function EquityCurveChart({
)}
</div>
</div>
{result.meta.warnings?.length ? (
<details className="backtestAssumptions">
<summary>回测数据与成交假设</summary>
<ul>{result.meta.warnings.map((message) => <li key={message}>{message}</li>)}</ul>
</details>
) : null}
{result.meta.errors?.length ? (
<p role="alert">未完成回测:{result.meta.errors.map((e) => `${e.strategy}: ${e.error}`).join(";")}</p>
) : null}
<div className="legendRow">
{result.strategies.length === 0 && (
<span className="legendEmpty">从左侧选取策略加入对比</span>

View File

@@ -17,7 +17,7 @@ function ensureCacheDir(): void {
}
function configHash(config: BacktestConfig): string {
const serialized = stableStringify(config);
const serialized = stableStringify({ engine: "cash-ledger-pit-v2", config });
return createHash("md5").update(serialized).digest("hex").slice(0, 12);
}

View File

@@ -77,7 +77,7 @@ export interface InjectionEvent {
amount: number;
}
// Backtest metrics are time-weighted (computed on the no-injection curve).
// Backtest metrics use unit NAV after executing contributions and their fees.
// The bridge serializes NaN as null, so every field may be null.
export interface StrategyMetrics {
totalReturn: number | null;
@@ -107,7 +107,7 @@ export interface StrategyResult {
label: string;
// Money curve (includes injections when a schedule exists).
equityCurve: EquityPoint[];
// Same strategy without injections; metrics are computed on this curve.
// Same strategy without injections, supplied for comparison.
baseEquityCurve: EquityPoint[] | null;
metrics: StrategyMetrics;
capitalFlows?: CapitalFlows;
@@ -121,6 +121,11 @@ export interface StrategyResult {
}
export interface BacktestMeta {
requestedYears?: number;
engineVersion?: string;
warnings?: string[];
coverage?: { minimum: number; median: number } | null;
errors?: { strategy: string; error: string }[];
market: StrategyMarket;
years: number;
initialCapital: number;
@@ -163,6 +168,9 @@ export interface TradeLogEntry {
// A live book: a cohort of strategies, each with its own account, started on
// one date with one capital and fee schedule. id "" is the original book.
export interface LiveBook {
feeSchedule?: Partial<Record<StrategyMarket, {
tx_cost: number; fixed_fee: number; fee_base: number; fee_per_share: number;
}>> | null;
id: string;
label: string;
startDate: Record<StrategyMarket, string> | null;