一笔成交之后,账户资金是如何计算的"

admin 2026-10-22 11:47:04 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

一笔成交之后,账户资金是如何计算的

原创

交易系统研究员
交易系统研究员

CppGuide

2026年10月21日 11:52
上海

去阅读

账户初始资金为 1,000 元,提交一笔限价单后,可用余额降至 898.95 元。随后,委托部分成交,账户余额降至 999.92 元,可用余额则略微回升至 899.07 元。

这些指标为什么会出现不同的变化?手续费从哪一项余额中扣除?撤单后,应恢复多少可用余额?

理解这些问题,需要区分两个方面:账户资金的实际增减,以及现有资金中已被交易占用的部分。 手续费影响前者,保证金主要影响后者。成交结算需要同时更新这两部分,才能得到正确的可用余额。

下面用一笔完整的交易说明这个过程:提交限价买单,成交其中 4 张,再撤销剩余 6 张。最后补充一次平仓,核对整个交易过程的资金变化。

一、区分账户中的几类余额

交易界面上经常同时显示账户余额、可用余额和保证金。它们描述的是同一账户的不同方面,不能简单相加。

名称 含义 主要影响因素
账户余额 已入账的资金余额,不含未实现盈亏 入金、出金、已实现盈亏、实际收取的手续费等
未实现盈亏 尚未平仓的持仓按当前标记价估值产生的盈亏 标记价、持仓数量和开仓成本变化
保证金余额 计入未实现盈亏后的账户权益 账户余额或未实现盈亏变化
委托保证金 尚未成交的委托所占用的资金 下单、成交、撤单或改单
仓位保证金 已有持仓所占用的资金 持仓、杠杆、价格及相关风险参数变化
可用余额 按当前规则可用于支持新交易的资金余额 上述各项变化,以及出金冻结等

标记价是系统用于持仓估值和风险计算的参考价格,实际成交价则是每笔交易达成的价格。两者可能不同。

本系统按结算货币分别计算账户,核心关系如下:

保证金余额 = 账户余额 + 未实现盈亏

可用余额 = 保证金余额 − 委托保证金 − 仓位保证金 − 出金冻结

假设一笔挂单占用了 100 元,这部分资金仍在账户中,但暂时不能用于其他交易。因此,下单可能使可用余额下降,而账户余额保持不变。成交时实际收取手续费,才会减少账户余额。

“冻结”在这里表示限制资金用途。它不一定对应一笔实际划转;本系统会登记未成交委托,再据此计算委托保证金。

二、为这笔交易设定明确的参数

本例使用一个以货币结算、价值随标的价格线性变化的演示合约。每张合约对应固定的标的数量,名义价值按“价格 × 每张对应数量 × 张数”计算。金额统一以“元”表示,标的数量以“个标的单位”表示。

项目 本例设定
初始账户余额 1,000 元
初始状态 无持仓、无挂单、无出金冻结
每张合约对应的标的数量 0.01 个标的单位
委托 限价买入 10 张,价格为 10,000 元/标的单位
杠杆 10 倍,整个过程保持不变
挂单方费率 0.02%,即 0.0002
吃单方费率 0.05%,即 0.0005
成交过程 买单先进入订单簿,随后以每个标的单位 10,000 元的价格成交 4 张,剩余 6 张被撤销
标记价 全程保持在 10,000 元/标的单位

上述合约与参数均为教学设定,不对应某个实际交易品种。例子采用本系统的预留费用与保证金公式;其他系统可能有不同的计算口径。

每张合约的名义价值为:

10,000 元/标的单位 × 0.01 个标的单位 = 100 元

10 张合约的名义价值 = 100 × 10 = 1,000 元

名义价值表示这笔交易对应的规模。下文所说的“敞口”,是指会随合约价格变化而产生盈亏的交易规模。使用 10 倍杠杆时,起始保证金按名义价值的十分之一计算,但还需要预留相关手续费。

为集中观察成交与撤单,假设期间没有资金费收付、资金转入转出、其他委托或风险档位变化。标记价与开仓成交价相同,所以主案例中持仓的未实现盈亏始终为零。

三、下单时为什么预留 101.05 元

委托进入撮合之前,清算服务先检查资金是否充足。检查时必须把这笔新委托纳入计算,因为一旦允许它进入订单簿,它就可能随时成交。

本系统的委托保证金由两部分组成:委托起始保证金,以及预计交易手续费。

先计算起始保证金:

委托起始保证金 = 委托名义价值 ÷ 杠杆
               = 1,000 ÷ 10
               = 100 元

再计算预留费用。本系统对本例这类新增敞口采用以下公式:

预计交易手续费 =(2 × 应计委托价值 + 委托起始保证金)× 吃单方费率
               =(2 × 1,000 + 100)× 0.0005
               = 1.05 元

这里的“应计委托价值”,指经过持仓与买卖方向抵扣后,实际参与保证金计算的委托价值。本例从空仓开始,只有这一笔买单,所以它恰好等于 1,000 元。已有持仓或双向挂单时,需要按系统规则重新计算,不能直接套用所有挂单价值之和。

1.05 元 的预留费用可分为两部分:

  • 预计开仓费用:1,000 × 0.0005 = 0.50 元;
  • 预计平仓费用:(1,000 + 100)× 0.0005 = 0.55 元。

第二项中的保证金加项属于本系统的预留规则。它表示预留额度,并不意味着未来一定收取 0.55 元;执行平仓时仍需根据实际成交重新计算。

因此,下单后的资金占用为:

委托保证金 = 100 + 1.05 = 101.05 元
可用余额   = 1,000 − 101.05 = 898.95 元
账户余额   = 1,000 元,保持不变

本系统预留费用时采用吃单方费率。实际收取的金额则取决于每笔成交的角色:先挂入订单簿等待的一方按挂单方费率计费,主动与已有挂单成交的一方按吃单方费率计费。

本例买单已经进入订单簿,因此接下来成交的 4 张按挂单方费率收费。限价单也可能主动成交,不能仅凭“限价”二字判断手续费率。

四、成交 4 张后,先更新持仓并收取实际手续费

撮合引擎确认这 4 张成交后,会产生包含订单号、成交价、成交量及成交角色的回报。清算服务根据回报更新账户。

本例原来没有持仓,买入成交的 4 张全部形成多头持仓。这里的“多头”表示持有买入方向的合约敞口。

持仓数量 = 4 张
开仓名义价值 = 10,000 × 0.01 × 4 = 400 元
开仓均价 = 10,000 元/标的单位

实际手续费按这次成交的金额与角色计算:

实际成交手续费 = 400 × 0.0002 = 0.08 元
账户余额       = 1,000 − 0.08 = 999.92 元

本次成交仅涉及开仓,没有平仓盈亏,因此账户余额的变化完全来自 0.08 元 手续费。400 元 表示形成的合约敞口,不能将其作为现货购买支出从账户余额中扣除。

实现中,手续费会计入已实现盈亏的增量:

本次已实现盈亏增量 = 本次平仓盈亏 − 本次成交手续费
                   = 0 − 0.08
                   = −0.08 元

所以,“没有平仓”并不意味着已实现盈亏字段一定为零。阅读接口或设计界面时,要确认该字段是否包含手续费。

五、成交部分的占用如何从委托转为持仓

完成手续费计算后,还需更新两类保证金。如果只增加持仓占用,却保留原有的全部委托占用,同一批已成交敞口就会被重复计算。

首先计算剩余委托的资金占用。10 张中已成交 4 张,仍在等待成交的有 6 张,对应名义价值 600 元:

剩余委托起始保证金 = 600 ÷ 10 = 60 元
剩余委托预留费用   =(2 × 600 + 60)× 0.0005 = 0.63 元
剩余委托保证金     = 60 + 0.63 = 60.63 元

委托保证金由 101.05 元 降至 60.63 元,减少了 40.42 元。

随后计算已形成的 4 张持仓的资金占用。其起始保证金为 40 元,预计平仓手续费按本系统公式计算:

持仓起始保证金 = 400 ÷ 10 = 40 元
预计平仓手续费 =(400 + 40)× 0.0005 = 0.22 元
仓位保证金     = 40 + 0.22 = 40.22 元

这里能够直接相加,是因为本例的未实现盈亏、仓位亏损记录和全仓交叉占用均为零。一般情况下,仓位保证金还需要结合这些项目重新计算,不能一概简化为“名义价值除以杠杆”。

据此可以计算成交后的可用余额:

可用余额 = 账户余额 − 委托保证金 − 仓位保证金
         = 999.92 − 60.63 − 40.22
         = 899.07 元

为什么成交后,可用余额较 898.95 增加了 0.12 元?因为委托侧释放的 40.42 元 中,40.22 元 继续作为仓位保证金,0.08 元 用于支付实际手续费,其余 0.12 元 恢复为可用额度:

40.42 = 40.22 + 0.08 + 0.12

这 0.12 元 来自预估与实收的差额。已成交部分原本按吃单方费率预留开仓费用 0.20 元,实际按挂单方费率收取 0.08 元。它不是交易利润。

实现过程中并未发生从“委托账户”到“持仓账户”的实际资金划转。清算服务更新剩余委托、持仓和已实现盈亏,再按统一公式重算占用与可用余额。图中的分配关系是对计算结果的解释。

六、撤销剩余 6 张,只释放剩余委托的占用

用户此时撤单,撤销的是仍未成交的 6 张。已经成交的 4 张继续作为持仓存在。

撤单成功后,剩余委托保证金由 60.63 元 变为零:

账户余额   = 999.92 元
仓位保证金 = 40.22 元
委托保证金 = 0 元
可用余额   = 999.92 − 40.22 = 959.70 元

可用余额增加了 60.63 元,账户余额没有变化。撤单解除的是未成交部分的资金占用;已经支付的 0.08 元 成交手续费也不会随撤单退回。

这个过程有两个重要边界。

第一,提交撤单请求时不能提前释放资金。 撤单请求到达撮合之前,剩余委托仍可能成交。清算应依据成功撤单回报完成撤单处理;如果撤单失败,仅凭用户已提交请求就释放保证金,会使仍在订单簿中的委托失去相应资金约束。

第二,释放量必须以当前剩余量为准。 本例是 6 张,不能按最初的 10 张再释放一次。若撤单途中又成交了 2 张,就需要先正确处理相应成交,再按确认撤销的剩余部分结束委托。

最终委托应显示“部分成交后撤销”:累计成交 4 张、撤销 6 张,仍在挂单的数量为零。账户的持仓数量仍为 4 张。

七、释放仓位保证金需要完成平仓

撤单后,账户还有 4 张多头持仓。若希望结束这部分敞口,需要提交卖出平仓委托,并实际成交。

继续假设以每个标的单位 10,000 元的价格卖出这 4 张,此次主动与对手挂单成交,按吃单方费率 0.05% 收费:

平仓盈亏 =(平仓价 − 开仓价)× 每张标的数量 × 平仓张数
         =(10,000 − 10,000)× 0.01 × 4
         = 0 元

平仓手续费 = 400 × 0.0005 = 0.20 元
最终账户余额 = 999.92 − 0.20 = 999.72 元

持仓归零后,仓位保证金也归零。账户没有其他占用,因此最终可用余额同样为 999.72 元。

原来预留的平仓费用是 0.22 元,实际收取 0.20 元,未使用的预留部分随占用解除而恢复可用。预留金额只是计算可用余额时的约束,不能在实际成交时作为一笔既有扣款再次叠加。

至此,完整过程可以核对为:

初始账户余额 1,000
− 开仓手续费 0.08
− 平仓手续费 0.20
+ 平仓盈亏 0
= 最终账户余额 999.72 元

成交价没有变化,账户资金仍减少了 0.28 元,原因是两次实际成交的手续费。

八、用一张表核对整个过程

下面列出每个事件处理完成后的稳定状态,金额单位均为元。所有阶段的未实现盈亏和出金冻结都是零,因此账户余额也等于保证金余额。

阶段 账户余额 委托保证金 仓位保证金 可用余额
初始状态 1,000.00 0.00 0.00 1,000.00
买入 10 张的委托被接受 1,000.00 101.05 0.00 898.95
成交 4 张,剩余挂单 6 张 999.92 60.63 40.22 899.07
撤销剩余 6 张,保留持仓 4 张 999.92 0.00 40.22 959.70
另行平仓 4 张并全部成交 999.72 0.00 0.00 999.72

读表时可以做两次核对。

首先检查“账户余额”一列:它仅在两次实际成交收费后减少,挂单和撤单本身没有改变账户余额。再逐行检查:在本例的前提下,可用余额 + 委托保证金 + 仓位保证金,始终等于账户余额。

如果这一关系不成立,应先排查遗漏更新或重复计算的项目。若账户余额本身存在差异,则应继续核对手续费、已实现盈亏及资金流水,不能仅通过调整可用余额掩盖差额。

九、实现时需要注意的四个细节

1. 释放委托价值时,要沿用下单时登记的口径

清算侧会按订单号记录剩余数量,以及下单时每张委托对应的价值。成交或撤单时,先从这份记录中扣除对应数量和价值,再重算委托保证金。

为什么需要保留原始口径?假设限价买单按每个标的单位 10,000 元登记,后来以更优价格 9,900 元成交。如果释放委托记录时改用成交价,扣除的就不是当初登记的那部分价值,可能在已经结束的订单上留下残余占用。

成交价用于结算这笔实际交易,原委托价值用于准确减少原有挂单记录。 两者各有用途。一般场景下,应更新挂单状态后重新计算保证金,不应假定每成交一张都能按固定金额释放保证金。

2. 预留费用与实收费用分别计算

本系统对正的预留费用采用远离零进位,对实际成交手续费采用朝零截断,目的是分别控制预留不足和多收费用的问题。

取整时还必须明确计量单位。假设某费用的理论值为 2.4 个最小记账单位,预留时可能进位为 3 个,实收时截断为 2 个。这并不表示将 2.4 元 直接取整为 3 元。

本系统的清算金额使用最小记账单位,再通过定点数运算。本例将一个最小记账单位设为 10⁻⁸ 元。主案例中的金额展示到两位小数,是因为选定参数恰好得到这些精确结果,并非系统只保留两位小数,也不代表实际业务统一采用这一精度。实际实现应保留规定精度,最后再做展示格式化。

3. 可用余额应由基础状态统一计算

本系统保留账户累计量、持仓、挂单记录及必要的风险状态,可用余额由这些数据推导。下单、成交、撤单后都应使用同一套重算逻辑。

如果下单时直接扣减可用余额,成交时使用另一套公式调整,撤单时再按估计值恢复,就容易逐渐产生差额。统一算法还能保证资金回报、持仓列表与下单前置检查采用一致口径。

标记价变化时,即使用户没有新成交,未实现盈亏及相关保证金也可能变化。账户余额是否改变,要看是否发生了实际入账事件;可用余额则需要按完整规则重算。

4. 重复回报和缺失回报都需要处理

同一笔成交不能结算两次,否则手续费、持仓和已实现盈亏都会重复变化。服务需要在业务状态变更之前完成相应去重,恢复过程还要保持确定性。

撤单处理也应避免重复释放。本系统在剩余数量释放完毕后移除逐单记录,再次处理该订单的释放请求时不会继续减量。但这一保护只约束挂单释放,不能代替成交结算本身的去重。

缺失回报同样危险:订单在撮合侧已经撤销,清算侧却仍保留挂单记录,用户就会看到可用余额长期偏低。因此,需要核对撮合订单状态、清算挂单记录和成交流水之间的一致性。

十、如何验证这条资金链路

验证时,应把前面的手工计算变成一组能够反复执行的检查:给定初始状态,按顺序输入事件,每处理一条就核对结果。 只检查最后的 999.72 元还不够,因为中途少冻结一次、随后又少释放一次,也可能碰巧得到正确的最终余额。

本节提供了完整的 C++ 验证示例。它直接调用仓库中的下单、成交和撤单处理器,通过内存消息总线运行,无须启动消息中间件、数据库或网页。预期金额取自前文的手工演算。

1. 先准备一份状态明确的测试账户

创建新的测试环境,使用第二节的参数。每个独立场景都从 1,000 元重新开始;同一场景内部则连续使用同一份账户状态。

准备项 具体设置 为什么需要固定
账户与合约 账户 DEMO,演示合约 DEMO-LINEAR 避免其他账户、合约的状态混入结果
结算标识 UNIT ,仅作为教学示例中的内部标识 合约配置与资金账户必须使用相同标识
初始资金 账户余额 1,000 元,已实现盈亏为零 明确后续手续费和盈亏的累计起点
初始占用 无持仓、无挂单、无出金冻结 使每一项变化都能对应到本次输入
价格与数量 委托价、标记价均为 10,000 元/标的单位;每张对应 0.01 个标的单位 让主案例中的未实现盈亏保持为零
费率与杠杆 挂单方 0.02%,吃单方 0.05%,杠杆 10 倍 得到可事先计算的保证金与费用
其他参数 维持保证金率 0.5%,价格步长 0.5;不设置额外风险档位 满足合约配置要求,并避免引入额外变量

这里容易出错的是单位换算。本例约定 1 元 = 100,000,000 个最小记账单位。因此,初始化 1,000 元时,要写入 100000000000,不能直接向清算金额字段写入 1000。示例用 money(\"1000\") 完成这一转换。

价格、费率和张数使用各自的计量方式:价格写作 Decimal::parse(\"10000\"),费率写作 Decimal::parse(\"0.0002\"),数量写作整数 10。合约乘数则按内核要求设置为 0.01 × 100000000 = 1000000。不能把所有字段都统一放大一亿倍。

还要区分基础字段与展示结果。本文的“账户余额”对应重算结果 AccountDerived.walletBalance;在本实现中,它等于基础账户的 walletBalance 加上累计的 realisedPnl。成交手续费先记入后者。若只读取基础字段,就可能误以为成交后没有扣费。

2. 编译并运行配套示例

在仓库根目录执行以下命令。需要可用的 g++,支持 C++17 和内核使用的 128 位整数。示例只依赖仓库源码与 C++ 标准库。

Windows PowerShell:

New-Item -ItemType Directory -Force -Path tmp/article-settlement-check | Out-Null
g++ -std=c++17 -O0 -pthread -I cpp \"docs/column/示例/settlement_walkthrough.cpp\" cpp/common/decimal.cpp cpp/wire/codec_order.cpp -o tmp/article-settlement-check/settlement_walkthrough.exe
./tmp/article-settlement-check/settlement_walkthrough.exe

Linux 或 WSL:

mkdir -p tmp/article-settlement-check
g++ -std=c++17 -O0 -pthread -I cpp \"docs/column/示例/settlement_walkthrough.cpp\" cpp/common/decimal.cpp cpp/wire/codec_order.cpp -o tmp/article-settlement-check/settlement_walkthrough
./tmp/article-settlement-check/settlement_walkthrough

编译成功后再执行程序。每次启动都会重新创建测试状态,可以反复运行。程序中的 Fixture 负责准备账户、合约和处理器;send() 负责投递事件并等待本批处理完成;check() 负责比较实际状态与预期值。

3. 按事件顺序重现一次完整交易

消息的 offset 可以理解为同一来源分区中记录的顺序编号。示例固定使用一个清算分区,正常事件的位点递增;重复位点只用于验证重放行为。订单号用于关联同一张委托,成交编号用于标识具体成交,它们与消息位点各有用途。

示例按下表执行主流程。每发送一条消息,立即调用 worker.drain(),处理完成后再断言,避免把“事件还没处理”误判为计算错误。

顺序 输入事件 关键字段 处理后重点核对
1 限价买单 位点 1,订单 101,买入 10 张,价格 10,000 订单登记 10 张;委托保证金 101.05 元;可用余额 898.95 元
2 部分成交回报 位点 2,订单 101,成交 T-001,买入 4 张,价格 10,000,挂单方 持仓 4 张,剩余挂单 6 张;手续费 0.08 元;可用余额 899.07 元
3 原样重投上条回报 仍为位点 2,其余内容完全相同 资金、数量不变,不新增对外回报
4 撤单失败回报 位点 3,订单 101,错误码为非零值 6 张挂单及 60.63 元委托占用均保留
5 撤单成功回报 位点 4,订单 101,错误码为零,累计成交 4 张,状态为部分成交后撤销 挂单归零,持仓仍为 4 张,可用余额 959.70 元
6 再次收到同一订单的成功撤单回报 位点 5,仍为订单 101 不重复释放,账户状态不变
7 新的卖出委托 位点 6,订单 102,卖出 4 张,价格 10,000 新订单被接受并登记;尚不能视为已经平仓
8 平仓成交回报 位点 7,订单 102,成交 T-002,卖出 4 张,吃单方,全部成交 实收手续费 0.20 元;持仓与挂单归零;账户余额、可用余额均为 999.72 元

成交回报由示例直接构造,用于验证清算收到该事件后的行为。示例并未运行撮合引擎,也没有验证这些成交是否能由真实订单簿产生。

4. 每个检查点同时核对金额、数量和回报

以“成交 4 张”这个检查点为例,应该同时核对以下内容:

检查层次 预期结果 对应的错误风险
账户资金 账户余额 999.92 元,累计已实现盈亏 −0.08 元 手续费漏扣、重复扣除或费率使用错误
资金占用 委托保证金 60.63 元,仓位保证金 40.22 元,可用余额 899.07 元 成交后未释放委托占用,或仓位占用计算错误
持仓 多头 4 张,开仓成本 400 元 成交数量或开平处理错误
逐单记录 订单 101 剩余 6 张,每张登记价值 100 元 剩余数量错误,或沿用了错误的释放价格
挂单汇总 买入挂单 6 张,登记价值 600 元 逐单记录更新了,汇总记录却未同步
资金回报 回报中的可用余额换算后也是 899.07 元 内部状态正确,对外发送的数据却已过时

示例中的核心断言如下,参数依次为阶段、账户余额、委托保证金、仓位保证金、可用余额、持仓张数、剩余买单张数和持仓成本:

f.check(\"partial-fill\", \"999.92\", \"60.63\", \"40.22\", \"899.07\", 4, 6, \"400\");

这些预期值是独立列出的常量。check() 会从实际状态中取值逐项比较;金额按完整精度比较,不先截成两位小数,也不使用浮点数的近似相等判断。

另外再检查资金关系:

可用余额 + 委托保证金 + 仓位保证金 + 出金冻结
= 账户余额 + 未实现盈亏

这一关系用于发现项目遗漏,但不能代替逐项断言。例如账户余额与可用余额同时多出 1 元,等式仍然可能成立,所以必须把“每一项数值正确”和“各项关系一致”结合起来检查。

5. 改变一个条件,验证异常和边界

主流程通过后,按下表改变输入。每个场景只引入一个主要变化,便于判断结果为什么不同。前四项已穿插在主流程中检查,后两项使用新的测试环境。

场景 具体操作 应得到的结果
撤单失败 在部分成交后发送非零错误码的撤单回报 挂单仍为 6 张,委托保证金仍为 60.63 元,可用余额仍为 899.07 元
重复撤单 成功撤销订单 101 后,以新位点再次发送订单 101 的撤单成功回报 逐单记录仍不存在;挂单数量、价值均为零;可用余额仍为 959.70 元
相同位点重放 原样重投位点 2 的成交回报 持仓仍为 4 张,累计手续费仍为 0.08 元,输出消息数量不增加
部分成交后撤单的回报 检查撤单成功后产生的订单通知 累计成交量为 4,剩余量为零,状态为部分成交后撤销
更优价格成交 仍以 10,000 委托,但将 4 张成交的价格改为 9,900 剩余挂单价值仍为 600 元;持仓成本为 396 元;实际手续费为 0.0792 元
一笔成交反向开仓 先买入成交 4 张并撤销余单,再以 10,000 卖出成交 6 张 平掉 4 张多头并新开 2 张空头;本笔手续费 0.30 元;最终账户余额 999.62 元,仓位保证金 20.11 元,可用余额 979.51 元

重复撤单测试必须使用已经撤销的那个订单号。改用一个从未存在过的订单号,只能验证“找不到订单时怎么处理”,不能证明重复回报的处理正确。

更优价格场景还会引入未实现盈亏:标记价仍为 10,000,开仓价变为 9,900,所以未实现盈亏为 (10,000 − 9,900) × 0.01 × 4 = 4 元。按本系统公式,仓位保证金为 43.8178 元,可用余额为 899.473 元。撤销余单后,可用余额为 960.103 元。不能继续套用主案例中“未实现盈亏为零”的计算结果。

还要单独验证:成交编号相同,消息位点却不同。 在一份新的测试环境中完成 T-001,再把相同回报作为位点 3 投递。业务上仍是同一笔成交,理想结果应保持持仓 4 张、账户余额 999.92 元。

但当前清算入口按已处理位点拦截重放,成交处理器没有按成交编号再次去重。配套示例在这项探测中会看到持仓变为 8 张、账户余额变为 999.84 元,并输出 LIMIT。这说明相同位点重放受到保护,新位点上的重复成交仍需由上游保证不产生,或补充业务去重机制。不能用前一项测试的通过结果替代这一项验收。

6. 看懂运行结果,并定位第一处差异

正常检查通过时,输出包含以下记录;金额显示时会省略末尾的零:

[PASS] partial-fill balance=999.92 order=60.63 position=40.22 available=899.07 qty=4
[PASS] cancel-succeeded balance=999.92 order=0 position=40.22 available=959.7 qty=4
[PASS] close-filled balance=999.72 order=0 position=0 available=999.72 qty=0
[LIMIT] same trade ID at a new offset: balance=999.84 qty=8

balance、order、position、available 分别表示账户余额、委托保证金、仓位保证金和可用余额,单位均为元;qty 是持仓张数,正数表示多头,负数表示空头。

普通检查失败时,程序输出 FAIL 并以状态码 1 退出;金额断言会列出预期值与实际值。默认运行时,已知的业务去重限制单独报告,不让它阻断其他场景的学习。如果要把“新位点上的重复成交必须被拦截”作为验收条件,可以在运行命令末尾加上参数:

./tmp/article-settlement-check/settlement_walkthrough.exe --require-business-dedup

Linux 或 WSL 使用前面的无扩展名程序,并加上同一参数。当前实现会因这项要求未满足而以状态码 2 退出。因此,默认运行成功只说明基础场景检查通过,不能据此宣布业务去重验收通过。

排查时,从第一条失败记录开始,而不是只观察最后余额:

首先出现的差异 优先检查
下单就被拒绝 初始资金是否正确换算,合约和账户的结算标识是否一致,价格和合约配置是否合法
手续费是 0.20 元而非 0.08 元 成交角色是否误设为吃单方,账户是否使用了其他费率
成交后委托占用仍为 101.05 元 回报中的订单号是否匹配,逐单剩余量是否减少,挂单汇总是否同步更新
撤单后可用余额没有恢复 错误码是否为零,撤单成功回报是否真正进入处理器
内部可用余额正确,通知值却不同 是否在状态更新前构造了回报,通知是否取自同一次账户重算
金额相差一亿倍 是否混淆了展示单位、清算金额单位和 Decimal 自身的内部表示

7. 将清算验证延伸到完整链路

上述程序验证了清算处理器及其输出。接入真实撮合、消息传递和查询接口后,还需要使用同一套参数做一次联调:先让测试账户的买入 10 张委托进入订单簿,再由对手账户提交卖出 4 张的委托,等待真实成交回报;随后撤销剩余 6 张,最后提交卖出 4 张并安排对手委托使其成交。

每个阶段都保存订单号、成交编号、消息位点、处理后的账户状态和对外回报。只有收到对应成交或撤单成功回报后,才核对第八节中的稳定状态。查询结果可能晚于事件处理,应按关联编号和已处理位点确认查询已反映该事件,避免只固定等待若干秒后就下结论。

还可以在测试环境中暂缓投递一次撤单成功回报:撮合侧已经撤销订单,清算侧仍保留 6 张挂单,此时可用余额会停留在 899.07 元,与正确结果相差 60.63 元。这个实验用于验证是否能通过订单状态与清算挂单记录的对账发现遗漏。

补齐事件时必须保持原有处理顺序。如果后续事件尚未处理,可以继续按顺序投递;如果清算位点已经越过缺失事件,则应按恢复流程,从缺失事件之前的状态重放有序日志。简单重投旧位点会被入口忽略,任意改成新位点又可能破坏业务顺序。

阅读代码与定位问题时,可以按以下顺序对照:

要理解的步骤 对应实现
下单风控与挂单登记 OrderInsertHandler
保证金与费用公式 algorithm.h
开平量拆分、手续费与持仓成本计算 settleTrade
成交后的状态更新与回报 TradeHandler
按订单减少挂单数量和价值 releaseOpenOrder
成功撤单后的处理 CancelRtnHandler
账户与持仓派生量重算 account_recalc.h
按已处理位点拦截重放 AbstractInfoHandler
本文金额与事件序列的可运行验证 settlement_walkthrough.cpp
仓库已有的挂单释放测试 clear_frozen_release_test.cpp

分析余额变化时,应先确定对应事件,再分别核对账户资金的实际收支、未成交委托的占用和已有持仓的占用。三部分各自有据可查,才能对可用余额进行准确解释、复算和验证。

如果你对交易系统的开发与设计感兴趣,可以阅读小方在知识星球的专栏:

使用 C++20 从零构建一个完整的低延迟交易系统

如果你想求职交易系统相关的岗位,可以阅读小方在知识星球的专栏:

交易系统开发岗位求职与面试指南

如果你对交易系统开发感兴趣或者想这方面的工作,可以看看交易系统开发训练营,这个训练营中小方将带你从零开发一套完整的交易系统,包括上文中提到的全部技术:

AI实战训练营:用AI从零开发一套高可用交易系统 即将开营

推荐阅读:

C/C++ 网络编程实战训练营一期

C/C++ 网络编程二期录像

C/C++ 项目实战训练营

用 AI 带你从零开发大型商业远程控制软件

AI实战训练营:用AI从零开发一套高可用交易系统

大型Linux内核网络专栏

小方说服务器开发:一个实实在在帮你提高开发能力的优质圈子!


一笔成交之后,账户资金是如何计算的" 网络安全文章

一笔成交之后,账户资金是如何计算的"

本文通过一笔完整交易示例,详细说明账户资金计算逻辑,区分账户余额、可用余额、保证金等概念,强调手续费影响账户余额,保证金占用影响可用余额,撤单释放委托保证金,平仓释放仓位保证金,并给出实现细节与核对方法。
【新图】美国太平洋舰队兵力部署" 网络安全文章

【新图】美国太平洋舰队兵力部署"

文档介绍了美国太平洋舰队的兵力部署情况,包括舰队规模约200艘舰船、近1200架飞机及13万人员,责任区覆盖近半个地球表面。同时推广了知远所制作的长2.05米宽1.5米的兵力部署地图产品,提供了购买方式和联系方式。内容主要为产品推广,军事信息较为基础。
活动预告|“敏捷作战运用(ACE)战保协同”兵棋推演" 网络安全文章

活动预告|“敏捷作战运用(ACE)战保协同”兵棋推演"

该文档为兵棋推演活动预告,由鼎邦联合防务、知远所和航天二院208所联合举办,定于2026年11月28日在北京举行,依托美国空军王鱼兵棋系统,聚焦敏捷作战运用概念,围绕机场开设、架次生成、战保联动三大模块进行实战化推演,旨在验证ACE作战模式的效能与局限,活动采用多组并行对抗复盘形式,报名截止11月24日,收取资料费3500元每人。
美军F-22首次从座舱操控FQ-44无人机事件解析" 网络安全文章

美军F-22首次从座舱操控FQ-44无人机事件解析"

美国空军F-22A战斗机飞行员首次在飞行中直接控制FQ-44A无人机,标志着有/无人协同作战迈出关键一步。FQ-44A由安杜里尔公司生产,具备低成本、低可探测性,可执行空战、对面打击及ISR任务。未来有望形成有人机统筹、无人机前出的协同模式,但座舱交互、链路抗干扰及飞行员认知负荷仍是挑战。
评论:0   参与:  0