# 策略工厂与信号引擎验证清单(Strategy Factory & Signal Engine Validation) > 目的:把需要“逐项验证”的点全部列清楚,变成可打勾的 checklist。 > 范围:`backend/signal_engine.py`(single-engine 数据发射源) + V5.4 策略工厂(`strategies` 表 → 信号 / 模拟盘)。 > 权威规格仍以 `docs/arbitrage-engine-full-spec.md` 为准,本文件只列“需要检验的点”。 --- ## 1. 背景与角色划分(人话版) - single-engine(`backend/signal_engine.py`)在本系统里扮演的是: - 从 `agg_trades` + `market_indicators` 等数据源,滚动算出各种指标; - 每 15 秒左右,对每个运行中策略打分、判断是否开仓; - 把结果写入: - `signal_indicators`(信号与指标快照); - `paper_trades`(模拟盘开仓记录); - 并通过 NOTIFY 推送给其他进程。 - V5.4 策略工厂的职责是: - 把 `strategies` 表中 `status='running'` 的每一行,当成一个“策略实例”; - 对每个实例,组合: - 多窗口 CVD(5m / 15m / 30m / 1h / 4h); - ALT 四层评分(Direction / Crowding / Environment / Auxiliary); - 5 个否决 Gate(vol / cvd / whale / obi / spot_perp)的阈值与开关; - 在统一的信号模型上,跑出不同风格的策略。 当前假设: - 你只对 **aggTrades 数据流** 有信心,其他所有东西(指标/门控/评分/策略映射/落库)都需要重新审计; - 我们需要一个尽可能完整的“验证清单”,后续可以按照这份清单逐条打勾。 下面把需要检查的点分成 3 层: - A. 数据与指标层:所有输入信号是不是对的(基础指标本身) - B. 策略配置与工厂层:DB 字段是否正确映射成策略参数 - C. 决策与落库层:评分 / 门控 / 开仓 / 写库 / 冷却 / 方向限制等是否符合规则 --- ## 2. 总体结构视图(先有个地图) - **数据来源** - `agg_trades`:逐笔成交 → 多窗口 CVD / ATR / VWAP / 大单 P95/P99 / 巨鲸成交; - `market_indicators`:OI、多空比、Top Trader、资金费率、Coinbase Premium、OBI、期现背离等; - 实时 WebSocket:OBI 实时值、spot-perp divergence 实时值(如果有)。 - **策略配置** - `strategies` 表:策略实例的参数(symbol、方向、CVD 窗口、四层权重、五门阈值、TP/SL、timeout、flip_threshold 等); - `load_strategy_configs_from_db()`:把这些行映射成内部配置 dict。 - **决策 / 落库** - `evaluate_factory_strategy()`(原 `_evaluate_v53`):统一的评分与 Gates 逻辑; - `save_indicator()`:写 `signal_indicators`; - `paper_open_trade()`:写 `paper_trades`; - `NOTIFY new_signal`:推送给 live_executor 等其他进程。 这 3 层每一处的小错误,都可能直接导致“赚钱 / 亏钱”的行为偏差,所以需要逐项验证。 --- ## 3. A 层:数据与指标验证(single-engine 基础指标) ### A1. 时间线与窗口基础 - ✅ `agg_trades.time_ms` 的语义是否统一为 **毫秒时间戳**,无混用秒/毫秒的情况。 - ✅ 冷启动加载历史时,时间范围是否覆盖至少 `24h+`,足够支持所有窗口(5m/15m/30m/1h/4h/24h)。 - 当前实现:冷启动预载 4h(WINDOW_MID),之后由实时数据自然填充到 24h。此行为已知且暂视为接受的设计,不视为 bug。 - ✅ 滚动窗口裁剪规则是否一致(例如统一使用 `t >= now_ms - window_ms`),避免 off-by-one。 - ✅ 历史回补或乱序插入时,是否会导致窗口内时间回跳,进而影响 CVD/ATR 等指标。 - 当前实现:agg_trades_collector 通过 REST 补洞 + 连续性巡检,配合按 agg_id/time_ms 有序读取,认为不会产生系统性时间回跳问题。 ### A2. CVD 多窗口计算 目标:确认 5m / 15m / 30m / 1h / 4h 所有 CVD 计算都符合“主动买入量 - 主动卖出量”的定义。 - ✅ 对 is_buyer_maker 的解释是否正确: - 实现:采集时 `is_buyer_maker = 1 if t["m"] else 0`,CVD 里 `is_buyer_maker == 0` 计为买量(taker 买),`==1` 计为卖量(taker 卖); - 结论:与 Binance aggTrade 语义一致,CVD = taker 买入量 - taker 卖出量,符合预期。 - ✅ 基础 30m / 4h 窗口(`win_fast` / `win_mid`)中,新增/裁剪 trade 时 buy/sell 累计是否严格同步更新。 - 实现:所有 trade 通过 `TradeWindow.add()` 进入窗口,`trim()` 时同步回退 buy/sell 累计,CVD 始终等于当前窗口内的买减卖。 - ✅ 其他窗口(5m/15m/1h)的 CVD 是否通过对已有窗口(win_fast/win_mid)的列表按时间切片重算,而不是重新查库。 - 实现:通过 `_window_ms()` 把窗口字符串转毫秒,从 `win_fast`/`win_mid` 的 `trades` 列表按 `t_ms >= now_ms - window_ms` 切片重算,未重复查库。 - ✅ 所有窗口的截止时间点是否都使用当前 `now_ms`,不会出现“5m 用旧 now_ms,30m 用新 now_ms”的错位。 - 实现:`build_evaluation_snapshot(now_ms)` 与 `evaluate_factory_strategy(now_ms, ...)` 使用同一个 `now_ms`,动态切片和快照在同一时间基准上计算。 - ⚠️ 斜率与加速度: - ✅ 定义:`cvd_fast_slope = cvd_fast(now) - prev_cvd_fast`,`cvd_fast_accel = cvd_fast_slope(now) - prev_cvd_fast_slope`,在持续运行时语义正确; - ⚠️ 冷启动第一帧使用 `prev_* = 0` 计算,未做专门回退为 0 的处理,可能使第一帧 slope/accel 数值偏大;目前视为需要后续评估/是否调整的注意点。 ### A3. ATR 与 ATR 百分位 - ✅ 5m K 线 bucket 是否按 `bar_ms = (time_ms // period_ms) * period_ms` 计算,保证时间对齐。 - 实现:`ATRCalculator.update()` 中按 `time_ms // period_ms` 对齐 5m bar,符合预期。 - ✅ Candle 的 open/high/low/close 更新逻辑是否正确,所有 trade 都被处理到。 - 实现:新 bar 时写入旧 bar 到队列,当前 bar 逐笔更新 high/low/close。 - ✅ TR(True Range)计算是否符合标准定义: - 实现:`max(high - low, |high - prev_close|, |low - prev_close|)`,与标准 TR 定义一致。 - ✅ ATR 是按何种算法计算(简单平均 / Wilder EMA),与 full-spec 中的期望是否一致。 - 实现:先取第一根 TR 作为初始值,再用 `(atr_val*(length-1)+tr)/length` 逐步平滑,为 Wilder 风格 EMA,符合设计。 - ✅ 当 candle 数量不足(小于 2 或小于 ATR length)时,ATR 返回 0 或合理默认值。 - 实现:`len(candles) < 2` 时直接返回 0.0,避免未成熟 ATR 参与决策。 - ✅ `atr_percentile`: - ✅ 历史 ATR 样本长度、维护方式(队列长度)是否合理; - ✅ 百分位算法(<= current / count)是否正确; - ✅ 样本太少时是否返回 50.0,避免无意义的极端值。 - 实现:`atr_history` 长度上限 288,少于 10 条或当前 ATR=0 时返回 50.0,其余按 rank 百分位计算。 ### A4. P95 / P99 大单阈值 - ✅ 使用的样本是否来自 24h 窗口(`win_day`),并且只取 qty(不乘 price)。 - 实现:`compute_p95_p99()` 直接遍历 `win_day.trades`,取 `t[1]`(qty)构造样本数组,窗口长度即 24h。 - ✅ 样本数 < 100 时是否使用保底常数(BTC: p95>=5, p99>=10;ALT: p95>=50, p99>=100),是否与策略设计一致。 - 实现:少于 100 条时直接返回 `(5.0, 10.0)`;样本足够时再按 symbol 区分 BTC/ALT 保底。 - ✅ 排序和索引算法:`sorted(qtys)` 后,`int(n * 0.95)` / `int(n * 0.99)` 是否符合预期的分位点定义。 - 实现:对 qtys 排序后按整数下标取 95% / 99% 位置,属于合理的分位点近似。 - ✅ ALT 的分类逻辑是否只是“非 BTC 即 ALT”,以及这是否满足未来新增 symbol 的需求。 - 实现:判断条件为 `"BTC" in self.symbol`,否则按 ALT 处理;目前支持的 symbol 集合有限(BTC/ETH/XRP/SOL),该简化规则可接受,如未来扩展再细化。 ### A5. 巨鲸成交与 `whale_cvd_ratio` - ✅ 巨鲸交易过滤条件是否为 `price * qty >= 100_000`(100k USD),阈值可配置或硬编码? - 实现:`usd_val = price * qty`,`usd_val >= 100_000` 视为巨鲸成交,阈值目前硬编码为 100k。 - ✅ `_whale_trades` 窗口长度是否为 15 分钟,裁剪逻辑是否正确(按 time_ms)。 - 实现:窗口长度 `WHALE_WINDOW_MS = 15min`,按 trade 的 `time_ms` 滑动裁剪。 - ✅ `whale_cvd_ratio` 计算: - ✅ buy_usd = 所有巨鲸买单金额之和; - ✅ sell_usd = 所有巨鲸卖单金额之和; - ✅ ratio = (buy_usd - sell_usd) / (buy_usd + sell_usd); - ✅ 无数据时返回 0.0 是否符合预期。 - ✅ BTC 与 ALT 对巨鲸数据的使用差异是否符合设计: - 实现:BTC 用 `whale_cvd_ratio`(或 DB 中 `tiered_cvd_whale`)做 Gate;ALT 使用 `recent_large_trades` 中的对立/同向大单判断。 - 备注:鲸鱼 Gate 的“占比阈值”在 C2 中有一个缩放 bug(`whale_flow_pct` 被多除以 100),已在 Gate 小节标记为问题点。 ### A6. `market_indicators` 中的 JSON 指标 需要逐个核对字段名称与意义(对照实际 JSON): - ✅ `long_short_ratio`: - 实现:从 `market_indicators` 读取 JSON `value`,使用 key `longShortRatio` 转为 float;默认 1.0。 - ✅ `top_trader_position`: - 实现:使用 key `longAccount`,代表 top trader 多头占比(0~1)。 - ✅ `open_interest_hist`: - 实现:使用 key `sumOpenInterestValue` 作为 OI 数值,后续只用来计算相对变化率。 - ✅ `coinbase_premium`: - 实现:使用 key `premium_pct`,并统一除以 100 转成小数,避免量纲混乱。 - ✅ `funding_rate`: - 实现:优先使用 `fundingRate`,否则 fallback `lastFundingRate`。 - ✅ `obi_depth_10`: - 实现:使用 key `obi`,作为 [-1,1] 区间内的订单簿不平衡(正=买压,负=卖压),与 Gate4 逻辑一致。 - ✅ `spot_perp_divergence`: - 实现:使用 key `divergence`,注释中定义为 `(spot - mark) / mark`,与 Gate5 使用方式一致。 ### A7. OI 环境指标 - ✅ OI 读取:`open_interest_hist` 是否转换为 float; - 实现:通过 `to_float(self.market_indicators.get("open_interest_hist"))` 转为 float。 - ✅ OI 变化率定义:`oi_change = (oi_now - oi_prev) / oi_prev`,oi_prev>0 的保护是否到位。 - 实现:仅在 `prev_oi_value > 0` 时计算变化率,否则 oi_change=0.0。 - ✅ environment_score_raw 的区间划分(例如 >=3%、0~3%、<0)对应基础分是否与 full-spec 一致。 - 实现:oi_change >=3% → 15 分;>0 → 10 分;<=0 → 5 分,符合设计。 - ✅ `prev_oi_value` 的更新时机:只在 oi_value>0 时更新是否会导致长时间停留在 0。 - 实现:oi_value>0 时才覆盖 prev,启动后首帧用默认 10 分,后续随 OI 变化更新,行为符合预期。 ### A8. 其他环境与拥挤指标 - ✅ `long_short_ratio` 的逻辑:>1 多头拥挤,<1 空头拥挤,这个假设需要与真实数据验证。 - 实现:LONG 方向时 lsr 较低(<1,<0.7,<0.5)给更高分,SHORT 方向时 lsr 较高(>1,>1.5,>2)给更高分,符合“多空占比”直觉。 - ✅ crowding 层中对 LONG / SHORT 的条件: - 实现与上述区间一致,并在 V5.1 路径中用 crowding_score=ls_score+top_trader_score;V5.3 路径中 crowding_score capped 为 25 分。 - ✅ `top_trader_position` 映射: - 实现:LONG:>=0.55 给 10 分、<=0.45 给 0 分、其他给 5 分;SHORT 反向,对应“top trader 站在我们这一边时加分”的逻辑。 - ✅ `coinbase_premium` 阈值: - 实现:>0.0005 / <-0.0005 视为强信号得 5 分;|premium|<=0.0005 得 2 分;其他情况 0 分,与 full-spec 描述一致。 - 备注:精确阈值是否需要微调属于策略优化问题,暂不视为实现 bug。 ### A9. 时间与时区统一 - ✅ `agg_trades.time_ms`、`market_indicators.timestamp_ms`、`signal_indicators.ts` 是否全部为 UTC 毫秒。 - 实现:agg_trades 来自 Binance aggTrade `T`(毫秒),market_indicators 查询按 `timestamp_ms` 排序,signal_indicators.ts 由 `int(time.time()*1000)` 生成,语义统一为毫秒。 - ✅ signal_engine 的 `now_ms` 是否与最新成交的时间差在可接受范围内(比如 <5 秒)。 - 实现:每轮循环用当前 `time.time()` 作为 now_ms,立刻用 `fetch_new_trades` 拉取自上次 agg_id 之后的 tick 并入窗口,逻辑上不会积累大延迟。 - ✅ 所有窗口、冷却时间、timeout 等是否都用毫秒表达,避免混用秒/分钟。 - 实现:窗口常量、冷却时间、timeout 以及 liquidations 窗口等全部以毫秒表示。 --- ## 4. B 层:策略配置与工厂验证(DB → 内存策略) ### B1. 行筛选与顺序 - ✅ `load_strategy_configs_from_db()` 是否只选择 `status='running'` 的行。 - 实现:SQL 显式 `WHERE status = 'running'`。 - ⚠️ 是否有必要基于 `schema_version` 做过滤(未来版本升级时)。 - 实现:当前未按 `schema_version` 过滤,所有 running 策略视为同一版本;如未来引入多版本策略,需要补充。 - ✅ ORDER BY created_at 是否会在行为上产生影响(一般无关,但需要意识到)。 - 实现:仅影响策略评估与写入的顺序,对行为影响有限。 ### B2. 字段映射完整性 逐字段确认 DB → cfg 的映射: - ✅ `strategy_id`(uuid)是否被转为 string,并赋值到 `cfg["strategy_id"]`。 - 实现:SQL 中 `strategy_id::text`,后续直接写入 cfg。 - ✅ `display_name` 是否写入 `cfg["strategy_name_snapshot"]`。 - ✅ `symbol` / `direction` 是否原样保留,大小写约定是否固定。 - 实现:直接从 DB 取值赋入 cfg,不做大小写转换。 - ✅ `cvd_fast_window` / `cvd_slow_window` 是否直接映射到 cfg。 - ✅ `weight_direction` / `weight_env` / `weight_aux` / `weight_momentum` 是否正确组成 `cfg["weights"]`。 - ✅ `entry_score` / `flip_threshold` 是否正确映射到 `cfg["threshold"]` / `cfg["flip_threshold"]`。 - ✅ `gate_obi_enabled` / `gate_whale_enabled` / `gate_vol_enabled` / `gate_spot_perp_enabled` / `gate_cvd_enabled` 是否被正确翻译成 bool。 - 实现:直接使用 DB 中的 boolean 字段。 - ✅ 所有 Gate 相关 float 字段(`obi_threshold`、`whale_usd_threshold`、`whale_flow_pct`、`vol_atr_pct_min`、`spot_perp_threshold`) 在 None 时有合理默认(与 full-spec 匹配)。 - 实现:在转入 cfg 时对 None 做 `or 默认值` 处理,默认值来源于迁移脚本/旧 symbol_gates。 - ✅ `sl_atr_multiplier` / `tp1_ratio` / `tp2_ratio` 是否正确映射到 `cfg["tp_sl"]`。 - ✅ `timeout_minutes` 是否正确映射到 `cfg["timeout_minutes"]`。 ### B3. legacy 策略兼容逻辑 - [ ] 固定 UUID: - `...000053` → `"v53"`; - `...000054` → `"v53_middle"`; - `...000055` → `"v53_fast"`; 是否只对 deprecated legacy 策略生效。 - [ ] 其他策略是否统一命名为 `custom_`,与历史 `strategy` 字段约定一致。 - [ ] 当 DB 无策略时,是否 fallback 到 JSON 文件配置;当 DB 有策略时是否会重复加载 JSON。 更新: - ✅ 固定 UUID 的映射逻辑正确,且只通过 `LEGACY_UUID_MAP` 在内存层生效,不会修改 DB 中的 strategy_id。 - ✅ 其他策略统一命名为 `custom_`,与历史 `strategy` 字段兼容。 - ✅ main() 中当 DB 读不到策略(异常)时才 fallback 到 JSON,正常情况下不会 DB+JSON 混用。 ### B4. CVD 窗口参数 - ✅ `cvd_fast_window` / `cvd_slow_window` 的合法取值集合:是否只支持 {5m,15m,30m,1h,4h}。 - 实现:`_window_ms()` 支持任意 m/h,策略层目前只用上述几种组合。 - ✅ 非法值时行为:抛出异常还是回退到默认 30m/4h。 - 实现:未匹配 "m"/"h" 后缀时回退到 30m,对异常配置有兜底。 - ✅ `_window_ms()` 的实现是否覆盖所有这些枚举值。 - ✅ `evaluate_factory_strategy()` 中选择 trade 源时,fast/slow 窗口与 win_fast/win_mid 的映射是否一致。 - 实现:fast 窗口长度 <=30m 用 win_fast,否则用 win_mid;slow 总是用 win_mid,与 full-spec 描述一致。 ### B5. 四层权重与 entry/flip - ✅ `weights` 是否已真实参与 scoring(direction/env/aux/momentum 四层权重)。 - 现状:V5.4 `strategies` 中的权重字段被映射到 cfg["weights"](direction/env/aux/momentum),`evaluate_factory_strategy()` 会将四个权重归一化到 100 分,并把 direction+momentum 总权重按 55:25 的比例拆分给 direction/crowding,再按各层原始得分比例缩放,真实影响 total_score。 - ✅ `entry_score` 是否只用于“达到该分数即可考虑开仓”,不会在别处被重载。 - 实现:在 `evaluate_factory_strategy()` 中作为标准开仓阈值(total_score < entry_score 时不会产生 signal),signal_engine 通过是否有 signal 决定是否尝试开仓。 - ✅ `flip_threshold` 是否用于“重仓 / 反向强平”逻辑,阈值关系是否是 `flip_threshold >= entry_score`。 - 实现:在 `evaluate_factory_strategy()` 中,total_score >= flip_threshold 时记为重仓 tier="heavy";在 `signal_engine` 中,当反向信号的 `score >= flip_threshold` 时触发 signal_flip 平仓。代码允许设置任意数值,上层应保证配置关系。 - ⚠️ 当 `flip_threshold < entry_score` 时,系统行为是否有定义或需要禁止。 - 现状:代码未禁止这种配置,会导致实际行为变成“更容易触发 heavy 仓”;需要在策略配置侧约束,暂不视为实现错误。 ### B6. Gate 参数与开关 逐 Gate 检查 DB → cfg 的映射: - [ ] Gate vol: - `gate_vol_enabled` → `gates["vol"]["enabled"]`; - `vol_atr_pct_min` → `gates["vol"]["vol_atr_pct_min"]`,默认值与 full-spec 一致。 - [ ] Gate cvd: - `gate_cvd_enabled` → `gates["cvd"]["enabled"]`; - 默认是否为 True,并与 full-spec 一致。 - ✅ Gate whale: - `gate_whale_enabled` → `gates["whale"]["enabled"]`; - `whale_usd_threshold` → `gates["whale"]["whale_usd_threshold"]`; - ✅ `whale_flow_pct` → `gates["whale"]["whale_flow_pct"]`(注意是否有 /100 的处理)。 - 更新:`evaluate_factory_strategy()` 中已去掉多余的 `/100` 缩放,现使用 0~1 的比例值与 DB/迁移脚本一致,鲸鱼 Gate 灵敏度恢复到设计水平。 - [ ] Gate obi: - `gate_obi_enabled` → `gates["obi"]["enabled"]`; - `obi_threshold` → `gates["obi"]["threshold"]`。 - [ ] Gate spot_perp: - `gate_spot_perp_enabled` → `gates["spot_perp"]["enabled"]`; - `spot_perp_threshold` → `gates["spot_perp"]["threshold"]`。 ### B7. 策略方向限制 - ⚠️ DB 中的 `direction` 字段枚举是否固定为 `"long" | "short" | "both"`(大小写统一)。 - 现状:代码层直接使用 DB 文本,不验证枚举/大小写;依赖上游迁移与 API 保证有效值。 - ✅ `evaluate_factory_strategy()` 或上层逻辑中是否有对该字段的应用: - 预期:direction=long → 禁止 SHORT 信号开仓;direction=short → 禁止 LONG 信号开仓;direction=both → 不限制。 - 现状:已在 `evaluate_factory_strategy()` 内增加 per-strategy 方向约束: - long 策略在方向判定为 SHORT 时不会产生 signal(但仍计算评分与指标快照); - short 策略在方向判定为 LONG 时不会产生 signal; - both 不做限制。 - ⚠️ 若当前实现未对 strategy direction 做约束,需要在此记为一个重点检查项。 - 更新:方向约束已在信号生成阶段生效,但反向 signal_flip 平仓仍按“环境方向”判断,是否也需要 direction 约束留待后续决策。 --- ## 5. C 层:决策与落库验证(评分 + 门控 + 开仓 + 写库) ### C1. 逐 symbol / 逐策略评估流程 - ✅ 评估循环是否为:先按 symbol 分组,再对每个 symbol 内的所有策略依次评估。 - ⚠️ 对于每个策略,是否在评估前检查 `cfg["symbol"] == state.symbol`,确保不会在错误币种上开仓。 - 现状:评估阶段对所有策略都计算分数,但在开/平仓阶段才按 `strategy_cfg["symbol"]` 过滤,因此不会在错误币种上实际开仓,只是多算了一些“无用分”。 - ✅ 是否存在“一个策略在多个 symbol 上同时运行”的预期场景,如果有,需要明确规则。 - 现状:当前配置一条策略只绑定一个 symbol,引擎逻辑允许同一策略在多个 symbol 开仓,但上层 API 不这么用。 ### C2. 五个 Gate 的逻辑 对于每一门,需要验证条件与 full-spec 一致: - Gate1:波动率门(ATR/price 下限) - ✅ `atr_pct_price = atr / price` 的计算是否正确; - ✅ 阈值 `min_vol` 是否来自 DB 的 `vol_atr_pct_min` 或 JSON fallback; - ✅ 条件:`atr_pct_price < min_vol` 时,gate_block = `"low_vol(...)"`,并且后续直接视为不通过。 - Gate2:CVD 共振门 - ✅ 快慢 CVD 同向时方向判断是否为: fast>0 且 mid>0 → LONG;fast<0 且 mid<0 → SHORT; - ✅ 不同向时,若 gate_cvd_enabled=True,则设置 `no_direction=True` 且 `gate_block="no_direction_consensus"`; - ✅ 当 Gate2 关闭(enabled=False)时,是否允许仅根据 fast CVD 决定方向。 - Gate3:鲸鱼否决门 - BTC: - ✅ 使用 `whale_cvd_ratio` 或 fallback 到 `market_indicators["tiered_cvd_whale"]`; - ✅ 条件: - LONG 且 whale_cvd_ratio < -阈值 → 否决; - SHORT 且 whale_cvd_ratio > 阈值 → 否决。 - ALT: - ✅ `recent_large_trades` 中,对立大单与同向大单的判定逻辑是否正确; - ✅ 只有“有对立大单且无同向大单”时才否决,且金额阈值是否为 whale_usd_threshold × price。 - Gate4:OBI 否决门 - ✅ OBI 采样顺序是否为:先用实时 `rt_obi`,否则用 DB 中 `obi_depth_10`; - ✅ 条件: - LONG 且 obi_raw < -obi_veto → Gate 拒绝; - SHORT 且 obi_raw > obi_veto → Gate 拒绝。 - Gate5:期现背离否决门 - ✅ 采样顺序:先用实时 `rt_spot_perp_div`,否则用 DB 中 `spot_perp_divergence`; - ✅ 条件: - LONG 且 divergence < -spd_veto → Gate 拒绝; - SHORT 且 divergence > spd_veto → Gate 拒绝。 ### C3. gate_block / gate_passed 对评分和信号的影响 - ✅ 任意一门触发后,`gate_block` 是否被设置为对应字符串,`gate_passed=False`。 - ✅ 评分计算完后,如果 `gate_passed=False`,是否将 `total_score` 强制置为 0。 - ✅ `result["signal"]` 在 gate_passed=False 或 no_direction=True 时是否总是 None。 - ✅ `factors` 中是否仍然保留原始四层分数和 Gate 细节,方便事后分析。 ### C4. 四层评分计算细节 - Direction Layer: - ✅ CVD 共振基础分数(例如 30 分)是否与 full-spec 一致; - ✅ p99_flow:有同向大单 / 无大单 / 有对立大单 三种情况的得分是否符合预期; - ✅ accel_bonus:cvd_fast_accel 与方向同向时报 5 分; - ✅ v53_fast 的独立加速路径(不要求双线共振)是否正确实现。 - Crowding Layer: - ✅ long_short_ratio 与 top_trader_position 的区间划分与得分是否符合 full-spec; - ✅ crowding_score capped 到 25 分。 - Environment Layer: - ✅ ALT(或默认)路径:只用 OI 变化率映射出 environment_score_raw; - ✅ v53_fast 额外的 OBI bonus:强/弱阈值与得分 5/3 是否正确; - ✅ 总分 capped 到 15。 - Auxiliary Layer: - ✅ coinbase_premium 的区间与得分(5/2/0)是否正确实施。 - 汇总: - ✅ total_score = 四层得分之和 capped 到 100,并四舍五入到 0.1; - ✅ 如果 `gate_passed=False`,最终 total_score 是否强制为 0。 ### C5. 权重的实际使用情况 - ✅ 当前实现已经使用 `weights` 来调整各层最大分/权重(direction/env/aux/momentum),需要确认缩放规则是否符合预期; - ✅ future:如要进一步调整权重逻辑,需要设计迁移/验证方案,并评估对历史统计的影响。 ### C6. signal 生成与冷却(entry / flip / cooldown) - ✅ 条件逻辑: - total_score >= flip_threshold → `tier="heavy"`; - entry_score <= total_score < flip_threshold → `tier="standard"`; - total_score < entry_score → 不开仓。 - ✅ 冷却: - COOLDOWN_MS = 10 分钟; - 以 `strategy_name` 为 key 的 last_signal_ts 是否正确记录; - 冷却期内不再生成新 signal。 - ✅ `direction` 限制: - 策略 direction=long/short/both 是否在最终生成 signal 和 signal_flip 时被应用: - 只多策略:不会生成空头 signal,也不会因为空头评估方向触发 signal_flip 平仓; - 只空策略:反之同理; - both:行为与原来一致。 ### C7. `signal_indicators` 落库字段映射 - ✅ ts:是否使用当前 `now_ms`,单位为毫秒。 - ✅ symbol:与当前 `SymbolState.symbol` 一致。 - ✅ strategy:写入 `cfg["name"]`(legacy 名称)。 - ✅ strategy_id:写入 `cfg["strategy_id"]`(uuid string)。 - ✅ strategy_name_snapshot:写入 `cfg["strategy_name_snapshot"]`。 - ✅ 所有指标字段: - cvd_fast / cvd_mid / cvd_day / cvd_fast_slope; - atr_5m / atr_percentile / atr_value; - vwap_30m / price; - p95_qty / p99_qty; - cvd_fast_5m(仅 v53_fast 有值); 是否与 `snapshot` / 重新计算的一致。 - ✅ factors(JSON): - 是否包含 gate_passed / gate_block / atr_pct_price / obi_raw / spot_perp_div / whale_cvd_ratio; - direction / crowding / environment / auxiliary 四个子对象的 score/max 等字段是否齐全。 ### C8. `signal_feature_events`(如仍在使用) - ✅ 只对 v53 系列策略写入(策略名以 "v53" 开头)。 - ✅ 原始字段(cvd_fast_raw 等)是否与 `result` 中的一致。 - ✅ score_direction / score_crowding / score_environment / score_aux 是否与 factors 中一致。 - ✅ gate_passed / block_reason 是否正确记录,便于后期分析。 ### C9. `paper_open_trade` 模拟盘开仓逻辑 - ✅ ATR <= 0 时是否直接拒绝开仓,避免无意义 SL/TP。 - ✅ SL/TP 价格计算: - LONG:`SL = price - risk_distance`,`TP1 = price + tp1_ratio × risk_distance`,`TP2 = price + tp2_ratio × risk_distance`; - SHORT:`SL = price + risk_distance`,`TP1 = price - tp1_ratio × risk_distance`,`TP2 = price - tp2_ratio × risk_distance`; - 旧版 JSON 策略仍按 `tp*_mult × ATR` 方式计算,属于兼容分支; 是否与 full-spec 中“以 R 计目标”的定义一致。 - ✅ risk_distance = sl_multiplier × ATR 是否正确计算并写入。 - ✅ SL 合理性校验: - 实际 SL 距离是否在 [0.8, 1.2] × risk_distance 之间; - 不满足时是否拒绝开仓并打日志(避免奇怪配置)。 - ✅ 模拟盘全局开关: - `PAPER_TRADING_ENABLED` / `PAPER_ENABLED_STRATEGIES` / `PAPER_MAX_POSITIONS` 是否生效; - 同一策略是否有 per-strategy 的最大持仓控制(如无,需要记录)。 - ✅ 写库字段: - symbol / direction / score / tier / entry_price / entry_ts; - tp1_price / tp2_price / sl_price / atr_at_entry; - score_factors = factors JSON; - strategy = `cfg["name"]`; - strategy_id / strategy_name_snapshot; - risk_distance; 是否全部正确写入,对应字段类型与含义与 full-spec 一致。 ### C10. 重复开仓与持仓交互 - ✅ 当前设计是否允许“同一策略在同一 symbol 上同时有多笔持仓”(分批进场)。 - ✅ 是否存在逻辑限制“同一策略同方向仅允许一笔活动持仓”,如果没有,需要明确这一点。 - ✅ 与 `paper_monitor` 的交互: - 反向 signal 是否会触发 signal_flip 平仓; - 平仓后冷却和再开仓的行为是否符合预期。 ### C11. 通知与其他 side-effect - ✅ 当 `result["signal"]` 非空时,是否总是触发 `NOTIFY new_signal`,payload 格式是否是: `"symbol:strategy:signal:score"`。 - ✅ 当 gate_block 不为空(gate_passed=False)或 no_direction=True 时,是否绝不会发 NOTIFY。 - ✅ 如果 live_executor 等进程依赖该 payload,是否已经约定好格式且长期不变。 --- ## 6. 验证优先级建议 后续可以按下面优先顺序逐项打勾: **P0(必须最先验证的)** - A2:CVD 多窗口计算(5m/15m/30m/1h/4h 全链路)。 - A3+A9:ATR / ATR 百分位 / 波动率门。 - A5+A6:巨鲸、OBI、期现背离的数据源与符号方向。 - C2+C3+C4+C6:五个 Gate + 四层评分 + entry/flip + cooldown 的交互。 - C7+C9:`signal_indicators` / `paper_trades` 落库字段,特别是 strategy_id / strategy_name_snapshot 与 SL/TP/risk_distance。 **P1(中优先级)** - B2~B7:`strategies` 表字段映射完整性、CVD 窗口、Gate 参数、方向限制。 - C1:per-symbol 遍历逻辑与 symbol 过滤。 - C8:`signal_feature_events` 的字段映射(如还在使用)。 **P2(相对低优先级)** - 对权重缩放逻辑的长期行为做验证(C5),确保与预期一致; - 日志/监控是否足够支撑线上排查; - 未来如需长期在线学习,可在此基础上加更多验证点。 --- ## 7. 使用方式(给人类 / AI 的操作建议) - 把本文件当作 **审计 checklist**: - 每次改动前,先在这里找相关条目,确认“期望行为”是什么; - 审计现有代码时,对照这些条目逐项比对,实现与期望不一致的地方要记录为 bug。 - 对于 P0 条目,建议配合: - 真实历史数据回放(从数据库抽样一段时间); - 针对单一策略 / 单一 symbol 的离线重算脚本(对比 signal_indicators / paper_trades)。 - 改动任何 C 层(评分/开仓/落库)逻辑时,必须同时: - 更新 `docs/arbitrage-engine-full-spec.md` 中对应章节; - 在本验证清单中勾选/更新受影响的条目; - 附上简短的“为什么这样改 + 对历史行为的影响”说明。 > 一句话: > 以后任何人(包括 AI)想动 signal_engine / 策略工厂,都应该先看 full-spec,再看这份验证清单, > 确保“为什么这么算”和“有没有算对”都在一个有记录的地方。