"""影子把过滤网的信号写到这里,实盘执行器读这里。 ## 为什么不让实盘自己算信号 三个理由,第三个最要紧: 1. 2 核上再来一份十币计算,清空会从 247ms 推到 500ms+ 2. 实盘进程崩溃不该影响正在采的数据集 3. **实盘交易的必须是影子测量的那一个信号。** 各算一份会让两边悄悄分叉, 之后就没法把实盘的实际成交和影子测的滑点曲线对照——而那个对照是整件事 的目的 ## 为什么用 append-only 文件而不是队列 崩溃安全 + 留审计轨迹。实盘进程重启后能从文件里看到自己漏掉了哪些信号, 而不是像内存队列那样直接消失。文件也让"影子在跑、实盘没在跑"这种状态成为 可观测的(信号在攒着),而不是静默丢弃。 每行一个 JSON,字段见 `emit`。`key` 是幂等键,实盘按它去重——同一根被重复 处理(补根、进程池重建后重放)不该开两次仓。 """ from __future__ import annotations import json import os import time from pathlib import Path BUS = Path(os.environ.get( "SIGNAL_BUS", "research/out/signals_live.jsonl")) def key_of(sym: str, kline_ts: int, direction: int) -> str: return f"{sym}:{int(kline_ts)}:{int(direction):+d}" def emit(sym: str, kline_ts: int, direction: int, entry_px: float, atr_pct: float, lag_ms: float, path: Path | None = None) -> None: """追写一条信号。任何失败只打日志——总线写不进去不能连坐采集。 `entry_px` 是次根开盘价,也就是回测口径的成交价。实盘据此算止损/止盈的 绝对价位,**不要**用实盘自己看到的现价,否则价位会随执行延迟漂移,跑的 就不是回测那个结构。 """ p = path or BUS rec = {"key": key_of(sym, kline_ts, direction), "sym": sym, "kline_ts": int(kline_ts), "direction": int(direction), "entry_px": float(entry_px), "atr_pct": float(atr_pct), "lag_ms": float(lag_ms), "emit_ms": int(time.time() * 1000)} try: p.parent.mkdir(parents=True, exist_ok=True) with p.open("a") as f: f.write(json.dumps(rec) + "\n") f.flush() # 实盘要在毫秒级看到,且进程被 SIGKILL 时不能丢——这两点都要求 # 落到磁盘,不能只停在 libc 缓冲里 os.fsync(f.fileno()) except Exception as e: print(f" [bus] 写信号失败 {type(e).__name__}: {e}", flush=True) def read_all(path: Path | None = None): """读全部信号。坏行跳过——半行只可能出现在文件末尾的崩溃点。""" p = path or BUS if not p.exists(): return for line in p.read_text().splitlines(): if not line.strip(): continue try: yield json.loads(line) except json.JSONDecodeError: continue