第一章:单机交易系统的诞生——从手工记账到电子化交易
大家好,我是你们的老朋友。
今天咱们聊聊交易系统的起点。说实话,每次我带新人入行,都会先问一个问题:“你觉得最早的交易系统长什么样?”
很多人会想到满屏跳动的K线、高速运转的服务器。其实不是。最早的交易系统,就是一张纸、一支笔,外加一个算盘。嗯,你没听错。
1.1 手工记账时代:交易员的“肌肉记忆”
我入行时,带我的老交易员给我看过他当年的“交易本”。那是一个硬皮笔记本,每一页都画着格子,左边是买单,右边是卖单。价格、数量、时间,全靠手写。
你想想看,那时候的撮合是怎么做的?
- 客户打电话进来:“老王,帮我买1000股工商银行,价格5.20元。”
- 交易员翻开本子,找到工商银行那一页,看看有没有人卖5.20元。
- 有?成交!没有?记下来,等着。
说白了,这就是最原始的订单簿。只不过它是纸质的,而且完全依赖人的记忆和反应速度。
核心概念:订单簿(Order Book)本质上就是一个“价格-数量”的映射表。买单按价格从高到低排列,卖单按价格从低到高排列。这个逻辑,从手工时代到现在的分布式系统,从来没变过。
我曾在一次内部培训中做过实验:让两个实习生用手工方式模拟撮合10笔交易,平均耗时47秒。而同样的交易量,电子系统只需要0.01秒。差距有多大?你自己品。
3.2 电子化的第一步:把纸上的东西搬进内存
到了80年代末90年代初,PC开始普及。有人就想:“为什么不用电脑来记账呢?”
于是,第一代电子化交易系统诞生了。它的核心逻辑极其简单:
- 把订单簿从笔记本搬到内存里——用一个数组或者链表来存。
- 撮合逻辑从“人脑判断”变成“代码循环”——遍历买单和卖单,找到匹配的。
- 成交记录从“手写”变成“打印”或者“存文件”。
我见过一个90年代的老系统,代码是用C语言写的,整个撮合引擎就一个函数,不到200行。但就是这200行代码,撑起了一个营业部每天几千笔的交易量。
一个小技巧:如果你现在去研究那些老系统的源码,会发现它们的核心数据结构就是两个有序链表。一个存买单(降序),一个存卖单(升序)。每次新订单进来,就从链表头开始比较。这个设计到今天依然是所有撮合引擎的基石。
1.3 最原始的撮合引擎:价格优先、时间优先
咱们来拆解一下这个“200行代码”到底干了什么。
第一步:订单进来,先判断是买还是卖。
第二步:如果是买单,就去卖单链表里找。从表头开始,只要卖单价格 ≤ 买单价格,就成交。成交价取卖单价(这叫“被动单优先”)。
第三步:如果卖单数量不够,剩下的买单继续挂在链表里。如果卖单数量多了,剩下的卖单继续等。
第四步:如果是卖单,反过来操作。
你看,就这么简单。但这里面有个关键点:时间优先。什么意思?
假设有两个买单,价格都是5.20元。谁先来,谁先成交。在链表里,就是谁排在前面谁先被遍历到。
注意:我曾经在维护一个老系统时发现过一个bug——新订单插入链表时,如果价格相同,它被插到了尾部。结果导致后到的订单反而先成交。这就是典型的时间优先实现错误。所以,插入位置一定要保证先来后到。
下面我画了一张图,帮你理解这个流程:
1.4 代码实现:一个极简的撮合引擎
光说不练假把式。我写了一个最简版本的撮合引擎,用Python实现。你一看就懂:
class Order:
def __init__(self, order_id, side, price, quantity):
self.order_id = order_id
self.side = side # 'buy' or 'sell'
self.price = price
self.quantity = quantity
self.next = None # 链表指针
class OrderBook:
def __init__(self):
self.buy_head = None # 买单链表头(降序)
self.sell_head = None # 卖单链表头(升序)
def add_order(self, order):
if order.side == 'buy':
self._insert_buy(order)
self._match()
else:
self._insert_sell(order)
self._match()
def _insert_buy(self, order):
# 按价格降序插入,同价按时间顺序
if not self.buy_head or order.price > self.buy_head.price:
order.next = self.buy_head
self.buy_head = order
else:
curr = self.buy_head
while curr.next and curr.next.price >= order.price:
curr = curr.next
order.next = curr.next
curr.next = order
def _match(self):
# 撮合逻辑:只要买单头 >= 卖单头,就成交
while self.buy_head and self.sell_head and \
self.buy_head.price >= self.sell_head.price:
buy = self.buy_head
sell = self.sell_head
trade_qty = min(buy.quantity, sell.quantity)
trade_price = sell.price # 被动单价格
print(f"成交:{trade_qty}股 @ {trade_price}元")
buy.quantity -= trade_qty
sell.quantity -= trade_qty
if buy.quantity == 0:
self.buy_head = buy.next
if sell.quantity == 0:
self.sell_head = sell.next
个人经验:这个代码虽然简单,但我在实际项目中见过它的“升级版”——把链表换成了跳表(Skip List),把O(n)的插入变成了O(log n)。但核心逻辑一模一样。所以,别小看这几十行代码,它是所有交易系统的灵魂。
1.5 单机系统的天花板
单机系统能撑多久?我告诉你一个真实数据。
| 指标 | 手工记账 | 单机电子化 |
|---|---|---|
| 每秒处理订单数 | 0.1笔 | 100-500笔 |
| 单笔撮合延迟 | 5-30秒 | 1-10毫秒 |
| 支持交易品种 | ≤10个 | ≤100个 |
| 故障恢复时间 | 立即(人脑记忆) | 分钟级(重启) |
| 最大用户数 | ≤10人 | ≤100人 |
看到没?单机系统虽然比手工强了上千倍,但它有硬伤:
- 单点故障——机器一挂,全完蛋。我经历过一次硬盘损坏,整个营业部停了半天,那叫一个惨。
- 性能瓶颈——CPU、内存就那么多,订单量一上来,CPU直接100%,撮合延迟从毫秒变成秒级。
- 扩展困难——想加一个交易品种?得改代码、重新编译、停机部署。每次升级都像在拆炸弹。
避坑指南:我曾经接手过一个单机系统,它的订单簿是存在文件里的。每次撮合前先读文件,撮合完再写回去。你想想看,这得有多慢?所以,千万别把订单簿持久化到磁盘,内存才是它的家。持久化可以用日志,但订单簿本身必须常驻内存。
1.6 小结:这一章你学到了什么?
咱们回顾一下:
- 最早的交易系统是手工记账,订单簿就是一本笔记本。
- 电子化之后,订单簿搬到了内存里,用链表或者数组实现。
- 撮合引擎的核心逻辑是“价格优先、时间优先”,代码不到200行。
- 单机系统有天花板:性能、可靠性、扩展性都不够。
嗯,这一章就到这里。下一章咱们聊聊:当单机撑不住的时候,人们是怎么想到用“主从复制”来解决问题的。到时候我会分享一个我亲身经历的“宕机两小时”事故,保证让你印象深刻。