订单跟踪BUG22

订单跟踪系统 Bug 分析报告

分析时间: 2026-02-21
涉及文件: executor.py, position_manager.py, websocket_order_manager.py, realtime_kline_service_base.py


Bug 1: limit_openresult.leg_b 已被置为 None 后丢失错误信息

严重度: 中高

文件: executor.py:946-951

result.leg_b = None  # ← 先置为 None
result.leg_a.error_message = (
    f"Leg B 市价单失败且回滚失败,Leg A 仓位已转为单腿跟踪: "
    f"{result.leg_b_error}"           # ← 访问不存在的属性 leg_b_error
    if hasattr(result, 'leg_b_error') # ← PairOrderResult 没有 leg_b_error 字段
    else f"Leg B 市价单失败且回滚失败,Leg A 仓位已转为单腿跟踪"
)

问题分析:

  • PairOrderResult 没有 leg_b_error 属性,hasattr 永远返回 False
  • 虽然当前不会崩溃(走 else 分支),但原始的 result.leg_b.error_messageleg_b = None 之后已经丢失
  • 回滚前应该先保存 leg_b 的错误信息再置 None,这是一个逻辑遗漏

影响: 不会崩溃但会丢失关键的错误诊断信息,排查问题时无法得知 Leg B 失败的具体原因。

修复建议:

leg_b_err = result.leg_b.error_message if result.leg_b else "未知错误"
result.leg_b = None
result.leg_a.error_message = f"Leg B 市价单失败且回滚失败,Leg A 仓位已转为单腿跟踪: {leg_b_err}"

Bug 2: 缓冲区回放的线程安全问题(竞态条件)

严重度: 高

文件: realtime_kline_service_base.py:618-626

if self._order_msg_buffer:         # 无锁检查
    mgr = self._get_ws_order_manager()
    if mgr is not None:
        while self._order_msg_buffer:
            buffered = self._order_msg_buffer.popleft()  # deque.popleft 虽然原子但...

问题分析:

self._order_msg_buffer 是一个普通 deque(第 193 行),没有任何锁保护on_message 可能从多线程调用(WebSocket 回调线程),同时:

  • 线程 A 正在回放缓冲区(popleft
  • 线程 B 正在追加新消息(append,第 637 行)
  • 线程 C 也在回放

虽然 CPython 的 GIL 让 deque.popleft()append() 各自原子,但 while self._order_msg_buffer 判空 + popleft() 组合不是原子的。如果两个线程同时进入 while 循环,可能导致 popleftIndexError(空 deque 上调用)。

影响: 会导致消息处理线程崩溃,订单状态更新丢失。

修复建议:

# 方案 1: 使用 try/except 保护
if self._order_msg_buffer:
    mgr = self._get_ws_order_manager()
    if mgr is not None:
        while True:
            try:
                buffered = self._order_msg_buffer.popleft()
            except IndexError:
                break
            try:
                mgr.handle_message(buffered)
            except Exception as e:
                self.logger.error(f"回放缓冲订单消息失败: {e}", exc_info=True)

# 方案 2: 使用 threading.Lock 保护

Bug 3: _close_ops 类型标注与实际 tuple 长度不匹配

严重度: 低

文件: position_manager.py:1134 vs 1163-1167

收集阶段 append 了 8 个元素的 tuple:

_close_ops.append((
    pos.position_id, now, symbol, pos.base_symbol,
    pos.direction, pos.alt_entry_price, pos.alt_size,
    ghost_pnl,  # 第8个
))

但类型声明只有 7 个类型

_close_ops: list[tuple[str, datetime, str, str, str, float, float]] = []

消费阶段用 *rest 解包然后取 rest[-1]

for pos_id, close_time, *rest in _close_ops:
    ghost_pnl = rest[-1] if rest else 0.0

影响: 当前运行正确(rest[-1] 确实是 ghost_pnl),但类型标注与实际数据不符,是维护隐患。

修复建议: 更新类型标注为 8 元素 tuple,或使用 @dataclass 替代 tuple。


Bug 4: open_time 使用本地时间而非 UTC

严重度: 中

文件: position_manager.py:154

open_time=datetime.now().astimezone(),

问题分析:

datetime.now().astimezone() 获取的是本地时区时间。如果系统部署在不同时区(如 Docker 容器默认 UTC vs 开发机 UTC+8),会导致:

  • 持仓时长计算错误
  • 止损超时判断偏差
  • DB 中时间不一致

同样问题出现在 close_time, _adopt_residual_base_leg 等多处。

影响: 在单机部署下不影响,但跨时区/容器部署时会引发持仓时长误判。

修复建议:

open_time=datetime.now(timezone.utc),

Bug 5: _get_actual_position_size 在限价单超时后使用缓存数据

严重度: 高

文件: executor.py:664-676

def _get_actual_position_size(self, coin: str, expected_side: str | None = None) -> float:
    try:
        positions = self.get_positions()  # ← 可能返回缓存(5s TTL)

limit_open 超时处理(第 826 行)中调用 _get_actual_position_size,但 get_positions() 默认使用缓存。限价单超时撤单后需要查询实时持仓量来判断部分成交量,使用缓存可能返回过时数据。

后果:

  • 缓存显示有持仓(但实际撤单已清零)→ 创建了错误的仓位
  • 缓存显示无持仓(但实际已部分成交)→ 丢失了已成交的部分仓位,交易所有残留仓位无人管理

影响: 直接导致资金损失风险。

修复建议:

def _get_actual_position_size(self, coin: str, expected_side: str | None = None,
                               force_refresh: bool = False) -> float:
    try:
        positions = self.get_positions(force_refresh=force_refresh)

调用处改为:

actual_filled = self._get_actual_position_size(alt_coin, expected_side=alt_side, force_refresh=True)

Bug 6: limit_close 撤单后 _get_actual_position_size 同样使用缓存

严重度: 高

文件: executor.py:1023, 1064

与 Bug 5 相同的问题,在平仓超时撤单后也使用了缓存数据:

remaining_size = self._get_actual_position_size(alt_coin, expected_side=position.alt_side)
# ...
remaining_base = self._get_actual_position_size(base_coin, expected_side=position.base_side)

后果:

  • 缓存显示仍有残余持仓 → 执行市价平仓更大数量 → 可能导致仓位翻转(虽然有 reduceOnly 保护,但如果 size > 实际持仓,reduceOnly 会被拒绝)
  • 缓存显示无残余 → 视为已成交 → 但实际仍有仓位残留

影响: 平仓时使用过期数据,可能导致仓位残留或平仓失败。

修复建议: 同 Bug 5,对 _get_actual_position_size 增加 force_refresh 参数,在撤单后强制刷新。


Bug 7: sync_with_exchangeclosed_pairs 可能重复追加

严重度: 低

文件: position_manager.py:1259-1263

close_result = self.close_position(
    lost_symbol, lost_base, reason="base_leg_lost", force_market=True
)
if close_result:
    closed_pairs.append(lost_key)  # ← 已在 1168 行通过幽灵清理追加过?

问题分析:

如果同一个 key 既因为 alt 腿在交易所消失被幽灵清理(第 1168 行 closed_pairs.append(key)),又因为 base 腿消失触发强制平仓,会导致 closed_pairs 中出现重复的 key。

影响: 目前返回值仅用于日志,不影响核心逻辑。但如果上层依赖这个列表做去重操作,会有问题。


Bug 8: _monitor_order 早期 HTTP 检查与 WS 消息处理的竞态窗口

严重度: 低

文件: websocket_order_manager.py:404-421

# 检查是否已有 WS 终态
with self._lock:
    if tracking._ws_status is not None:
        return
    current = self._tracking.get(oid)
    if current is not tracking:
        return
    if tracking.status != OrderStatus.PENDING:
        return
# ← 释放锁
# ← 此处 WS 消息到达,_on_order_update 设置 _ws_status 并启动 grace timer
logger.info(f"早期 HTTP 检查: {tracking.coin} oid={oid}")
result = self._http_check(oid)  # ← HTTP 查询期间 grace timer 触发 _finish()

问题分析:

在释放锁和 _http_check 之间,如果 WS orderUpdates 消息到达并设置了 _ws_status = FILLED + 启动了 grace timer,然后 grace timer 5 秒后触发 _finish() 完成订单。同时 _http_check 也查到了 filled 并调用 _finish_direct()

由于 _finish_finish_direct 都在锁内检查 tracking.status != OrderStatus.PENDING 才操作,所以不会重复结算。但如果 HTTP 查到 filled 的价格与 userFills 不同,先完成的路径会使用其价格,可能导致价格记录不是最准确的 userFills 价格。

影响: identity check 保证了不会重复结算,但价格精度可能受影响。


总结

严重度 Bug 文件 影响
Bug 5 executor.py:664 限价单超时后用缓存判断部分成交量,可能创建错误仓位或丢失仓位
Bug 6 executor.py:1023,1064 平仓撤单后用缓存查残余持仓,可能残留无人管理仓位
Bug 2 realtime_kline_service_base.py:618 缓冲区回放无锁保护,多线程下可能 IndexError 导致消息处理崩溃
中高 Bug 1 executor.py:946 leg_b=None 后丢失错误信息,诊断困难
Bug 4 position_manager.py:154 跨时区部署时持仓时长/超时计算偏差
Bug 3 position_manager.py:1134 类型标注与实际 tuple 长度不匹配
Bug 7 position_manager.py:1259 closed_pairs 列表可能重复追加
Bug 8 websocket_order_manager.py:404 HTTP 与 WS 竞态可能影响价格精度

最需要立即修复的是 Bug 5 和 Bug 6:在限价单超时撤单后,应使用 self.get_positions(force_refresh=True) 替代 self.get_positions() 来获取实时持仓数据,避免缓存导致的仓位判断错误。这在真实交易中可能直接导致资金损失。

Read more

跑步的技巧(滚动落地)

“滚动落地(rolling contact / rolling foot strike)”不是一种教条式的“脚法”,而是一种 让冲击沿着整只脚、整条后链逐级传递的落地机制。 它的核心不是“你先用哪儿着地”,而是: 你的脚落地之后,冲击是不是像轮子一样滚过去,而不是像锤子一样砸下去。 这就是滚动落地的本质。 一、什么叫“滚动落地”? 你可以把它理解成两种完全不同的落地方式: 1. 砸地(撞击式) 脚像锤子一样拍到地上: * 要么后跟先砸 * 要么前掌先戳 * 冲击集中在一个点 * 一个结构瞬间吃掉大部分载荷 结果就是: * 后跟砸 → 膝盖难受 * 前掌戳 → 前脚掌磨烂 * 都不是长跑友好模式 这叫 撞击式着地(impact strike)。 2. 滚地(滚动式) 脚像轮胎一样“滚”过地面: * 不是某一点硬砸 * 而是外侧中足先轻触 * 再向前滚到前掌 * 最后从大脚趾蹬离

By SHI XIAOLONG

AMI的优越性

世界模型(World Models)的具体例子 如下,我按类型分类,便于理解。每类都附带实际实现、演示效果和应用场景。 1. Yann LeCun / Meta 的 JEPA 系列(最直接对应“世界模型”概念) 这些是 LeCun 主张的非生成式抽象预测世界模型代表。 * I-JEPA(Image JEPA,2023) 输入一张图像,模型把不同区域(context 和 target)编码成抽象表示,然后预测 target 的表示(不在像素级别重建)。 例子:给定一张遮挡了部分物体的图片,模型能预测“被遮挡物体的大致位置和属性”,构建对物体持久性和空间关系的理解。 这是一个“原始世界模型”,能学习物理常识(如物体不会凭空消失)。 * V-JEPA / V-JEPA 2(Video JEPA,

By SHI XIAOLONG

什么是:“世界模型(World Models)”

世界模型(World Models) 是人工智能领域的一个核心概念,尤其在 Yann LeCun 等研究者推动的下一代 AI 架构中占据中心位置。它指的是 AI 系统在内部构建的对现实世界的抽象模拟或内部表示,让机器能够像人类或动物一样“理解”物理世界、预测未来、规划行动。 简单比喻 想象你闭上眼睛也能“看到”房间里的物体会如何移动、碰撞或掉落——这就是你大脑里的世界模型。AI 的世界模型就是类似的“数字孪生”(digital twin)或“内部模拟器”:它不是简单记住数据,而是学习世界的动态、因果关系和物理直觉(如重力、物体持久性、遮挡、因果等)。 为什么需要世界模型? 当前主流的大型语言模型(LLM) 擅长处理文本(统计模式预测),但存在根本局限: * 缺乏对物理世界的真正理解 → 容易“幻觉”、无法可靠规划。 * 样本效率低 → 人类/

By SHI XIAOLONG

K线周期可配置化设计方案

K线周期可配置化设计方案 1. 背景与目标 当前 Beta 套利策略的 K 线周期硬编码为 "1h",分散在多个文件中。需要: 1. 将 K 线周期从 1h 改为 2h 2. 提取为环境变量 BETA_ARB_KLINE_INTERVAL,使其可在 .env 中配置 2. 影响范围分析 2.1 需要修改的文件(共 6 个) 文件 硬编码位置 修改内容 src/trading/config.py BetaArbConfig dataclass 新增 kline_interval 字段,

By SHI XIAOLONG