feat: 新增2h/6h时间框架支持,策略重构为增量指标计算

- 数据层: build_aggregates_sql 新增 2h/6h 聚合视图,默认起始时间调整为 2017-05
- 模型层: KlineInterval 类型扩展 2h/6h,DataService 新增对应表名和毫秒映射
- 指标层: 新增 incremental.py 增量指标模块 (EmaInc/AtrInc/RsiInc/BbInc),O(1) per bar
- 策略重构: long_short.py 和 regime_all.py 从批量 ema/atr 迁移至增量指标,避免每 bar 重复全量计算
- regime 探测器: RegimeDetector3 改为增量 EMA200,detect() 接口简化
- 回测扩展: regime_timeframe_comparison 从 4h/1d 扩展至 2h/4h/6h/1d
- 新增示例: multi_strategy_report, vol_break_compare/periods, intraday_explore, top3_trades 等分析脚本
This commit is contained in:
Rekey
2026-06-13 19:30:25 +08:00
parent b5cdb41993
commit edc50e8809
20 changed files with 484544 additions and 34 deletions
+96
View File
@@ -0,0 +1,96 @@
"""获取近一年Top3策略的详细交易记录"""
import asyncio, sys, json
from datetime import datetime, timedelta, timezone
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.config import config
from engine.backtest.models import BacktestConfig
from engine.example.long_short import LongShortEngine
from engine.example.full_comparison import (
VolBreakStrategy, VolBreakConfig,
EmaCrossStrategy, EmaCrossConfig,
)
from engine.common.base import Signal
NOW = datetime.now(timezone.utc)
ONE_YEAR_AGO = NOW - timedelta(days=365)
INITIAL = 10_000.0
TASKS = [
("ATR波动率突破 ETHUSDT 4h", VolBreakConfig, VolBreakStrategy,
"ETHUSDT", "4h", lambda s: VolBreakConfig(symbol=s, atr_period=14, squeeze_period=20, squeeze_ratio=0.7, atr_stop=2.0)),
("EMA双均线多空 ETHUSDT 1d", EmaCrossConfig, EmaCrossStrategy,
"ETHUSDT", "1d", lambda s: EmaCrossConfig(symbol=s, fast=10, slow=50, atr_stop=2.5)),
("EMA双均线多空 BTCUSDT 1d", EmaCrossConfig, EmaCrossStrategy,
"BTCUSDT", "1d", lambda s: EmaCrossConfig(symbol=s, fast=10, slow=50, atr_stop=2.5)),
]
def fmt_ts(ts_ms):
return datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
async def run_one(label, config_cls, strategy_cls, symbol, interval, mkcfg):
sc = mkcfg(symbol)
bt = BacktestConfig(symbol=symbol, interval=interval,
start_time=ONE_YEAR_AGO, end_time=NOW,
initial_capital=INITIAL, warmup_bars=150)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(strategy_cls, sc)
return label, r
async def main():
results = []
for task in TASKS:
label, r = await run_one(*task)
results.append((label, r))
for label, r in results:
m = r.metrics
trades = r.trades
# 配对交易
paired = []
pending = None
for t in trades:
if t.side == "BUY" and t.pnl is None:
pending = {"entry_ts": t.timestamp, "entry_price": t.price, "entry_reason": t.reason}
elif t.side == "SELL" and pending and t.pnl is not None:
paired.append({**pending, "exit_ts": t.timestamp, "exit_price": t.price,
"exit_reason": t.reason, "pnl": t.pnl})
pending = None
elif t.side == "SELL" and t.pnl is None:
pending = {"entry_ts": t.timestamp, "entry_price": t.price, "entry_reason": t.reason, "short": True}
elif t.side == "BUY" and pending and t.pnl is not None and pending.get("short"):
paired.append({**pending, "exit_ts": t.timestamp, "exit_price": t.price,
"exit_reason": t.reason, "pnl": t.pnl})
pending = None
cfg = r.config
print(f"\n═══ {label} ═══")
print(f" 本金 {INITIAL:,.0f} → 终值 {m.final_equity:,.0f} | {m.total_return_pct:+.1f}% | 年化 {m.annual_return_pct:+.1f}%")
print(f" 夏普 {m.sharpe_ratio:.2f} | 回撤 {m.max_drawdown_pct:.1f}% | 胜率 {m.win_rate*100:.1f}% | 盈亏比 {m.profit_factor:.2f} | {m.total_trades}")
print(f" 日期 {cfg.start_time.date()} ~ {cfg.end_time.date()}")
print()
print(f" {'#':>3} {'入场时间':<19} {'入场价':>10} {'入场原因':<25} {'出场时间':<19} {'出场价':>10} {'出场原因':<25} {'盈亏':>10}")
print(" " + "" * 130)
total_pnl = 0
for i, p in enumerate(paired):
total_pnl += p["pnl"]
side = "做空" if p.get("short") else "做多"
print(f" {i+1:>3} {fmt_ts(p['entry_ts']):<19} {p['entry_price']:>10.4f} {p['entry_reason']:<25} {fmt_ts(p['exit_ts']):<19} {p['exit_price']:>10.4f} {p['exit_reason']:<25} {p['pnl']:>+10.2f}")
print(" " + "" * 130)
wins = sum(1 for p in paired if p["pnl"] > 0)
print(f" 合计 {len(paired)} 笔 | 盈利 {wins} 笔 | 总盈亏 {total_pnl:+,.2f} USDT")
print()
if __name__ == "__main__":
asyncio.run(main())