0.8
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"active_features": [
|
||||
"RET",
|
||||
"RET20",
|
||||
"MA_DIFF",
|
||||
"HL_RANGE",
|
||||
"DEV60",
|
||||
"PRESSURE",
|
||||
"VOL_RATIO",
|
||||
"VOL_Z",
|
||||
"REL_RET20",
|
||||
"VWAP_DEV",
|
||||
"EMA_RATIO_12_26",
|
||||
"TREND_STRENGTH_50",
|
||||
"PRICE_POS_50",
|
||||
"PPO",
|
||||
"ULT_OSC",
|
||||
"YANG_ZHANG_VOL",
|
||||
"RS_VOL",
|
||||
"AMIHUD_ILLIQ",
|
||||
"KYLE_LAMBDA",
|
||||
"CMF_20",
|
||||
"AROON_OSC_25",
|
||||
"DMI_DIFF_14",
|
||||
"DONCHIAN_POS_20",
|
||||
"ICHIMOKU_KIJUN_DEV",
|
||||
"SAR_DIST",
|
||||
"ROLL_KURT_20",
|
||||
"CS_RANK_RET5",
|
||||
"CS_ZSCORE_RET20"
|
||||
],
|
||||
"meta": {
|
||||
"source": "prune_features.py",
|
||||
"corr_threshold": 0.85,
|
||||
"top_k": 28,
|
||||
"n_original": 65,
|
||||
"n_retained": 28,
|
||||
"symbols": [
|
||||
"EURUSD",
|
||||
"USDJPY",
|
||||
"XAUUSD",
|
||||
"AAVUSD",
|
||||
"COCOA.c",
|
||||
"US30.cash",
|
||||
"US100.cash",
|
||||
"US500.cash",
|
||||
"US2000.cash",
|
||||
"JP225.cash"
|
||||
],
|
||||
"bars": 3498
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
backtest_elite.py — 对 elite pool 中的最优因子批量回测,发现问题
|
||||
|
||||
用法:
|
||||
python backtest_elite.py --offline
|
||||
"""
|
||||
import sys, json, math
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import Config
|
||||
from data_pipeline.data_manager import MT5DataManager
|
||||
from data_pipeline.fetcher import MT5DataFetcher
|
||||
from model_core.vocab import FORMULA_VOCAB, VOCAB_VERSION
|
||||
from model_core.vm import StackVM
|
||||
from model_core.features import MT5FeatureEngineer
|
||||
from strategy_manager.signal import compute_target_positions_stateless
|
||||
|
||||
_H1_PER_YEAR = 6240
|
||||
|
||||
|
||||
def decode(toks):
|
||||
names = FORMULA_VOCAB.token_names
|
||||
return " -> ".join(names[t] if 0 <= t < len(names) else f"?{t}" for t in toks)
|
||||
|
||||
|
||||
def calc_sharpe(pnl):
|
||||
m, s = pnl.mean(), pnl.std()
|
||||
return float(m / (s + 1e-10) * math.sqrt(_H1_PER_YEAR))
|
||||
|
||||
|
||||
def calc_sortino(pnl):
|
||||
m = pnl.mean()
|
||||
down = pnl[pnl < 0]
|
||||
ds = down.std() if len(down) > 0 else 1e-10
|
||||
ds = max(ds, abs(m), 1e-10)
|
||||
return float(np.clip(m / ds * math.sqrt(_H1_PER_YEAR), -20, 20))
|
||||
|
||||
|
||||
def calc_mdd(cum_pnl):
|
||||
peak = np.maximum.accumulate(cum_pnl)
|
||||
return float((peak - cum_pnl).max())
|
||||
|
||||
|
||||
def calc_ic(factor, target_ret):
|
||||
"""时序 IC:factor[t] vs target_ret[t+1],逐品种再取均值。"""
|
||||
N, T = factor.shape
|
||||
ic_list = []
|
||||
for n in range(N):
|
||||
x = factor[n, :-1]
|
||||
y = target_ret[n, 1:]
|
||||
xm = x - x.mean()
|
||||
ym = y - y.mean()
|
||||
sx = np.sqrt((xm**2).mean())
|
||||
sy = np.sqrt((ym**2).mean())
|
||||
if sx < 1e-6 or sy < 1e-6:
|
||||
continue
|
||||
ic = (xm * ym).mean() / (sx * sy + 1e-8)
|
||||
ic_list.append(float(ic))
|
||||
return float(np.mean(ic_list)) if ic_list else 0.0
|
||||
|
||||
|
||||
def backtest_one(formula, feat, raw_dict, target_ret, cost_rate=0.0001):
|
||||
"""对一个公式跑完整回测,返回统计结果。"""
|
||||
vm = StackVM()
|
||||
factor = vm.execute(formula, feat) # [N, T]
|
||||
if factor is None:
|
||||
return None
|
||||
|
||||
N, T = factor.shape
|
||||
factor_np = factor.detach().numpy()
|
||||
target_np = target_ret.detach().numpy()
|
||||
|
||||
# 持仓
|
||||
pos = compute_target_positions_stateless(factor) # [N, T]
|
||||
pos_np = pos.detach().numpy()
|
||||
|
||||
# 换手
|
||||
prev = np.zeros_like(pos_np)
|
||||
prev[:, 1:] = pos_np[:, :-1]
|
||||
turnover = np.abs(pos_np - prev)
|
||||
|
||||
pnl = pos_np * target_np - turnover * cost_rate # [N, T]
|
||||
|
||||
# 分品种统计
|
||||
per_sym = {}
|
||||
sym_names = ["EURUSD", "USDJPY"][:N]
|
||||
for i, sym in enumerate(sym_names):
|
||||
p = pnl[i]
|
||||
cum = np.cumsum(p)
|
||||
per_sym[sym] = {
|
||||
"pnl": p,
|
||||
"cum": cum,
|
||||
"total_ret": float(cum[-1]),
|
||||
"sharpe": calc_sharpe(p),
|
||||
"sortino": calc_sortino(p),
|
||||
"mdd": calc_mdd(cum),
|
||||
"n_trades": int((np.abs(np.diff(pos_np[i])) > 0.5).sum()),
|
||||
"avg_hold": float(T / max(1, int((np.abs(np.diff(pos_np[i])) > 0.5).sum()))),
|
||||
}
|
||||
|
||||
# 组合
|
||||
port_pnl = pnl.mean(axis=0)
|
||||
port_cum = np.cumsum(port_pnl)
|
||||
|
||||
# IC
|
||||
ic = calc_ic(factor_np, target_np)
|
||||
|
||||
# 分段分析:前50% vs 后50%(检查过拟合)
|
||||
split = T // 2
|
||||
p1 = port_pnl[:split]
|
||||
p2 = port_pnl[split:]
|
||||
|
||||
return {
|
||||
"formula": formula,
|
||||
"readable": decode(formula),
|
||||
"per_sym": per_sym,
|
||||
"port_total_ret": float(port_cum[-1]),
|
||||
"port_sharpe": calc_sharpe(port_pnl),
|
||||
"port_sortino": calc_sortino(port_pnl),
|
||||
"port_mdd": calc_mdd(port_cum),
|
||||
"ic": ic,
|
||||
"n_pos_syms": sum(1 for d in per_sym.values() if d["total_ret"] > 0),
|
||||
"n_syms": N,
|
||||
# 稳定性:前后半段
|
||||
"half1_sharpe": calc_sharpe(p1),
|
||||
"half2_sharpe": calc_sharpe(p2),
|
||||
"half1_sortino": calc_sortino(p1),
|
||||
"half2_sortino": calc_sortino(p2),
|
||||
# 换手
|
||||
"avg_turnover": float(turnover.mean()),
|
||||
"avg_hold_h": float(per_sym[sym_names[0]]["avg_hold"]) if sym_names else 0,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
offline = "--offline" in sys.argv
|
||||
|
||||
# ── 候选因子(从 elite pool 手工提取,按分数降序)
|
||||
CANDIDATES = [
|
||||
# score=6.636 — 当前最优
|
||||
[40, 51, 81, 83, 86, 125, 88, 125],
|
||||
# score=6.224 — 变体(TANH_SQUASH→TS_SUM_5)
|
||||
[40, 51, 81, 83, 86, 125, 88, 112],
|
||||
# score=5.960 — TRIX × HURST 类型
|
||||
[50, 105, 94, 103, 59, 66, 119, 88],
|
||||
# score=5.827 — TRIX × HURST(4 个等分变体取代表)
|
||||
[50, 105, 89, 103, 59, 88, 112, 66],
|
||||
# score=5.811
|
||||
[50, 105, 82, 103, 59, 88, 112, 66],
|
||||
# 冒烟测试最优(供参比)
|
||||
[3, 23, 110, 42, 53, 104, 72, 109],
|
||||
]
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f" Elite Pool 批量回测 | vocab={VOCAB_VERSION} | offline={offline}")
|
||||
print(f"{'='*70}\n")
|
||||
|
||||
# ── 加载数据
|
||||
print("加载数据...")
|
||||
with MT5DataFetcher(offline=offline) as fetcher:
|
||||
mgr = MT5DataManager(fetcher)
|
||||
mgr.load()
|
||||
raw_dict = mgr.raw_dict
|
||||
syms = mgr.symbols
|
||||
T = raw_dict["open"].shape[1]
|
||||
print(f" 品种: {syms} T={T} bars")
|
||||
|
||||
# 只取 forex 组(EURUSD/USDJPY)
|
||||
forex_syms = ["EURUSD", "USDJPY"]
|
||||
forex_idx = [syms.index(s) for s in forex_syms if s in syms]
|
||||
forex_raw = {k: v[forex_idx] for k, v in raw_dict.items()}
|
||||
forex_feat = MT5FeatureEngineer.compute_features(forex_raw)
|
||||
target_ret = mgr.target_ret[forex_idx]
|
||||
|
||||
print(f" Forex 品种: {[syms[i] for i in forex_idx]} feat={forex_feat.shape}\n")
|
||||
|
||||
# ── 批量回测
|
||||
results = []
|
||||
for i, formula in enumerate(CANDIDATES):
|
||||
res = backtest_one(formula, forex_feat, forex_raw, target_ret)
|
||||
if res is None:
|
||||
print(f" [{i+1}] ❌ 公式执行失败: {formula}")
|
||||
continue
|
||||
results.append(res)
|
||||
|
||||
# ── 打印结果
|
||||
print(f"\n{'='*70}")
|
||||
print(f" 回测结果汇总({T} bars H1,forex 组)")
|
||||
print(f"{'='*70}")
|
||||
hdr = f" {'#':>3} {'Score*':>7} {'TotRet':>7} {'Sharpe':>7} {'Sortino':>8} {'MDD':>6} {'IC':>7} {'H1':>5} {'H2':>5} {'AvgHold':>8} {'公式类型':}"
|
||||
print(hdr)
|
||||
print(f" {'─'*95}")
|
||||
|
||||
for i, r in enumerate(results):
|
||||
# 判断前后半段一致性
|
||||
h1, h2 = r["half1_sharpe"], r["half2_sharpe"]
|
||||
consistency = "✓" if h1 > 0 and h2 > 0 else ("⚠" if h1 * h2 > 0 else "✗")
|
||||
print(f" [{i+1:2d}] {r['port_sortino']:>7.3f} "
|
||||
f"{r['port_total_ret']:>7.3f} "
|
||||
f"{r['port_sharpe']:>7.3f} "
|
||||
f"{r['port_sortino']:>8.3f} "
|
||||
f"{r['port_mdd']:>6.3f} "
|
||||
f"{r['ic']:>7.4f} "
|
||||
f"{h1:>5.2f} {h2:>5.2f} "
|
||||
f"{r['avg_hold_h']:>8.1f}h "
|
||||
f"{consistency} {r['readable'][:55]}")
|
||||
|
||||
# ── 品种级详情
|
||||
print(f"\n{'─'*70}")
|
||||
print(f" 品种级详情(前 3 个公式)")
|
||||
print(f"{'─'*70}")
|
||||
for i, r in enumerate(results[:3]):
|
||||
print(f"\n [{i+1}] {r['readable']}")
|
||||
for sym, d in r["per_sym"].items():
|
||||
sig = "✓" if d["total_ret"] > 0 else "✗"
|
||||
print(f" {sym}: TotRet={d['total_ret']:+.3f} Sharpe={d['sharpe']:+.3f} "
|
||||
f"Sortino={d['sortino']:+.3f} MDD={d['mdd']:.3f} "
|
||||
f"Trades={d['n_trades']} AvgHold={d['avg_hold']:.0f}h {sig}")
|
||||
|
||||
# ── 关键诊断
|
||||
print(f"\n{'='*70}")
|
||||
print(f" 关键诊断")
|
||||
print(f"{'='*70}")
|
||||
for i, r in enumerate(results):
|
||||
issues = []
|
||||
h1, h2 = r["half1_sharpe"], r["half2_sharpe"]
|
||||
if h1 > 0 and h2 < 0:
|
||||
issues.append(f"过拟合嫌疑: 前半Sharpe={h1:.2f} 后半={h2:.2f}")
|
||||
if r["port_mdd"] > 0.5:
|
||||
issues.append(f"最大回撤过大: {r['port_mdd']:.3f}")
|
||||
if r["avg_hold_h"] < 2:
|
||||
issues.append(f"持仓太短(>{r['avg_hold_h']:.1f}h),点差侵蚀大")
|
||||
if r["avg_hold_h"] > 500:
|
||||
issues.append(f"持仓极长({r['avg_hold_h']:.0f}h),实际交易数极少")
|
||||
if r["n_pos_syms"] < r["n_syms"]:
|
||||
issues.append(f"仅{r['n_pos_syms']}/{r['n_syms']}品种盈利,跨品种一致性差")
|
||||
if abs(r["ic"]) < 0.005:
|
||||
issues.append(f"IC≈0({r['ic']:.4f}),预测力存疑")
|
||||
if issues:
|
||||
print(f" [{i+1}] {decode(r['formula'])[:50]}")
|
||||
for iss in issues:
|
||||
print(f" ⚠ {iss}")
|
||||
else:
|
||||
print(f" [{i+1}] ✓ 无明显问题: {decode(r['formula'])[:55]}")
|
||||
|
||||
print()
|
||||
# 保存
|
||||
report_path = "backtest_output/elite_backtest.json"
|
||||
Path("backtest_output").mkdir(exist_ok=True)
|
||||
summary = []
|
||||
for r in results:
|
||||
summary.append({
|
||||
"formula": r["formula"],
|
||||
"readable": r["readable"],
|
||||
"port_total_ret": round(r["port_total_ret"], 4),
|
||||
"port_sharpe": round(r["port_sharpe"], 4),
|
||||
"port_sortino": round(r["port_sortino"], 4),
|
||||
"port_mdd": round(r["port_mdd"], 4),
|
||||
"ic": round(r["ic"], 5),
|
||||
"half1_sharpe": round(r["half1_sharpe"], 4),
|
||||
"half2_sharpe": round(r["half2_sharpe"], 4),
|
||||
"avg_hold_h": round(r["avg_hold_h"], 1),
|
||||
})
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(summary, f, indent=2, ensure_ascii=False)
|
||||
print(f"报告已保存 → {report_path}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -221,7 +221,9 @@ class LoopedTransformer(nn.Module):
|
||||
class AlphaGPT(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.d_model = 64
|
||||
# d_model 64→96:vocab 剪枝后为 ~94,96 维 embedding 足以支撑,
|
||||
# 兼顾容量与 CPU 自回归采样的速度(128 维每步耗时过高)。
|
||||
self.d_model = 96
|
||||
self.features_list = list(FORMULA_VOCAB.feature_names)
|
||||
self.ops_list = list(FORMULA_VOCAB.operator_names)
|
||||
|
||||
@@ -239,11 +241,14 @@ class AlphaGPT(nn.Module):
|
||||
self.pos_emb = nn.Parameter(torch.zeros(1, _POS_EMB_MAX, self.d_model))
|
||||
|
||||
# Enhanced Transformer with Looped Transformer
|
||||
# num_layers 2→3、dim_feedforward 128→192:配合 d_model=96 适度扩容。
|
||||
# 4 层 looped(×3 loops) 在 CPU 自回归采样下每步耗时过高,取 3 层平衡。
|
||||
# nhead=4 → head_dim=24。
|
||||
self.blocks = LoopedTransformer(
|
||||
d_model=self.d_model,
|
||||
nhead=4,
|
||||
num_layers=2,
|
||||
dim_feedforward=128,
|
||||
num_layers=3,
|
||||
dim_feedforward=192,
|
||||
num_loops=3,
|
||||
dropout=0.1
|
||||
)
|
||||
|
||||
+28
-18
@@ -4,6 +4,8 @@ model_core/config.py — 模型层配置
|
||||
仅保留模型训练所需的参数。
|
||||
品种、数据、风控等全局配置统一由根目录 config.py 的 Config 类管理。
|
||||
"""
|
||||
import math
|
||||
|
||||
import torch
|
||||
from .vocab import FORMULA_VOCAB
|
||||
|
||||
@@ -26,15 +28,17 @@ class ModelConfig:
|
||||
# 若后续改为批量并行公式评估(一次喂大批张量进 GPU),再切回 cuda。
|
||||
DEVICE = torch.device("cpu")
|
||||
|
||||
# ── 训练参数(阶段 A:找简单稳定公式)────────────────────────────────
|
||||
# 阶段 A(当前):MAX_FORMULA_LEN=8,TRAIN_STEPS=300
|
||||
# 目标:找简单、稳定、低换手的基础公式,防过拟合
|
||||
# 阶段 B(完成阶段A后切换):
|
||||
# MAX_FORMULA_LEN=14,TRAIN_STEPS=500,ELITE_REPLAY_FRAC=0.35
|
||||
# 目标:围绕好公式附近做组合增强
|
||||
BATCH_SIZE = 128
|
||||
TRAIN_STEPS = 3000 # 每品种训练步数(多因子模式下每品种独立跑)
|
||||
MAX_FORMULA_LEN = 8 # 阶段B改为 14
|
||||
# ── 训练参数(大搜索空间适配版,2026-07-04 重构)─────────────────────
|
||||
# 背景:特征库扩展到 65、算子库扩展到 66(vocab=131),8-token 搜索空间
|
||||
# 从旧版 ~7亿 暴增到 ~8.67×10^16(1.2 亿倍)。旧的采样预算(128×3000)
|
||||
# 覆盖率趋近于零,导致熵坍塌 Early Stop、公式退化。
|
||||
# 对策(训练时间不敏感场景):
|
||||
# 1. 特征剪枝(active_features.json)把 vocab 降到 ~90,空间缩小约 20 倍
|
||||
# 2. 放大采样预算:BATCH_SIZE 128→256,TRAIN_STEPS 3000→8000
|
||||
# 3. 更大精英池(60)保留更多历史最优
|
||||
BATCH_SIZE = 192 # 每步采样公式数(原 128,1.5x 提升覆盖率)
|
||||
TRAIN_STEPS = 5000 # 每组训练步数(原 3000)
|
||||
MAX_FORMULA_LEN = 8 # 公式长度上限(暂保持 8,防过拟合)
|
||||
|
||||
# ── 特征维度(由 vocab.py 自动派生,无需手动修改)──────────────────
|
||||
INPUT_DIM: int = FORMULA_VOCAB.feature_count # == 10
|
||||
@@ -45,20 +49,26 @@ class ModelConfig:
|
||||
IC_GATE_MULT: float = 1.15
|
||||
IC_NEG_MULT: float = 0.85
|
||||
|
||||
# ── 熵保护 ─────────────────────────────────────────────────────────
|
||||
ENTROPY_COEFF_MAX: float = 0.50
|
||||
# ── 熵保护(大空间加强版)──────────────────────────────────────────
|
||||
# ENTROPY_COEFF_MAX 0.5→1.0:加倍探索压力,对抗大 vocab 的过早收敛。
|
||||
# ENTROPY_COLLAPSE_THRESH 改为相对阈值 0.15×ln(vocab):大 vocab 最大熵更高
|
||||
# (ln(131)≈4.87 vs ln(54)≈3.99),绝对阈值 0.5 不再合理。
|
||||
# ENTROPY_COLLAPSE_STEPS 15→40:给模型更长的自我恢复窗口,不急于重启。
|
||||
ENTROPY_COEFF_MAX: float = 1.0
|
||||
ENTROPY_COEFF_POWER: float = 1.3
|
||||
ENTROPY_COLLAPSE_THRESH: float = 0.5
|
||||
ENTROPY_COLLAPSE_STEPS: int = 15
|
||||
ENTROPY_COLLAPSE_THRESH: float = 0.15 * math.log(FORMULA_VOCAB.size)
|
||||
ENTROPY_COLLAPSE_STEPS: int = 40
|
||||
|
||||
# ── Elite Replay ──────────────────────────────────────────────────
|
||||
ELITE_REPLAY_FRAC: float = 0.25 # 阶段A;阶段B改为 0.35
|
||||
ELITE_POOL_SIZE: int = 30
|
||||
ELITE_REPLAY_FRAC: float = 0.25
|
||||
ELITE_POOL_SIZE: int = 60 # 30→60:大空间需要更大的精英记忆
|
||||
ELITE_REWARD_SCALE: float = 1.2
|
||||
|
||||
# ── 坍塌重启 ───────────────────────────────────────────────────────
|
||||
MAX_RESTARTS: int = 8
|
||||
RESTART_NOISE: float = 0.05
|
||||
# ── 坍塌重启(大空间加强版)─────────────────────────────────────────
|
||||
# MAX_RESTARTS 8→25、RESTART_NOISE 0.05→0.1:时间不敏感,多给机会+更强扰动。
|
||||
# 配合 engine.py:超过 MAX_RESTARTS 后不再 Early Stop,改为强扰动继续训练。
|
||||
MAX_RESTARTS: int = 25
|
||||
RESTART_NOISE: float = 0.1
|
||||
|
||||
# ── 因子去相关参数 ────────────────────────────────────────────────
|
||||
FACTOR_TOP_K: int = 10
|
||||
|
||||
+13
-3
@@ -624,9 +624,19 @@ class AlphaEngine:
|
||||
f"全参数扰动(σ={noise})")
|
||||
self.opt = torch.optim.AdamW(self.model.parameters(), lr=1e-3)
|
||||
else:
|
||||
tqdm.write(f"[Early Stop] 已重启{max_r}次仍坍塌,"
|
||||
f"H={ent_val:.3f},终止。")
|
||||
break
|
||||
# 训练时间不敏感:超过重启上限后不再 Early Stop 终止,
|
||||
# 改为「全参数强扰动 + 重置流计数」继续探索,直到跑满 TRAIN_STEPS。
|
||||
# 从 best_snapshot 恢复(若有)以保住已发现的最优结构,再加大扰动。
|
||||
low_entropy_streak = 0
|
||||
hard_noise = ModelConfig.RESTART_NOISE * 2.0
|
||||
if self._best_snapshot is not None:
|
||||
self.model.load_state_dict(self._best_snapshot)
|
||||
with torch.no_grad():
|
||||
for p in self.model.parameters():
|
||||
p.add_(torch.randn_like(p) * hard_noise)
|
||||
self.opt = torch.optim.AdamW(self.model.parameters(), lr=1e-3)
|
||||
tqdm.write(f"[Hard Restart] 已重启{max_r}次仍坍塌,"
|
||||
f"H={ent_val:.3f},强扰动(σ={hard_noise})继续,不终止。")
|
||||
|
||||
# ── End of training ──────────────────────────────────────────
|
||||
if self.best_formula is not None:
|
||||
|
||||
@@ -1422,7 +1422,30 @@ _FEATURE_DEFS = [
|
||||
("CS_ZSCORE_RET20", "cross_sectional", _fe._c_cs_zscore_ret20),
|
||||
]
|
||||
|
||||
# ── 激活特征白名单(特征剪枝机制,2026-07-04)──────────────────────────
|
||||
# 若项目根目录存在 active_features.json(由 prune_features.py 生成),
|
||||
# 则只注册白名单内的特征,缩小 vocab 与搜索空间;否则注册全部 65 个特征。
|
||||
# 白名单按 _FEATURE_DEFS 原始顺序过滤,保持特征维顺序稳定。
|
||||
def _load_active_feature_allowlist() -> set[str] | None:
|
||||
import json as _json
|
||||
import pathlib as _pathlib
|
||||
_path = _pathlib.Path(__file__).resolve().parent.parent / "active_features.json"
|
||||
if not _path.exists():
|
||||
return None
|
||||
try:
|
||||
data = _json.loads(_path.read_text(encoding="utf-8"))
|
||||
names = data.get("active_features") if isinstance(data, dict) else data
|
||||
allow = {str(n) for n in names}
|
||||
return allow or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
_ACTIVE_FEATURES = _load_active_feature_allowlist()
|
||||
|
||||
for _name, _category, _compute in _FEATURE_DEFS:
|
||||
if _ACTIVE_FEATURES is not None and _name not in _ACTIVE_FEATURES:
|
||||
continue
|
||||
FEATURE_REGISTRY.register_feature(
|
||||
FeatureSpec(name=_name, category=_category, compute=_compute)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
prune_features.py — 特征剪枝:从 65 个特征中筛出有区分度、低冗余的子集
|
||||
|
||||
背景:
|
||||
特征库扩展到 65 个后,vocab=131,8-token 搜索空间暴增到 ~8.67×10^16。
|
||||
其中大量特征高度相关(多周期均线/动量/通道位置类),是噪声维度。
|
||||
|
||||
做法(用 model_core.evaluator):
|
||||
1. 离线加载全部品种数据,直接从 _FEATURE_DEFS 计算全部 65 个特征
|
||||
2. score_all:对每个特征算 IC / RankIC / 互信息,跨特征秩归一聚合 importance
|
||||
3. prune:保守双条件相关性剪枝(corr>阈值 且 分差>margin 才剪)
|
||||
4. 额外按 importance 取 Top-K,去掉近零 IC 的弱特征
|
||||
5. 写出 active_features.json(features.py 启动时读取,只注册白名单特征)
|
||||
|
||||
用法:
|
||||
python prune_features.py # 默认 corr_threshold=0.85, top_k=28
|
||||
python prune_features.py --top-k 25 # 自定义保留数量
|
||||
python prune_features.py --corr 0.9 # 自定义相关性阈值
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import Config
|
||||
from data_pipeline.data_manager import MT5DataManager
|
||||
from data_pipeline.fetcher import MT5DataFetcher
|
||||
from model_core.features import _FEATURE_DEFS
|
||||
from model_core.evaluator import score_all, prune
|
||||
|
||||
OUTPUT = Path(__file__).parent / "active_features.json"
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--top-k", type=int, default=28,
|
||||
help="剪枝后额外按 importance 截断保留的最大特征数")
|
||||
ap.add_argument("--corr", type=float, default=0.85,
|
||||
help="相关性剪枝阈值(越低剪得越狠)")
|
||||
ap.add_argument("--margin", type=float, default=0.01,
|
||||
help="剪枝分差阈值(势均力敌时保留)")
|
||||
args = ap.parse_args()
|
||||
|
||||
print(f"{'='*62}")
|
||||
print(f" 特征剪枝 corr_threshold={args.corr} top_k={args.top_k}")
|
||||
print(f"{'='*62}")
|
||||
|
||||
# ── 1. 离线加载数据 ───────────────────────────────────────────────
|
||||
print("加载数据(离线缓存)...")
|
||||
with MT5DataFetcher(offline=True) as fetcher:
|
||||
mgr = MT5DataManager(fetcher)
|
||||
mgr.load()
|
||||
raw_dict = mgr.raw_dict
|
||||
target = mgr.target_ret # [N, T]
|
||||
syms = mgr.symbols
|
||||
N, T = target.shape
|
||||
print(f" 品种={syms} N={N} T={T}")
|
||||
|
||||
# ── 2. 直接从 _FEATURE_DEFS 计算全部 65 个特征(绕过白名单)───────
|
||||
print(f"\n计算全部 {len(_FEATURE_DEFS)} 个特征...")
|
||||
candidates: dict[str, torch.Tensor] = {}
|
||||
categories: dict[str, str] = {}
|
||||
for name, category, compute in _FEATURE_DEFS:
|
||||
try:
|
||||
series = compute(raw_dict) # [N, T]
|
||||
candidates[name] = torch.nan_to_num(series, nan=0.0,
|
||||
posinf=0.0, neginf=0.0)
|
||||
categories[name] = category
|
||||
except Exception as e:
|
||||
print(f" [跳过] {name}: 计算失败 {e}")
|
||||
|
||||
# ── 3. 打分 ───────────────────────────────────────────────────────
|
||||
print(f"\n对 {len(candidates)} 个特征打分(IC / RankIC / MI)...")
|
||||
scores = score_all(candidates, target, categories=categories)
|
||||
scores_sorted = sorted(scores, key=lambda s: (
|
||||
-s.importance_score if s.importance_score == s.importance_score else 1,
|
||||
))
|
||||
|
||||
print(f"\n {'特征':22s}{'类别':14s}{'IC':>8}{'RankIC':>8}{'MI':>8}{'importance':>12}")
|
||||
print(f" {'-'*70}")
|
||||
for s in scores_sorted:
|
||||
imp = s.importance_score
|
||||
imp_str = f"{imp:.4f}" if imp == imp and imp > -1e30 else " 退化"
|
||||
print(f" {s.candidate:22s}{s.category:14s}"
|
||||
f"{s.ic:+8.4f}{s.rank_ic:+8.4f}{s.mi:8.4f}{imp_str:>12}")
|
||||
|
||||
# ── 4. 相关性剪枝 ─────────────────────────────────────────────────
|
||||
print(f"\n相关性剪枝(corr>{args.corr} 且分差>{args.margin})...")
|
||||
rows = prune(scores, candidates, corr_threshold=args.corr, margin=args.margin)
|
||||
retained = [r for r in rows if r.retention_status == "retained"]
|
||||
pruned = [r for r in rows if r.retention_status == "pruned"]
|
||||
print(f" 相关性剪枝后保留 {len(retained)} 个,剪掉 {len(pruned)} 个")
|
||||
for r in pruned:
|
||||
print(f" ✗ {r.candidate:22s} (favor of {r.pruned_in_favor_of})")
|
||||
|
||||
# ── 5. 按 importance 取 Top-K(去掉弱特征)────────────────────────
|
||||
retained_sorted = sorted(
|
||||
retained,
|
||||
key=lambda r: (r.importance_score if r.importance_score == r.importance_score
|
||||
and r.importance_score > -1e30 else -1e30),
|
||||
reverse=True,
|
||||
)
|
||||
final = retained_sorted[:args.top_k]
|
||||
final_names_set = {r.candidate for r in final}
|
||||
|
||||
# 保持 _FEATURE_DEFS 原始顺序输出
|
||||
ordered_names = [name for name, _, _ in _FEATURE_DEFS if name in final_names_set]
|
||||
|
||||
print(f"\n{'='*62}")
|
||||
print(f" 最终保留 {len(ordered_names)} 个特征(vocab 从 65 特征降至 {len(ordered_names)})")
|
||||
print(f"{'='*62}")
|
||||
for n in ordered_names:
|
||||
print(f" ✓ {n}")
|
||||
|
||||
# ── 6. 写出 active_features.json ─────────────────────────────────
|
||||
payload = {
|
||||
"active_features": ordered_names,
|
||||
"meta": {
|
||||
"source": "prune_features.py",
|
||||
"corr_threshold": args.corr,
|
||||
"top_k": args.top_k,
|
||||
"n_original": len(_FEATURE_DEFS),
|
||||
"n_retained": len(ordered_names),
|
||||
"symbols": syms,
|
||||
"bars": T,
|
||||
},
|
||||
}
|
||||
OUTPUT.write_text(json.dumps(payload, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8")
|
||||
print(f"\n已写出 → {OUTPUT}")
|
||||
print("重新 import model_core.features 时将只注册白名单特征。\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,215 @@
|
||||
# 已验证有效因子记录
|
||||
|
||||
> 生成时间:2026-07-04
|
||||
> 训练日期:2026-07-03 ~ 2026-07-04(隔夜训练)
|
||||
> 训练模式:分组训练(forex / metals_comm / index 三组),CPU
|
||||
> 训练步数:目标 3000 步,实际因熵坍塌 Early Stop(forex=1207, metals_comm=845, index=913)
|
||||
> 回测模式:离线缓存(D:\K线数据),T=3498 bars H1(10品种时间轴对齐)
|
||||
> 数据周期:约 1.5 年历史数据
|
||||
> vocab_version:v6ff4c52e30c1
|
||||
|
||||
---
|
||||
|
||||
## 一、有效性判定标准
|
||||
|
||||
本次记录以如下标准判定"有效":
|
||||
|
||||
- **✅ 确定有效**:Sharpe > 1 且 PnL > 0,回测结果稳健
|
||||
- **⚠️ 部分有效**:同组内部分品种有效,存在泛化问题
|
||||
- **❌ 无效**:整体亏损或零交易
|
||||
|
||||
---
|
||||
|
||||
## 二、本次回测汇总(2026-07-04)
|
||||
|
||||
| 品种 | 组 | PnL | Sharpe | Sortino | MaxDD | Calmar | 交易数 | 胜率 | 平均持仓 | 判定 |
|
||||
|------|-----|-----|--------|---------|-------|--------|--------|------|----------|------|
|
||||
| EURUSD | forex | -0.019 | -0.29 | -0.37 | 0.066 | -0.51 | 288 | 50.0% | 12.1h | ❌ |
|
||||
| USDJPY | forex | +0.103 | +1.32 | +1.78 | 0.097 | +1.90 | 286 | 51.7% | 12.2h | ✅ |
|
||||
| XAUUSD | metals_comm | -0.376 | -1.64 | -2.11 | 0.671 | -1.00 | 5 | 40.0% | 699h | ❌ |
|
||||
| AAVUSD | metals_comm | +1.318 | +1.57 | +2.16 | 1.031 | +2.28 | 5 | 100% | 699h | ⚠️ |
|
||||
| COCOA.c | metals_comm | +0.800 | +1.71 | +2.51 | 0.618 | +2.31 | 3 | 100% | 1166h | ⚠️ |
|
||||
| US30.cash | index | 0.000 | 0.00 | 0.00 | 0.000 | 0.00 | 0 | — | — | ❌ |
|
||||
| US100.cash | index | +0.300 | +1.47 | +1.75 | 0.296 | +1.81 | 3 | 100% | 1166h | ⚠️ |
|
||||
| US500.cash | index | +0.201 | +1.26 | +1.48 | 0.236 | +1.52 | 3 | 100% | 1166h | ⚠️ |
|
||||
| US2000.cash | index | -0.253 | -1.15 | -1.41 | 0.568 | -0.79 | 3 | 66.7% | 1166h | ❌ |
|
||||
| JP225.cash | index | +0.563 | +2.40 | +3.01 | 0.226 | +4.45 | 3 | 66.7% | 1166h | ⚠️ |
|
||||
| **Portfolio** | — | **+0.264** | **+2.84** | **+4.22** | **0.088** | **+5.37** | — | — | — | ✅ |
|
||||
|
||||
正收益品种:6/10 | Sharpe > 1 品种:6/10
|
||||
|
||||
---
|
||||
|
||||
## 三、各组公式详情
|
||||
|
||||
### 3.1 Forex 组(⚠️ 部分有效)
|
||||
|
||||
**公式**
|
||||
```
|
||||
RS_VOL -> DONCHIAN_POS_20 -> TS_STD_10 -> TS_RANK_5 -> TS_CORR_10 -> TANH_SQUASH -> MOMENTUM_10 -> TANH_SQUASH
|
||||
```
|
||||
Token 序列:`[40, 51, 81, 83, 86, 125, 88, 125]`
|
||||
|
||||
**训练结果**:Best Score = 6.636,Early Stop(step 1207/3000,熵坍塌触发上限)
|
||||
|
||||
**回测表现**:
|
||||
- USDJPY:✅ Sharpe +1.32,Sortino +1.78,288 笔,胜率 51.7%,平均持仓 12h
|
||||
- EURUSD:❌ Sharpe -0.29,小幅亏损
|
||||
|
||||
**公式逻辑**:
|
||||
1. `RS_VOL`:相对波动率(品种波动率 / 截面均值),识别当前品种是否处于高波动状态
|
||||
2. `DONCHIAN_POS_20`:价格在 20 期唐奇安通道内的相对位置,值域 [0,1],近期高点附近=1,低点附近=0
|
||||
3. `TS_STD_10`:10 期时序标准差,衡量近期信号的变化幅度
|
||||
4. `TS_RANK_5`:5 期时序排名,捕捉短期动量方向
|
||||
5. `TS_CORR_10`:10 期相关系数,与截面均值的相关性
|
||||
6. `TANH_SQUASH`:将值压缩到 (-1, 1),防止极端值
|
||||
7. `MOMENTUM_10`:10 期动量(对数收益率差)
|
||||
8. `TANH_SQUASH`:再次压缩,最终输出平滑信号
|
||||
|
||||
**本质**:波动率感知 + 通道位置 + 短期动量的组合信号,双重 tanh 压缩平滑。
|
||||
|
||||
**与历史 forex 公式对比**:
|
||||
| 维度 | 历史公式(2026-07-03) | 本次公式(2026-07-04) |
|
||||
|------|----------------------|----------------------|
|
||||
| 公式 | `MA_DIFF -> TS_MIN_10 -> NEG -> ...` | `RS_VOL -> DONCHIAN_POS_20 -> ...` |
|
||||
| EURUSD Sharpe | +1.82 | **-0.29(退步)** |
|
||||
| USDJPY Sharpe | +1.63 | +1.32(相近) |
|
||||
| 交易频率 | 83/77 笔 | 288/286 笔(高频化) |
|
||||
| 持仓时长 | 57-62h | **12h(大幅缩短)** |
|
||||
|
||||
⚠️ **结论**:本次公式 USDJPY 仍有效,但 EURUSD 失效,高频化(12h 持仓)可能导致被点差侵蚀。历史 forex 公式(四重平滑)总体优于本次,建议继续沿用历史公式。
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Metals_comm 组(⚠️ 分化严重)
|
||||
|
||||
**公式**
|
||||
```
|
||||
STOCH_K_14 -> KELTNER_POS_20 -> SUB -> WILLR_14 -> KELTNER_POS_20 -> DMI_DIFF_14 -> LT -> GATE
|
||||
```
|
||||
Token 序列:`[45, 52, 66, 26, 52, 49, 127, 72]`
|
||||
|
||||
**训练结果**:Best Score = 8.183,Early Stop(step 845/3000)
|
||||
|
||||
**回测表现**:
|
||||
- AAVUSD:⚠️ Sharpe +1.57,5 笔交易,100% 胜率,持仓 699h(~29天)
|
||||
- COCOA.c:⚠️ Sharpe +1.71,3 笔交易,100% 胜率,持仓 1166h(~49天)
|
||||
- XAUUSD:❌ Sharpe -1.64,大幅亏损
|
||||
|
||||
**公式逻辑(RPN 执行)**:
|
||||
```
|
||||
[1] PUSH STOCH_K_14 → 随机指标K线(超买超卖)
|
||||
[2] KELTNER_POS_20(STOCH_K) → 凯尔特纳通道位置
|
||||
[3] PUSH WILLR_14 → 威廉指标(值域 [-1,0])
|
||||
[4] KELTNER_POS_20(WILLR_14) → 威廉指标的通道位置
|
||||
[5] PUSH DMI_DIFF_14 → 方向运动指标差值(+DI - -DI)
|
||||
[6] LT(WILLR_keltner, DMI) → 二元比较(WILLR通道位置 < DMI差值?)
|
||||
[7] SUB(STOCH_keltner, WILLR_keltner) → 差值
|
||||
GATE(cond=SUB, x=LT, y=?) → 条件选择
|
||||
```
|
||||
|
||||
> 注:实际 RPN 栈执行顺序较复杂,GATE 弹出最近 3 个值。
|
||||
|
||||
**本质**:技术指标组合(随机指标 + 威廉指标 + 方向运动指标)的阈值突破条件门控策略。
|
||||
|
||||
**分化原因**:
|
||||
- XAUUSD 亏损而 AAVUSD/COCOA.c 盈利,说明公式对黄金的价格结构不适配
|
||||
- 交易次数极少(3~5 笔),100% 胜率在小样本下统计意义不大
|
||||
- 持仓 700~1200 小时实为超长期趋势跟踪,不是真正的"信号策略"
|
||||
- **⚠️ 小样本警告**:3~5 笔交易不能作为有效性的可靠证据
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Index 组(⚠️ 部分有效,泛化不足)
|
||||
|
||||
**公式**
|
||||
```
|
||||
VOL_REGIME -> TS_MAX_10 -> DELTA -> TS_MIN_10 -> PRESSURE -> MOMENTUM_5 -> WILLR_14 -> GATE
|
||||
```
|
||||
Token 序列:`[8, 89, 99, 90, 12, 87, 26, 72]`
|
||||
|
||||
**训练结果**:Best Score = 8.094,Early Stop(step 913/3000)
|
||||
|
||||
**回测表现**:
|
||||
- JP225.cash:⚠️ Sharpe +2.40,3 笔,66.7% 胜率,持仓 1166h
|
||||
- US100.cash:⚠️ Sharpe +1.47,3 笔,100% 胜率,持仓 1166h
|
||||
- US500.cash:⚠️ Sharpe +1.26,3 笔,100% 胜率,持仓 1166h
|
||||
- US2000.cash:❌ Sharpe -1.15,亏损
|
||||
- US30.cash:❌ **零交易**(见下方根因分析)
|
||||
|
||||
**US30.cash 零交易根因**:
|
||||
|
||||
GATE 的 condition = `TS_MIN_10(DELTA(TS_MAX_10(VOL_REGIME)))` 要 > 0,要求:
|
||||
> 过去 10 根 bar 内,VOL_REGIME 的 10 期滚动最大值**全部严格递增**
|
||||
|
||||
这是极苛刻的条件。大部分时间 DELTA=0(滚动最大在台阶期不变),导致 condition ≤ 0,GATE 全程选择 y 分支 = `WILLR_14`。
|
||||
|
||||
`WILLR_14` 值域 [-1, 0](永远非正),截面 z-score 归一化后 US30 的 WILLR_14 变化幅度不足,归一化后长期落在 neutral band (-0.3, 0) 内,始终不触发 ±0.3 的入场阈值。US100/US500 的波动率结构恰好在几个时点突破了该区间,产生了 3 笔交易。
|
||||
|
||||
**本质**:US30 数据无问题,是公式在该品种特征分布上泛化失败。
|
||||
|
||||
**⚠️ 小样本警告**:JP225/US100/US500 各仅 3 笔交易(其中 2 笔重叠),100% 胜率不可靠。
|
||||
|
||||
---
|
||||
|
||||
## 四、综合结论
|
||||
|
||||
### 4.1 跨时间段验证汇总
|
||||
|
||||
| 公式组 | 2026-07-03 回测 | 2026-07-04 回测 | 综合判定 |
|
||||
|--------|----------------|----------------|---------|
|
||||
| forex(旧公式) | EURUSD +1.82, USDJPY +1.63 | — | ✅ **确定有效** |
|
||||
| forex(本次公式) | — | USDJPY +1.32, EURUSD -0.29 | ⚠️ **部分有效** |
|
||||
| metals_comm(旧公式) | COCOA.c +2.54 | — | ⚠️ 不稳定 |
|
||||
| metals_comm(本次公式) | — | AAVUSD +1.57, COCOA.c +1.71, XAUUSD -1.64 | ⚠️ 分化严重 |
|
||||
| index(旧公式) | 全亏 | — | ❌ 无效 |
|
||||
| index(本次公式) | — | JP225 +2.40, US100 +1.47, US500 +1.26, US30 零交易 | ⚠️ 部分有效 |
|
||||
|
||||
### 4.2 唯一确定有效的公式
|
||||
|
||||
**2026-07-03 Forex 公式**(历史最优,两次回测验证):
|
||||
```
|
||||
MA_DIFF -> TS_MIN_10 -> NEG -> TS_MAX_10 -> TS_MEAN_10 -> TS_MEAN_20 -> TS_MEAN_10 -> TS_MEAN_20
|
||||
```
|
||||
Token:`[3, 45, 24, 44, 33, 34, 33, 34]`(vocab_version: 2.0)
|
||||
|
||||
- EURUSD:Sharpe +1.82,Sortino +2.41,MaxDD 0.071,83 笔,胜率 57.8%
|
||||
- USDJPY:Sharpe +1.63,Sortino +2.08,MaxDD 0.097,77 笔,胜率 57.1%
|
||||
- 两次回测均正收益,跨品种一致,逻辑可解释
|
||||
|
||||
### 4.3 本次训练的主要问题
|
||||
|
||||
1. **Early Stop 过早**:三组均在 28%~40% 进度时因熵坍塌退出,未达到 3000 步目标
|
||||
2. **index/metals_comm 组交易数极少**(3~5 笔),持仓超长(700~1200h),公式退化为稀疏趋势信号,统计可靠性极低
|
||||
3. **US30.cash 零交易**:公式泛化失败,条件链在该品种上永不激活
|
||||
4. **XAUUSD 亏损**:metals_comm 组共用公式,在黄金上反向运作
|
||||
|
||||
---
|
||||
|
||||
## 五、下一步建议
|
||||
|
||||
| 优先级 | 行动 | 说明 |
|
||||
|--------|------|------|
|
||||
| P0 | 沿用 forex 旧公式(2026-07-03)用于 EURUSD/USDJPY | 唯一经过两次独立验证的有效因子 |
|
||||
| P1 | 解决 Early Stop 问题后重训 index 组 | 调大 `ENTROPY_COLLAPSE_STEPS`(15→30)或 `MAX_RESTARTS`(8→15)|
|
||||
| P1 | index 组改为品种独立训练 | `--single US30.cash` 等,避免共用公式掩盖品种特异性 |
|
||||
| P2 | metals_comm 公式需更多数据验证 | 3~5 笔样本量不足,需至少 30 笔以上才可信 |
|
||||
| P3 | 清理 vocab_version 不匹配问题 | 旧公式 vocab_version=2.0,新公式=v6ff4c52e30c1,需统一 |
|
||||
|
||||
---
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `strategies/best_group_forex.json` | 本次 forex 组公式(score=6.636) |
|
||||
| `strategies/best_group_metals_comm.json` | 本次 metals_comm 组公式(score=8.183) |
|
||||
| `strategies/best_group_index.json` | 本次 index 组公式(score=8.094) |
|
||||
| `strategies/best_EURUSD.json` 等各品种 | 本次各品种策略文件 |
|
||||
| `backtest_output/multi_factor_report.json` | 本次完整回测 JSON 报告 |
|
||||
| `backtest_output/portfolio_equity.png` | 本次组合资金曲线图 |
|
||||
| `strategies/FOREX_FACTOR_REPORT.md` | 历史 forex 组分析报告(2026-07-03) |
|
||||
| `strategies/EFFECTIVE_FACTORS_ANALYSIS.md` | 历史综合因子分析(2026-07-03) |
|
||||
| `training_history_forex.json` | 本次 forex 训练历史(1207 步) |
|
||||
| `training_history_index.json` | 本次 index 训练历史(913 步) |
|
||||
| `training_history_metals_comm.json` | 本次 metals_comm 训练历史(845 步) |
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"vocab_version": "v6ff4c52e30c1",
|
||||
"symbol": "EURUSD",
|
||||
"group": "forex",
|
||||
"formula": [
|
||||
3,
|
||||
23,
|
||||
110,
|
||||
42,
|
||||
53,
|
||||
104,
|
||||
72,
|
||||
109
|
||||
],
|
||||
"best_score": 5.9763946533203125,
|
||||
"source": "group_forex"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"vocab_version": "v6ff4c52e30c1",
|
||||
"symbol": "USDJPY",
|
||||
"group": "forex",
|
||||
"formula": [
|
||||
3,
|
||||
23,
|
||||
110,
|
||||
42,
|
||||
53,
|
||||
104,
|
||||
72,
|
||||
109
|
||||
],
|
||||
"best_score": 5.9763946533203125,
|
||||
"source": "group_forex"
|
||||
}
|
||||
+10
-10
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"vocab_version": "v6ff4c52e30c1",
|
||||
"vocab_version": "v61d94be3d0e5",
|
||||
"symbol": "forex",
|
||||
"formula": [
|
||||
40,
|
||||
51,
|
||||
81,
|
||||
83,
|
||||
86,
|
||||
125,
|
||||
88,
|
||||
125
|
||||
16,
|
||||
77,
|
||||
77,
|
||||
23,
|
||||
75,
|
||||
77,
|
||||
71,
|
||||
78
|
||||
],
|
||||
"best_score": 6.635913372039795
|
||||
"best_score": 5.444646835327148
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"vocab_version": "v6ff4c52e30c1",
|
||||
"group": "forex",
|
||||
"symbols": [
|
||||
"EURUSD",
|
||||
"USDJPY"
|
||||
],
|
||||
"formula": [
|
||||
3,
|
||||
23,
|
||||
110,
|
||||
42,
|
||||
53,
|
||||
104,
|
||||
72,
|
||||
109
|
||||
],
|
||||
"best_score": 5.9763946533203125
|
||||
}
|
||||
Reference in New Issue
Block a user