Bug 修复报告:币对大小写匹配问题
Bug 修复报告:币对大小写匹配问题
日期:2026-02-20
文件:src/trading/config.py
严重程度:高(功能性 Bug,导致配置项完全失效)
问题概述
config.py 中存在两类大小写处理不一致问题,导致多个配置项在运行时静默失效。
根因分析
Bug 1:is_close_disabled() 永远返回 False(高危)
位置:_parse_close_disabled_symbols() + is_close_disabled()
复现路径:
环境变量: TRADING_CLOSE_DISABLED_SYMBOLS=purr
↓
_parse_close_disabled_symbols() 存储: {"PURR"} ← 强制 .upper()
↓
交易所返回 symbol: "purr/usdc:usdc"
↓
is_close_disabled() 比较: "purr" in {"PURR"} ← 永远 False
↓
结果: 平仓保护失效,系统持续尝试平仓并产生告警
根本原因:_parse_close_disabled_symbols() 用 .upper() 存储配置值,但 is_close_disabled() 直接用交易所原始符号(可能是小写)做精确比较,两侧大小写不一致导致匹配永远失败。
Bug 2:get_strategy_params() 破坏交易所原始数据(中危)
位置:get_strategy_params() 第 124–125 行(修复前)
# 修复前(错误)
alt_asset = symbol.split('/')[0].upper() # 强制大写交易所符号
问题:交易所返回的 symbol 是系统的原始数据,强制转换违反了"交易所数据永不修改"原则。虽然在当前代码路径中因 override keys 也是大写而偶然正确,但属于脆弱实现,在大小写敏感场景下会静默失败。
设计缺陷:两个黑名单字段行为不一致
| 字段 | 存储方式(修复前) | 比较方式(修复前) |
|---|---|---|
symbol_blacklist |
原始大小写(用户输入) | 精确匹配 |
close_disabled_symbols |
强制大写 | 精确匹配 |
两个功能相似的字段行为不一致,增加了使用和维护的认知负担。
修复方案
核心原则:
- 交易所 API 数据:保持原始大小写,永不修改
- 环境变量配置 keys:规范化为大写(匹配 Linux ENV 命名惯例,如
TRADING_STRATEGY_PURR_EMA_SPAN) - 所有字符串比较:统一使用
casefold()大小写不敏感匹配
具体改动
1. 新增 _casefold_get() 辅助函数
def _casefold_get(d: dict, key: str):
"""大小写不敏感字典查找(先精确匹配,再 casefold 匹配)"""
if key in d:
return d[key]
key_cf = key.casefold()
for k, v in d.items():
if k.casefold() == key_cf:
return v
return None
精确匹配优先,保证性能;回退到 casefold 全扫描,保证正确性。
2. get_strategy_params() — 删除对交易所符号的 .upper()
# 修复前
alt_asset = symbol.split('/')[0].upper()
# 修复后
alt_asset = symbol.split('/')[0] # 保持原始大小写
result = _casefold_get(self.symbol_strategy_overrides, alt_asset) # 不敏感查找
3. is_symbol_allowed() — casefold 集合比较
# 修复前
if alt in self.symbol_blacklist: # 精确匹配,大小写敏感
# 修复后
blacklist_lower = {s.casefold() for s in self.symbol_blacklist}
if alt.casefold() in blacklist_lower: # casefold 不敏感匹配
4. is_close_disabled() — casefold 集合比较
# 修复前
if alt in self.close_disabled_symbols: # 精确匹配,大小写敏感
# 修复后
disabled_lower = {s.casefold() for s in self.close_disabled_symbols}
if alt.casefold() in disabled_lower: # casefold 不敏感匹配
5. _parse_close_disabled_symbols() — 删除强制大写
# 修复前
return {s.strip().upper() for s in raw.split(",") if s.strip()}
# 修复后
return {s.strip() for s in raw.split(",") if s.strip()}
# 保持原始大小写,比较由调用方用 casefold 完成
修复后行为对照
| 场景 | 修复前 | 修复后 |
|---|---|---|
TRADING_CLOSE_DISABLED_SYMBOLS=purr,交易所返回 "purr" |
❌ 匹配失败,平仓保护失效 | ✅ 正确禁止平仓 |
TRADING_CLOSE_DISABLED_SYMBOLS=PURR,交易所返回 "purr" |
❌ 匹配失败 | ✅ 正确禁止平仓 |
TRADING_SYMBOL_BLACKLIST=PURR,交易所返回 "purr" |
❌ 匹配失败,黑名单失效 | ✅ 正确拦截 |
TRADING_STRATEGY_OVERRIDE_SYMBOLS=PURR,查询 "purr/usdc:usdc" |
偶然正确(脆弱) | ✅ 明确正确 |
未改动部分
_load_symbol_strategy_overrides() 和 _load_pair_strategy_overrides() 中的 .upper() 保留。
原因:这两个函数处理的是环境变量配置 keys,用于构建 Linux 大小写敏感的环境变量前缀(如 TRADING_STRATEGY_PURR_EMA_SPAN)。规范化为大写是正确且必要的。查找侧已由 _casefold_get() 负责不敏感匹配,两者配合无需改动加载逻辑。
数据流设计总结
环境变量输入 (用户配置)
├── override keys → .upper() 规范化 → casefold 查找
├── symbol_blacklist → 原始大小写保留 → casefold 比较
└── close_disabled → 原始大小写保留 → casefold 比较
交易所 API 数据 (symbol)
└── 永不转换,原样传入各方法 → casefold 比较
验证
运行内联验证脚本,覆盖以下场景:
✅ _casefold_get:精确匹配 / casefold 回退 / 无匹配返回 None
✅ get_strategy_params:purr/PURR 均命中 override,ETH 回退全局默认
✅ is_symbol_allowed:小写/大写/配对级黑名单均正确拦截,非黑名单放行
✅ is_close_disabled:小写/大写/配对级均正确禁止,非配置符号放行