第三章 常见内置异常:ValueError、TypeError、KeyError 等实战场景

做市商策略跑在线上,最怕什么?

不是行情暴跌,而是代码突然崩了。

行情暴跌你还能止损,代码崩了那是直接断线。我见过太多新手,一遇到异常就慌,要么 try-except 一把梭,要么干脆不处理。这两种做法,在实盘里都是定时炸弹。

今天咱们就聊聊 Python 里最常见的三个内置异常:ValueErrorTypeErrorKeyError。别看它们基础,用好了能帮你躲掉 80% 的线上事故。

3.1 ValueError:数据格式的「守门员」

ValueError 说白了就是「值不对」。函数接收了正确类型,但值本身不合法。

举个例子,你在做市商策略里经常要解析订单簿数据。交易所返回的价格字段,有时候是字符串,有时候是浮点数。如果你直接拿字符串去做数学运算,Python 不会报 TypeError,但如果你用 int() 去转一个带小数点的字符串,它就炸了。

# 错误示范:直接转换
price_str = "1234.56"
price = int(price_str)  # ValueError: invalid literal for int() with base 10: '1234.56'

我在项目中遇到过类似情况。有一次,某交易所的深度数据里,价格字段偶尔会返回空字符串。我当时没做校验,直接 float(price),结果策略在凌晨三点崩了。嗯,那晚我睡得特别香——被电话吵醒的。

我的习惯:所有从外部接口拿到的数值,都先做一次「安全转换」。

def safe_float(value, default=0.0):
    try:
        return float(value)
    except (ValueError, TypeError):
        return default

你想想看,一个 safe_float 函数,能省掉多少排查时间。

3.2 TypeError:类型不匹配的「警报器」

TypeErrorValueError 更底层。它告诉你:你给的东西,类型不对。

做市商策略里,最常见的场景是混合了 Decimalfloat。比如你从数据库读出来的是 Decimal,从交易所接口拿到的是 float,然后你直接做加法——TypeError 就来了。

# 典型错误
from decimal import Decimal
price_decimal = Decimal("1234.56")
price_float = 1234.56
result = price_decimal + price_float  # TypeError: unsupported operand type(s) for +: 'Decimal' and 'float'

我曾经在写一个价差套利策略时,就踩过这个坑。两个交易所的报价,一个用 Decimal,一个用 float,我直接相减算价差。结果策略跑了三天,价差一直显示为 0——因为 TypeError 被 try-except 吞掉了,程序没崩,但逻辑全错了。

避坑指南:不要盲目吞掉异常。我曾经以为「只要不崩就是对的」,结果亏了钱才发现逻辑根本没执行。

我的建议是:在策略内部统一使用 Decimal 做价格计算,只在展示或传输时转成 float。这样能避免 90% 的类型问题。

3.3 KeyError:字典操作的「地雷」

KeyError 是字典访问时最常见的异常。做市商策略里,你经常要处理嵌套的 JSON 数据,比如订单簿、成交记录、账户信息。一个字段缺失,整个程序就炸了。

# 危险写法
order_book = {"bids": [...], "asks": [...]}
spread = order_book["spread"]  # KeyError: 'spread'

为什么会出现这种情况?交易所的 API 文档说会返回 spread 字段,但实际在某些市场状态下,它可能不返回。你想想看,文档和实现不一致,太常见了。

我个人习惯用 .get() 方法代替直接索引:

spread = order_book.get("spread", 0.0)  # 安全,默认返回 0.0

但注意,.get() 只能解决「字段缺失」的问题。如果字段存在但值为 None,它不会报错,但后续计算可能出问题。所以更稳妥的做法是:

def safe_get(data, key, default=None):
    value = data.get(key, default)
    if value is None:
        return default
    return value

一个小技巧:在解析交易所返回数据时,我会先打印出完整的 JSON 结构,看看哪些字段是「可选」的。然后对可选字段全部用 .get() 加默认值。这样即使交易所改了字段名,策略也不会崩。

3.4 实战:一个完整的异常处理模板

说了这么多,咱们来写一个实战模板。假设你正在写一个市商策略的订单管理模块:

class OrderManager:
    def __init__(self):
        self.orders = {}
    
    def place_order(self, order_data):
        try:
            # 安全解析
            symbol = safe_get(order_data, "symbol", "")
            price = safe_float(safe_get(order_data, "price", 0.0))
            quantity = safe_float(safe_get(order_data, "quantity", 0.0))
            
            # 类型校验
            if not isinstance(price, (float, Decimal)):
                raise TypeError(f"price 类型错误: {type(price)}")
            
            # 值校验
            if price <= 0:
                raise ValueError(f"price 必须大于 0: {price}")
            
            # 核心逻辑
            order_id = self._send_to_exchange(symbol, price, quantity)
            self.orders[order_id] = {
                "symbol": symbol,
                "price": price,
                "quantity": quantity,
                "status": "pending"
            }
            return order_id
            
        except KeyError as e:
            print(f"订单数据缺少关键字段: {e}")
            # 记录日志,但不崩
            return None
        except ValueError as e:
            print(f"订单数据值不合法: {e}")
            return None
        except TypeError as e:
            print(f"订单数据类型错误: {e}")
            return None
        except Exception as e:
            print(f"未知异常: {e}")
            # 这里可以触发告警
            return None

这个模板的核心思想是:精确捕获,分别处理。不要一把 except Exception 全吞掉,那样你永远不知道问题出在哪。

3.5 异常处理的「黄金法则」

最后,分享几条我总结的法则:

  1. 能预判的异常,提前处理。 比如用 .get() 代替 [],用 safe_float 代替 float()
  2. 不能预判的异常,精确捕获。 先捕获具体的异常类型,最后再兜底。
  3. 异常处理里不要做复杂逻辑。 记录日志、返回默认值、触发告警,就够了。别在 except 里再调 API 或写数据库。
  4. 线上环境要记录完整堆栈。logging.exception() 而不是 print(),方便事后排查。

我曾经犯过的错:在 except 块里又调了一次交易所 API 重试,结果重试也抛异常,导致递归调用,最后栈溢出。嗯,那次事故让我记住了:异常处理里,越简单越好。

好了,这一章的内容就到这里。记住,异常处理不是「防崩溃」,而是「优雅降级」。你的策略可以暂停,但不能死掉。下一章咱们聊聊更高级的异常处理技巧——自定义异常和上下文管理器。