做交易机器人这行当,最容易出事的环节从来不在策略本身。我见过太多这样的情况:本地跑得好好的脚本,挂到服务器上三天两头出问题——下单没回执、仓位对不上、半夜连接断了没人知道、醒来发现挂了一堆重复单。OKX 交易机器人开发这件事,真正吃功夫的是 API 接入的细节把控和生产级部署的工程化能力:签名怎么算、时间戳怎么校准、订单状态怎么维护、断线重连之后怎么对账、限频怎么控制、风控怎么兜底。这篇内容就是把这套东西从头到尾拆一遍,从最小可运行的下单链路,一直讲到能长期挂在服务器上无人值守的部署方案。适合有两三年 Python 基础、想认真做一套可靠交易工具的开发同学,也适合已经跑通 Demo 但被各种线上问题折磨的中阶选手。文中所有接口示例基于 OKX 公开的 v5 API 规范,代码可直接改写复用,涉及资金和风控的部分我会把取舍逻辑讲透,重点放在工程可靠性上,不涉及任何策略收益的讨论。
1. 整体架构与选型思路拆解
1.1 先想清楚机器人到底要解决什么问题
动手写第一行代码之前,得先明确机器人要承担的职责边界。一个典型的交易机器人至少要做四件事:拉行情、做决策、发订单、管仓位。但真正决定它能不能长期活下来的,是这四件事之外的第五件事——状态一致性。本地数据库里记着"我有 0.5 个 BTC 多仓",交易所那边实际是 0.3 个,这种偏差不解决,后面所有逻辑都是空中楼阁。
我把机器人拆成三类角色来看:行情消费者(订阅 WebSocket,维护本地订单簿和 K 线缓存)、决策执行者(跑策略逻辑,产出下单意图)、状态守护者(维护订单状态机、定时对账、异常兜底)。很多人把三者揉在一个大循环里,短时间能跑,但只要出现一次网络抖动或者重复回报,整个循环就会被卡死或者算错。拆开之后,每个角色有独立的失败边界,行情断了不影响对账,决策卡住不影响风控熔断,这是能不能上生产的第一道分水岭。
另外一个必须提前想明白的问题:你的机器人是"无状态重放型"还是"有状态持有型"。前者每次启动都从交易所拉全量状态重建上下文,代码简单但启动慢、对账压力大;后者把状态落到本地库,启动快但要处理"本地状态与远端不一致"的各种边界。我一般选折中方案:本地库只做"未完成订单"和"最近 N 笔成交"的缓存,持仓和余额一律以交易所为准,启动时强制全量对账一次。这样既不会每次启动都慢吞吞,也不会因为本地脏数据把仓位算错。
1.2 三层架构与关键技术选型
落实到代码结构上,我习惯分成三层:
接入层负责所有网络交互,包括 REST 请求封装、WebSocket 长连接管理、限频器、重试策略。这一层对外暴露的是"语义化方法",比如place_order()、subscribe_ticker(),上层完全不关心底层是 HTTP 还是 WebSocket。
领域层是交易逻辑的核心,包含订单状态机、仓位计算、风控规则、策略信号。这一层的原则是纯逻辑、可单测——给定一组输入(行情快照 + 当前状态),输出确定的动作。这样你可以把历史行情灌进去做离线验证,不用真的连交易所。
运行时层是进程编排,负责事件循环、任务调度、优雅退出、指标上报。这一层决定了机器人在异常情况下的行为,也是最容易被忽略的部分。
选型上我倾向 Python + asyncio 的组合。原因很实际:交易机器人的瓶颈几乎永远在网络 IO 和 JSON 解析上,不在 CPU;asyncio 单线程事件循环天然避免锁竞争,调试时心智负担小;生态上aiohttp做 REST、websockets做长连接、Decimal处理价格精度,都是成熟方案。如果对延迟有极致要求(比如做高频做市),那确实要换编译型语言,但绝大多数中低频策略用 Python 完全够,把工程可靠性做好比抠那几十微秒有意义得多。数据持久化上,未完成订单和成交记录我用 SQLite 就够了,单机部署零运维成本;如果要做多实例或者需要复杂查询再上 PostgreSQL。
提示:不要一上来就搞微服务拆分。我见过有人把行情、下单、风控拆成三个服务加消息队列,结果光是处理消息乱序和重复消费就写了两千行代码。单进程多协程的架构,配合清晰的分层,已经能扛住绝大多数场景。
1.3 模块边界与数据流走向
把数据流画清楚,后面写代码几乎是照着填。行情从 WebSocket 进来,先落到一个内存状态容器(订单簿、最新价、持仓),然后发布事件通知策略模块。策略模块收到事件后判断是否触发信号,产出OrderIntent对象——注意这里是"意图"不是"订单",意图里只写方向、数量、价格逻辑,具体用limit还是post_only、数量怎么按步长取整,交给执行模块去决定。
执行模块拿到意图后,先过一遍风控检查(单笔名义价值、日内累计、持仓上限、价格偏离度),通过了才组装成真实请求发给接入层。接入层发出请求后立即拿到clOrdId并写入本地待确认表,然后等 WebSocket 的orders频道推送状态变化。如果 5 秒内没收到任何回报,触发一次 REST 主动查询兜底。
状态回流同样是单向的:任何来自交易所的状态变更,都必须经过状态机校验,只有合法的状态迁移才落库。比如live → filled合法,filled → live就是异常,说明收到了乱序或者重复的消息,这时候应该记日志告警而不是盲目覆盖。这条规则我踩过坑:早期直接把推送内容覆盖本地记录,结果一次乱序推送把已成交的订单改回"挂单中",机器人以为还挂着,又补了一单,直接双倍仓位。
2. API 接入的关键细节
2.1 密钥管理、签名算法与时间戳校准
OKX v5 的鉴权体系是API Key + Secret Key + Passphrase三件套,每个私有请求都要带四个头:OK-ACCESS-KEY、OK-ACCESS-SIGN、OK-ACCESS-TIMESTAMP、OK-ACCESS-PASSPHRASE。签名规则是:把时间戳 + 请求方法(大写) + 请求路径(含 query string) + 请求体拼成一个字符串,用 Secret Key 做 HMAC-SHA256,再 Base64 编码。
import base64 import hashlib import hmac from datetime import datetime, timezone def build_timestamp() -> str: now = datetime.now(timezone.utc) return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z" def build_sign(secret: str, ts: str, method: str, path: str, body: str = "") -> str: payload = f"{ts}{method.upper()}{path}{body}" digest = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).digest() return base64.b64encode(digest).decode()这里有几个高频坑。第一,时间戳格式必须是 ISO8601 带毫秒和 Z 后缀,精度到毫秒,不能用YYYY-MM-DD HH:MM:SS这种常见格式。第二,服务器时间漂移超过 30 秒就会被拒,返回签名相关的错误。云服务器上默认的 NTP 同步有时候会失效,我的做法是启动时调用一次/api/v5/public/time拿服务器时间,算出本地与服务器的偏移量,之后签名用的时间戳一律基于"本地时间 + 偏移量"生成,同时后台每十分钟校准一次。这个偏移量要打到日志里,一旦超过 500 毫秒就告警——它往往是机器快出问题的前兆。
第三,GET 请求的 query string 必须原样参与签名,参数顺序不能变。我见过有人用字典拼 query 再签名,结果 requests 库内部重新排序了参数,签名永远对不上。稳妥做法是自己拼 query 字符串,签名和实际请求用同一个字符串。第四,请求体必须和签名时用的字符串完全一致,不能一个用json.dumps一个用json.dumps(..., separators=(",", ":")),空格差异都会导致签名失败。
注意:密钥绝对不要硬编码在代码里或者提交到 Git。最小可行方案是环境变量注入,再好一点用云厂商的密钥管理服务或者本地加密配置文件(主密码从环境变量读)。模拟盘和实盘的密钥要分开,请求头里
x-simulated-trading: 1表示走模拟盘,这个头漏了就会打到真实账户上,我在早期测试时干过这事,好在当时账户里没钱。
2.2 REST 下单的参数细节与精度处理
下单接口/api/v5/trade/order的核心参数有instId(产品 ID,如 BTC-USDT)、tdMode(cash现货、cross全仓、isolated逐仓)、side(buy/sell)、ordType(limit/market/post_only/fok/ioc)、sz(数量)、px(价格,市价单不传)。此外clOrdId是客户端订单 ID,用于幂等和后续关联,这东西的价值在断线场景下会体现得淋漓尽致。
精度问题是最容易翻车的地方。每个交易对都有三个精度参数:tickSz(价格最小变动)、lotSz(数量最小变动)、minSz(最小下单量)。这些参数会变,必须在启动时和每隔一段时间拉一次/api/v5/public/instruments缓存起来。所有价格和数量在做任何运算之前,先按步长向下取整到合法值:
from decimal import Decimal, ROUND_DOWN def floor_to_step(value, step) -> Decimal: v, s = Decimal(str(value)), Decimal(str(step)) if s == 0: return v return (v / s).to_integral_value(rounding=ROUND_DOWN) * s为什么必须用Decimal?浮点数在金融计算里就是灾难。0.1 + 0.2 = 0.30000000000000004,你把这个数发给交易所,轻则参数错误,重则下出一个你不想要的数量。所有价格、数量、手续费、盈亏计算,一律用Decimal,从字符串构造,绝不经过 float 中转。这条我强调一百遍都不为过。
还有两个细节值得说。一是市价单的sz语义,不同产品类型下可能是基础币数量也可能是计价币金额,下单前一定确认清楚,别把 USDT 金额当 BTC 数量发出去。二是我个人的习惯:所有limit单默认加post_only(只做 maker),避免不小心吃单付出更高的手续费,除非策略明确要求吃单。这个改动让我的实盘手续费省了不少。
2.3 WebSocket 连接的鉴权、订阅与心跳维持
私有频道的鉴权是在连接建立后发一条login消息:
import json login_msg = { "op": "login", "args": [{ "apiKey": api_key, "passphrase": passphrase, "timestamp": ts, "sign": build_sign(secret, ts, "GET", "/users/self/verify"), }], } await ws.send(json.dumps(login_msg))注意 WebSocket 登录的签名串里路径固定是/users/self/verify,方法用GET,这是和 REST 签名不一样的地方,很多人在这里卡半天。登录成功后服务端回{"event":"login","code":"0"},收到才算成功,没收到就重连。
订阅用{"op":"subscribe","args":[{"channel":"tickers","instId":"BTC-USDT"}]},一次可以订阅多个。行情类常用tickers(最新成交价)、books5(五档深度)、candles1m(一分钟 K 线);私有类用orders(订单状态)、positions(持仓)、account(账户余额)。订阅成功会有subscribe事件回执,必须校验回执,我遇到过参数写错但连接不断的情况,机器人安安静静跑了半小时,一单没成交,查了半天才发现频道名拼错了。
心跳机制上,OKX 的 WebSocket 支持文本ping/pong。别只依赖库自带的 ping frame,我建议自己起一个协程每 20 秒发一次业务层ping,30 秒内没收到pong就主动断开重连。为什么?因为 TCP 层的 keepalive 检测不到"连接还在但服务端已经不推数据"这种半死状态,而业务层心跳能。另外要注意,行情冷清的时候长时间收不到数据是正常的,不要把"没数据"当成"连接挂了",判断依据只能是 pong 和最后一次收到任意消息的时间。
3. 核心交易引擎的实现要点
3.1 订单状态机与幂等设计
订单状态机的价值在于把不可信的外部输入变成可信的内部状态。交易所推送的订单状态有live(挂单中)、partially_filled(部分成交)、filled(完全成交)、canceled(已撤单)、mmp_canceled(做市商保护撤单)。本地要有明确的状态迁移表:
| 当前状态 | 允许迁移到 | 触发来源 |
|---|---|---|
| pending_new(本地已发未回) | live / canceled / filled | REST 回执或 WS 推送 |
| live | partially_filled / filled / canceled | WS orders 频道 |
| partially_filled | filled / canceled | WS orders 频道 |
| filled / canceled | 终态,不可变更 | — |
关键规则:终态订单收到任何更新一律忽略并记 warn 日志。这条规则直接决定你会不会重复下单。我早期就是没做终态保护,一次重复推送把 filled 改成 live,补单逻辑又发了一笔,仓位直接翻倍。
clOrdId是幂等的抓手。规则是:字母数字组合,长度有上限(32 字符左右,具体以文档为准),全局唯一。我的生成方式是用"策略标识 + 毫秒时间戳 + 自增序号"拼接,比如mm01-1735689600123-0042。发单前先把这个 ID 写进本地"待确认表",如果请求超时(网络超时无法确定是否下单成功),不要盲目重试,而是用这个clOrdId去查一次订单状态。查到了说明下单成功,查不到再决定是否重发。这套逻辑把"超时"从"不知道成没成"变成了"可以确定"。
对账任务也靠它。启动时做一次全量对账:拉取所有未完成订单和当前持仓,和本地库逐条比对,以交易所为准修正本地;运行中每 5 分钟做一次轻量对账,只比对未完成订单。对账发现的差异要记结构化日志,差异条数作为监控指标上报——这个指标一旦非零就应该告警,它意味着你的状态机有漏洞。
3.2 行情处理的节拍控制与乱序防御
行情处理最容易犯的错是"来一条算一次"。tickers频道在活跃交易对上一秒能推几十条,如果每条都触发一遍完整策略计算(还要查数据库、算指标),CPU 直接打满,而且策略会被高频噪声带偏。正确做法是引入节拍器:行情进来只更新内存中的最新状态(这一步极快,纯赋值),策略协程按固定节拍(比如每秒一次,或者每根 1 分钟 K 线收盘时)读取当前快照做决策。这样计算量和推送频率解耦,行情再猛也不会把机器人打崩。
订单簿处理要额外小心乱序和快照拼接。深度频道的推送是增量更新,必须严格按seqId和prevSeqId校验连续性:新消息的prevSeqId应该等于本地记录的seqId,不相等说明中间丢了包,此时不能"凑合着用",必须重新订阅拉全量快照。这条规则我写成了硬约束,宁可断流两秒也不能用一份错误的订单簿做决策。另外要处理一种情况:服务端可能推送seqId比本地还小的消息(网络乱序),直接丢弃即可。
数据落地上,我建议把收到的原始推送异步写一份到文件或队列,和策略处理解耦。好处有三:出问题时能复盘"当时到底收到了什么";可以拿这批真实数据回放测试策略改动;能统计各频道的实际推送频率,为限频估算提供依据。写入用独立协程加有界队列,队列满了就丢最旧的,绝不能因为写磁盘慢把行情处理堵死。
3.3 风控前置与熔断机制
风控必须放在下单链路的必经之路上,而不是事后检查。我在执行模块里串了一组检查器,任何一个不通过就拒绝这次意图并记日志:
- 单笔名义价值上限:
价格 × 数量超过阈值直接拒。 - 单品种持仓上限:结合当前持仓 + 本次意图计算,超限拒绝或自动缩量。
- 日内累计成交额上限:防止策略失控刷单,每天零点重置。
- 价格偏离度检查:限价单价格偏离最新成交价超过 X% 时拒绝,防止精度错乱或者行情断流导致的离谱挂单。
- 频率限制:同一品种每秒下单次数上限,配合令牌桶。
- 余额/保证金检查:本地缓存的可用余额要留安全垫(比如只用 80%),避免因为手续费或滑点导致下单失败。
熔断机制是最后一道闸。我设置了三类触发条件:连续 N 次下单失败(说明 API 或参数有问题)、单日亏损超过阈值(策略失效)、对账差异连续出现(状态管理出问题)。任一触发就进入"只读模式"——不再开新仓,只允许平仓和撤单,同时推告警通知。恢复必须人工确认,不做自动恢复。这个设计有点"笨",但金融系统里"笨"往往比"聪明"可靠。
class RiskGuard: def __init__(self, cfg): self.cfg = cfg self.tripped = False self.reason = "" def check(self, intent, snapshot) -> tuple[bool, str]: if self.tripped: return False, f"circuit open: {self.reason}" notional = intent.price * intent.size if notional > self.cfg.max_notional: return False, f"notional {notional} > {self.cfg.max_notional}" deviation = abs(intent.price - snapshot.last) / snapshot.last if deviation > self.cfg.max_deviation: return False, f"price deviation {deviation:.4f} too high" return True, "ok"4. 生产环境部署与运维
4.1 配置分层、容器化与进程守护
配置必须分层。密钥类(API Key / Secret / Passphrase)走环境变量或密钥管理服务,绝不进代码库;环境类(REST 域名、是否模拟盘、日志级别)走环境变量或启动参数;策略类(参数、阈值、品种列表)走配置文件,支持热加载。我做了一个约定:所有配置项在启动时打印一次(密钥类脱敏成前 4 位 + 星号),这样出问题时第一眼看日志就能确认"跑的是哪套配置",省去大量排查时间。
容器化方面,Dockerfile 用多阶段构建,基础镜像选python:3.11-slim,依赖用pip install --no-cache-dir装,最后用非 root 用户运行。几个必须设置的点:PYTHONUNBUFFERED=1(不然日志会缓冲,出问题时看不到现场)、TZ=UTC(交易系统统一用 UTC,避免时区换算错误)、容器重启策略设为unless-stopped。
FROM python:3.11-slim WORKDIR /app ENV PYTHONUNBUFFERED=1 TZ=UTC COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd -m bot && chown -R bot:bot /app USER bot CMD ["python", "-m", "bot.main"]进程守护上,单机部署用 systemd 就够,关键是配好Restart=always、RestartSec=10,加上StartLimitIntervalSec防止无限重启刷日志。同时要有优雅退出:收到 SIGTERM 时,先停止接受新信号,把未完成订单列表落盘,主动断开 WebSocket,然后退出。我见过太多人直接docker stop把进程杀了,重启后完全不知道上次挂到一半的订单是什么状态,只能人工去网页端一个个看。
4.2 可观测性:日志、指标与告警三件套
日志用结构化 JSON,字段固定:ts、level、module、event、clOrdId、instId、detail。这样可以用jq直接过滤,也方便后续接日志系统。级别使用要有纪律:DEBUG 放详细请求响应(生产默认关)、INFO 放订单生命周期和状态变更、WARN 放可恢复异常(重连、限频、对账差异)、ERROR 放需要人介入的问题(熔断、鉴权失败、连续下单失败)。日志里绝对不能出现密钥和完整签名,这个要在封装层做统一脱敏。
指标至少要有这几类:当前持仓和可用余额(Gauge)、累计下单数和成交数(Counter)、下单延迟 P50/P95/P99(Histogram)、WebSocket 重连次数、对账差异条数、风控拒绝次数、事件循环延迟。事件循环延迟这个指标特别有用——asyncio里如果某个同步操作阻塞超过 100 毫秒,延迟会明显上升,这是发现"偷偷写了个同步数据库查询"的最好办法。用 Prometheus client 暴露一个/metrics端点,配 Grafana 面板,一眼能看出系统健不健康。
告警要分级,且必须能"打通"。我配了三条硬告警:进程挂了(心跳端点连续 3 次探测失败)、熔断触发、对账差异持续非零。渠道用邮件或即时通讯的 webhook,内容里带上机器标识和关键指标快照。这里有个经验:告警必须带"自愈检查",比如进程挂了先自动重启一次,重启成功就只发通知不发紧急告警,避免半夜被无意义的抖动叫醒。
4.3 灰度上线、灾备与一键停机
上线流程我固定为四步:本地单测 → 模拟盘跑够 72 小时 → 实盘小资金灰度 → 正式参数。模拟盘这一步千万别省,它主要验证的不是策略赚不赚钱,而是验证"你的订单状态机在真实推送下是否稳定"。我在模拟盘阶段重点看三个数:对账差异条数是否为 0、WS 重连是否能自动恢复、限频是否被触发。这三个都干净了,才考虑碰真实账户。
灰度阶段用最小可下单位,跑一天,人工核对每一次成交记录和日志是否对得上。这一步主要抓"参数级"的错,比如数量精度、最小下单量、下单类型语义,这些在模拟盘里往往因为参数宽松测不出来。
灾备方案核心是一键停机。我做了两个操作入口:一个是本地信号文件(进程每秒检查一次,文件存在就进入只读模式),一个是管理用的 HTTP 端点(带 token 鉴权)。触发后立即撤销所有未成交订单,停止开新仓,把当前状态完整落盘,推送告警。为什么要两种?因为网络出问题时 HTTP 端点可能连不上,而信号文件只要你能登上机器就能用。反过来,人在外面的时候用 HTTP 端点更方便。这套东西必须在上线前演练过至少一次,没演练过的应急预案等于没有。
5. 常见问题与排查实录
5.1 高频故障速查表
下面这张表是我这几年攒下来的,覆盖了 80% 以上的现场问题。错误码含义以官方文档为准,这里写的是常见成因和排查动作:
| 现象 / 错误 | 最可能的原因 | 排查动作 |
|---|---|---|
| 签名校验失败 | 时间戳格式错、query 顺序被改、body 与签名串不一致、Secret 配错 | 打印签名原文和实际请求串逐字符比对,确认服务器时间偏移 |
| 时间戳过期类报错 | 服务器 NTP 失效,本地时间漂移 | 调/api/v5/public/time算偏移,检查 NTP 服务状态 |
| 参数错误类报错 | 价格或数量未按tickSz/lotSz取整,低于minSz | 打印实际发送的px/sz,和 instruments 缓存比对 |
| 请求频率超限 | 无节流直发、重试风暴、多个实例共用密钥 | 加令牌桶,重试加指数退避,确认是否有多实例 |
| 余额不足 | 本地缓存的可用余额过期,未预留手续费 | 下单前强制拉一次余额,或预留 10%~20% 安全垫 |
| 订单重复 | 超时后盲目重试、clOrdId未做唯一约束 | 超时走"查询再决定",本地对clOrdId建唯一索引 |
| WebSocket 频繁断连 | 心跳缺失、订阅过多、本机网络抖动 | 加业务层心跳,拆分成多个连接,记录断开码 |
| 订单簿明显错误 | 增量推送丢包,未按seqId校验 | 校验prevSeqId连续性,不连续就重订阅拉快照 |
| 本地仓位与交易所不一致 | 状态机未做终态保护,漏处理部分成交 | 启动全量对账 + 定时轻量对账,差异告警 |
| 进程无响应但没退出 | 某个协程里有同步阻塞调用 | 查事件循环延迟指标,全局搜同步 IO 调用 |
排查方法上,我的习惯是先看时间线再看内容。把某个clOrdId相关的所有日志按时间排序拉出来,从"发单意图"到"REST 请求"到"WS 回报"到"状态落库",四个环节一对比,问题基本就浮出来了。90% 的诡异问题都能通过这条时间线定位。
5.2 几个踩过的坑和规避手法
坑一:用 float 算数量。早期我用 float 算仓位,0.1 + 0.2出来0.30000000000000004,发给交易所直接参数错误,加了日志才发现。改成全链路Decimal之后这类问题绝迹。规避手法很简单:代码里加一条 lint 规则或者 code review 检查项,凡是在交易相关模块里出现float(或者直接用小数字面量做运算,一律打回。
坑二:重连后重复订阅。WebSocket 断开重连后,很多人只在首次连接时发订阅消息,重连后忘了重发,结果连接是通的但一条数据都不来。更隐蔽的是反过来——重连逻辑里既重发了订阅,外层又发了一遍,导致同一个频道收到两份推送,策略被重复触发。规避手法:订阅逻辑收敛到一个函数,连接建立后统一调用,维护一个"已订阅频道集合",重连时整体重放,避免重复。
坑三:限频器只记住了成功请求。限频是按"发出的请求数"算的,不是按"成功的请求数"。我最早写的限频器在请求失败时不计入,结果遇到连续失败时疯狂重试,直接把自己限死了。正确做法是在发请求之前就占用令牌,无论成功失败都算消耗。
坑四:日志把磁盘写爆。DEBUG 级别打开跑了一周,日志文件涨到几十个 G,直接把机器塞满。规避:日志按天轮转 + 单文件大小上限 + 总保留天数,生产环境默认 INFO,需要排查时再用信号临时打开 DEBUG,并设置自动关闭时间。
坑五:没考虑夏令时和时区。交易系统内部统一用 UTC 时间戳,展示层再转本地时区。凡是涉及"日内"统计的,明确是以 UTC 零点还是本地零点重置,这个不说清楚,日内风控额度会在你意想不到的时候被重置。
我个人的做法是给每个坑都写一条自动化检查放进 CI,比如扫描代码里的
float(、扫描配置里是否出现明文密钥、检查 Dockerfile 是否用了 root 用户。人总会忘,让机器记住更靠谱。
6. 测试验证与上线前的最后把关
6.1 单元测试与故障注入清单
签名和精度这两个纯函数必须单测,而且要覆盖边界:时间戳格式、空 body、带 query 的 GET、特殊字符的 passphrase、步长为 0 的极端情况、负数和小数位超长的输入。这类测试写起来便宜,能挡住 90% 的低级错误。
状态机的测试用"回放"方式最有效:把一批真实推送消息存成 JSON 文件,喂给状态机,断言最终状态。重点是构造异常序列——重复推送、乱序推送、终态之后再推送、中途插入一条未知clOrdId的消息,看状态机是否稳得住。这部分测试我建议写到覆盖率 100%,因为它太关键了。
故障注入得在模拟盘环境做实操,不能只看看代码。我固定做这几个演练:
- 手动杀进程,看重启后能否正确对账、会不会重复下单。
- 拔网线 30 秒,看重连逻辑、心跳超时、待确认订单的兜底查询是否生效。
- 把限频阈值调到极低,验证限频器和退避策略是否按预期工作。
- 故意传错参数(数量低于
minSz),验证错误处理和告警链路是否打通。 - 模拟磁盘写满,看日志系统是否会拖垮主流程。
每次演练都记录"现象 / 预期 / 实际 / 修复项",形成一份检查清单,每次发版前跑一遍。
6.2 上线自检与长期运行建议
上线前我会过一遍这份检查清单:密钥是否走环境变量、是否明确指向模拟盘或实盘、精度缓存是否已加载、限频参数是否按官方文档配置且留了余量、风控阈值是否配置、告警通道是否测通、一键停机是否演练过、日志是否会无限增长、数据库是否有定期备份、时区是否统一 UTC。
长期运行方面,有三个经验值得分享。第一,定期做"空跑测试":把风控阈值调到极小,强制执行几次拒绝,确认熔断和告警链路还是通的。链路会腐化,半年不测的东西往往已经坏了。第二,每周看一次对账差异统计和重连次数趋势,这两个指标缓慢上升通常意味着网络环境或代码逻辑在退化,是早期预警信号。第三,版本回滚方案要提前准备好,镜像打 tag、配置版本化,出问题时能十分钟退回上一个稳定版本,这比现场 debug 靠谱得多。
最后说个实际体会:交易机器人的开发时间分配,大概是 20% 写策略、30% 写接入、50% 写状态管理和容错。刚入行的时候总觉得前两项才是正事,跑久了才明白,能一直不出错的机器人,靠的全是那 50% 看起来不"酷"的工程代码。把这些枯燥的部分做扎实,晚上才能睡得着觉。