交易技术

CTP API断线重连机制:如何防止量化系统在夜盘断网时漏单

凌晨两点四十五分,沪镍主力合约突然放量击穿止损位。策略逻辑没问题,止损单也已经发出——但它没有成交。等到行情快速反转,账户里那个被深套的仓位还在,夜盘却已经收盘。此时再回头查日志,往往只能看到一段模糊的断线记录,却说不清那几秒钟里究竟发生了什么。…

CTP API断线重连机制:如何防止量化系统在夜盘断网时漏单

这不是剧本里的桥段,而是量化实盘里反复出现的事故。根因通常不在策略层,而在基于CTP的交易系统没有正确处理断线、重连、认证和私有流补偿。系统可能已经重新连上前置机,行情也重新开始跳动,但断线期间的成交回报没有进入本地状态机;策略以为订单还在挂着,柜台却已经成交,或者订单根本没有发出去。

CTP是国内期货市场最常用的柜台接口之一。它的底层API确实提供了心跳检测和自动重连能力,但这并不等于量化系统具备了完整的断线恢复能力。API负责把连接重新建立起来,至于身份认证、会话更新、报单引用、私有流同步和本地状态重建,仍然需要由交易系统自己完成。

真正可靠的ctp api断线自动重连方案,不是在断线回调里不断调用初始化函数,而是把“连接恢复”和“交易状态恢复”拆成两个不同的问题分别处理。前者由CTP底层完成,后者必须由你的程序负责。

CTP底层重连机制:那个你控制不了的OnFrontDisconnected

先认清一件事:CTP的自动重连属于API底层行为,应用层不能把它当成一个可自由编排的普通网络连接池。

CTP的C++ API内部维护着心跳与网络连接线程。当网络异常、心跳超时、前置机主动关闭连接,或者底层读写发生错误时,API会调用:

OnFrontDisconnected(int nReason)

这个回调告诉上层:“当前与前置机的连接已经断开。”在此之后,API通常会自行尝试重新建立连接。连接成功后,再调用:

OnFrontConnected()

这两个回调只代表网络连接状态发生了变化,并不代表交易会话已经恢复,更不代表系统马上可以继续下单。

很多系统的问题,就出在把OnFrontConnected()理解成“登录成功”。实际上,重新连接前置机只是恢复流程的第一步。连接建立以后,通常还要重新完成客户端认证、终端信息上报、用户登录,并等待登录响应。登录成功后,还要同步新的会话信息,查询断线期间的报单和成交,最后才能解除交易熔断状态。

OnFrontDisconnected中的nReason是排查断线原因的重要线索。它一般以整数形式传入,日志中最好同时记录十进制值和十六进制值。不过,不同API版本、柜台实现和封装层对错误码的解释可能存在差异,不能只凭一张固定的错误码表下结论。生产环境应该以所使用版本的头文件、接口文档和实际日志为准。

常见的排查方向可以这样理解:

现象或错误类型更可能涉及的问题处理重点
网络读取失败链路中断、出口设备异常、前置机无响应检查网络路径、路由和防火墙日志
网络写入失败本机网络栈、网卡、路由或出口带宽异常检查服务器网卡、TCP连接和出口质量
心跳超时丢包、延迟抖动、链路暂时不可达对比心跳日志、网络监控和机房链路
收到异常报文API版本、报文解析或环境异常核对API版本、柜台环境和异常响应
频繁断开又立即恢复链路抖动、进程阻塞或连接数问题检查线程调度、CPU负载和前置机连接限制

这张表的意义不是让你把每个错误码硬背下来,而是帮助你区分“网络真的断了”和“应用层把连接拖死了”。如果断线原因集中出现在心跳超时,首先要看网络链路和进程是否长时间阻塞;如果每次发送特定请求后都出现异常报文,则要回到请求参数、API版本和柜台环境去排查。

OnFrontDisconnected里,建议只完成几件事:

  • 记录断线时间、错误码、当前交易节和进程状态;
  • 把连接状态标记为“断线”;
  • 暂停依赖私有流的下单、撤单和仓位判断;
  • 记录断线前最后收到的订单、成交和查询响应时间;
  • 向监控系统发送告警。

不要在这里启动多个线程反复调用Init,也不要在回调内部叠加一套自定义的“重连循环”。CTP底层已经拥有自己的连接管理逻辑,应用层再次创建重连机制,容易造成多个连接对象并行运行、回调交叉、资源未释放以及会话状态覆盖。

CTP的自动重连是底层黑盒,你能控制的不是它什么时候重试,而是重连成功后系统如何恢复。

如果确实需要设置“最长不可用时间”,应该把它设计成监控和故障升级机制,而不是第二套连接机制。比如连续一段时间没有收到OnFrontConnected,或者重连后认证一直没有进入下一状态,就触发人工告警、备用通道切换或策略降级,而不是继续创建新的API实例。

用状态机管理恢复过程,而不是靠回调里堆代码

断线重连最容易失控的写法,是在各个回调里直接做下一步操作:

  • OnFrontConnected里直接登录;
  • OnRspUserLogin里直接查询;
  • 某个查询回调里直接恢复下单;
  • 另一个异常回调又重新发起登录。

这种写法在网络稳定时看不出问题,一旦断线、重复回调或响应乱序,就会出现重复认证、重复查询和提前放开交易的问题。

更稳妥的方式是给连接恢复单独建一个状态机,例如:

1. 断线;

2. 等待前置机重新连接;

3. 连接成功;

4. 等待认证响应;

5. 等待终端信息上报完成;

6. 等待用户登录成功;

7. 同步会话和报单引用;

8. 查询报单与成交;

9. 完成本地状态校验;

10. 恢复交易。

每个阶段都要有明确的进入条件、成功条件、失败处理和超时告警。回调只负责投递事件,状态机负责决定下一步做什么。这样即使OnFrontConnected因为底层状态变化被多次触发,也不会重复推进流程。

重连后的身份认证与看穿式监管合规流程

看穿式监管环境下,断线重连后的登录流程不能再简单理解为“重新调用一次用户登录”。

在常见的CTP接入流程中,重新建立前置连接后,系统通常要完成客户端身份认证、终端信息相关上报以及用户登录。不同期货公司、柜台版本和接入方式可能在具体接口顺序上有所差异,因此生产系统应以期货公司提供的接入说明为准,但有一个原则不会变:认证流程必须被当成交易恢复的必要条件,而不是启动时只做一次的初始化动作。

通常需要关注以下环节。

客户端身份认证

客户端认证一般通过ReqAuthenticate发起,涉及应用标识、认证码以及柜台要求的终端信息。认证响应要检查错误码和错误信息,不能只判断回调是否到达。

认证失败的原因可能包括:

  • 应用标识配置错误;
  • 认证码失效或与当前账户不匹配;
  • 连接到了错误的交易环境;
  • 终端信息发生变化;
  • API或柜台版本不兼容;
  • 期货公司侧没有开通对应权限。

有些程序只打印一条“认证响应收到”,不判断ErrorID,然后继续调用用户登录。结果是系统表面上已经连接,实际上后续请求全部处于不可交易状态。更麻烦的是,策略线程往往只观察连接对象是否存在,于是把“已连接但不可下单”误判成正常状态。

终端信息上报与设备指纹

看穿式监管涉及终端设备信息。部署环境一旦发生变化,认证结果可能与开发机完全不同。常见变化包括服务器迁移、虚拟机重建、网卡更换、硬盘更换、系统镜像更新以及容器运行环境变化。

因此,不要只在开发机上验证认证流程,然后直接把程序复制到生产服务器。生产环境需要单独完成:

  • 认证参数核对;
  • 终端信息采集;
  • 断线重连测试;
  • 认证失败告警测试;
  • 交易权限恢复测试。

如果系统运行在云服务器上,尤其要注意实例重建和网卡变化。某些团队平时使用固定实例,认证一直正常,直到云主机故障迁移或重新部署,才发现终端信息已经变化。程序仍然能连接前置机,OnFrontConnected也能正常触发,但认证阶段持续失败,夜盘交易实际上已经被锁死。

用户登录不是恢复流程的终点

ReqUserLogin成功后,系统至少还要确认:

  • 返回的错误码为成功;
  • FrontIDSessionID已经更新;
  • MaxOrderRef等登录响应字段已经被正确保存;
  • 当前交易日与本地交易日一致;
  • 查询请求可以正常发出并收到完整响应;
  • 行情和交易两个通道的状态没有混淆。

部分系统将行情连接和交易连接放在同一个抽象对象里,只要行情还在跳,就认为交易也已经恢复。这是危险的。行情前置和交易前置可能拥有不同的连接状态,交易登录失败时行情仍然可以正常接收。风控模块如果只看行情心跳,就会误以为系统处于可交易状态。

看穿式认证不是合规部门的附属流程,而是交易系统恢复链路中的一道硬闸门。认证没通过,就不能把“已连接”写成“可交易”。

SessionID变更与OrderRef报单引用冲突的规避

重连登录成功后,柜台可能为当前连接分配新的FrontIDSessionID。这两个字段不能继续沿用断线前的旧值。

在许多CTP系统中,订单的本地关联键会包含:

FrontID + SessionID + OrderRef

这意味着,断线前和断线后看似属于同一个账户,底层会话实际上已经发生了变化。旧订单仍然存在,但新的连接不能简单地把旧会话信息当作当前会话使用。

旧订单不等于旧会话

断线前已经提交的订单,可能处于以下几种状态:

  • 已经成交;
  • 部分成交;
  • 仍在交易所或柜台挂单;
  • 已撤单;
  • 被拒绝;
  • 处于未知状态。

断线时,本地可能只知道其中一部分。重连成功后,如果程序拿旧的会话信息去发撤单请求,柜台可能拒绝请求;即使撤单请求能够发出,本地订单映射也可能因为会话字段错误而无法正确更新。

正确的做法是把“旧订单识别”和“新会话操作”分开:

  • 用本地数据库保存原始报单信息;
  • 用交易日、合约、方向、开平、价格、数量和本地引用等字段建立多重关联;
  • 登录成功后更新当前会话的FrontIDSessionID
  • 通过当日订单查询确认旧订单的真实状态;
  • 只有在状态明确、引用完整时才发起撤单;
  • 对无法确认的订单进入人工或风控复核队列。

不要因为本地记录里有一张“未成交订单”,就直接认为柜台仍然有一张可撤订单。断线期间订单可能已经成交,也可能已经被柜台拒绝。状态确认必须以交易柜台返回的查询结果和回报为准。

OrderRef要有单调、可恢复的生成策略

OrderRef是报单引用,不是简单的随机字符串。生产系统应保证它在当前会话内能够被柜台正确识别,并且本地生成器在重启、断线和进程恢复后不会回退。

最常见的错误有三种:

1. 程序重启后计数器从初始值重新开始;

2. 多个策略线程各自维护一套计数器;

3. 断线重连后没有根据登录响应中的MaxOrderRef更新本地值。

比较稳妥的处理方式是:

  • 所有报单统一经过一个OrderRef分配器;
  • 分配器使用线程安全的递增操作;
  • 每次分配后持久化最近使用值,避免进程异常退出导致回退;
  • 登录成功后读取柜台返回的MaxOrderRef
  • 将本地最大值与柜台返回值比较,取更大的值继续递增;
  • 对长度限制和字符格式做校验;
  • 记录OrderRef与策略、信号、报单请求的对应关系。

可以把恢复逻辑理解为:

新的起始引用 = max(本地已使用最大值,柜台返回的MaxOrderRef) + 1

这里的关键不是某一行代码,而是这个动作必须发生在登录响应处理阶段,并且在恢复交易之前完成。如果OrderRef分配器仍然使用旧状态,第一笔恢复后的报单就可能出现重复或错位。

不要把OrderRef当成唯一订单身份

OrderRef适合做本地关联字段,但不应该承担全部订单身份识别功能。实际交易系统还需要保存柜台返回的订单系统编号、交易所报单编号以及本地生成的业务订单编号。

建议至少维护三层标识:

  • 策略订单编号:由策略或订单管理模块生成,用于追踪交易意图;
  • 本地报单编号:对应一次具体的报单请求;
  • 柜台和交易所编号:用于查询、撤单和事故复盘。

这样即使断线后OrderRef与会话发生变化,也可以通过多个字段重新建立关联,而不是把整个订单状态机押在一个字符串上。

私有流数据同步:如何通过主动查询补全断线期间的成交回报

CTP的私有流主要包含报单状态和成交回报。资金与持仓不应被统称为私有流,而应通过主动查询进行校验。它们都属于账户恢复时必须核对的状态,但来源和恢复方式并不完全相同,不能用“重新登录后继续接收实时回调”一概而论。

断线期间发生的成交,可能已经在交易所完成,但本地进程没有收到对应的OnRtnTrade。如果重连后使用只推送登录后新内容的订阅方式,本地就永远不会自动得到那笔历史回报。

私有流订阅模式的差异

常见私有流订阅模式可以这样理解:

模式主要含义断线后的数据表现更适合的场景
重传当日数据从当前交易日重新推送私有流有机会补回当日回报,数据量可能较大生产环境更容易恢复
从上次位置续传依赖本地流文件和上次接收位置本地文件完整时可减少重复数据有成熟流水管理的系统
仅推送登录后的数据只接收重新登录后的新回报断线期间数据不会自动补发仿真、测试或明确不依赖历史流的场景

具体常量名称和行为应以所用API版本为准。不能只因为某种模式在测试环境里“能收到回调”,就认为它适合生产。生产系统关心的不是回调是否出现,而是断线窗口内发生的所有状态变化能不能最终被找回来。

只使用快速模式的最大问题,是它可能让本地状态出现不可见的空洞。比如断线期间发生了成交,重连后行情继续更新,本地持仓却没有变化。策略看到的仓位比真实仓位少,风控看到的可用资金也可能不准确。之后一旦触发止损或对冲,程序会根据错误状态发出更多错误订单。

主动查询是最后的事实校验

无论选择哪种私有流模式,生产系统都应该在重连和登录完成后主动发起查询,把流式回报当成增量更新,把查询结果当成一次事实校验。

通常至少需要查询:

  • 当日订单;
  • 当日成交;
  • 当前持仓;
  • 资金账户;
  • 必要时查询合约、交易所和保证金相关状态。

其中,订单和成交查询是断线恢复的核心。可以按以下顺序处理:

1. 暂停所有依赖私有流的策略下单;

2. 等待用户登录成功;

3. 查询当日订单并写入临时恢复表;

4. 查询当日成交并写入临时恢复表;

5. 将查询结果与本地订单库、成交库逐笔比对;

6. 对本地缺失的成交补写记录;

7. 对本地状态与柜台状态不一致的订单标记为待处理;

8. 重算持仓、可用资金和冻结数量;

9. 校验多空方向、开平标志、成交数量和成交均价;

10. 通过后再解除下单保护。

这里不能简单地把查询结果全部覆盖本地数据库。因为查询回调可能重复返回,私有流也可能在恢复过程中补发同一条数据。订单和成交写入必须具备幂等性,至少要根据柜台订单编号、交易所成交编号或稳定的组合键去重。

对账不只是补一条成交记录

“补回漏掉的OnRtnTrade”只是第一层工作。真正需要恢复的是整个账户状态:

  • 某张报单是否已经被部分成交;
  • 剩余数量是否仍然挂在市场上;
  • 冻结保证金是否已经释放;
  • 撤单是否成功;
  • 成交对应的是开仓、平仓还是平今;
  • 组合策略的另一条腿是否也完成;
  • 本地持仓与柜台持仓是否一致;
  • 交易日切换后昨仓和今仓是否被正确区分。

例如,策略发出一张平仓单,断线前本地只记录了“已发送”,断线期间它实际成交了一部分。重连后如果只补成交数量,不更新剩余报单状态,程序可能继续发送重复平仓;如果只更新订单状态,不更新持仓和冻结数量,下一次开仓又会使用错误的可用资金。

因此,恢复流程应该有一个临时的“重连中”状态。对账完成之前,不允许策略模块把账户重新视为普通交易状态。

漏单不是策略错误,而是状态一致性错误。只要本地持仓和柜台持仓不一致,策略再聪明也只是在错误的账户上做决定。

为恢复过程设置降级交易状态

断线恢复完成后,也不建议立刻把所有策略全部放开。可以根据账户风险和策略类型设置几个交易状态:

  • 不可交易:连接、认证或查询尚未完成;
  • 仅允许撤单:需要先降低风险,但禁止新增仓位;
  • 限制仓位:只允许小规模试探性报单;
  • 正常交易:对账完成且核心状态一致;
  • 人工确认:出现无法自动解释的订单或成交差异。

这比简单的布尔值“连接正常/连接异常”更适合真实环境。连接正常不代表订单状态正常,订单查询完成也不代表持仓已经校验。系统应根据恢复阶段逐步放开权限。

非交易时段的API生命周期管理与资源释放

还有一个经常被忽视的场景:非交易时段的连接管理。

CTP前置机并不是全天候保持同样的交易服务状态。中午休市、夜盘收盘后、节假日前后,前置机可能主动断开连接,或者暂时不再响应交易请求。如果程序没有根据交易日历处理这些时段,底层API可能反复触发OnFrontDisconnected,然后尝试重新连接。日志里刷满心跳超时和网络错误,看起来像服务器网络故障,实际只是当前交易服务尚未开放。

持续重连会带来几个问题:

  • 日志被大量重复错误淹没;
  • CPU和网络资源被无意义消耗;
  • 监控系统不断产生误报;
  • 多次初始化和释放不完整时,容易出现线程或句柄泄漏;
  • 下一个交易节开始时,程序可能仍停留在异常状态;
  • 交易日切换后,本地状态与新交易节不匹配。

主动关闭比让API无限重试更干净

对于明确的非交易时段,可以由调度模块主动进入休眠或维护状态。常见做法包括:

  • 停止策略新报单;
  • 取消不再需要的行情和交易请求;
  • 等待必要的回调处理完毕;
  • 调用Release()释放API底层资源;
  • 等待工作线程退出;
  • 清理回调对象、连接对象和本地临时状态;
  • 在下一个交易节开始前重新创建实例并调用Init
  • 重新执行认证、登录和状态查询。

释放时要注意对象生命周期。不能在API线程仍然运行时直接销毁回调对象,也不能让策略线程持有已经失效的交易接口指针。一个常见的事故是,程序表面上调用了Release(),但策略线程还在发送请求,随后出现野指针、异常退出或回调访问已释放对象的问题。

如果程序不在非交易时段关机,就应该根据交易所交易日历建立明确的生命周期调度。不要把交易时间硬编码成一组固定时刻后长期不维护,因为节假日、临时休市和特殊交易安排都会让固定时间表失效。交易日历模块至少要能区分:

  • 当前是否为交易日;
  • 当前处于哪个交易节;
  • 是否临近夜盘开始;
  • 是否已经进入收盘清理窗口;
  • 下一交易节何时启动;
  • 是否存在特殊休市安排。

不要把所有断线都当成同一种故障

自动重连方案还需要区分断线类型。

如果是交易时段内的短暂链路抖动,系统应快速进入恢复状态,并在对账完成后恢复交易;如果是夜盘收盘后的正常柜台关闭,则不必把它当成严重网络故障;如果是认证失败、API崩溃或服务器网络长期不可用,则应进入更高等级的故障处理。

监控告警最好至少包含:

  • 断线发生时间;
  • 当前交易节;
  • 断线原因;
  • 最近一次收到私有流的时间;
  • 最近一次收到行情的时间;
  • 是否已经触发重连;
  • 认证是否成功;
  • 登录是否成功;
  • 对账是否完成;
  • 当前是否允许下单。

只有这样,值班人员才能判断账户到底处于“正常恢复中”“连接成功但不可交易”还是“需要人工介入”。

把重连流程写进交易系统,而不是只写进运维文档

一套可执行的CTP重连策略,应该在代码、日志、监控和运维手册里保持同一套状态定义。仅仅在文档里写一句“断线后自动重连”没有意义,因为真正的风险发生在重连后的几分钟里。

可以把系统恢复过程拆成五个阶段。

第一阶段:断线感知

收到OnFrontDisconnected(nReason)后:

  • 保存错误码和时间戳;
  • 保存断线前的FrontIDSessionID和最近OrderRef;
  • 标记交易连接不可用;
  • 暂停策略新单;
  • 记录最后一笔订单和成交的接收时间;
  • 启动告警,但避免重复告警刷屏。

此时不要立即清空本地订单,也不要擅自把所有未完成订单标记为撤单。断线只是说明通信中断,不代表柜台端订单已经消失。

第二阶段:等待底层重连

让CTP底层完成它自己的连接重试。应用层要做的是监控:

  • 是否再次收到OnFrontConnected
  • 重连等待时间是否超过阈值;
  • 是否出现重复连接对象;
  • 是否存在API线程卡死;
  • 是否有新的断线回调覆盖旧状态。

如果超时,进入故障升级流程。可以通知人工、切换备用服务器,或者停止策略,但不要在同一个API对象外面不断叠加InitRelease

第三阶段:重新完成认证和登录

OnFrontConnected触发后,按照当前接入环境要求完成认证和登录:

  • 发起客户端身份认证;
  • 等待认证响应并检查错误码;
  • 完成终端信息相关流程;
  • 发起用户登录;
  • 检查登录响应;
  • 保存新的会话参数;
  • 确认交易日和前置机返回信息。

任何一步失败,都应该停留在恢复状态并报警,不能直接跳到“交易恢复”。

第四阶段:重建订单、成交和持仓状态

登录成功后:

  • 更新新的FrontIDSessionID
  • 根据MaxOrderRef修正本地OrderRef分配器;
  • 查询当日订单;
  • 查询当日成交;
  • 查询持仓和资金;
  • 对查询结果去重并写入数据库;
  • 对比本地与柜台状态;
  • 重算冻结数量、可用资金和持仓成本;
  • 标记所有无法自动解释的差异。

如果系统有多个策略共享同一个账户,还要把账户级恢复结果广播给所有策略。不能让某个策略已经恢复下单,另一个策略仍然持有旧仓位视图。账户恢复必须是一个整体动作,至少要有一个统一的交易闸门。

第五阶段:逐步恢复交易

对账确认后,也可以采用渐进式恢复:

  • 先允许撤单和风险降低操作;
  • 再允许小规模开仓;
  • 观察前几笔报单是否正常收到回报;
  • 检查私有流、查询结果和本地状态是否继续一致;
  • 最后恢复正常策略仓位。

对于高频报单、套利和组合策略,这个过程尤其重要。组合策略不能只看单腿恢复情况,必须确认所有腿的订单状态都已同步,否则容易出现一条腿成交、另一条腿丢回报的裸露敞口。

断线演练要覆盖真正的夜盘故障

很多团队做过“重启程序测试”,却没有做过真正的断线恢复测试。两者不是一回事。程序重启通常发生在状态可控的时刻,而夜盘断线可能发生在订单已经发出、成交正在回报、行情快速变化的瞬间。

一次有价值的演练,至少应该覆盖:

  • 发送订单后立即断网;
  • 订单部分成交时断网;
  • 撤单请求发出后断网;
  • 断线期间发生多笔成交;
  • 重连后私有流选择不同订阅模式;
  • 认证失败后恢复;
  • 登录成功但查询超时;
  • 本地OrderRef已经接近边界;
  • API重复收到连接回调;
  • 非交易时段主动释放并重新启动;
  • 进程在对账过程中异常退出。

演练的验收标准也不能只写“程序没有崩溃”。至少要核对:

  • 柜台实际成交数量与本地成交数量一致;
  • 本地持仓方向和数量一致;
  • 可用资金、冻结资金和持仓占用一致;
  • 未完成订单状态一致;
  • 所有断线期间的异常都能在日志中定位;
  • 恢复前没有偷偷发出新单;
  • 恢复后OrderRef没有重复;
  • 报警能够准确区分网络故障、认证失败和对账差异。

每周手动拔一次网线当然简单,但还不够。更重要的是把断线窗口放在有实际订单的场景中测试,并保存柜台结果与本地结果进行比对。否则你只能证明“连接可以重新建立”,却没有证明“账户状态可以恢复”。

最容易被忽略的几个实现细节

回调线程不能承担重型业务

CTP回调线程里不要直接执行耗时的数据库事务、复杂策略计算或长时间阻塞的网络请求。回调线程被阻塞后,心跳和后续回报可能无法及时处理,应用层看到的“网络断线”实际上是自己把回调线程卡住了。

更好的方式是回调只做轻量工作:

  • 复制必要字段;
  • 写入线程安全队列;
  • 更新最小连接状态;
  • 尽快返回。

认证、查询、对账和状态重建交给独立的恢复线程或事件循环执行。

数据库写入要考虑重复回报

断线重连后,私有流和查询结果可能带来重复信息。数据库表不能假设每一条回调只会到达一次。成交表、订单表和资金快照表都应该设计稳定的去重逻辑,写入操作具备幂等性。

如果一条成交已经通过实时回调写入,之后查询又返回同一条成交,系统应更新或忽略,而不是再增加一笔成交。重复成交会直接导致持仓数量翻倍,随后所有风控判断都会偏离真实账户。

时间必须区分本地时间和交易日

夜盘交易跨越自然日。凌晨发生的成交,业务上可能仍属于前一交易日。日志时间、本地数据库时间、柜台交易日和结算日期不能混为一谈。

对账时要使用柜台返回的交易日和业务字段,不要只按照服务器自然日筛选数据。否则凌晨断线后,程序可能查询不到属于前一交易日的夜盘订单,误以为柜台没有成交。

恢复期间要保留原始日志

事故复盘时,最有价值的不是一句“重连成功”,而是完整的事件顺序:

  • 什么时候最后收到行情;
  • 什么时候最后收到私有流;
  • 什么时候发生断线;
  • 断线前最后一个请求是什么;
  • 什么时候重新连接;
  • 认证响应是什么;
  • 登录返回了哪些会话字段;
  • 查询何时发出;
  • 哪些成交通过流收到,哪些通过查询补回;
  • 什么时候解除交易闸门。

日志需要带上账户、交易日、策略、请求编号和事件类型。没有这些上下文,几天后很难从几万行普通日志里还原一场夜盘事故。

结语:连接恢复只是起点,状态一致才算真正恢复

夜盘断网不是“会不会发生”的问题,而是“什么时候发生”的问题。CTP底层的自动重连可以帮你重新建立网络连接,却不能替你完成看穿式认证,不能替你更新新的SessionID,不能替你修正OrderRef,也不能替你判断断线期间账户到底成交了什么。

可靠的交易接口断线重连,不应该以“回调了OnFrontConnected”作为成功标准,而应该以几个更实际的结果来判断:

  • 认证和登录已经成功;
  • 新会话参数已经更新;
  • OrderRef不会重复;
  • 断线期间的订单和成交已经通过私有流或主动查询补齐;
  • 本地持仓、资金和柜台状态一致;
  • 风控确认后才重新放开策略。

如果系统没有做到这些,那么所谓自动重连只是把网络线路接回来了,交易状态仍然可能停留在断线前。此时行情继续流入,策略继续运行,账户却已经脱离真实世界。

把连接、认证、查询、对账和恢复交易写成一套明确的状态机,再用真实订单做断线演练。这样到了凌晨三点,系统面对的就不再是一场无法解释的事故,而是一条有日志、有边界、能够回滚和复核的恢复流程。

Related reading: 极速柜台真的比普通CTP快吗:量化交易柜台的选择与延迟实测 and 量化交易的区别与判断标准.

常见问题

CTP的OnFrontConnected回调触发后,可以直接开始下单吗?
不可以。该回调仅代表网络连接已建立,系统还需完成客户端认证、终端信息上报、用户登录、会话同步及断线期间的订单成交查询,确认状态一致后方可恢复交易。
为什么断线重连后需要重新查询订单和成交?
断线期间发生的成交或报单状态更新可能无法通过私有流实时接收,必须通过主动查询进行事实校验,以补全本地缺失的记录并修正持仓与资金状态。
如何避免断线重连后的OrderRef报单引用冲突?
应使用线程安全的递增分配器,并在登录成功后读取柜台返回的MaxOrderRef,将本地最大值与柜台返回值比较,取较大值作为新的起始引用,确保报单引用在会话间不回退且不重复。
在非交易时段,应该如何管理CTP API的连接?
不建议让API无限重试,应由调度模块根据交易日历主动停止策略、取消请求、调用Release释放资源,并在下一个交易节开始前重新创建实例并初始化。
为什么说行情连接和交易连接不能混用?
行情前置和交易前置拥有独立的连接状态,交易登录失败时行情可能依然正常接收。若风控模块仅依赖行情心跳,会误判系统处于可交易状态,导致严重的交易风险。