Files
trade/engine/example/vol_break_periods.py
T
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

213 lines
8.5 KiB
Python

"""
策略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())