2026-07-02 00:51:15 +08:00
|
|
|
|
"""
|
2026-07-03 12:35:29 +08:00
|
|
|
|
main.py — 多因子训练入口(分组训练)
|
2026-07-02 00:51:15 +08:00
|
|
|
|
|
|
|
|
|
|
使用方法:
|
2026-07-03 12:35:29 +08:00
|
|
|
|
python main.py # 按 SYMBOL_GROUPS 分组训练(默认推荐)
|
2026-07-03 14:55:57 +08:00
|
|
|
|
python main.py --offline # 仅使用本地缓存,不连接 MT5
|
2026-07-03 12:35:29 +08:00
|
|
|
|
python main.py --single XAUUSD # 只训练单个品种
|
|
|
|
|
|
python main.py --cross-section # 所有品种一起训练(截面)
|
|
|
|
|
|
python main.py --group risk # 只训练 risk 组
|
2026-07-02 00:51:15 +08:00
|
|
|
|
|
2026-07-03 12:35:29 +08:00
|
|
|
|
分组说明(Config.SYMBOL_GROUPS):
|
|
|
|
|
|
forex 组(EURUSD, USDJPY):外汇,美元方向因子,2品种×11000=22000样本
|
|
|
|
|
|
risk 组(XAUUSD, US100.cash, US500.cash):风险资产,3品种×11000=33000样本
|
2026-07-02 00:51:15 +08:00
|
|
|
|
"""
|
2026-07-03 12:35:29 +08:00
|
|
|
|
import sys, pathlib, json
|
2026-07-03 01:15:41 +08:00
|
|
|
|
|
2026-07-03 14:55:57 +08:00
|
|
|
|
# 无控制台环境(如 Start-Process -WindowStyle Hidden)下,sys.stdout 可能为 None,
|
|
|
|
|
|
# 导致 tqdm.write 报错。此时重定向到日志文件。
|
|
|
|
|
|
if sys.stdout is None or sys.stderr is None:
|
|
|
|
|
|
_log_path = r"D:\素材\自动挖因子\training_stdout.log"
|
|
|
|
|
|
_log_fp = open(_log_path, "a", encoding="utf-8")
|
|
|
|
|
|
if sys.stdout is None:
|
|
|
|
|
|
sys.stdout = _log_fp
|
|
|
|
|
|
if sys.stderr is None:
|
|
|
|
|
|
sys.stderr = _log_fp
|
|
|
|
|
|
|
2026-07-02 00:51:15 +08:00
|
|
|
|
from config import Config
|
|
|
|
|
|
from data_pipeline.data_manager import MT5DataManager
|
|
|
|
|
|
from data_pipeline.fetcher import MT5DataFetcher
|
2026-07-03 01:15:41 +08:00
|
|
|
|
from data_pipeline.single_symbol_manager import SingleSymbolDataManager
|
2026-07-02 00:51:15 +08:00
|
|
|
|
from model_core.engine import AlphaEngine
|
2026-07-03 01:15:41 +08:00
|
|
|
|
from model_core.config import ModelConfig
|
2026-07-03 12:35:29 +08:00
|
|
|
|
from model_core.vocab import VOCAB_VERSION
|
2026-07-03 01:15:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 12:35:29 +08:00
|
|
|
|
class GroupDataManager:
|
|
|
|
|
|
"""品种分组数据视图,兼容 AlphaEngine 接口(N = 组内品种数)。"""
|
|
|
|
|
|
def __init__(self, multi_manager, symbols: list[str]):
|
|
|
|
|
|
self._multi = multi_manager
|
|
|
|
|
|
self._symbols = [s for s in symbols if s in multi_manager.symbols]
|
|
|
|
|
|
self._idxs = [multi_manager.symbols.index(s) for s in self._symbols]
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def symbols(self): return list(self._symbols)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def raw_dict(self):
|
|
|
|
|
|
full = self._multi.raw_dict
|
|
|
|
|
|
return {k: v[self._idxs] for k, v in full.items()}
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def feat_tensor(self):
|
|
|
|
|
|
from model_core.features import MT5FeatureEngineer
|
|
|
|
|
|
return MT5FeatureEngineer.compute_features(self.raw_dict)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def target_ret(self):
|
|
|
|
|
|
return self._multi.target_ret[self._idxs]
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def bar_time(self):
|
|
|
|
|
|
return self._multi.bar_time[self._idxs]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_group_strategy(engine: AlphaEngine, group_name: str, symbols: list[str]):
|
|
|
|
|
|
"""保存分组策略:group 总文件 + 各品种 best_*.json。"""
|
|
|
|
|
|
pathlib.Path("strategies").mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 分组总文件
|
|
|
|
|
|
gp = pathlib.Path("strategies") / f"best_group_{group_name}.json"
|
|
|
|
|
|
gp.write_text(json.dumps({
|
|
|
|
|
|
"vocab_version": VOCAB_VERSION,
|
|
|
|
|
|
"group": group_name,
|
|
|
|
|
|
"symbols": symbols,
|
|
|
|
|
|
"formula": engine.best_formula,
|
|
|
|
|
|
"best_score": engine.best_score,
|
|
|
|
|
|
}, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
# 各品种文件(runner 按品种名加载)
|
|
|
|
|
|
for sym in symbols:
|
|
|
|
|
|
sp = pathlib.Path("strategies") / f"best_{sym}.json"
|
|
|
|
|
|
sp.write_text(json.dumps({
|
|
|
|
|
|
"vocab_version": VOCAB_VERSION,
|
|
|
|
|
|
"symbol": sym,
|
|
|
|
|
|
"group": group_name,
|
|
|
|
|
|
"formula": engine.best_formula,
|
|
|
|
|
|
"best_score": engine.best_score,
|
|
|
|
|
|
"source": f"group_{group_name}",
|
|
|
|
|
|
}, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
print(f" 已保存: best_group_{group_name}.json + {len(symbols)} 个品种文件")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 02:26:39 +08:00
|
|
|
|
def train_group(fetcher, group_name: str, symbols: list[str], offline: bool):
|
|
|
|
|
|
"""训练一个品种组,使用组内独立的 DataManager(不与其他组取时间交集)。
|
|
|
|
|
|
|
2026-07-05 09:48:30 +08:00
|
|
|
|
自动检测是否有可续训的 checkpoint:若存在则从断点继续,否则从 step 0 开始。
|
2026-07-05 02:26:39 +08:00
|
|
|
|
"""
|
2026-07-03 12:35:29 +08:00
|
|
|
|
print(f"\n{'─'*60}")
|
|
|
|
|
|
print(f" [{group_name}] 组: {symbols}")
|
|
|
|
|
|
print(f"{'─'*60}")
|
|
|
|
|
|
|
2026-07-05 02:26:39 +08:00
|
|
|
|
# 临时覆盖 SYMBOLS 只加载本组品种
|
|
|
|
|
|
original_symbols = Config.SYMBOLS[:]
|
|
|
|
|
|
Config.SYMBOLS = [s for s in symbols if s in original_symbols or True]
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
group_mgr = MT5DataManager(fetcher)
|
|
|
|
|
|
group_mgr.load()
|
|
|
|
|
|
actual_symbols = group_mgr.symbols
|
|
|
|
|
|
T = group_mgr.raw_dict["open"].shape[1]
|
|
|
|
|
|
print(f" 独立加载: {actual_symbols} T={T} bars")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" [错误] 数据加载失败: {e}")
|
|
|
|
|
|
Config.SYMBOLS = original_symbols
|
|
|
|
|
|
return None
|
|
|
|
|
|
finally:
|
|
|
|
|
|
Config.SYMBOLS = original_symbols # 恢复原始配置
|
|
|
|
|
|
|
|
|
|
|
|
if not actual_symbols:
|
2026-07-03 12:35:29 +08:00
|
|
|
|
print(f" [跳过] 无有效品种")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-07-05 02:26:39 +08:00
|
|
|
|
engine = AlphaEngine(data_manager=group_mgr, target_symbol=group_name)
|
2026-07-05 09:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
# ── 自动续训:检测最新 checkpoint ────────────────────────────────
|
|
|
|
|
|
import glob as _glob
|
|
|
|
|
|
ckpt_pattern = str(pathlib.Path("checkpoints") / f"ckpt_{group_name}_step_*.pt")
|
|
|
|
|
|
ckpt_files = sorted(_glob.glob(ckpt_pattern))
|
|
|
|
|
|
start_step = 0
|
|
|
|
|
|
if ckpt_files:
|
|
|
|
|
|
latest_ckpt = ckpt_files[-1]
|
|
|
|
|
|
try:
|
|
|
|
|
|
start_step = engine.load_checkpoint(latest_ckpt)
|
|
|
|
|
|
print(f" [续训] 从 {latest_ckpt} 恢复,start_step={start_step}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" [警告] checkpoint 加载失败({e}),从头开始训练")
|
|
|
|
|
|
start_step = 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" [新训] 未找到 checkpoint,从 step 0 开始")
|
|
|
|
|
|
|
|
|
|
|
|
if start_step >= ModelConfig.TRAIN_STEPS:
|
|
|
|
|
|
print(f" [完成] {group_name} 已完成全部 {ModelConfig.TRAIN_STEPS} 步,跳过训练")
|
|
|
|
|
|
save_group_strategy(engine, group_name, actual_symbols)
|
|
|
|
|
|
return engine
|
|
|
|
|
|
|
|
|
|
|
|
engine.train(start_step=start_step)
|
2026-07-05 02:26:39 +08:00
|
|
|
|
save_group_strategy(engine, group_name, actual_symbols)
|
2026-07-03 12:35:29 +08:00
|
|
|
|
return engine
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
cross = "--cross-section" in sys.argv
|
|
|
|
|
|
single = "--single" in sys.argv
|
2026-07-03 14:55:57 +08:00
|
|
|
|
offline = "--offline" in sys.argv
|
2026-07-03 12:35:29 +08:00
|
|
|
|
grp_only = None
|
|
|
|
|
|
if "--group" in sys.argv:
|
|
|
|
|
|
idx = sys.argv.index("--group")
|
|
|
|
|
|
grp_only = sys.argv[idx + 1] if idx + 1 < len(sys.argv) else None
|
|
|
|
|
|
single_sym = [s for s in sys.argv[1:] if not s.startswith("--")] or None
|
|
|
|
|
|
|
|
|
|
|
|
mode = "截面" if cross else ("单品种" if single else "分组")
|
2026-07-03 01:15:41 +08:00
|
|
|
|
print(f"{'='*60}")
|
2026-07-03 14:55:57 +08:00
|
|
|
|
print(f" AlphaGPT 训练 [{mode}模式]" + (" [离线缓存]" if offline else ""))
|
2026-07-03 01:15:41 +08:00
|
|
|
|
print(f" TRAIN_STEPS={ModelConfig.TRAIN_STEPS} "
|
|
|
|
|
|
f"MAX_FORMULA_LEN={ModelConfig.MAX_FORMULA_LEN} "
|
|
|
|
|
|
f"BATCH_SIZE={ModelConfig.BATCH_SIZE}")
|
2026-07-03 12:35:29 +08:00
|
|
|
|
print(f" SYMBOLS={Config.SYMBOLS}")
|
2026-07-03 01:15:41 +08:00
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
|
|
2026-07-03 14:55:57 +08:00
|
|
|
|
with MT5DataFetcher(offline=offline) as fetcher:
|
2026-07-03 12:35:29 +08:00
|
|
|
|
if single and single_sym:
|
2026-07-05 02:26:39 +08:00
|
|
|
|
# 单品种模式:用全局 multi_mgr
|
|
|
|
|
|
multi_mgr = MT5DataManager(fetcher)
|
|
|
|
|
|
multi_mgr.load()
|
2026-07-03 12:35:29 +08:00
|
|
|
|
sym = single_sym[0]
|
|
|
|
|
|
s_mgr = SingleSymbolDataManager(multi_mgr, sym)
|
|
|
|
|
|
engine = AlphaEngine(data_manager=s_mgr, target_symbol=sym)
|
2026-07-03 01:15:41 +08:00
|
|
|
|
engine.train()
|
|
|
|
|
|
|
2026-07-03 12:35:29 +08:00
|
|
|
|
elif cross:
|
2026-07-05 02:26:39 +08:00
|
|
|
|
multi_mgr = MT5DataManager(fetcher)
|
|
|
|
|
|
multi_mgr.load()
|
2026-07-03 12:35:29 +08:00
|
|
|
|
engine = AlphaEngine(data_manager=multi_mgr, target_symbol=None)
|
|
|
|
|
|
engine.train()
|
|
|
|
|
|
save_group_strategy(engine, "cross_section", multi_mgr.symbols)
|
2026-07-03 12:01:30 +08:00
|
|
|
|
|
|
|
|
|
|
else:
|
2026-07-05 02:26:39 +08:00
|
|
|
|
# 分组训练:每组独立加载,不共享 DataManager
|
2026-07-03 12:35:29 +08:00
|
|
|
|
groups = getattr(Config, "SYMBOL_GROUPS", {
|
|
|
|
|
|
"forex": ["EURUSD", "USDJPY"],
|
|
|
|
|
|
"risk": ["XAUUSD", "US100.cash", "US500.cash"],
|
|
|
|
|
|
})
|
|
|
|
|
|
if grp_only:
|
|
|
|
|
|
groups = {grp_only: groups[grp_only]} if grp_only in groups else groups
|
2026-07-03 12:01:30 +08:00
|
|
|
|
|
|
|
|
|
|
results = {}
|
2026-07-03 12:35:29 +08:00
|
|
|
|
for gname, gsyms in groups.items():
|
2026-07-05 02:26:39 +08:00
|
|
|
|
eng = train_group(fetcher, gname, gsyms, offline)
|
2026-07-03 12:35:29 +08:00
|
|
|
|
if eng:
|
|
|
|
|
|
results[gname] = {
|
|
|
|
|
|
"score": eng.best_score,
|
|
|
|
|
|
"formula": eng._decode_formula(eng.best_formula),
|
|
|
|
|
|
}
|
2026-07-03 12:01:30 +08:00
|
|
|
|
|
|
|
|
|
|
print(f"\n{'='*60}")
|
2026-07-03 12:35:29 +08:00
|
|
|
|
print(f" 分组训练完成")
|
2026-07-03 12:01:30 +08:00
|
|
|
|
print(f"{'='*60}")
|
2026-07-03 12:35:29 +08:00
|
|
|
|
for gname, r in results.items():
|
|
|
|
|
|
print(f" [{gname}]: score={r['score']:.4f}")
|
|
|
|
|
|
print(f" {r['formula']}")
|
2026-07-03 12:01:30 +08:00
|
|
|
|
print()
|
2026-07-03 01:15:41 +08:00
|
|
|
|
|
2026-07-02 00:51:15 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-07-03 12:35:29 +08:00
|
|
|
|
main()
|