feat: save all strategy factors + independent verification results

- Add all 6 strategy factors (forex/index/metals_comm, v1+v2)
- Add independent backtest verification script and results
- Add strategies README with full factor documentation
- Update .gitignore to track strategy files
- Include engine.py anti-beta constraints + entropy floor fixes
- Include config.py new training parameters
This commit is contained in:
rosemarycox5334-debug
2026-07-06 09:40:39 +08:00
parent 97a5964b34
commit 3793f8283b
12 changed files with 1529 additions and 52 deletions
+8 -3
View File
@@ -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
+136
View File
@@ -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 构造错误但不影响正式回测结论。
+73
View File
@@ -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/`
+17
View File
@@ -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")
+42 -4
View File
@@ -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:
+112 -37
View File
@@ -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 恒正问题),保留仅供分析
- 实盘交易需考虑滑点、点差、流动性等额外成本
- 过去表现不代表未来收益
+17
View File
@@ -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"
}
+17
View File
@@ -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"
}
+8 -8
View File
@@ -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
}
+19
View File
@@ -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
}
+621
View File
@@ -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
}
}
}
}
+459
View File
@@ -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()