This commit is contained in:
qq564020069
2026-07-17 16:08:18 +08:00
parent 542bec9263
commit ea34014326
4 changed files with 102 additions and 20 deletions
+47
View File
@@ -27,6 +27,53 @@ from .config import ModelConfig
_H1_PERIODS_PER_YEAR = 6240
_SORTINO_CLIP = 20.0
_SECONDS_PER_YEAR = 365.25 * 86400.0
def estimate_periods_per_year(times) -> int:
"""从时间戳序列估计「每年 bar 数」(年化因子)。
用 T / years_span,years_span = (t_last - t_first) / SECONDS_PER_YEAR。
该方法自动适应不同市场(A 股 / 外汇 / 加密)与周期——因为数据里只包含
交易时段的 bar,跨日历年的 bar 密度天然反映了该市场的交易频率,
无需按周期/市场写死。
替代了原先全局写死 _H1_PERIODS_PER_YEAR=6240(仅外汇 H1 正确)的做法:
A 股日线(~244 bar/年)、A 股 15min(~3904 bar/年)、加密日线(365 bar/年)
都会被正确年化,不再被按 H1 放大/缩小。
Args:
times: [N, T] 或 [T] 的 Unix 秒时间戳(torch.Tensor 或 np.ndarray)。
Returns:
int,每年 bar 数;数据不足时回退到 _H1_PERIODS_PER_YEAR。
"""
import numpy as _np
if hasattr(times, "detach"):
arr = times.detach().cpu().numpy()
else:
arr = _np.asarray(times)
arr = arr.astype(_np.float64)
if arr.ndim == 2:
t_count = arr.shape[1]
row = arr[0]
elif arr.ndim == 1:
t_count = arr.shape[0]
row = arr
else:
return _H1_PERIODS_PER_YEAR
if t_count < 2:
return _H1_PERIODS_PER_YEAR
span = float(row[-1] - row[0])
if span <= 0:
return _H1_PERIODS_PER_YEAR
years = span / _SECONDS_PER_YEAR
if years <= 0:
return _H1_PERIODS_PER_YEAR
ppy = t_count / years
# 合理范围钳制,防止异常时间戳产生极端值
return int(max(10, min(600000, round(ppy))))
class MT5Backtest:
"""MT5 组合级回测评估器。"""
+17 -1
View File
@@ -14,7 +14,7 @@ from tqdm import tqdm
from .config import ModelConfig
from .alphagpt import AlphaGPT, NewtonSchulzLowRankDecay, StableRankMonitor
from .vm import StackVM
from .backtest import MT5Backtest
from .backtest import MT5Backtest, estimate_periods_per_year
from .vocab import FORMULA_VOCAB, VOCAB_VERSION, VocabVersionMismatchError # task 12.2
# P3:冠军在场时间稳健性校验所需
@@ -471,6 +471,22 @@ class AlphaEngine:
# 每个 t 的归一化参数只用 [t-w+1..t],walk-forward 折叠切片无泄露
feat = self.data_manager.feat_tensor.to(ModelConfig.DEVICE)
t_ret = self.data_manager.target_ret.to(ModelConfig.DEVICE)
# 数据驱动年化因子:按训练数据的实际时间戳估计每年 bar 数,
# 替代 MT5Backtest 默认的 H1=6240。A 股日线/15min、加密日线等
# 非 H1 周期不再被按 H1 年化(否则 Sharpe/年化收益被放大数倍)。
_dm_raw = getattr(self.data_manager, "raw_dict", None) or {}
_dm_time = _dm_raw.get("time", None)
if _dm_time is not None:
try:
_ppy = estimate_periods_per_year(_dm_time)
if _ppy != self.bt.periods_per_year:
if verbose_header:
print(f" 年化因子: {_ppy} bar/年(按数据周期自动估计)")
self.bt.periods_per_year = _ppy
except Exception:
pass # 估计失败则保留默认 6240
bs = ModelConfig.BATCH_SIZE
n_elite = max(1, int(bs * ModelConfig.ELITE_REPLAY_FRAC))
n_new = bs - n_elite
+12 -3
View File
@@ -41,9 +41,18 @@ def _op_gate(condition: torch.Tensor, x: torch.Tensor, y: torch.Tensor) -> torch
return mask * x + (1.0 - mask) * y
def _op_jump(x: torch.Tensor) -> torch.Tensor:
"""降低稀疏度:阈值从 3σ 改为 1.5σ,让更多时间步有非零输出"""
mean = x.mean(dim=1, keepdim=True)
std = x.std(dim=1, keepdim=True) + 1e-6
"""降低稀疏度:阈值从 3σ 改为 1.5σ,让更多时间步有非零输出。
因果 expanding zscore:每个 t 仅用 x[:, :t+1] 计算 mean/std,
避免原 dim=1 全局聚合(含未来统计量)引入的 look-ahead bias。
"""
N, T = x.shape
cnt = torch.arange(1, T + 1, device=x.device, dtype=x.dtype).view(1, T)
cumsum = x.cumsum(dim=1)
mean = cumsum / cnt # [N,T],t 位 = x[:,:t+1].mean()
cumsum_sq = (x * x).cumsum(dim=1)
var = (cumsum_sq / cnt) - mean * mean # E[x^2] - E[x]^2
std = var.clamp(min=1e-12).sqrt() + 1e-6
z = (x - mean) / std
return torch.tanh(z - 1.5) # tanh 软化,不再产生全零区间
+26 -16
View File
@@ -27,6 +27,7 @@ from backtest_viz import BacktestEngine
from model_core.vocab import FORMULA_VOCAB, VOCAB_VERSION
from model_core.vm import StackVM
from model_core.features import MT5FeatureEngineer
from model_core.backtest import estimate_periods_per_year
from strategy_manager.signal import compute_target_positions_stateless
_H1_PER_YEAR = 6240
@@ -129,13 +130,15 @@ def _setup_chinese_font() -> None:
plt.rcParams["axes.unicode_minus"] = False
def plot_equity_curves(results_map: dict, output_dir: str, times_arr: np.ndarray | None = None):
def plot_equity_curves(results_map: dict, output_dir: str, times_arr: np.ndarray | None = None,
periods_per_year: int = _H1_PER_YEAR):
"""绘制各品种 + 等权组合的资金曲线(中文标注)。
Args:
results_map: {symbol: {"pnl": np.array, "cum_pnl": np.array, ...}}
output_dir: 输出目录
times_arr: 时间戳数组(Unix秒),用于 X 轴刻度
periods_per_year: 年化因子(每年 bar 数),由调用方按数据周期估计
"""
_setup_chinese_font()
Path(output_dir).mkdir(parents=True, exist_ok=True)
@@ -174,7 +177,7 @@ def plot_equity_curves(results_map: dict, output_dir: str, times_arr: np.ndarray
)
ax_eq.plot(
x, port_cum, linewidth=2.2, color="black",
label=f"等权组合(索提诺 {calc_sortino(port_pnl):+.2f})",
label=f"等权组合(索提诺 {calc_sortino(port_pnl, periods_per_year):+.2f})",
)
ax_eq.fill_between(x, port_cum, 0, where=port_cum >= 0, alpha=0.06, color="#1565c0")
ax_eq.fill_between(x, port_cum, 0, where=port_cum < 0, alpha=0.06, color="#b71c1c")
@@ -188,8 +191,8 @@ def plot_equity_curves(results_map: dict, output_dir: str, times_arr: np.ndarray
ax_eq.set_title(
f"{title_head} | "
f"总收益={show_cum[-1]:+.3f} "
f"夏普={calc_sharpe(show_pnl):+.2f} "
f"索提诺={calc_sortino(show_pnl):+.2f} "
f"夏普={calc_sharpe(show_pnl, periods_per_year):+.2f} "
f"索提诺={calc_sortino(show_pnl, periods_per_year):+.2f} "
f"盈亏比={_fmt_pl_ratio(results_map)}",
fontsize=11, pad=8,
)
@@ -220,6 +223,7 @@ def export_equity_json(
times_arr: np.ndarray | None = None,
max_points: int = 1500,
rolling_window: int = 500,
periods_per_year: int = _H1_PER_YEAR,
):
"""导出资金曲线原始数据为 JSON,供前端渲染交互式 HTML 图表。
@@ -277,7 +281,8 @@ def export_equity_json(
}
for s in syms:
cum = results_map[s]["cum_pnl"]
roll = calc_rolling_sharpe(results_map[s]["pnl"], window=rolling_window)
roll = calc_rolling_sharpe(results_map[s]["pnl"], window=rolling_window,
periods_per_year=periods_per_year)
pl = results_map[s].get("profit_loss_ratio")
out["symbols"][s] = {
"equity": _sample(cum),
@@ -297,9 +302,10 @@ def export_equity_json(
port_pl = float(sum(pl_vals) / len(pl_vals)) if pl_vals else None
out["portfolio"] = {
"equity": _sample(port_cum),
"rolling_sharpe": _sample(calc_rolling_sharpe(port_pnl, window=rolling_window)),
"sharpe": round(float(calc_sharpe(port_pnl)), 4),
"sortino": round(float(calc_sortino(port_pnl)), 4),
"rolling_sharpe": _sample(calc_rolling_sharpe(port_pnl, window=rolling_window,
periods_per_year=periods_per_year)),
"sharpe": round(float(calc_sharpe(port_pnl, periods_per_year)), 4),
"sortino": round(float(calc_sortino(port_pnl, periods_per_year)), 4),
"total_return": round(float(port_cum[-1]), 6),
"profit_loss_ratio": round(port_pl, 4) if port_pl is not None else None,
}
@@ -437,7 +443,10 @@ def main():
T = raw_dict["open"].shape[1]
times_all = raw_dict.get("time", None)
print(f" 品种: {syms} T={T} bars\n")
# 数据驱动年化因子:按实际时间戳估计每年 bar 数,替代写死的 H1=6240。
# A 股日线(~244)、A 股 15min(~3904)、外汇 H1(6240)、加密日线(365) 都会被正确年化。
ppy = estimate_periods_per_year(times_all) if times_all is not None else _H1_PER_YEAR
print(f" 品种: {syms} T={T} bars 年化因子={ppy} bar/年\n")
# ── 4. 为每品种计算因子 + 回测 ───────────────────────────────
vm = StackVM()
@@ -458,15 +467,16 @@ def main():
feat_i = feat[i:i+1]
raw_i = {k: v[i:i+1] for k, v in raw_dict.items()}
engine = BacktestEngine(formula=formula, cost_rate=cost_rate)
engine = BacktestEngine(formula=formula, cost_rate=cost_rate,
periods_per_year=ppy)
sym_res = engine.run(raw_i, feat_i, [sym])
backtest_results.extend(sym_res)
r = sym_res[0]
pnl_arr = r.pnl
cum_arr = r.cum_pnl
sharpe = calc_sharpe(pnl_arr)
sortino = calc_sortino(pnl_arr)
sharpe = calc_sharpe(pnl_arr, ppy)
sortino = calc_sortino(pnl_arr, ppy)
pl_ratio = r.profit_loss_ratio
results_map[sym] = {
@@ -507,8 +517,8 @@ def main():
all_pnls = np.stack([d["pnl"] for d in results_map.values()], axis=0)
port_pnl = all_pnls.mean(axis=0)
port_cum = np.cumsum(port_pnl)
p_sharpe = calc_sharpe(port_pnl)
p_sortino = calc_sortino(port_pnl)
p_sharpe = calc_sharpe(port_pnl, ppy)
p_sortino = calc_sortino(port_pnl, ppy)
pl_vals = [d["profit_loss_ratio"] for d in results_map.values()
if d["profit_loss_ratio"] is not None]
p_pl_ratio = float(sum(pl_vals) / len(pl_vals)) if pl_vals else None
@@ -527,8 +537,8 @@ def main():
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
if results_map:
times_np = times_all[0].numpy() if times_all is not None else None
plot_equity_curves(results_map, OUTPUT_DIR, times_np)
export_equity_json(results_map, OUTPUT_DIR, times_np)
plot_equity_curves(results_map, OUTPUT_DIR, times_np, periods_per_year=ppy)
export_equity_json(results_map, OUTPUT_DIR, times_np, periods_per_year=ppy)
# ── 7. 资金曲线图已在步骤 6 生成;跳过 K 线/逐笔交易图以加快回测 ─────