我先把这次折腾的真实经过捋一遍。你没看错,标题就是最终的结果:我用 112 个 AI 同时跑股票交易,第一个交易日结束,交易链路确实从头到尾都通着,但从券商那边拉回来的对账单和我的系统内部资金流水一比对,账差了 6 万多块。这个事情说实话挺打脸的,但恰恰是这半天的对账过程,逼着我把整个系统的资金流、成交回报、手续费计提、分红送股这些环节重新过了一遍,踩出不少教科书上不会写的坑。今天把这些东西整理出来,做量化交易系统或者做自动化交易脚本的朋友,尤其是打算上多策略、多智能体的,这篇可以直接当避坑手册用。
1. 项目到底在干什么:112 个 AI 的架构设计与分工逻辑
先说清楚这 112 个 AI 是什么,不然很容易被理解成“112 个模型喊单”“112 个程序同时按一个按钮”。我这个项目的设计目标,是在一套统一的底层交易系统上,同时运行 112 个独立决策单元。每个 AI 被当成一个有自己观察窗口、自己策略参数、自己风控逻辑的“交易员”,而不是一个简单的信号发生器。
1.1 不是 112 个模型,是 112 个独立决策单元
这 112 个 AI,本质上是 112 个策略容器,每个容器里运行一个决策算法。有些是趋势跟踪模型,专门做均线突破;有些是统计套利模型,在跨品种价差上做回归;还有一些是基于量价特征做短周期预测的机器学习模型。每个 AI 只配了一笔独立资金,资金的规模和该策略的风险等级绑定,比如激进型策略给 5 万,稳健型给 20 万,对冲型给 50 万。这样设计的核心原因是:我要的不是“一个智能决策”,而是“一个决策委员会”在分散下注。如果只有一个模型,它观点一旦错误,整体就是满仓踏空;但 112 个模型同时运转,即使有 30 个观点偏了,剩余 82 个依然能维持系统的整体生命力。
在这个结构下,每个 AI 都有自己的“大脑”,互不通气、互不共享持仓信息。一套中央调度器负责给每个 AI 下发资金额度、接收 AI 的买卖信号、检查信号是否超出该 AI 的风控边界,如果合规就推送到统一的交易执行模块。执行模块再通过券商接口发送真实委托。
1.2 核心链路:从信号到订单的流转路径
系统运行的核心链路分成四层,每一层都有独立的日志记录和状态快照:
- 决策层:112 个 AI 各自计算目标持仓和买卖方向;
- 风控层:中央调度器校验每个信号的资金占用、持仓集中度、单票止损线、当日开仓次数限制;
- 执行层:通过统一的订单管理模块把委托发往券商柜台,并记录委托编号、委托价格、委托量;
- 结算层:回传成交回报,更新内部持仓和资金余额,生成当日的资金流水。
这四层各管各的,理论上每层都有日志,出了问题能逐层排查。当时我就是这么安慰自己的,结果真出了问题才发现,光有日志不够,日志和日志之间如果口径不一致,排查起来更崩溃。
1.3 为什么选 112 个而不是 10 个
其实一开始也没想用 112 这个数。最早是 12 个策略,跑了一段时间,收益曲线还行,但波动太大,单一策略回撤经常超过 15%。后来我逐步把策略参数做了更细粒度的拆分——同一个趋势模型,用 10 天均线、20 天均线、30 天均线的各自算一个独立 AI;同一个套利模型,在 5 分钟级别、15 分钟级别、30 分钟级别上各算一个独立 AI。再叠加不同品种池的过滤条件,数量自然就涨上来了。
最后变成 112 个,其实是很自然的数字:12 个基础策略乘以 7 个观察周期乘以 2 个市场方向(多头和空头),再剔除掉几个明显重叠的组合,差不多就是 112。这样做的好处是,任何一个 AI 的失败不会伤筋动骨,任何单一市场风格的变化也不会让系统全部失效。坏处也很明显:资金量稍微一大,下单频率一高,对系统的资金核算能力和对账能力就是一次极限测试。
2. 第一个交易日:从启动信号到收盘清算的全流程复盘
先把时间线拉出来。早上 9 点 15 分集合竞价开始,系统准时连上行情源和交易接口,112 个 AI 全部进入待命状态。我本来预期第一天能顺利完成几十笔交易就不错了,毕竟 AI 需要积累数据,策略也不会一开盘就疯狂下单。结果现实给了我一巴掌:从 9 点 30 分正式开盘到 10 点 45 分,系统累计发起了 886 次委托,最终成交 641 笔,涉及 78 个不同标的。这个频率确实超出了我的预判。
2.1 开盘前的状态检查与预演准备
开盘前我做了一件很重要但设计上有漏洞的事——跑了一次模拟环境的预演。预演时行情数据用的是前一天的收盘数据和一段随机延时模拟的盘中走势,所有订单都发到模拟柜台,成交回报按“等量全成交”处理。这个预演结果当然是很漂亮的:641 笔委托,641 笔全部成交,资金流水正正好好,持仓余额完全对得上。
问题出在“等量全成交”这个假设上。真实市场里,委托可能部分成交、可能延迟成交、也可能被拒单,而我的内部资金流水却按照“全部成交”的预期先扣款了。模拟环境没有训练“失败路径”,这是很多自研系统第一个交易日出幺蛾子的根源。
2.2 盘中运行:从信号生成到执行链路的实际表现
10 点到 11 点那一个小时,系统整体跑得还算顺。委托推送平均延迟在 180 毫秒左右,从 AI 生成信号到券商返回委托确认,这个数值对于非高频场景完全够用。成交回报的推送这里开始出现第一个坑:券商返回的成交回报并不是严格按时间顺序到达的。有两笔委托,先发的反而后成交回报,后发的却先成交了。
我的内部流水表是按“成交回报到达顺序”更新持仓的,没按“委托时间排序后再更新”。这就导致后来对账时,某些股票的持仓数量在内部账本里对不上,因为两笔不同方向的委托先后顺序被颠倒,持仓扣减和增加的时间戳错位了。
2.3 收盘清算:问题是在哪个环节开始悄悄积累的
真正让我意识到账不平,是收盘后跑清算脚本。系统内部资金总账算出来的余额是 93 万多,而券商资金账户里实际显示余额只有 86 万多,中间差了 6 万 8 千多。第一反应是券商数据拉错了,反复刷新了几次,最终确认券商那边数据没问题。
然后我开始按科目排查。先查手续费,系统里计提的手续费总和是 3126 元,券商实际扣了 4183 元,差 1057 元。再查持仓市值,系统内部按收盘价计算的持仓市值和券商对账单里的市值差了 2 万多。再查资金流水,系统里记录了 641 笔成交,但对账单里却有 655 笔成交记录,多出来的 14 笔成交我在系统里根本没有对应流水。
那一刻我是真的有点慌。641 和 655 这个数字差说明,我的订单管理模块漏掉了部分成交回报。后来拉明细一看,漏掉的是 14 笔部分成交的订单——系统下了一笔 200 股的委托,真实成交只有 150 股,另外 50 股一直挂在那边直到收盘都没成交,而内部系统却按照 200 股全成交做了资金扣减和持仓记录。这一下就解释了大部分差额。
3. 账没算平的深度归因:对账机制与资金差异拆解
前面说的是现象,这里重点说根因。我花了一整天,把内部账户和券商账户之间的差异拆成四大类。每一类对应一个系统设计上的缺陷,也对应一个可以优化的点。理解了这些,你以后做任何交易系统,对账这块都能少走弯路。
3.1 第一类差异:手续费与滑点的集体偏差
手续费差异是最表面的一类,也是最容易被忽略的一类。很多人在做系统回测时,手续费一般按万 2.5 固定费率算,佣金不足 5 元按 5 元收。这个规则在单笔模拟中没问题,但在批量交易中很容易产生累积误差。
比如我的系统里某笔委托是 1500 元,手续费按万 2.5 计提了 0.375 元,但券商实际按最低 5 元收取。单笔差异 4.6 元,看起来微不足道,但 112 个 AI 一天下来产生 641 笔成交,其中 236 笔金额较低、触发了最低佣金规则。236 乘以平均差异,光这一项就差了 1000 多块。
滑点的差异更隐蔽。系统内部记录成交价时,用的是委托成交回报里的成交均价,但券商对账单里的成交均价包含了过户费、规费等杂项分摊。也就是说,同样一笔成交,我记录的“净价”和券商记录的“全价”之间存在口径差。一支股票成交了几十笔,算上分摊的规费,看起来没多少,但乘以 78 个标的、641 笔成交,差异就累积到近 2000 元。
3.2 第二类差异:现金管理粒度与挂单拒绝
这里要说到另一个让我头疼的问题:现金余额的并发更新。系统里有 112 个 AI 同时运行,每个 AI 在产生买卖信号后都要检查自己的可用资金,如果可用资金够,就扣减资金然后下单。
问题出在“检查资金”和“扣减资金”这两个动作之间存在时间窗口。两个 AI 几乎同时检查额度,发现各自的可用资金都够,然后同时扣款,结果其中一个扣款时发现余额不足。更拧巴的是,我的订单管理模块在资金不足时会自动拒单,但拒单处理的顺序和委托发送的顺序在日志里是颠倒的。
这就导致资金流水里的冻结金额和实际冻结金额不一致。系统认为 A 订单已经发出去了,资金冻结了;券商那边 A 订单被拒,根本没有占用资金。两边一对照,我这边多算了一笔冻结,账自然对不平。这是典型的并发一致性问题,单线程策略永远不会踩到,一旦上多智能体并行,立刻暴露。
3.3 第三类差异:分红、停牌等非同步事件
第三个差异是最让我意外的:有两只持仓股票在当天有分红到账,但我的系统里完全没有处理分红这件事。
每笔分红是几百到一千多,两只加起来两千多块。因为我做策略时只考虑了价格涨跌和买卖交易,根本没把分红送股纳入资金流水的更新逻辑。券商那边资金账户多了这笔钱,系统内部资金账不知道这笔收入,账户余额自然不一致。
还有一只股票盘中有 20 分钟的临时停牌。停牌期间,我有一笔委托撤单了,但撤单回报延迟了大概 40 秒才回来。这 40 秒内,AI 以为委托还挂着,又针对同一标的下了一笔新的买入指令。系统没有做“同一标的、同一方向的活跃委托去重”,导致了两笔重复指令。停牌恢复后,两笔指令先后成交,持仓超出预期仓位。虽然不是资金差异的大头,但给我的持仓管理带来了混乱。
3.4 对账口径的先天缺陷:内部账本缺少“在途”科目
最后一个根因,是内部账本的三级科目体系设计有问题。最明显的体现在资金流水的科目上:我只设置了“可用资金”和“冻结资金”两个科目,没有设置“在途资金”科目。而真实交易中,一笔委托发出后、尚未成交期间的资金状态,既不是完全可用,也不是完全冻结,它处于一个中间状态。
特别是在“部分成交”场景下,一笔 200 股的委托成交了 150 股,剩余 50 股的状态是“待成交”,但我的核算逻辑已经把这 50 股对应的资金判定为冻结。到了日终,这部分未成交的挂单被撤掉,冻结应该释放回可用,但我的释放逻辑只在“全部未成交”的状态下才触发,对于“部分未成交”没有做释放处理。加上前面说的部分成交漏单,累计未释放的资金就滚成了一个不小的数字。
4. 现场排查实录与修复思路
发现问题到解决问题,我大概花了六个小时。这里把排查过程和修复方案记录下来。核心思路是:先修复系统让账重新对平,再提升系统让它不再产生同类问题。修复过程本身也有不少可复用的经验。
4.1 “按成交价计算,但成交价只保留三位小数”的硬编码教训
第一处坑:我的持仓成本计算源码里,写了一段“round(成交价, 3)”的处理。这本来是为了显示美观,但问题是它被用在资金流水里了。真实成交均价是 7.2485,我的系统硬编码保留三位小数变成 7.248,然后拿这个价格去计算持仓市值和资金占用。单笔误差不到一分钱,但架不住成交次数多。641 笔成交里,接近 500 笔都存在这种四舍五入的误差,累积起来就是几百块的差异。
修复思路很直接:资金核算一律使用原始成交价,只在展示层做四舍五入。我加了一个内部价格类,保留原始浮点值,仅在输出报告时格式化。这个改动听起来基础,但它暴露出一个原则:底层账务逻辑永远不要跟展示逻辑混在一起。
4.2 冻结资金与成交回报的时序竞态处理
第二个修复点,是处理冻结资金和成交回报的时序竞态。前面提到,两个 AI 并发扣减资金时会相互踩踏。我的修复方案是给资金账户加了一把分布式锁,对每个策略的资金池独立加锁,减少锁冲突。同时,在资金扣减前先做“实际可用资金重算”——读当前冻结总额、累计未成交金额、确认在途金额,综合计算真实可用额度,再决定能否通过。
关键点在于重算时机的选择:重算和扣减必须在一个事务里完成,中间不能插入其他操作。我直接用内存队列做串行化,把所有资金变动请求投递到同一个队列,由单线程处理。这个方式对于 112 个 AI 的单机系统完全够用,延迟增加也不明显。
4.3 现金科目不健全:重建一套四级资金台账
第三个修复,是把二级资金科目升级成四级。原来只有“可用资金-总账”和“冻结资金-总账”,现在拆成:
- 可用资金-各策略
- 冻结资金-各委托单
- 在途资金-已成交待结算
- 待释放资金-已撤单待返还
资金台账不再允许直接在总账上加减,而是必须通过明细流水表逐笔记账,再由定时任务汇总生成总账。这样做能保证每个资金变动都有据可查,对账时不需要再倒推。
重建台账之后,我又加了一个日终试算平衡的步骤:当天最后一笔成交回报和资金流水全部入账后,系统自动执行一次“内部账户权益 = 可用资金 + 冻结资金 + 在途资金 + 持仓市值”的校验,如果不平,立即输出差异明细,而不是等第二天人工发现。
5. 这一波测试跑下来,我最想提醒的几件事
这轮跑通虽然翻了车,但翻得值。几个经验教训,比赚到钱更珍贵。如果你也在做类似的多 AI 交易系统,这几条可以直接拿来用。
5.1 先对账,再谈收益
第一,任何交易系统上线前,先把对账模块做扎实。很多人一上来就研究策略收益率、夏普比率,结果跑了一段时间才发现资金账目居然对不上。对的策略加错的账务,就是灾难。建议上线第一周,每天收盘后做一次全量对账:逐笔核对委托、成交、资金流水、手续费、持仓市值五个维度,确保系统内部账本和券商对账单完全一致。
实际操作中的一个小技巧:不要把对账做成一个单独的定时脚本,而是做成一个独立服务,专门监听所有的成交回报和资金变动事件,每次变动都实时更新对账状态。等到收盘再批处理,差异来源早就淹没在几千笔流水里了。
5.2 用影子账验证风控边界
第二,风控模块最好有一本影子账,和策略仓位的实际账分开。什么意思?我的风控逻辑里有一条“单票持仓不能超过总资金的 15%”,但这条规则检查的是系统内部持仓,而不是真实持仓。如果内部持仓因为部分成交漏单而虚高,风控就会错误地拦截下一笔本该允许的交易。
影子账的思路是:风控模块不直接依赖策略账本,而是维护一套实时同步的镜像仓位,专门用于规则校验。这样即使策略账本出了问题,风控判断也不受影响。我后来就是这么修的,效果立竿见影。
5.3 从 112 到 12:做减法比做加法重要
第三,对于多 AI 系统,做减法往往比做加法重要。112 个 AI 听起来很厉害,但真正在第一个交易日贡献了有效交易信号的只有 61 个,其中表现稳定且没触发风控预警的,只有 27 个。剩下那些,要么长期空仓,要么频繁产生无效信号,要么在极端行情下集体触发止损。
我的建议是:每跑一个月,就把那些连续 20 个交易日都跑不出有效策略信号的 AI 先下线,把它们的资金额度分给表现好的 AI。长时间不产出有效信号的策略,留着不只是浪费算力,更会让整体资金调度变笨重。我这个项目现在已经从 112 个逐步收缩到 48 个,系统的资金利用效率反而明显提升。
5.4 分场景保存日志,别把宝都押在一个大文件里
第四,日志设计一定得分场景。我这次排查之所以费了六个小时,很大原因是所有的交易日志、风控日志、资金流水日志全都写在同一个大文件里。几十万条日志混在一起,过滤条件稍微写错,就要从头翻一遍。
现在我把日志拆成了四个独立通道:信号日志、委托日志、成交日志、资金流水日志,每个通道单独滚动。排查问题时,先看资金流水日志定位异常单,再回委托日志和成交日志确认原因,效率提升不是一点半点。再加上每个通道都记录同一个 requestId(请求唯一标识),跨通道检索一条完整链路只需几秒。
5.5 后续的大规模重测计划
修完这些问题后,我已经把这个系统挂到了模拟环境里跑第二轮的完整重测,这轮重测特意加了三组压力场景:成交回报乱序到达、部分成交比例从 30% 到 80% 随机分布、瞬时并发下单数量翻倍到 300 笔/秒。只有这三类场景下账仍然保持分毫不差,我才会考虑接真实的小资金跑。如果你也在做类似的事,我强烈建议你在上线前也把这些肮脏场景主动加进去,别指望市场会温柔地对待你的第一版系统。
这次的经验让我更坚信一件小事:在交易系统里,“能不能自动跑通”和“跑完对不对得上”根本是两个世界的问题。前者是功能问题,后者是工程问题。我这次就是吃了“功能跑通了就以为万事大吉”的亏。把这套对账修复的完整记录分享出来,希望做类似项目的朋友能少熬几个夜。