第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 数据校验规则

我个人习惯做三层校验:

  1. 格式校验:字段类型、范围是否正确
  2. 逻辑校验:买卖价差是否合理,价格是否在正常波动范围内
  3. 历史校验:跟过去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分钟内只告警一次
  • 要有手动开关:所有自动处理都要能手动干预
  • 保留现场:异常发生时,保存所有上下文信息

我曾经遇到过一个情况:系统自动重连成功了,但订单状态没同步,结果重复下了单。从那以后,我要求所有重连后必须做一次全量订单同步。

异常处理做得好不好,直接决定了你的系统能活多久。别指望不出问题,要指望出了问题能快速恢复。这才是做市系统的生存之道。


交易系统化学习资料 微信Strategy888888