策略希望把持仓从 60 增加到 100,于是买入 40 单位。成交 10 后,持仓变成 70,原单还有 30 单位可能继续成交。若策略再次输出目标 100,系统只按“100 − 70”补买 30,原单与新单全部成交后,持仓就会达到 130。
这是一个现金现货账户的教学场景。错误出在新增决策漏掉了在途订单。紧接着,撤单可能超时,原单可能继续成交,回报可能重复到达,进程也可能在写日志之后退出。交易系统需要持续回答:目标是否仍有效、订单是否仍可能成交、资金占用了多少、外部结果能否确认。
此前的低延迟交易系统系列讨论了计算、内存、网络和执行路径,量化投研平台系列讨论了数据、实验、回测与策略交付。本篇沿着这张买单建立系统全貌:先划定模块责任,再核对数量与资金,最后说明三模式、运行架构和故障恢复。
交易系统的建设目标,是把策略意图转成受约束的交易,并在正常执行与故障之后,都能解释订单、成交与资产之间的关系。
一、先确定交易系统的责任边界
1.1 交易意图与账户事实
策略可以输出目标仓位、目标报价、增量订单或多腿意图。目标 100 表示最终持仓达到 100;买入 100 表示增加 100。接口必须固定数量语义、账户、品种、目标版本、有效期和替换规则,不能只靠“方向+数量”表达所有意图。
投研平台交付策略、模型、参数与数据依赖;交易系统在市场和账户约束下解释意图;场所、柜台及结算机构产生受理、成交、拒绝与结算事实。研究结果、本地状态和外部事实,需要各自明确交接条件。
1.2 模块按状态所有权划分
表1 状态所有权决定模块边界

EMS(Execution Management System,执行管理系统)管理计划的拆分、剩余量、时限与各腿进度;OMS(Order Management System,订单管理系统)管理每张订单的身份、状态与撤改单关联。一次调仓可以拆出多张子单。计划结束、订单终结和账务核清,是三个不同的完成条件。
模块划分首先确定谁有权改变哪份状态。同一账户的准入、预占、订单受理和成交入账,需要遵守共同的更新协议。
表中的主写责任不要求各自部署成服务,也不允许各模块独立维护相互覆盖的“可用余额”。状态所有权的核心在于单写者可以减少并发冲突,但崩溃原子性、日志耐久性和跨实例发送隔离仍要单独保证。策略侧读到可用余额只是一份视图;正式准入必须在承担预占责任的域内重验。
多资产支持也会改变账户模型。数量单位、合约乘数、最小变动价位、可卖量、保证金、费用与结算规则,应保留在业务契约中。
二、一笔交易的数据与控制流

上图展示了账户域先确认 Journal 提交,再发布状态与允许后续发送的流程。受理、拒绝及执行进度返回 EMS;账户视图返回策略。查询和分析消费提交后的事实。图中表示逻辑责任,不限定进程划分。
计算与计划域输出带版本、有效期和归属的子单;账户域共同建立预占、订单身份与待发送责任。网关将外部消息规范化,账户域处理订单进度与成交入账,再向 EMS 反馈,驱动计划继续执行。拒绝反馈同样需要明确归属,避免计划永久等待。
Journal 是本地提交协议的一部分,不能与异步查询投影混为一谈。本文采用先可靠提交、后发布状态的设计;快照和日志用于恢复,查询与分析只读取带水位的已提交事实。控制面则管理配置、预热、停止新增和接管代次。
2.1 行情的可得时间与对象身份
行情接入需要完成解析、品种映射、源序号检查和时间记录,再更新盘口与特征。市场事件发生时间、系统接收时间、数据对策略可用的时间各有含义:历史数据被归入某个窗口,不代表当时的策略已经能读取它。预热不足、源序号缺口和数据陈旧,应影响策略资格。
意图向下传递时,对象身份也必须保留。下表区分运行隔离、目标版本、计划归属与经济事件身份。
表2 六类身份各自约束不同的顺序与责任

同一个重试键只有在请求含义相同时才表示重复。键相同而数量、价格或账户变化,应报告身份冲突。成交号的唯一性是否跨交易日、会话或账户,必须按适配协议确定。
2.2 目标差分与在途数量
回到开头。在同一目标归属、没有其他订单和成交更正的条件下,计划可以把持仓与仍可能成交的订单合在一起考虑。已受理但尚未获得场所确认的子单,也计入在途责任。
计划差分:目标 −(已持仓 + 买单在途余量 − 卖单在途余量)。
受理前差分为 100 − 60 = 40;成交 10 后为 100 −(70 + 30)= 0。ACK 迟到、撤单超时或连接断开,都不能作为删除在途余量的依据。
这段差分只回答还需安排多少净交易量。风险检查则要考虑各订单分别成交的可能性,不能把买卖在途简单净掉。若买卖单同时在场,持仓可能落在“已持仓 − 卖单余量”到“已持仓 + 买单余量”之间。多个线程也不能读取同一旧视图后各自补单:目标版本校验、差分计算与子单受理要衔接。新目标替换旧计划时,旧计划未结订单仍须保留归属;多个策略共享账户时,还需明确各自分配与账户总目标的关系。
2.3 订单受理的本地提交点
准入检查权限、目标有效性、风险与资源。通过后,预占、订单身份、幂等回执和待发送责任需要共同建立可恢复的提交边界。否则会出现资金已冻结却找不到订单,或订单已发出却没有恢复记录。
Journal 是记录交易状态变更、供恢复重放使用的日志。本文采用“先可靠提交,再发布并发送”的设计。成功需要同时满足:回执报告已提交,提交序号符合预期,日志没有被标记为损坏,且记录数与有效载荷字节水位吻合。matches 用来核对后三项中的日志状态与水位。
成功后,预先构造的候选状态整体发布;记录容器已预留容量,记录移动构造不抛异常。这样可避免提交成功后又因发布阶段扩容失败而留下半份状态。若成功条件不成立,不能统一解释为“没写进去”。下面保留实际实现中最关键的失败分流。
代码1 C++:区分确定未提交与提交未知(成功分支之后的核心节选)
outcome.commit_sequence = 0;
outcome.journal_status = receipt.status;
if (!halted_ && receipt.commit_sequence == 0 &&
matches(after, count, payload_bytes_) &&
(receipt.status == JournalStatus::Invalid ||
receipt.status == JournalStatus::CapacityExceeded)) {
halted_ = true;
outcome.status = OwnerStatus::JournalRejected;
return outcome;
}
halted_ = true;
outcome.status = OwnerStatus::CommitUnknown;
return outcome;
第一条分支要求日志仍与提交前的水位一致、回执明确拒绝且此前未进入停止状态,才能判定未提交。fact 区分外部已发生事实与新增预占请求:新增请求可返回拒绝;已发生成交若无法提交,必须停止继续推进并保留补读或恢复责任。
其余情况进入 CommitUnknown。例如追加日志时抛出异常,并不能证明异常发生在写入之前;停止标记会阻止把这种情况降格为普通拒绝。恢复时先核清完整提交边界,再决定是否重做。
这里的“可靠提交”由 Journal 的存储契约与实现提供;内存水位本身不能证明掉电可恢复。代码展示本地账户事务的分流,不包含完整的网关或待发送队列。候选状态准备也需要评估成本:全量重放会随历史增长,高事件率场景可采用增量准备、批提交和快照,但必须保留同样的失败语义。
本地提交保留了发送责任,场所是否受理仍需单独确认。接纳子单、交给网关、写入网络、场所受理和最终成交,不能合并成一个“成功”。
三、订单与账户如何闭合
3.1 余额、预占与费用的口径
继续开头的现金现货买单。账户初始现金为 10,000.00 元,持仓 60 单位;买入 40 单位,限价每单位 10.00 元,整张订单的正手续费预算上限为 4.00 元。数量取整数,金额精确到分,不考虑融资、税费、返佣、结算冻结和其他订单。
在这个教学模型中,现金余额包含冻结资金,可用现金等于余额减预占。订单尚未终结时,预占为“剩余数量 × 限价 + 尚未消耗的手续费预算”。手续费预算是整单上限,不假设它随余量等比例下降。
第一笔成交 10 单位,价格 9.80 元,费用 1.00 元。现金减少 99.00 元;未成交 30 单位,剩余费用预算 3.00 元,预占为 303.00 元。成交价优于限价的差额可以释放,未成交部分的责任仍保留。
随后请求撤单,但结果尚未知;此时又收到 5 单位成交,价格 9.90 元,费用 0.50 元。下面按事件顺序逐笔核对。
表3 部分成交与撤单交错的小账(金额单位:元)

撤单请求没有改变现金与持仓,也没有立即释放预占。释放需要确认剩余委托已撤销,并核清此前的成交与费用。
第二笔成交后,剩余 25 单位仍可能成交,费用预算还有 2.50 元,预占因此为 252.50 元。若只收到“已撤”状态,成交明细和费用却尚未补齐,应保留相应未决项,不能把差异直接清零。
以整数“分”复算:总支出为 10 × 980 + 100 + 5 × 990 + 50 = 14,900 分;现金余额为 985,100 分;预占为 25 × 1,000 + 250 = 25,250 分;可用现金为 959,850 分,即 9,598.50 元。
小账没有计算收益。真实账户还需定义结算资金、保证金、可卖量、借贷与费用币种;整数运算仍须检查尺度、溢出与舍入。

最后的释放以委托余量、成交与费用均已核清为前提;迟到回报对应的成交发生在取消生效之前。
3.2 订单进度与成交入账
订单累计成交量表示执行进度;独立成交明细表示新增经济事件。账户不能每次收到累计量就再次入账。例如累计报告按 5、3、5 到达,若先覆盖旧值,再用“本次累计减上次累计,负数取零”记账,就可能把实际累计 5 记成 7。旧观察不能回退已确认进度;但遇到真实成交更正,也不能简单取最大值掩盖它。
继续使用这张订单:原量 40,累计成交 15,余下 25 已确认撤销。此时“原量减累计成交”仍是 25,“仍可执行的活动余量”却为 0。下面契约中的 remaining 表示前一种数量;是否仍有成交义务,还要结合订单状态和未决回报判断,不能拿它直接替代计划中的在途余量。
代码2 C++:在固定数量契约下检查普通进度(核心分支节选)
const auto original = intent_.quantity();
const auto cumulative = event.cumulative_quantity();
const auto remaining = event.remaining_quantity();
if (cumulative.scale() != original.scale() ||
remaining.scale() != original.scale() ||
cumulative.units() < 0 ||
cumulative.units() > original.units() ||
remaining.units() != original.units() - cumulative.units()) {
return ApplyResult::QuantityMismatch;
}
const auto previous_cumulative = previous
? previous->cumulative_quantity().units() : 0;
if (cumulative.units() < previous_cumulative) {
return ApplyResult::QuantityRegression;
}
代码先约束累计量,再用减法核对余量,避免相加溢出;最后拒绝普通进度回退。此前的账户、身份与源序号检查,以及后续状态转换检查未列出。适配层需转换场所字段的语义;成交更正和撤销成交则使用独立事件契约,不能靠取最大累计量处理。
成交入账还需要把业务身份与传输观察分开。假设成交 E17 买入 10 单位、价格 9.80 元、费用 1.00 元,在 10:00:00.100 首次收到,10:00:00.350 又收到一次。两次接收时间应留在传输观察中;按场所身份范围确认经济内容相同后,复用首次建立的规范成交事件。账本因此只新增一次现金减少 99.00 元、持仓增加 10 的变化。
若第二次 E17 的价格变成 9.90 元,且没有更正标识或关联关系,应报告身份冲突并核验;若协议明确它是更正,则生成关联原成交的新事件。不能为了去重而抹去经济内容差异。
代码3 C++:命中成交身份后,区分重复与内容冲突(核心分支节选)
for (const auto &old : fills_) {
if (old.header().event_id == fill.header().event_id ||
old.header().source_sequence ==
fill.header().source_sequence ||
old.fill_id() == fill.fill_id() ||
old.venue_execution_id() == fill.venue_execution_id()) {
return same_fill(old, fill)
? AccountResult::Duplicate
: AccountResult::IdentityConflict;
}
}
该分支运行在已绑定的账户、运行范围与来源中。same_fill 比较事件头和成交内容,要求上游提供稳定的规范事件。直接把第二次网络接收时间写进事件头再送入,会被判为冲突;片段本身没有实现前述适配过程。
这里逐条扫描保留的成交,单次查找成本为 O(n)。它适合说明身份判断;扩大事件量时,还需设计有界索引、去重保留周期及恢复方式。清理记录之后,“本地查不到”不能直接推断从未入账。
去重记录与资产变化必须共同提交。先改余额、后记成交号,崩溃后可能重复入账;顺序相反则可能漏记资产。外部账户快照用于定位差异,在本地仍有在途事件时直接覆盖余额,也可能与随后到达的成交明细重复计算。
订单事实与操作结果同样要分开:撤单被拒绝只说明撤单操作失败,原订单仍可能在场并继续成交。OMS 应同时保留原单状态和未决操作。
四、三模式与低中高频
4.1 共用语义,明确环境差异
Backtest、Paper 和 Live 的复用重点,是相同业务事件遵守相同的订单、账户与风险规则。数据源、时钟、执行结果来源和发送资格则需要显式装配。模式选择不应只是一个藏在网络函数里的开关。
表4 三种模式的装配与验证边界

回测需固定数据、配置、代码、随机源与事件排序规则。同一时间戳下,撮合、计时器和策略回调仍有先后关系。成交模型也受数据粒度限制:分钟线无法恢复真实逐笔队列位置。
Paper 检验实时接入、调度、限频、反馈与停止流程,成交仍来自模型。触价即成交或固定延迟成交,只验证各自假设下的闭环。Live 则引入真实权限、排队、外部未知结果、费用修订与结算责任。
三模式一致性应以规范事件为比较对象:相同的受理、成交、拒绝与更正输入,是否得到相同的业务结果。真实市场并不需要重现某次回测轨迹。虚拟账户与真实账户的状态、凭证和发送通道必须隔离,恢复时也要检查模式绑定。
4.2 低中高频改变调度与资源预算
频率并不能代替业务模型。低频调仓也可能需要快速撤单,高频做市也不能省略资金预占与未知订单核验。运行架构应根据事件到达速度、可接受的数据年龄、执行时限和故障预算选择。
表5 低、中、高频的运行重点

延迟预算需要分段。行情可得、策略计算、计划生成、账户准入、排队和网关发送应有明确测点;进入场所后的网络与撮合等待则是另一组边界。一个组件的 p99 不能代表整条链路。队列等待时间与服务时间也要分别测量,否则“函数变快”可能只是把压力转移到了队列。
有界队列满载时,需要按业务对象处理:新增子单可以明确拒绝;行情丢失需要检测源序号缺口并重新同步;已发生成交需要可恢复接收或补读,不能静默丢弃。容量应覆盖突发到达与暂停服务的积压,并同时观测队列年龄、高水位、满队列次数和事件完整性。无法保证事实完整性时,应停止新增并进入核验。
语言与部署围绕这些约束选择。C++ 可承担原生关键路径和资源管理,Python 可用于研究、编排、分析及符合时限预算的策略。是否跨进程取决于故障隔离、数据规模、通信成本与状态所有权;慢查询不应阻塞关键交易线程。
五、未知结果与交易恢复
5.1 本地提交与外部受理的边界
发送返回超时,只说明本地没有在期限内确认结果。场所可能没有收到,也可能已受理甚至已成交。此时换一个新订单号直接重发,会创造两张独立订单同时存在的可能性。即使复用原身份,是否具备幂等效果也要由具体协议保证,不能由本地约定代替场所行为。

A / B / C 标出三个故障切点;A 与 B 的实际经历不同,但恢复者可能只有相同的本地证据。图示为设计契约,不代表代码 1 已实现完整发送链路。
表6 同一链路上的故障切点与恢复责任

5.2 恢复如何重新取得交易资格
恢复先重建身份、订单、预占、已入账成交、执行进度和处理水位。快照需要绑定完整提交边界,日志从该边界之后续放。不同来源的序号各有顺序域,不能因数值相同就视作全局进度。
随后核验外部订单与逐笔成交,解释缺失记录、数量差异和费用未决。一次查询“未找到”也可能受查询范围或数据可见性影响;只有按场所协议取得足以确认订单不存在或终结的证据,才能消除对应责任。仍无法确认时,保留身份与占用,限制相关新增,继续补读或升级人工处置。重启不能替代这一步。
新实例接管还需要隔离旧发送者。故障恢复中的 epoch 表示当前有资格发送的运行代次,必须在实际发送边界校验;旧进程仍活着时也应被拒绝。本机锁文件无法约束另一台机器。旧资格失效、新资格生效、未决责任核验和数据资格恢复,共同决定能否继续交易;仅连接恢复或登录成功不够。
停止流程也要有分层完成条件:停止生成目标和新子单,处置挂单,继续接收回报,核清剩余责任。系统可以停止主动承担新风险,同时继续履行已有订单义务。日志与监控也应有容量和故障语义,避免无限队列最终耗尽内存。
能够重新启动进程,只说明程序恢复了运行。能说明订单去向、资产变化和未决责任,并重新取得受控发送资格,才说明交易状态具备继续运行的条件。
六、系统承诺如何验证
可以从单账户、单品种、限价单、部分成交与撤单,检验一套交易系统的基本关系。先写出可检查的不变量:同一范围的经济事件只新增一次账务变化;总预占不超过对应规则下的可分配资源;计划进度不遗失未决子单;来源与运行范围错配的事件不得改状态。除了正常成交,还要验证重复、乱序、未知结果、停止和重启。
表7 从架构到验证的最小问题集

向多策略、多账户、多资产与多市场扩展时,要重新检查原假设。跨场所套利的各腿可能部分成交或结果未知,补偿本身也需要资金、价格边界和权限。两个场所不能视作一个原子撮合器。
以数量已归一、没有其他订单的两腿计划为例:买卖各 10 单位,买腿确认成交 10,卖腿确认成交 6,余量 4 的发送或成交结果未知。已确认净敞口为 4;未知卖腿全部成交时,净敞口可回到 0。此时另卖 4,旧卖腿随后全部成交便会形成 −4。补偿决策要共同考虑确认敞口、未知腿与补偿单的未决责任。补偿的价格边界、时限、容量、资金及失败升级路径,属于 EMS 与账户准入的共同契约。
多个账户还需要各自的原子性边界与额度授权。一个账户预占成功不代表另一个账户可用;跨币种保证金和组合风险,需要独立协调。排入同一队列不能替代业务授权与结算关系。
目标与在途一致,订单与操作分开,成交与账本对应,本地提交与外部结果分别确认;恢复后仍能解释未决责任,才有继续交易的依据。
下篇预告:《一笔交易在系统里经过哪些对象和模块》
交易系统 #量化交易系统 #订单管理 #资金预占 #在途订单 #成交去重 #撤单 #量化系统开发 #交易系统架构 #量化研究