全系统key配对升级 bug19

配对维度优化后(全系统Key改为PairKey)缺陷与不足分析

分析日期:2026-02-20
分析范围:src/trading/ 所有核心模块
基准版本:main 分支(最新提交 40e8849


一、严重缺陷


1. 启动SQL查询无时间边界

位置src/trading/orchestrator.py:138

all_rows = db_client.execute_query("""
    SELECT symbol, base_symbol, kline_time, zscore_4h
    FROM (
        SELECT ..., ROW_NUMBER() OVER (...) AS rn
        FROM analysis_results
        WHERE zscore_4h IS NOT NULL   # ← 无时间过滤!
    ) sub WHERE rn = 1
    ORDER BY symbol, base_symbol, kline_time DESC
""", params=())

问题描述:查询 analysis_results 全表,没有 kline_time >= NOW() - INTERVAL '...' 限制。随着运行时间增加,表中历史数据越积越多(数十万乃至百万行),每次重启都需要全表扫描,应用层再取前 144 条。

后果:生产环境运行一个月后,重启可能需要数十秒乃至数分钟才能完成初始化。

修复建议

WHERE zscore_4h IS NOT NULL
  AND kline_time >= NOW() - INTERVAL '30 days'   -- 144条4h K线约24天,30天足够

2. KillSwitch 不平仓现有仓位

位置src/trading/safety.py:28src/trading/risk_manager.py:67-69

# safety.py
def is_killed(self) -> bool:
    try:
        with open(self._file_path, "rb"):
            return True
    except FileNotFoundError:
        return False

# risk_manager.py - 只在开仓前检查
if self._kill_switch.is_killed():
    return False, f"Kill Switch 已激活: {self._config.kill_switch_file}"

问题描述is_killed() 只在 pre_trade_check 中被调用,仅阻止新建仓位,不触发平仓。激活 KillSwitch 后:

  • 现有仓位继续持有,止损/超时机制仍在运行
  • 若需紧急平仓,运维人员还要额外手动操作

后果:紧急情况下 KillSwitch 无法实现真正的"一键清仓",与运维预期不符。

修复建议:在止损监控线程 _stop_loss_monitor 中增加 KillSwitch 检查,触发时关闭所有仓位:

if self._kill_switch.is_killed():
    for pos in self._position_manager.open_positions:
        self._close_with_retry(pos, "kill_switch")

3. 止损平仓后冷却期结束仍无法自动再入场(逻辑陷阱)

位置src/trading/strategy.py:310-320src/trading/strategy.py:612-651

# on_position_closed 清除突破状态
def on_position_closed(self, symbol, base_symbol, timestamp=None):
    key = (symbol, base_symbol)
    with self._lock:
        if key in self._positions:
            self._last_trade_time[key] = timestamp  # 冷却计时开始
            self._positions.pop(key, None)
            self._prev_above_threshold.pop(key, None)  # ← 清除状态

问题描述:复现路径如下:

  1. 仓位止损平仓,on_position_closed 清除 _prev_above_threshold[key],冷却计时开始
  2. 下一个tickprev_above = False(已清除),若 adaptive_z 仍超阈值 → breakout = True,但冷却期拦截 → 跳过,并在末尾写入 _prev_above_threshold[key] = True
  3. 冷却期内后续 tickprev_above = Truecurrent_above = Truebreakout = False(非首次突破)→ 不产生信号
  4. 冷却期结束后prev_above 仍为 Truebreakout = False永远不产生新信号

只有等 adaptive_z 先跌破阈值、再重新上穿,才能产生新信号。若止损平仓后行情继续极端,即便冷却期过了也无法自动再入场,错失反弹机会。

后果:策略信号完整性损失,在止损后行情持续极端的场景下系统哑火。

修复建议:冷却期结束时主动重置突破检测状态:

# 在 _check_entry 中,冷却期结束时清除 _prev_above_threshold 使其可重新检测
if elapsed >= cooldown_seconds:
    self._prev_above_threshold.pop(key, None)  # 允许重新检测突破
    return None  # 本 tick 不产生信号,下 tick 可能触发

二、重要缺陷


4. __post_init__ 方法错误地插入在字段定义中间

位置src/trading/models.py:81-84models.py:139-142models.py:189-191

@dataclass
class PairTradeSignal:
    signal_id: str = ...
    signal_type: SignalType = ...
    symbol: str = ""
    base_symbol: str = ""

    def __post_init__(self):          # ← 在字段中间定义
        if self.base_symbol is None:
            self.base_symbol = ""
    direction: str = ""               # ← 方法后面还有字段!
    zscore_5m: float = 0.0
    ...

同样问题出现在 PairPosition(第139行)和 ReversionInfo(第189行)。

问题描述:虽然Python的dataclass机制能正确处理(所有字段在 __post_init__ 调用前完成初始化),但这种写法极易误导维护者误以为 __post_init__ 后面的字段不会被初始化,埋下理解隐患。

修复建议:将 __post_init__ 移到所有字段定义之后:

@dataclass
class PairTradeSignal:
    signal_id: str = ...
    ...
    # 所有字段定义完毕后
    def __post_init__(self):
        if self.base_symbol is None:
            self.base_symbol = ""

5. 残留 base 腿大小不匹配时无修复措施

位置src/trading/position_manager.py:699-708

with self._lock:
    if orphan_key in self._positions:
        existing = self._positions[orphan_key]
        if abs(existing.alt_size - actual_size) > 1e-6:
            logger.warning(
                f"⚠️ 孤儿 key {orphan_key} 已被现有仓位占用且 size 不符..."
            )
        else:
            logger.info(f"base 腿 {orphan_symbol} 已在管理中(size 一致),跳过收纳")
        return   # ← size不匹配时只警告然后return,没有更新size!

问题描述:当 orphan key 已存在但 size 不一致时,只打 warning,实际不做任何修正。这意味着系统内存中记录的持仓大小与交易所实际持仓大小不一致,止损计算将使用错误数值。

修复建议:size 不匹配时更新内存中的 size:

if abs(existing.alt_size - actual_size) > 1e-6:
    logger.warning(f"⚠️ 孤儿 size 不符,强制同步: {orphan_key} | 现有={existing.alt_size} → 交易所={actual_size}")
    existing.alt_size = actual_size  # 以交易所实际为准

6. Pair 模式下 leg_b 平仓失败时 PnL 计算不完整

位置src/trading/position_manager.py:633-648

base_pnl = 0.0
if position.pair_mode == "pair" and order_result.leg_b and order_result.leg_b.success:
    ...
    base_pnl = ...  # leg_b 失败时 base_pnl 永远为 0

问题描述:若 leg_b 经过3次重试后依然失败(leg_b_final_failed = True),base 腿通过 _adopt_residual_base_leg 转为独立孤儿仓位继续持有。但数据库记录的 realized_pnl 只包含 alt 腿的盈亏,缺失 base 腿部分,最终统计数字与实际不符。

修复建议:leg_b 转孤儿时,用当前市价估算 base 腿的未实现 PnL 并记录:

if leg_b_final_failed:
    # 估算 base 腿 PnL(将在孤儿仓位关闭时最终结算)
    base_mid = self._executor.get_all_mids().get(symbol_to_coin(position.base_symbol), 0.0)
    if base_mid > 0 and position.base_entry_price > 0:
        if position.base_side == "buy":
            estimated_base_pnl = (base_mid - position.base_entry_price) * position.base_size
        else:
            estimated_base_pnl = (position.base_entry_price - base_mid) * position.base_size
        # 记录备注说明 PnL 不完整

7. stop() 方法不主动平仓

位置src/trading/orchestrator.py:236-259

def stop(self):
    ...
    if self._position_manager:
        open_count = self._position_manager.open_count
        if open_count > 0:
            logger.warning(f"交易模块停止时仍有 {open_count} 个活跃仓位")
        # ← 只打日志,不平仓

问题描述:计划性停机时(如服务升级),现有仓位保留在交易所,依赖重启后恢复机制。若是永久停机或长时间不重启,仓位将无人监控,止损/超时保护完全失效。

修复建议:增加可配置的停机平仓选项:

# 配置项
close_positions_on_stop: bool = False  # 停机时是否平仓

# stop() 中
if self._config.close_positions_on_stop:
    for pos in self._position_manager.open_positions:
        self._position_manager.close_position(pos.symbol, pos.base_symbol, reason="graceful_stop")

三、中等缺陷


8. RateLimiter 全局共享,多配对场景下过于保守

位置src/trading/orchestrator.py:56-59

self._rate_limiter = RateLimiter(
    max_requests=self._config.rate_limit_per_minute,  # 默认30次/分钟
    window_seconds=60,
)

问题描述:所有配对共享同一个频率限制器。若同时跟踪 PURR|HYPE、ETH|BTC、SOL|BTC 三个配对,信号频繁时30次/分钟的限制很快被打满,其他配对的合法信号会被误拒。

修复建议:采用两级限速:全局总限速 + 按 PairKey 单独限速。


9. CircuitBreaker 只受开仓操作影响,平仓成功不重置计数

位置src/trading/orchestrator.py:557-572

if open_result:
    ...
    self._circuit_breaker.record_success()  # 仅开仓成功重置
else:
    ...
    self._circuit_breaker.record_failure()  # 仅开仓失败计数

问题描述

  • 平仓成功不重置熔断计数器
  • 平仓失败不增加失败计数
  • 网络瞬时抖动导致连续开仓失败 → 熔断 → 即使后续平仓成功,熔断状态也不会提前解除

10. _compute_adaptive_z 含隐藏副作用

位置src/trading/strategy.py:364-375

def _compute_adaptive_z(self, bl: SymbolBaseline, realtime_z4h=None) -> float | None:
    ...
    bl.last_std = std   # ← 修改了传入对象,违反 Command-Query Separation
    ...
    return (current_z4h - bl.ema) / std

问题描述:方法名暗示这是"计算"(查询),但实际修改了 bl.last_std(命令),违反 Command-Query Separation 原则,增加理解难度和隐患。

修复建议:将 bl.last_std = std 移到调用处,或将其作为返回值的一部分:

def _compute_adaptive_z(self, bl: SymbolBaseline, realtime_z4h=None) -> tuple[float | None, float]:
    ...
    return (current_z4h - bl.ema) / std, std

11. 止损平仓不计入 exit_signals 统计

位置src/trading/orchestrator.py 各平仓路径

平仓路径 trades_closed exit_signals
on_exit_signal(策略信号) ✅(position_manager内)
_stop_loss_monitor(止损/超时) ✅(position_manager内) ❌ 未计入
sync_with_exchange(幽灵清理) ✅(position_manager内) ❌ 未计入

问题描述:每日统计中 exit_signals 只反映策略信号引起的平仓,止损/超时/对账关闭的仓位不在统计之内,数据不完整。


12. 仓位关闭后部分诊断状态未清理,存在内存积累

位置src/trading/strategy.py:310-320

on_position_closed 清理了 _positions, _exit_pending, _prev_above_threshold但未清理

  • _last_near_thresh_time[key]
  • _last_status_time[key]
  • _tick_count[key]
  • _last_adaptive_z[key]

长期运行后(尤其是多配对场景),这些 dict 会积累大量已关闭配对的历史条目。

修复建议:在 on_position_closed 末尾补充清理:

self._last_near_thresh_time.pop(key, None)
self._last_status_time.pop(key, None)
self._tick_count.pop(key, None)
self._last_adaptive_z.pop(key, None)

四、轻微缺陷 / 设计局限


13. coin_to_symbol 硬编码 USDC,限制扩展性

位置src/trading/models.py:210-218

def coin_to_symbol(coin: str) -> str:
    if '/' in coin:
        return coin
    return f"{coin}/USDC:USDC"   # ← 硬编码 USDC

若未来扩展至 USDT 或其他计价币种,此函数将返回错误的 symbol 格式,导致订单下单失败或数据查询错误。


14. PnL 计算缺失手续费和资金费

位置src/trading/position_manager.py:620-648

alt_pnl = (alt_exit_price - alt_entry_price) * alt_size

仅计算价差盈亏,未包含:

  • Maker/Taker 手续费(Hyperliquid 约 0.01%~0.035%/笔)
  • 资金费(永续合约每8小时结算一次,最长持仓72小时可积累9次资金费)

对于长持仓,资金费累积可能显著影响实际盈亏,导致统计中的 realized_pnl 与账户实际变动不符。


15. save_position 将 exit_price 初始化为硬编码 0

位置src/trading/trade_repository.py:144-145

0,  # alt_exit_price 初始为0
0,  # base_exit_price 初始为0

ON CONFLICT DO UPDATE 会将 EXCLUDED.alt_exit_price 写回。若因意外情况在平仓后再次调用 save_position(例如恢复流程中的边缘情况),exit_price 会被覆盖为 0,破坏数据完整性。

修复建议ON CONFLICT 中对 exit_price 使用 COALESCE 保留非零值:

alt_exit_price = COALESCE(NULLIF(EXCLUDED.alt_exit_price, 0), pair_positions.alt_exit_price),

五、总结优先级矩阵

优先级 # 缺陷描述 影响范围
🔴 严重 1 启动SQL无时间限制,全表扫描 启动性能退化
🔴 严重 2 KillSwitch不平仓现有仓位 运维紧急响应能力
🔴 严重 3 止损后冷却期结束仍无法再入场 策略信号完整性
🟠 重要 4 __post_init__ 位置异常(3处) 代码可维护性
🟠 重要 5 残留base腿size不匹配无修复 止损计算准确性
🟠 重要 6 Pair模式leg_b失败时PnL不完整 盈亏统计准确性
🟠 重要 7 stop() 不主动平仓 计划停机安全性
🟡 中等 8 RateLimiter全局共享 多配对信号处理
🟡 中等 9 CircuitBreaker只受开仓影响 风控有效性
🟡 中等 10 _compute_adaptive_z 含副作用 代码质量
🟡 中等 11 止损平仓不计入exit_signals 统计完整性
🟡 中等 12 诊断状态未在仓位关闭时清理 长期内存积累
🟢 轻微 13 coin_to_symbol 硬编码USDC 扩展性
🟢 轻微 14 PnL缺失手续费和资金费 统计精度
🟢 轻微 15 exit_price初始化为0的覆盖风险 数据完整性

最优先修复:缺陷 #3(冷却期逻辑陷阱)和 #1(启动查询),两者对系统实际运行效果影响最大且改动成本最低。

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