开仓信号详细告警模块设计分析报告
结论:未发现严重 BUG
整体设计具备多层降级与异常隔离,递归有界,关键依赖有兜底,不存在会导致崩溃、死循环或错误告警内容的严重缺陷。以下为完整因果链与审查要点。
完整因果链:输入 → 状态变化 → 调用路径 → 出错点 → 根因
| 来源 |
内容 |
| 策略/服务层 |
on_entry_signal(symbol, multi_period_result, latest_alt_price, avg_zscore_4h, direction, signal_strength, adaptive_z, l2_snapshot) |
| 开仓成功后 |
signal(本函数内构造)、multi_period_result(原样透传)、position, order_result(来自 open_position 返回值)、adaptive_z |
| 配置/运行时 |
ENABLE_SIGNAL_DETAIL_ALERT、leverage、strategy_adaptive_threshold、strategy_reversion_factor、network_label;get_available_balance() 的实时返回值 |
上述输入在进入告警路径前已被使用:multi_period_result.get("zscore_list") 等会先执行,若 multi_period_result 为 None 会在此处抛错,不会进入 _send_entry_alert。
状态变化 (State changes)
| 阶段 |
位置 |
变化 |
| 开仓成功 |
src/trading/orchestrator.py |
save_signal(signal, "opened")、update_daily_stats(..., trades_opened=1)、circuit_breaker.record_success() |
| 发告警前 |
_send_entry_alert |
available_balance = self._executor.get_available_balance()(只读,无持久状态) |
| 格式化 |
format_signal_alert / _format_full |
无持久状态,纯函数计算 (title, content) |
| 发送成功 |
src/utils/monitoring/alert_sender.py |
_record(pair_name, content_hash) → _history[pair_name].append(now)、_recent_hashes[content_hash] = now(仅当未限流、未去重且即将调用 sender_colourful 时;若之后抛错则未执行 _record) |
限流/去重时 send() 直接返回 THROTTLED,不调用 sender_colourful,也不执行 _record,故不会改变 AlertSender 内部状态。
调用路径 (Call path)
策略/服务层
→ Orchestrator.on_entry_signal(symbol, multi_period_result, ...)
→ multi_period_result.get("zscore_list") / .get("base_symbol") / .get("cointegration_count") [若 None 则 AttributeError,在此结束]
→ 构造 PairTradeSignal(signal)
→ 频率/风控检查 → position_manager.open_position(signal, adaptive_z)
→ 若 open_result 为真:(position, order_result) = open_result
→ save_signal / update_daily_stats / circuit_breaker.record_success()
→ _send_entry_alert(signal, multi_period_result, position, order_result, adaptive_z)
_send_entry_alert
→ available_balance = self._executor.get_available_balance()
→ if not ENABLE_SIGNAL_DETAIL_ALERT: 直接跳到 _send_simple_entry_notification
→ try:
title, content = format_signal_alert(signal, multi_period_result, position, order_result, ...)
status = self._alert_sender.send(title, content, pair_name=signal.symbol, priority=...)
if status == AlertStatus.SUCCESS: return
else: 记录 warning,继续往下
except Exception: 记录 error,继续往下
→ _send_simple_entry_notification(signal, position, order_result, adaptive_z, available_balance)
format_signal_alert(level=FULL)
→ _format_full(...)
→ risk = evaluate_risk(multi_period_result) [内部 try/except,异常时返回默认 RiskAssessment]
→ for (name, builder, args) in builders: try builder(*args) → sections.append(...); except → sections.append("**{name}**: ⚠️ 数据缺失")
→ content = join(sections);若超长则截断
→ return (title, content)
若上述任一步抛异常:
→ level==FULL → format_signal_alert(..., level=SIMPLIFIED)
→ level==SIMPLIFIED → format_signal_alert(..., level=BASIC)
→ level==BASIC → 兜底 return ("⚠️ 开仓成功(告警格式化异常) - {coin}", "**方向**: ...")
AlertSender.send
→ if priority != "high" and not _check_rate_limit(pair_name): return THROTTLED
→ if _is_duplicate(content_hash): return THROTTLED
→ sender_colourful(content=content, title=title) [可能抛 RuntimeError 等]
→ _record(pair_name, content_hash)
→ return SUCCESS
_send_simple_entry_notification
→ 拼装 title/content(使用 position.*, order_result.*, signal.*, available_balance)
→ _send_trading_notification(title, content)
→ try: sender_colourful(content=..., title=...)
except (OSError, ValueError): logger.error(...)
出错点 (Error points)
| # |
位置 |
现象 |
是否被捕获/降级 |
| E1 |
on_entry_signal 内 multi_period_result.get(...) |
multi_period_result 为 None → AttributeError |
否,抛给调用方,不会进入告警路径 |
| E2 |
_format_full 内 evaluate_risk(multi_period_result) |
异常数据结构导致内部异常 |
是,evaluate_risk 内部 try/except 返回默认 RiskAssessment |
| E3 |
_format_full 内某 builder(*args) |
某区块依赖字段缺失/类型错误 |
是,该区块 try/except → append "数据缺失" |
| E4 |
format_signal_alert 兜底分支 |
_symbol_to_coin(signal.symbol) 或 signal.direction / signal.zscore_4h 若为 None 或非预期类型 |
仅在 level==BASIC 且再次抛异常时进入兜底;兜底内若 signal.symbol 为 None 会 AttributeError,无再一层捕获 |
| E5 |
AlertSender.send |
限流 → return THROTTLED;去重 → return THROTTLED |
不抛异常,orchestrator 收到 THROTTLED 后走 _send_simple_entry_notification |
| E6 |
AlertSender.send 内 sender_colourful(...) |
队列满等 → RuntimeError |
未在 send 内捕获,抛给 orchestrator;orchestrator 的 try 捕获后走 _send_simple_entry_notification |
| E7 |
_send_simple_entry_notification 内拼装 content |
position.alt_size / position.position_id 等为 None |
未在此处 try,异常会抛到 _send_entry_alert 的调用方 |
| E8 |
_send_trading_notification 内 sender_colourful(...) |
OSError / ValueError |
已捕获,只打日志 |
| E9 |
同上 |
RuntimeError(如队列满) |
未捕获,向 _send_entry_alert 的调用方传播 |
根因 (Root cause)
| 出错点 |
根因归纳 |
| E1 |
输入约束未在边界保证:调用方传入 multi_period_result=None 或未做前置校验,导致在 on_entry_signal 内首次 .get 即崩溃。 |
| E2 |
数据形态多样:multi_period_result 来自上游分析/DB,可能缺键或类型异常;evaluate_risk 通过内部 try/except 兜底,避免影响告警。 |
| E3 |
区块依赖的键或类型不一致:如 details 中 period key 为 tuple 与 string 混用、health_monitor 缺失等;单区块失败被隔离为「数据缺失」。 |
| E4 |
兜底分支仍依赖 signal 必选字段:若 signal.symbol 为 None(理论上不应出现,因由 on_entry_signal 构造),兜底会再抛错;根因是 signal 构造或传递未保证非空。 |
| E5 |
限流/去重策略:同一 pair 在窗口内超过 N 条或内容哈希重复,主动返回 THROTTLED,属业务策略而非缺陷。 |
| E6 |
异步发送队列满或 lark_bot 内部异常:sender_colourful 或下层实现抛错,AlertSender 未吞掉异常,由 orchestrator 统一降级为简短通知。 |
| E7 |
position/order_result 形态依赖 open_position 契约:若 open_position 返回的 position 某字段为 None(违反契约),会在此暴露;根因在仓位/订单结果构造或序列化。 |
| E8/E9 |
发送层异常类型与捕获范围不一致:_send_trading_notification 只捕获 OSError/ValueError,RuntimeError 等未捕获会导致「开仓成功但通知失败且异常上抛」;根因是异常分类未覆盖发送端可能抛出的全部类型。 |
流程与数据流
sequenceDiagram
participant O as Orchestrator
participant F as format_signal_alert
participant A as AlertSender
participant S as sender_colourful
O->>O: get_available_balance()
alt ENABLE_SIGNAL_DETAIL_ALERT
O->>F: format_signal_alert (level=FULL)
alt FULL 成功
F-->>O: title, content
O->>A: send(title, content, ...)
alt SUCCESS
A->>S: sender_colourful
A-->>O: SUCCESS
O-->>O: return
else THROTTLED
A-->>O: THROTTLED
O->>O: _send_simple_entry_notification
O->>S: sender_colourful (直接调用)
end
else FULL/SIMPLIFIED 异常
F-->>F: 降级 SIMPLIFIED/BASIC 或兜底
F-->>O: title, content
O->>A: send(...)
end
end
O->>O: _send_simple_entry_notification
O->>S: sender_colourful (直接)
- 详细告警路径:仅通过
src/utils/monitoring/alert_sender.py 的 AlertSender.send(限流+去重)。
- 降级路径:走
src/trading/orchestrator.py 的 _send_trading_notification → 直接调用 sender_colourful,不经过 AlertSender。
已确认的安全设计
| 点 |
说明 |
| 递归有界 |
format_signal_alert 仅在 except 中按 FULL → SIMPLIFIED → BASIC 降级并递归,最多 3 层;BASIC 的兜底不再递归。 |
| evaluate_risk 兜底 |
evaluate_risk 内部 try/except,异常时返回默认 RiskAssessment,不会把 None/异常结构抛给告警格式化。 |
| 区块级隔离 |
_format_full 中 10 个区块各自 try/except,单块失败只写「数据缺失」,不拖垮整条告警。 |
| 超长截断 |
content 超过 _MAX_CONTENT_LENGTH 时按核心区块截断,且仅在 len(sections) >= 10 时用固定下标,与 10 个 builder 一致,无越界。 |
| 入参来源 |
multi_period_result 来自 on_entry_signal 参数,在到达 _send_entry_alert 前已用过 .get("zscore_list") 等,若为 None 会提前抛错,不会在告警内出现 None。 |
设计点与可改进边界
3.1 降级路径不经过 AlertSender(设计取舍)
- 现象:详细告警被限流/去重时,会走
_send_simple_entry_notification → _send_trading_notification → 直接 sender_colourful,不经过 AlertSender。
- 影响:同一币对在限流窗口内多次开仓时,可能收到「多次简短通知」(每次开仓一条),而详细告警仍被限流。
- 判断:属于设计取舍——保证「至少有一条开仓通知」;若希望简短通知也受同一限流/去重约束,需要让简短通知也走 AlertSender 或统一入口。
3.2 详细告警发送抛错时的重复风险(边缘)
- 现象:若
AlertSender.send() 在调用 sender_colourful 之后、_record() 之前抛错(如队列满),orchestrator 会 catch 后再发简短通知。
- 影响:理论上存在「详细消息已入队 + 再发简短」导致两条通知的极小概率。
- 建议:若 lark_bot 在「队列满」时是先抛错、未入队,则当前逻辑无重复;否则可在文档或注释中说明该边缘情况,或考虑在 send 内更细的异常处理与记录。
3.3 _send_trading_notification 的异常范围
- 现象:
_send_trading_notification 仅捕获 OSError, ValueError;若 sender_colourful 抛出 RuntimeError(如队列满),会向调用方继续传播。
- 影响:降级后的简短通知发送若因此抛错,会一路抛到
_send_entry_alert 的调用方;开仓本身已成功,仅通知失败。
- 建议:若希望「通知失败不干扰主流程」,可在此处增加对
RuntimeError 的捕获并打日志,或统一为「所有发送异常只记日志不抛出」。
3.4 ENABLE_SIGNAL_DETAIL_ALERT 类型
- 现象:配置通过
os.getenv(..., 'true').lower() in ('true', '1', 'yes') 得到 bool,行为明确,无类型问题。
小结
- 是否存在严重 BUG:否。未发现崩溃、无限递归、错误告警内容或数据越界等问题;降级链与风险评估均有兜底。
- 建议:若希望简短通知与详细告警共享限流/去重,可将简短通知也经 AlertSender 发送;并可在
_send_trading_notification 中扩展异常捕获(如 RuntimeError)或文档化边缘行为,以便后续维护。