4. 金融行业(三)
金融行业这一块,咱们今天聊点硬核的——量化交易平台。说白了,就是用程序代替人脑做决策,机器下单。我这些年接触过不少量化团队,从几个人小作坊到几十人的自营部门,架构上踩过的坑真不少。今天重点拆解三个核心模块:低延迟行情接入、策略回测框架、订单管理系统。
嗯,先看一张整体架构图,心里有个谱。
一、低延迟行情接入:与时间赛跑的第一公里
量化交易里,行情就是生命线。你比别人晚1毫秒看到价格,可能就错过了一笔交易。我个人习惯把行情接入拆成三层:网络层、解码层、分发层。
1.1 网络层:放弃TCP,拥抱UDP组播
为什么?TCP有重传机制,丢包了要等,延迟不可控。交易所的行情基本都是UDP组播,一发多收。我在项目中遇到过一个问题:组播数据流太大,网卡中断处理不过来,导致丢包。后来怎么解决的?
- 网卡调优:开启RSS(接收端缩放),多队列分散中断到不同CPU核心。
- 内核旁路:用DPDK或者Solarflare的OpenOnload,跳过内核协议栈,用户态直接收包。
- 绑定核心:收行情线程固定在一个物理核心上,避免上下文切换。
1.2 解码层:快,还要准
交易所的行情格式五花八门:STEP、Binary、FIX/FAST……解码速度直接影响延迟。我见过有人用Python写解码器,延迟直接飙到几十微秒。说实话,这不行。
正确的做法:
- 用C++或Rust写解码器,内存直接映射,零拷贝。
- 预分配对象池,避免运行时动态分配内存。
- 用SIMD指令加速字段解析,比如一次处理16个字节。
// 简化的行情解码示例(C++风格伪代码)
struct MarketDataPacket {
uint64_t timestamp; // 纳秒时间戳
uint32_t price; // 价格,放大1万倍存储
uint32_t volume; // 成交量
char symbol[8]; // 合约代码
};
// 直接从内存映射区域读取,无拷贝
const MarketDataPacket* packet =
reinterpret_cast<const MarketDataPacket*>(mmap_addr + offset);
1.3 分发层:谁需要谁拿走
解码后的行情要分发给多个策略实例。这里我推荐用无锁环形缓冲区(Lock-Free Ring Buffer)。为什么?因为互斥锁在争抢时,延迟会急剧恶化。
| 分发方式 | 延迟(P50) | 延迟(P99) | 适用场景 |
|---|---|---|---|
| 互斥锁 + 条件变量 | ~500 ns | ~5 μs | 低频策略,非核心 |
| 无锁环形缓冲区 | ~50 ns | ~200 ns | 高频策略,推荐 |
| 共享内存 + 信号量 | ~100 ns | ~1 μs | 跨进程分发 |
二、策略回测框架:用历史数据检验你的想法
回测是量化策略的试金石。但很多人把回测做成了“过拟合大赛”。我见过一个策略,回测年化收益200%,实盘一个月亏了30%。为什么会这样?
说白了,回测框架的设计决定了结果的可靠性。我建议关注三个要点:
2.1 数据精度:用Tick级,别用分钟级
分钟级回测会忽略很多细节:比如开盘瞬间的冲击成本、大单扫货的滑点。我个人习惯用Tick级数据回测,虽然数据量大(一天可能几GB),但结果更真实。
- 数据清洗:剔除异常Tick(比如价格超过涨跌停板)。
- 复权处理:分红、送股、拆合都要精确到Tick时间点。
- 对齐时间戳:不同交易所的时间基准可能不同,需要统一到纳秒级。
2.2 撮合引擎:模拟真实交易所
回测的核心是撮合逻辑。不能简单用“最新价成交”,要考虑盘口深度。我建议实现一个订单簿回测引擎:
// 订单簿回测核心逻辑(伪代码)
class OrderBook {
// 买盘,价格从高到低
std::map<double, double, std::greater<double>> bids;
// 卖盘,价格从低到高
std::map<double, double> asks;
// 限价单撮合
MatchResult matchLimitOrder(Order order) {
if (order.side == BUY) {
// 从最低卖价开始吃
for (auto it = asks.begin(); it != asks.end(); ) {
if (it->first > order.price) break;
double traded = std::min(order.volume, it->second);
// 记录成交
it->second -= traded;
if (it->second == 0) it = asks.erase(it);
else ++it;
order.volume -= traded;
if (order.volume == 0) break;
}
}
// 类似逻辑处理SELL
}
};
2.3 绩效评估:别只看夏普比率
夏普比率是经典指标,但不够。我建议至少看这几个:
| 指标 | 含义 | 我的经验阈值 |
|---|---|---|
| 年化收益率 | 复利年化收益 | > 20% 才值得实盘 |
| 最大回撤 | 从峰值到谷底的最大跌幅 | < 15% 比较安全 |
| 收益回撤比 | 年化收益 / 最大回撤 | > 2.0 算合格 |
| 胜率 | 盈利交易占比 | 40%-60% 正常 |
| 盈亏比 | 平均盈利 / 平均亏损 | > 1.5 较好 |
三、订单管理系统:从决策到成交的最后一公里
订单管理系统(OMS)是策略和交易所之间的桥梁。它不复杂,但必须零差错。因为一个错误的订单可能造成巨大损失。
3.1 订单生命周期管理
一个订单从创建到成交或撤销,状态变化必须严格记录。我习惯用状态机来管理:
// 订单状态枚举
enum OrderStatus {
PENDING_NEW, // 待发送
NEW, // 已发送,未成交
PARTIALLY_FILLED, // 部分成交
FILLED, // 全部成交
CANCELLED, // 已撤销
REJECTED, // 被交易所拒绝
EXPIRED // 过期
};
每个状态变更都要记录时间戳和原因。为什么?因为事后分析时,你需要知道“订单为什么被拒”或者“为什么成交慢了”。
3.2 订单路由:智能选择交易场所
现在很多品种在多个交易所上市。比如股指期货,中金所、新加坡A50都有。订单路由要解决一个问题:去哪下单最划算?
- 价格优先:选最优价格的市场。
- 流动性优先:选盘口深度最好的市场。
- 延迟优先:选网络延迟最低的市场。
- 成本优先:考虑手续费和过户费差异。
3.3 风控:OMS的最后一道防线
OMS里必须内嵌风控模块。我见过一个团队,因为OMS没有做资金校验,策略下了一个超过账户余额10倍的订单,结果被交易所罚款。嗯,这种错误一次都不能犯。
必备的风控检查:
- 资金校验:下单金额不能超过可用资金。
- 持仓校验:卖空数量不能超过持仓(如果是股票)。
- 频率限制:每秒下单次数不能超过阈值(比如100笔/秒)。
- 价格限制:订单价格不能偏离最新价太远(比如±5%)。
- 自成交防护:避免自己的买单和卖单互相成交。
// 风控检查伪代码
bool RiskCheck(const Order& order) {
// 1. 资金校验
if (order.volume * order.price > account.available_balance) {
log("资金不足");
return false;
}
// 2. 频率限制
if (order_counter.increment_and_check() > MAX_ORDER_PER_SEC) {
log("下单频率超限");
return false;
}
// 3. 价格限制
double last_price = market_data.get_last_price(order.symbol);
if (std::abs(order.price - last_price) / last_price > 0.05) {
log("价格偏离过大");
return false;
}
return true;
}
四、总结:架构设计的三个核心原则
聊了这么多,其实就三个原则:
- 低延迟:从行情到订单,每一微秒都要争取。用C++、用DPDK、用无锁结构。
- 高可靠:数据不丢、订单不错、状态可追溯。用状态机、用日志、用风控。
- 可回测:回测越真实,实盘越放心。用Tick数据、用订单簿撮合、用样本外测试。
量化交易平台的设计,说白了就是一场与时间、与风险、与不确定性的博弈。架构师的任务,就是让这场博弈的胜率,尽可能高一点。