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:
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
策略3 ATR波动率突破 — 1h 全量 vs 近两年对比 + 近两年详细订单
|
||||
|
||||
用法:
|
||||
source .venv/bin/activate && python example/vol_break_compare.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
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 import BacktestConfig
|
||||
from engine.data import DataService
|
||||
from engine.example.long_short import LongShortEngine
|
||||
from engine.example.intraday_explore import VolBreakStrategy, VolBreakConfig
|
||||
|
||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||||
INTERVAL = "1h"
|
||||
INITIAL = 10_000.0
|
||||
|
||||
PARAMS = {
|
||||
"BTCUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
||||
"ETHUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
||||
"BNBUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
||||
"SOLUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
||||
}
|
||||
|
||||
# 近两年截止日期
|
||||
RECENT_END = datetime(2025, 6, 1, tzinfo=timezone.utc)
|
||||
RECENT_START = RECENT_END - timedelta(days=365 * 2) # 2023-06
|
||||
|
||||
|
||||
def fmt_ts(ts_ms: float) -> str:
|
||||
dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def pair_trades(trades: list) -> list[dict]:
|
||||
"""将 BUY/SELL 配对为完整交易"""
|
||||
paired = []
|
||||
pending_open = None # (side, price, reason, ts)
|
||||
|
||||
for t in trades:
|
||||
if t.side == "BUY":
|
||||
if pending_open and pending_open["side"] == "short":
|
||||
# 平空仓 (BUY to close short)
|
||||
paired.append({
|
||||
"type": "做空",
|
||||
"entry_ts": pending_open["ts"],
|
||||
"entry_price": pending_open["price"],
|
||||
"entry_reason": pending_open["reason"],
|
||||
"exit_ts": t.timestamp,
|
||||
"exit_price": t.price,
|
||||
"exit_reason": t.reason,
|
||||
"pnl": t.pnl or 0,
|
||||
})
|
||||
pending_open = None
|
||||
elif t.pnl is None:
|
||||
# 开多仓
|
||||
pending_open = {"side": "long", "price": t.price, "reason": t.reason, "ts": t.timestamp}
|
||||
|
||||
elif t.side == "SELL":
|
||||
if pending_open and pending_open["side"] == "long":
|
||||
# 平多仓 (SELL to close long)
|
||||
paired.append({
|
||||
"type": "做多",
|
||||
"entry_ts": pending_open["ts"],
|
||||
"entry_price": pending_open["price"],
|
||||
"entry_reason": pending_open["reason"],
|
||||
"exit_ts": t.timestamp,
|
||||
"exit_price": t.price,
|
||||
"exit_reason": t.reason,
|
||||
"pnl": t.pnl or 0,
|
||||
})
|
||||
pending_open = None
|
||||
elif t.pnl is None:
|
||||
# 开空仓
|
||||
pending_open = {"side": "short", "price": t.price, "reason": t.reason, "ts": t.timestamp}
|
||||
|
||||
return paired
|
||||
|
||||
|
||||
async def run_backtest(symbol, start, end):
|
||||
sc = VolBreakConfig(symbol=symbol, **PARAMS[symbol])
|
||||
bt = BacktestConfig(symbol=symbol, interval=INTERVAL, start_time=start, end_time=end, initial_capital=INITIAL)
|
||||
engine = LongShortEngine(bt, db_config=config.db)
|
||||
r = await engine.run(VolBreakStrategy, sc)
|
||||
return r
|
||||
|
||||
|
||||
async def main():
|
||||
ds = DataService(config.db)
|
||||
await ds.connect()
|
||||
|
||||
# 获取数据范围
|
||||
full_ranges = {}
|
||||
for symbol in SYMBOLS:
|
||||
try:
|
||||
s, e = await ds.fetch_symbol_date_range(symbol, INTERVAL)
|
||||
full_ranges[symbol] = (s, e)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 运行全量 + 近两年 ──
|
||||
full_results = {}
|
||||
recent_results = {}
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in full_ranges:
|
||||
continue
|
||||
fs, fe = full_ranges[symbol]
|
||||
|
||||
print(f" 运行 {symbol} 全量 ({fs.date()}~{fe.date()})...", end=" ", flush=True)
|
||||
t0 = time.time()
|
||||
full_results[symbol] = await run_backtest(symbol, fs, fe)
|
||||
print(f"{time.time()-t0:.1f}s")
|
||||
|
||||
rs = max(RECENT_START, fs)
|
||||
re = min(RECENT_END, fe)
|
||||
print(f" 运行 {symbol} 近两年 ({rs.date()}~{re.date()})...", end=" ", flush=True)
|
||||
t0 = time.time()
|
||||
recent_results[symbol] = await run_backtest(symbol, rs, re)
|
||||
print(f"{time.time()-t0:.1f}s")
|
||||
|
||||
await ds.close()
|
||||
|
||||
# ── 全量 vs 近两年 对比表 ──
|
||||
print()
|
||||
print("═" * 145)
|
||||
print(" ATR波动率突破 1h — 全量 vs 近两年 (2023.06~2025.06)")
|
||||
print("═" * 145)
|
||||
print()
|
||||
header = f" {'币种':<10} | {'—————— 全量数据 ——————':>55} | {'—————— 近两年 ——————':>55}"
|
||||
print(header)
|
||||
sub = f" {'':<10} | {'本金':>6} {'终值':>8} {'总收益%':>8} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} | {'本金':>6} {'终值':>8} {'总收益%':>8} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}"
|
||||
print(sub)
|
||||
print(" " + "─" * 143)
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in full_results:
|
||||
continue
|
||||
f = full_results[symbol].metrics
|
||||
r = recent_results[symbol].metrics
|
||||
print(f" {symbol:<10} | {INITIAL:>6.0f} {f.final_equity:>8.0f} {f.total_return_pct:>7.1f}% {f.annual_return_pct:>6.1f}% {f.sharpe_ratio:>6.2f} {f.max_drawdown_pct:>6.1f}% {f.total_trades:>5} | {INITIAL:>6.0f} {r.final_equity:>8.0f} {r.total_return_pct:>7.1f}% {r.annual_return_pct:>6.1f}% {r.sharpe_ratio:>6.2f} {r.max_drawdown_pct:>6.1f}% {r.total_trades:>5}")
|
||||
|
||||
print("\n═" * 145)
|
||||
|
||||
# ── 近两年详细订单 ──
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in recent_results:
|
||||
continue
|
||||
result = recent_results[symbol]
|
||||
m = result.metrics
|
||||
rng = recent_results[symbol].config
|
||||
paired = pair_trades(result.trades)
|
||||
|
||||
print(f"\n{'─' * 145}")
|
||||
print(f" {symbol} 近两年 ({rng.start_time.date()}~{rng.end_time.date()}) — {len(paired)} 笔完整交易")
|
||||
print(f" 本金 {INITIAL:,.0f} → 终值 {m.final_equity:,.0f} | 总收益 {m.total_return_pct:.1f}% | 年化 {m.annual_return_pct:.1f}% | 夏普 {m.sharpe_ratio:.2f} | 回撤 {m.max_drawdown_pct:.1f}%")
|
||||
print(f" 胜率 {m.win_rate*100:.1f}% | 盈亏比 {m.profit_factor:.2f} | 最佳单笔 {m.best_trade_pnl:+,.0f} | 最差 {m.worst_trade_pnl:+,.0f} | 平均 {m.avg_trade_pnl:+,.0f}")
|
||||
print(f"{'─' * 145}")
|
||||
print(f" {'#':>3} {'类型':<5} {'入场时间':<19} {'入场价':>10} {'出场时间':<19} {'出场价':>10} {'盈亏':>12} {'入场原因':<30} {'出场原因':<30}")
|
||||
print(f" {'─' * 141}")
|
||||
|
||||
total_pnl = 0
|
||||
wins = 0
|
||||
for i, p in enumerate(paired):
|
||||
pnl = p["pnl"]
|
||||
total_pnl += pnl
|
||||
if pnl > 0:
|
||||
wins += 1
|
||||
pnl_str = f"{pnl:+,.0f}"
|
||||
print(f" {i+1:>3} {p['type']:<5} {p['entry_ts']:<19} {p['entry_price']:>10.4f} {p['exit_ts']:<19} {p['exit_price']:>10.4f} {pnl_str:>12} {p['entry_reason']:<30} {p['exit_reason']:<30}")
|
||||
|
||||
print(f" {'─' * 141}")
|
||||
print(f" 合计: {len(paired)} 笔 | 盈利 {wins} 笔 ({wins/len(paired)*100 if paired else 0:.0f}%) | 总盈亏 {total_pnl:+,.0f} USDT")
|
||||
|
||||
print(f"\n{'─' * 145}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user