188 lines
6.4 KiB
Python
188 lines
6.4 KiB
Python
"""
|
||
run_backtest_new_account.py — 用新账号品种名跑旧因子回测
|
||
|
||
新账号品种映射:
|
||
EURUSDm → EURUSD
|
||
USDJPYm → USDJPY
|
||
XAUUSDm → XAUUSD
|
||
USTECm → US100.cash
|
||
US500m → US500.cash
|
||
"""
|
||
import json, sys, math
|
||
from pathlib import Path
|
||
import numpy as np
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
||
import MetaTrader5 as mt5
|
||
import pandas as pd
|
||
import torch
|
||
|
||
from model_core.vm import StackVM
|
||
from model_core.features import MT5FeatureEngineer
|
||
from model_core.vocab import FORMULA_VOCAB
|
||
from strategy_manager.signal import compute_target_positions_stateless
|
||
from backtest_viz import BacktestEngine, BacktestChart
|
||
|
||
_H1_PER_YEAR = 6240
|
||
|
||
# ── 品种映射(旧名 → 新名)────────────────────────────────────────────
|
||
SYMBOL_MAP = {
|
||
"EURUSDm": "EURUSD",
|
||
"USDJPYm": "USDJPY",
|
||
"XAUUSDm": "XAUUSD",
|
||
"USTECm": "US100.cash",
|
||
"US500m": "US500.cash",
|
||
}
|
||
|
||
# ── 要测试的因子(旧名 → formula tokens)────────────────────────────
|
||
STRATEGIES = {
|
||
"v1_all": {"formula": [6, 44, 3, 39, 4, 29, 34, 27],
|
||
"name": "v1 通用(均线差排名)",
|
||
"symbols": ["EURUSDm","USDJPYm","XAUUSDm","USTECm","US500m"]},
|
||
"us500_v2": {"formula": [5, 4, 28, 16, 23, 47, 18, 27],
|
||
"name": "US500 专属(ATR斜率/量价)",
|
||
"symbols": ["US500m"]},
|
||
"xauusd_v1": {"formula": [6, 44, 3, 39, 4, 29, 34, 27],
|
||
"name": "XAU 专属(v1)",
|
||
"symbols": ["XAUUSDm"]},
|
||
}
|
||
|
||
def sharpe(p): m=p.mean(); s=p.std(); return float(m/s*math.sqrt(_H1_PER_YEAR)) if s>1e-10 else 0
|
||
def sortino(p):
|
||
m=p.mean(); d=p[p<0]
|
||
ds=max(d.std() if len(d)>0 else 1e-10, abs(m), 1e-10)
|
||
return float(np.clip(m/ds*math.sqrt(_H1_PER_YEAR), -20, 20))
|
||
def mdd(c): return float((np.maximum.accumulate(c)-c).max()) if len(c)>0 else 0
|
||
|
||
def fetch(new_sym, bars=12000):
|
||
r = mt5.copy_rates_from_pos(new_sym, 16385, 0, bars)
|
||
if r is None or len(r) == 0:
|
||
return None
|
||
return pd.DataFrame(r)[["time","open","high","low","close","tick_volume"]]
|
||
|
||
def df_to_raw(df):
|
||
t = torch.tensor(df[["open","high","low","close","tick_volume"]].values.T, dtype=torch.float32)
|
||
raw = {
|
||
"open": t[0:1],
|
||
"high": t[1:2],
|
||
"low": t[2:3],
|
||
"close": t[3:4],
|
||
"volume": t[4:5],
|
||
"time": torch.tensor(df["time"].values[None], dtype=torch.int64),
|
||
}
|
||
return raw
|
||
|
||
names = FORMULA_VOCAB.token_names
|
||
|
||
def main():
|
||
mt5.initialize()
|
||
print("\n获取实时点差...")
|
||
costs = {}
|
||
for old, new in SYMBOL_MAP.items():
|
||
tick = mt5.symbol_info_tick(new)
|
||
if tick and tick.ask > 0:
|
||
mid = (tick.ask + tick.bid) / 2
|
||
costs[old] = (tick.ask - tick.bid) / mid / 2
|
||
else:
|
||
costs[old] = 0.0001
|
||
print(f" {old}({new}): cost={costs[old]:.6f}")
|
||
|
||
# 拉数据
|
||
print("\n拉取 K 线数据...")
|
||
raw_data = {}
|
||
for old, new in SYMBOL_MAP.items():
|
||
df = fetch(new)
|
||
if df is None:
|
||
print(f" {new}: 无数据,跳过")
|
||
continue
|
||
raw_data[old] = df_to_raw(df)
|
||
print(f" {new}: {len(df)} bars")
|
||
|
||
mt5.shutdown()
|
||
|
||
vm = StackVM()
|
||
all_results = []
|
||
|
||
print(f"\n{'='*65}")
|
||
print(" 多因子回测(新账号数据,真实点差)")
|
||
print(f"{'='*65}")
|
||
header = f"{'品种':12s}{'策略':18s}{'PnL':>8}{'Sharpe':>8}{'Sortino':>8}{'MaxDD':>8}{'Trades':>7}{'WinRate':>8}"
|
||
print(f" {header}")
|
||
print(f" {'─'*75}")
|
||
|
||
# 按策略组织:同一策略的品种合并算组合
|
||
for strat_id, strat in STRATEGIES.items():
|
||
formula = strat["formula"]
|
||
sym_list = strat["symbols"]
|
||
rd = ' -> '.join(names[t] for t in formula)
|
||
pnl_list = []
|
||
|
||
for old_sym in sym_list:
|
||
if old_sym not in raw_data:
|
||
continue
|
||
new_sym = SYMBOL_MAP[old_sym]
|
||
cost = costs.get(old_sym, 0.0001)
|
||
raw = raw_data[old_sym]
|
||
|
||
feat = MT5FeatureEngineer.compute_features(raw) # [1,F,T]
|
||
engine = BacktestEngine(formula=formula, cost_rate=cost)
|
||
res = engine.run(raw, feat, [old_sym])
|
||
r = res[0]
|
||
|
||
pnl_list.append(r.pnl)
|
||
all_results.append(r)
|
||
|
||
# 单品种指标
|
||
cum = r.cum_pnl
|
||
print(f" {old_sym:12s}{strat['name'][:17]:18s}"
|
||
f"{r.total_return:+8.3f}"
|
||
f"{sharpe(r.pnl):+8.3f}"
|
||
f"{sortino(r.pnl):+8.3f}"
|
||
f"{mdd(cum):8.3f}"
|
||
f"{r.n_trades:7d}"
|
||
f"{r.win_rate:8.1%}")
|
||
|
||
# 等权组合(v1全品种)
|
||
v1_syms = [s for s in STRATEGIES["v1_all"]["symbols"] if s in raw_data]
|
||
if v1_syms:
|
||
pnls = []
|
||
for s in v1_syms:
|
||
raw = raw_data[s]
|
||
feat = MT5FeatureEngineer.compute_features(raw)
|
||
res = BacktestEngine(formula=STRATEGIES["v1_all"]["formula"],
|
||
cost_rate=costs.get(s, 0.0001)).run(raw, feat, [s])
|
||
pnls.append(res[0].pnl)
|
||
port = np.stack(pnls).mean(0)
|
||
cum_p = np.cumsum(port)
|
||
print(f" {'─'*75}")
|
||
print(f" {'Portfolio(v1)':12s}{'均等权重':18s}"
|
||
f"{cum_p[-1]:+8.3f}"
|
||
f"{sharpe(port):+8.3f}"
|
||
f"{sortino(port):+8.3f}"
|
||
f"{mdd(cum_p):8.3f}")
|
||
n_pos = sum(1 for s in v1_syms
|
||
for r in [BacktestEngine(STRATEGIES['v1_all']['formula'],
|
||
costs.get(s,0.0001)).run(
|
||
raw_data[s],
|
||
MT5FeatureEngineer.compute_features(raw_data[s]),[s])[0]]
|
||
if r.total_return > 0)
|
||
print(f" 正收益品种: {n_pos}/{len(v1_syms)}")
|
||
|
||
print(f"\n{'='*65}")
|
||
|
||
# 生成图表
|
||
print("\n生成图表...")
|
||
OUTPUT = "backtest_output_new_account"
|
||
Path(OUTPUT).mkdir(exist_ok=True)
|
||
chart = BacktestChart(max_bars=120)
|
||
chart.plot_all(all_results, output_dir=OUTPUT)
|
||
for r in all_results:
|
||
chart.plot_all_trade_zooms(r, output_dir=OUTPUT, max_trades=8)
|
||
print(f" {r.symbol}: done")
|
||
|
||
print(f"\n图表已保存 → {OUTPUT}/\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|