1. 先把"接入"这件事拆开看清楚
做券商交易接口接入,最怕的不是代码写不出来,而是压根没想清楚自己到底要接什么。我见过太多人上来就问"哪家柜台快",结果一问需求,连自己要的是行情还是交易、是单账户还是多账户、是低频委托还是日内高频都没搞清楚。国内常见券商交易接口接入方法这条路,本质上是一条"协议适配 + 会话管理 + 状态同步"的工程活,跟写业务代码完全不是一个思路。
简单说,这类接入干的事就是:把你的策略程序或交易终端,通过券商给的通道,连到柜台,完成登录、订阅行情、报单、撤单、收回报、做对账这一整套闭环。它解决的是"程序化操作账户"的问题,适合三类人参考:一是刚开始做量化、想把手工交易变成程序化交易的开发者;二是已有策略框架、需要对接多家柜台做适配的工程团队;三是做交易终端、需要把多个通道统一成一套接口的产品方。不同基础的人关注点差别很大,新手最该盯的是会话和状态机,老手最该抠的是延迟和异常路径。
我个人的判断是,接口接入的难点八成在"异常路径"上,正常下单那条路谁都能跑通,跑不通的都是断线、乱序、重复回报、结算窗口这些边角。所以这篇文章不按"API文档翻译"的路子写,而是按一个真正落过地的人会怎么拆、怎么试、怎么踩坑来写。文中涉及的券商、柜台、终端一律用代称,比如"某券商A的网关""某柜台B",具体报文也做了抽象处理,思路通用,细节按各家文档对号入座即可。
1.1 接入前必须先回答的六个问题
动手写第一行代码之前,我建议先把下面这张表填满。填不满就说明需求还没定型,此时写代码一定是白写。
| 问题 | 典型选项 | 影响的技术决策 |
|---|---|---|
| 交易频率量级 | 分钟级 / 秒级 / 毫秒级 | 决定用轮询还是长连接推送 |
| 账户数量 | 单账户 / 多账户 / 多市场 | 决定会话复用与连接池设计 |
| 行情需求 | 快照 / 逐笔 / 全档 | 决定带宽、缓存、落盘策略 |
| 部署位置 | 本地 / 托管机房 / 云主机 | 决定网络时延与白名单配置 |
| 品种范围 | 股票 / 基金 / 债券 / 期权 | 决定报单字段与价格档位差异 |
| 容灾要求 | 允许中断 / 不允许中断 | 决定主备切换与幂等设计 |
这六个问题里,最容易被人忽略的是最后两个。品种范围看着只是换个代码,实际上期权和股票在报单字段、价格精度、持仓方向上的差异能让你重构一遍订单模块。容灾要求更典型,很多人默认"断了重连就行",但重连之后在途委托怎么办、已经发出去但没收到确认的那笔单子是死是活,这个不解决,重连就等于重复下单。
提示:接入方案的技术选型,八成由"频率量级"和"容灾要求"这两个维度锁死。先把这两项定死,剩下的都是细节。
1.2 一套能长期用的分层结构
我不推荐把接口调用直接散落在策略代码里。真实项目里我的做法是固定三层,从上到下依次是策略层、适配层、通道层。
策略层只认一套自己定义的抽象接口,比如place_order(symbol, side, price, qty)和on_order_update(callback),它不知道底下连的是哪家柜台。适配层负责把这套抽象接口翻译成具体柜台的报文和字段,一家券商一个适配器,互相隔离。通道层管的是连接、心跳、重连、序号校验、限流这些纯粹的通信问题,跟业务无关。
这么分的好处是,换一家券商只需要新写一个适配器,策略层一行不用动。我做过一个需要同时对接三家通道的项目,正是因为提前分了层,第三家接入只花了两天,而第一家花了两周。反过来,如果一开始就在策略里直接写报文,后面每加一家都是一次小型重构。
代价也有,就是前期要多写一层转换代码,字段映射表会变得比较长。这个投入我认为完全值得,尤其当你预期未来会接第二家、第三家的时候。
1.3 为什么不做"一把梭"的万能适配器
有人会想,干脆写一个超级适配器,把所有券商的差异都用 if-else 塞进去。我试过,最终结果是这个文件长到三千行,改一个字段要翻半天,谁都不敢动。
更合理的做法是"抽象接口统一、适配器隔离、差异用配置表驱动"。字段名的映射、价格精度的换算、数量单位的换算,这些都可以做成配置,不需要写死在代码里。真正需要写代码的只有行为差异,比如某家柜台的撤单是异步确认、某家是同步返回,这种才进适配器逻辑。
配置表驱动还有一个隐形好处:出问题的时候,你能快速判断是"配置写错了"还是"代码逻辑错了",排查范围直接砍半。
2. 接口形态与协议细节,先搞明白你在连什么
国内常见券商交易接口接入,形态其实就那么几种,但每种的门槛、稳定性、适用范围差别很大。搞不清楚形态就去写代码,等于闭着眼睛挑工具。
2.1 五种常见通道形态的横向对比
下面这张表是我按实际使用感受整理的,只讲特征不讲品牌。
| 形态 | 通信方式 | 典型时延量级 | 稳定性 | 接入门槛 | 适用场景 |
|---|---|---|---|---|---|
| 柜台网关直连 | TCP 私有二进制 | 最低 | 高 | 高,需申请与联调 | 日内中高频、多账户 |
| 开放接口服务 | HTTP/JSON + WebSocket | 中等 | 较高 | 中,注册申请即可 | 中低频、策略托管 |
| 终端插件方式 | 本地动态库调用 | 低 | 依赖终端进程 | 中 | 已有终端、做增强 |
| 文件交换 | 定时生成与读取 | 最高,分钟级 | 高 | 低 | 批量委托、日终对账 |
| 消息中间件转发 | 消息队列订阅 | 中等 | 较高 | 中高 | 多系统解耦、内部转发 |
选型逻辑很简单:如果你的委托频率在秒级以下、对成交概率敏感,那基本只能走柜台网关直连或者终端插件;如果一天就几十笔调仓,开放接口服务完全够用,没必要为了那点时延去啃二进制协议;如果只是做日终核对,文件交换最省事,还天然带一份可审计的落地记录。
我踩过的一个坑是"过度选型":早期一个日频调仓的项目,非要上柜台直连,结果光是申请流程和联调就拖了一个多月,最后发现日频策略根本感知不到那几十毫秒的差异。反过来也见过"选型不足":用轮询 HTTP 去做需要抢时点的操作,每次都慢半拍。选型这件事,合适比先进重要。
2.2 会话层的三件事:登录、心跳、序号
不管你连的是哪种形态,只要它是长连接,就一定绕不开会话管理。这三件事做不好,后面全是玄学问题。
登录握手。一般是先建立传输层连接,再发登录报文,报文里带账号、密钥或令牌、终端标识、版本号。有的通道还要求先做一次时间同步,或者先查询一次服务器时间做偏移校正。我建议登录成功后立刻本地记录一次服务器时间与本机时间的差值,后面所有时间戳比较都用校正后的时间,不然跨机器部署时日志时间对不上,排查会很痛苦。
心跳。心跳间隔不能拍脑袋定,要看通道文档给的建议值,通常比服务端的超时判定要短,比如服务端 60 秒无数据断开,你就要在 20 到 30 秒之间发一次。心跳不能只发不收,必须校验返回,连续 N 次没收到就得主动判定连接已死并触发重连。我见过只发心跳不校验返回的实现,连接早就半开了,程序还以为自己活着,委托全丢。
序号连续性。很多柜台报文带递增序号,用来做丢包和乱序检测。收到报文先比序号,不连续就说明中间丢了,这时候要么请求重传,要么直接判定会话不可信、走重建。序号这块我建议做一个滑动窗口的本地记录,把最近的序号落在内存里,重连后按约定规则重新对齐,不要指望"重连后序号自动归零"这种假设。
注意:心跳失败后不要立刻重连,先做一次"连接探活",确认是真的断了还是只是心跳包丢了。频繁重建连接在很多通道上会触发风控,反而更麻烦。
2.3 行情通道和交易通道为什么要分开
新手常犯的错是把行情和交易塞在一条连接里。技术上可行,工程上是自找麻烦。
原因很直接:行情的量级远大于交易。一个全市场订阅的行情推流,峰值每秒几千条消息,而交易报文一天可能就几百条。混在一起,行情一旦拥塞,交易报文的发送和接收就会被拖慢,最坏情况是委托发不出去或者回报收不到。而且行情处理里通常要做计算、落盘、指标更新,这些耗时操作会阻塞 IO,交易侧的时延就没法保证了。
我的做法是物理隔离:行情一条连接(或者多条,按订阅分片),交易一条连接,两者通过内存队列把数据汇聚到策略层。行情侧的线程只负责收和解析,解析完丢进无锁队列就走,耗时的处理放到独立线程池。交易侧的线程模型更简单,一个收、一个发,中间用队列衔接,回调里绝对不做重活。
隔离之后还有个附带好处:行情断了不影响交易,交易断了行情还能继续跑,故障域变小了。
2.4 报文字段里最容易出错的几个地方
字段映射看着是体力活,实际上埋雷最多。我列几个高频的:
数量单位。有的通道用"股",有的用"手",一手等于一百股。这个搞错就是十倍偏差,而且不一定被拒单,可能直接成交,损失真实。我的做法是在适配层入口统一做单位归一,配置表里明确写清qty_unit: share还是qty_unit: lot,转换函数只在一处,不允许别的地方再转。
价格精度。不同品种的最小变动价位不同,股票通常到分,基金有的到千分之一,债券更细。你算出来的价格如果不落在合法档位上,很多通道会直接拒单,返回的错误码还可能只说"参数错误"。正确做法是下单前按该品种的档位做一次向合法方向的取整,取整规则(四舍五入还是向下取)要跟业务确认,因为这会影响成交概率。
买卖方向与开平标志。股票一般只有买卖方向,衍生品还要带开平、备兑等标志,字段枚举值各家还不一样。这块我建议在适配层里维护一张枚举映射表,双向映射都要写,回来解析回报时用得上。
报单引用号。这是你自己生成的本地唯一标识,用来把委托、成交、撤单串起来。务必保证全局唯一,建议用"实例ID + 时间戳 + 自增序列"的组合,不要只用自增,多进程会撞号。这个号是你的救命稻草,后面幂等、对账全靠它。
编码。证券名称这类中文字段,有的通道用 GBK,有的用 UTF-8,转换点如果搞错就是乱码。乱码本身不致命,但它会污染你的日志,让你排查问题时看错标的。
3. 从零跑通一条下单链路的完整实操
这一章我按实际动手顺序写,从环境准备到全链路跑通,把每一步的意图和验证方法都标出来。你要复现的话,顺序不要跳。
3.1 环境准备与仿真验证
第一步永远是拿仿真环境,不要拿真实账户试。多数通道都提供模拟撮合环境,撮合规则会简化,但报文格式、字段校验、回报时序基本一致,用来验证链路足够。
环境准备清单我一般这么列:一台部署机(和最终环境尽量同地域)、通道提供的连接地址与端口、账户与密钥、IP 白名单登记、必要的证书文件、时钟同步服务。别小看时钟同步,我遇到过因为本机时间漂移两分钟,导致日志里回报时间早于委托时间,排查了半天以为是乱序。
验证顺序建议这样:先只做登录,确认握手成功并拿到会话标识;再只做一次查询(比如查资金或查持仓),确认请求-响应这条路通;然后订阅最小范围的行情,确认能收到推送;最后才是报单。每一步单独验证,出问题范围小。
提示:仿真环境一定要测两类东西,一是正常流程,二是异常注入。断网、杀进程、中途改网络配置,这些在仿真环境随便折腾,在真实环境一次都别试。
3.2 登录与会话建立的可复用写法
登录这块我给一个通用的结构思路,语言用什么不重要,重点是顺序。
# 伪代码,具体报文按各家文档替换 def connect_and_login(cfg): sock = open_channel(cfg.host, cfg.port, timeout=cfg.connect_timeout) time_offset = sync_server_time(sock) # 先校正时间偏移 send(sock, build_login(cfg.account, cfg.secret, cfg.terminal_id)) resp = recv_until(sock, expect_type="LOGIN_RSP", timeout=5.0) if resp.code != 0: raise LoginError(resp.code, resp.message) session = Session( sock=sock, session_id=resp.session_id, time_offset=time_offset, seq_window=SlidingWindow(size=1024), ) return session这里有几个刻意的设计。时间偏移在登录时就取,后面所有时间戳都用local_time + offset,日志和业务判断口径统一。会话对象里带一个序号窗口,收到任何报文先过一遍窗口校验,不合格直接丢弃并记事件。登录失败要抛出带错误码的异常,不要吞掉,错误码是排查的第一手信息。
心跳建议单独一个线程或定时任务,跟业务收发解耦。心跳线程只管两件事:按时发、检查最近一次收到服务端数据的时间。如果超过阈值没收到,置一个"疑似失联"标志,让上层决定是否重建。
3.3 行情订阅与本地缓存的落地方式
行情这块,我最推荐的结构是"订阅表 + 本地快照字典 + 增量队列"。
订阅表记录你要订阅的标的集合,启动时一次性下发,之后只在变更时增量订阅/退订。本地快照字典以标的代码为键,存最新的完整快照,供策略随时读取,读操作不加锁或者用读写锁,保证高频读不阻塞。增量队列存逐笔变化,供需要事件驱动的策略消费。
时间戳处理是个细节。行情报文里的时间通常是交易所侧时间,精度可能是毫秒也可能是微秒,有的还带本地接收时间。我建议两个都存:交易所时间用于业务判断(比如判断是否在某个交易时段),本地接收时间用于测量端到端延迟。判断乱序时用交易所时间,如果收到的报文时间戳比缓存里的还旧,直接丢弃并计数,这个计数是监控行情质量的重要指标。
落盘要节制。全量落盘在高频行情下 IO 扛不住,我的做法是原始报文按分片滚动写,只保留最近若干小时,同时把解析后的关键字段抽样落盘做回放用。回放能力非常重要,很多问题只有回放才能复现。
3.4 委托、撤单与订单状态机的处理
订单状态机是整个接入里最容易写错的地方,因为回报可能乱序。举个真实的场景:你先收到"全部成交",再收到"部成",如果你按"来一条覆盖一条"的方式更新状态,最终状态就退回了部成,持仓计算全错。
正确做法是给状态定优先级,只允许单向推进。典型的状态序列是:待报 → 已报 → 部成 → 全成;撤单路径是:已报 → 待撤 → 部撤或已撤;异常路径是:待报 → 废单。收到回报时先比优先级,低于当前状态的一律忽略,只记日志。
| 状态 | 含义 | 允许迁入的前置状态 | 终态 |
|---|---|---|---|
| 待报 | 本地已发出未确认 | 初始 | 否 |
| 已报 | 柜台已接受 | 待报 | 否 |
| 部成 | 部分成交 | 已报、部成 | 否 |
| 全成 | 全部成交 | 已报、部成 | 是 |
| 待撤 | 已发撤单未确认 | 已报、部成 | 否 |
| 部撤 | 部分成交后撤余量 | 待撤 | 是 |
| 已撤 | 全部撤单成功 | 待撤 | 是 |
| 废单 | 柜台拒单 | 待报、已报 | 是 |
成交回报要按成交编号去重,同一笔成交重复推送的情况真实存在,尤其是在重连补推的时候。去重集合建议保留最近若干分钟,太长了内存吃不消,太短了覆盖不住补推窗口。
撤单有一个细节要注意:撤单请求发出后到收到撤单回报之间,这笔委托仍然可能继续成交。所以"待撤"状态下的委托,成交回报照常处理,只是最后一步按实际剩余量决定是"已撤"还是"部撤"。
3.5 断线重连与幂等设计,这是保命的部分
重连逻辑的核心不是"连上",而是"连上之后怎么把状态对齐"。我的流程是四步。
第一步,重建连接并登录,拿到新的会话标识。第二步,做一次在途委托查询,把所有未终结状态的委托拉回来,跟本地状态机逐笔核对,本地有柜台无的标记为可疑,柜台有本地无的补录。第三步,做一次成交查询,按时间范围拉取断线期间的成交,用成交编号去重后补进状态机。第四步,重新订阅行情,并对比断线期间是否有订阅遗漏。
幂等靠报单引用号。发单之前先在本地持久化一条记录(状态待报),再发出去。如果发送过程中出异常,本地记录还在,重连后先查询这笔引用号在柜台是否存在,存在就按柜台状态更新,不存在才允许重发。这个过程不能省,很多人重连后直接重发,结果就是重复下单,运气不好还会形成对倒。
注意:日终结算时段通道通常会停止服务,这段时间的重连一定是失败的,程序里要识别这种"计划内不可用",不要疯狂重试把账号搞进风控名单。
3.6 日终对账:不做这一步等于裸奔
不管你的实时回报处理得多好,日终必须对一次账。对账的内容包括成交明细、持仓数量、资金余额三块,口径用柜台给的结算文件为准。
对账流程一般是:收盘后等待通道发布对账文件,下载并解析,与你本地落库的数据逐笔比对。成交按成交编号比对,持仓按标的比对数量,资金比对可用与总额。差异要分类:可能是本地漏记、可能是回报重复导致多记、可能是手续费口径不同导致资金差几分钱。
差异处理我建议做成"分级 + 人工确认"。数量级差异自动告警,绝不自动修正,因为自动修正会把一次真实的漏单掩盖掉。金额的小额差异可以设置容差阈值。这套东西我第一次做的时候觉得很繁琐,做了三个月之后发现它救过我好几次,尤其是那些实时回报丢了但成交确实发生的情况。
4. 常见问题与排查技巧实录
下面这张表是我这几年攒下来的,按出现频率排序,基本覆盖了八成的现场问题。
| 现象 | 最可能原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 登录成功但收不到回报 | 未订阅私有流或回报通道未开通 | 查登录回报里的订阅列表 | 补订阅或联系通道开通 |
| 下单被拒,提示参数错误 | 价格不在合法档位或数量单位错误 | 打印原始报文核对档位与单位 | 归一化后重发 |
| 委托成功但持仓不变 | 回报未处理或状态机卡在中间态 | 查该引用号的状态流转日志 | 修状态推进逻辑 |
| 重连后出现双份委托 | 重发前未查询在途委托 | 查本地是否持久化了报单记录 | 补幂等查询 |
| 行情偶发丢包后错乱 | 序号未校验 | 检查序号窗口日志 | 开启连续性校验,丢弃异常包 |
| 程序运行几小时后卡死 | 回调里做了阻塞操作 | 抓线程栈看阻塞点 | 耗时逻辑移出 IO 线程 |
| 撤单一直返回失败 | 委托已成交或已终结 | 查该委托当前状态 | 识别错误码语义,不要盲重试 |
| 日终数据对不上几分钱 | 手续费或过户费口径不同 | 逐笔比对费用字段 | 设置金额容差 |
4.1 几个文档里不会写的坑
坑一:交易日切换时段的行为不一致。有的通道在非交易时段接受报单但直接拒掉,有的会缓存到开盘再报,还有的直接断连。这三种行为对你的状态机影响完全不同。缓存到开盘那种最坑,你以为废单了,结果开盘成交了。处理办法是配置一个"允许报单时段"白名单,时段外一律不发。
坑二:集合竞价时段的报单类型限制。某些时段只接受限价单,市价单会被拒;某些时段撤单不受理。这个不能靠试,要在代码里按时段做前置校验,拒单原因里看到"时段不符"再去翻文档就晚了。
坑三:长连接半开。网线拔了、对端静默关闭,本地 socket 可能还是"已连接"状态,发数据也不报错。这就是为什么心跳必须校验返回。我现在的做法是:除了应用层心跳,再开一个 TCP keepalive 兜底,两层保险。
坑四:日志把密钥写进去了。排查问题的时候图省事打了完整登录报文,结果日志里全是明文密钥。这个必须从第一天就做脱敏,登录报文、密钥、令牌这些字段在日志输出层统一打码,别指望后面补。
坑五:多进程抢同一个账号。同一账户在多个进程或多个终端同时登录,很多通道会互相踢,表现为"随机掉线"。这个现象特别迷惑人,因为代码看起来没问题。遇到随机掉线先查是不是重复登录。
4.2 延迟到底该怎么测
很多人说自己的链路"很快",但你问他快多少、怎么测的,就答不上来了。延迟要分三段测,混在一起没有意义。
第一段是本地到通道的网络往返,用一次轻量的查询请求来回测时间,取多次的中位数。第二段是委托从发出到收到柜台确认的时间,这个才是业务真正感知的报单延迟。第三段是行情从交易所时间戳到本地处理完成的时间,需要交易所时间和本机校正时间配合计算。
测的时候要注意两点。一是不要在业务高峰测一次就下结论,要连续采样看分布,P99 比平均值有意义得多。二是本机时钟必须同步,否则第二段和第三段的测量结果全是错的。我一般把这三段指标做成定时上报,画成曲线,异常时一眼能看出来是哪一段变慢了。
5. 稳定性工程:让这套东西能长期跑住
接口接入跑通只是开始,能连着跑几个月不出事才是本事。这一章讲几个我认为必须做的工程化动作。
5.1 线程模型与限流
线程模型上,我的固定搭配是:行情收包一个线程、行情解析一个线程、业务处理一个线程池、交易收一个、交易发一个、心跳一个。所有跨线程的数据用队列传递,禁止在收包线程里做任何可能阻塞的事。
限流是必须做的,不是为了性能,是为了不触发通道侧的风控。多数通道对单账户的报单频率、撤单频率、每秒请求数都有上限,超了轻则拒单重则限制权限。我的做法是在发送端做令牌桶,频率设成通道上限的七八成,留出余量。同时统计被拒次数,如果因为"频率超限"被拒,说明桶的配置不对,要回调。
还有一个容易忽略的限流点:撤单。有些人为了追价频繁撤单重报,撤单频率往往比下单还高,先撞上限的常常是撤单。
5.2 监控指标该盯哪几个
指标不要贪多,我固定盯六个:登录状态、心跳最近成功时间、在途委托数量、回报处理队列积压、行情端到端延迟 P99、当日被拒单次数。
这六个里,"在途委托数量"是最灵敏的。正常情况下它应该在个位数波动,如果它开始持续上涨,说明回报处理出了问题或者通道侧卡住了,这时候往往业务还没感知到异常,你已经能提前介入。
"回报处理队列积压"也很关键,它反映的是你的处理能力和回报速度是否匹配。队列一旦持续增长,说明处理慢了,再发展下去就是内存涨、延迟涨。
5.3 灰度与回滚
新接入一家通道,或者改动适配层逻辑,不要一次性切全量。我的做法是先用极小仓位跑一段时间,比如只用一个标的、固定最小数量,观察一两天,确认成交、回报、对账三条线都正常,再逐步放大。
回滚方案要提前想好:出问题时怎么在秒级切回上一版或者切回手工操作。这个预案写起来很枯燥,但真出事的时候,它决定了你损失多少。
5.4 我个人的几条硬规矩
第一,任何发往柜台的请求,本地必须先落记录再发送,绝不能先发后记。第二,所有错误码都要有明确的处理分支,不允许出现"未知错误就重试"这种写法,未知错误只允许告警和人工介入。第三,回调函数里只做三件事:解析、入队、更新状态,别的一概不做。第四,每改一次适配层,都要跑一遍仿真环境的全链路用例,包括异常注入用例。
踩过几次坑之后我越来越觉得,这套东西的技术含量不在协议本身,而在你愿不愿意为异常路径多写那几百行代码。协议看文档两天就能懂,异常路径是拿真金白银换来的经验。如果你正准备接第一条通道,我的建议是把七成时间花在会话管理、状态机、幂等和对账上,剩下三成留给正常流程,这个比例看着别扭,但跑起来之后你会感谢自己。