这页不是回测页,也不是只看结果的 Dashboard。它专门把 信号 → 选币 → 新鲜度 → 风控 → 下单 → 挂 TP/SL → timeout → 状态文件 整条链路透明化,方便你以后更快定位 bug 和理解能力边界。
页面生成时间:2026-05-13 06:03:44 北京时间 / 2026-05-12 22:03:44 UTC
先记住一句最重要的:扫描最近 72h 信号,但只允许 <= 3 分钟的信号真正进入交易队列。这就是“陈旧信号”问题最核心的透明化结论。
先看这张图,知道 32b 实盘现在到底是怎么从一根 K 线走到一笔交易的。后面每一步都有展开说明。
如果你只想先搞懂 32b 的 alpha 到底在“看什么”,先看这张图。它回答的是 哪根 15m bar 会被认成 long/short 信号,还没进入 strongest-only、freshness、risk、下单这些执行层。
32b 不是裸追涨杀跌。它先用 1h EMA fast/slow 定结构方向,再要求 15m close 穿越 fast EMA,同时要求 EMA slope 够强,才把这根 bar 记成可交易信号。
这张图只解释 alpha 信号如何触发。信号触发后,是否真的变成交易,还要继续经过 same-bar strongest-only、signal_too_old、风控、并发和下单链路。
ema_fast_1h > ema_slow_1h 只允许看多;ema_fast_1h < ema_slow_1h 只允许看空。15m close 从 fast EMA 一侧重新穿回另一侧,形成一次 close-confirmed cross。fast_slope 和 slow_slope 不只是同向,而且达到最小斜率门槛,避免把太弱的均线结构也当 continuation。slope_strength = |fast_slope| + |slow_slope| 不是新信号条件,而是多个币同窗竞争时用来选“谁更强”。这一步在做什么:每轮先同步 pending/live,再看新的交易候选。
为什么重要:先同步状态,避免系统以为自己 flat,但交易所里其实还有仓或挂单。
排障先看:phase6_last_run_summary.json 的 latest_evaluated_bar_time、managed_pending_entries、managed_live_positions。
源码位置:ops/systemd/momentum-rank32b-canary-phase6.timer + run_rank32b_canary_phase6.py main()
这一步在做什么:Signal adapter 从最近窗口里捞出候选信号,附带 ATR14、slope_strength。
为什么重要:这里负责“看到什么机会”,不是最终是否能交易。
排障先看:phase6_status.json 的 recent_signal_count、phase6_events.jsonl 里的 SignalReceived。
源码位置:src/momentum/execution/canary32b/signal_adapter.py:80
这一步在做什么:同一 timestamp 多币同时触发时,只保留 strongest 一个。
为什么重要:把策略从“多币全追”收敛成“同窗只打最强”。
排障先看:phase6_last_run_summary.json 的 skipped_weaker_signals、recent_rejections 的 weaker_than_strongest_signal_in_same_bar。
源码位置:run_rank32b_canary_phase6.py:201
这一步在做什么:即使 adapter 扫到了旧信号,只要超龄,也会被 signal_too_old 拒绝。
为什么重要:这是修复陈旧信号 bug 的关键逻辑。
排障先看:phase6_recent_rejections.json 是否出现 signal_too_old;payload 里看 signal_age_seconds。
源码位置:run_rank32b_canary_phase6.py:1554-1602
这一步在做什么:检查 symbol、并发、live/pending、ATR、data_delay、API 健康等。
为什么重要:它回答的是“这个信号能不能真的变成一笔交易”。
排障先看:recent_rejections 的 reason;phase6_last_run_summary.json 的 risk_rejections。
源码位置:src/momentum/risk/canary32b_guard.py:53
这一步在做什么:当前 live 默认走 MARKET;如果未来改回 limit_gtx,则可能走 TTL 后 fallback。
为什么重要:这里决定 admission/fill 确定性与成本结构。
排障先看:phase6_recent_orders.json 的 order_role=entry;events 里的 ORDER_PLACED / PositionOpened。
源码位置:run_rank32b_canary_phase6.py:1720+ / 1155+
这一步在做什么:入场后立即挂 STOP_MARKET 止损 + LIMIT 止盈,并设置 timeout。
为什么重要:这是 live 风险闭环;任何 attach 失败都要被重点监控。
排障先看:warnings.json、recent_orders、recent_closed_trades 的 exit_reason。
源码位置:run_rank32b_canary_phase6.py:558
这一步在做什么:所有页面、巡检、邮件、排障都依赖这些文件。
为什么重要:如果状态文件不透明,后面就只能靠猜。
排障先看:phase6_status.json、phase6_state.json、phase6_events.jsonl。
源码位置:run_rank32b_canary_phase6.py run end checkpoint + dashboard builders
Runner 大约每 5 分钟跑一次,但信号本身仍然是按 15m bar 来定义。每次 run 会先同步 pending/live/交易所状态,再看新的交易机会。
Signal adapter 会为 2 个币读取 bars、构建 frame、计算 ATR14,并在最近 72 小时窗口内找出满足 ema_cross_plus_slope_floor 的信号,同时产出 slope_strength、atr14、signal_id 等元数据。
若多个币在同一个 timestamp 同时出信号,系统只保留 slope_strength 最强的那个,其余信号会被记录成 weaker_than_strongest_signal_in_same_bar,而不是“静默消失”。
这里是陈旧信号 bug 的核心修复点:即便 adapter 扫描最近 72h,真正进入交易决策的信号也必须满足 now - signal.timestamp <= 3 分钟。超龄信号会被记为 signal_too_old,并写入 rejections / warnings。
风控会依次检查:交易开关、kill switch、symbol 是否在 universe、同币是否已有 live/pending、总并发是否超限、是否达到日内交易数上限、API 健康、data delay、ATR 是否可用等。任何一条不过,都会给出明确 reject reason。
当前是“六选一、单席位”模型:Universe 为 BTCUSDT, ETHUSDT,每轮最多只处理 1 个新信号,同时总并发仓位上限为 1。这意味着它更像“扩机会池”,而不是“6 个币一起上仓”。
当前 live 默认是市价单入场。 如果以后切回 limit_gtx,则会先挂限价单,等到 TTL 到期后,若配置允许,可自动 fallback 到 market。这里要特别注意:phase6.entry 才是当前 live 的主配置入口。
一旦 entry 成交,系统会立即尝试挂:STOP_MARKET 止损 + LIMIT 止盈,并记录 timeout 时间。若既没 hit TP、也没 hit SL,到 120 分钟会走 timeout_market 市价平仓。
每轮 run 结束后,phase6 会写出状态、最近订单、最近仓位、最近平仓、最近拒绝、warnings、以及带 checkpoint 的 phase6_state.json。这就是你以后定位“策略到底在想什么”的透明化基础设施。
现在的逻辑已经把“历史扫描窗口”和“真实可交易新鲜度”拆开了:
72h<= 3msignal_too_oldexecution.entry 这块配置还留着旧 phase/辅助语义,容易让人误以为 live 还在用 post-only maker-first 入口。当前真正 controlling live 入场行为的是 phase6.entry。
这不是历史大盘,而是 最近一轮 phase6 自己留下的事件时间线。以后你想知道“这轮到底做了什么”,先看这里,不用先翻日志。
本次 run:2026-05-04 23:37:03 北京时间 / 2026-05-04 15:37:03 UTC → 2026-05-04 23:37:05 北京时间 / 2026-05-04 15:37:05 UTC
最近一次 run 没有留下可展示的逐步事件,通常意味着这轮只是常规同步,没有新 signal / intention / order / rejection。
你之前最痛的 bug 就卡在这里,所以这块我单独拆清楚:
signal_age_seconds = now - signal.timestampsignal_age_seconds > 180,则拒绝交易,并记录成 signal_too_old所以“最近 72h 扫描”本身不等于“最近 72h 都能下单”。真正能下单的是:最近 3 分钟内、未 seen、未 same-bar consumed、且风控放行的信号。
MARKETSTOP_MARKETLIMITMARKETlimit_gtx15mMARKET| 配置项 | 当前值 |
|---|---|
| 运行模式 | live_canary |
| Universe | BTCUSDT, ETHUSDT |
| Signal variant | ema_cross_plus_slope_floor |
| Signal lookback days | 30 |
| Signal scan window | 72h |
| Trade freshness gate | <= 3m |
| Selection | strongest_signal_only / slope_strength |
| Max new signals per run | 1 |
| Max concurrent positions | 1 |
| Require ATR | 是 |
| Entry order type | market |
| Entry TTL | 15m |
| Limit TTL fallback to market | 是 |
| Desired notional | 20.00 USDT / symbol |
| Default leverage | 1x |
| TP / SL / Timeout | 1.00 ATR / 1.00 ATR / 120m |
| Safety pause cooldown | 0m (当前关闭) |
| Data delay coarse limit | 259200s ≈ 72.0h |
| 风险点 | 为什么容易出问题 | 你该先看什么 |
|---|---|---|
| 历史信号 backlog | 当前 adapter 仍扫描最近 72h;真正防 bug 的是 3m 新鲜度门。 | 优先看 phase6_recent_rejections.json 是否出现 signal_too_old |
| 同窗多币竞争 | 同一 timestamp 只留最强 slope_strength;其余会被 weaker_than_strongest_signal_in_same_bar 拒掉。 | 不要把“没下 ETH”误判成“没看到 ETH 信号” |
| 单席位限制 | max_concurrent_positions=1;有 live/pending 后,后续新币信号会被 too_many_positions 拦截。 | 看 recent_rejections 和 phase6_state |
| entry 配置双轨 | 当前 live 真正使用的是 phase6.entry.order_type=market;execution.entry 更像旧 phase/辅助配置。 | 改配置时优先改 phase6.entry,避免改错块 |
| 外部账户干扰 | 交易所里存在非 whitelist 仓位时,Dashboard 可能提示 unexpected_exchange_positions。 | 它不会自动替你清仓,但会影响保证金和判断 |
| 安全暂停 | 当前已关闭;若未来打开,只会看当前 code_version 之后发生的 attach-failure。 | 旧版本失败不应继续封锁新版本测试 |
| 文件 | 它回答什么问题 |
|---|---|
phase6_status.json | 当前 live 总状态:latest bar、trade_enabled、kill_switch、system_health |
phase6_last_run_summary.json | 最近一轮到底看到了几个信号、处理了几个、下了几笔单、有没有 safety pause |
phase6_state.json | seen_signal_ids / consumed_signal_bars / pending_entries / live_positions / closed_trades 的真实 checkpoint |
phase6_recent_rejections.json | 为什么没交易:signal_too_old / same_bar / risk reject / safety pause |
phase6_recent_orders.json | 下单明细:entry / stop_loss / take_profit / timeout_close |
phase6_recent_closed_trades.json | 已平仓明细:exit_reason / pnl / fee / code_version / config_version |
phase6_warnings.json | attach 失败、query 失败、外部仓位干扰等告警 |
phase6_events.jsonl | 最细的逐步事件时间线,适合还原一次 run 的全过程 |
| 逻辑块 | 源码位置 | 说明 |
|---|---|---|
| 信号扫描 | src/momentum/execution/canary32b/signal_adapter.py:80 | load_recent_signals():扫 recent_hours 内的候选信号,并生成 signal_id / atr14 / slope_strength |
| 同窗最强选择 | scripts/run_rank32b_canary_phase6.py:201 | select_signals_for_execution():同一 timestamp 只保留 slope_strength 最强的币 |
| 风险拦截 | src/momentum/risk/canary32b_guard.py:53 | evaluate_entry_risk():trade_enabled / kill_switch / 并发 / ATR / data_delay / API 健康等 |
| 安全暂停 + 新鲜度 | scripts/run_rank32b_canary_phase6.py:1524-1602 | 先检查 exit_attach failure 安全暂停,再做 signal_too_old 新鲜度过滤 |
| 挂单成交 / TTL fallback | scripts/run_rank32b_canary_phase6.py:1155 | manage_pending_entries():limit_gtx 若超时,可 fallback 市价进场 |
| 入场后挂退出计划 | scripts/run_rank32b_canary_phase6.py:558 | attach_exit_plan():挂 STOP_MARKET 止损 + LIMIT 止盈 + timeout 时间 |
目标不是把页面做得花,而是把“策略现在到底怎么工作”说人话、说透明、说能排障。