phase 7-8 完成 + warmup 修复 + 产物结构化重组

主要内容:
- Phase 8 PROMOTE: finalist #1 (trial #324) registry 条目,自动生成
- Optuna objective warmup bug 修复 (shared/optimizer/objective.py)
- studies/ 目录按用途重组为 optuna/ + finalists/ + features/ 三层
- reports/ 加入 Optuna 中文 dashboard (5 主图 + 18 slice + 15 contour)
- 新增 PROJECT_GUIDE.md 项目说明文档
- 新增 build_registry_entry.py / build_optuna_dashboard.py / build_feature_datasets.py
- .gitignore: 允许提交 studies/*.db (Optuna DB) 和 reports/*.html (MT5 + dashboard)
This commit is contained in:
2026-06-27 00:28:07 +08:00
parent 0dcbfe0781
commit 63a829cc46
45 changed files with 7240 additions and 9 deletions
+4 -1
View File
@@ -17,6 +17,9 @@ from pathlib import Path
from typing import Any, Mapping
SET_FILE_ENCODING = "utf-16-le"
# MT5's Strategy Tester silently ignores .set files without a UTF-16-LE BOM.
# Python's "utf-16-le" codec does NOT emit a BOM, so prepend one explicitly.
UTF16_LE_BOM = b"\xff\xfe"
@dataclass
@@ -68,4 +71,4 @@ def write_set_file(
if extra_lines:
lines.extend(extra_lines)
text = "\n".join(lines) + "\n"
out.write_bytes(text.encode(SET_FILE_ENCODING))
out.write_bytes(UTF16_LE_BOM + text.encode(SET_FILE_ENCODING))
+48 -5
View File
@@ -77,6 +77,20 @@ class ObjectiveConfig:
# strategy pipe tunable point values into its engine without the optimizer
# knowing about strategy-specific config objects. None → no extra kwargs.
build_engine_kwargs: Optional[Callable[[dict], dict]] = None
# M1 bars for tick-level exit simulation. MANDATORY if the EA moves its
# SL intra-trade (break-even / trailing / basket trailing) — bar-level
# exit simulation produces a 40% to 50% net gap on those EAs (doc 03 §7
# measured failure mode). Leave None only for clean-directional setups
# that don't move the SL. The bars must cover the same window as ``bars``.
m1_bars: Optional[pd.DataFrame] = None
# Indicator-warmup bars (doc 03 §8 / reeval_finalist_forward.py pattern).
# When set, the objective builds signals on this FULL history (so EMA/RSI/
# ATR are already stable before the eval window starts — matching MT5
# Strategy Tester's pre-test chart-history warmup), then slices the signal
# arrays to ``bars``' time range before running the engine. Must be a
# contiguous superset of ``bars`` (same OHLC source, same timestamps).
# Leave None to build signals directly on ``bars`` (legacy behaviour).
signals_full_bars: Optional[pd.DataFrame] = None
@dataclass
@@ -141,14 +155,43 @@ def build_objective(cfg: ObjectiveConfig):
def objective(trial) -> float:
sampled = suggest_params(trial, cfg.search_space, cfg.int_params)
merged = {**cfg.frozen_baseline, **sampled}
pack = cfg.build_signals(merged, cfg.bars, cfg.instrument)
if cfg.signals_full_bars is not None:
# Warmup mode (doc 03 §8 / reeval_finalist_forward.py pattern):
# build signals on the FULL bars so indicators are already stable
# at the eval window start — mirrors MT5 Strategy Tester's pre-test
# chart-history warmup. Then slice the signal arrays to cfg.bars'
# time range. cfg.bars must be a contiguous sub-range of
# signals_full_bars (same OHLC source, same timestamps).
pack = cfg.build_signals(merged, cfg.signals_full_bars, cfg.instrument)
eval_start = pd.Timestamp(cfg.bars["timestamp"].iloc[0])
full_ts = pd.to_datetime(cfg.signals_full_bars["timestamp"].to_numpy())
lo = int(full_ts.searchsorted(eval_start, side="left"))
hi = lo + len(cfg.bars)
sig_long = pack.signals_long[lo:hi]
sig_short = pack.signals_short[lo:hi]
sl_p = pack.sl_prices[lo:hi]
tp_p = pack.tp_prices[lo:hi]
else:
# Legacy mode: build signals directly on the (already-trimmed)
# cfg.bars. Indicators warm up at the eval window start — fine for
# short-period indicators but produces ~14h of EMA-stabilization
# noise at the start of long-period EMA strategies.
pack = cfg.build_signals(merged, cfg.bars, cfg.instrument)
sig_long = pack.signals_long
sig_short = pack.signals_short
sl_p = pack.sl_prices
tp_p = pack.tp_prices
extra = cfg.build_engine_kwargs(merged) if cfg.build_engine_kwargs else {}
# If M1 bars are wired up, pass them so the engine switches to tick-
# level exit simulation (mandatory for trailing/BE EAs — doc 03 §7).
if cfg.m1_bars is not None:
extra = {**extra, "m1_bars": cfg.m1_bars}
result = cfg.engine.run(
cfg.bars,
pack.signals_long,
pack.signals_short,
pack.sl_prices,
pack.tp_prices,
sig_long,
sig_short,
sl_p,
tp_p,
cfg.instrument,
cfg.sizing,
cfg.initial_deposit,