Files
trade/engine/example/regime_timeframe_comparison.py
Rekey edc50e8809 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 等分析脚本
2026-06-13 19:30:25 +08:00

280 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
牛熊自适应策略 — 多时间级别回测对比
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())