本节摘要:本节用三条路径把前两节的静态边界与设计哲学串成动态图景。路径一:一次下单——从 trading-worker 里策略产出 intent,经风控闸门,订单落库为待处理状态,经交易所适配器发往交易所,成交回报回流更新状态,全程经数据库与队列、进程之间零直连。路径二:行情入库——交易所接口经 data_sources 模块拉取或订阅,规范化后落库,供指标与策略消费。路径三:对账回流——定时任务比对本地订单状态与交易所真实状态,修正漂移。每条路径都标注「谁、读写什么、失败时怎样」,末尾附排查收敛树与一节常见问题,读完你会获得在日志与数据库里定位任意一笔订单的能力。
trading-worker PostgreSQL 交易所 ┌─────────────────┐ intent ┌──────────────┐ 适配器下单 ┌──────────┐ │ 策略 on_bar 计算 │ ─────────▶ │ 风控闸门检查 │ ──────────▶ │ Binance │ │ (读库:行情/仓位) │ │ 订单落库 │ │ OKX … │ └─────────────────┘ │ 状态=待处理 │ └────┬─────┘ ▲ └──────┬───────┘ │ │ │ 轮询/推送回报 ▼ │ ┌───▼────────┐ 成交/拒绝/部分成交 └──────────────────────────│ 状态更新 │ ◀──────────────┘ 策略读到新状态,继续计算 │ 状态=已成交 │ └────────────┘ 注:backend 不在此图主链上——它创建/修改的是「意图与配置」, 不是订单本身(第 2.2 节哲学的直接体现)
| 步 | 谁 | 读写 | 失败时 |
|---|---|---|---|
| 1 | 策略(trading-worker 内) | 读:行情、账户状态 | 数据缺失则本周期不出 intent |
| 2 | 风控闸门 | 读:限额规则;写:检查结果 | 拦截,订单停留在意图态,留痕可查 |
| 3 | 订单落库 | 写:订单记录(待处理) | 落库失败则重试,宁可慢不可丢 |
| 4 | 交易所适配器 | 读:待处理订单;外呼交易所 | 网络失败按重试策略处理,状态可见 |
| 5 | 回报处理 | 写:订单状态(成交/拒绝/部分) | 回报丢失由对账兜底(路径三) |
两个读图要点:每一步都留库痕——这是可观测的来源;失败不消失——要么重试、要么留痕等待对账,任何一步都不会静默丢弃。
交易所 REST / WebSocket data_sources 模块 PostgreSQL ┌──────────────────┐ 拉取/订阅 ┌───────────────────┐ 规范化写 ┌──────────┐ │ K 线 / 深度 / 成交 │ ─────────▶ │ 连接管理·限速·重连 │ ────────▶ │ 行情表 │ └──────────────────┘ │ 字段映射·单位统一 │ └────┬─────┘ └───────────────────┘ │ 读 ┌─────────────┴──────────┐ ▼ ▼ 指标计算(第4章) 策略 on_bar(第5章)
入库节奏的两类口径(具体策略以官方文档为准):K 线类通常以「闭合」为准落库——只有一根 K 线走完,它才是事实;未闭合的当前根属于「现在进行时」,供展示可以,进回测不行。深度与成交类是流式快照,按节流策略更新。这条纪律直接决定第 6 章回测会不会偷看未来数据。
失败处理:接口断流时先重连、再补数(REST 拉历史区间补洞),补不上的缺口进入第 3.3 节的质检流程。行情入库的责任人按任务类型分布在 scheduler-worker 与 celery-worker(第 2.1 节职责表),但「事实」只认库里的数据。
对账回答一个尖锐的问题:**你认为的状态,和交易所认为的状态,一致吗?**漂移的来源很多——回报丢失、进程在写状态的半路被杀、交易所侧的维护性撤销。对账任务定期做三方比对:
| 比对项 | 本地(库) | 对端(交易所) | 不一致时 |
|---|---|---|---|
| 订单状态 | 待处理/已成交/已撤销 | 交易所订单状态 | 以对端为准修正本地,留修正记录 |
| 持仓数量 | 账户表 | 交易所账户 | 差异超阈值则告警并暂停相关策略 |
| 可用余额 | 账户表 | 交易所账户 | 告警;人工确认后修正 |
用最小代码把「以对端为准」的比对逻辑写出来(教学示意):
# reconcile_demo.py —— 对账比对的最小示意(纯标准库) def reconcile(local_orders, remote_orders): """两侧均为 {订单号: 状态};状态示意:pending / filled / cancelled""" drift = [] for oid, local_state in local_orders.items(): remote_state = remote_orders.get(oid) if remote_state is None: drift.append((oid, local_state, "对端不见此单", "本地核查后处理")) elif local_state != remote_state: drift.append((oid, local_state, remote_state, "以对端为准修正并留痕")) return drift if __name__ == "__main__": local = {"A1": "pending", "A2": "filled", "A3": "filled"} remote = {"A1": "filled", "A2": "filled"} for row in reconcile(local, remote): print(row) # 示意:A1 被修正为已成交;A3 对端不见,转入本地核查
注意代码里没有任何「自动重下单」之类的补救——对账的职责是修正认知,不是替策略决策。发现漂移之后的动作(重发、放弃、告警人工介入)属于上层流程,混进对账会让它变成第二个交易循环。
对账由定时任务驱动(celery-beat 节拍 + celery-worker / scheduler-worker 执行,具体分工以官方文档为准),这本身就是第 2.2 节哲学的应用:对账是长周期、必须与交易循环解耦的动作,它的裁判依据永远是交易所这个外部事实源,而不是本地内存。
在 paper 模式下,signal-only 虚拟账户(官方口径:只记录信号不发真实订单)没有真实对端,对账退化为「信号记录完整性检查」——这也是 paper 与实盘在工程上的本质差异之一,第 7、8 章会反复回到这一点。
设想你在界面(backend 写库的意图)配置了一个 paper 策略,某根 K 线闭合后它应该入场。排查顺序:
四步各自「查哪里」再落具体一点(表名与字段示意,以官方文档为准):
| 排查步 | 查哪里 | 找什么 |
|---|---|---|
| 1 行情 | 行情表:该标的该周期的闭合根 | 那根 K 线存在吗、字段完整吗 |
| 2 意图 | 意图与风控记录 | 有没有产出、被哪条规则拦下 |
| 3 订单 | 订单表:状态与时间戳 | 停在待处理还是已成交、卡在哪一步 |
| 4 对账 | 对账修正记录 | 有没有自动修正、修正了什么 |
排查的收敛路径(示意) 症状:该成交没成交 └─▶ 行情在吗 ──否──▶ 数据层问题(第 3 章) │是 └─▶ 意图在吗 ──否──▶ 策略条件没触发(第 4、5 章) │是 └─▶ 被拦了吗 ──是──▶ 风控处置日志(5.3 节) │否 └─▶ 订单状态 ──卡住──▶ 适配器与回报链路
四步之内必有答案,因为每条路径的每一步都有库痕。这就是架构章的最终回报:系统对你不再是黑箱,而是一张可以按图索骥的地图。
| 疑问 | 排查方向 |
|---|---|
| 订单一直停在待处理 | 先看适配器日志与重试策略,再看对账记录是否已在跟进 |
| 同一根 K 线,回测与实盘行为不同 | 查闭合纪律:实盘是否消费了进行时根(3.1 节) |
| 对账频繁修正本地状态 | 病因多在回报链路稳定性,不在对账本身;查适配器连接质量 |
| 行情有缺口但策略照常在跑 | 缺口应进 3.3 节质检;策略侧靠幂等保证补数重放无害 |
| paper 模式下需要对账吗 | 退化为信号记录完整性检查,同样不可省略 |
三条路径里反复出现同一个源头:数据。行情从哪里来、宏观数据如何聚合、多源口径怎么统一——第 3 章把这条源头展开成完整一章。