本节摘要:QuantDinger 提供一种最轻量的实时验证形态:signal-only 虚拟账户(官方 README 口径)——账户里跑的是真实策略、接的是实时行情,但产生的每个信号只被记录,不会生成任何真实订单,也不做撮合模拟。本节先讲清这个机制在架构里的位置,再展开它与回测的三个本质差别:实时行情(数据不再整齐连续)、实时决策(每次判断都在"现在"发生)、真实延迟(从信号到可见之间存在网络与处理耗时);然后划定适用与不适用场景——它验证"策略行为",不验证"成交质量";最后给出配置示例与一份观察清单。理解边界比使用功能更重要:把 signal-only 当成撮合级 paper 会得出错误结论。
按官方 README 口径,QuantDinger 支持 signal-only virtual accounts:账户层面的开关,策略照常运行、行情照常订阅,产出物从"订单"换成"信号记录"。在 v5 六进程架构(第 02 章《v5架构总览》)里,它的位置如下:
signal-only 模式下的数据流 scheduler-worker(定时触发) │ ▼ trading-worker(执行策略运行时) │ 产出 intents ▼ ┌─────────────────────┐ signal-only 开关打开 │ 虚拟账户(signal-only) │ ──▶ 只写信号表:时间/标的/方向/规模/理由 └─────────────────────┘ × 不进入交易所适配器,不产生任何真实订单
工程上有两点值得注意。第一,策略代码不需要任何改动:第 05 章写的 Strategy API V2 策略,注册到 signal-only 账户即以该模式运行——同一代码路径原则在虚拟账户上同样成立。第二,信号记录是持久化的:每条信号带时间戳与策略上下文,这让它可以被事后审计,而不只是看一眼就走。
signal-only 看似"迷你回测",但三个差别使它观察到的世界与回测不同:
| 差别 | 回测 | signal-only | 引入的新问题 |
|---|---|---|---|
| 实时行情 | 历史数据,完整连续 | 实时到达,可能有缺口与乱序 | 数据缺口时策略行为是否仍正常 |
| 实时决策 | 全序列已知,逐段计算 | 每次决策只见"现在"之前 | 策略是否有依赖全量历史的隐藏假设 |
| 真实延迟 | 零延迟,信号即成交 | 信号产生到记录有网络与处理耗时 | 高频信号在真实延迟下的有效性存疑 |
逐条展开。实时行情的差别最容易被低估:回测里数据永远是整齐的,实时流里行情晚到、缺根、字段异常是常态——第 03 章《数据层》的清洗逻辑是否覆盖到位,第一次跑 signal-only 就会暴露。实时决策的差别针对代码:任何"先把整段历史算完再出信号"的写法在回测里工作良好、在实时里不可复现,signal-only 是发现这类隐藏假设的低成本手段。真实延迟的差别决定适用边界:信号从产生到可执行之间隔着延迟,意味着所有"看到价格即成交"的回测假设在实时世界里都要打折。
适合 signal-only 回答的问题:
不适合 signal-only 回答的问题:
一句话边界:signal-only 验证"策略行为",完整 paper 验证"成交假设",实盘小仓验证"全链路"——三级递进,不可跳读。
(示意结构,字段名与账户形态以官方文档为准。)
# account_signal_only.yaml —— signal-only 虚拟账户配置(示意,字段以官方文档为准) account: name: breakout_v2_signal_watch mode: signal_only # 核心开关:只记录信号 strategy: breakout_v2 # 第 05 章注册的策略,无需改动 symbols: [BTC/USDT] timeframe: 1h data: feed: live # 实时行情通道(第 03 章数据层) decision_gate: # 可选:预演第 09 章决策门 enabled: true engine: jev record: context: full # 记录信号时的策略上下文,供事后审计 retention_days: 90 # 信号记录保留天数(示意值)
两个建议。其一,record.context 尽量开全:事后解释"当时为什么出这个信号",靠的是上下文而不是记忆。其二,先不开决策门跑几天,再打开对比信号流差异——你会直观看到门在拦什么。
| 观察项 | 正常形态 | 异常信号 | 动作 |
|---|---|---|---|
| 信号频率 | 与回测量级一致 | 突然翻倍或长期沉默 | 查数据缺口与策略状态 |
| 信号时间分布 | 与设计节奏相符 | 集中在异常时段 | 查 scheduler 触发配置 |
| 信号理由字段 | 可读、可复现 | 空洞或雷同 | 查策略日志质量 |
| 与回测信号重合度 | 合理重合 | 完全不同 | 查数据源口径差异 |
最后一行需要说明:实时信号与"同期回放回测"的信号不会百分之百重合(实时数据修订、延迟、缺口处理都会造成差别),但完全对不上意味着某处的数据口径或代码路径不一致——这通常比策略本身的问题更值得优先修复。
把观察清单固化成脚本,是让"盯"可持续的办法。下面是一个纯标准库的日报工具:输入信号记录的导出文件,输出四项观察指标里的前三项读数:
# signal_report.py —— signal-only 信号流日报(纯标准库,输入为信号记录导出的 CSV) import csv from collections import Counter def load_signals(path): with open(path, newline="", encoding="utf-8") as f: return list(csv.DictReader(f)) def daily_report(rows): per_day = Counter(r["date"] for r in rows) # 按天聚合:查频率突变 per_hour = Counter(r["ts"][11:13] for r in rows) # 小时分布:查异常时段集中 kinds = Counter(r["reason"].split(":")[0] for r in rows) # 理由前缀:查空洞或雷同 return { "total": len(rows), "per_day": dict(sorted(per_day.items())), "top_hours": per_hour.most_common(3), "reason_kinds": len(kinds), } if __name__ == "__main__": rep = daily_report(load_signals("signals_export.csv")) print("总信号数:", rep["total"]) print("按天分布:", rep["per_day"]) print("最集中时段:", rep["top_hours"]) print("理由种类数:", rep["reason_kinds"])
读这份日报要看三处。per_day 对应观察清单第一行的信号频率:单日读数没有意义,连续两周的读数连起来才是基线,突变只有对着基线才看得见。top_hours 对应时间分布:如果信号集中出现在你设计之外的时段,第一怀疑对象是 scheduler 触发配置而不是策略逻辑。reason_kinds 对应理由字段:种类数长期为一且理由文本高度雷同,说明策略日志在偷懒,事后审计的价值会大打折扣。第四项"与回测重合度"需要把同期回放回测的信号同样导出后另行比对,不适合塞进这个日报里。
| 问题现象 | 优先排查方向 | 处理要点 |
|---|---|---|
| 信号长期为零 | 数据层新鲜度、策略运行状态 | 先确认行情在流动,再查策略是否处于设计内的休眠条件 |
| 信号突然翻倍 | 触发来源叠加(定时与手动并存)、缺口回填引发补算 | 对照触发时间戳,确认补算行为符合第 03 章设计 |
| 理由字段空洞或雷同 | record.context 是否开全、策略日志质量 | 开全上下文重放该时段核对,必要时回第 05 章补日志 |
| 与同期回放回测完全对不上 | 两边数据源口径、时区、粒度差异 | 按 7.2 节差异表先核对口径,再谈策略问题 |
| 打开决策门后信号全被拦 | 门的工作模式、否决理由分布 | 调决策日志看理由聚类,方法见 9.1 节影子期 |
排查表的整体顺序设计遵循本节的主线:凡是异常,先怀疑环境(数据、触发、口径),再怀疑策略,最后才怀疑门——因为前三者的验证成本远低于后者,而且按第 03 章的经验,问题出现在前一环的概率也更大。
signal-only 告诉你策略在实时世界里"会做什么"。但第 06 章放行依据里的一个核心假设——成交假设——它验证不了。下一节给出从回测到 paper 的完整迁移清单:三张差异表核对配置,两个偏差指标量化差距,并回答"paper 至少跑多久"这个没有标准答案但有依据框架的问题。