""" 多周期牛熊矩阵 — 方案一:独立多周期判定矩阵 在统一时间轴(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())