0.998
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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 线/逐笔交易图以加快回测 ─────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user