← 返回目录
p8.6动手⏱ 约 15 分钟

实时行情接入:限频、抖动、断线

三种客户端跑同一个源——朴素轮询用 128 次请求换 86 条数据并触发 21 次限频,指数退避用 92 次换 84 条、零次限频

🔎 最后验证 2026-08📚 来源:三种客户端 × 300 步的限频/抖动/断线模拟,2026-08-01 于 ECS 实测,输出见 code/outputs/stdout.txt🧰 numpy 2.5.1、Python 3.12.13(离线模拟,不联网)
为什么学这个

真实行情源会做三件事让你难受:限频、抖动、断线

用一个确定性的假数据源(固定种子)模拟它们,三种客户端的正常轮询节奏完全相同,唯一区别是失败之后怎么办:

客户端                      请求数   成功   429   500  断线读  成功率%
A 朴素轮询(失败立刻重试)      128    86    21     8    13    67.2
B 固定间隔重试(3 步)          97    86     0     7     4    88.7
C 指数退避+抖动+熔断            92    84     0     5     3    91.3

C 用少了 28% 的请求,拿到几乎一样多的数据(84 vs 86),而且零次限频。

A 触发 21 次 429,C 是 0 次——失败立刻重试,是在给限频火上浇油。

⚠️ 不联网:真实源不可复现,而不可复现的实验测不出策略差异(p5.9)。

💡 打个比方

电梯按钮按一次和按十次,电梯来的速度是一样的。

但如果按钮是坏的,你连按十次只会让它坏得更彻底。

失败之后立刻重试,就是连按十次。

一、三条必须实现的机制

机制做什么为什么
指数退避失败后等待时间翻倍(上限 32)避免把限频打得更死
抖动 jitter等待时间加一个随机量避免多个客户端同时重试形成尖峰
熔断连续失败 N 次就停一段时间源真的挂了时,别再浪费配额与日志

抖动这一条最容易被省掉。 如果所有客户端都是"失败后等 2 秒、4 秒、8 秒",那它们会在同一时刻集体重试——把一次故障变成一次雪崩。

二、这个实验一开始是错的

⚠️ 避坑

第一版的假数据源用「自己被调用了多少次」当时间。

结果是:低频客户端的"最近 10 次调用"永远就是它最近的 10 次调用,于是它也必然被限频。三种客户端因此测不出任何差别——C 只拿到 5 条数据,还被 429 了 11 次。

排查结论:模型错了,不是客户端错了。

修法是把客户端的时间步传进去:

def fetch(self, step: int):
    # ⚠️ 必须用客户端的时间步,不能用「源自己被调用了多少次」
    self.t = step

这与 p7.4 的第一版实验、p7.6 的第一版体检工具是同一个模式:

实验没测出预期的差别时,先怀疑实验设计。

三、断线重连之后:最容易错的一步

重连成功后不能直接续着推,必须先做两件事:

  1. 拉一次快照补齐断线期间的缺口——否则你的持仓/行情状态是残的
  2. 按主键去重(symbol + 时间戳)——因为补齐的快照会和后续推送重叠

漏掉 ① → 状态残缺且不报错;漏掉 ② → 同一根 K 线被消费两次。

这两种错都不会抛异常,只会让你的结果慢慢变得不可解释——这正是 p1.6 讲的那类问题。

四、实时接入的三条纪律(接 p1.5)

纪律理由
落盘先于计算收到就写原始报文,再做解析与计算
时间戳用源的,不用本地的本地时钟会漂,而回放要按源的顺序
任何丢弃都要计数去重丢了多少、限频丢了多少——不记录就是黑洞

第 ③ 条最容易被忽略。系统里每一次"这条数据我不要了"的决定,都必须留下一个计数器;否则数据缺了一段,你连"是不是我自己丢的"都答不出来。

五、一个必须说清的取舍

本节 A 拿到的数据(86 条)比 C(84 条)略多。

这是实测结果,不能藏。但要连着后半句一起读:

A 付出了 21 次 429,而 C 是 0 次。

真实环境里,持续触发限频通常会被降级或封禁——那时 A 的「更多数据」会一次性归零。

用两条数据换 21 次限频,是一笔看起来赚、实际上在借高利贷的交易。

🔧 动手做:C 那套复杂机制,到底什么时候才值?(8 分钟)

默认源下,B(固定间隔)和 C(退避+熔断)看着差不多——429 都是 0、成功率 88.7% vs 91.3%。那 C 多出来的复杂度,是白费吗?

压测它:把第 20 行 RATE_LIMIT = 5 改成 2(模拟一个严得多的源),重跑 python p86_realtime.py

你会看到(实测):

客户端成功率(源宽松)成功率(源变严)
A 朴素轮询67.2%崩到 2.8%(237 次 429)
B 固定间隔88.7%也崩了,5.3%
C 退避+熔断91.3%39.1%——是 B 的 7 倍

想明白:平时源宽松,B 够用,C 的退避+熔断根本看不出价值。但源一变严、故障一变长,只有 C 扛得住。 这类机制不是给顺境写的,是给出事那天写的——而你事先不知道哪天出事。这才是"复杂度要花在刀刃上"的真正含义:不是平时炫技,是灾难时保命。

❓ 测验
你的行情客户端在源故障时会立刻重试。同事说「反正失败了不占配额,重试没坏处」。哪里不对?
✏️ 填空
退避机制里最容易被省掉的是 ___,没有它,多个客户端会在同一时刻集体重试,把一次故障变成一次雪崩。

📌 免责:本课为技术教学,行情源为离线模拟,不构成投资建议。

✅ 小结

四件事:

  1. 三种客户端正常节奏完全相同,唯一区别是失败后怎么办:朴素轮询 128 次请求 / 86 条数据 / 21 次 429;指数退避 92 次 / 84 条 / 0 次 429C 少用 28% 的请求,拿到几乎一样多的数据
  2. 三条必须实现的机制:指数退避 · 抖动(最容易省掉,没有它多客户端会集体重试形成雪崩)· 熔断
  3. 这个实验第一版是错的:假数据源用"自己被调用了多少次"当时间,导致低频客户端也必然被限频,三者测不出差别。排查结论是模型错了,不是客户端错了——与 p7.4、p7.6 同一个模式:实验没测出预期差别时,先怀疑实验设计
  4. 断线重连后必须先做两件事:拉快照补缺口 · 按主键去重。漏掉前者状态残缺、漏掉后者同一根 K 线被消费两次,而两种错都不会抛异常

三条纪律:落盘先于计算 · 时间戳用源的 · 任何丢弃都要计数(不记录就是黑洞)。

最后一个必须说清的取舍:A 确实多拿到 2 条数据,但付出了 21 次 429——真实环境里持续触发限频通常会被降级或封禁,那时这点优势会一次性归零。

下一节是本章的核心:模拟撮合引擎——七条 A 股规则,以及一条回测里根本不存在的状态。

下一节 → 模拟撮合引擎
🔎 来源与核验· 2 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「三种客户端 × 300 步模拟(限频每 10 步 5 次、8% 概率 500、随机 3 次断线各 12 步,种子 20260801,正常节奏均为每 3 步一次):朴素轮询 128 请求/86 成功/21 次 429/成功率 67.2%;固定间隔 97/86/0/88.7%;指数退避+抖动+熔断 92/84/0/91.3%」
📚 本节 code/p86_realtime.py,2026-08-01 于 ECS 实测(离线模拟,不联网),输出见 code/outputs/stdout.txt✓ 已核验 2026-08
「第一版模拟的设计错误:假数据源以自身被调用次数作为时间基准,导致低频客户端同样必然触发限频,三种客户端无法区分」
📚 本节 code/p86_realtime.py(第一版以调用次数当时间基准的设计错误原样保留在代码注释 L37 中)✓ 已核验 2026-08
行情源为离线确定性模拟,不代表任何真实数据源的限频参数或故障率。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p8.7
模拟撮合引擎:七条规则,加一条回测里不存在的状态
继续读下一节 →