feat: 多周期牛熊判定模块 — 方案一矩阵展示 + 四法投票 + 多TF策略
- engine/indicators/regime.py: RegimeDetector(四法投票) + MultiTimeframeRegime(多周期并行) 四法: EMA200斜率 / 价格vsEMA200 / ATH回撤 / 窄幅盘整(<3%振幅) 全部 O(1)/bar 增量计算,适用于回测和实时 - engine/example/regime_display.py: 多周期牛熊矩阵展示脚本 独立加载各周期数据 → 运行判定 → 日线对齐矩阵 + 详细拆解 + 统计 输出 engine/backtest/REGIME_MATRIX_BTCUSDT.md - engine/example/regime_mtf_strategy.py: 多周期共识策略 + 四策略对比回测 MTF Consensus: 1w定方向 + 1d确认 + 4h EMA入场 vs Old Regime(单TF基线) vs Long/Short(无过滤) - engine/indicators/__init__.py: 导出 RegimeDetector, MultiTimeframeRegime
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
多周期牛熊矩阵 — 方案一:独立多周期判定矩阵
|
||||
|
||||
在统一时间轴(1d bar 边界)上对齐全周期牛熊判定,
|
||||
输出矩阵表格、统计信息和详细拆解。
|
||||
|
||||
用法:
|
||||
source .venv/bin/activate && python example/regime_display.py
|
||||
|
||||
定制:
|
||||
python example/regime_display.py --symbol ETHUSDT
|
||||
python example/regime_display.py --matrix-rows 50 # 输出最近 50 个时间点
|
||||
"""
|
||||
|
||||
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.config import config
|
||||
from engine.data.service import DataService, INTERVAL_MS, INTERVAL_TO_TABLE
|
||||
from engine.indicators.regime import RegimeDetector
|
||||
|
||||
# ── 默认配置 ──
|
||||
|
||||
SYMBOL = "BTCUSDT"
|
||||
TIMEFRAMES = ["1h", "4h", "1d", "1w"]
|
||||
MATRIX_ROWS = 30
|
||||
OUTPUT_FILE = None # 运行时设置
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
args = sys.argv[1:]
|
||||
kwargs = {"symbol": SYMBOL, "matrix_rows": MATRIX_ROWS}
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--symbol" and i + 1 < len(args):
|
||||
kwargs["symbol"] = args[i + 1].upper()
|
||||
i += 2
|
||||
elif args[i] == "--matrix-rows" and i + 1 < len(args):
|
||||
kwargs["matrix_rows"] = int(args[i + 1])
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
return kwargs
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 1. 加载各周期数据并运行独立判定
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
async def load_and_detect(
|
||||
ds: DataService, symbol: str, timeframe: str
|
||||
) -> tuple[list[float], list[dict], datetime, datetime]:
|
||||
"""加载指定周期全量数据,逐根运行 RegimeDetector,返回 (times, regimes, 数据起始, 数据结束)
|
||||
|
||||
regimes[i] = {"time_ms": float, "close": float, "regime": str, "detail": dict}
|
||||
仅从第 220 根 bar 开始有效判定,之前为 unknown。
|
||||
"""
|
||||
start, end = await ds.fetch_symbol_date_range(symbol, timeframe)
|
||||
all_klines = await ds.fetch_klines(
|
||||
symbol=symbol,
|
||||
interval=timeframe,
|
||||
start_time=start,
|
||||
end_time=end,
|
||||
limit=10_000_000,
|
||||
)
|
||||
|
||||
det = RegimeDetector()
|
||||
regimes = []
|
||||
times = []
|
||||
for k in all_klines:
|
||||
times.append(k.open_time)
|
||||
det.update(k.close)
|
||||
if det.ready:
|
||||
regime = det.detect(k.close, len(det._e200) - 1)
|
||||
detail = det.detect_detail(k.close, len(det._e200) - 1)
|
||||
else:
|
||||
regime = "unknown"
|
||||
detail = {"final": "unknown"}
|
||||
regimes.append({
|
||||
"time_ms": k.open_time,
|
||||
"time_dt": datetime.fromtimestamp(k.open_time / 1000, tz=timezone.utc),
|
||||
"close": k.close,
|
||||
"regime": regime,
|
||||
"detail": detail,
|
||||
"ema200": det.ema200,
|
||||
"ath": det.ath,
|
||||
})
|
||||
|
||||
return times, regimes, start, end
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 2. 时间对齐 — 以日线为基准,查找各周期的即时判定
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def find_regime_at(
|
||||
regimes: list[dict], target_ms: float, interval_ms: float
|
||||
) -> dict:
|
||||
"""在指定周期的 regime 序列中,找到 target_ms 时刻的判定。
|
||||
|
||||
规则:取开盘时间最接近且不晚于 target_ms 的那根 bar 的判定。
|
||||
若 target_ms 在该 bar 的 [open_time, open_time + interval_ms) 范围内,返回该 bar 的判定。
|
||||
"""
|
||||
best = None
|
||||
for r in regimes:
|
||||
bar_time = r["time_ms"]
|
||||
if bar_time <= target_ms < bar_time + interval_ms:
|
||||
return r
|
||||
if bar_time > target_ms:
|
||||
break
|
||||
best = r
|
||||
return best
|
||||
|
||||
|
||||
def build_matrix(
|
||||
times_by_tf: dict[str, list[float]],
|
||||
regimes_by_tf: dict[str, list[dict]],
|
||||
reference_tf: str = "1d",
|
||||
num_rows: int = MATRIX_ROWS,
|
||||
) -> list[dict]:
|
||||
"""以 reference_tf 的 bar 时间点为基准,构建多周期判定矩阵。
|
||||
|
||||
Returns:
|
||||
[{time_dt, time_str, 1h: regime, 4h: regime, 1d: regime, 1w: regime}, ...]
|
||||
按时间倒序,只包含各周期均有有效数据的时间点。
|
||||
"""
|
||||
ref_regimes = regimes_by_tf[reference_tf]
|
||||
# 只取有效区间(所有周期都过了 220 根 bar 之后)
|
||||
ref_valid = [
|
||||
r for r in ref_regimes
|
||||
if r["regime"] != "unknown"
|
||||
]
|
||||
|
||||
matrix = []
|
||||
for ref_r in ref_valid[-num_rows:]:
|
||||
row = {
|
||||
"time_dt": ref_r["time_dt"],
|
||||
"time_str": ref_r["time_dt"].strftime("%Y-%m-%d %H:%M"),
|
||||
"time_ms": ref_r["time_ms"],
|
||||
f"{reference_tf}_close": ref_r["close"],
|
||||
}
|
||||
for tf in TIMEFRAMES:
|
||||
if tf == reference_tf:
|
||||
row[tf] = ref_r["regime"]
|
||||
row[f"{tf}_detail"] = ref_r["detail"]
|
||||
row[f"{tf}_ema200"] = ref_r.get("ema200", 0)
|
||||
row[f"{tf}_ath"] = ref_r.get("ath", 0)
|
||||
else:
|
||||
tf_regime = find_regime_at(
|
||||
regimes_by_tf[tf],
|
||||
ref_r["time_ms"],
|
||||
INTERVAL_MS[tf],
|
||||
)
|
||||
if tf_regime:
|
||||
row[tf] = tf_regime["regime"]
|
||||
row[f"{tf}_detail"] = tf_regime["detail"]
|
||||
row[f"{tf}_ema200"] = tf_regime.get("ema200", 0)
|
||||
row[f"{tf}_ath"] = tf_regime.get("ath", 0)
|
||||
else:
|
||||
row[tf] = "—"
|
||||
matrix.append(row)
|
||||
|
||||
# 倒序(最新在前)
|
||||
return list(reversed(matrix))
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 3. 统计分析
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def regime_icon(r: str) -> str:
|
||||
return {"bull": "🐂", "bear": "🐻", "sideways": "⚪", "unknown": "⏳", "—": "—"}.get(r, r)
|
||||
|
||||
|
||||
def calc_alignment_stats(matrix: list[dict]) -> dict:
|
||||
"""计算各周期判定对齐统计"""
|
||||
total = len(matrix)
|
||||
if total == 0:
|
||||
return {}
|
||||
|
||||
stats = {}
|
||||
for tf in TIMEFRAMES:
|
||||
cnt_bull = sum(1 for r in matrix if r.get(tf) == "bull")
|
||||
cnt_bear = sum(1 for r in matrix if r.get(tf) == "bear")
|
||||
cnt_side = sum(1 for r in matrix if r.get(tf) == "sideways")
|
||||
stats[tf] = {
|
||||
"bull": cnt_bull,
|
||||
"bull_pct": cnt_bull / total * 100,
|
||||
"bear": cnt_bear,
|
||||
"bear_pct": cnt_bear / total * 100,
|
||||
"sideways": cnt_side,
|
||||
"sideways_pct": cnt_side / total * 100,
|
||||
}
|
||||
|
||||
# 全周期一致(所有非 "—" 的周期判定相同)
|
||||
all_agree = 0
|
||||
for r in matrix:
|
||||
regimes = [r[tf] for tf in TIMEFRAMES if r.get(tf) not in ("—", "unknown")]
|
||||
if regimes and len(set(regimes)) == 1:
|
||||
all_agree += 1
|
||||
stats["all_agree"] = all_agree
|
||||
stats["all_agree_pct"] = all_agree / total * 100
|
||||
|
||||
# 大小周期背离(1h 与 1w 相反)
|
||||
diverge = 0
|
||||
for r in matrix:
|
||||
r1h = r.get("1h")
|
||||
r1w = r.get("1w")
|
||||
if r1h and r1w and r1h != "—" and r1w != "—":
|
||||
if (r1h == "bull" and r1w == "bear") or (r1h == "bear" and r1w == "bull"):
|
||||
diverge += 1
|
||||
stats["diverge_1h_1w"] = diverge
|
||||
stats["diverge_1h_1w_pct"] = diverge / total * 100
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 4. 输出格式化
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def render_matrix_table(matrix: list[dict]) -> str:
|
||||
"""渲染矩阵表格为 Markdown 格式"""
|
||||
header = "| 时间 |"
|
||||
sep = "|------|"
|
||||
for tf in TIMEFRAMES:
|
||||
header += f" {tf} |"
|
||||
sep += "----|"
|
||||
header += " BTC Close |"
|
||||
sep += "-----------|"
|
||||
|
||||
lines = [header, sep]
|
||||
for row in matrix:
|
||||
line = f"| {row['time_str']} |"
|
||||
for tf in TIMEFRAMES:
|
||||
r = row.get(tf, "—")
|
||||
line += f" {regime_icon(r)} {r} |"
|
||||
close_str = f"{row.get('1d_close', 0):.2f}"
|
||||
line += f" {close_str} |"
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_detail_table(matrix: list[dict], tf: str, n: int = 5) -> str:
|
||||
"""渲染单个周期的详细判定表(最近 n 条)"""
|
||||
lines = [
|
||||
f"### {tf} 详细判定(最近 {n} 个时间点)",
|
||||
"",
|
||||
"| 时间 | 最终 | EMA200斜率 | 价格vsEMA200 | ATH回撤 | 窄幅盘整 | 振幅% | 牛票 | 熊票 | 震票 | EMA200 | ATH |",
|
||||
"|------|------|-----------|-------------|---------|---------|------|------|------|------|--------|-----|",
|
||||
]
|
||||
|
||||
detail_key = f"{tf}_detail"
|
||||
for row in matrix[:n]:
|
||||
d = row.get(detail_key, {})
|
||||
f_icon = regime_icon(d.get("final", "—"))
|
||||
ema200_val = row.get(f"{tf}_ema200", 0)
|
||||
ath_val = row.get(f"{tf}_ath", 0)
|
||||
range_pct = d.get("range_pct", 0) * 100
|
||||
lines.append(
|
||||
f"| {row['time_str']} | {f_icon} {d.get('final', '—')} | "
|
||||
f"{d.get('ema200_slope', '—')} | {d.get('price_vs_ema200', '—')} | "
|
||||
f"{d.get('ath_drawdown', '—')} | {d.get('price_range', '—')} | "
|
||||
f"{range_pct:.1f}% | {d.get('bull_votes', '—')} | "
|
||||
f"{d.get('bear_votes', '—')} | {d.get('sideways_votes', '—')} | "
|
||||
f"{ema200_val:.2f} | {ath_val:.2f} |"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_current_status(regimes_by_tf: dict[str, list[dict]]) -> str:
|
||||
"""渲染当前最新状态"""
|
||||
lines = [
|
||||
"## 当前各周期牛熊状态",
|
||||
"",
|
||||
"| 周期 | 判定 | EMA200斜率 | 价格vsEMA200 | ATH回撤 | 窄幅盘整 | 振幅% | 牛票 | 熊票 | 震票 | EMA200 | Close |",
|
||||
"|------|------|-----------|-------------|---------|---------|------|------|------|------|--------|-------|",
|
||||
]
|
||||
|
||||
for tf in TIMEFRAMES:
|
||||
regs = regimes_by_tf[tf]
|
||||
if not regs or regs[-1]["regime"] == "unknown":
|
||||
lines.append(f"| {tf} | ⏳ 数据不足 | — | — | — | — | — | — | — | — | — | — |")
|
||||
continue
|
||||
r = regs[-1]
|
||||
d = r["detail"]
|
||||
icon = regime_icon(r["regime"])
|
||||
range_pct = d.get("range_pct", 0) * 100
|
||||
lines.append(
|
||||
f"| {tf} | {icon} **{r['regime']}** | "
|
||||
f"{d.get('ema200_slope', '—')} | {d.get('price_vs_ema200', '—')} | "
|
||||
f"{d.get('ath_drawdown', '—')} | {d.get('price_range', '—')} | "
|
||||
f"{range_pct:.1f}% | {d.get('bull_votes', '—')} | "
|
||||
f"{d.get('bear_votes', '—')} | {d.get('sideways_votes', '—')} | "
|
||||
f"{r.get('ema200', 0):.2f} | {r['close']:.2f} |"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_stats(stats: dict) -> str:
|
||||
"""渲染统计信息"""
|
||||
lines = [
|
||||
"## 周期判定分布统计",
|
||||
"",
|
||||
"| 周期 | 🐂 牛市 | 占比 | 🐻 熊市 | 占比 | ⚪ 震荡 | 占比 |",
|
||||
"|------|--------|------|--------|------|--------|------|",
|
||||
]
|
||||
for tf in TIMEFRAMES:
|
||||
s = stats.get(tf, {})
|
||||
lines.append(
|
||||
f"| {tf} | {s.get('bull', 0)} | {s.get('bull_pct', 0):.1f}% | "
|
||||
f"{s.get('bear', 0)} | {s.get('bear_pct', 0):.1f}% | "
|
||||
f"{s.get('sideways', 0)} | {s.get('sideways_pct', 0):.1f}% |"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("## 周期一致性统计")
|
||||
lines.append("")
|
||||
lines.append(f"- **全周期一致**(所有周期判定相同):{stats.get('all_agree', 0)} 天 ({stats.get('all_agree_pct', 0):.1f}%)")
|
||||
lines.append(f"- **1h ↔ 1w 背离**(1h 与 1w 方向相反):{stats.get('diverge_1h_1w', 0)} 天 ({stats.get('diverge_1h_1w_pct', 0):.1f}%)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 主流程
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
async def main():
|
||||
kw = parse_args()
|
||||
symbol = kw["symbol"]
|
||||
matrix_rows = kw["matrix_rows"]
|
||||
global OUTPUT_FILE
|
||||
OUTPUT_FILE = (
|
||||
Path(__file__).resolve().parent.parent
|
||||
/ "backtest"
|
||||
/ f"REGIME_MATRIX_{symbol}.md"
|
||||
)
|
||||
|
||||
out: list[str] = []
|
||||
|
||||
def w(line: str = ""):
|
||||
out.append(line)
|
||||
print(line)
|
||||
|
||||
w(f"# 多周期牛熊判定矩阵 — {symbol}")
|
||||
w()
|
||||
w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC")
|
||||
w(f"> 判定方法:EMA200斜率 + 价格vsEMA200 + ATH回撤 + 窄幅盘整,四选二投票")
|
||||
w(f"> 时间对齐基准:日线(1d) bar 边界")
|
||||
w()
|
||||
|
||||
# ── 加载数据并运行判定 ──
|
||||
ds = DataService(config.db)
|
||||
await ds.connect()
|
||||
|
||||
try:
|
||||
times_by_tf: dict[str, list[float]] = {}
|
||||
regimes_by_tf: dict[str, list[dict]] = {}
|
||||
date_ranges: dict[str, tuple[datetime, datetime]] = {}
|
||||
|
||||
for tf in TIMEFRAMES:
|
||||
print(f" 加载 {tf} 数据...", end=" ", flush=True)
|
||||
times, regimes, data_start, data_end = await load_and_detect(ds, symbol, tf)
|
||||
times_by_tf[tf] = times
|
||||
regimes_by_tf[tf] = regimes
|
||||
date_ranges[tf] = (data_start, data_end)
|
||||
valid_count = sum(1 for r in regimes if r["regime"] != "unknown")
|
||||
print(f"{len(regimes)} 根 bar,{valid_count} 根有效(>220热身)")
|
||||
|
||||
# ── 数据范围 ──
|
||||
w()
|
||||
w("## 数据覆盖范围")
|
||||
w()
|
||||
w("| 周期 | 数据条数 | 有效条数 | 起始时间 | 结束时间 |")
|
||||
w("|------|---------|---------|---------|---------|")
|
||||
for tf in TIMEFRAMES:
|
||||
regs = regimes_by_tf[tf]
|
||||
valid = sum(1 for r in regs if r["regime"] != "unknown")
|
||||
s, e = date_ranges[tf]
|
||||
w(f"| {tf} | {len(regs)} | {valid} | {s.strftime('%Y-%m-%d')} | {e.strftime('%Y-%m-%d')} |")
|
||||
w()
|
||||
|
||||
# ── 当前状态 ──
|
||||
w(render_current_status(regimes_by_tf))
|
||||
w()
|
||||
|
||||
# ── 历史矩阵 ──
|
||||
matrix = build_matrix(times_by_tf, regimes_by_tf, "1d", num_rows=matrix_rows)
|
||||
|
||||
w(f"## 历史牛熊矩阵(最近 {len(matrix)} 个日线时间点)")
|
||||
w()
|
||||
w("> 以日线 bar 的 open_time 为基准,查找各周期在该时刻的即时判定。")
|
||||
w()
|
||||
w(render_matrix_table(matrix))
|
||||
w()
|
||||
|
||||
# ── 各周期详细拆解 ──
|
||||
w("## 各周期详细拆解")
|
||||
w()
|
||||
for tf in TIMEFRAMES:
|
||||
w(render_detail_table(matrix, tf, n=min(5, len(matrix))))
|
||||
w()
|
||||
|
||||
# ── 统计 ──
|
||||
stats = calc_alignment_stats(matrix)
|
||||
w(render_stats(stats))
|
||||
w()
|
||||
|
||||
finally:
|
||||
await ds.close()
|
||||
|
||||
# ── 写出文件 ──
|
||||
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(out) + "\n")
|
||||
|
||||
print(f"\n✓ 结果已保存到: {OUTPUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
多周期牛熊共识策略 — 四周期协同判定
|
||||
|
||||
核心思路:
|
||||
1w 定宏观方向(只做多 / 只做空)→ 1d 确认中周期 → 4h EMA 金叉/死叉入场
|
||||
配合 ATR 动态止损 + 多周期投票确认出场
|
||||
|
||||
对比基准:单周期 RegimeEmaStrategy(regime_all.py)、无过滤 LongShortEmaStrategy
|
||||
|
||||
用法:
|
||||
source .venv/bin/activate && python example/regime_mtf_strategy.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
_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.incremental import EmaInc, AtrInc
|
||||
from engine.indicators.regime import MultiTimeframeRegime
|
||||
from engine.example.long_short import LongShortEngine
|
||||
from engine.example.regime_all import RegimeEmaConfig as OldRegimeCfg
|
||||
from engine.example.regime_all import RegimeEmaStrategy as OldRegimeS
|
||||
from engine.example.long_short import LongShortEmaConfig as LSCfg
|
||||
from engine.example.long_short import LongShortEmaStrategy as LSS
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 多周期牛熊共识策略
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class MTFConfig(StrategyConfig):
|
||||
fast: int = 10
|
||||
slow: int = 50
|
||||
atr_stop: float = 2.5
|
||||
|
||||
|
||||
class MTFRegimeStrategy(BaseStrategy):
|
||||
"""四周期协同牛熊策略
|
||||
|
||||
判定层(MultiTimeframeRegime,即时法):
|
||||
每个 4h bar 的 close 同时更新 1h / 4h / 1d / 1w 四个检测器,
|
||||
1w 定方向宏调,1d 确认中周期,1h 预警微观背离。
|
||||
|
||||
入场:
|
||||
做多 — 1w bull + 1d not bear + 4h EMA 金叉
|
||||
做空 — 1w bear + 1d not bull + 4h EMA 死叉
|
||||
|
||||
出场:
|
||||
EMA 交叉反转、ATR 跟踪止损、高周期方向逆转
|
||||
"""
|
||||
|
||||
strategy_type = "mtf_regime"
|
||||
|
||||
def __init__(self, c: MTFConfig):
|
||||
super().__init__(c)
|
||||
self.cfg = c
|
||||
self._mtf = MultiTimeframeRegime(["1h", "4h", "1d", "1w"])
|
||||
self._ema_fast = EmaInc(c.fast)
|
||||
self._ema_slow = EmaInc(c.slow)
|
||||
self._atr = AtrInc(14)
|
||||
self._side: str = ""
|
||||
self._hp: float = 0.0
|
||||
self._lp: float = float("inf")
|
||||
|
||||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||||
self._mtf.update(k.close)
|
||||
self._ema_fast.update(k.close)
|
||||
self._ema_slow.update(k.close)
|
||||
self._atr.update(k.high, k.low, k.close)
|
||||
|
||||
n = len(self._mtf._prices)
|
||||
if n < 220:
|
||||
return None
|
||||
|
||||
regimes = self._mtf.detect_all()
|
||||
r1h = regimes.get("1h", "unknown")
|
||||
r4h = regimes.get("4h", "unknown")
|
||||
r1d = regimes.get("1d", "unknown")
|
||||
r1w = regimes.get("1w", "unknown")
|
||||
|
||||
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
|
||||
ca = self._atr[-1]
|
||||
pf, ps = self._ema_fast[-2], self._ema_slow[-2]
|
||||
if cf == 0 or cs == 0 or ca == 0:
|
||||
return None
|
||||
|
||||
golden = pf <= ps and cf > cs
|
||||
death = pf >= ps and cf < cs
|
||||
|
||||
# ── 多头持仓 ──
|
||||
if self._side == "long":
|
||||
self._hp = max(self._hp, k.high)
|
||||
stop = self._hp - self.cfg.atr_stop * ca
|
||||
|
||||
exit_reason = None
|
||||
if death:
|
||||
exit_reason = "死叉"
|
||||
elif k.close < stop:
|
||||
exit_reason = "ATR止损"
|
||||
elif r1w == "bear":
|
||||
exit_reason = "1w转熊→平多"
|
||||
elif r1d == "bear" and r4h == "bear":
|
||||
exit_reason = "1d+4h双双转熊→平多"
|
||||
|
||||
if exit_reason:
|
||||
self._side = ""
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL",
|
||||
reason=exit_reason, timestamp=k.open_time)
|
||||
|
||||
# ── 空头持仓 ──
|
||||
elif self._side == "short":
|
||||
self._lp = min(self._lp, k.low)
|
||||
stop = self._lp + self.cfg.atr_stop * ca
|
||||
|
||||
exit_reason = None
|
||||
if golden:
|
||||
exit_reason = "金叉"
|
||||
elif k.close > stop:
|
||||
exit_reason = "ATR止损"
|
||||
elif r1w == "bull":
|
||||
exit_reason = "1w转牛→平空"
|
||||
elif r1d == "bull" and r4h == "bull":
|
||||
exit_reason = "1d+4h双双转牛→平空"
|
||||
|
||||
if exit_reason:
|
||||
self._side = ""
|
||||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||||
reason=exit_reason, timestamp=k.open_time)
|
||||
|
||||
# ── 空仓等信号 ──
|
||||
else:
|
||||
# 做多条件:1w牛 + 1d不熊 + 4h金叉
|
||||
long_ok = r1w == "bull" and r1d != "bear" and golden
|
||||
# 做空条件:1w熊 + 1d不牛 + 4h死叉
|
||||
short_ok = r1w == "bear" and r1d != "bull" and death
|
||||
|
||||
if long_ok:
|
||||
self._side = "long"
|
||||
self._hp = k.close
|
||||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||||
reason=f"多周期做多({r1w}/{r1d}/{r4h})",
|
||||
timestamp=k.open_time)
|
||||
|
||||
if short_ok:
|
||||
self._side = "short"
|
||||
self._lp = k.close
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL",
|
||||
reason=f"多周期做空({r1w}/{r1d}/{r4h})",
|
||||
timestamp=k.open_time)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 对照策略 2:1w 绝对过滤(更强约束)
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class MTFStrictConfig(StrategyConfig):
|
||||
fast: int = 10
|
||||
slow: int = 50
|
||||
atr_stop: float = 2.5
|
||||
|
||||
|
||||
class MTFStrictStrategy(BaseStrategy):
|
||||
"""严格多周期策略 — 必须 1w + 1d 方向一致才入场
|
||||
|
||||
入场:
|
||||
做多 — 1w bull + 1d bull + 4h 金叉
|
||||
做空 — 1w bear + 1d bear + 4h 死叉
|
||||
出场:
|
||||
4h 死叉/金叉 或 ATR 止损 或 1w 方向逆转
|
||||
"""
|
||||
|
||||
strategy_type = "mtf_strict"
|
||||
|
||||
def __init__(self, c: MTFStrictConfig):
|
||||
super().__init__(c)
|
||||
self.cfg = c
|
||||
self._mtf = MultiTimeframeRegime(["1h", "4h", "1d", "1w"])
|
||||
self._ema_fast = EmaInc(c.fast)
|
||||
self._ema_slow = EmaInc(c.slow)
|
||||
self._atr = AtrInc(14)
|
||||
self._side: str = ""
|
||||
self._hp: float = 0.0
|
||||
self._lp: float = float("inf")
|
||||
|
||||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||||
self._mtf.update(k.close)
|
||||
self._ema_fast.update(k.close)
|
||||
self._ema_slow.update(k.close)
|
||||
self._atr.update(k.high, k.low, k.close)
|
||||
|
||||
n = len(self._mtf._prices)
|
||||
if n < 220:
|
||||
return None
|
||||
|
||||
regimes = self._mtf.detect_all()
|
||||
r4h = regimes.get("4h", "unknown")
|
||||
r1d = regimes.get("1d", "unknown")
|
||||
r1w = regimes.get("1w", "unknown")
|
||||
|
||||
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
|
||||
ca = self._atr[-1]
|
||||
pf, ps = self._ema_fast[-2], self._ema_slow[-2]
|
||||
if cf == 0 or cs == 0 or ca == 0:
|
||||
return None
|
||||
|
||||
golden = pf <= ps and cf > cs
|
||||
death = pf >= ps and cf < cs
|
||||
|
||||
# 多头持仓
|
||||
if self._side == "long":
|
||||
self._hp = max(self._hp, k.high)
|
||||
stop = self._hp - self.cfg.atr_stop * ca
|
||||
if death or k.close < stop or r1w == "bear":
|
||||
self._side = ""
|
||||
reason = "死叉" if death else ("ATR止损" if k.close < stop else "1w转熊")
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL",
|
||||
reason=reason, timestamp=k.open_time)
|
||||
|
||||
# 空头持仓
|
||||
elif self._side == "short":
|
||||
self._lp = min(self._lp, k.low)
|
||||
stop = self._lp + self.cfg.atr_stop * ca
|
||||
if golden or k.close > stop or r1w == "bull":
|
||||
self._side = ""
|
||||
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "1w转牛")
|
||||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||||
reason=reason, timestamp=k.open_time)
|
||||
|
||||
# 空仓
|
||||
else:
|
||||
if r1w == "bull" and r1d == "bull" and golden:
|
||||
self._side = "long"
|
||||
self._hp = k.close
|
||||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||||
reason=f"严格做多({r1w}+{r1d}+{r4h})",
|
||||
timestamp=k.open_time)
|
||||
if r1w == "bear" and r1d == "bear" and death:
|
||||
self._side = "short"
|
||||
self._lp = k.close
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL",
|
||||
reason=f"严格做空({r1w}+{r1d}+{r4h})",
|
||||
timestamp=k.open_time)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 回测入口
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||||
|
||||
PARAMS = {
|
||||
"BTCUSDT": (10, 50),
|
||||
"ETHUSDT": (10, 75),
|
||||
"BNBUSDT": (20, 50),
|
||||
"SOLUSDT": (30, 50),
|
||||
}
|
||||
|
||||
DATE_START = datetime(2024, 1, 1)
|
||||
DATE_END = datetime(2026, 1, 1)
|
||||
|
||||
|
||||
async def run_one(engine, strategy_cls, config_cls, symbol, desc, params=None):
|
||||
fast, slow = PARAMS[symbol] if params is None else params
|
||||
sc = config_cls(symbol=symbol, fast=fast, slow=slow)
|
||||
bt = BacktestConfig(
|
||||
symbol=symbol, interval="4h",
|
||||
start_time=DATE_START, end_time=DATE_END,
|
||||
initial_capital=10_000.0, warmup_bars=250,
|
||||
)
|
||||
eng = engine(bt, db_config=config.db)
|
||||
r = await eng.run(strategy_cls, sc)
|
||||
return r
|
||||
|
||||
|
||||
async def main():
|
||||
print()
|
||||
print("═" * 120)
|
||||
print(" 多周期牛熊共识策略 — MTF Regime × 4h EMA | 2024-2026")
|
||||
print("═" * 120)
|
||||
print()
|
||||
print(" 对比策略:")
|
||||
print(" • MTF Consensus — 1w定方向 + 1d确认 + 4h金叉/死叉入场(新)")
|
||||
print(" • MTF Strict — 1w+1d必须同向才入场(新)")
|
||||
print(" • Old Regime — 单周期(4h)三法投票(regime_all.py 基线)")
|
||||
print(" • Long/Short — 无牛熊过滤,始终多空(long_short.py 基线)")
|
||||
print()
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
print(f" ── {symbol} ──")
|
||||
print(f" {'策略':<18} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'多头P&L':>9} {'空头P&L':>9}")
|
||||
print(" " + "─" * 110)
|
||||
|
||||
results = {}
|
||||
|
||||
# 1. MTF Consensus (new)
|
||||
try:
|
||||
r1 = await run_one(LongShortEngine, MTFRegimeStrategy, MTFConfig, symbol, "MTF Consensus")
|
||||
m = r1.metrics
|
||||
long1 = [t for t in r1.trades if t.pnl is not None and t.side == "SELL"]
|
||||
short1 = [t for t in r1.trades if t.pnl is not None and t.side == "BUY"]
|
||||
lp1 = sum(t.pnl for t in long1) if long1 else 0
|
||||
sp1 = sum(t.pnl for t in short1) if short1 else 0
|
||||
results["mtf"] = m
|
||||
print(f" {'MTF Consensus':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp1:>+9.0f} {sp1:>+9.0f}")
|
||||
except Exception as e:
|
||||
print(f" {'MTF Consensus':<18} 错误: {e}")
|
||||
|
||||
# 2. MTF Strict (new)
|
||||
try:
|
||||
r2 = await run_one(LongShortEngine, MTFStrictStrategy, MTFStrictConfig, symbol, "MTF Strict")
|
||||
m = r2.metrics
|
||||
long2 = [t for t in r2.trades if t.pnl is not None and t.side == "SELL"]
|
||||
short2 = [t for t in r2.trades if t.pnl is not None and t.side == "BUY"]
|
||||
lp2 = sum(t.pnl for t in long2) if long2 else 0
|
||||
sp2 = sum(t.pnl for t in short2) if short2 else 0
|
||||
results["strict"] = m
|
||||
print(f" {'MTF Strict':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp2:>+9.0f} {sp2:>+9.0f}")
|
||||
except Exception as e:
|
||||
print(f" {'MTF Strict':<18} 错误: {e}")
|
||||
|
||||
# 3. Old Regime (baseline)
|
||||
try:
|
||||
r3 = await run_one(LongShortEngine, OldRegimeS, OldRegimeCfg, symbol, "Old Regime")
|
||||
m = r3.metrics
|
||||
long3 = [t for t in r3.trades if t.pnl is not None and t.side == "SELL"]
|
||||
short3 = [t for t in r3.trades if t.pnl is not None and t.side == "BUY"]
|
||||
lp3 = sum(t.pnl for t in long3) if long3 else 0
|
||||
sp3 = sum(t.pnl for t in short3) if short3 else 0
|
||||
results["old"] = m
|
||||
print(f" {'Old Regime':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp3:>+9.0f} {sp3:>+9.0f}")
|
||||
except Exception as e:
|
||||
print(f" {'Old Regime':<18} 错误: {e}")
|
||||
|
||||
# 4. Long/Short (baseline, no filter)
|
||||
try:
|
||||
r4 = await run_one(LongShortEngine, LSS, LSCfg, symbol, "Long/Short")
|
||||
m = r4.metrics
|
||||
long4 = [t for t in r4.trades if t.pnl is not None and t.side == "SELL"]
|
||||
short4 = [t for t in r4.trades if t.pnl is not None and t.side == "BUY"]
|
||||
lp4 = sum(t.pnl for t in long4) if long4 else 0
|
||||
sp4 = sum(t.pnl for t in short4) if short4 else 0
|
||||
results["ls"] = m
|
||||
print(f" {'Long/Short':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp4:>+9.0f} {sp4:>+9.0f}")
|
||||
except Exception as e:
|
||||
print(f" {'Long/Short':<18} 错误: {e}")
|
||||
|
||||
# 简要对比
|
||||
if len(results) >= 2:
|
||||
best_sharpe = max(results.items(), key=lambda x: x[1].sharpe_ratio)
|
||||
worst_dd = min(results.items(), key=lambda x: x[1].max_drawdown_pct)
|
||||
print(f" └ 最佳夏普: {best_sharpe[0]} ({best_sharpe[1].sharpe_ratio:.2f}) | "
|
||||
f"最小回撤: {worst_dd[0]} ({worst_dd[1].max_drawdown_pct:.1f}%)")
|
||||
|
||||
print()
|
||||
|
||||
print("═" * 120)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user