第27章 异常处理体系:网络异常、交易所故障、数据异常、资金异常
做市系统跑在真实市场上,说白了就是跟各种意外打交道。我见过太多系统上线第一天就被干趴下的案例——不是策略不行,是异常处理没做好。今天咱们就把这块硬骨头啃下来。
27.1 异常处理的整体架构
异常处理不是零散地写几个try-catch就完事了。我个人习惯把它分成四个层次:
- 检测层:发现异常,判断类型
- 隔离层:不让异常扩散
- 恢复层:自动或手动恢复
- 记录层:留痕,方便复盘
你想想看,如果网络断了,你的订单还在往外发,那会是什么后果?所以隔离比恢复更重要。
核心原则:宁可漏单,不要错单。漏单可以补,错单可能直接爆仓。
27.2 网络异常处理
网络问题是最常见的。我在项目中遇到过,某次交易所的API网关升级,我们的连接全部断开了,但本地还显示正常——这就是典型的"假连接"问题。
27.2.1 心跳检测
不要相信TCP的长连接。我建议每500ms发一次ping,如果连续3次没收到pong,就判定网络异常。
class NetworkMonitor:
def __init__(self):
self.last_pong_time = time.time()
self.ping_interval = 0.5 # 500ms
self.timeout_count = 0
self.max_timeout = 3
def check_connection(self):
now = time.time()
if now - self.last_pong_time > self.ping_interval * self.max_timeout:
self.trigger_alarm("网络连接异常")
self.start_reconnect()
27.2.2 重连策略
重连不是简单地循环尝试。我见过有人每秒重连一次,结果把交易所的网关打挂了。正确的做法是:
- 第一次重连:等待1秒
- 第二次重连:等待2秒
- 第三次重连:等待4秒
- ...指数退避,最大30秒
小技巧:重连时带上递增的序列号,方便交易所那边排查问题。我曾经靠这个帮交易所定位了一个网关bug。
27.3 交易所故障处理
交易所出故障,说白了就是"别人家的系统崩了"。但你不能跟着崩。
27.3.1 故障类型识别
| 故障类型 | 表现 | 处理方式 |
|---|---|---|
| 行情中断 | 价格不更新 | 使用本地缓存价格,暂停做市 |
| 交易暂停 | 下单全部失败 | 立即停止所有策略 |
| 成交异常 | 订单状态不更新 | 启动订单同步流程 |
| 资金异常 | 余额显示错误 | 冻结交易,人工介入 |
27.3.2 熔断机制
我建议设置三级熔断:
class CircuitBreaker:
def __init__(self):
self.error_count = 0
self.thresholds = {
'level1': 5, # 警告,降低仓位
'level2': 10, # 暂停部分策略
'level3': 20 # 完全停止交易
}
def on_error(self, error_type):
self.error_count += 1
if self.error_count >= self.thresholds['level3']:
self.emergency_stop()
self.notify_admin("紧急熔断!")
注意:熔断后不要自动恢复。必须人工确认问题已解决,再手动恢复交易。自动恢复可能造成二次伤害。
27.4 数据异常处理
数据异常是最隐蔽的。行情数据错了一个小数点,你的策略可能就全错了。
27.4.1 数据校验规则
我个人习惯做三层校验:
- 格式校验:字段类型、范围是否正确
- 逻辑校验:买卖价差是否合理,价格是否在正常波动范围内
- 历史校验:跟过去N笔数据对比,看是否有突变
def validate_tick_data(tick):
# 格式校验
if tick.price <= 0 or tick.volume <= 0:
return False
# 逻辑校验
if tick.ask_price <= tick.bid_price:
return False
# 历史校验
price_change = abs(tick.price - last_price) / last_price
if price_change > 0.1: # 超过10%的波动
return False
return True
27.4.2 数据降级策略
当数据异常时,不能直接停掉整个系统。我建议:
- 行情数据异常:使用备用的数据源
- 深度数据异常:缩小报价范围
- 成交数据异常:暂停撤单操作
27.5 资金异常处理
资金异常是红线。我曾经因为一个资金计算bug,导致系统多下了10倍的手数——还好被风控拦住了。
27.5.1 资金监控指标
| 指标 | 正常范围 | 告警阈值 |
|---|---|---|
| 可用余额 | > 初始资金80% | < 初始资金50% |
| 持仓市值 | < 总资产70% | > 总资产85% |
| 当日盈亏 | < 总资产5% | > 总资产10% |
| 资金变动频率 | < 100次/分钟 | > 500次/分钟 |
27.5.2 资金对账机制
我建议每5分钟做一次资金对账:
def reconcile_funds():
# 本地记录的资金
local_balance = get_local_balance()
# 交易所查询的资金
exchange_balance = query_exchange_balance()
# 差值计算
diff = abs(local_balance - exchange_balance)
if diff > THRESHOLD:
# 记录异常快照
save_snapshot()
# 暂停交易
pause_trading()
# 发送告警
send_alert(f"资金对账异常,差值:{diff}")
关键点:资金对账要区分"在途资金"和"可用资金"。订单已提交但未成交的资金,不能算作可用资金。
27.6 异常处理的最佳实践
嗯,这里要注意几个容易踩的坑:
- 不要吞异常:空catch块是最大的敌人
- 不要重复告警:同一异常5分钟内只告警一次
- 要有手动开关:所有自动处理都要能手动干预
- 保留现场:异常发生时,保存所有上下文信息
我曾经遇到过一个情况:系统自动重连成功了,但订单状态没同步,结果重复下了单。从那以后,我要求所有重连后必须做一次全量订单同步。
异常处理做得好不好,直接决定了你的系统能活多久。别指望不出问题,要指望出了问题能快速恢复。这才是做市系统的生存之道。
无相订单流研究社 微信Lucian808555