""" 策略3 ATR波动率突破 — 全量 / 近两年 / 近一年 / 近半年 对比 用法: source .venv/bin/activate && python example/vol_break_periods.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 TODAY = datetime.now(timezone.utc) # 时间段定义 PERIODS = { "全量": (None, TODAY), # start 动态获取 "近两年": (TODAY - timedelta(days=365 * 2), TODAY), "近一年": (TODAY - timedelta(days=365), TODAY), "近半年": (TODAY - timedelta(days=182), TODAY), } 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}, } 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]: paired = [] pending_open = None for t in trades: if t.side == "BUY": if pending_open and pending_open["side"] == "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": 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) print(f" {symbol}: {s.date()} ~ {e.date()}") except Exception as ex: print(f" {symbol}: 获取范围失败 {ex}") # 运行所有时间段 all_results: dict[str, dict[str, object]] = {} # symbol -> period_name -> result for symbol in SYMBOLS: if symbol not in full_ranges: continue all_results[symbol] = {} fs, fe = full_ranges[symbol] for period_name, (ps, pe) in PERIODS.items(): start = fs if period_name == "全量" else max(ps, fs) end = min(pe, fe) if start >= end: print(f" {symbol} {period_name}: 无有效数据范围,跳过") continue print(f" 运行 {symbol} {period_name} ({start.date()}~{end.date()})...", end=" ", flush=True) t0 = time.time() try: all_results[symbol][period_name] = await run_backtest(symbol, start, end) print(f"{time.time()-t0:.1f}s") except Exception as ex: print(f"错误: {ex}") await ds.close() # ════════════════════════════════════════════════════ # 对比表 # ════════════════════════════════════════════════════ print() print("═" * 165) print(f" ATR波动率突破 1h — 全量 / 近两年 / 近一年 / 近半年 对比 ({TODAY.strftime('%Y-%m-%d')} 截止)") print("═" * 165) print() col_w = 36 for symbol in SYMBOLS: if symbol not in all_results: continue print(f" ■ {symbol}") print(f" {'时段':<8} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>5} {'数据范围'}") print(" " + "─" * 163) for period_name in PERIODS: if period_name not in all_results[symbol]: continue r = all_results[symbol][period_name] m = r.metrics data_range = f"{r.config.start_time.date()}~{r.config.end_time.date()}" print(f" {period_name:<8} {INITIAL:>7.0f} {m.final_equity:>9.0f} {m.total_return_pct:>7.1f}% {m.annual_return_pct:>7.1f}% {m.sharpe_ratio:>7.2f} {m.max_drawdown_pct:>7.1f}% {m.win_rate*100:>6.1f}% {m.profit_factor:>7.2f} {m.total_trades:>5} {data_range}") print() # ════════════════════════════════════════════════════ # 详细订单 # ════════════════════════════════════════════════════ print("═" * 165) print(" 各时段详细订单") print("═" * 165) for symbol in SYMBOLS: if symbol not in all_results: continue for period_name in PERIODS: if period_name not in all_results[symbol]: continue result = all_results[symbol][period_name] m = result.metrics paired = pair_trades(result.trades) if len(paired) == 0: continue print(f"\n{'─' * 155}") print(f" {symbol} {period_name} — {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" 数据范围: {result.config.start_time.date()} ~ {result.config.end_time.date()}") print(f"{'─' * 155}") print(f" {'#':>3} {'类型':<5} {'入场时间':<19} {'入场价':>10} {'出场时间':<19} {'出场价':>10} {'盈亏':>12} {'入场原因':<35} {'出场原因':<30}") print(f" {'─' * 153}") 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']:<35} {p['exit_reason']:<30}") print(f" {'─' * 153}") print(f" 合计: {len(paired)} 笔 | 盈利 {wins} 笔 ({wins/len(paired)*100 if paired else 0:.0f}%) | 总盈亏 {total_pnl:+,.0f} USDT") print(f"\n{'─' * 155}") print() if __name__ == "__main__": asyncio.run(main())