diff --git a/.gitignore b/.gitignore index 0ef8b1b..08ec744 100644 --- a/.gitignore +++ b/.gitignore @@ -64,12 +64,15 @@ checkpoints/ training_history*.json best_mt5_strategy.json -# ── 策略输出(训练/回测生成,非源码)──────────────────────────────── -strategies/best_*.json -strategies/strategy_*.json +# ── 策略输出 ────────────────────────────────────────────────────── +# 策略因子文件保留跟踪(研究成果) +# strategies/best_*.json +# strategies/strategy_*.json strategies/aligned_symbols*.json strategies/aligned_symbols*.txt strategies/*_progress.txt +strategies/best_forex_smoke.json +strategies/best_group_forex_smoke.json # ── 回测输出(图表与报告)────────────────────────────────────────── backtest_output/ @@ -92,3 +95,5 @@ data/cache/ # ── 临时脚本/分析输出 ────────────────────────────────────────────── _tmp_*.py +backtest_current_forex.py +_quick_bt.py diff --git a/backtest_audit_20250706_0010.md b/backtest_audit_20250706_0010.md new file mode 100644 index 0000000..8600879 --- /dev/null +++ b/backtest_audit_20250706_0010.md @@ -0,0 +1,136 @@ +# 回测前视偏差审计报告 + +**时间**: 2026-07-06 00:10 +**审计范围**: backtest_all_groups.py, backtest_current.py, backtest_detailed.py, backtest_elite.py, backtest_index_best.py, backtest_current_forex.py +**结论**: 主力回测脚本无前视偏差;backtest_current_forex.py 有 target_ret 构造错误 + +--- + +## 1. Target Return 构造 + +### 训练引擎 (engine.py) +- 来源: `data_manager.target_ret` +- 公式: `target_ret[n, t] = log(open[t+2] / open[t+1])` +- 含义: t 时刻决策 → t+1 开仓 → t+2 平仓的收益 +- **正确,无前视** + +### 主力回测脚本 (backtest_all_groups.py, backtest_current.py, backtest_detailed.py, backtest_elite.py, backtest_index_best.py) +- 来源: `group_mgr.target_ret` / `mgr.target_ret` +- 与训练引擎一致 +- **正确,无前视** + +### 临时脚本 (backtest_current_forex.py) ❌ +- 来源: 手动构造 `target_ret[:, 1:] = (close[:, 1:] / close[:, :-1] - 1)` +- 公式: `target_ret[t] = close[t]/close[t-1] - 1` (t-1→t 的收益放在 t 位置) +- 问题: `pnl[t] = pos[t] * (close[t]/close[t-1] - 1)` 相当于用 t 时刻信息决定 t-1 的仓位 +- **有前视偏差**,但此脚本仅为临时检查用途,不影响正式回测结论 + +--- + +## 2. PnL 计算时序 + +### 训练引擎 evaluate_fold +```python +position = compute_target_positions_stateless(factors) # pos[t] = tanh(factor[t]) +pnl = position * target_ret - turnover * cost_rate +# pnl[t] = tanh(factor[t]) * log(open[t+2]/open[t+1]) - |pos[t]-pos[t-1]| * cost +``` + +时序: +1. t 时刻: close[t] 完成 (H1 K线 [t, t+1) 收盘) → 计算 factor[t] → 决定 pos[t] +2. t+1 时刻: 以 open[t+1] 建仓 (换手成本在此刻发生) +3. t+2 时刻: 以 open[t+2] 平仓 +4. 收益 = log(open[t+2]/open[t+1]) + +**因果正确**。pos[t] 基于 close[t](t+1 时刻可知),收益从 t+1 开始。 + +### backtest_all_groups.py +```python +pos = compute_target_positions_stateless(factor) +pnl = pos_np * target_np - turnover * cost_rate +``` +与训练引擎一致。**正确**。 + +--- + +## 3. 特征引擎前视检查 + +### _robust_norm (features.py L137) +- 因果滚动窗口: `[t-w+1..t]` 共 w 期 +- unfold + median/MAD +- **无前视** + +### SCALE 算子 (ops.py L237) +- `scale(x)[t] = x[t] / cumsum(|x[0..t]|)` (因果累积和) +- **无前视** + +### 特征中 close 的使用 +所有特征只用 `close[t]` 和历史值 (`close[:, :-1]` 等),无 `close[t+1]` 或未来索引。 +`close[t]` 在 H1 K 线中需 t+1 时刻才完成,但 target_ret 从 t+1 开始计收益,时序对齐正确。 + +### TS_RANK / TS_MIN / TS_MAX 等滚动算子 +均使用 `_ts_rolling(x, d)` = `x.unfold(1, d, 1)`,是因果窗口 [t-d+1..t]。 +**无前视**。 + +--- + +## 4. Walk-Forward 折叠 + +```python +folds = _build_walk_forward_folds(T, n_folds, gap=20) +``` +- 第 k 折: train[0, train_end) → gap → val[val_start, val_end) +- train_end ≤ val_start - gap,验证段严格在训练段之后 +- **无前视** + +--- + +## 5. 换手率与成本 + +```python +prev[:, 1:] = pos[:, :-1] # prev[t] = pos[t-1] +turnover = |pos - prev| # turnover[t] = |pos[t] - pos[t-1]| +``` +- t 时刻的仓位变化 (pos[t-1] → pos[t]) +- 成本在 t 时刻发生,收益从 t+1 开始 +- **因果正确** + +--- + +## 6. compute_target_positions_stateless (signal.py) + +```python +pos = torch.tanh(factors) +pos = torch.where(pos.abs() >= 0.05, pos, 0) # MIN_TRADE_EXPOSURE +``` +- 逐元素操作,无时序依赖 +- **无前视** + +--- + +## 7. IC 计算说明 + +backtest_all_groups.py 中: +```python +x = factor[n, :-1] # factor[t] +y = target_ret[n, 1:] # target_ret[t+1] = log(open[t+3]/open[t+2]) +``` +衡量 factor[t] 对 t+2→t+3 收益的预测力(2 步滞后 IC)。 +这不是前视,只是 IC 指标的定义方式,不影响 PnL 计算。 + +--- + +## 总结 + +| 检查项 | 状态 | +|--------|------| +| target_ret 构造 | ✅ 正确 (主力脚本) | +| PnL 时序对齐 | ✅ 正确 | +| 特征引擎因果性 | ✅ 正确 | +| _robust_norm 因果性 | ✅ 正确 | +| SCALE 算子因果性 | ✅ 正确 | +| Walk-Forward 折叠 | ✅ 正确 | +| 换手率计算 | ✅ 正确 | +| backtest_current_forex.py | ❌ target_ret 构造错误 (临时脚本,不影响正式结论) | + +**最终结论**: 主力回测脚本 (backtest_all_groups.py 等) 无前视偏差,回测结果可信。backtest_current_forex.py 有 target_ret 构造错误但不影响正式回测结论。 diff --git a/independent_verification_20250706_0933.md b/independent_verification_20250706_0933.md new file mode 100644 index 0000000..87cfc8e --- /dev/null +++ b/independent_verification_20250706_0933.md @@ -0,0 +1,73 @@ +# 独立回测验证报告 — 2026-07-06 09:33 + +## 目标 +从第一性原理出发,不复用训练框架 reward 函数,对所有已保存策略做独立回测验证。 + +## 方法 +- 脚本: `verify_all_strategies.py` +- target_ret = log(open[t+2]/open[t+1]),无前视偏差 +- 仓位 = tanh(factor),应用 MIN_TRADE_EXPOSURE=0.05 +- PnL = pos[t] * target_ret[t] - |pos[t]-pos[t-1]| * cost_rate +- cost_rate = 0.0001 (单边) +- 统计: 年化、Sharpe、Sortino、MDD、胜率、多空比、前后一致性、Walk-Forward 4折、成本压力测试(1x~5x) + +## 汇总结果 + +| 策略 | 年化% | Sharpe | MDD% | 多空比 | H1/H2同号 | WF正折 | 2x成本盈利 | 判定 | +|------|-------|--------|------|--------|-----------|--------|-----------|------| +| forex_v1 (旧) | 3.23 | 0.879 | 7.03 | 38/38 | 是 | 3/3 | 是 | **VALID** | +| forex_v2 (新) | 2.85 | 0.684 | 6.88 | 49/49 | 是 | 2/3 | 是 | **VALID** | +| index_v1 (旧) | -1.79 | -1.136 | 9.68 | 11/7 | 是 | 0/3 | 否 | **INVALID** | +| index_v2 (新) | -1.79 | -1.136 | 9.68 | 11/7 | 是 | 0/3 | 否 | **INVALID** | +| metals_comm_v1 (旧) | 125.63 | 3.420 | 22.18 | 49/49 | 是 | 3/3 | 是 | **INVALID** (MDD>20%) | +| metals_comm_v2 (新) | 151.58 | 4.072 | 13.62 | 55/42 | 是 | 3/3 | 是 | **SUSPICIOUS** (MDD>10%) | + +## 详细分析 + +### Forex 组 (8.01年, 49998 bars) +- **forex_v1** (AROON_OSC_25→TS_MIN_10→EMA_20→CS_SCALE→SIGN→CS_SCALE→EMA_5→MOMENTUM_5) + - 年化3.23%, Sharpe 0.88, MDD 7.03% — 通过FTMO + - 多空完美均衡(38/38), 前后一致(H1=2.78%, H2=3.67%), WF 3/3正 + - 2x成本仍盈利(1.45%), 3x转负(-0.33%) + - 品种: EURUSD +2.57%, USDJPY +3.88% + - **结论: 有效策略,但收益偏低** + +- **forex_v2** (DMI_DIFF_14→HL_RANGE→TS_MIN_20→SUB→TS_MIN_20→TS_MEAN_20→CS_SCALE→WMA) + - 年化2.85%, Sharpe 0.68, MDD 6.88% — 通过FTMO + - 多空完美均衡(49/49), 前后一致(H1=3.24%, H2=2.46%), WF 2/3正 + - 2x成本仍盈利(1.43%), 3x接近平手(0.02%) + - 品种: EURUSD几乎不赚钱(+0.35%), USDJPY贡献全部利润(+5.35%) + - **结论: 有效但脆弱,利润高度依赖USDJPY** + +### Index 组 (5.15年, 32134 bars) +- **index_v1 = index_v2** (同一公式, PPO→TS_RANK_10→TS_SUM_5→TS_RANK_10→TS_SUM_20→TS_MEAN_5→CLIP→SQRT) + - 年化-1.79%, Sharpe -1.14, 亏损策略 + - 81.8%时间空仓(平), 仅10.8%多/7.4%空 + - WF 0/3正, 所有成本场景均亏损 + - 品种: US30 -2.35%, US100 +3.99%, US500 -7.00% + - **结论: 完全无效,TS_RANK恒正问题导致因子退化** + +### Metals_Comm 组 (1.37年, 8546 bars) +- **metals_comm_v1** (CS_ZSCORE_RET20→EMA_20→TS_MAX_10→TANH_SQUASH→EMA_20→AMIHUD_ILLIQ→MAX→MOMENTUM_5) + - 年化125.63% — 异常高,主因仅1.37年数据+高波动品种 + - MDD 22.18% — 超过FTMO限制 + - XAUUSD亏损(-11.23%), 利润全部来自AAVUSD(+210%)和COCOA(+178%) + - **结论: 无效,数据量不足+MDD超标** + +- **metals_comm_v2** (SAR_DIST→TREND_STRENGTH_50→TS_RANK_10→TS_MAX_20→ROLL_KURT_20→DELTA_5→TS_DECAY_EXP_5→IF_GT) + - 年化151.58%, MDD 13.62% — 超FTMO + - 同样数据量不足 + - **结论: 可疑,不可实盘** + +## 关键发现 + +1. **Forex v1 是最佳策略**: 全维度通过验证,但年化仅3.23% +2. **Index 策略完全无效**: TS_RANK恒正导致因子退化,需重新训练 +3. **Metals_Comm 不可靠**: 1.37年数据不足以支撑统计显著性 +4. **成本敏感性**: Forex策略在3x成本(0.03%)下转负,实际交易需关注滑点和点差 +5. **品种集中风险**: forex_v2利润几乎全部来自USDJPY + +## 文件 +- 验证脚本: `verify_all_strategies.py` +- 详细结果: `verification_results.json` +- 策略文件: `strategies/` 和 `strategies/archive/` diff --git a/model_core/config.py b/model_core/config.py index 1b83b68..16d871f 100644 --- a/model_core/config.py +++ b/model_core/config.py @@ -68,6 +68,13 @@ class ModelConfig: ENTROPY_COLLAPSE_THRESH: float = 0.15 * math.log(FORMULA_VOCAB.size) ENTROPY_COLLAPSE_STEPS: int = 40 + # ── 熵下限惩罚(Fix 1: H→0 时熵项归零问题)────────────────────────── + # 当 H < ENTROPY_FLOOR_THRESH 时,加入固定惩罚 λ×(thresh-H)。 + # 这确保即使 mean_ent→0,loss 中仍有非零探索压力。 + ENTROPY_FLOOR: bool = True + ENTROPY_FLOOR_THRESH: float = 0.5 # 熵低于此值时触发固定惩罚 + ENTROPY_FLOOR_LAMBDA: float = 2.0 # 惩罚强度系数 + # ── Elite Replay ────────────────────────────────────────────────── ELITE_REPLAY_FRAC: float = 0.25 ELITE_POOL_SIZE: int = 60 # 30→60:大空间需要更大的精英记忆 @@ -89,6 +96,16 @@ class ModelConfig: NOISE_MAX: float = 0.60 NOISE_BOOST_FACTOR: float = 2.0 # noise += 0.2 * (stagnation / window) + # ── 重启多样性(Fix 2: best_snapshot 吸引子效应)───────────────────── + # 每 FULL_RESET_EVERY 次重启中,做 1 次完全随机初始化而非从 best_snapshot 恢复。 + FULL_RESET_EVERY: int = 3 # 每 3 次重启中第 3 次做 full reset + + # ── Reward baseline(Fix 3: 全负 batch 相对优选问题)────────────────── + # 用 EMA baseline 替代 batch mean 计算 advantage,避免全负 batch 的问题。 + REWARD_EMA_BASELINE: bool = True + REWARD_EMA_DECAY: float = 0.95 # EMA 衰减系数 + REWARD_EMA_WARMUP: int = 10 # 前 N 步用 batch mean(EMA 未稳定) + # ── 重启时部分重置参数:保留底层,扰动顶层 ─────────────────────────── PARTIAL_RESET: bool = True PARTIAL_RESET_LAYERS: tuple = ("ln_f", "mtp_head", "head_critic", "blocks", "token_emb") diff --git a/model_core/engine.py b/model_core/engine.py index 1dbbdc5..3c9c408 100644 --- a/model_core/engine.py +++ b/model_core/engine.py @@ -232,6 +232,10 @@ class AlphaEngine: self._best_update_step = 0 self._stagnation_steps = 0 + # Fix 3: EMA reward baseline + self._reward_ema: float | None = None + self._reward_ema_step: int = 0 + # ── IC computation ──────────────────────────────────────────────────────── @staticmethod @@ -667,8 +671,20 @@ class AlphaEngine: # ── Part D: REINFORCE gradient update ──────────────────── - rew_std = rewards.std().clamp(min=0.1) - adv = (rewards - rewards.mean()) / (rew_std + 1e-5) + # Fix 3: EMA baseline 替代 batch mean,避免全负 batch 的相对优选问题 + batch_mean = rewards.mean().item() + batch_std = rewards.std().clamp(min=0.1) + if ModelConfig.REWARD_EMA_BASELINE and self._reward_ema_step >= ModelConfig.REWARD_EMA_WARMUP: + baseline = self._reward_ema + adv = (rewards - baseline) / (batch_std + 1e-5) + else: + adv = (rewards - batch_mean) / (batch_std + 1e-5) + # 更新 EMA + if self._reward_ema is None: + self._reward_ema = batch_mean + else: + self._reward_ema = ModelConfig.REWARD_EMA_DECAY * self._reward_ema + (1.0 - ModelConfig.REWARD_EMA_DECAY) * batch_mean + self._reward_ema_step += 1 adv_new = adv[:n_new] adv_elite = adv[n_new:] @@ -697,7 +713,14 @@ class AlphaEngine: ent_coeff = ModelConfig.ENTROPY_COEFF_MAX / ( (1.0 + ent_val) ** ModelConfig.ENTROPY_COEFF_POWER ) - loss = policy_loss - ent_coeff * mean_ent + # Fix 1: 熵下限惩罚——当 H < threshold 时加入固定惩罚,确保探索压力不归零 + ent_floor_loss = torch.zeros(1, device=ModelConfig.DEVICE) + if ModelConfig.ENTROPY_FLOOR and ent_val < ModelConfig.ENTROPY_FLOOR_THRESH: + floor_gap = ModelConfig.ENTROPY_FLOOR_THRESH - ent_val + ent_floor_loss = ModelConfig.ENTROPY_FLOOR_LAMBDA * torch.tensor( + floor_gap, device=ModelConfig.DEVICE, dtype=mean_ent.dtype + ) + loss = policy_loss - ent_coeff * mean_ent + ent_floor_loss self.opt.zero_grad() loss.backward() @@ -813,7 +836,22 @@ class AlphaEngine: if self._restart_count < max_r: self._restart_count += 1 low_entropy_streak = 0 - if self._best_snapshot is not None: + + # Fix 2: 每 N 次重启做一次完全随机初始化,逃离 best_snapshot 吸引子 + do_full_reset = (self._restart_count % ModelConfig.FULL_RESET_EVERY == 0) + + if do_full_reset: + # 完全重新初始化模型参数 + for layer in self.model.modules(): + if hasattr(layer, 'reset_parameters'): + layer.reset_parameters() + tqdm.write( + f"[Restart {self._restart_count}/{max_r} @ step {step}] " + f"mode=FULL_RESET (escaped best_snapshot attractor) " + f"stagnation={self._stagnation_steps} " + f"H={ent_val:.3f}" + ) + elif self._best_snapshot is not None: self.model.load_state_dict(self._best_snapshot) with torch.no_grad(): if ModelConfig.PARTIAL_RESET: diff --git a/strategies/README.md b/strategies/README.md index a181839..6743fda 100644 --- a/strategies/README.md +++ b/strategies/README.md @@ -1,46 +1,121 @@ -# AlphaGPT 策略库 +# MT5 AlphaGPT — 策略因子库 -训练出的有效因子公式,按版本存档。每个文件包含:公式 tokens、回测指标、训练配置、分析结论。 +> 生成时间: 2026-07-06 09:38 +> 训练框架: AlphaGPT (REINFORCE + Transformer + 公式 DSL) +> 回测周期: H1 时间帧,独立验证脚本 `verify_all_strategies.py` + +## 策略一览 + +| 策略名 | 品种组 | 品种 | 数据长度 | 年化收益 | Sharpe | MDD | 多空比 | 判定 | +|--------|--------|------|----------|----------|--------|-----|--------|------| +| forex_v1 | forex | EURUSD, USDJPY | 8.01年 (49998 bars) | +3.23% | 0.879 | 7.03% | 38/38 | ✅ VALID | +| forex_v2 | forex | EURUSD, USDJPY | 8.01年 (49998 bars) | +2.85% | 0.684 | 6.88% | 49/49 | ✅ VALID | +| index_v1 | index | US30.cash, US100.cash, US500.cash | 5.15年 (32134 bars) | -1.79% | -1.136 | 9.68% | 11/7 | ❌ INVALID | +| index_v2 | index | US30.cash, US100.cash, US500.cash | 5.15年 (32134 bars) | -1.79% | -1.136 | 9.68% | 11/7 | ❌ INVALID | +| metals_comm_v1 | metals_comm | XAUUSD, AAVUSD, COCOA.c | 1.37年 (8546 bars) | +125.63% | 3.420 | 22.18% | 49/49 | ❌ INVALID | +| metals_comm_v2 | metals_comm | XAUUSD, AAVUSD, COCOA.c | 1.37年 (8546 bars) | +151.58% | 4.072 | 13.62% | 55/42 | ⚠️ SUSPICIOUS | + +> index_v1 和 index_v2 公式相同(v2 是 v1 的副本) + +## 公式详情 + +### forex_v1 +- **Token IDs**: [20, 53, 57, 73, 34, 73, 56, 50] +- **Decoded**: AROON_OSC_25 → TS_MIN_10 → EMA_20 → CS_SCALE → SIGN → CS_SCALE → EMA_5 → MOMENTUM_5 +- **训练 best_score**: 0.4851 +- **公式逻辑**: Aroon 振荡器的 10 周期最小值经 EMA 平滑后横截面标准化,取符号再标准化,最后用 EMA+动量平滑 + +### forex_v2 +- **Token IDs**: [21, 3, 60, 29, 60, 42, 73, 54] +- **Decoded**: DMI_DIFF_14 → HL_RANGE → TS_MIN_20 → SUB → TS_MIN_20 → TS_MEAN_20 → CS_SCALE → WMA +- **训练 best_score**: 0.6354 +- **公式逻辑**: DMI 差值减去高低价差的 20 周期最小值,再减去另一个 TS_MIN_20,经 TS_MEAN_20 平滑后横截面标准化,最后 WMA 加权 + +### index_v1 (= index_v2) +- **Token IDs**: [13, 47, 75, 47, 77, 40, 86, 82] +- **Decoded**: PPO → TS_RANK_10 → TS_SUM_5 → TS_RANK_10 → TS_SUM_20 → TS_MEAN_5 → CLIP → SQRT +- **训练 best_score**: 0.4464 +- **问题**: TS_RANK 输出 [0,1) 恒正,导致整个公式链恒正,因子退化为 beta 因子 + +### metals_comm_v1 +- **Token IDs**: [27, 57, 52, 88, 57, 17, 79, 50] +- **Decoded**: CS_ZSCORE_RET20 → EMA_20 → TS_MAX_10 → TANH_SQUASH → EMA_20 → AMIHUD_ILLIQ → MAX → MOMENTUM_5 +- **训练 best_score**: 2.3198 +- **注意**: 仅 1.37 年数据,MDD 22.18%,XAUUSD 亏损 -11.23% + +### metals_comm_v2 +- **Token IDs**: [25, 11, 47, 61, 26, 71, 70, 89] +- **Decoded**: SAR_DIST → TREND_STRENGTH_50 → TS_RANK_10 → TS_MAX_20 → ROLL_KURT_20 → DELTA_5 → TS_DECAY_EXP_5 → IF_GT +- **训练 best_score**: 2.6153 +- **注意**: 仅 1.37 年数据,MDD 13.62% + +## 回测方法 + +- **target_ret**: log(open[t+2] / open[t+1]) — 无前视偏差 +- **仓位**: pos = tanh(factor),|pos| < 0.05 时归零 +- **PnL**: pos[t] × target_ret[t] - |pos[t] - pos[t-1]| × cost_rate +- **成本**: 单边 0.01% (1bp) +- **年化**: mean(daily_pnl) × 6240 (H1 bars/year) +- **Walk-Forward**: 4 折时间序列切分,每折独立计算年化/Sharpe/MDD +- **成本压力**: 1x/2x/3x/5x 成本倍数 + +## 文件结构 + +``` +strategies/ +├── README.md # 本文件 +├── STATUS_20250705.md # 训练状态记录 +├── best_forex.json # forex_v2 策略 +├── best_index.json # index_v2 策略 +├── best_metals_comm.json # metals_comm_v2 策略 +├── best_group_forex.json # forex 组策略 +├── best_EURUSD.json # EURUSD 单品种 +├── best_USDJPY.json # USDJPY 单品种 +├── archive/ +│ ├── best_forex_20250705_pre_refactor.json # forex_v1 策略 +│ ├── best_index_20250705_pre_refactor.json # index_v1 策略 +│ └── best_metals_comm_20250705_pre_refactor.json # metals_comm_v1 策略 +└── verification_results.json # 独立回测验证详细结果 +``` ## 使用方式 ```python import json -with open("strategies/strategy_v1_20260702.json") as f: - s = json.load(f) -tokens = s["formula"]["tokens"] +from model_core.vm import StackVM +from model_core.vocab import FORMULA_VOCAB +from data_pipeline.fetcher import MT5DataFetcher +from data_pipeline.data_manager import MT5DataManager +from config import Config +from strategy_manager.signal import compute_target_positions_stateless + +# 加载策略 +with open("strategies/best_forex.json") as f: + strat = json.load(f) +formula = strat["formula"] + +# 解码公式 +decoded = [FORMULA_VOCAB.token_names[t] for t in formula] +print(" → ".join(decoded)) + +# 加载数据 +Config.SYMBOLS = ["EURUSD", "USDJPY"] +with MT5DataFetcher(offline=True) as fetcher: + mgr = MT5DataManager(fetcher) + mgr.load() + + # 执行公式 + vm = StackVM() + factor = vm.execute(formula, mgr.feat_tensor) + + # 计算仓位 + positions = compute_target_positions_stateless(factor) + # positions: [N, T] in [-1, +1] ``` -注意:`vocab_version` 必须与当前 `model_core/vocab.py` 中的 `VOCAB_VERSION` 一致,否则 token 语义不同。 +## 免责声明 ---- - -## 策略列表 - -| ID | 日期 | 核心因子 | 品种 | PnL | Sharpe | Sortino | 状态 | -|----|------|--------|------|-----|--------|---------|------| -| [v1](strategy_v1_20260702.json) | 2026-07-02 | `TS_RANK_10(MA_DIFF)` 均线差排名 | 全品种组合 | +11.4% | +0.65 | +0.93 | ✅ 存档(跨账号验证有效)| -| [xauusd_v1](strategy_xauusd_v1_20260703.json) | 2026-07-03 | v1公式黄金专项存档 | XAUUSDm/XAUUSD | +22.4% | +0.51 | +0.72 | ✅ 存档 | -| ~~us500_v2~~ | ~~2026-07-03~~ | ~~ATR斜率/量价~~ | ~~US500m~~ | ~~+38%~~ | — | — | ❌ 已删除(过拟合,新账号失效)| - ---- - -## 版本说明 - -### v1 — MA_RANK_VOL_TREND(2026-07-02) - -**有效核心**:`TS_RANK_10(MA_DIFF)` -- MA_DIFF = MA10/MA30 - 1(短长均线差) -- TS_RANK_10 = 10期历史排名(值域 [0,1)) -- 公式完整形式带 GATE/RVOL/SLOPE20 的冗余结构,实际只有核心部分在工作 - -**适用品种**:风险资产(指数 > 黄金 > 外汇) - -**不适用**:EUR/USD、USD/JPY 趋势信号噪声过大 - -**训练条件** -- 阶段 A,8-token 公式,300步(实际跑 620步,冠军在 step 125 出现) -- 5 品种,H1 周期,约 1.8 年历史数据 -- vocab v2.0(20个特征) - -**下一步**:切换阶段B(14-token,500步)搜索更复杂结构 +- metals_comm 组仅 1.37 年数据,统计显著性不足 +- index 组策略已确认无效(TS_RANK 恒正问题),保留仅供分析 +- 实盘交易需考虑滑点、点差、流动性等额外成本 +- 过去表现不代表未来收益 diff --git a/strategies/best_EURUSD.json b/strategies/best_EURUSD.json new file mode 100644 index 0000000..8cd9dbd --- /dev/null +++ b/strategies/best_EURUSD.json @@ -0,0 +1,17 @@ +{ + "vocab_version": "v71336c7c7b25", + "symbol": "EURUSD", + "group": "forex_smoke", + "formula": [ + 26, + 23, + 21, + 51, + 40, + 75, + 47, + 89 + ], + "best_score": -0.030277235433459282, + "source": "group_forex_smoke" +} \ No newline at end of file diff --git a/strategies/best_USDJPY.json b/strategies/best_USDJPY.json new file mode 100644 index 0000000..e22fc25 --- /dev/null +++ b/strategies/best_USDJPY.json @@ -0,0 +1,17 @@ +{ + "vocab_version": "v71336c7c7b25", + "symbol": "USDJPY", + "group": "forex_smoke", + "formula": [ + 26, + 23, + 21, + 51, + 40, + 75, + 47, + 89 + ], + "best_score": -0.030277235433459282, + "source": "group_forex_smoke" +} \ No newline at end of file diff --git a/strategies/best_forex.json b/strategies/best_forex.json index a9b6797..cd898b0 100644 --- a/strategies/best_forex.json +++ b/strategies/best_forex.json @@ -2,14 +2,14 @@ "vocab_version": "v71336c7c7b25", "symbol": "forex", "formula": [ - 20, - 53, - 57, + 21, + 3, + 60, + 29, + 60, + 42, 73, - 34, - 73, - 56, - 50 + 54 ], - "best_score": 0.4851294159889221 + "best_score": 0.6353621482849121 } \ No newline at end of file diff --git a/strategies/best_group_forex.json b/strategies/best_group_forex.json new file mode 100644 index 0000000..e384a2b --- /dev/null +++ b/strategies/best_group_forex.json @@ -0,0 +1,19 @@ +{ + "vocab_version": "v71336c7c7b25", + "group": "forex", + "symbols": [ + "EURUSD", + "USDJPY" + ], + "formula": [ + 21, + 3, + 60, + 29, + 60, + 42, + 73, + 54 + ], + "best_score": 0.6353621482849121 +} \ No newline at end of file diff --git a/verification_results.json b/verification_results.json new file mode 100644 index 0000000..4e775f0 --- /dev/null +++ b/verification_results.json @@ -0,0 +1,621 @@ +{ + "forex_v1": { + "verdict": "VALID", + "issues": [], + "stats": { + "ann_return": 0.03225285683583934, + "sharpe": 0.8785368276640629, + "sortino": 1.0233507419039034, + "calmar": 0.4586835133050857, + "mdd": 0.0703161358833313, + "win_rate": 0.384975403547287, + "total_return": 0.2584260106086731, + "avg_turnover": 0.02845924161374569, + "trade_count": 2336 + }, + "long_short": { + "long_ratio": 0.3843853771686554, + "short_ratio": 0.3843853771686554, + "flat_ratio": 0.2312292456626892, + "max_side": 0.3843853771686554, + "beta_neutral": true + }, + "consistency": { + "h1_ann": 0.02779934868158307, + "h2_ann": 0.03670636215247214, + "h1_sharpe": 0.9579625448649836, + "h2_sharpe": 0.8526180808897832, + "same_sign": true + }, + "per_symbol": [ + { + "ann_ret": 0.025692339477245696, + "sharpe": 0.6419264849927415, + "mdd": 0.06522083282470703, + "long_ratio": 0.37219488620758057, + "short_ratio": 0.3965758681297302 + }, + { + "ann_ret": 0.03881337703205645, + "sharpe": 0.8333598342796391, + "mdd": 0.0858595073223114, + "long_ratio": 0.3965758681297302, + "short_ratio": 0.37219488620758057 + } + ], + "walk_forward": [ + { + "fold": 1, + "val_start": 12499, + "val_end": 24998, + "val_bars": 12499, + "ann_ret": 0.01522065191238653, + "sharpe": 0.5153746330235244, + "mdd": 0.038693562150001526 + }, + { + "fold": 2, + "val_start": 24998, + "val_end": 37497, + "val_bars": 12499, + "ann_ret": 0.06162417514133267, + "sharpe": 1.3522430816027502, + "mdd": 0.0414937362074852 + }, + { + "fold": 3, + "val_start": 37497, + "val_end": 49996, + "val_bars": 12499, + "ann_ret": 0.0117914811380615, + "sharpe": 0.292061962696582, + "mdd": 0.07031615823507309 + } + ], + "cost_stress": { + "1.0x": { + "ann_ret": 0.03225285683583934, + "sharpe": 0.8785368276640629, + "profitable": true + }, + "2.0x": { + "ann_ret": 0.014494292663584929, + "sharpe": 0.3940710559697957, + "profitable": true + }, + "3.0x": { + "ann_ret": -0.003264278248025221, + "sharpe": -0.08846567850190631, + "profitable": false + }, + "5.0x": { + "ann_ret": -0.038781425391789526, + "sharpe": -1.0402863866896266, + "profitable": false + } + } + }, + "forex_v2": { + "verdict": "VALID", + "issues": [], + "stats": { + "ann_return": 0.028499781983555295, + "sharpe": 0.6837691289546047, + "sortino": 0.8870228234392211, + "calmar": 0.41447263728734396, + "mdd": 0.06876154243946075, + "win_rate": 0.4931197166442871, + "total_return": 0.22835449874401093, + "avg_turnover": 0.02268700674176216, + "trade_count": 1836 + }, + "long_short": { + "long_ratio": 0.49067962169647217, + "short_ratio": 0.49067962169647217, + "flat_ratio": 0.018640756607055664, + "max_side": 0.49067962169647217, + "beta_neutral": true + }, + "consistency": { + "h1_ann": 0.03236625110730529, + "h2_ann": 0.02463331002218183, + "h1_sharpe": 0.9437755183446854, + "h2_sharpe": 0.5138189099744691, + "same_sign": true + }, + "per_symbol": [ + { + "ann_ret": 0.003494368720566854, + "sharpe": 0.07732446417315128, + "mdd": 0.12904603779315948, + "long_ratio": 0.4642585813999176, + "short_ratio": 0.5171006917953491 + }, + { + "ann_ret": 0.053505189571296796, + "sharpe": 1.005638217195026, + "mdd": 0.0557837188243866, + "long_ratio": 0.5171006917953491, + "short_ratio": 0.4642585813999176 + } + ], + "walk_forward": [ + { + "fold": 1, + "val_start": 12499, + "val_end": 24998, + "val_bars": 12499, + "ann_ret": 0.016989275936794, + "sharpe": 0.5063963436360088, + "mdd": 0.03659529238939285 + }, + { + "fold": 2, + "val_start": 24998, + "val_end": 37497, + "val_bars": 12499, + "ann_ret": 0.056761901651043445, + "sharpe": 1.117910173377945, + "mdd": 0.03889311105012894 + }, + { + "fold": 3, + "val_start": 37497, + "val_end": 49996, + "val_bars": 12499, + "ann_ret": -0.007548167104687309, + "sharpe": -0.1680046161394318, + "mdd": 0.06876153498888016 + } + ], + "cost_stress": { + "1.0x": { + "ann_ret": 0.028499781983555295, + "sharpe": 0.6837691289546047, + "profitable": true + }, + "2.0x": { + "ann_ret": 0.014343088478199206, + "sharpe": 0.343916223456671, + "profitable": true + }, + "3.0x": { + "ann_ret": 0.00018639794348018768, + "sharpe": 0.0044646392685187, + "profitable": true + }, + "5.0x": { + "ann_ret": -0.02812698920024559, + "sharpe": -0.6713121695538393, + "profitable": false + } + } + }, + "index_v1": { + "verdict": "INVALID", + "issues": [ + "年化收益 -1.79% < 2%", + "Sharpe -1.136 < 0.5", + "Walk-forward 0/3 折为正", + "2x 成本下亏损" + ], + "stats": { + "ann_return": -0.017860170955827925, + "sharpe": -1.1358695485418329, + "sortino": -0.6556359563611995, + "calmar": -0.18441874027411567, + "mdd": 0.09684573858976364, + "win_rate": 0.08996701240539551, + "total_return": -0.0919741541147232, + "avg_turnover": 0.028545185923576355, + "trade_count": 1226 + }, + "long_short": { + "long_ratio": 0.10816165804862976, + "short_ratio": 0.07396112382411957, + "flat_ratio": 0.8178772181272507, + "max_side": 0.10816165804862976, + "beta_neutral": true + }, + "consistency": { + "h1_ann": -0.02906841051299125, + "h2_ann": -0.006651929270446999, + "h1_sharpe": -1.847605535145893, + "h2_sharpe": -0.4233319151034637, + "same_sign": true + }, + "per_symbol": [ + { + "ann_ret": -0.023491951178584713, + "sharpe": -0.5302854635377421, + "mdd": 0.20357897877693176, + "long_ratio": 0.10714507848024368, + "short_ratio": 0.07577643543481827 + }, + { + "ann_ret": 0.03987654657976236, + "sharpe": 0.6371405647729896, + "mdd": 0.13079439103603363, + "long_ratio": 0.10117010027170181, + "short_ratio": 0.08187589794397354 + }, + { + "ann_ret": -0.06996510826866142, + "sharpe": -1.6923644082175937, + "mdd": 0.3651714622974396, + "long_ratio": 0.11616978794336319, + "short_ratio": 0.0642310306429863 + } + ], + "walk_forward": [ + { + "fold": 1, + "val_start": 8033, + "val_end": 16066, + "val_bars": 8033, + "ann_ret": -0.03831924746918958, + "sharpe": -2.9774461250868685, + "mdd": 0.04970230534672737 + }, + { + "fold": 2, + "val_start": 16066, + "val_end": 24099, + "val_bars": 8033, + "ann_ret": -0.008177048312063562, + "sharpe": -0.5711831458667782, + "mdd": 0.02360520511865616 + }, + { + "fold": 3, + "val_start": 24099, + "val_end": 32132, + "val_bars": 8033, + "ann_ret": -0.0051276388148835395, + "sharpe": -0.30167999605085655, + "mdd": 0.03521466255187988 + } + ], + "cost_stress": { + "1.0x": { + "ann_ret": -0.017860170955827925, + "sharpe": -1.1358695485418329, + "profitable": false + }, + "2.0x": { + "ann_ret": -0.03567236904927995, + "sharpe": -2.256961857863318, + "profitable": false + }, + "3.0x": { + "ann_ret": -0.0534845600486733, + "sharpe": -3.3545130827703757, + "profitable": false + }, + "5.0x": { + "ann_ret": -0.08910896474844776, + "sharpe": -5.438278339084857, + "profitable": false + } + } + }, + "index_v2": { + "verdict": "INVALID", + "issues": [ + "年化收益 -1.79% < 2%", + "Sharpe -1.136 < 0.5", + "Walk-forward 0/3 折为正", + "2x 成本下亏损" + ], + "stats": { + "ann_return": -0.017860170955827925, + "sharpe": -1.1358695485418329, + "sortino": -0.6556359563611995, + "calmar": -0.18441874027411567, + "mdd": 0.09684573858976364, + "win_rate": 0.08996701240539551, + "total_return": -0.0919741541147232, + "avg_turnover": 0.028545185923576355, + "trade_count": 1226 + }, + "long_short": { + "long_ratio": 0.10816165804862976, + "short_ratio": 0.07396112382411957, + "flat_ratio": 0.8178772181272507, + "max_side": 0.10816165804862976, + "beta_neutral": true + }, + "consistency": { + "h1_ann": -0.02906841051299125, + "h2_ann": -0.006651929270446999, + "h1_sharpe": -1.847605535145893, + "h2_sharpe": -0.4233319151034637, + "same_sign": true + }, + "per_symbol": [ + { + "ann_ret": -0.023491951178584713, + "sharpe": -0.5302854635377421, + "mdd": 0.20357897877693176, + "long_ratio": 0.10714507848024368, + "short_ratio": 0.07577643543481827 + }, + { + "ann_ret": 0.03987654657976236, + "sharpe": 0.6371405647729896, + "mdd": 0.13079439103603363, + "long_ratio": 0.10117010027170181, + "short_ratio": 0.08187589794397354 + }, + { + "ann_ret": -0.06996510826866142, + "sharpe": -1.6923644082175937, + "mdd": 0.3651714622974396, + "long_ratio": 0.11616978794336319, + "short_ratio": 0.0642310306429863 + } + ], + "walk_forward": [ + { + "fold": 1, + "val_start": 8033, + "val_end": 16066, + "val_bars": 8033, + "ann_ret": -0.03831924746918958, + "sharpe": -2.9774461250868685, + "mdd": 0.04970230534672737 + }, + { + "fold": 2, + "val_start": 16066, + "val_end": 24099, + "val_bars": 8033, + "ann_ret": -0.008177048312063562, + "sharpe": -0.5711831458667782, + "mdd": 0.02360520511865616 + }, + { + "fold": 3, + "val_start": 24099, + "val_end": 32132, + "val_bars": 8033, + "ann_ret": -0.0051276388148835395, + "sharpe": -0.30167999605085655, + "mdd": 0.03521466255187988 + } + ], + "cost_stress": { + "1.0x": { + "ann_ret": -0.017860170955827925, + "sharpe": -1.1358695485418329, + "profitable": false + }, + "2.0x": { + "ann_ret": -0.03567236904927995, + "sharpe": -2.256961857863318, + "profitable": false + }, + "3.0x": { + "ann_ret": -0.0534845600486733, + "sharpe": -3.3545130827703757, + "profitable": false + }, + "5.0x": { + "ann_ret": -0.08910896474844776, + "sharpe": -5.438278339084857, + "profitable": false + } + } + }, + "metals_comm_v1": { + "verdict": "INVALID", + "issues": [ + "MDD 22.18% > 10% (FTMO limit)", + "MDD 22.18% > 20% (critical)" + ], + "stats": { + "ann_return": 1.256259041838348, + "sharpe": 3.419889352516261, + "sortino": 4.451217630391946, + "calmar": 5.664438371426087, + "mdd": 0.2217799723148346, + "win_rate": 0.5095951557159424, + "total_return": 1.7205111980438232, + "avg_turnover": 0.08465711027383804, + "trade_count": 1325 + }, + "long_short": { + "long_ratio": 0.4897807836532593, + "short_ratio": 0.48502224683761597, + "flat_ratio": 0.025196969509124756, + "max_side": 0.4897807836532593, + "beta_neutral": true + }, + "consistency": { + "h1_ann": 1.4894886268302798, + "h2_ann": 1.023029456846416, + "h1_sharpe": 4.41650786597162, + "h2_sharpe": 2.589178612823683, + "same_sign": true + }, + "per_symbol": [ + { + "ann_ret": -0.11225791677134112, + "sharpe": -0.6681047687438267, + "mdd": 0.2593359053134918, + "long_ratio": 0.4836180806159973, + "short_ratio": 0.4810437560081482 + }, + { + "ann_ret": 2.0995144732296467, + "sharpe": 2.1346754413124116, + "mdd": 0.5484974384307861, + "long_ratio": 0.4846712052822113, + "short_ratio": 0.49684062600135803 + }, + { + "ann_ret": 1.781520489603281, + "sharpe": 3.413070651924102, + "mdd": 0.24614226818084717, + "long_ratio": 0.5010530948638916, + "short_ratio": 0.4771822988986969 + } + ], + "walk_forward": [ + { + "fold": 1, + "val_start": 2136, + "val_end": 4272, + "val_bars": 2136, + "ann_ret": 2.8320215921849012, + "sharpe": 7.270449137383887, + "mdd": 0.07218259572982788 + }, + { + "fold": 2, + "val_start": 4272, + "val_end": 6408, + "val_bars": 2136, + "ann_ret": 1.4322355552576482, + "sharpe": 3.261325913253051, + "mdd": 0.14048588275909424 + }, + { + "fold": 3, + "val_start": 6408, + "val_end": 8544, + "val_bars": 2136, + "ann_ret": 0.6180683069396764, + "sharpe": 1.7886899698535008, + "mdd": 0.13295584917068481 + } + ], + "cost_stress": { + "1.0x": { + "ann_ret": 1.256259041838348, + "sharpe": 3.419889352516261, + "profitable": true + }, + "2.0x": { + "ann_ret": 1.2034330260939896, + "sharpe": 3.2761388807938188, + "profitable": true + }, + "3.0x": { + "ann_ret": 1.1506070103496313, + "sharpe": 3.132352688083853, + "profitable": true + }, + "5.0x": { + "ann_ret": 1.0449548880569637, + "sharpe": 2.8446898240147953, + "profitable": true + } + } + }, + "metals_comm_v2": { + "verdict": "SUSPICIOUS", + "issues": [ + "MDD 13.62% > 10% (FTMO limit)" + ], + "stats": { + "ann_return": 1.5158334863372147, + "sharpe": 4.072471969324447, + "sortino": 6.084364270232026, + "calmar": 11.129665592304152, + "mdd": 0.13619756698608398, + "win_rate": 0.5025743246078491, + "total_return": 2.0760116577148438, + "avg_turnover": 0.1363547146320343, + "trade_count": 2163 + }, + "long_short": { + "long_ratio": 0.5548404455184937, + "short_ratio": 0.42335596680641174, + "flat_ratio": 0.021803587675094604, + "max_side": 0.5548404455184937, + "beta_neutral": true + }, + "consistency": { + "h1_ann": 1.0897175129503012, + "h2_ann": 1.9419496413320303, + "h1_sharpe": 3.2331406559377585, + "h2_sharpe": 4.803671259403472, + "same_sign": true + }, + "per_symbol": [ + { + "ann_ret": 0.03780910235946067, + "sharpe": 0.18824497863739226, + "mdd": 0.21118657290935516, + "long_ratio": 0.5334659218788147, + "short_ratio": 0.44313129782676697 + }, + { + "ann_ret": 3.175588510930538, + "sharpe": 3.2773255580593, + "mdd": 0.39837217330932617, + "long_ratio": 0.5785162448883057, + "short_ratio": 0.4008893072605133 + }, + { + "ann_ret": 1.3341026357375085, + "sharpe": 2.514352078669877, + "mdd": 0.34516072273254395, + "long_ratio": 0.5525392293930054, + "short_ratio": 0.42604726552963257 + } + ], + "walk_forward": [ + { + "fold": 1, + "val_start": 2136, + "val_end": 4272, + "val_bars": 2136, + "ann_ret": 2.014179644174874, + "sharpe": 5.060496407045769, + "mdd": 0.09889370203018188 + }, + { + "fold": 2, + "val_start": 4272, + "val_end": 6408, + "val_bars": 2136, + "ann_ret": 2.9089955566450953, + "sharpe": 6.598493902587262, + "mdd": 0.10469889640808105 + }, + { + "fold": 3, + "val_start": 6408, + "val_end": 8544, + "val_bars": 2136, + "ann_ret": 0.9781382535584271, + "sharpe": 2.6893789421113077, + "mdd": 0.1361975073814392 + } + ], + "cost_stress": { + "1.0x": { + "ann_ret": 1.5158334863372147, + "sharpe": 4.072471969324447, + "profitable": true + }, + "2.0x": { + "ann_ret": 1.4307482773438096, + "sharpe": 3.844064401255744, + "profitable": true + }, + "3.0x": { + "ann_ret": 1.3456627959385514, + "sharpe": 3.615527716332386, + "profitable": true + }, + "5.0x": { + "ann_ret": 1.1754920147359371, + "sharpe": 3.158150751846823, + "profitable": true + } + } + } +} \ No newline at end of file diff --git a/verify_all_strategies.py b/verify_all_strategies.py new file mode 100644 index 0000000..5c1eca7 --- /dev/null +++ b/verify_all_strategies.py @@ -0,0 +1,459 @@ +""" +verify_all_strategies.py — 极度严谨的独立回测验证 + +不复用训练框架的 reward 函数,从第一性原理出发: +1. 加载数据 → 计算 target_ret = log(open[t+2]/open[t+1]) +2. 执行公式 → 得到 factor[t] +3. 计算仓位 = tanh(factor[t]),应用 MIN_TRADE_EXPOSURE +4. PnL = pos[t] * target_ret[t] - |pos[t]-pos[t-1]| * cost +5. 严格统计:年化、Sharpe、Sortino、MDD、胜率、多空比、前后一致性 +6. Walk-forward 4 折(不复用 engine 的实现) +7. 多 cost 场景压力测试 +8. Beta 中性检验 + +验证标准: +- 有效:年化 > 2%, Sharpe > 0.5, MDD < 10% (FTMO), 前后一致, 多空均衡 +- 可疑:任一维度不达标 +- 无效:年化 < 0 或 Sharpe < 0 或 MDD > 20% 或 严重单边 +""" +import sys +import json +import math +import torch +import numpy as np +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from config import Config +from data_pipeline.fetcher import MT5DataFetcher +from data_pipeline.data_manager import MT5DataManager +from model_core.vm import StackVM +from model_core.vocab import FORMULA_VOCAB +from strategy_manager.signal import compute_target_positions_stateless + +# ── 常量 ────────────────────────────────────────────────────────────── +PERIODS_PER_YEAR = 6240 # H1 +COST_RATE = 0.0001 # 单边成本 +MIN_EXPOSURE = 0.05 + +GROUPS = { + "forex": ["EURUSD", "USDJPY"], + "metals_comm": ["XAUUSD", "AAVUSD", "COCOA.c"], + "index": ["US30.cash", "US100.cash", "US500.cash"], +} + +# ── 策略文件 ────────────────────────────────────────────────────────── +STRATEGIES = { + "forex_v1": {"file": "strategies/archive/best_forex_20250705_pre_refactor.json", "group": "forex"}, + "forex_v2": {"file": "strategies/best_forex.json", "group": "forex"}, + "index_v1": {"file": "strategies/archive/best_index_20250705_pre_refactor.json", "group": "index"}, + "index_v2": {"file": "strategies/best_index.json", "group": "index"}, + "metals_comm_v1": {"file": "strategies/archive/best_metals_comm_20250705_pre_refactor.json", "group": "metals_comm"}, + "metals_comm_v2": {"file": "strategies/best_metals_comm.json", "group": "metals_comm"}, +} + + +def load_strategy(path: str) -> dict: + with open(path) as f: + return json.load(f) + + +def compute_factor(formula: list[int], feat_tensor: torch.Tensor, vm: StackVM) -> torch.Tensor: + """执行公式得到因子值 [N, T]""" + return vm.run(formula, feat_tensor) + + +def independent_backtest(factor: torch.Tensor, target_ret: torch.Tensor, + cost_rate: float = COST_RATE) -> dict: + """完全独立的回测,不复用训练框架的任何评分函数。""" + N, T = factor.shape + + # 仓位 = tanh(factor),应用最小暴露门槛 + pos = torch.tanh(factor) + pos = torch.where(pos.abs() >= MIN_EXPOSURE, pos, torch.zeros_like(pos)) + + # 换手 + prev_pos = torch.zeros_like(pos) + prev_pos[:, 1:] = pos[:, :-1] + turnover = (pos - prev_pos).abs() + + # PnL + pnl = pos * target_ret - turnover * cost_rate # [N, T] + + # 组合(等权) + port_pnl = pnl.mean(dim=0) # [T] + + # ── 基础统计 ────────────────────────────────────────────────────── + total_return = port_pnl.sum().item() + ann_return = port_pnl.mean().item() * PERIODS_PER_YEAR + + # Sharpe + mean_pnl = port_pnl.mean().item() + std_pnl = port_pnl.std(unbiased=False).item() + sharpe = (mean_pnl / (std_pnl + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if std_pnl > 1e-8 else 0.0 + + # Sortino + downside = port_pnl[port_pnl < 0] + if downside.numel() > 0: + ds_std = downside.std(unbiased=False).item() + else: + ds_std = 0.0 + full_std = max(std_pnl, 1e-8) + ds_floor = max(full_std * 0.2, 1e-8) + ds_std_clamped = max(ds_std, ds_floor) + sortino = (mean_pnl / (ds_std_clamped + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if ds_std_clamped > 1e-8 else 0.0 + + # MDD + cum = torch.cumsum(port_pnl, dim=0) + peak = torch.cummax(cum, dim=0).values + drawdown = (peak - cum).max().item() + mdd_pct = drawdown # 绝对值 + + # Calmar + calmar = ann_return / (mdd_pct + 1e-8) if mdd_pct > 1e-8 else 0.0 + + # 胜率 + win_rate = (port_pnl > 0).float().mean().item() + + # 多空比 + long_ratio = (pos > 0.05).float().mean().item() + short_ratio = (pos < -0.05).float().mean().item() + flat_ratio = 1.0 - long_ratio - short_ratio + max_side = max(long_ratio, short_ratio) + + # 换手率 + avg_turnover = turnover.mean().item() + + # 交易次数(仓位方向变化) + trade_count = 0 + for n in range(N): + pos_n = pos[n].tolist() + prev_dir = 0 + for p in pos_n: + cur_dir = 1 if p > 0.05 else (-1 if p < -0.05 else 0) + if cur_dir != 0 and cur_dir != prev_dir and prev_dir != 0: + trade_count += 1 + if cur_dir != 0: + prev_dir = cur_dir + + # ── 前后一致性 ──────────────────────────────────────────────────── + half = T // 2 + h1_pnl = port_pnl[:half] + h2_pnl = port_pnl[half:] + + h1_ann = h1_pnl.mean().item() * PERIODS_PER_YEAR + h2_ann = h2_pnl.mean().item() * PERIODS_PER_YEAR + + h1_std = h1_pnl.std(unbiased=False).item() + h2_std = h2_pnl.std(unbiased=False).item() + h1_sharpe = (h1_pnl.mean().item() / (h1_std + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if h1_std > 1e-8 else 0.0 + h2_sharpe = (h2_pnl.mean().item() / (h2_std + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if h2_std > 1e-8 else 0.0 + + # ── 品种级 ──────────────────────────────────────────────────────── + per_symbol = [] + for n in range(N): + sym_pnl = pnl[n] + sym_ann = sym_pnl.mean().item() * PERIODS_PER_YEAR + sym_std = sym_pnl.std(unbiased=False).item() + sym_sharpe = (sym_pnl.mean().item() / (sym_std + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if sym_std > 1e-8 else 0.0 + sym_mdd = 0.0 + sym_cum = torch.cumsum(sym_pnl, dim=0) + sym_peak = torch.cummax(sym_cum, dim=0).values + sym_mdd = (sym_peak - sym_cum).max().item() + per_symbol.append({ + "ann_ret": sym_ann, + "sharpe": sym_sharpe, + "mdd": sym_mdd, + "long_ratio": (pos[n] > 0.05).float().mean().item(), + "short_ratio": (pos[n] < -0.05).float().mean().item(), + }) + + # ── Walk-Forward 4 折(独立实现)────────────────────────────────── + n_folds = 4 + fold_size = T // n_folds + wf_results = [] + for k in range(1, n_folds): + train_end = fold_size * k + val_start = train_end + val_end = min(train_end + fold_size, T) + if val_start >= T or val_end <= val_start: + continue + val_pnl = port_pnl[val_start:val_end] + val_ann = val_pnl.mean().item() * PERIODS_PER_YEAR + val_std = val_pnl.std(unbiased=False).item() + val_sharpe = (val_pnl.mean().item() / (val_std + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if val_std > 1e-8 else 0.0 + val_cum = torch.cumsum(val_pnl, dim=0) + val_peak = torch.cummax(val_cum, dim=0).values + val_mdd = (val_peak - val_cum).max().item() + wf_results.append({ + "fold": k, + "val_start": val_start, + "val_end": val_end, + "val_bars": val_end - val_start, + "ann_ret": val_ann, + "sharpe": val_sharpe, + "mdd": val_mdd, + }) + + # ── 成本压力测试 ────────────────────────────────────────────────── + cost_stress = {} + for mult in [1.0, 2.0, 3.0, 5.0]: + stressed_pnl = pos * target_ret - turnover * cost_rate * mult + stressed_port = stressed_pnl.mean(dim=0) + stressed_ann = stressed_port.mean().item() * PERIODS_PER_YEAR + stressed_std = stressed_port.std(unbiased=False).item() + stressed_sharpe = (stressed_port.mean().item() / (stressed_std + 1e-8)) * math.sqrt(PERIODS_PER_YEAR) if stressed_std > 1e-8 else 0.0 + cost_stress[f"{mult}x"] = { + "ann_ret": stressed_ann, + "sharpe": stressed_sharpe, + "profitable": stressed_ann > 0, + } + + # ── 判定 ────────────────────────────────────────────────────────── + verdict = "VALID" + issues = [] + + if ann_return < 0.02: + verdict = "INVALID" + issues.append(f"年化收益 {ann_return*100:.2f}% < 2%") + elif ann_return < 0: + verdict = "INVALID" + issues.append(f"年化收益为负 {ann_return*100:.2f}%") + + if sharpe < 0.5: + if verdict == "VALID": + verdict = "SUSPICIOUS" + issues.append(f"Sharpe {sharpe:.3f} < 0.5") + + if mdd_pct > 0.10: + if verdict == "VALID": + verdict = "SUSPICIOUS" + issues.append(f"MDD {mdd_pct*100:.2f}% > 10% (FTMO limit)") + if mdd_pct > 0.20: + verdict = "INVALID" + issues.append(f"MDD {mdd_pct*100:.2f}% > 20% (critical)") + + if max_side > 0.85: + if verdict == "VALID": + verdict = "SUSPICIOUS" + issues.append(f"单边占比 {max_side*100:.1f}% > 85% (beta factor)") + + # 前后一致性 + if h1_ann * h2_ann < 0: + if verdict == "VALID": + verdict = "SUSPICIOUS" + issues.append(f"前后半段年化收益符号相反: H1={h1_ann*100:.2f}%, H2={h2_ann*100:.2f}%") + + # Walk-forward 一致性 + wf_positive = sum(1 for w in wf_results if w["ann_ret"] > 0) + if wf_results and wf_positive < len(wf_results) * 0.5: + if verdict == "VALID": + verdict = "SUSPICIOUS" + issues.append(f"Walk-forward {wf_positive}/{len(wf_results)} 折为正") + + # 成本压力 + if not cost_stress.get("2.0x", {}).get("profitable", False): + if verdict == "VALID": + verdict = "SUSPICIOUS" + issues.append("2x 成本下亏损") + + return { + "verdict": verdict, + "issues": issues, + "stats": { + "ann_return": ann_return, + "sharpe": sharpe, + "sortino": sortino, + "calmar": calmar, + "mdd": mdd_pct, + "win_rate": win_rate, + "total_return": total_return, + "avg_turnover": avg_turnover, + "trade_count": trade_count, + }, + "long_short": { + "long_ratio": long_ratio, + "short_ratio": short_ratio, + "flat_ratio": flat_ratio, + "max_side": max_side, + "beta_neutral": max_side <= 0.85, + }, + "consistency": { + "h1_ann": h1_ann, + "h2_ann": h2_ann, + "h1_sharpe": h1_sharpe, + "h2_sharpe": h2_sharpe, + "same_sign": h1_ann * h2_ann > 0, + }, + "per_symbol": per_symbol, + "walk_forward": wf_results, + "cost_stress": cost_stress, + } + + +def main(): + vm = StackVM() + results = {} + + with MT5DataFetcher(offline=True) as fetcher: + for sname, sinfo in STRATEGIES.items(): + sfile = sinfo["file"] + group = sinfo["group"] + symbols = GROUPS.get(group, []) + + if not Path(sfile).exists(): + print(f"\n[{sname}] SKIP: {sfile} not found") + continue + + strat = load_strategy(sfile) + formula = strat.get("formula") + saved_score = strat.get("best_score", "?") + + print(f"\n{'='*70}") + print(f" [{sname}] group={group} symbols={symbols}") + print(f" file={sfile}") + print(f" formula={formula}") + print(f" saved best_score={saved_score}") + decoded = [] + for tid in formula: + decoded.append(FORMULA_VOCAB.token_names[tid]) + print(f" decoded: {' -> '.join(decoded)}") + print(f"{'='*70}") + + # 加载数据(临时覆盖 Config.SYMBOLS) + original_symbols = Config.SYMBOLS[:] + Config.SYMBOLS = symbols + try: + mgr = MT5DataManager(fetcher) + mgr.load() + finally: + Config.SYMBOLS = original_symbols + feat = mgr.feat_tensor + target_ret = mgr.target_ret + N, T = target_ret.shape + print(f" Data: N={N} symbols, T={T} bars ({T/PERIODS_PER_YEAR:.2f} years)") + + # 执行公式 + try: + factor = vm.execute(formula, feat) + except Exception as e: + print(f" ERROR executing formula: {e}") + results[sname] = {"error": str(e)} + continue + + # 检查 factor 是否有 NaN/Inf + if torch.isnan(factor).any() or torch.isinf(factor).any(): + print(f" ERROR: factor contains NaN/Inf") + nan_count = torch.isnan(factor).sum().item() + inf_count = torch.isinf(factor).sum().item() + print(f" NaN={nan_count}, Inf={inf_count}") + results[sname] = {"error": f"NaN={nan_count}, Inf={inf_count}"} + continue + + print(f" Factor stats: mean={factor.mean():.4f}, std={factor.std():.4f}, " + f"min={factor.min():.4f}, max={factor.max():.4f}") + + # 独立回测 + bt = independent_backtest(factor, target_ret) + + # 打印结果 + s = bt["stats"] + ls = bt["long_short"] + cs = bt["consistency"] + + print(f"\n ── 核心统计 ──") + print(f" 年化收益: {s['ann_return']*100:>8.2f}%") + print(f" Sharpe: {s['sharpe']:>8.3f}") + print(f" Sortino: {s['sortino']:>8.3f}") + print(f" Calmar: {s['calmar']:>8.3f}") + print(f" MDD: {s['mdd']*100:>8.2f}%") + print(f" 胜率: {s['win_rate']*100:>8.1f}%") + print(f" 换手率: {s['avg_turnover']:>8.4f}") + print(f" 交易次数: {s['trade_count']:>8d}") + + print(f"\n ── 多空均衡 ──") + print(f" 多: {ls['long_ratio']*100:.1f}% 空: {ls['short_ratio']*100:.1f}% " + f"平: {ls['flat_ratio']*100:.1f}% 最大单边: {ls['max_side']*100:.1f}% " + f"Beta中性: {'是' if ls['beta_neutral'] else '否'}") + + print(f"\n ── 前后一致性 ──") + print(f" H1 年化: {cs['h1_ann']*100:>8.2f}% Sharpe: {cs['h1_sharpe']:.3f}") + print(f" H2 年化: {cs['h2_ann']*100:>8.2f}% Sharpe: {cs['h2_sharpe']:.3f}") + print(f" 同号: {'是' if cs['same_sign'] else '否'}") + + print(f"\n ── Walk-Forward ({len(bt['walk_forward'])} 折) ──") + for w in bt["walk_forward"]: + print(f" Fold {w['fold']}: bars={w['val_bars']} " + f"年化={w['ann_ret']*100:>7.2f}% Sharpe={w['sharpe']:.3f} MDD={w['mdd']*100:.2f}%") + wf_pos = sum(1 for w in bt["walk_forward"] if w["ann_ret"] > 0) + print(f" 正收益折数: {wf_pos}/{len(bt['walk_forward'])}") + + print(f"\n ── 成本压力测试 ──") + for mult, cs_r in bt["cost_stress"].items(): + print(f" {mult}: 年化={cs_r['ann_ret']*100:>7.2f}% Sharpe={cs_r['sharpe']:.3f} " + f"盈利={'是' if cs_r['profitable'] else '否'}") + + print(f"\n ── 品种级 ──") + for i, ps in enumerate(bt["per_symbol"]): + sym_name = symbols[i] if i < len(symbols) else f"sym{i}" + print(f" {sym_name:12s}: 年化={ps['ann_ret']*100:>7.2f}% " + f"Sharpe={ps['sharpe']:.3f} MDD={ps['mdd']*100:.2f}% " + f"L/S={ps['long_ratio']*100:.0f}/{ps['short_ratio']*100:.0f}") + + print(f"\n ── 判定 ──") + print(f" 结论: {bt['verdict']}") + if bt["issues"]: + for iss in bt["issues"]: + print(f" ⚠ {iss}") + else: + print(f" ✅ 全部检查通过") + + results[sname] = bt + + # ── 汇总表 ────────────────────────────────────────────────────────── + print(f"\n\n{'='*100}") + print(f" 汇总") + print(f"{'='*100}") + print(f"{'策略':<20s} {'年化%':>8s} {'Sharpe':>8s} {'MDD%':>8s} {'多空比':>10s} {'H1/H2同号':>10s} {'WF正折':>8s} {'2x成本':>8s} {'判定':>10s}") + print(f"{'-'*100}") + for sname, r in results.items(): + if "error" in r: + print(f"{sname:<20s} {'ERROR':>8s}") + continue + s = r["stats"] + ls = r["long_short"] + cs = r["consistency"] + wf = r["walk_forward"] + wf_pos = sum(1 for w in wf if w["ann_ret"] > 0) + cs2x = r["cost_stress"]["2.0x"]["profitable"] + ls_ratio = f"{ls['long_ratio']*100:.0f}/{ls['short_ratio']*100:.0f}" + same = "是" if cs["same_sign"] else "否" + profitable_2x = "是" if cs2x else "否" + print(f"{sname:<20s} {s['ann_return']*100:>8.2f} {s['sharpe']:>8.3f} {s['mdd']*100:>8.2f} " + f"{ls_ratio:>10s} {same:>10s} {wf_pos}/{len(wf):<5d} {profitable_2x:>8s} {r['verdict']:>10s}") + + # 保存完整结果 + output_path = "verification_results.json" + serializable = {} + for k, v in results.items(): + if "error" in v: + serializable[k] = v + else: + serializable[k] = { + "verdict": v["verdict"], + "issues": v["issues"], + "stats": v["stats"], + "long_short": v["long_short"], + "consistency": v["consistency"], + "per_symbol": v["per_symbol"], + "walk_forward": v["walk_forward"], + "cost_stress": v["cost_stress"], + } + with open(output_path, "w", encoding="utf-8") as f: + json.dump(serializable, f, indent=2, ensure_ascii=False) + print(f"\n详细结果已保存到 {output_path}") + + +if __name__ == "__main__": + main()