""" 牛熊自适应策略 — 多时间级别回测对比 2h / 4h / 6h / 1d × 全量数据 / 近两年 (2024.06-2026.06) 用法: source .venv/bin/activate && python example/regime_timeframe_comparison.py """ import asyncio import sys from datetime import datetime, 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.base import BaseStrategy, Signal, StrategyConfig from engine.common.models import Kline from engine.common.config import config from engine.backtest import BacktestConfig from engine.indicators import ema, atr from engine.data import DataService from engine.example.long_short import LongShortEngine from engine.example.regime_all import RegimeEmaConfig, RegimeEmaStrategy # ═══════════════════════════════════ # 配置 # ═══════════════════════════════════ SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"] PARAMS = { "BTCUSDT": (10, 50), "ETHUSDT": (10, 75), "BNBUSDT": (20, 50), "SOLUSDT": (30, 50), } INTERVALS = ["2h", "4h", "6h", "1d"] # 近两年:2024年6月 → 2026年6月 YEAR_START = datetime(2024, 6, 1) YEAR_END = datetime(2026, 6, 12) FULL_DEFAULT_START = datetime(2017, 1, 1) async def get_actual_range(symbol: str, interval: str) -> tuple[datetime, datetime]: ds = DataService(config.db) await ds.connect() try: start, end = await ds.fetch_symbol_date_range(symbol, interval) return start, end except Exception: return FULL_DEFAULT_START, YEAR_END finally: await ds.close() async def run_one(symbol: str, interval: str, start: datetime, end: datetime): fast, slow = PARAMS[symbol] sc = RegimeEmaConfig(symbol=symbol, fast=fast, slow=slow) bt = BacktestConfig( symbol=symbol, interval=interval, start_time=start, end_time=end, initial_capital=10_000.0, warmup_bars=250, ) engine = LongShortEngine(bt, db_config=config.db) return await engine.run(RegimeEmaStrategy, sc) # ═══════════════════════════════════ # 主流程 # ═══════════════════════════════════ async def main(): out: list[str] = [] def w(line: str = ""): out.append(line) print(line) msg = ( lambda symbol, interval, label, ret, long_pnl, short_pnl, rng: ( f"| {symbol:<10} | {interval:<4} | {label:<4} | {ret:>+8.1f}% | " f"{r.metrics.annual_return_pct:>+7.1f}% | {r.metrics.sharpe_ratio:>6.2f} | " f"{r.metrics.max_drawdown_pct:>7.1f}% | {r.metrics.total_trades:>5} | " f"{r.metrics.win_rate*100:>6.1f}% | {r.metrics.profit_factor:>6.2f} | " f"{long_pnl:>+9.0f} | {short_pnl:>+9.0f} | {rng} |" ) ) all_rows: list[dict] = [] w("# 牛熊自适应策略 — 多时间级别回测对比") w() w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}") w() # ── 一、全量数据 ── w("## 一、全量数据(所有可用历史)") w() for interval in INTERVALS: w(f"### {interval} 周期") w() w( "| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |" ) w( "|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|" ) for symbol in SYMBOLS: try: act_start, act_end = await get_actual_range(symbol, interval) rng = f"{act_start.date()}~{act_end.date()}" except Exception: act_start, act_end = FULL_DEFAULT_START, YEAR_END rng = "2017-2026" try: r = await run_one(symbol, interval, act_start, act_end) m = r.metrics long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"] short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"] lp = sum(t.pnl for t in long_t) if long_t else 0 sp = sum(t.pnl for t in short_t) if short_t else 0 row = m.total_return_pct w( f"| {symbol:<10} | {interval:<4} | 全量 | {row:>+8.1f}% | " f"{m.annual_return_pct:>+7.1f}% | {m.sharpe_ratio:>6.2f} | " f"{m.max_drawdown_pct:>7.1f}% | {m.total_trades:>5} | " f"{m.win_rate*100:>6.1f}% | {m.profit_factor:>6.2f} | " f"{lp:>+9.0f} | {sp:>+9.0f} | {rng} |" ) all_rows.append( { "symbol": symbol, "interval": interval, "label": "全量", "rng": rng, "return": m.total_return_pct, "annual": m.annual_return_pct, "sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct, "trades": m.total_trades, "win": m.win_rate, "pf": m.profit_factor, } ) except Exception as e: w( f"| {symbol:<10} | {interval:<4} | 全量 | — | — | — | — | — | — | — | — | — | 错误 |" ) print(f" ✗ {symbol} {interval} 全量: {e}") w() # ── 二、近两年 ── w("## 二、近两年(2024.06 — 2026.06)") w() for interval in INTERVALS: w(f"### {interval} 周期") w() w( "| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |" ) w( "|------|------|------|--------|------|------|------|------|------|------|---------|---------|" ) for symbol in SYMBOLS: try: r = await run_one(symbol, interval, YEAR_START, YEAR_END) m = r.metrics long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"] short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"] lp = sum(t.pnl for t in long_t) if long_t else 0 sp = sum(t.pnl for t in short_t) if short_t else 0 row = m.total_return_pct w( f"| {symbol:<10} | {interval:<4} | 近2年 | {row:>+8.1f}% | " f"{m.annual_return_pct:>+7.1f}% | {m.sharpe_ratio:>6.2f} | " f"{m.max_drawdown_pct:>7.1f}% | {m.total_trades:>5} | " f"{m.win_rate*100:>6.1f}% | {m.profit_factor:>6.2f} | " f"{lp:>+9.0f} | {sp:>+9.0f} |" ) all_rows.append( { "symbol": symbol, "interval": interval, "label": "近2年", "rng": "2024.06~2026.06", "return": m.total_return_pct, "annual": m.annual_return_pct, "sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct, "trades": m.total_trades, "win": m.win_rate, "pf": m.profit_factor, } ) except Exception as e: w( f"| {symbol:<10} | {interval:<4} | 近1年 | — | — | — | — | — | — | — | — | — |" ) print(f" ✗ {symbol} {interval} 近2年: {e}") w() # ── 三、汇总 ── w("---") w() w("## 三、全维度汇总") w() w( "| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |" ) w( "|------|------|------|--------|------|------|------|------|------|" ) for row in sorted(all_rows, key=lambda x: (x["symbol"], x["label"], x["interval"])): w( f"| {row['symbol']:<10} | {row['interval']:<4} | {row['label']:<4} | " f"{row['return']:>+8.1f}% | {row['sharpe']:>6.2f} | " f"{row['dd']:>7.1f}% | {row['trades']:>5} | " f"{row['win']*100:>6.1f}% | {row['pf']:>6.2f} |" ) # ── 四、最优组合 ── w() w("## 四、各币种最佳组合(按夏普排序)") w() w( "| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |" ) w( "|------|------|------|--------|------|------|------|------|------|" ) for symbol in SYMBOLS: candidates = [x for x in all_rows if x["symbol"] == symbol] if not candidates: continue best = max(candidates, key=lambda x: x["sharpe"]) w( f"| {best['symbol']:<10} | **{best['interval']}** | {best['label']} | " f"{best['return']:>+8.1f}% | {best['annual']:>+7.1f}% | {best['sharpe']:>6.2f} | " f"{best['dd']:>7.1f}% | {best['trades']:>5} | {best['win']*100:>6.1f}% |" ) w() w("---") w() w("## 五、结论") w() w("- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h") w("- **全量 vs 近两年**:近两年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期") w("- **交易频率**:1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略") w() # 写出文件 out_path = ( Path(__file__).resolve().parent.parent / "backtest" / "TIMEFRAME_COMPARISON_2H_6H.md" ) with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(out) + "\n") print(f"\n✓ 结果已保存到: {out_path}") if __name__ == "__main__": asyncio.run(main())