Claude Cursor Skill

5minbtc

BTC 5分钟K线实时方向预测 + 币安预测市场错价套利(paper)。v6.0 真OFI驱动: 方向由当前K线原生 in-candle OFI 净流一票决定(ofi_n=2*(tb/v)-1, REST klines[9] 聚合, 零WS依赖; WS ofi.json 做新鲜度反转保护与交叉校准), 概率=P(close>open|ofi) 经验校准表+Bayesian shrink, edge=EV=p−ask 错价检测, 方向二选一无中性。13因子自v5.9起收敛到3个有证据信号且不再决定bias。黑天鹅防护: ATR spike+FNG<25。l

LLM Mart · 0 points · 1 views 17 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download aaaaqwq-agi-super-team-skills_5minbtc-cdb04e8.zip · 2658 KB
Part of aaaaqwq/agi-super-team — 46 skills

Install

skills CLI npx skills add https://github.com/aAAaqwq/AGI-Super-Team/tree/main/skills/5minbtc
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aaaaqwq-agi-super-team@llmmart
Git git clone https://github.com/aAAaqwq/AGI-Super-Team.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole aaaaqwq/agi-super-team collection as a plugin from our marketplace. Git is the plain clone.

README

5minbtc — BTC 5 分钟实时方向预测引擎

引擎 + LLM 混合架构:真 OFI 净流定方向 + 错价检测定 edge,LLM 综合裁决,预测 BTC 单根 5 分钟 K 线的方向与收盘价区间。

⚠️ v6.0 起方向不再来自因子打分 — 13 个价量因子中 11 个经公平回测为 47–49%(等同抛硬币),现仅保留输出供参考,方向由当前 K 线原生 OFI 净流一票决定。详见 对抗式审查报告

⚠️ NOT FINANCIAL ADVICE / 非投资建议 — 本项目仅为量化研究与学习目的,不构成任何投资、交易或持仓建议。预测存在显著误差,实际交易将面临资金损失风险。详见文末 免责声明

Engine Python Deps


目录


核心理念

5minbtc 解决一个具体问题:在每根 5 分钟 BTC K 线进行到 ~60–80% 时,预测这根 K 线收盘时的方向和价格区间

它不是全自动交易机器人,而是一个 引擎 + LLM 混合决策系统

   量化引擎 (12 因子 + ATR 归一化)        LLM (新闻 + 因子 + 裁决规则)
   ┌─────────────────────────┐           ┌──────────────────────────┐
   │ 4 路并行拉取市场数据      │  JSON →  │ 3 路并行新闻搜索          │
   │ 12 正交因子打分           │ ──────→  │ 综合裁决 (可微调引擎结论)  │
   │ Platt Scaling 置信度      │           │ 模板化输出 (15–25 行)      │
   └─────────────────────────┘           └──────────────────────────┘
         纯 Python 标准库                       可由 cron 调度

为什么要混合? 回测证明:纯价格因子在公平(无前视)条件下无显著预测力(47–49%)。实盘的 edge 主要来自 K 线后段(progress ≥ 0.9)的"已确认走势延续"以及新闻冲击。把数值因子交给 LLM 做上下文综合,比纯规则或纯 LLM 都更稳健。

系统架构

组件

组件 文件 职责
引擎 5minbtc-engine-v6.0.py 拉数据 → 计算 OFI(ofi_n=2*(tb/v)-1) → OFI 一票定方向 + 概率 P(close>open\|ofi) → 输出预测 JSON(stdout)。13 因子仍计算但只作参考
新闻 5minbtc-news.py 抓取 CoinDesk RSS,输出风险等级到 data/news-risk-level.json
日志 5minbtc-log.py 预测记录追加(jsonl)+ 增量 settle(结算上一根 K 线实际结果)
回测 backtest/*.py 历史数据回测,多版本(v5.6 / v5.7 / v5.8)
LLM (外部) 读引擎 JSON + 新闻,综合裁决并输出

数据流(单次预测)

cron 触发 (每根 5min K线 第4分钟)
   │
   ├─ 5minbtc-log.py settle-all      ← 结算上一根 K 线(写入实际收盘)
   │
   ├─ 5minbtc-engine-v6.0.py         ← 引擎核心
   │     │
   │     ├─ ThreadPoolExecutor(4) 并行:
   │     │    ├─ Binance  Klines   (200 根 5min K线)
   │     │    ├─ Binance  Depth    (订单簿 top-20)
   │     │    ├─ alternative.me FNG (恐惧贪婪指数)
   │     │    └─ Coinbase  BTC-USD  (Chainlink 参考)
   │     │
   │     ├─ 指标: EMA9/21, RSI, MACD, Bollinger, Wilder ATR, Vol-Regime
   │     ├─ 12 正交因子 → score → bias / confidence
   │     ├─ 黑天鹅过滤 (ATR spike / FNG<25 / 新闻熔断)
   │     └─ Chainlink 偏移补偿 (±$300 上限)
   │
   ├─ 5minbtc-news.py                 ← 写 news-risk-level.json
   │
   └─ LLM 读取引擎 JSON + 3 路新闻 → 裁决 → 写日志

引擎单次执行约 ~3 秒(v5.7.3 并行化后,原串行 ~11–18 秒)。

12 正交因子体系

v5.0 起用正交因子替代共线指标,所有阈值经 ATR 归一化,避免波动率变化导致阈值失效。

因子 类别 含义
momentum 趋势 EMA9−EMA21 差值,ATR 归一化的 t-stat
meanrev 均值回归 偏离布林带中轨程度
rsi 动量 Wilder RSI 的 Z-score
volume 量能 条件化放量信号(区分突破 vs 衰竭)
fatigue 衰竭 趋势动能衰减检测
imbalance 微结构 订单簿买卖盘失衡
microprice 微结构 Stoikov microprice 偏离 mid
decel 衰竭 价格变化减速(动量冲突时动态降权)
position 位置 K 线在布林带中的相对位置
v_reversal 反转 低点抬高 + 收>开 的 V 型反转模式
vol_breakout 突破 最近 3 根完成 K 线的放量突破
half_body 半 K 线 核心 edge:progress≥45% 时已形成 body 的延续性

权重由 BASE_W 表给定,并随波动率 Regime(TREND / RANGE / HIGH_VOL / LOW_VOL)动态调整。

半 K 线预测策略(Half‑Candle)

这是 v5.7 的核心贡献,也是实盘 edge 的正式建模:

  • S1:在 K 线进度 ≥ 45% 时激活 half_body 因子——已经形成的 body 方向在剩余时间内倾向于延续。
  • S2:预测范围收窄——ATR 乘数 × 0.55,只预测剩余 ~55% 时间内的波动。
  • S3:调度时机从每根 K 线第 2 分钟推迟到第 4 分钟(progress ~60–80%),让前半段充分形成信号再预测后半段。

实证:回测中 11 因子组合无预测力(47–49%),但实盘 66% 的胜率主要来自 progress=0.9+ 时"确认已有走势"——half_body 把这个隐性逻辑变成了显式因子。

黑天鹅三重防护(v5.7.1)

引擎在输出预测前进行三层过滤,任一触发即压低置信度或转为 neutral:

  1. ATR Spike 检测:当前 ATR 相对历史出现尖峰时,方向可靠性下降。
  2. FNG < 25 过滤:恐惧贪婪指数低于 25(极度恐慌)时标记 fng_black_swan
  3. 新闻冲击熔断5minbtc-news.py 写入 BLACK_SWAN/CRITICAL 时,引擎强制 bias=neutral、confidence=30。

数据源

全部为公开、免认证接口,零 API Key:

数据 来源 用途
K 线 data-api.binance.vision/api/v3/klines 200 根 5min OHLCV
订单簿 data-api.binance.vision/api/v3/depth top-20 bids/asks
恐惧贪婪 api.alternative.me/fng/ 黑天鹅过滤
参考价 api.coinbase.com/v2/prices/BTC-USD/spot Chainlink 偏移补偿
新闻 www.coindesk.com/.../rss/ 风险等级(~14min 延迟)

快速开始

依赖:仅 Python 3.8+ 标准库,无需 pip install 任何第三方包。

# 单次运行引擎(输出预测 JSON 到 stdout)
python3 5minbtc-engine-v6.0.py

# 单独跑新闻扫描
python3 5minbtc-news.py

# 结算上一根 K 线 + 追加新预测
python3 5minbtc-log.py settle-all
python3 5minbtc-log.py log "<candle.iso>" <pred_close> <pred_high> <pred_low> \
    <conf> <bias> <news_sent> <vol_pct>

引擎输出示例(节选):

{
  "version": "5.7.3",
  "candle": { "progress_pct": 72.3, "...": "..." },
  "indicators": { "ema9": 61950.1, "rsi": 47.2, "atr": 85.4, "...": "..." },
  "factors": { "half_body": -0.31, "momentum": -0.42, "...": "..." },
  "regime": "TREND",
  "news_risk": "NORMAL",
  "prediction": {
    "bias": "bear", "strength": "moderate",
    "confidence": 48, "pred_close": 61920, "pred_high": 61960, "pred_low": 61870
  }
}

完整运行流程

完整的"引擎 + LLM"混合流程由 cron 每 5 分钟触发:

SKILL_DIR=/path/to/5minbtc

# 1. 并行执行:结算 + 引擎 + 新闻
python3 $SKILL_DIR/5minbtc-log.py settle-all
python3 $SKILL_DIR/5minbtc-engine-v6.0.py
python3 $SKILL_DIR/5minbtc-news.py

# 2. LLM 侧:3 路并行新闻搜索
#    - "Bitcoin BTC breaking news price"
#    - "crypto market macro stocks today"
#    - "比特币 BTC 最新 晚间"

# 3. LLM 综合引擎 JSON + 新闻 → 按模板输出(15–25 行)

# 4. 写入预测日志
python3 $SKILL_DIR/5minbtc-log.py log "<candle.iso>" <pred_close> ...

💡 用 cp .env.example .env 并设置 SKILL_DIR 指向你的克隆目录,可避免把路径写死。详见 配置说明

目录结构

5minbtc/
├── 5minbtc-engine-v6.0.py     # 主引擎(cron 调用,输出预测 JSON)
├── 5minbtc-engine-v5.py       # v5 旧版(回测因子模块 import 用)
├── 5minbtc-news.py            # 新闻扫描(CoinDesk RSS)
├── 5minbtc-log.py             # 日志记录 + settle
├── SKILL.md                   # 技能索引文档(Hermes/Claude skill 格式)
├── README.md                  # 本文件
├── .env.example               # 配置模板
├── backtest/
│   ├── fetch_data.py          # Binance 历史数据下载
│   ├── run_backtest.py        # v5.6 回测(公平,因子无预测力)
│   ├── run_backtest_v57.py    # v5.7 回测(含已知前视偏差)
│   ├── run_backtest_v58.py    # v5.8 回测(真实 1min 半 K线,零前视)⭐
│   ├── run.sh                 # 一键运行
│   ├── data/                  # 缓存的 K 线 JSON
│   └── results/               # 回测结果(.gitignore)
├── references/                # 深度文档(架构/教训/pitfalls/黑天鹅…)
├── reviews/                   # 每日复盘报告(按月归档)
├── scripts/                   # 辅助脚本
└── data/                      # 运行时数据(新闻风险等级,.gitignore)

回测系统

cd backtest

# 一键回测(下载最新数据 + 运行)
./run.sh

# 快速模式(每小时采样,最近 180 天)
./run.sh --fast

# 最近 90 天,每 6 根 K 线采样
./run.sh --days 90 --sample=6

# 完整因子贡献分析
./run.sh --full-report

# 模拟 K 线进度(progress=0.5 = 中段)
python3 run_backtest.py --progress=0.5

公平性保障

  • 无前视偏差(当前 K 线价格信息被屏蔽:c=h=l=open, v=0)
  • 因子仅基于 200 根已完成 K 线计算
  • 回测中订单簿不可获取,imbalance/microprice 设为 0

详见 backtest/README.md

性能快照

以下为历史实测数据,过去表现不代表未来收益,且小样本下统计显著性有限。

版本 设定 方向准确率
v4.x 实盘 62.5%
v5.7.1 实盘 273 轮 75.7% (+13.2pp)
v5.8 回测(前 2 根 1min) 61.4%
v5.8 回测(前 4 根 1min) 69.5%

⚠️ 见 references/backtest-findings.md:公平回测下纯价格因子无预测力。实盘高胜率很可能部分来自小样本 + K 线后段信息优势,并非稳定 edge。在 edge 来源明确前,不应按高胜率预期下注。

配置说明

当前引擎不读取任何环境变量——所有参数(symbol、interval、HTTP timeout、ATR 乘数、因子权重)均硬编码在 .py 源码中,这保证了 cron 调用的零配置可复现性。

.env.example 列出了建议配置化的项(当前为预留/文档用途),供希望二次开发的用户参考。如需让引擎实际读取这些变量,需要修改对应源码(欢迎 PR)。

主要硬编码常量位置:

常量 默认值 位置
symbol BTCUSDT fetch_klines() / fetch_depth() 形参
interval 5m fetch_klines() 形参
HTTP timeout 510 s urlopen() 调用
ATR 乘数 0.55 predict_close_v5()
BASE_W 因子权重表 模块级常量
REGIME_ADJ Regime 权重调整 模块级常量

已知局限

  1. 回测与实盘的差距:公平回测下因子无预测力,实盘 edge 主要来自 K 线后段信息优势(可能不稳定的"半前视")。
  2. 订单簿简化:仅用 top-20 深度,未实现完整的 OFI(Cont et al. 2014)与 microprice 模型。
  3. 新闻延迟:CoinDesk RSS 约 14 分钟延迟,无法捕捉瞬时冲击。
  4. SSL 验证禁用:引擎全局设置 ssl._create_unverified_context,是为对抗高延迟网络下的 SSL 握手超时(见 references/high-latency-network-handling.md)。在可信网络环境下可安全恢复验证。
  5. 无自动交易:本项目只输出预测,不执行任何下单。

路线图

  • OFI 微结构因子(v6.0 已落地):引擎现由真 OFI 净流一票决定方向 + P(close>open|ofi) 概率, 见 SKILL.md 架构strategy-adversarial-review.md
  • 🟡 HMM Regime 检测 + 自适应仓位
  • 🟡 LightGBM 自动化因子筛选
  • 🟡 CVaR 动态止损
  • 🟢 TFT 多时间尺度模型替代贝叶斯引擎

references/architecture.md(13 因子模型 + 升级路线图)已于 2026-09-10 删除: 其"13 正交因子"描述被 v5.9 清零、OFI 也已从路线图变为已实现,内容已由 changelog.mdSKILL.md 取代。

  • 🟢 Deribit 期权 IV 信号

免责声明 (NOT FINANCIAL ADVICE)

本项目是量化研究与教育项目,不构成投资建议、交易信号或任何形式的财务建议。

  • 加密货币市场波动剧烈,交易可能导致全部本金损失
  • 引擎预测存在显著误差,历史准确率不代表未来表现。
  • 作者与贡献者对任何基于本项目的交易决策不承担责任
  • 请在了解当地法律法规的前提下自行承担风险。

如本项目对你有帮助,欢迎 Star。但请勿将其视为稳赚的交易工具。

License

本项目暂未指定开源许可证。如需使用、修改或分发代码,请先联系作者或等待 License 文件添加。在未指定 License 前,根据默认版权法,代码保留全部权利(仅供阅读与学习)。

Skill manifest

5minbtc — BTC 5分钟实时预测 v6.0.0

BTC 单根 5min K线 方向 + 收盘价预测。引擎+LLM 混合架构。 SKILL.md 是索引, 详细内容见 references/ v5.9 认知转变: 从"13因子预测器"→"3信号错价检测器" — 方向准确率不是 edge, EV = p − P − 成本 才是 (见 对抗式审查报告) v6.0 执行转变: 方向从"body 延续统计"改成真 OFI 净流一票决定 — 赌的是"净流已发生、token 价还没定价"的 flow-gap

⚠️ 同名易混: 本 skill 只做 BTC 单标的 5min 方向 + 预测市场 EV。要做全市场永续的量价突破扫描, 用 coin-vp-scanner — 两者区别见下方 与 coin-vp-scanner 的分工

触发

5minbtc / 5min btc / btc 5min / 监控 (配合持续盯盘)

何时使用

场景 做法
当前 5min K线 方向+价位 ✅ 标准流程, 方向=OFI 净流, edge=错价 EV (v5.8 零前视回测 61.4% 是延续性不是 alpha, 见性能快照)
会话内持续盯盘, 等明确信号 ✅ 监控模式 (scripts/5minbtc-monitor.py + Monitor 工具)
币安预测市场 Up/Down 5m 交易 ✅ paper 模拟/实时监控 (见 预测市场策略)
"今晚 BTC 涨跌" (宽窗口) ⚠️ 跑当前 K线 + 给方向倾向, 标注"超出引擎置信区间"
"下根 K线" / "1小时后" 引导在该 K线 起始时间再触发

监控模式 (Claude Code)

详见 monitoring-claude-code.md

# 无限持续, 直到用户喊停 (会话内盯盘推荐)
Monitor(command: "python3 <SKILL>/scripts/5minbtc-monitor.py --max-runs 0", persistent: true)

# 默认 20 次采样 (约 40 分钟)
python3 <SKILL>/scripts/5minbtc-monitor.py

# 单次判断 (非持续)
python3 <SKILL>/scripts/5minbtc-monitor.py --dry-run
  • 明确信号 = bias 非中性 + strength∈{medium,moderate,strong} + conf≥50 → 自动停 (⚠️ 实测引擎 strength 输出 medium, 判定集合需同时含 mediummoderate)
  • 每根 K 线第 2/3 分钟采样 (progress ~40-70%), 比 cron 第 4 分钟更早
  • 事件流: START / DIR-CHANGE / CLEAR-SIGNAL / ENGINE-ERR / MAX-RUNS
  • 停止: 用户说「停/结束」→ TaskStop 停 Monitor
  • CLEAR-SIGNAL 后必须拉一次完整引擎快照二次确认
  • 引擎验证: python3 scripts/test_engine.py (13 项, 自带 runner 无需 pytest; 慢测加 --slow) — 装了 pytest 也可 python3 -m pytest scripts/test_engine.py -v (原文件名 test_engine_v58.py 带版本号, 引擎升到 v6.0 后即漂移, 2026-09-10 改为不带版本号)

快速开始

SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc

# 1. 并行: 引擎 + 新闻 + settle (前一根)
python3 $SKILL_DIR/5minbtc-log.py settle-all 2>&1
python3 $SKILL_DIR/5minbtc-engine-v6.0.py 2>&1
python3 $SKILL_DIR/5minbtc-news.py 2>&1

# 2. 3 路 web_search (并行)
# "Bitcoin BTC breaking news price" / "crypto market macro stocks today" / "比特币 BTC 最新 晚间"

# 3. LLM 分析 → 输出 (见 output-template.md)
# 4. 写日志
python3 $SKILL_DIR/5minbtc-log.py log \
  "<candle.iso>" <pred_close> <pred_high> <pred_low> \
  <conf> <bias> <news_sent> <vol_pct>

架构 (1 行/组件)

  • 引擎 5minbtc-engine-v6.0.py (v6.0, 输出 "version": "6.0.0"): bias 由真 OFI 净流一票决定 (ofi_n>0→bull / <0→bear, 二选一无中性; ofi_n 缺失/为0 用 body 符号兜底并标 meta.body_fallback) + 概率 P(close>open|ofi) 三层(经验校准表 Bayesian shrink + flow-gap + 最近60s流) + 三层独立过滤(多周期4h/1h/15m 结构 + 跨资产ETH/SOL 广度 + WS OFI 新鲜度反转保护) + 9路并行HTTP
    • ⚠️ K 线开局阶段概率不可信(已知问题,见 pitfalls):主源 ofi_n = 2*(tb/v)−1 取自当前进行中的 K 线,_ofi_native() 只挡 v<=0无最小样本量保护 → 开局几秒的少量成交就能把 ofi_n 推到 ±0.9。而 vol_gate(L692 按 MIN_VOL_FRAC=0.25 判断流量是否够)算出来但从不参与任何决策,只是输出里的装饰字段。建议只在进度 ≥40% 后采信概率/下单
    • 门限: T_OFI_GATE=0.20 / T_OFI_60=0.35 / WS 质量闸 OFI_CR_MIN=0.80 / OFI_BAYES_N=30
    • 13 因子仍计算并输出(JSON 契约保留), 但自 v6.0 起不参与 bias 决策 — score 只用于 strength 标签与 LLM 参考
  • 订单流 scripts/ofi_feed.py: trade+bookTicker 组合流 tick规则推断主动买卖, 写 ~/bb-auto/ofi.json, 带 ts 保鲜(>30s 引擎降级为只信 REST 主源) (launchd: com.daniel.ofi-feed)
  • 日志 5minbtc-log.py: jsonl 追加 + 增量 settle (写入 logs/)
  • 新闻 5minbtc-news.py: CoinDesk RSS (唯一稳定源, ~14min 延迟)
  • 常驻进程 (launchd): 5minbtc_realtime.py (5s 刷新, 预测快照+EV下单) / 5minbtc_watch.py / 5minbtc_trader.py --paper-monitor
  • LLM: 因子打分基准 + LLM 综合裁决 + 模板输出

铁律

  1. 每次必须重新执行引擎脚本 — 不缓存
  2. 每次必须重新搜索3组新闻
  3. 先 settle 上一根, 再 log 新预测
  4. LLM 可微调引擎的 pred_close/range, 但必须说明理由 ⚠️ bias 不要轻易覆盖 (v6.0): 方向是 OFI 一票决定的, LLM 用因子/新闻翻方向 = 退回被证伪的路径。 仅在 meta.body_fallback=True 或 OFI 数据缺失时才允许改 bias, 并显式写明原因。
  5. 输出 15-25 行 (平衡深度和 Telegram 可读性)

关键裁决规则 (⚠️ 历史规则: v5.7.x 因子打分路径)

v6.0 起 bias 由真 OFI 一票决定, 下列规则不再决定方向。 仅在两条兜底路径上仍有参考价值: ① meta.body_fallback=True (ofi_n 缺失/为0, 方向退回 body 符号) ② LLM 复核时解释 meta 冲突字段。 引擎的 raw score 仍输出, 但只影响 strength 标签, 不影响 bias。

  • half_body vs imbalance 冲突 (v5.7.2): |half_body|>0.25 + |imbalance|>0.5 + progress≥45% → 优先 half_body (注: 两者自 v5.9 起权重已清零)
  • TREND 强趋势 decel 约束 (v5.7.4): EMA delta>$100 时 |decel|>0.7 需 |half_body|>0.15 同向确认
  • fatigue≥0.8: conf 上限 40, 反向 +10pp
  • chainlink_offset 矛盾: bias=bull 但 pred_close<current → 以 current 为锚 ±ATR×0.3
  • 极端进度 (>80%): pred_close 按剩余时间比例缩放
  • Body=0 持续模式: pred_close → current ± ATR×0.2, conf 降至 35-42%

性能快照 (2026-09-10 更新)

数字口径以 对抗式审查报告 为准 — 旧数字多数含前视偏差。

  • v5.7 回测 71.2% 含前视偏差(文件里自己标注"已知"); 零前视 v5.8: 前2根1min=61.4%, 前4根=69.5%
  • 但 61–70% 是"看着 K 线走完再确认"的延续性, 不是 alpha — 做市商已把它定价进 token 价
  • 去掉水分后的真实基准: non-neutral 方向 57.7% | bull 63.9% (唯一显著) | bear 50.0% = 纯硬币
  • paper 交易: 全样本 UP 成交 44% (+$0.24) | 甜区 UP ask 0.40–0.50 → 60% (+$1.39) ← 唯一正 EV 区间
  • 结论: edge 不在方向准不准, 在 EV = p − P − 成本 > 0。v6.0 的错价检测(flow-gap)就是直接做这件事。
  • 当前 LLM: zai/glm-5.2 (8-15s/次) | opencaio 实测 MiniMax-M3 ~2.7s 可作更快选项

📚 引用索引 (references/)

核心方法论

  • strategy-adversarial-review.md对抗式审查报告: 第一性原理 + 13因子证伪 + 该留/删/缺失 + P0/P1/P2行动清单 (v5.9 依据)
  • lessons.md25 条核心教训 (必读, 含 2026-07-05 新增 23-25)
  • pitfalls.md20 条 pitfalls 集中索引 (必读, 含并行 max() 评估陷阱; #18 = K线开局概率不可信; #19 = "真OFI" 只覆盖单一市场; #20 = 端点故障致静默断档)
  • changelog.md — v5.0 ~ v6.0 详细变更
  • skill-organization.mdSkill 文件结构模式 (可复用) — SKILL.md INDEX + references/ 分专题

执行与输出

数据源 & 网络

Binance 三种故障模式各一份(431/451 区域、隧道抖动、高延迟 SSL 超时),不是重复文档,见 setup-from-scratch.md §6

引擎专项

回测 & 复盘

数据采集 & 仓库

  • sync-procedure.md — AGI-Super-Team 同步流程 (路径映射 + -c 校验 + logs 归档策略)
  • archive.md版本归档: v5.0→v6.0 沿革 + 哪些版本已不可取回 + 归档 SOP
  • setup-from-scratch.md从零搭建: 依赖/目录/密钥/6 个 launchd 服务/验证清单/日志轮转/故障表
  • scheduled-tasks.md系统级定时任务教程: 常驻型vs定时型 / 2 个任务 / launchd+cc-connect+hermes 三路线 / plist 模板 / 13 条坑
  • quant-knowledge-index.md — 50 轮蒸馏知识库索引

复盘记录

⚠️ reviews/ 目录本地不存在 —— 早期文件树声称有 23 份按月归档的复盘记录(2026-05/06/07),实际从未落盘。 复盘结论目前散落在 lessons.mdpitfalls.mdstrategy-adversarial-review.mdreports/ 里。 若要恢复这个习惯,需先补 review-procedure.md 的落盘步骤,别只在文档里声明。

报告库

14 份深度蒸馏报告在 reports/ 目录 (也同步在 AGI-Super-Team): R01-R14。

与 coin-vp-scanner 的分工

两个都是交易类 skill, 但赌的不是同一件事, 不要混用:

5minbtc (本 skill) coin-vp-scanner
标的 单标的 BTC 全量币安 USDT 永续
周期 单根 5min K线 1–30min 短线
核心逻辑 真 OFI 净流 + 错价检测 (净流已发生、token 价未定价 → flow-gap) 确定性结构突破 (放量 + 强实体 + 收盘破 1h 结构位 + 趋势同向, 四条全 ✓)
决策依据 概率 P(close>open\|ofi) vs 市场 token 价 → EV = p − P 结构规则布尔判定, 非概率
输出 方向 bull/bear + 概率 + edge 杠杆 / TP / SL 交易卡
落地方式 预测市场 Up/Down token, 仅 paper (LIVE_GATE 硬闸门) 直接做合约短线
代码位置 本目录自包含 (引擎+脚本+launchd) 代码在 ~/projects/coin-vp-scanner/, 本 skill 只是索引

一句话选型: 要赌"市场定价错了" → 5minbtc; 要抓"价格放量破位了" → coin-vp-scanner。

仓库同步

详见 sync-procedure.md。简述:

rsync -av --exclude='data/' --exclude='__pycache__/' \
  --exclude='*.jsonl' --exclude='*.jsonl.*' --exclude='*.gz' \
  /home/aa/.hermes/profiles/cqo/skills/5minbtc/ \
  /home/aa/clawd/repos/AGI-Super-Team/skills/5minbtc/
cd /home/aa/clawd/repos/AGI-Super-Team
git add skills/5minbtc/ && git commit -m "sync(skills/5minbtc): <版本>" && git push origin main

回测系统

backtest/
├── fetch_data.py            # Binance 历史数据下载
├── run_backtest.py          # v5.6 回测 (因子无预测力, 公平回测)
├── run_backtest_v57.py      # v5.7 回测 (含前视偏差, 已知)
├── run_backtest_v58.py      # v5.8 回测 (真实 1min 半 K线, 零前视) ← 推荐
├── run.sh                   # 一键运行
├── data/                    # 5min (105K) + 1min (259K) K线
└── results/                 # 回测结果 (gitignore)

文件结构

5minbtc/
├── SKILL.md                      # 本文件 (~170 行 INDEX)
├── 5minbtc-engine-v6.0.py        # 主引擎 (v6.0 真OFI 一票定方向, launchd 调用)
├── 5minbtc-news.py               # 新闻扫描 (CoinDesk RSS, 唯一稳定源)
├── 5minbtc-log.py                # 日志记录 (写入 logs/)
├── logs/                         # 日志 (当月 live + 历史按月压缩)
│   ├── 5minbtc-log.jsonl         # 当月 live (不入库)
│   └── archive/*.jsonl.gz        # 月度压缩归档 (入库, 见 archive.md)
├── references/                   # 24 份专项 ref (含 skill-organization 模式)
├── backtest/                     # 回测系统 (results/ 不入库)
├── data/                         # 运行时 (news-risk-level.json 等)
├── scripts/                      # 复盘/监控/交易脚本
│   ├── 5minbtc-monitor.py        # ★ 监控脚本 (Claude Code Monitor 集成, v1.0)
│   ├── 5minbtc_watch.py          # ★ Telegram 推送监控 daemon (事件驱动+预测记录+收盘结算)
│   ├── 5minbtc_day_stats.py      # 预测战绩查询 (今日/历史, --push 推送)
│   ├── 5minbtc_trader.py         # ★ 币安预测交易桥接 (--once/--loop/--monitor/--paper/--paper-monitor)
│   ├── prediction_ws_feed.py     # 币安 w3w-prediction WS 实时价源 (<200ms)
│   ├── ofi_feed.py               # ★ 真订单流采集 (trade+bookTicker, launchd com.daniel.ofi-feed)
│   ├── 5minbtc_keyless_paper.py  # 免密钥模拟盘 (公开BTC数据模拟UP/DOWN价)
│   ├── telegram_push.py          # 通用 Telegram 推送助手
│   ├── daily-review-stats.py
│   └── fetch-github-repo.sh

⚠️ archive/ 目录与 reviews/ 目录本地并不存在 —— 本文件早期版本的文件树里声称有它们(还列了 23 份复盘),属于文档/现实漂移,已更正为上面的实际结构:

  • 旧版本引擎源码不保留在本地,只留文字归档 → references/archive.md
  • 每日复盘从未落盘到 reviews/,复盘结论散在 references/lessons.md / pitfalls.mdreports/

最后更新: 2026-09-10 — 引擎 v5.7→v6.0 改名 (文件/SKILL/引用全线对齐, 修引擎内"无中性"过期注释) + SKILL.md 版本刷到 6.0.0 + 性能快照改为审查后的诚实口径 + 补 v5.9/v5.10/v6.0 changelog + 新增与 coin-vp-scanner 的分工说明 + 修文件树未闭合的代码块与 archive//reviews/ 假目录声明 + 新增 archive.mdsetup-from-scratch.md + logs 月度压缩归档入库

Files (agi-super-team)
  • backtest
    • results
      • backtest_20260527_2328.json 16.3 MB
        {"timestamp": "20260527_2328", "engine": "v5.6", "sample_rate": 1, "max_days": null, "stats": {"meta": {"total_predictions": 104920, "directional_predictions": 54499, "neutral_predictions": 50421, "backtest_period": "2025-05-28 16:00 ~ 2026-05-27 23:15"}, "overall": {"direction_accuracy": 48.6, "correct": 26470, "wrong": 28029}, "bias_breakdown": {"bull": {"n": 26474, "correct": 12793, "accuracy": 48.3}, "bear": {"n": 28025, "correct": 13677, "accuracy": 48.8}, "neutral_n": 50421}, "baseline": {"always_bull_accuracy": 49.6, "always_bear_accuracy": 50.4, "random_50": 50.0, "edge_vs_random": -1.4}, "regime_stats": {"TREND": {"n": 30621, "correct": 14533, "accuracy": 47.5}, "RANGE": {"n": 15379, "correct": 7697, "accuracy": 50.0}, "LOW_VOL": {"n": 8499, "correct": 4240, "accuracy": 49.9}}, "confidence_stats": {"40-44": {"n": 17025, "accuracy": 50.2}, "45-49": {"n": 29147, "accuracy": 47.6}, "50-54": {"n": 7985, "accuracy": 48.6}, "55-59": {"n": 342, "accuracy": 49.7}}, "monthly_stats": {"2025-05": {"n": 515, "accuracy": 48.2, "bull_ratio": 45.0}, "2025-06": {"n": 4481, "accuracy": 49.0, "bull_ratio": 49.6}, "2025-07": {"n": 4548, "accuracy": 48.2, "bull_ratio": 49.5}, "2025-08": {"n": 4616, "accuracy": 48.2, "bull_ratio": 47.1}, "2025-09": {"n": 4553, "accuracy": 49.5, "bull_ratio": 48.3}, "2025-10": {"n": 4595, "accuracy": 48.8, "bull_ratio": 51.2}, "2025-11": {"n": 4440, "accuracy": 48.9, "bull_ratio": 47.3}, "2025-12": {"n": 4675, "accuracy": 48.4, "bull_ratio": 48.0}, "2026-01": {"n": 4715, "accuracy": 47.9, "bull_ratio": 49.1}, "2026-02": {"n": 4155, "accuracy": 47.4, "bull_ratio": 47.0}, "2026-03": {"n": 4638, "accuracy": 49.6, "bull_ratio": 49.1}, "2026-04": {"n": 4479, "accuracy": 47.9, "bull_ratio": 50.2}, "2026-05": {"n": 4089, "accuracy": 48.8, "bull_ratio": 46.6}}, "streaks": {"max_win_streak": 14, "max_lose_streak": 14}, "conflict_detection": {"triggered_n": 7373, "accuracy": 50.1, "overall_accuracy": 48.6}, "factor_contribution": {"momentum": {"avg_when_correct": 0.6882, "avg_when_wrong": 0.6994, "signal_strength_diff": -0.0112, "directional_consistency": 48.1, "n_positive_signals": 26325}, "meanrev": {"avg_when_correct": 0.4676, "avg_when_wrong": 0.4774, "signal_strength_diff": -0.0098, "directional_consistency": 51.7, "n_positive_signals": 24029}, "rsi": {"avg_when_correct": 0.2012, "avg_when_wrong": 0.2061, "signal_strength_diff": -0.0048, "directional_consistency": 47.3, "n_positive_signals": 20036}, "volume": {"avg_when_correct": 0.1, "avg_when_wrong": 0.1, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "fatigue": {"avg_when_correct": 0.1927, "avg_when_wrong": 0.1958, "signal_strength_diff": -0.0031, "directional_consistency": 52.3, "n_positive_signals": 9226}, "imbalance": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "microprice": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "decel": {"avg_when_correct": 0.5911, "avg_when_wrong": 0.5739, "signal_strength_diff": 0.0172, "directional_consistency": 49.8, "n_positive_signals": 22713}, "position": {"avg_when_correct": 0.5973, "avg_when_wrong": 0.6063, "signal_strength_diff": -0.0091, "directional_consistency": 51.6, "n_positive_signals": 24093}, "v_reversal": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "vol_breakout": {"avg_when_correct": 0.1362, "avg_when_wrong": 0.1389, "signal_strength_diff": -0.0027, "directional_consistency": 48.0, "n_positive_signals": 9060}}}, "predictions_count": 104920, "predictions": [{"idx": 200, "ts": 1748419200000, "open": 108816.44, "actual_close": 108904.41, "prev_close": 108816.44, "price_chg": 87.97000000000116, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.4924, "meanrev": 0.1811, "rsi": -0.0862, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.0694, "v_reversal": 0, "vol_breakout": -0.4782}}, {"idx": 203, "ts": 1748420100000, "open": 108737.98, "actual_close": 108747.02, "prev_close": 108737.99, "price_chg": 9.029999999998836, "actual_dir": "UP", "bias": "bull", "strength": "medium", "confidence": 45, "score": 6, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.803, "meanrev": 0.608, "rsi": -0.1648, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.6424, "position": 0.7251, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 206, "ts": 1748421000000, "open": 108910.01, "actual_close": 108861.08, "prev_close": 108910.01, "price_chg": -48.929999999993015, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 42, "score": -2, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.5663, "meanrev": -0.1581, "rsi": 0.0588, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.3751, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 209, "ts": 1748421900000, "open": 109073.97, "actual_close": 109053.16, "prev_close": 109073.96, "price_chg": -20.80000000000291, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 43, "score": -4, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.3604, "meanrev": -0.7133, "rsi": 0.2139, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.879, "v_reversal": 0, "vol_breakout": -0.3035}}, {"idx": 212, "ts": 1748422800000, "open": 109040.45, "actual_close": 108970.7, "prev_close": 109040.45, "price_chg": -69.75, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 44, "score": -1, "regime": "TREND", "dir_correct": true, "conflict": true, "factors": {"momentum": 0.8409, "meanrev": -0.4477, "rsi": 0.1623, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.6588, "v_reversal": 0, "vol_breakout": -0.3608}}, {"idx": 215, "ts": 1748423700000, "open": 108772.73, "actual_close": 108785.99, "prev_close": 108772.8, "price_chg": 13.190000000002328, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 43, "score": 4, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.2962, "meanrev": 0.52, "rsi": -0.1666, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.3561, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 218, "ts": 1748424600000, "open": 108595.1, "actual_close": 108607.49, "prev_close": 108595.1, "price_chg": 12.389999999999418, "actual_dir": "UP", "bias": "bear", "strength": "medium", "confidence": 49, "score": -6, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.8518, "meanrev": 0.7268, "rsi": -0.3196, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.2475, "position": 0.9346, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 221, "ts": 1748425500000, "open": 108804.44, "actual_close": 108809.94, "prev_close": 108804.44, "price_chg": 5.5, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9613, "meanrev": 0.1286, "rsi": -0.0412, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.0462, "v_reversal": 0, "vol_breakout": 0.4383}}, {"idx": 224, "ts": 1748426400000, "open": 108789.17, "actual_close": 108467.75, "prev_close": 108789.17, "price_chg": -321.41999999999825, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 45, "score": -1, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.706, "meanrev": 0.1853, "rsi": -0.0601, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.6103, "position": 0.0952, "v_reversal": 0, "vol_breakout": -0.4265}}, {"idx": 227, "ts": 1748427300000, "open": 108562.63, "actual_close": 108355.07, "prev_close": 108562.63, "price_chg": -207.55999999999767, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.7696, "meanrev": 0.4594, "rsi": -0.2227, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.609, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 230, "ts": 1748428200000, "open": 108636.36, "actual_close": 108708.58, "prev_close": 108636.37, "price_chg": 72.2100000000064, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.6158, "meanrev": 0.1462, "rsi": -0.0687, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.0756, "v_reversal": 0, "vol_breakout": -0.4588}}, {"idx": 233, "ts": 1748429100000, "open": 108754.16, "actual_close": 108793.66, "prev_close": 108754.16, "price_chg": 39.5, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.2545, "meanrev": -0.2376, "rsi": 0.0305, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.5104, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 236, "ts": 1748430000000, "open": 108829.0, "actual_close": 108917.02, "prev_close": 108829.01, "price_chg": 88.01000000000931, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 47, "score": 4, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.6755, "meanrev": -0.3782, "rsi": 0.0898, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.0914, "position": -0.6642, "v_reversal": 0, "vol_breakout": 0.5463}}, {"idx": 239, "ts": 1748430900000, "open": 108850.01, "actual_close": 108801.95, "prev_close": 108850.01, "price_chg": -48.05999999999767, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9919, "meanrev": -0.309, "rsi": 0.0864, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.711, "v_reversal": 0, "vol_breakout": 0.3193}}, {"idx": 242, "ts": 1748431800000, "open": 108760.0, "actual_close": 108744.2, "prev_close": 108760.01, "price_chg": -15.809999999997672, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 44, "score": -1, "regime": "TREND", "dir_correct": true, "conflict": true, "factors": {"momentum": 0.7026, "meanrev": -0.1197, "rsi": -0.0132, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.4509, "v_reversal": 0, "vol_breakout": -0.3735}}, {"idx": 245, "ts": 1748432700000, "open": 108794.29, "actual_close": 108842.07, "prev_close": 108794.29, "price_chg": 47.78000000001339, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.309, "meanrev": -0.1498, "rsi": 0.0309, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.55, "v_reversal": 0, "vol_breakout": 0.3359}}, {"idx": 248, "ts": 1748433600000, "open": 108870.39, "actual_close": 108867.97, "prev_close": 108870.38, "price_chg": -2.4100000000034925, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "LOW_VOL", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.0624, "meanrev": -0.3465, "rsi": 0.1095, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.6584, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 251, "ts": 1748434500000, "open": 108881.53, "actual_close": 108973.83, "prev_close": 108881.53, "price_chg": 92.30000000000291, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 46, "score": -3, "regime": "LOW_VOL", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.5718, "meanrev": -0.3472, "rsi": 0.1209, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.5545, "position": -0.3567, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 254, "ts": 1748435400000, "open": 109013.34, "actual_close": 108981.07, "prev_close": 109013.34, "price_chg": -32.26999999998952, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 48, "score": -4, "regime": "LOW_VOL", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.9997, "meanrev": -0.6984, "rsi": 0.2823, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": -0.3166, "position": -0.7779, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 257, "ts": 1748436300000, "open": 109027.42, "actual_close": 108926.01, "prev_close": 109027.42, "price_chg": -101.41000000000349, "actual_dir": "DOWN", "bias": "bear", "strength": "medium", "confidence": 56, "score": -12, "regime": "LOW_VOL", "dir_correct": true, "conflict": true, "factors": {"momentum": 0.9992, "meanrev": -0.5701, "rsi": 0.2749, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.8443, "v_reversal": 0, "vol_breakout": -0.4665}}, {"idx": 260, "ts": 1748437200000, "open": 108891.94, "actual_close": 108802.15, "prev_close": 108891.94, "price_chg": -89.79000000000815, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.2005, "meanrev": -0.0034, "rsi": 0.022, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.0212, "v_reversal": 0, "vol_breakout": -0.348}}, {"idx": 263, "ts": 1748438100000, "open": 108839.25, "actual_close": 108767.25, "prev_close": 108839.26, "price_chg": -72.00999999999476, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.6549, "meanrev": 0.3243, "rsi": -0.0424, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.4156, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 266, "ts": 1748439000000, "open": 108722.45, "actual_close": 108956.52, "prev_close": 108722.45, "price_chg": 234.07000000000698, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 48, "score": -5, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.9979, "meanrev": 0.6327, "rsi": -0.1834, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": 0.2589, "position": 0.6011, "v_reversal": 0, "vol_breakout": -0.3412}}, {"idx": 269, "ts": 1748439900000, "open": 108515.83, "actual_close": 108363.64, "prev_close": 108515.84, "price_chg": -152.1999999999971, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9525, "meanrev": 0.896, "rsi": -0.2752, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.826, "position": 0.6516, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 272, "ts": 1748440800000, "open": 108068.45, "actual_close": 108189.01, "prev_close": 108068.45, "price_chg": 120.55999999999767, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 49, "score": -5, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.9704, "meanrev": 0.9052, "rsi": -0.4573, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.2001, "position": 0.888, "v_reversal": 0, "vol_breakout": -0.3673}}, {"idx": 275, "ts": 1748441700000, "open": 107880.78, "actual_close": 107699.99, "prev_close": 107880.78, "price_chg": -180.7899999999936, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 45, "score": -1, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.9987, "meanrev": 0.7302, "rsi": -0.4625, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.5829, "position": 0.9929, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 278, "ts": 1748442600000, "open": 107954.55, "actual_close": 107828.12, "prev_close": 107954.54, "price_chg": -126.41999999999825, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9967, "meanrev": 0.4434, "rsi": -0.3061, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.5165, "v_reversal": 0, "vol_breakout": -0.3615}}, {"idx": 281, "ts": 1748443500000, "open": 107603.84, "actual_close": 107738.63, "prev_close": 107603.85, "price_chg": 134.77999999999884, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 48, "score": -4, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.9988, "meanrev": 0.5319, "rsi": -0.4319, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.34, "position": 0.9238, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 284, "ts": 1748444400000, "open": 107522.71, "actual_close": 107572.25, "prev_close": 107522.72, "price_chg": 49.529999999998836, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9933, "meanrev": 0.4697, "rsi": -0.4025, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.9254, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 287, "ts": 1748445300000, "open": 107136.37, "actual_close": 107261.44, "prev_close": 107136.37, "price_chg": 125.07000000000698, "actual_dir": "UP", "bias": "bear", "strength": "medium", "confidence": 49, "score": -6, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.9946, "meanrev": 0.687, "rsi": -0.5084, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.1473, "position": 1.0, "v_reversal": 0, "vol_breakout": -0.2913}}, {"idx": 290, "ts": 1748446200000, "open": 107292.83, "actual_close": 107309.16, "prev_close": 107292.83, "price_chg": 16.330000000001746, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9948, "meanrev": 0.4712, "rsi": -0.3381, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.7303, "v_reversal": 0, "vol_breakout": 0.3982}}, {"idx": 293, "ts": 1748447100000, "open": 107424.38, "actual_close": 107264.82, "prev_close": 107424.38, "price_chg": -159.55999999999767, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.7778, "meanrev": 0.2142, "rsi": -0.2237, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.3634, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 296, "ts": 1748448000000, "open": 107107.31, "actual_close": 107280.0, "prev_close": 107107.31, "price_chg": 172.69000000000233, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 49, "score": -5, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.8831, "meanrev": 0.5444, "rsi": -0.3636, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -0.17, "position": 0.8653, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 299, "ts": 1748448900000, "open": 107283.31, "actual_close": 107375.18, "prev_close": 107283.32, "price_chg": 91.85999999998603, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.175, "meanrev": 0.205, "rsi": -0.2085, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.396, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 302, "ts": 1748449800000, "open": 107416.8, "actual_close": 107211.48, "prev_close": 107416.81, "price_chg": -205.33000000000175, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.173, "meanrev": -0.2572, "rsi": -0.1009, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.1755, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 305, "ts": 1748450700000, "open": 107220.0, "actual_close": 107272.73, "prev_close": 107220.01, "price_chg": 52.720000000001164, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.388, "meanrev": 0.2757, "rsi": -0.2207, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.4561, "position": 0.2243, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 308, "ts": 1748451600000, "open": 107420.0, "actual_close": 107661.53, "prev_close": 107420.0, "price_chg": 241.52999999999884, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 42, "score": -3, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.6856, "meanrev": -0.4392, "rsi": -0.0301, "volume": 0.1, "fatigue": -0.8, "imbalance": 0, "microprice": 0, "decel": 0.7442, "position": -0.6645, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 311, "ts": 1748452500000, "open": 107649.98, "actual_close": 107767.06, "prev_close": 107649.98, "price_chg": 117.08000000000175, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 44, "score": -1, "regime": "TREND", "dir_correct": false, "conflict": true, "factors": {"momentum": 0.8788, "meanrev": -0.6134, "rsi": 0.1191, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.6281, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 314, "ts": 1748453400000, "open": 107604.42, "actual_close": 107533.72, "prev_close": 107604.42, "price_chg": -70.69999999999709, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9342, "meanrev": -0.3481, "rsi": 0.0561, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.4206, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 317, "ts": 1748454300000, "open": 107447.05, "actual_close": 107514.66, "prev_close": 107447.05, "price_chg": 67.61000000000058, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.5962, "meanrev": 0.0215, "rsi": -0.0625, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.1294, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 320, "ts": 1748455200000, "open": 107679.01, "actual_close": 107744.01, "prev_close": 107679.01, "price_chg": 65.0, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 48, "score": 5, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.2189, "meanrev": -0.3769, "rsi": 0.1225, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.5285, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 323, "ts": 1748456100000, "open": 107829.99, "actual_close": 107729.54, "prev_close": 107830.0, "price_chg": -100.4600000000064, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 48, "score": 5, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.423, "meanrev": -0.5321, "rsi": 0.2322, "volume": 0.1, "fatigue": -0.8, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.9645, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 326, "ts": 1748457000000, "open": 107519.21, "actual_close": 107520.75, "prev_close": 107519.21, "price_chg": 1.5399999999935972, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.4486, "meanrev": 0.3497, "rsi": -0.0732, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.2467, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 329, "ts": 1748457900000, "open": 107694.39, "actual_close": 107680.14, "prev_close": 107694.39, "price_chg": -14.25, "actual_dir": "DOWN", "bias": "bull", "strength": "medium", "confidence": 51, "score": 7, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.4514, "meanrev": -0.2104, "rsi": 0.0913, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.382, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 332, "ts": 1748458800000, "open": 107476.54, "actual_close": 107636.35, "prev_close": 107476.55, "price_chg": 159.8000000000029, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 47, "score": -4, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.8596, "meanrev": 0.3889, "rsi": -0.0969, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.29, "position": 0.5282, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 335, "ts": 1748459700000, "open": 107669.02, "actual_close": 107651.72, "prev_close": 107669.02, "price_chg": -17.30000000000291, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.4337, "meanrev": -0.1315, "rsi": 0.0597, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.276, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 338, "ts": 1748460600000, "open": 107499.34, "actual_close": 107454.55, "prev_close": 107499.33, "price_chg": -44.779999999998836, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 42, "score": 2, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.2121, "meanrev": 0.4555, "rsi": -0.0905, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.5098, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 341, "ts": 1748461500000, "open": 107127.61, "actual_close": 107199.99, "prev_close": 107127.62, "price_chg": 72.3700000000099, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 48, "score": -4, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.8761, "meanrev": 0.8797, "rsi": -0.3468, "volume": 0.1, "fatigue": 0.8, "imbalance": 0, "microprice": 0, "decel": -0.2785, "position": 0.9827, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 344, "ts": 1748462400000, "open": 106946.0, "actual_close": 106942.0, "prev_close": 106946.01, "price_chg": -4.009999999994761, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 47, "score": -3, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.9784, "meanrev": 0.7747, "rsi": -0.4035, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.3394, "position": 0.982, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 347, "ts": 1748463300000, "open": 107035.19, "actual_close": 107096.0, "prev_close": 107035.18, "price_chg": 60.820000000006985, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9995, "meanrev": 0.4424, "rsi": -0.2936, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.4833, "v_reversal": 0, "vol_breakout": -0.4306}}, {"idx": 350, "ts": 1748464200000, "open": 107178.42, "actual_close": 107213.18, "prev_close": 107178.42, "price_chg": 34.75999999999476, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.7949, "meanrev": 0.1774, "rsi": -0.1268, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.2048, "v_reversal": 0, "vol_breakout": 0.5787}}, {"idx": 353, "ts": 1748465100000, "open": 107352.37, "actual_close": 107346.29, "prev_close": 107352.38, "price_chg": -6.0900000000110595, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 42, "score": 3, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.6611, "meanrev": -0.1647, "rsi": 0.0395, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.4372, "position": -0.1333, "v_reversal": 0, "vol_breakout": 0.3548}}, {"idx": 356, "ts": 1748466000000, "open": 107318.19, "actual_close": 107230.11, "prev_close": 107318.18, "price_chg": -88.06999999999243, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9907, "meanrev": -0.2667, "rsi": 0.0093, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.399, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 359, "ts": 1748466900000, "open": 107329.13, "actual_close": 107407.01, "prev_close": 107329.12, "price_chg": 77.88999999999942, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 44, "score": -1, "regime": "TREND", "dir_correct": false, "conflict": true, "factors": {"momentum": 0.8867, "meanrev": -0.4195, "rsi": 0.0301, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.7645, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 362, "ts": 1748467800000, "open": 107381.93, "actual_close": 107435.53, "prev_close": 107381.94, "price_chg": 53.58999999999651, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 44, "score": -1, "regime": "TREND", "dir_correct": false, "conflict": true, "factors": {"momentum": 0.8012, "meanrev": -0.3909, "rsi": 0.0664, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.8672, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 365, "ts": 1748468700000, "open": 107494.15, "actual_close": 107498.75, "prev_close": 107494.15, "price_chg": 4.600000000005821, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 48, "score": 5, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.8661, "meanrev": -0.5682, "rsi": 0.172, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 0.2859, "position": -1.0, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 368, "ts": 1748469600000, "open": 107495.74, "actual_close": 107404.01, "prev_close": 107495.74, "price_chg": -91.73000000001048, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.996, "meanrev": -0.4539, "rsi": 0.1586, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.6706, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 371, "ts": 1748470500000, "open": 107306.49, "actual_close": 107302.84, "prev_close": 107306.5, "price_chg": -3.6600000000034925, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 41, "score": 1, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.1564, "meanrev": 0.2729, "rsi": -0.0869, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.3811, "v_reversal": 0, "vol_breakout": -0.3456}}, {"idx": 374, "ts": 1748471400000, "open": 107234.97, "actual_close": 107211.42, "prev_close": 107234.97, "price_chg": -23.55000000000291, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 48, "score": -4, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.9252, "meanrev": 0.4915, "rsi": -0.1732, "volume": 0.1, "fatigue": 0.8, "imbalance": 0, "microprice": 0, "decel": -0.0131, "position": 0.7481, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 377, "ts": 1748472300000, "open": 107317.94, "actual_close": 107379.2, "prev_close": 107317.94, "price_chg": 61.25999999999476, "actual_dir": "UP", "bias": "bull", "strength": "medium", "confidence": 49, "score": 6, "regime": "LOW_VOL", "dir_correct": true, "conflict": true, "factors": {"momentum": -0.9599, "meanrev": 0.2196, "rsi": -0.0267, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.2107, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 380, "ts": 1748473200000, "open": 107400.73, "actual_close": 107590.84, "prev_close": 107400.73, "price_chg": 190.11000000000058, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 45, "score": 1, "regime": "LOW_VOL", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.2634, "meanrev": -0.1036, "rsi": 0.0823, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.1784, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 383, "ts": 1748474100000, "open": 107863.63, "actual_close": 107943.12, "prev_close": 107863.63, "price_chg": 79.48999999999069, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 48, "score": 5, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.978, "meanrev": -0.8912, "rsi": 0.4875, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 0.0306, "position": -0.8442, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 386, "ts": 1748475000000, "open": 107956.55, "actual_close": 107934.8, "prev_close": 107956.55, "price_chg": -21.75, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 45, "score": 1, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.9999, "meanrev": -0.6497, "rsi": 0.5328, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.6466, "position": -0.8372, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 389, "ts": 1748475900000, "open": 107915.99, "actual_close": 107710.68, "prev_close": 107916.0, "price_chg": -205.32000000000698, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9979, "meanrev": -0.4221, "rsi": 0.3823, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.7137, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 392, "ts": 1748476800000, "open": 107781.78, "actual_close": 107629.1, "prev_close": 107781.78, "price_chg": -152.67999999999302, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.6981, "meanrev": -0.1825, "rsi": 0.1639, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.4135, "v_reversal": 0, "vol_breakout": -0.3139}}, {"idx": 395, "ts": 1748477700000, "open": 107546.5, "actual_close": 107656.01, "prev_close": 107546.5, "price_chg": 109.50999999999476, "actual_dir": "UP", "bias": "bear", "strength": "medium", "confidence": 50, "score": -6, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.8816, "meanrev": 0.2665, "rsi": -0.0853, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.2855, "position": 0.1849, "v_reversal": 0, "vol_breakout": -0.4088}}, {"idx": 398, "ts": 1748478600000, "open": 108010.0, "actual_close": 107859.96, "prev_close": 108010.0, "price_chg": -150.0399999999936, "actual_dir": "DOWN", "bias": "bear", "strength": "medium", "confidence": 46, "score": -6, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.3683, "meanrev": -0.4912, "rsi": 0.2992, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.8944, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 401, "ts": 1748479500000, "open": 107801.91, "actual_close": 108016.96, "prev_close": 107801.91, "price_chg": 215.0500000000029, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 42, "score": -2, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.0066, "meanrev": 0.0342, "rsi": 0.0954, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.2228, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 404, "ts": 1748480400000, "open": 108285.31, "actual_close": 108046.99, "prev_close": 108285.32, "price_chg": -238.33000000000175, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 49, "score": 5, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.9529, "meanrev": -0.8135, "rsi": 0.3763, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.0058, "position": -0.9713, "v_reversal": 0, "vol_breakout": 0.3173}}, {"idx": 407, "ts": 1748481300000, "open": 108009.58, "actual_close": 107966.13, "prev_close": 108009.58, "price_chg": -43.44999999999709, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9475, "meanrev": -0.3158, "rsi": 0.1368, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.3391, "v_reversal": 0, "vol_breakout": -0.439}}, {"idx": 410, "ts": 1748482200000, "open": 107918.99, "actual_close": 107883.19, "prev_close": 107919.0, "price_chg": -35.80999999999767, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.3098, "meanrev": -0.0961, "rsi": 0.0622, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.133, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 413, "ts": 1748483100000, "open": 108045.45, "actual_close": 107900.0, "prev_close": 108045.45, "price_chg": -145.4499999999971, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.1089, "meanrev": -0.31, "rsi": 0.1578, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.3996, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 416, "ts": 1748484000000, "open": 108156.23, "actual_close": 108120.17, "prev_close": 108156.23, "price_chg": -36.05999999999767, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 41, "score": -1, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.1489, "meanrev": -0.4913, "rsi": 0.1769, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.7968, "position": -0.3295, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 419, "ts": 1748484900000, "open": 108008.02, "actual_close": 108045.37, "prev_close": 108008.01, "price_chg": 37.36000000000058, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.5558, "meanrev": -0.0032, "rsi": 0.0569, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.1894, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 422, "ts": 1748485800000, "open": 108079.15, "actual_close": 108150.79, "prev_close": 108079.14, "price_chg": 71.64999999999418, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 45, "score": 1, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.6567, "meanrev": -0.1945, "rsi": 0.1023, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.564, "position": 0.0044, "v_reversal": 0, "vol_breakout": 0.2737}}, {"idx": 425, "ts": 1748486700000, "open": 108126.11, "actual_close": 108313.8, "prev_close": 108126.1, "price_chg": 187.6999999999971, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 43, "score": -3, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.4687, "meanrev": -0.3897, "rsi": 0.1141, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.8213, "position": -0.1712, "v_reversal": 0, "vol_breakout": 0.6201}}, {"idx": 428, "ts": 1748487600000, "open": 108426.93, "actual_close": 108309.55, "prev_close": 108426.94, "price_chg": -117.38999999999942, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 48, "score": 4, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.809, "meanrev": -0.7116, "rsi": 0.2862, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.0028, "position": -0.8283, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 431, "ts": 1748488500000, "open": 108203.75, "actual_close": 108192.0, "prev_close": 108203.74, "price_chg": -11.740000000005239, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9041, "meanrev": -0.146, "rsi": 0.0837, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.1184, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 434, "ts": 1748489400000, "open": 108175.9, "actual_close": 108141.67, "prev_close": 108175.91, "price_chg": -34.24000000000524, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.5216, "meanrev": -0.0223, "rsi": 0.0532, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.1156, "v_reversal": 0, "vol_breakout": -0.3341}}, {"idx": 437, "ts": 1748490300000, "open": 108036.0, "actual_close": 107999.43, "prev_close": 108036.0, "price_chg": -36.570000000006985, "actual_dir": "DOWN", "bias": "bear", "strength": "medium", "confidence": 49, "score": -6, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.6484, "meanrev": 0.345, "rsi": -0.0722, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.1956, "position": 0.6211, "v_reversal": 0, "vol_breakout": -0.3922}}, {"idx": 440, "ts": 1748491200000, "open": 108044.87, "actual_close": 108106.93, "prev_close": 108044.88, "price_chg": 62.04999999998836, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9981, "meanrev": 0.3449, "rsi": -0.0549, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.6621, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 443, "ts": 1748492100000, "open": 107915.64, "actual_close": 107876.09, "prev_close": 107915.64, "price_chg": -39.55000000000291, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 45, "score": -1, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.9939, "meanrev": 0.5799, "rsi": -0.1695, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.526, "position": 0.841, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 446, "ts": 1748493000000, "open": 107867.4, "actual_close": 107716.71, "prev_close": 107867.41, "price_chg": -150.6999999999971, "actual_dir": "DOWN", "bias": "bull", "strength": "medium", "confidence": 52, "score": 8, "regime": "LOW_VOL", "dir_correct": false, "conflict": true, "factors": {"momentum": -0.9936, "meanrev": 0.4995, "rsi": -0.1916, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.5231, "v_reversal": 0, "vol_breakout": -0.3518}}, {"idx": 449, "ts": 1748493900000, "open": 107399.99, "actual_close": 107317.73, "prev_close": 107399.98, "price_chg": -82.25, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 48, "score": -5, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.9902, "meanrev": 0.859, "rsi": -0.4936, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -0.0051, "position": 0.877, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 452, "ts": 1748494800000, "open": 107564.44, "actual_close": 107597.84, "prev_close": 107564.44, "price_chg": 33.39999999999418, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.994, "meanrev": 0.412, "rsi": -0.2424, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.0703, "v_reversal": 0, "vol_breakout": -0.5543}}, {"idx": 455, "ts": 1748495700000, "open": 107585.5, "actual_close": 107619.1, "prev_close": 107585.5, "price_chg": 33.60000000000582, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.8761, "meanrev": 0.2905, "rsi": -0.2046, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.0825, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 458, "ts": 1748496600000, "open": 107615.2, "actual_close": 107640.7, "prev_close": 107615.2, "price_chg": 25.5, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.3152, "meanrev": 0.1599, "rsi": -0.1667, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.1514, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 461, "ts": 1748497500000, "open": 107727.28, "actual_close": 107691.33, "prev_close": 107727.27, "price_chg": -35.94000000000233, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 40, "score": -1, "regime": "RANGE", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.9662, "meanrev": -0.202, "rsi": -0.0223, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 0.069, "position": -0.5484, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 464, "ts": 1748498400000, "open": 107681.81, "actual_close": 107707.4, "prev_close": 107681.81, "price_chg": 25.589999999996508, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9823, "meanrev": -0.2162, "rsi": -0.0674, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.5512, "v_reversal": 0, "vol_breakout": 0.3033}}, {"idx": 467, "ts": 1748499300000, "open": 107753.94, "actual_close": 107829.99, "prev_close": 107753.94, "price_chg": 76.05000000000291, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 48, "score": 5, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.99, "meanrev": -0.4697, "rsi": 0.0365, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": -0.0005, "position": -0.9234, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 470, "ts": 1748500200000, "open": 107936.06, "actual_close": 107932.85, "prev_close": 107936.05, "price_chg": -3.1999999999970896, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 49, "score": -5, "regime": "LOW_VOL", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.9952, "meanrev": -0.8343, "rsi": 0.2593, "volume": 0.1, "fatigue": -0.8, "imbalance": 0, "microprice": 0, "decel": -0.023, "position": -0.9731, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 473, "ts": 1748501100000, "open": 107870.48, "actual_close": 107874.61, "prev_close": 107870.49, "price_chg": 4.119999999995343, "actual_dir": "UP", "bias": "bear", "strength": "medium", "confidence": 53, "score": -9, "regime": "LOW_VOL", "dir_correct": false, "conflict": true, "factors": {"momentum": 0.9868, "meanrev": -0.4432, "rsi": 0.1383, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.5979, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 476, "ts": 1748502000000, "open": 107915.46, "actual_close": 107752.55, "prev_close": 107915.46, "price_chg": -162.9100000000035, "actual_dir": "DOWN", "bias": "bear", "strength": "medium", "confidence": 54, "score": -10, "regime": "LOW_VOL", "dir_correct": true, "conflict": true, "factors": {"momentum": 0.96, "meanrev": -0.4807, "rsi": 0.175, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.8391, "v_reversal": 0, "vol_breakout": 0.5575}}, {"idx": 479, "ts": 1748502900000, "open": 107993.57, "actual_close": 107963.47, "prev_close": 107993.57, "price_chg": -30.10000000000582, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.5703, "meanrev": -0.6634, "rsi": 0.2103, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.4121, "position": -0.9999, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 482, "ts": 1748503800000, "open": 107984.78, "actual_close": 107912.0, "prev_close": 107984.78, "price_chg": -72.77999999999884, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.459, "meanrev": -0.5043, "rsi": 0.1848, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.8587, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 485, "ts": 1748504700000, "open": 107843.62, "actual_close": 107831.49, "prev_close": 107843.61, "price_chg": -12.119999999995343, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.3177, "meanrev": 0.1916, "rsi": -0.0079, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.0298, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 488, "ts": 1748505600000, "open": 107880.73, "actual_close": 107772.72, "prev_close": 107880.72, "price_chg": -108.0, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 43, "score": 4, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.1899, "meanrev": 0.0368, "rsi": 0.0427, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.0119, "v_reversal": 0, "vol_breakout": -0.2568}}, {"idx": 491, "ts": 1748506500000, "open": 107940.01, "actual_close": 108090.13, "prev_close": 107940.01, "price_chg": 150.1200000000099, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 41, "score": -1, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": -0.1641, "meanrev": -0.3169, "rsi": 0.109, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.5602, "v_reversal": 0, "vol_breakout": 0.3158}}, {"idx": 494, "ts": 1748507400000, "open": 108185.43, "actual_close": 108230.14, "prev_close": 108185.43, "price_chg": 44.7100000000064, "actual_dir": "UP", "bias": "bull", "strength": "weak", "confidence": 47, "score": 4, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.8261, "meanrev": -0.7198, "rsi": 0.3155, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.1143, "position": -0.8325, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 497, "ts": 1748508300000, "open": 108220.0, "actual_close": 108258.78, "prev_close": 108220.0, "price_chg": 38.779999999998836, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9929, "meanrev": -0.5213, "rsi": 0.2767, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.6946, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 500, "ts": 1748509200000, "open": 108492.5, "actual_close": 108433.66, "prev_close": 108492.49, "price_chg": -58.830000000001746, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 48, "score": 4, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.9998, "meanrev": -0.6826, "rsi": 0.4616, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": -0.2563, "position": -0.8598, "v_reversal": 0, "vol_breakout": 0.2557}}, {"idx": 503, "ts": 1748510100000, "open": 108523.77, "actual_close": 108522.26, "prev_close": 108523.76, "price_chg": -1.5, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9987, "meanrev": -0.5507, "rsi": 0.389, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.7629, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 506, "ts": 1748511000000, "open": 108459.99, "actual_close": 108453.59, "prev_close": 108459.99, "price_chg": -6.400000000008731, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9774, "meanrev": -0.3361, "rsi": 0.2945, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.6294, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 509, "ts": 1748511900000, "open": 108405.17, "actual_close": 108508.38, "prev_close": 108405.17, "price_chg": 103.2100000000064, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.6383, "meanrev": -0.1541, "rsi": 0.209, "volume": 0.1, "fatigue": 0.8, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.3956, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 512, "ts": 1748512800000, "open": 108520.01, "actual_close": 108429.23, "prev_close": 108520.01, "price_chg": -90.77999999999884, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 46, "score": 2, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.4748, "meanrev": -0.3409, "rsi": 0.262, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.0622, "position": -0.5579, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 515, "ts": 1748513700000, "open": 108465.7, "actual_close": 108522.43, "prev_close": 108465.69, "price_chg": 56.73999999999069, "actual_dir": "UP", "bias": "bear", "strength": "weak", "confidence": 43, "score": -4, "regime": "RANGE", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.1477, "meanrev": -0.0791, "rsi": 0.1667, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.2081, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 518, "ts": 1748514600000, "open": 108434.78, "actual_close": 108531.27, "prev_close": 108434.78, "price_chg": 96.49000000000524, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.1108, "meanrev": 0.2915, "rsi": 0.0846, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": 0.2759, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 521, "ts": 1748515500000, "open": 108525.99, "actual_close": 108605.01, "prev_close": 108526.0, "price_chg": 79.00999999999476, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 40, "score": 0, "regime": "RANGE", "dir_correct": null, "conflict": false, "factors": {"momentum": 0.5418, "meanrev": -0.2825, "rsi": 0.1757, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 0.1022, "position": -0.2313, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 524, "ts": 1748516400000, "open": 108746.01, "actual_close": 108816.11, "prev_close": 108746.01, "price_chg": 70.10000000000582, "actual_dir": "UP", "bias": "bull", "strength": "medium", "confidence": 49, "score": 6, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": 0.8817, "meanrev": -0.8614, "rsi": 0.3817, "volume": 0.1, "fatigue": -0.5, "imbalance": 0, "microprice": 0, "decel": 0.2596, "position": -0.5633, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 527, "ts": 1748517300000, "open": 108886.72, "actual_close": 108863.64, "prev_close": 108886.71, "price_chg": -23.070000000006985, "actual_dir": "DOWN", "bias": "bull", "strength": "weak", "confidence": 46, "score": 3, "regime": "TREND", "dir_correct": false, "conflict": false, "factors": {"momentum": 0.9981, "meanrev": -0.7751, "rsi": 0.4369, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -0.4828, "position": -0.9952, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 530, "ts": 1748518200000, "open": 108778.0, "actual_close": 108772.72, "prev_close": 108778.0, "price_chg": -5.279999999998836, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.9908, "meanrev": -0.3632, "rsi": 0.2532, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.5534, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 533, "ts": 1748519100000, "open": 108734.49, "actual_close": 108761.9, "prev_close": 108734.49, "price_chg": 27.40999999998894, "actual_dir": "UP", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": 0.8138, "meanrev": -0.1584, "rsi": 0.1601, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": -1, "position": -0.3445, "v_reversal": 0, "vol_breakout": -0.2632}}, {"idx": 536, "ts": 1748520000000, "open": 108779.83, "actual_close": 108701.82, "prev_close": 108779.84, "price_chg": -78.01999999998952, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": false, "factors": {"momentum": -0.1337, "meanrev": -0.2009, "rsi": 0.1927, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": -0.5333, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 539, "ts": 1748520900000, "open": 108638.33, "actual_close": 108552.19, "prev_close": 108638.33, "price_chg": -86.13999999999942, "actual_dir": "DOWN", "bias": "bear", "strength": "weak", "confidence": 49, "score": -5, "regime": "TREND", "dir_correct": true, "conflict": false, "factors": {"momentum": -0.985, "meanrev": 0.4195, "rsi": -0.0202, "volume": 0.1, "fatigue": 0.5, "imbalance": 0, "microprice": 0, "decel": 0.3317, "position": 0.3297, "v_reversal": 0, "vol_breakout": -0.3246}}, {"idx": 542, "ts": 1748521800000, "open": 108683.18, "actual_close": 108681.82, "prev_close": 108683.17, "price_chg": -1.3499999999912689, "actual_dir": "DOWN", "bias": "neutral", "strength": "weak", "confidence": 44, "score": 0, "regime": "TREND", "dir_correct": null, "conflict": true, "factors": {"momentum": -0.9595, "meanrev": 0.2382, "rsi": 0.0566, "volume": 0.1, "fatigue": 0, "imbalance": 0, "microprice": 0, "decel": 1, "position": 0.1406, "v_reversal": 0, "vol_breakout": 0}}, {"idx": 545, "ts": 1748522700000, "ope
      • backtest_v57_20260528_1433.json 7.1 KB
        {"timestamp": "20260528_1433", "engine": "v5.7 半K线", "sample_rate": 6, "max_days": 180, "progress": 0.5, "stats_v57": {"label": "v5.7 半K线", "meta": {"total_predictions": 8640, "directional_predictions": 4529, "neutral_predictions": 4111, "backtest_period": "2025-11-28 23:20 ~ 2026-05-27 22:50"}, "overall": {"direction_accuracy": 71.2, "correct": 3223, "wrong": 1306}, "bias_breakdown": {"bull": {"n": 2227, "correct": 1588, "accuracy": 71.3}, "bear": {"n": 2302, "correct": 1635, "accuracy": 71.0}, "neutral_n": 4111}, "baseline": {"always_bull_accuracy": 49.7, "always_bear_accuracy": 50.3, "random_50": 50.0, "edge_vs_random": 21.2}, "regime_stats": {"TREND": {"n": 2503, "correct": 1826, "accuracy": 73.0}, "RANGE": {"n": 1304, "correct": 927, "accuracy": 71.1}, "LOW_VOL": {"n": 722, "correct": 470, "accuracy": 65.1}}, "confidence_stats": {"40-44": {"n": 1432, "accuracy": 72.7}, "45-49": {"n": 2086, "accuracy": 58.9}, "50-54": {"n": 936, "accuracy": 93.8}, "55-59": {"n": 75, "accuracy": 100.0}}, "monthly_stats": {"2025-11": {"n": 49, "accuracy": 61.2, "bull_ratio": 51.0}, "2025-12": {"n": 785, "accuracy": 72.2, "bull_ratio": 50.1}, "2026-01": {"n": 783, "accuracy": 69.3, "bull_ratio": 50.4}, "2026-02": {"n": 714, "accuracy": 70.7, "bull_ratio": 45.1}, "2026-03": {"n": 759, "accuracy": 74.0, "bull_ratio": 51.6}, "2026-04": {"n": 736, "accuracy": 72.0, "bull_ratio": 51.0}, "2026-05": {"n": 703, "accuracy": 69.1, "bull_ratio": 46.2}}, "streaks": {"max_win_streak": 19, "max_lose_streak": 6}, "conflict_detection": {"triggered_n": 998, "accuracy": 79.0, "overall_accuracy": 71.2}, "factor_contribution": {"momentum": {"avg_when_correct": 0.6959, "avg_when_wrong": 0.6901, "signal_strength_diff": 0.0058, "directional_consistency": 51.0, "n_positive_signals": 2200}, "meanrev": {"avg_when_correct": 0.4408, "avg_when_wrong": 0.4829, "signal_strength_diff": -0.0421, "directional_consistency": 42.2, "n_positive_signals": 1969}, "rsi": {"avg_when_correct": 0.1868, "avg_when_wrong": 0.1955, "signal_strength_diff": -0.0087, "directional_consistency": 58.2, "n_positive_signals": 1536}, "volume": {"avg_when_correct": 0.125, "avg_when_wrong": 0.117, "signal_strength_diff": 0.008, "directional_consistency": 35.3, "n_positive_signals": 1760}, "fatigue": {"avg_when_correct": 0.1532, "avg_when_wrong": 0.1995, "signal_strength_diff": -0.0463, "directional_consistency": 47.2, "n_positive_signals": 619}, "imbalance": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "microprice": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "decel": {"avg_when_correct": 0.6594, "avg_when_wrong": 0.5749, "signal_strength_diff": 0.0845, "directional_consistency": 60.2, "n_positive_signals": 1970}, "position": {"avg_when_correct": 0.5211, "avg_when_wrong": 0.5604, "signal_strength_diff": -0.0394, "directional_consistency": 43.8, "n_positive_signals": 1984}, "v_reversal": {"avg_when_correct": 0.4091, "avg_when_wrong": 0.0351, "signal_strength_diff": 0.3741, "directional_consistency": 99.9, "n_positive_signals": 999}, "vol_breakout": {"avg_when_correct": 0.1434, "avg_when_wrong": 0.1608, "signal_strength_diff": -0.0175, "directional_consistency": 51.0, "n_positive_signals": 789}, "half_body": {"avg_when_correct": 0.1321, "avg_when_wrong": 0.107, "signal_strength_diff": 0.0252, "directional_consistency": 100.0, "n_positive_signals": 1282}}, "half_body_analysis": {"activated_n": 4316, "activated_pct": 95.3, "activated_accuracy": 71.7, "strong_bull": {"n": 0, "accuracy": 0}, "strong_bear": {"n": 0, "accuracy": 0}, "weak_signal": {"n": 4316, "accuracy": 71.7}, "direction_consistency": {"pos_predicts_up": 100.0, "neg_predicts_down": 100.0}}}, "stats_v56": {"label": "v5.6 开盘", "meta": {"total_predictions": 8640, "directional_predictions": 4471, "neutral_predictions": 4169, "backtest_period": "2025-11-28 23:20 ~ 2026-05-27 22:50"}, "overall": {"direction_accuracy": 47.2, "correct": 2112, "wrong": 2359}, "bias_breakdown": {"bull": {"n": 2177, "correct": 1057, "accuracy": 48.6}, "bear": {"n": 2294, "correct": 1055, "accuracy": 46.0}, "neutral_n": 4169}, "baseline": {"always_bull_accuracy": 51.2, "always_bear_accuracy": 48.8, "random_50": 50.0, "edge_vs_random": -2.8}, "regime_stats": {"TREND": {"n": 2395, "correct": 1108, "accuracy": 46.3}, "RANGE": {"n": 1289, "correct": 604, "accuracy": 46.9}, "LOW_VOL": {"n": 787, "correct": 400, "accuracy": 50.8}}, "confidence_stats": {"40-44": {"n": 1416, "accuracy": 46.7}, "45-49": {"n": 2367, "accuracy": 47.0}, "50-54": {"n": 652, "accuracy": 49.2}, "55-59": {"n": 36, "accuracy": 47.2}}, "monthly_stats": {"2025-11": {"n": 49, "accuracy": 36.7, "bull_ratio": 46.9}, "2025-12": {"n": 776, "accuracy": 47.7, "bull_ratio": 49.5}, "2026-01": {"n": 787, "accuracy": 47.4, "bull_ratio": 47.4}, "2026-02": {"n": 692, "accuracy": 46.1, "bull_ratio": 46.2}, "2026-03": {"n": 750, "accuracy": 48.4, "bull_ratio": 51.1}, "2026-04": {"n": 741, "accuracy": 47.9, "bull_ratio": 51.7}, "2026-05": {"n": 676, "accuracy": 46.4, "bull_ratio": 46.0}}, "streaks": {"max_win_streak": 10, "max_lose_streak": 13}, "conflict_detection": {"triggered_n": 663, "accuracy": 49.2, "overall_accuracy": 47.2}, "factor_contribution": {"momentum": {"avg_when_correct": 0.6937, "avg_when_wrong": 0.7, "signal_strength_diff": -0.0063, "directional_consistency": 50.6, "n_positive_signals": 2188}, "meanrev": {"avg_when_correct": 0.4632, "avg_when_wrong": 0.4699, "signal_strength_diff": -0.0067, "directional_consistency": 53.0, "n_positive_signals": 1920}, "rsi": {"avg_when_correct": 0.1961, "avg_when_wrong": 0.2014, "signal_strength_diff": -0.0053, "directional_consistency": 48.8, "n_positive_signals": 1601}, "volume": {"avg_when_correct": 0.1, "avg_when_wrong": 0.1, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "fatigue": {"avg_when_correct": 0.1801, "avg_when_wrong": 0.1878, "signal_strength_diff": -0.0077, "directional_consistency": 54.4, "n_positive_signals": 673}, "imbalance": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "microprice": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "decel": {"avg_when_correct": 0.5958, "avg_when_wrong": 0.5819, "signal_strength_diff": 0.0139, "directional_consistency": 51.0, "n_positive_signals": 1902}, "position": {"avg_when_correct": 0.5663, "avg_when_wrong": 0.5829, "signal_strength_diff": -0.0165, "directional_consistency": 53.0, "n_positive_signals": 1941}, "v_reversal": {"avg_when_correct": 0.0, "avg_when_wrong": 0.0, "signal_strength_diff": 0.0, "directional_consistency": null, "n_positive_signals": 0}, "vol_breakout": {"avg_when_correct": 0.1464, "avg_when_wrong": 0.1515, "signal_strength_diff": -0.0052, "directional_consistency": 49.2, "n_positive_signals": 777}, "half_body": {"avg_when_correct": 0, "avg_when_wrong": 0, "signal_strength_diff": 0, "directional_consistency": null, "n_positive_signals": 0}}, "half_body_analysis": {"activated_n": 0}}, "predictions_count": 8640}
      • backtest_v58_1min_20260528_2018.json 3.7 KB
        {
          "version": "v5.8-1min-half",
          "timestamp": "20260528_2018",
          "n_half": 2,
          "sample_rate": 6,
          "max_days": 180,
          "stats_1m": {
            "label": "v5.7 + 真实1min半K线(前2根)",
            "total_candles": 8598,
            "directional": 4477,
            "correct": 2764,
            "wrong": 1713,
            "neutral": 4121,
            "accuracy": 61.7,
            "edge_vs_random": 11.7,
            "max_win_streak": 16,
            "max_lose_streak": 12,
            "avg_win_streak": 2.6,
            "avg_lose_streak": 1.6,
            "half_body_active": 7044,
            "half_body_active_pct": 81.9,
            "half_body_dir": 3674,
            "half_body_correct": 2342,
            "half_body_acc": 63.7,
            "monthly": {
              "2025-11": {
                "total": 20,
                "correct": 13,
                "neutral": 36
              },
              "2025-12": {
                "total": 766,
                "correct": 489,
                "neutral": 722
              },
              "2026-01": {
                "total": 787,
                "correct": 483,
                "neutral": 701
              },
              "2026-02": {
                "total": 704,
                "correct": 437,
                "neutral": 640
              },
              "2026-03": {
                "total": 760,
                "correct": 477,
                "neutral": 728
              },
              "2026-04": {
                "total": 755,
                "correct": 466,
                "neutral": 685
              },
              "2026-05": {
                "total": 685,
                "correct": 399,
                "neutral": 609
              }
            },
            "factor_correlation": {
              "decel": {
                "acc": 48.6,
                "count": 3947
              },
              "fatigue": {
                "acc": 48.5,
                "count": 1266
              },
              "half_body": {
                "acc": 49.3,
                "count": 2998
              },
              "meanrev": {
                "acc": 48.8,
                "count": 3979
              },
              "momentum": {
                "acc": 50.5,
                "count": 4223
              },
              "position": {
                "acc": 48.9,
                "count": 4065
              },
              "rsi": {
                "acc": 51.3,
                "count": 2951
              },
              "v_reversal": {
                "acc": 50.0,
                "count": 1999
              },
              "vol_breakout": {
                "acc": 49.5,
                "count": 1641
              },
              "volume": {
                "acc": 57.7,
                "count": 3707
              }
            }
          },
          "stats_open": {
            "label": "v5.6 开盘baseline",
            "total_candles": 8640,
            "directional": 4471,
            "correct": 2112,
            "wrong": 2359,
            "neutral": 4169,
            "accuracy": 47.2,
            "edge_vs_random": -2.8,
            "max_win_streak": 10,
            "max_lose_streak": 13,
            "avg_win_streak": 1.9,
            "avg_lose_streak": 2.1,
            "half_body_active": 0,
            "half_body_active_pct": 0.0,
            "half_body_dir": 0,
            "half_body_correct": 0,
            "half_body_acc": 0,
            "monthly": {
              "2025-11": {
                "total": 49,
                "correct": 18,
                "neutral": 49
              },
              "2025-12": {
                "total": 776,
                "correct": 370,
                "neutral": 712
              },
              "2026-01": {
                "total": 787,
                "correct": 373,
                "neutral": 701
              },
              "2026-02": {
                "total": 692,
                "correct": 319,
                "neutral": 652
              },
              "2026-03": {
                "total": 750,
                "correct": 363,
                "neutral": 738
              },
              "2026-04": {
                "total": 741,
                "correct": 355,
                "neutral": 699
              },
              "2026-05": {
                "total": 676,
                "correct": 314,
                "neutral": 618
              }
            },
            "factor_correlation": {
              "decel": {
                "acc": 49.1,
                "count": 3845
              },
              "fatigue": {
                "acc": 49.6,
                "count": 1412
              },
              "meanrev": {
                "acc": 49.5,
                "count": 4030
              },
              "momentum": {
                "acc": 50.4,
                "count": 4225
              },
              "position": {
                "acc": 49.2,
                "count": 4096
              },
              "rsi": {
                "acc": 51.0,
                "count": 3077
              },
              "vol_breakout": {
                "acc": 51.3,
                "count": 1652
              },
              "volume": {
                "acc": 47.2,
                "count": 4471
              }
            }
          }
        }
      • backtest_v58_1min_20260528_2019.json 2 KB
        {
          "version": "v5.8-1min-half",
          "timestamp": "20260528_2019",
          "n_half": 5,
          "sample_rate": 6,
          "max_days": 180,
          "stats_1m": {
            "label": "v5.7 + 真实1min半K线(前5根)",
            "total_candles": 8598,
            "directional": 4522,
            "correct": 3442,
            "wrong": 1080,
            "neutral": 4076,
            "accuracy": 76.1,
            "edge_vs_random": 26.1,
            "max_win_streak": 27,
            "max_lose_streak": 5,
            "avg_win_streak": 4.2,
            "avg_lose_streak": 1.3,
            "half_body_active": 7583,
            "half_body_active_pct": 88.2,
            "half_body_dir": 3996,
            "half_body_correct": 3110,
            "half_body_acc": 77.8,
            "monthly": {
              "2025-11": {
                "total": 23,
                "correct": 14,
                "neutral": 33
              },
              "2025-12": {
                "total": 773,
                "correct": 585,
                "neutral": 715
              },
              "2026-01": {
                "total": 806,
                "correct": 600,
                "neutral": 682
              },
              "2026-02": {
                "total": 704,
                "correct": 531,
                "neutral": 640
              },
              "2026-03": {
                "total": 776,
                "correct": 610,
                "neutral": 712
              },
              "2026-04": {
                "total": 743,
                "correct": 584,
                "neutral": 697
              },
              "2026-05": {
                "total": 697,
                "correct": 518,
                "neutral": 597
              }
            },
            "factor_correlation": {
              "decel": {
                "acc": 48.5,
                "count": 4021
              },
              "fatigue": {
                "acc": 49.4,
                "count": 1253
              },
              "half_body": {
                "acc": 48.5,
                "count": 3462
              },
              "meanrev": {
                "acc": 48.6,
                "count": 4088
              },
              "momentum": {
                "acc": 51.5,
                "count": 4274
              },
              "position": {
                "acc": 48.3,
                "count": 4136
              },
              "rsi": {
                "acc": 51.5,
                "count": 3079
              },
              "v_reversal": {
                "acc": 49.6,
                "count": 2061
              },
              "vol_breakout": {
                "acc": 48.9,
                "count": 1643
              },
              "volume": {
                "acc": 54.4,
                "count": 3445
              }
            }
          }
        }
      • backtest_v58_1min_20260528_2020.json 2 KB
        {
          "version": "v5.8-1min-half",
          "timestamp": "20260528_2020",
          "n_half": 4,
          "sample_rate": 6,
          "max_days": 180,
          "stats_1m": {
            "label": "v5.7 + 真实1min半K线(前4根)",
            "total_candles": 8598,
            "directional": 4460,
            "correct": 3100,
            "wrong": 1360,
            "neutral": 4138,
            "accuracy": 69.5,
            "edge_vs_random": 19.5,
            "max_win_streak": 22,
            "max_lose_streak": 11,
            "avg_win_streak": 3.3,
            "avg_lose_streak": 1.4,
            "half_body_active": 7502,
            "half_body_active_pct": 87.3,
            "half_body_dir": 3922,
            "half_body_correct": 2791,
            "half_body_acc": 71.2,
            "monthly": {
              "2025-11": {
                "total": 25,
                "correct": 14,
                "neutral": 31
              },
              "2025-12": {
                "total": 779,
                "correct": 552,
                "neutral": 709
              },
              "2026-01": {
                "total": 792,
                "correct": 545,
                "neutral": 696
              },
              "2026-02": {
                "total": 698,
                "correct": 489,
                "neutral": 646
              },
              "2026-03": {
                "total": 750,
                "correct": 532,
                "neutral": 738
              },
              "2026-04": {
                "total": 736,
                "correct": 509,
                "neutral": 704
              },
              "2026-05": {
                "total": 680,
                "correct": 459,
                "neutral": 614
              }
            },
            "factor_correlation": {
              "decel": {
                "acc": 48.0,
                "count": 3943
              },
              "fatigue": {
                "acc": 49.5,
                "count": 1238
              },
              "half_body": {
                "acc": 48.5,
                "count": 3377
              },
              "meanrev": {
                "acc": 48.9,
                "count": 3989
              },
              "momentum": {
                "acc": 50.9,
                "count": 4189
              },
              "position": {
                "acc": 48.3,
                "count": 4056
              },
              "rsi": {
                "acc": 51.5,
                "count": 3010
              },
              "v_reversal": {
                "acc": 49.8,
                "count": 1984
              },
              "vol_breakout": {
                "acc": 49.1,
                "count": 1630
              },
              "volume": {
                "acc": 56.2,
                "count": 3270
              }
            }
          }
        }
      • latest_summary.json 5.1 KB
        {
          "timestamp": "20260527_2328",
          "stats": {
            "meta": {
              "total_predictions": 104920,
              "directional_predictions": 54499,
              "neutral_predictions": 50421,
              "backtest_period": "2025-05-28 16:00 ~ 2026-05-27 23:15"
            },
            "overall": {
              "direction_accuracy": 48.6,
              "correct": 26470,
              "wrong": 28029
            },
            "bias_breakdown": {
              "bull": {
                "n": 26474,
                "correct": 12793,
                "accuracy": 48.3
              },
              "bear": {
                "n": 28025,
                "correct": 13677,
                "accuracy": 48.8
              },
              "neutral_n": 50421
            },
            "baseline": {
              "always_bull_accuracy": 49.6,
              "always_bear_accuracy": 50.4,
              "random_50": 50.0,
              "edge_vs_random": -1.4
            },
            "regime_stats": {
              "TREND": {
                "n": 30621,
                "correct": 14533,
                "accuracy": 47.5
              },
              "RANGE": {
                "n": 15379,
                "correct": 7697,
                "accuracy": 50.0
              },
              "LOW_VOL": {
                "n": 8499,
                "correct": 4240,
                "accuracy": 49.9
              }
            },
            "confidence_stats": {
              "40-44": {
                "n": 17025,
                "accuracy": 50.2
              },
              "45-49": {
                "n": 29147,
                "accuracy": 47.6
              },
              "50-54": {
                "n": 7985,
                "accuracy": 48.6
              },
              "55-59": {
                "n": 342,
                "accuracy": 49.7
              }
            },
            "monthly_stats": {
              "2025-05": {
                "n": 515,
                "accuracy": 48.2,
                "bull_ratio": 45.0
              },
              "2025-06": {
                "n": 4481,
                "accuracy": 49.0,
                "bull_ratio": 49.6
              },
              "2025-07": {
                "n": 4548,
                "accuracy": 48.2,
                "bull_ratio": 49.5
              },
              "2025-08": {
                "n": 4616,
                "accuracy": 48.2,
                "bull_ratio": 47.1
              },
              "2025-09": {
                "n": 4553,
                "accuracy": 49.5,
                "bull_ratio": 48.3
              },
              "2025-10": {
                "n": 4595,
                "accuracy": 48.8,
                "bull_ratio": 51.2
              },
              "2025-11": {
                "n": 4440,
                "accuracy": 48.9,
                "bull_ratio": 47.3
              },
              "2025-12": {
                "n": 4675,
                "accuracy": 48.4,
                "bull_ratio": 48.0
              },
              "2026-01": {
                "n": 4715,
                "accuracy": 47.9,
                "bull_ratio": 49.1
              },
              "2026-02": {
                "n": 4155,
                "accuracy": 47.4,
                "bull_ratio": 47.0
              },
              "2026-03": {
                "n": 4638,
                "accuracy": 49.6,
                "bull_ratio": 49.1
              },
              "2026-04": {
                "n": 4479,
                "accuracy": 47.9,
                "bull_ratio": 50.2
              },
              "2026-05": {
                "n": 4089,
                "accuracy": 48.8,
                "bull_ratio": 46.6
              }
            },
            "streaks": {
              "max_win_streak": 14,
              "max_lose_streak": 14
            },
            "conflict_detection": {
              "triggered_n": 7373,
              "accuracy": 50.1,
              "overall_accuracy": 48.6
            },
            "factor_contribution": {
              "momentum": {
                "avg_when_correct": 0.6882,
                "avg_when_wrong": 0.6994,
                "signal_strength_diff": -0.0112,
                "directional_consistency": 48.1,
                "n_positive_signals": 26325
              },
              "meanrev": {
                "avg_when_correct": 0.4676,
                "avg_when_wrong": 0.4774,
                "signal_strength_diff": -0.0098,
                "directional_consistency": 51.7,
                "n_positive_signals": 24029
              },
              "rsi": {
                "avg_when_correct": 0.2012,
                "avg_when_wrong": 0.2061,
                "signal_strength_diff": -0.0048,
                "directional_consistency": 47.3,
                "n_positive_signals": 20036
              },
              "volume": {
                "avg_when_correct": 0.1,
                "avg_when_wrong": 0.1,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "fatigue": {
                "avg_when_correct": 0.1927,
                "avg_when_wrong": 0.1958,
                "signal_strength_diff": -0.0031,
                "directional_consistency": 52.3,
                "n_positive_signals": 9226
              },
              "imbalance": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "microprice": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "decel": {
                "avg_when_correct": 0.5911,
                "avg_when_wrong": 0.5739,
                "signal_strength_diff": 0.0172,
                "directional_consistency": 49.8,
                "n_positive_signals": 22713
              },
              "position": {
                "avg_when_correct": 0.5973,
                "avg_when_wrong": 0.6063,
                "signal_strength_diff": -0.0091,
                "directional_consistency": 51.6,
                "n_positive_signals": 24093
              },
              "v_reversal": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "vol_breakout": {
                "avg_when_correct": 0.1362,
                "avg_when_wrong": 0.1389,
                "signal_strength_diff": -0.0027,
                "directional_consistency": 48.0,
                "n_positive_signals": 9060
              }
            }
          }
        }
      • latest_v57_summary.json 10 KB
        {
          "timestamp": "20260528_1433",
          "stats_v57": {
            "label": "v5.7 半K线",
            "meta": {
              "total_predictions": 8640,
              "directional_predictions": 4529,
              "neutral_predictions": 4111,
              "backtest_period": "2025-11-28 23:20 ~ 2026-05-27 22:50"
            },
            "overall": {
              "direction_accuracy": 71.2,
              "correct": 3223,
              "wrong": 1306
            },
            "bias_breakdown": {
              "bull": {
                "n": 2227,
                "correct": 1588,
                "accuracy": 71.3
              },
              "bear": {
                "n": 2302,
                "correct": 1635,
                "accuracy": 71.0
              },
              "neutral_n": 4111
            },
            "baseline": {
              "always_bull_accuracy": 49.7,
              "always_bear_accuracy": 50.3,
              "random_50": 50.0,
              "edge_vs_random": 21.2
            },
            "regime_stats": {
              "TREND": {
                "n": 2503,
                "correct": 1826,
                "accuracy": 73.0
              },
              "RANGE": {
                "n": 1304,
                "correct": 927,
                "accuracy": 71.1
              },
              "LOW_VOL": {
                "n": 722,
                "correct": 470,
                "accuracy": 65.1
              }
            },
            "confidence_stats": {
              "40-44": {
                "n": 1432,
                "accuracy": 72.7
              },
              "45-49": {
                "n": 2086,
                "accuracy": 58.9
              },
              "50-54": {
                "n": 936,
                "accuracy": 93.8
              },
              "55-59": {
                "n": 75,
                "accuracy": 100.0
              }
            },
            "monthly_stats": {
              "2025-11": {
                "n": 49,
                "accuracy": 61.2,
                "bull_ratio": 51.0
              },
              "2025-12": {
                "n": 785,
                "accuracy": 72.2,
                "bull_ratio": 50.1
              },
              "2026-01": {
                "n": 783,
                "accuracy": 69.3,
                "bull_ratio": 50.4
              },
              "2026-02": {
                "n": 714,
                "accuracy": 70.7,
                "bull_ratio": 45.1
              },
              "2026-03": {
                "n": 759,
                "accuracy": 74.0,
                "bull_ratio": 51.6
              },
              "2026-04": {
                "n": 736,
                "accuracy": 72.0,
                "bull_ratio": 51.0
              },
              "2026-05": {
                "n": 703,
                "accuracy": 69.1,
                "bull_ratio": 46.2
              }
            },
            "streaks": {
              "max_win_streak": 19,
              "max_lose_streak": 6
            },
            "conflict_detection": {
              "triggered_n": 998,
              "accuracy": 79.0,
              "overall_accuracy": 71.2
            },
            "factor_contribution": {
              "momentum": {
                "avg_when_correct": 0.6959,
                "avg_when_wrong": 0.6901,
                "signal_strength_diff": 0.0058,
                "directional_consistency": 51.0,
                "n_positive_signals": 2200
              },
              "meanrev": {
                "avg_when_correct": 0.4408,
                "avg_when_wrong": 0.4829,
                "signal_strength_diff": -0.0421,
                "directional_consistency": 42.2,
                "n_positive_signals": 1969
              },
              "rsi": {
                "avg_when_correct": 0.1868,
                "avg_when_wrong": 0.1955,
                "signal_strength_diff": -0.0087,
                "directional_consistency": 58.2,
                "n_positive_signals": 1536
              },
              "volume": {
                "avg_when_correct": 0.125,
                "avg_when_wrong": 0.117,
                "signal_strength_diff": 0.008,
                "directional_consistency": 35.3,
                "n_positive_signals": 1760
              },
              "fatigue": {
                "avg_when_correct": 0.1532,
                "avg_when_wrong": 0.1995,
                "signal_strength_diff": -0.0463,
                "directional_consistency": 47.2,
                "n_positive_signals": 619
              },
              "imbalance": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "microprice": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "decel": {
                "avg_when_correct": 0.6594,
                "avg_when_wrong": 0.5749,
                "signal_strength_diff": 0.0845,
                "directional_consistency": 60.2,
                "n_positive_signals": 1970
              },
              "position": {
                "avg_when_correct": 0.5211,
                "avg_when_wrong": 0.5604,
                "signal_strength_diff": -0.0394,
                "directional_consistency": 43.8,
                "n_positive_signals": 1984
              },
              "v_reversal": {
                "avg_when_correct": 0.4091,
                "avg_when_wrong": 0.0351,
                "signal_strength_diff": 0.3741,
                "directional_consistency": 99.9,
                "n_positive_signals": 999
              },
              "vol_breakout": {
                "avg_when_correct": 0.1434,
                "avg_when_wrong": 0.1608,
                "signal_strength_diff": -0.0175,
                "directional_consistency": 51.0,
                "n_positive_signals": 789
              },
              "half_body": {
                "avg_when_correct": 0.1321,
                "avg_when_wrong": 0.107,
                "signal_strength_diff": 0.0252,
                "directional_consistency": 100.0,
                "n_positive_signals": 1282
              }
            },
            "half_body_analysis": {
              "activated_n": 4316,
              "activated_pct": 95.3,
              "activated_accuracy": 71.7,
              "strong_bull": {
                "n": 0,
                "accuracy": 0
              },
              "strong_bear": {
                "n": 0,
                "accuracy": 0
              },
              "weak_signal": {
                "n": 4316,
                "accuracy": 71.7
              },
              "direction_consistency": {
                "pos_predicts_up": 100.0,
                "neg_predicts_down": 100.0
              }
            }
          },
          "stats_v56": {
            "label": "v5.6 开盘",
            "meta": {
              "total_predictions": 8640,
              "directional_predictions": 4471,
              "neutral_predictions": 4169,
              "backtest_period": "2025-11-28 23:20 ~ 2026-05-27 22:50"
            },
            "overall": {
              "direction_accuracy": 47.2,
              "correct": 2112,
              "wrong": 2359
            },
            "bias_breakdown": {
              "bull": {
                "n": 2177,
                "correct": 1057,
                "accuracy": 48.6
              },
              "bear": {
                "n": 2294,
                "correct": 1055,
                "accuracy": 46.0
              },
              "neutral_n": 4169
            },
            "baseline": {
              "always_bull_accuracy": 51.2,
              "always_bear_accuracy": 48.8,
              "random_50": 50.0,
              "edge_vs_random": -2.8
            },
            "regime_stats": {
              "TREND": {
                "n": 2395,
                "correct": 1108,
                "accuracy": 46.3
              },
              "RANGE": {
                "n": 1289,
                "correct": 604,
                "accuracy": 46.9
              },
              "LOW_VOL": {
                "n": 787,
                "correct": 400,
                "accuracy": 50.8
              }
            },
            "confidence_stats": {
              "40-44": {
                "n": 1416,
                "accuracy": 46.7
              },
              "45-49": {
                "n": 2367,
                "accuracy": 47.0
              },
              "50-54": {
                "n": 652,
                "accuracy": 49.2
              },
              "55-59": {
                "n": 36,
                "accuracy": 47.2
              }
            },
            "monthly_stats": {
              "2025-11": {
                "n": 49,
                "accuracy": 36.7,
                "bull_ratio": 46.9
              },
              "2025-12": {
                "n": 776,
                "accuracy": 47.7,
                "bull_ratio": 49.5
              },
              "2026-01": {
                "n": 787,
                "accuracy": 47.4,
                "bull_ratio": 47.4
              },
              "2026-02": {
                "n": 692,
                "accuracy": 46.1,
                "bull_ratio": 46.2
              },
              "2026-03": {
                "n": 750,
                "accuracy": 48.4,
                "bull_ratio": 51.1
              },
              "2026-04": {
                "n": 741,
                "accuracy": 47.9,
                "bull_ratio": 51.7
              },
              "2026-05": {
                "n": 676,
                "accuracy": 46.4,
                "bull_ratio": 46.0
              }
            },
            "streaks": {
              "max_win_streak": 10,
              "max_lose_streak": 13
            },
            "conflict_detection": {
              "triggered_n": 663,
              "accuracy": 49.2,
              "overall_accuracy": 47.2
            },
            "factor_contribution": {
              "momentum": {
                "avg_when_correct": 0.6937,
                "avg_when_wrong": 0.7,
                "signal_strength_diff": -0.0063,
                "directional_consistency": 50.6,
                "n_positive_signals": 2188
              },
              "meanrev": {
                "avg_when_correct": 0.4632,
                "avg_when_wrong": 0.4699,
                "signal_strength_diff": -0.0067,
                "directional_consistency": 53.0,
                "n_positive_signals": 1920
              },
              "rsi": {
                "avg_when_correct": 0.1961,
                "avg_when_wrong": 0.2014,
                "signal_strength_diff": -0.0053,
                "directional_consistency": 48.8,
                "n_positive_signals": 1601
              },
              "volume": {
                "avg_when_correct": 0.1,
                "avg_when_wrong": 0.1,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "fatigue": {
                "avg_when_correct": 0.1801,
                "avg_when_wrong": 0.1878,
                "signal_strength_diff": -0.0077,
                "directional_consistency": 54.4,
                "n_positive_signals": 673
              },
              "imbalance": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "microprice": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "decel": {
                "avg_when_correct": 0.5958,
                "avg_when_wrong": 0.5819,
                "signal_strength_diff": 0.0139,
                "directional_consistency": 51.0,
                "n_positive_signals": 1902
              },
              "position": {
                "avg_when_correct": 0.5663,
                "avg_when_wrong": 0.5829,
                "signal_strength_diff": -0.0165,
                "directional_consistency": 53.0,
                "n_positive_signals": 1941
              },
              "v_reversal": {
                "avg_when_correct": 0.0,
                "avg_when_wrong": 0.0,
                "signal_strength_diff": 0.0,
                "directional_consistency": null,
                "n_positive_signals": 0
              },
              "vol_breakout": {
                "avg_when_correct": 0.1464,
                "avg_when_wrong": 0.1515,
                "signal_strength_diff": -0.0052,
                "directional_consistency": 49.2,
                "n_positive_signals": 777
              },
              "half_body": {
                "avg_when_correct": 0,
                "avg_when_wrong": 0,
                "signal_strength_diff": 0,
                "directional_consistency": null,
                "n_positive_signals": 0
              }
            },
            "half_body_analysis": {
              "activated_n": 0
            }
          }
        }
      • latest_v58_1min_summary.json 245 B
        {
          "version": "v5.8-1min-half",
          "timestamp": "20260528_2020",
          "n_half": 4,
          "accuracy_1m": 69.5,
          "edge_vs_random": 19.5,
          "directional": 4460,
          "correct": 3100,
          "half_body_acc": 71.2,
          "max_win_streak": 22,
          "max_lose_streak": 11
        }
    • fetch_data.py 6 KB
      #!/usr/bin/env python3
      """5minbtc Backtest Data Fetcher — 从 Binance 下载历史5分钟K线数据
      
      用法:
        python fetch_data.py              # 下载1年数据(默认)
        python fetch_data.py --days 180   # 下载180天
        python fetch_data.py --force      # 强制重新下载
        python fetch_data.py --status     # 查看缓存状态
      
      数据源: Binance data-api.binance.vision (公开免费)
      输出格式: data/btcusdt_5m.json
      """
      import json, os, sys, time, urllib.request
      
      SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
      DATA_DIR = os.path.join(SCRIPT_DIR, "data")
      DATA_FILE = os.path.join(DATA_DIR, "btcusdt_5m.json")
      BINANCE_API = "https://data-api.binance.vision/api/v3/klines"
      BATCH_SIZE = 1000
      RATE_LIMIT_SEC = 0.05  # 请求间隔 (Binance 公开API限制 1200 req/min)
      
      
      def fetch_batch(symbol, interval, start_ms, end_ms):
          """获取一批K线数据(最多1000根)"""
          url = (f"{BINANCE_API}?symbol={symbol}&interval={interval}"
                 f"&startTime={start_ms}&endTime={end_ms}&limit={BATCH_SIZE}")
          req = urllib.request.Request(url, headers={"User-Agent": "5minbtc-backtest/1.0"})
          with urllib.request.urlopen(req, timeout=15) as resp:
              return json.loads(resp.read())
      
      
      def download(days=365, force=False):
          """下载指定期限的5分钟K线数据"""
          now_ms = int(time.time() * 1000)
          start_ms = now_ms - days * 24 * 3600 * 1000
      
          # 检查缓存
          if not force and os.path.exists(DATA_FILE):
              with open(DATA_FILE) as f:
                  cached = json.load(f)
              if cached:
                  first_time = cached[0].get("ot", cached[0].get("ct", 0))
                  last_time = cached[-1].get("ct", 0)
                  cached_days = (last_time - first_time) / (24 * 3600 * 1000)
                  age_hours = (now_ms - last_time) / (3600 * 1000)
                  print(f"📦 缓存: {len(cached)} 根K线, 覆盖 {cached_days:.0f} 天, "
                        f"数据 {age_hours:.1f} 小时前")
                  if age_hours < 24 and cached_days >= days * 0.9:
                      print(f"✅ 缓存有效(<24h且覆盖>{days*0.9:.0f}天),跳过下载")
                      print(f"   时间范围: {_ms_to_str(first_time)} ~ {_ms_to_str(last_time)}")
                      return DATA_FILE
                  print("⏳ 缓存过期或不足,重新下载...")
      
          print(f"🚀 下载 BTCUSDT 5m 数据: 最近 {days} 天")
          print(f"   预计 ~{days * 288:,} 根K线, ~{days * 288 // BATCH_SIZE + 1} 批请求")
      
          all_raw = []
          cursor = start_ms
          batch_num = 0
      
          while cursor < now_ms:
              try:
                  batch = fetch_batch("BTCUSDT", "5m", cursor, now_ms)
              except Exception as e:
                  print(f"❌ 请求失败 (batch {batch_num}): {e}")
                  if batch_num > 0:
                      print("   保留已下载的数据")
                      break
                  raise
      
              if not batch:
                  break
      
              all_raw.extend(batch)
              batch_num += 1
      
              # 进度报告
              if batch_num % 10 == 0:
                  last_ts = batch[-1][0]
                  pct = (last_ts - start_ms) / (now_ms - start_ms) * 100
                  print(f"   已下载 {len(all_raw):,} 根 ({pct:.0f}%) — "
                        f"batch {batch_num}, {_ms_to_str(last_ts)}")
      
              # 移动游标到最后一根K线的 close_time + 1
              cursor = batch[-1][6] + 1
              time.sleep(RATE_LIMIT_SEC)
      
          # 转换格式并去重
          candles = []
          for c in all_raw:
              candles.append({
                  "ot": int(c[0]),     # Open time
                  "o": float(c[1]),    # Open
                  "h": float(c[2]),    # High
                  "l": float(c[3]),    # Low
                  "c": float(c[4]),    # Close
                  "v": float(c[5]),    # Volume
                  "ct": int(c[6]),     # Close time
              })
      
          # 按 open_time 去重
          seen = set()
          unique = []
          for c in candles:
              if c["ot"] not in seen:
                  seen.add(c["ot"])
                  unique.append(c)
          candles = sorted(unique, key=lambda x: x["ot"])
      
          # 保存
          os.makedirs(DATA_DIR, exist_ok=True)
          with open(DATA_FILE, "w") as f:
              json.dump(candles, f)
      
          # 统计
          first = candles[0]
          last = candles[-1]
          days_covered = (last["ct"] - first["ot"]) / (24 * 3600 * 1000)
          size_mb = os.path.getsize(DATA_FILE) / 1024 / 1024
      
          print(f"\n✅ 下载完成!")
          print(f"   总K线数: {len(candles):,}")
          print(f"   时间范围: {_ms_to_str(first['ot'])} ~ {_ms_to_str(last['ct'])}")
          print(f"   覆盖天数: {days_covered:.0f} 天")
          print(f"   文件大小: {size_mb:.1f} MB")
          print(f"   保存路径: {DATA_FILE}")
      
          return DATA_FILE
      
      
      def show_status():
          """显示缓存数据状态"""
          if not os.path.exists(DATA_FILE):
              print("❌ 无缓存数据")
              return
      
          with open(DATA_FILE) as f:
              data = json.load(f)
      
          if not data:
              print("❌ 缓存为空")
              return
      
          first = data[0]
          last = data[-1]
          days = (last["ct"] - first["ot"]) / (24 * 3600 * 1000)
          age_h = (time.time() * 1000 - last["ct"]) / (3600 * 1000)
          size_mb = os.path.getsize(DATA_FILE) / 1024 / 1024
      
          print(f"📊 缓存状态:")
          print(f"   K线数: {len(data):,}")
          print(f"   品种: BTCUSDT 5m")
          print(f"   时间范围: {_ms_to_str(first['ot'])} ~ {_ms_to_str(last['ct'])}")
          print(f"   覆盖天数: {days:.0f} 天")
          print(f"   数据时效: {age_h:.1f} 小时前")
          print(f"   文件大小: {size_mb:.1f} MB")
      
          # 价格范围
          prices = [c["c"] for c in data]
          print(f"   价格范围: ${min(prices):,.0f} ~ ${max(prices):,.0f}")
      
      
      def _ms_to_str(ms):
          """毫秒时间戳转可读字符串"""
          return time.strftime("%Y-%m-%d %H:%M", time.localtime(ms / 1000))
      
      
      if __name__ == "__main__":
          args = sys.argv[1:]
      
          if "--status" in args:
              show_status()
          else:
              days = 365
              for a in args:
                  if a.startswith("--days="):
                      days = int(a.split("=")[1])
                  elif a == "--days" and args.index(a) + 1 < len(args):
                      days = int(args[args.index(a) + 1])
              force = "--force" in args
              download(days=days, force=force)
      
    • README.md 2.7 KB
      # 5minbtc Backtest System
      
      ## 快速使用
      
      ```bash
      # 一键回测 (下载最新数据 + 运行回测)
      cd backtest && ./run.sh
      
      # 快速回测 (每小时采样, 最近180天)
      ./run.sh --fast
      
      # 只回测最近90天, 每6根K线采样
      ./run.sh --days 90 --sample=6
      
      # 完整因子分析报告
      ./run.sh --full-report
      
      # 跳过数据下载 (使用缓存)
      ./run.sh --skip-fetch
      
      # 自定义 candle_progress (模拟K线完成度)
      python3 run_backtest.py --progress=0.5
      ```
      
      ## 文件结构
      
      ```
      backtest/
      ├── fetch_data.py        # Binance 历史数据下载器
      ├── run_backtest.py      # 回测引擎 (导入 v5.6 因子模块)
      ├── run.sh               # 一键运行脚本
      ├── README.md            # 本文件
      ├── data/                # 缓存的K线数据 (JSON)
      │   └── btcusdt_5m.json  # 365天 x 288根/天 ≈ 105k 根
      └── results/             # 回测结果
          ├── backtest_YYYYMMDD_HHMM.json  # 完整结果
          └── latest_summary.json          # 最新摘要
      ```
      
      ## 参数说明
      
      | 参数 | 默认值 | 说明 |
      |------|--------|------|
      | `--sample=N` | 1 | 采样率: 1=每根K线, 6=每30min, 12=每小时 |
      | `--days=N` | 全部 | 回测最近N天 |
      | `--progress=F` | 0.01 | candle_progress: 0.01=刚开盘, 1.0=已完成 |
      | `--fast` | - | 快速模式: sample=12 + days=180 |
      | `--full-report` | - | 输出完整因子贡献分析 |
      | `--force` | - | 强制重新下载数据 |
      
      ## 回测方法论
      
      ### 公平性保障
      - **无前视偏差**: 当前K线价格信息被屏蔽 (c=h=l=open, v=0)
      - **仅用历史数据**: 因子仅基于200根已完成K线计算
      - **无 orderbook**: imbalance/microprice 设为0 (回测不可获取)
      
      ### 模式对比
      
      | 模式 | candle_progress | 当前K线数据 | 含义 |
      |------|----------------|-------------|------|
      | 保守 (默认) | 0.01 | 屏蔽 | 模拟K线刚开盘, 最公平 |
      | 中等 | 0.5 | 屏蔽 | 模拟K线中段 |
      | 前视 | 1.0 | 使用实际值 | 含未来信息, 仅用于分析 |
      
      ### 指标体系
      - **方向准确率**: bull/bear 预测与实际涨跌一致的比例
      - **Regime 分析**: 按 TREND/RANGE/HIGH_VOL/LOW_VOL 分组
      - **置信度分层**: 按 confidence 5分档统计准确率
      - **因子贡献**: 每个因子在正确/错误预测中的信号强度差异
      - **Baseline 对比**: vs Always-Bull / Always-Bear / Random(50%)
      
      ## ⚠️ 已知局限性
      
      1. **无 orderbook 数据**: imbalance/microprice 始终为0, 可能低估实盘表现
      2. **无 Chainlink 补偿**: 回测使用 Binance 价格, 未模拟 Coinbase 偏移
      3. **无新闻信号**: news_factor 未集成
      4. **K线模拟简化**: 实际实盘在 K线末段(progress=0.9+)有更多信息
      
      ---
      
      *Created: 2026-05-27 | Engine: v5.6*
      
    • run.sh 2.8 KB
      #!/usr/bin/env bash
      # 5minbtc Backtest Runner — 一键运行完整回测
      #
      # 用法:
      #   ./run.sh                  # 下载最新数据 + 完整回测
      #   ./run.sh --fast           # 快速回测 (每小时采样, 最近180天)
      #   ./run.sh --sample 6       # 每30分钟采样
      #   ./run.sh --days 90        # 只回测最近90天
      #   ./run.sh --full-report    # 完整因子分析报告
      #   ./run.sh --skip-fetch     # 跳过数据下载, 直接回测
      
      set -euo pipefail
      cd "$(dirname "$0")"
      
      # 颜色
      GREEN='\033[0;32m'
      YELLOW='\033[1;33m'
      RED='\033[0;31m'
      NC='\033[0m'
      
      echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
      echo -e "${GREEN}  📊 5minbtc Engine v5.6 — Backtest Runner${NC}"
      echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
      echo ""
      
      SKIP_FETCH=false
      EXTRA_ARGS=""
      
      for arg in "$@"; do
          case $arg in
              --skip-fetch) SKIP_FETCH=true ;;
              --fast) EXTRA_ARGS="$EXTRA_ARGS --fast" ;;
              --full-report) EXTRA_ARGS="$EXTRA_ARGS --full-report" ;;
              --sample=*) EXTRA_ARGS="$EXTRA_ARGS $arg" ;;
              --days=*) EXTRA_ARGS="$EXTRA_ARGS $arg" ;;
              --force) EXTRA_ARGS="$EXTRA_ARGS --force" ;;
          esac
      done
      
      # Step 1: 数据下载
      if [ "$SKIP_FETCH" = false ]; then
          echo -e "${YELLOW}📡 Step 1: 下载/更新K线数据${NC}"
          python3 fetch_data.py
          echo ""
      else
          echo -e "${YELLOW}📡 Step 1: 跳过数据下载 (--skip-fetch)${NC}"
          if [ ! -f "data/btcusdt_5m.json" ]; then
              echo -e "${RED}❌ 数据文件不存在, 请先运行不带 --skip-fetch 的命令${NC}"
              exit 1
          fi
      fi
      
      # Step 2: 数据检查
      echo -e "${YELLOW}📂 Step 2: 验证数据${NC}"
      python3 -c "
      import json, os
      if not os.path.exists('data/btcusdt_5m.json'):
          print('❌ 数据文件不存在')
          exit(1)
      with open('data/btcusdt_5m.json') as f:
          data = json.load(f)
      import time
      days = (data[-1]['ct'] - data[0]['ot']) / (24*3600*1000)
      print(f'✅ {len(data):,} 根K线, {days:.0f} 天覆盖')
      if days < 30:
          print('⚠️  数据不足30天, 建议重新下载')
      "
      echo ""
      
      # Step 3: 运行回测
      echo -e "${YELLOW}🔄 Step 3: 运行回测${NC}"
      python3 run_backtest.py $EXTRA_ARGS
      
      echo ""
      echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
      echo -e "${GREEN}  ✅ 回测完成!${NC}"
      echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
      echo ""
      echo "结果文件:"
      ls -lh results/ 2>/dev/null || echo "  (无结果文件)"
      
    • run_backtest.py 19.7 KB
      #!/usr/bin/env python3
      """5minbtc Engine v5.6 Backtester — 历史回测引擎
      
      核心逻辑:
        - 对每根K线[i], 使用 candles[i-warmup:i] (200根已完成K线) 计算因子
        - 附加 candles[i] 并屏蔽为 "刚开盘" 状态 (c=h=l=open, v=0)
        - 用引擎 v5.6 全套因子体系预测方向
        - 对比实际方向: candles[i].c vs candles[i-1].c
      
      用法:
        python run_backtest.py                    # 完整回测 (每根K线)
        python run_backtest.py --sample 6         # 每6根K线取1根 (30分钟间隔)
        python run_backtest.py --days 90          # 只回测最近90天
        python run_backtest.py --fast             # 快速模式: sample=12 + 最近180天
        python run_backtest.py --full-report      # 输出完整因子分析报告
      """
      
      import json, math, os, sys, time, importlib.util
      from collections import defaultdict
      from datetime import datetime, timezone, timedelta
      
      SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
      SKILL_DIR = os.path.dirname(SCRIPT_DIR)
      ENGINE_PATH = os.path.join(SKILL_DIR, "archive", "engines", "5minbtc-engine-v5.py")
      DATA_FILE = os.path.join(SCRIPT_DIR, "data", "btcusdt_5m.json")
      RESULTS_DIR = os.path.join(SCRIPT_DIR, "results")
      
      CST = timezone(timedelta(hours=8))
      WARMUP = 200  # 因子计算所需的最少历史K线数
      
      
      # ======================== Engine Import ========================
      
      def load_engine():
          """动态导入引擎模块(避免 __name__ == '__main__' 执行 run())"""
          if not os.path.exists(ENGINE_PATH):
              print(f"❌ 引擎文件不存在: {ENGINE_PATH}")
              sys.exit(1)
          spec = importlib.util.spec_from_file_location("engine_v5", ENGINE_PATH)
          mod = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(mod)
          return mod
      
      
      # ======================== Backtest Core ========================
      
      def run_backtest(engine, candles, sample_rate=1, max_days=None, 
                       candle_progress=0.01, verbose=True):
          """
          回测主循环
          
          参数:
            engine: 引擎模块
            candles: K线数据列表
            sample_rate: 采样率 (1=每根, 6=每30分钟, 12=每小时)
            max_days: 最多回测多少天 (None=全部)
            verbose: 是否输出进度
          
          返回:
            results: 预测结果列表
            stats: 统计摘要
          """
          # 截取时间范围
          if max_days and len(candles) > WARMUP + 100:
              cutoff_ms = candles[-1]["ct"] - max_days * 24 * 3600 * 1000
              start_idx = WARMUP
              for i in range(WARMUP, len(candles)):
                  if candles[i]["ot"] >= cutoff_ms:
                      start_idx = i
                      break
              # 确保至少有 warmup 根历史
              start_idx = max(start_idx, WARMUP)
          else:
              start_idx = WARMUP
      
          total = len(candles) - start_idx
          total_samples = (total + sample_rate - 1) // sample_rate
      
          if verbose:
              print(f"\n🔄 开始回测")
              print(f"   数据范围: {_ts(candles[start_idx]['ot'])} ~ {_ts(candles[-1]['ct'])}")
              print(f"   总K线: {total:,} | 采样: {total_samples:,} (rate={sample_rate})")
              print(f"   引擎: v{getattr(engine, '__doc__', '').split('v5.')[1].split()[0] if 'v5.' in getattr(engine, '__doc__', '') else '5.6'}")
              print()
      
          results = []
          t0 = time.time()
      
          for idx, i in enumerate(range(start_idx, len(candles), sample_rate)):
              # 200 根已完成 K线
              window = candles[i - WARMUP:i]
      
              # 当前K线模拟"刚开盘" — 屏蔽 price/volume 信息
              raw = candles[i]
              open_price = raw["o"]
              simulated = {
                  "o": open_price, "h": open_price, "l": open_price,
                  "c": open_price, "v": 0.0, "ct": raw["ct"], "ot": raw["ot"]
              }
      
              full_candles = window + [simulated]
              closes = [c["c"] for c in full_candles]
      
              # ---- 指标计算 (与引擎 run() 一致) ----
              atr_val = engine.atr_wilder(full_candles)
              vr = engine.vol_regime_ratio(closes)
      
              # ---- 方向预测 (无 orderbook, candle_progress 由参数控制) ----
              bias, strength, confidence, score, factors, regime = engine.direction_rule_v5(
                  full_candles, closes, atr_val, vr,
                  depth_data=None, candle_progress=candle_progress
              )
      
              # ---- 实际结果 ----
              actual_close = raw["c"]
              prev_close = candles[i - 1]["c"]
              price_chg = actual_close - prev_close
              actual_dir = "UP" if price_chg > 0 else ("DOWN" if price_chg < 0 else "FLAT")
      
              # 方向正确性
              if bias == "neutral":
                  dir_correct = None  # neutral 不计入方向统计
              elif bias == "bull" and actual_dir == "UP":
                  dir_correct = True
              elif bias == "bear" and actual_dir == "DOWN":
                  dir_correct = True
              else:
                  dir_correct = False
      
              # 冲突检测标记
              mom_val = factors.get("momentum", 0)
              decel_val = factors.get("decel", 0)
              conflict = abs(mom_val) > 0.7 and abs(decel_val) > 0.8 and mom_val * decel_val < 0
      
              results.append({
                  "idx": i,
                  "ts": raw["ot"],
                  "open": open_price,
                  "actual_close": actual_close,
                  "prev_close": prev_close,
                  "price_chg": price_chg,
                  "actual_dir": actual_dir,
                  "bias": bias,
                  "strength": strength,
                  "confidence": confidence,
                  "score": score,
                  "regime": regime,
                  "dir_correct": dir_correct,
                  "conflict": conflict,
                  "factors": {k: round(v, 4) if isinstance(v, float) else v
                              for k, v in factors.items()},
              })
      
              # 进度报告
              if verbose and (idx + 1) % max(1, total_samples // 20) == 0:
                  elapsed = time.time() - t0
                  pct = (idx + 1) / total_samples * 100
                  eta = elapsed / (idx + 1) * (total_samples - idx - 1)
                  # 实时方向准确率
                  recent = [r for r in results[-200:] if r["dir_correct"] is not None]
                  if recent:
                      acc = sum(1 for r in recent if r["dir_correct"]) / len(recent) * 100
                  else:
                      acc = 0
                  print(f"   [{pct:5.1f}%] {idx+1:,}/{total_samples:,} | "
                        f"近期方向准确率: {acc:.1f}% | ETA: {eta:.0f}s")
      
          elapsed = time.time() - t0
          if verbose:
              print(f"\n✅ 回测完成! 耗时 {elapsed:.1f}s ({len(results):,} 条预测)")
      
          stats = compute_stats(results)
          return results, stats
      
      
      # ======================== Statistics ========================
      
      def compute_stats(results):
          """计算全面统计"""
          if not results:
              return {}
      
          # ---- 基础统计 ----
          directional = [r for r in results if r["dir_correct"] is not None]
          n_dir = len(directional)
          n_correct = sum(1 for r in directional if r["dir_correct"])
          n_wrong = n_dir - n_correct
      
          # ---- Bias 分布 ----
          bull_all = [r for r in directional if r["bias"] == "bull"]
          bear_all = [r for r in directional if r["bias"] == "bear"]
          neutral_all = [r for r in results if r["bias"] == "neutral"]
          bull_correct = sum(1 for r in bull_all if r["dir_correct"])
          bear_correct = sum(1 for r in bear_all if r["dir_correct"])
      
          # ---- Regime 统计 ----
          regime_stats = {}
          for rg in ["TREND", "RANGE", "HIGH_VOL", "LOW_VOL"]:
              subset = [r for r in directional if r["regime"] == rg]
              if subset:
                  correct = sum(1 for r in subset if r["dir_correct"])
                  regime_stats[rg] = {
                      "n": len(subset),
                      "correct": correct,
                      "accuracy": round(correct / len(subset) * 100, 1),
                  }
      
          # ---- 置信度分层 ----
          conf_buckets = defaultdict(lambda: {"n": 0, "correct": 0})
          for r in directional:
              bucket = (r["confidence"] // 5) * 5  # 5分一档
              conf_buckets[bucket]["n"] += 1
              if r["dir_correct"]:
                  conf_buckets[bucket]["correct"] += 1
          conf_stats = {}
          for bucket in sorted(conf_buckets.keys()):
              b = conf_buckets[bucket]
              conf_stats[f"{bucket}-{bucket+4}"] = {
                  "n": b["n"],
                  "accuracy": round(b["correct"] / b["n"] * 100, 1) if b["n"] > 0 else 0,
              }
      
          # ---- 月度统计 ----
          monthly = defaultdict(lambda: {"n": 0, "correct": 0, "bull": 0, "bear": 0})
          for r in directional:
              month = time.strftime("%Y-%m", time.localtime(r["ts"] / 1000))
              monthly[month]["n"] += 1
              if r["dir_correct"]:
                  monthly[month]["correct"] += 1
              if r["bias"] == "bull":
                  monthly[month]["bull"] += 1
              else:
                  monthly[month]["bear"] += 1
          monthly_stats = {}
          for m in sorted(monthly.keys()):
              monthly_stats[m] = {
                  "n": monthly[m]["n"],
                  "accuracy": round(monthly[m]["correct"] / monthly[m]["n"] * 100, 1),
                  "bull_ratio": round(monthly[m]["bull"] / monthly[m]["n"] * 100, 1),
              }
      
          # ---- 连胜/连败 ----
          streaks = _compute_streaks(directional)
          
          # ---- Baseline 对比 ----
          actual_up = sum(1 for r in directional if r["actual_dir"] == "UP")
          baseline_bull = round(actual_up / n_dir * 100, 1) if n_dir > 0 else 0
          baseline_bear = round((n_dir - actual_up) / n_dir * 100, 1) if n_dir > 0 else 0
      
          # ---- 冲突检测统计 ----
          conflict_all = [r for r in directional if r.get("conflict")]
          conflict_correct = sum(1 for r in conflict_all if r["dir_correct"])
      
          # ---- 因子贡献分析 ----
          factor_contribution = _analyze_factors(directional)
      
          # ---- 合成 ----
          stats = {
              "meta": {
                  "total_predictions": len(results),
                  "directional_predictions": n_dir,
                  "neutral_predictions": len(neutral_all),
                  "backtest_period": (
                      f"{_ts(results[0]['ts'])} ~ {_ts(results[-1]['ts'])}"
                  ),
              },
              "overall": {
                  "direction_accuracy": round(n_correct / n_dir * 100, 1) if n_dir > 0 else 0,
                  "correct": n_correct,
                  "wrong": n_wrong,
              },
              "bias_breakdown": {
                  "bull": {"n": len(bull_all), "correct": bull_correct,
                           "accuracy": round(bull_correct / len(bull_all) * 100, 1) if bull_all else 0},
                  "bear": {"n": len(bear_all), "correct": bear_correct,
                           "accuracy": round(bear_correct / len(bear_all) * 100, 1) if bear_all else 0},
                  "neutral_n": len(neutral_all),
              },
              "baseline": {
                  "always_bull_accuracy": baseline_bull,
                  "always_bear_accuracy": baseline_bear,
                  "random_50": 50.0,
                  "edge_vs_random": round(n_correct / n_dir * 100 - 50, 1) if n_dir > 0 else 0,
              },
              "regime_stats": regime_stats,
              "confidence_stats": conf_stats,
              "monthly_stats": monthly_stats,
              "streaks": streaks,
              "conflict_detection": {
                  "triggered_n": len(conflict_all),
                  "accuracy": round(conflict_correct / len(conflict_all) * 100, 1) if conflict_all else 0,
                  "overall_accuracy": round(n_correct / n_dir * 100, 1) if n_dir > 0 else 0,
              },
              "factor_contribution": factor_contribution,
          }
          return stats
      
      
      def _compute_streaks(directional):
          """计算连胜/连败"""
          win_streak, lose_streak = 0, 0
          max_win, max_lose = 0, 0
          current_win, current_lose = 0, 0
      
          for r in directional:
              if r["dir_correct"]:
                  current_win += 1
                  current_lose = 0
                  max_win = max(max_win, current_win)
              else:
                  current_lose += 1
                  current_win = 0
                  max_lose = max(max_lose, current_lose)
      
          return {
              "max_win_streak": max_win,
              "max_lose_streak": max_lose,
          }
      
      
      def _analyze_factors(directional):
          """分析每个因子对正确/错误预测的贡献"""
          factor_names = ["momentum", "meanrev", "rsi", "volume", "fatigue",
                          "imbalance", "microprice", "decel", "position",
                          "v_reversal", "vol_breakout"]
      
          analysis = {}
          for fn in factor_names:
              correct_vals = [abs(r["factors"].get(fn, 0)) for r in directional
                             if r["dir_correct"] and fn in r.get("factors", {})]
              wrong_vals = [abs(r["factors"].get(fn, 0)) for r in directional
                           if not r["dir_correct"] and fn in r.get("factors", {})]
      
              avg_c = sum(correct_vals) / len(correct_vals) if correct_vals else 0
              avg_w = sum(wrong_vals) / len(wrong_vals) if wrong_vals else 0
      
              # 因子方向一致性: 因子值>0时实际上涨的比例
              pos_signals = [r for r in directional if r["factors"].get(fn, 0) > 0.1]
              if pos_signals:
                  pos_up = sum(1 for r in pos_signals if r["actual_dir"] == "UP")
                  consistency = round(pos_up / len(pos_signals) * 100, 1)
              else:
                  consistency = None
      
              analysis[fn] = {
                  "avg_when_correct": round(avg_c, 4),
                  "avg_when_wrong": round(avg_w, 4),
                  "signal_strength_diff": round(avg_c - avg_w, 4),
                  "directional_consistency": consistency,
                  "n_positive_signals": len(pos_signals) if pos_signals else 0,
              }
      
          return analysis
      
      
      # ======================== Report ========================
      
      def print_report(stats, verbose=False):
          """输出回测报告"""
          s = stats
          o = s["overall"]
          m = s["meta"]
      
          print("\n" + "=" * 65)
          print("📊 5minbtc Engine v5.6 — 回测报告")
          print("=" * 65)
      
          print(f"\n📅 回测区间: {m['backtest_period']}")
          print(f"   总预测: {m['total_predictions']:,} | "
                f"方向性: {m['directional_predictions']:,} | "
                f"Neutral: {m['neutral_predictions']:,}")
      
          # ---- 总体方向准确率 ----
          print(f"\n🎯 方向准确率: {o['direction_accuracy']}% "
                f"({o['correct']:,}/{m['directional_predictions']:,})")
      
          # ---- vs Baseline ----
          bl = s["baseline"]
          print(f"   vs Always-Bull: {bl['always_bull_accuracy']}%")
          print(f"   vs Always-Bear: {bl['always_bear_accuracy']}%")
          print(f"   vs Random(50%): +{bl['edge_vs_random']}pp edge")
      
          # ---- Bull/Bear 分项 ----
          bb = s["bias_breakdown"]
          print(f"\n🐂 Bull: {bb['bull']['accuracy']}% ({bb['bull']['correct']}/{bb['bull']['n']})")
          print(f"🐻 Bear: {bb['bear']['accuracy']}% ({bb['bear']['correct']}/{bb['bear']['n']})")
          if bb["neutral_n"] > 0:
              print(f"⚖️  Neutral: {bb['neutral_n']}")
      
          # ---- Regime ----
          print(f"\n🌊 Regime 分析:")
          for rg, rs in s["regime_stats"].items():
              bar = "█" * int(rs["accuracy"] / 2) + "░" * (50 - int(rs["accuracy"] / 2))
              print(f"   {rg:10s}: {rs['accuracy']:5.1f}% ({rs['correct']}/{rs['n']:,}) {bar}")
      
          # ---- 冲突检测 ----
          cd = s["conflict_detection"]
          if cd["triggered_n"] > 0:
              print(f"\n⚡ Momentum/Decel 冲突检测:")
              print(f"   触发次数: {cd['triggered_n']:,}")
              print(f"   冲突时准确率: {cd['accuracy']}% (整体: {cd['overall_accuracy']}%)")
      
          # ---- 置信度 ----
          print(f"\n📈 置信度分层:")
          for bucket, cs in s["confidence_stats"].items():
              if cs["n"] >= 10:
                  marker = "✅" if cs["accuracy"] > 55 else ("⚠️" if cs["accuracy"] > 50 else "❌")
                  print(f"   conf {bucket}: {cs['accuracy']:5.1f}% ({cs['n']:,}) {marker}")
      
          # ---- 月度趋势 ----
          ms = s["monthly_stats"]
          if len(ms) > 1:
              print(f"\n📅 月度趋势:")
              for month, md in ms.items():
                  bar = "▓" * int(md["accuracy"] / 2)
                  print(f"   {month}: {md['accuracy']:5.1f}% ({md['n']:>5,}) bull={md['bull_ratio']:.0f}% {bar}")
      
          # ---- 连胜/连败 ----
          sk = s["streaks"]
          print(f"\n🔥 最长连胜: {sk['max_win_streak']} | 💀 最长连败: {sk['max_lose_streak']}")
      
          # ---- 因子分析 (verbose) ----
          if verbose:
              print(f"\n🔬 因子贡献分析:")
              fc = s["factor_contribution"]
              # 按信号强度差异排序
              sorted_factors = sorted(fc.items(),
                                     key=lambda x: abs(x[1]["signal_strength_diff"]),
                                     reverse=True)
              for fn, fa in sorted_factors:
                  diff_marker = "↑" if fa["signal_strength_diff"] > 0 else "↓"
                  cons_str = (f"一致性={fa['directional_consistency']}%"
                             if fa["directional_consistency"] is not None else "N/A")
                  print(f"   {fn:15s}: 正确时avg={fa['avg_when_correct']:.3f} "
                        f"错误时avg={fa['avg_when_wrong']:.3f} "
                        f"diff={fa['signal_strength_diff']:+.3f}{diff_marker} "
                        f"{cons_str}")
      
          print("\n" + "=" * 65)
      
      
      def save_results(results, stats, sample_rate, max_days):
          """保存回测结果到文件"""
          os.makedirs(RESULTS_DIR, exist_ok=True)
      
          ts = datetime.now(CST).strftime("%Y%m%d_%H%M")
          result_file = os.path.join(RESULTS_DIR, f"backtest_{ts}.json")
          summary_file = os.path.join(RESULTS_DIR, "latest_summary.json")
      
          # 完整结果 (不含因子详情以节省空间, 可选)
          output = {
              "timestamp": ts,
              "engine": "v5.6",
              "sample_rate": sample_rate,
              "max_days": max_days,
              "stats": stats,
              "predictions_count": len(results),
              # 完整预测列表 — 太大则只保留统计
              "predictions": results if len(results) <= 50000 else [
                  r for i, r in enumerate(results) if i % (len(results) // 50000 + 1) == 0
              ],
          }
      
          with open(result_file, "w") as f:
              json.dump(output, f, ensure_ascii=False, indent=None)
      
          # 摘要文件 (总是最新)
          with open(summary_file, "w") as f:
              json.dump({"timestamp": ts, "stats": stats}, f, ensure_ascii=False, indent=2)
      
          size_mb = os.path.getsize(result_file) / 1024 / 1024
          print(f"\n💾 结果已保存:")
          print(f"   完整结果: {result_file} ({size_mb:.1f} MB)")
          print(f"   摘要文件: {summary_file}")
      
          return result_file
      
      
      # ======================== Helpers ========================
      
      def _ts(ms):
          """毫秒时间戳转可读字符串"""
          return time.strftime("%Y-%m-%d %H:%M", time.localtime(ms / 1000))
      
      
      # ======================== Main ========================
      
      def main():
          args = sys.argv[1:]
      
          # 参数解析
          sample_rate = 1
          max_days = None
          candle_progress = 0.01  # 默认: K线刚开盘 (最保守)
          verbose = "--full-report" in args
          force = "--force" in args
      
          if "--fast" in args:
              sample_rate = 12
              max_days = 180
      
          for i, a in enumerate(args):
              if a == "--sample" and i + 1 < len(args):
                  sample_rate = int(args[i + 1])
              elif a.startswith("--sample="):
                  sample_rate = int(a.split("=")[1])
              elif a == "--days" and i + 1 < len(args):
                  max_days = int(args[i + 1])
              elif a.startswith("--days="):
                  max_days = int(a.split("=")[1])
              elif a == "--progress" and i + 1 < len(args):
                  candle_progress = float(args[i + 1])
              elif a.startswith("--progress="):
                  candle_progress = float(a.split("=")[1])
      
          # 加载数据
          if not os.path.exists(DATA_FILE):
              print("❌ 无数据文件, 请先运行: python fetch_data.py")
              sys.exit(1)
      
          print("📂 加载K线数据...")
          with open(DATA_FILE) as f:
              candles = json.load(f)
          print(f"   {len(candles):,} 根K线 | {_ts(candles[0]['ot'])} ~ {_ts(candles[-1]['ct'])}")
      
          if max_days:
              print(f"   限制回测最近 {max_days} 天")
      
          # 加载引擎
          print("⚙️  加载引擎 v5.6...")
          engine = load_engine()
          print(f"   OK — {ENGINE_PATH}")
      
          # 运行回测
          results, stats = run_backtest(
              engine, candles,
              sample_rate=sample_rate,
              max_days=max_days,
              candle_progress=candle_progress,
              verbose=True,
          )
      
          # 输出报告
          print_report(stats, verbose=verbose)
      
          # 保存结果
          save_results(results, stats, sample_rate, max_days)
      
          return stats
      
      
      if __name__ == "__main__":
          main()
      
    • run_backtest_v57.py 25 KB
      #!/usr/bin/env python3
      """5minbtc Engine v5.7 半K线回测 — 在K线50%进度处预测
      
      核心逻辑:
        - 对每根K线[i], 使用 candles[i-warmup:i] (200根已完成K线) 计算因子
        - 模拟progress=0.5的半K线状态:
          - close = K线中点价格 (open + (close-open)*0.5 的近似)
          - high/low 使用实际值的前50%估计
          - volume 使用实际值的50%
        - half_body因子在progress=0.5时激活(≥0.45)
        - ATR乘数×0.55, half_range=0.40 (已内置于v5.7引擎)
        - 对比实际方向: candles[i].c vs candles[i-1].c
      
      用法:
        python run_backtest_v57.py                    # 完整回测 (每根K线)
        python run_backtest_v57.py --sample 6         # 每6根K线取1根 (30分钟间隔)
        python run_backtest_v57.py --days 180         # 只回测最近180天
        python run_backtest_v57.py --fast             # 快速模式: sample=12 + 最近180天
        python run_backtest_v57.py --full-report      # 输出完整因子分析报告
        python run_backtest_v57.py --compare          # 同时跑v5.6 baseline对比
      """
      
      import json, math, os, sys, time, importlib.util, random
      from collections import defaultdict
      from datetime import datetime, timezone, timedelta
      
      SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
      SKILL_DIR = os.path.dirname(SCRIPT_DIR)
      ENGINE_V57 = os.path.join(SKILL_DIR, "5minbtc-engine-v6.0.py")
      ENGINE_V56 = os.path.join(SKILL_DIR, "archive", "engines", "5minbtc-engine-v5.py")
      DATA_FILE = os.path.join(SCRIPT_DIR, "data", "btcusdt_5m.json")
      RESULTS_DIR = os.path.join(SCRIPT_DIR, "results")
      
      CST = timezone(timedelta(hours=8))
      WARMUP = 200
      PROGRESS = 0.5  # 半K线
      
      
      # ======================== Engine Import ========================
      
      def load_engine(path):
          """动态导入引擎模块"""
          if not os.path.exists(path):
              print(f"❌ 引擎文件不存在: {path}")
              sys.exit(1)
          spec = importlib.util.spec_from_file_location("engine", path)
          mod = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(mod)
          return mod
      
      
      # ======================== Half-Candle Simulation ========================
      
      def simulate_half_candle(raw):
          """模拟K线执行到50%时的状态
          
          策略: 用实际OHLCV的中间状态近似
          - open: 保持不变
          - close: 用 (open + actual_close) / 2 近似中点价格
          - high: 取 open 和 mid_close 中的较大值 * 1.001 (模拟半程波动)
          - low: 取 open 和 mid_close 中的较小值 * 0.999
          - volume: 实际量的50%
          """
          open_price = raw["o"]
          actual_close = raw["c"]
          actual_high = raw["h"]
          actual_low = raw["l"]
          actual_vol = raw["v"]
          
          # 中点价格: 假设价格线性移动(简化假设)
          mid_close = (open_price + actual_close) / 2
          
          # 半程high/low: 取实际范围的一半 + open
          range_full = actual_high - actual_low
          if actual_close >= open_price:
              # 阳线: 先跌后涨, 半程low更接近open, high在中间
              mid_low = min(open_price, mid_close) - range_full * 0.1
              mid_high = max(open_price, mid_close) + range_full * 0.15
          else:
              # 阴线: 先涨后跌, 半程high更接近open, low在中间
              mid_high = max(open_price, mid_close) + range_full * 0.1
              mid_low = min(open_price, mid_close) - range_full * 0.15
          
          # 用实际数据约束: 不超过实际范围
          mid_high = min(mid_high, actual_high)
          mid_low = max(mid_low, actual_low)
          
          # 至少包含open
          mid_high = max(mid_high, open_price)
          mid_low = min(mid_low, open_price)
          
          return {
              "o": open_price,
              "h": mid_high,
              "l": mid_low,
              "c": mid_close,
              "v": actual_vol * 0.5,
              "ct": raw["ct"],
              "ot": raw["ot"],
          }
      
      
      def simulate_open_candle(raw):
          """模拟K线刚开盘(v5.6 baseline用)"""
          open_price = raw["o"]
          return {
              "o": open_price, "h": open_price, "l": open_price,
              "c": open_price, "v": 0.0, "ct": raw["ct"], "ot": raw["ot"],
          }
      
      
      # ======================== Backtest Core ========================
      
      def run_backtest_single(engine, candles, sample_rate=1, max_days=None,
                              progress=0.5, verbose=True):
          """单引擎回测"""
          if max_days and len(candles) > WARMUP + 100:
              cutoff_ms = candles[-1]["ct"] - max_days * 24 * 3600 * 1000
              start_idx = WARMUP
              for i in range(WARMUP, len(candles)):
                  if candles[i]["ot"] >= cutoff_ms:
                      start_idx = i
                      break
              start_idx = max(start_idx, WARMUP)
          else:
              start_idx = WARMUP
      
          total = len(candles) - start_idx
          total_samples = (total + sample_rate - 1) // sample_rate
      
          use_half = (progress >= 0.45)
      
          if verbose:
              ver = "v5.7 半K线" if use_half else "v5.6 开盘"
              print(f"\n🔄 开始回测 ({ver}, progress={progress})")
              print(f"   数据范围: {_ts(candles[start_idx]['ot'])} ~ {_ts(candles[-1]['ct'])}")
              print(f"   总K线: {total:,} | 采样: {total_samples:,} (rate={sample_rate})")
              print()
      
          results = []
          t0 = time.time()
      
          for idx, i in enumerate(range(start_idx, len(candles), sample_rate)):
              window = candles[i - WARMUP:i]
              raw = candles[i]
      
              if use_half:
                  simulated = simulate_half_candle(raw)
              else:
                  simulated = simulate_open_candle(raw)
      
              full_candles = window + [simulated]
              closes = [c["c"] for c in full_candles]
      
              atr_val = engine.atr_wilder(full_candles)
              vr = engine.vol_regime_ratio(closes)
      
              bias, strength, confidence, score, factors, regime = engine.direction_rule_v5(
                  full_candles, closes, atr_val, vr,
                  depth_data=None, candle_progress=progress
              )
      
              actual_close = raw["c"]
              prev_close = candles[i - 1]["c"]
              price_chg = actual_close - prev_close
              actual_dir = "UP" if price_chg > 0 else ("DOWN" if price_chg < 0 else "FLAT")
      
              if bias == "neutral":
                  dir_correct = None
              elif bias == "bull" and actual_dir == "UP":
                  dir_correct = True
              elif bias == "bear" and actual_dir == "DOWN":
                  dir_correct = True
              else:
                  dir_correct = False
      
              mom_val = factors.get("momentum", 0)
              decel_val = factors.get("decel", 0)
              conflict = abs(mom_val) > 0.7 and abs(decel_val) > 0.8 and mom_val * decel_val < 0
      
              half_body_val = factors.get("half_body", 0)
      
              results.append({
                  "idx": i,
                  "ts": raw["ot"],
                  "open": raw["o"],
                  "sim_close": simulated["c"],
                  "actual_close": actual_close,
                  "prev_close": prev_close,
                  "price_chg": price_chg,
                  "actual_dir": actual_dir,
                  "bias": bias,
                  "strength": strength,
                  "confidence": confidence,
                  "score": score,
                  "regime": regime,
                  "dir_correct": dir_correct,
                  "conflict": conflict,
                  "half_body": round(half_body_val, 4),
                  "factors": {k: round(v, 4) if isinstance(v, float) else v
                              for k, v in factors.items()},
              })
      
              if verbose and (idx + 1) % max(1, total_samples // 20) == 0:
                  elapsed = time.time() - t0
                  pct = (idx + 1) / total_samples * 100
                  eta = elapsed / (idx + 1) * (total_samples - idx - 1)
                  recent = [r for r in results[-200:] if r["dir_correct"] is not None]
                  if recent:
                      acc = sum(1 for r in recent if r["dir_correct"]) / len(recent) * 100
                  else:
                      acc = 0
                  print(f"   [{pct:5.1f}%] {idx+1:,}/{total_samples:,} | "
                        f"近期方向准确率: {acc:.1f}% | ETA: {eta:.0f}s")
      
          elapsed = time.time() - t0
          if verbose:
              print(f"\n✅ 回测完成! 耗时 {elapsed:.1f}s ({len(results):,} 条预测)")
      
          return results
      
      
      # ======================== Statistics ========================
      
      def compute_stats(results, label="v5.7"):
          if not results:
              return {}
      
          directional = [r for r in results if r["dir_correct"] is not None]
          n_dir = len(directional)
          n_correct = sum(1 for r in directional if r["dir_correct"])
      
          bull_all = [r for r in directional if r["bias"] == "bull"]
          bear_all = [r for r in directional if r["bias"] == "bear"]
          neutral_all = [r for r in results if r["bias"] == "neutral"]
          bull_correct = sum(1 for r in bull_all if r["dir_correct"])
          bear_correct = sum(1 for r in bear_all if r["dir_correct"])
      
          regime_stats = {}
          for rg in ["TREND", "RANGE", "HIGH_VOL", "LOW_VOL"]:
              subset = [r for r in directional if r["regime"] == rg]
              if subset:
                  correct = sum(1 for r in subset if r["dir_correct"])
                  regime_stats[rg] = {
                      "n": len(subset), "correct": correct,
                      "accuracy": round(correct / len(subset) * 100, 1),
                  }
      
          conf_buckets = defaultdict(lambda: {"n": 0, "correct": 0})
          for r in directional:
              bucket = (r["confidence"] // 5) * 5
              conf_buckets[bucket]["n"] += 1
              if r["dir_correct"]:
                  conf_buckets[bucket]["correct"] += 1
          conf_stats = {}
          for bucket in sorted(conf_buckets.keys()):
              b = conf_buckets[bucket]
              conf_stats[f"{bucket}-{bucket+4}"] = {
                  "n": b["n"],
                  "accuracy": round(b["correct"] / b["n"] * 100, 1) if b["n"] > 0 else 0,
              }
      
          monthly = defaultdict(lambda: {"n": 0, "correct": 0, "bull": 0, "bear": 0})
          for r in directional:
              month = time.strftime("%Y-%m", time.localtime(r["ts"] / 1000))
              monthly[month]["n"] += 1
              if r["dir_correct"]:
                  monthly[month]["correct"] += 1
              if r["bias"] == "bull":
                  monthly[month]["bull"] += 1
              else:
                  monthly[month]["bear"] += 1
          monthly_stats = {}
          for m in sorted(monthly.keys()):
              monthly_stats[m] = {
                  "n": monthly[m]["n"],
                  "accuracy": round(monthly[m]["correct"] / monthly[m]["n"] * 100, 1),
                  "bull_ratio": round(monthly[m]["bull"] / monthly[m]["n"] * 100, 1),
              }
      
          streaks = _compute_streaks(directional)
      
          actual_up = sum(1 for r in directional if r["actual_dir"] == "UP")
          baseline_bull = round(actual_up / n_dir * 100, 1) if n_dir > 0 else 0
          baseline_bear = round((n_dir - actual_up) / n_dir * 100, 1) if n_dir > 0 else 0
      
          conflict_all = [r for r in directional if r.get("conflict")]
          conflict_correct = sum(1 for r in conflict_all if r["dir_correct"])
      
          factor_contribution = _analyze_factors(directional)
      
          # half_body 特殊分析
          half_body_stats = _analyze_half_body(directional)
      
          return {
              "label": label,
              "meta": {
                  "total_predictions": len(results),
                  "directional_predictions": n_dir,
                  "neutral_predictions": len(neutral_all),
                  "backtest_period": (
                      f"{_ts(results[0]['ts'])} ~ {_ts(results[-1]['ts'])}"
                  ),
              },
              "overall": {
                  "direction_accuracy": round(n_correct / n_dir * 100, 1) if n_dir > 0 else 0,
                  "correct": n_correct,
                  "wrong": n_dir - n_correct,
              },
              "bias_breakdown": {
                  "bull": {"n": len(bull_all), "correct": bull_correct,
                           "accuracy": round(bull_correct / len(bull_all) * 100, 1) if bull_all else 0},
                  "bear": {"n": len(bear_all), "correct": bear_correct,
                           "accuracy": round(bear_correct / len(bear_all) * 100, 1) if bear_all else 0},
                  "neutral_n": len(neutral_all),
              },
              "baseline": {
                  "always_bull_accuracy": baseline_bull,
                  "always_bear_accuracy": baseline_bear,
                  "random_50": 50.0,
                  "edge_vs_random": round(n_correct / n_dir * 100 - 50, 1) if n_dir > 0 else 0,
              },
              "regime_stats": regime_stats,
              "confidence_stats": conf_stats,
              "monthly_stats": monthly_stats,
              "streaks": streaks,
              "conflict_detection": {
                  "triggered_n": len(conflict_all),
                  "accuracy": round(conflict_correct / len(conflict_all) * 100, 1) if conflict_all else 0,
                  "overall_accuracy": round(n_correct / n_dir * 100, 1) if n_dir > 0 else 0,
              },
              "factor_contribution": factor_contribution,
              "half_body_analysis": half_body_stats,
          }
      
      
      def _compute_streaks(directional):
          max_win, max_lose = 0, 0
          current_win, current_lose = 0, 0
          for r in directional:
              if r["dir_correct"]:
                  current_win += 1
                  current_lose = 0
                  max_win = max(max_win, current_win)
              else:
                  current_lose += 1
                  current_win = 0
                  max_lose = max(max_lose, current_lose)
          return {"max_win_streak": max_win, "max_lose_streak": max_lose}
      
      
      def _analyze_factors(directional):
          factor_names = ["momentum", "meanrev", "rsi", "volume", "fatigue",
                          "imbalance", "microprice", "decel", "position",
                          "v_reversal", "vol_breakout", "half_body"]
          analysis = {}
          for fn in factor_names:
              correct_vals = [abs(r["factors"].get(fn, 0)) for r in directional
                             if r["dir_correct"] and fn in r.get("factors", {})]
              wrong_vals = [abs(r["factors"].get(fn, 0)) for r in directional
                           if not r["dir_correct"] and fn in r.get("factors", {})]
              avg_c = sum(correct_vals) / len(correct_vals) if correct_vals else 0
              avg_w = sum(wrong_vals) / len(wrong_vals) if wrong_vals else 0
              pos_signals = [r for r in directional if r["factors"].get(fn, 0) > 0.1]
              if pos_signals:
                  pos_up = sum(1 for r in pos_signals if r["actual_dir"] == "UP")
                  consistency = round(pos_up / len(pos_signals) * 100, 1)
              else:
                  consistency = None
              analysis[fn] = {
                  "avg_when_correct": round(avg_c, 4),
                  "avg_when_wrong": round(avg_w, 4),
                  "signal_strength_diff": round(avg_c - avg_w, 4),
                  "directional_consistency": consistency,
                  "n_positive_signals": len(pos_signals) if pos_signals else 0,
              }
          return analysis
      
      
      def _analyze_half_body(directional):
          """half_body因子专项分析"""
          activated = [r for r in directional if abs(r.get("half_body", 0)) > 0.01]
          if not activated:
              return {"activated_n": 0}
      
          activated_correct = sum(1 for r in activated if r["dir_correct"])
          
          # 按half_body值分桶
          strong_bull = [r for r in activated if r["half_body"] > 0.3]
          strong_bear = [r for r in activated if r["half_body"] < -0.3]
          weak = [r for r in activated if abs(r["half_body"]) <= 0.3]
          
          def acc(lst):
              if not lst:
                  return {"n": 0, "accuracy": 0}
              c = sum(1 for r in lst if r["dir_correct"])
              return {"n": len(lst), "accuracy": round(c / len(lst) * 100, 1)}
          
          # half_body方向一致性: half_body>0时实际UP的比例
          hb_pos = [r for r in activated if r["half_body"] > 0.05]
          hb_neg = [r for r in activated if r["half_body"] < -0.05]
          
          hb_pos_up = sum(1 for r in hb_pos if r["actual_dir"] == "UP") if hb_pos else 0
          hb_neg_down = sum(1 for r in hb_neg if r["actual_dir"] == "DOWN") if hb_neg else 0
          
          return {
              "activated_n": len(activated),
              "activated_pct": round(len(activated) / len(directional) * 100, 1) if directional else 0,
              "activated_accuracy": round(activated_correct / len(activated) * 100, 1),
              "strong_bull": acc(strong_bull),
              "strong_bear": acc(strong_bear),
              "weak_signal": acc(weak),
              "direction_consistency": {
                  "pos_predicts_up": round(hb_pos_up / len(hb_pos) * 100, 1) if hb_pos else 0,
                  "neg_predicts_down": round(hb_neg_down / len(hb_neg) * 100, 1) if hb_neg else 0,
              },
          }
      
      
      # ======================== Report ========================
      
      def print_report(stats, verbose=False):
          s = stats
          o = s["overall"]
          m = s["meta"]
          label = s.get("label", "v5.7")
      
          print("\n" + "=" * 65)
          print(f"📊 5minbtc {label} — 半K线回测报告")
          print("=" * 65)
      
          print(f"\n📅 回测区间: {m['backtest_period']}")
          print(f"   总预测: {m['total_predictions']:,} | "
                f"方向性: {m['directional_predictions']:,} | "
                f"Neutral: {m['neutral_predictions']:,}")
      
          print(f"\n🎯 方向准确率: {o['direction_accuracy']}% "
                f"({o['correct']:,}/{m['directional_predictions']:,})")
      
          bl = s["baseline"]
          edge_emoji = "✅" if bl['edge_vs_random'] > 0 else "❌"
          print(f"   vs Always-Bull: {bl['always_bull_accuracy']}%")
          print(f"   vs Always-Bear: {bl['always_bear_accuracy']}%")
          print(f"   vs Random(50%): {bl['edge_vs_random']:+.1f}pp edge {edge_emoji}")
      
          bb = s["bias_breakdown"]
          print(f"\n🐂 Bull: {bb['bull']['accuracy']}% ({bb['bull']['correct']}/{bb['bull']['n']})")
          print(f"🐻 Bear: {bb['bear']['accuracy']}% ({bb['bear']['correct']}/{bb['bear']['n']})")
          if bb["neutral_n"] > 0:
              print(f"⚖️  Neutral: {bb['neutral_n']}")
      
          print(f"\n🌊 Regime 分析:")
          for rg, rs in s["regime_stats"].items():
              bar = "█" * int(rs["accuracy"] / 2) + "░" * (50 - int(rs["accuracy"] / 2))
              print(f"   {rg:10s}: {rs['accuracy']:5.1f}% ({rs['correct']}/{rs['n']:,}) {bar}")
      
          cd = s["conflict_detection"]
          if cd["triggered_n"] > 0:
              print(f"\n⚡ Momentum/Decel 冲突检测:")
              print(f"   触发次数: {cd['triggered_n']:,}")
              print(f"   冲突时准确率: {cd['accuracy']}% (整体: {cd['overall_accuracy']}%)")
      
          # half_body 专项
          hb = s.get("half_body_analysis", {})
          if hb.get("activated_n", 0) > 0:
              print(f"\n🔬 half_body 因子分析:")
              print(f"   激活次数: {hb['activated_n']:,} ({hb['activated_pct']}%)")
              print(f"   激活时准确率: {hb['activated_accuracy']}%")
              if hb.get("strong_bull", {}).get("n", 0) > 0:
                  print(f"   强bull信号(>0.3): {hb['strong_bull']['accuracy']}% ({hb['strong_bull']['n']})")
              if hb.get("strong_bear", {}).get("n", 0) > 0:
                  print(f"   强bear信号(<-0.3): {hb['strong_bear']['accuracy']}% ({hb['strong_bear']['n']})")
              dc = hb.get("direction_consistency", {})
              if dc:
                  print(f"   方向一致性: pos→UP={dc.get('pos_predicts_up', 0)}%, neg→DOWN={dc.get('neg_predicts_down', 0)}%")
      
          print(f"\n📈 置信度分层:")
          for bucket, cs in s["confidence_stats"].items():
              if cs["n"] >= 10:
                  marker = "✅" if cs["accuracy"] > 55 else ("⚠️" if cs["accuracy"] > 50 else "❌")
                  print(f"   conf {bucket}: {cs['accuracy']:5.1f}% ({cs['n']:,}) {marker}")
      
          ms = s["monthly_stats"]
          if len(ms) > 1:
              print(f"\n📅 月度趋势:")
              for month, md in ms.items():
                  bar = "▓" * int(md["accuracy"] / 2)
                  marker = "✅" if md["accuracy"] > 50 else "❌"
                  print(f"   {month}: {md['accuracy']:5.1f}% ({md['n']:>5,}) bull={md['bull_ratio']:.0f}% {bar} {marker}")
      
          sk = s["streaks"]
          print(f"\n🔥 最长连胜: {sk['max_win_streak']} | 💀 最长连败: {sk['max_lose_streak']}")
      
          if verbose:
              print(f"\n🔬 因子贡献分析:")
              fc = s["factor_contribution"]
              sorted_factors = sorted(fc.items(),
                                     key=lambda x: abs(x[1]["signal_strength_diff"]),
                                     reverse=True)
              for fn, fa in sorted_factors:
                  diff_marker = "↑" if fa["signal_strength_diff"] > 0 else "↓"
                  cons_str = (f"一致性={fa['directional_consistency']}%"
                             if fa["directional_consistency"] is not None else "N/A")
                  print(f"   {fn:15s}: 正确时avg={fa['avg_when_correct']:.3f} "
                        f"错误时avg={fa['avg_when_wrong']:.3f} "
                        f"diff={fa['signal_strength_diff']:+.3f}{diff_marker} "
                        f"{cons_str}")
      
          print("\n" + "=" * 65)
      
      
      def print_comparison(stats_57, stats_56):
          """v5.7 vs v5.6 对比"""
          o57 = stats_57["overall"]
          o56 = stats_56["overall"]
          m57 = stats_57["meta"]
          m56 = stats_56["meta"]
      
          print("\n" + "=" * 65)
          print("📊 v5.7 半K线 vs v5.6 开盘 — A/B 对比")
          print("=" * 65)
          print(f"\n{'指标':<25s} {'v5.7 半K线':>12s} {'v5.6 开盘':>12s} {'差值':>10s}")
          print("-" * 65)
          
          acc57 = o57["direction_accuracy"]
          acc56 = o56["direction_accuracy"]
          print(f"{'方向准确率':<25s} {acc57:>11.1f}% {acc56:>11.1f}% {acc57-acc56:>+9.1f}pp")
          
          bl57 = stats_57["baseline"]["edge_vs_random"]
          bl56 = stats_56["baseline"]["edge_vs_random"]
          print(f"{'Edge vs Random':<25s} {bl57:>+10.1f}pp {bl56:>+10.1f}pp {bl57-bl56:>+9.1f}pp")
          
          bb57 = stats_57["bias_breakdown"]
          bb56 = stats_56["bias_breakdown"]
          print(f"{'Bull准确率':<25s} {bb57['bull']['accuracy']:>11.1f}% {bb56['bull']['accuracy']:>11.1f}%")
          print(f"{'Bear准确率':<25s} {bb57['bear']['accuracy']:>11.1f}% {bb56['bear']['accuracy']:>11.1f}%")
          print(f"{'Neutral数':<25s} {bb57['neutral_n']:>12,} {bb56['neutral_n']:>12,}")
          
          sk57 = stats_57["streaks"]
          sk56 = stats_56["streaks"]
          print(f"{'最长连胜':<25s} {sk57['max_win_streak']:>12} {sk56['max_win_streak']:>12}")
          print(f"{'最长连败':<25s} {sk57['max_lose_streak']:>12} {sk56['max_lose_streak']:>12}")
          
          # 月度对比
          ms57 = stats_57["monthly_stats"]
          ms56 = stats_56["monthly_stats"]
          common_months = sorted(set(ms57.keys()) & set(ms56.keys()))
          if common_months:
              print(f"\n📅 月度准确率对比:")
              print(f"   {'月份':<12s} {'v5.7':>8s} {'v5.6':>8s} {'差值':>8s}")
              for m in common_months:
                  a57 = ms57[m]["accuracy"]
                  a56 = ms56[m]["accuracy"]
                  emoji = "✅" if a57 > a56 else "❌"
                  print(f"   {m:<12s} {a57:>7.1f}% {a56:>7.1f}% {a57-a56:>+7.1f}pp {emoji}")
      
          print("\n" + "=" * 65)
      
      
      # ======================== Helpers ========================
      
      def _ts(ms):
          return time.strftime("%Y-%m-%d %H:%M", time.localtime(ms / 1000))
      
      
      # ======================== Main ========================
      
      def main():
          args = sys.argv[1:]
      
          sample_rate = 1
          max_days = 180  # 默认半年
          verbose = "--full-report" in args
          compare_mode = "--compare" in args
      
          if "--fast" in args:
              sample_rate = 6
              max_days = 180
          if "--very-fast" in args:
              sample_rate = 12
              max_days = 180
      
          for i, a in enumerate(args):
              if a == "--sample" and i + 1 < len(args):
                  sample_rate = int(args[i + 1])
              elif a.startswith("--sample="):
                  sample_rate = int(a.split("=")[1])
              elif a == "--days" and i + 1 < len(args):
                  max_days = int(args[i + 1])
              elif a.startswith("--days="):
                  max_days = int(a.split("=")[1])
      
          # 加载数据
          if not os.path.exists(DATA_FILE):
              print("❌ 无数据文件, 请先运行: python fetch_data.py")
              sys.exit(1)
      
          print("📂 加载K线数据...")
          with open(DATA_FILE) as f:
              candles = json.load(f)
          print(f"   {len(candles):,} 根K线 | {_ts(candles[0]['ot'])} ~ {_ts(candles[-1]['ct'])}")
          if max_days:
              print(f"   限制回测最近 {max_days} 天")
      
          # ---- v5.7 半K线回测 ----
          print("\n⚙️  加载引擎 v5.7 (半K线, progress=0.5)...")
          engine57 = load_engine(ENGINE_V57)
          results57 = run_backtest_single(
              engine57, candles,
              sample_rate=sample_rate,
              max_days=max_days,
              progress=PROGRESS,
              verbose=True,
          )
          stats57 = compute_stats(results57, label="v5.7 半K线")
      
          # ---- v5.6 baseline (可选) ----
          stats56 = None
          if compare_mode:
              print("\n⚙️  加载引擎 v5.6 (开盘 baseline, progress=0.01)...")
              engine56 = load_engine(ENGINE_V56)
              results56 = run_backtest_single(
                  engine56, candles,
                  sample_rate=sample_rate,
                  max_days=max_days,
                  progress=0.01,
                  verbose=True,
              )
              stats56 = compute_stats(results56, label="v5.6 开盘")
      
          # 输出报告
          print_report(stats57, verbose=verbose)
      
          if stats56:
              print_comparison(stats57, stats56)
              print_report(stats56, verbose=False)
      
          # 保存结果
          os.makedirs(RESULTS_DIR, exist_ok=True)
          ts = datetime.now(CST).strftime("%Y%m%d_%H%M")
          
          output = {
              "timestamp": ts,
              "engine": "v5.7 半K线",
              "sample_rate": sample_rate,
              "max_days": max_days,
              "progress": PROGRESS,
              "stats_v57": stats57,
              "stats_v56": stats56,
              "predictions_count": len(results57),
          }
          
          result_file = os.path.join(RESULTS_DIR, f"backtest_v57_{ts}.json")
          with open(result_file, "w") as f:
              json.dump(output, f, ensure_ascii=False, indent=None)
          
          summary_file = os.path.join(RESULTS_DIR, "latest_v57_summary.json")
          with open(summary_file, "w") as f:
              json.dump({
                  "timestamp": ts,
                  "stats_v57": stats57,
                  "stats_v56": stats56,
              }, f, ensure_ascii=False, indent=2)
      
          size_mb = os.path.getsize(result_file) / 1024 / 1024
          print(f"\n💾 结果已保存:")
          print(f"   完整结果: {result_file} ({size_mb:.1f} MB)")
          print(f"   摘要文件: {summary_file}")
      
          return stats57
      
      
      if __name__ == "__main__":
          main()
      
    • run_backtest_v58.py 19.8 KB
      #!/usr/bin/env python3
      """5minbtc Engine v5.7 回测 — 用真实1分钟K线构建半K线状态 (零前视偏差)
      
      核心改进 (v5.8回测方法):
        - 对每根5分钟K线[i], 找到其对应的**前2-3根1分钟K线**
        - 用这些1分钟K线的真实OHLCV构建"半K线"状态
        - 模拟实盘场景: 在第3分钟时, 你确实看到了前2.5分钟的价格走势
        - 零前视偏差 — 不使用当前5分钟K线的close/high/low
      
      对比:
        - v5.6 baseline: progress=0.01 (开盘预测)
        - v5.7 有前视偏差: 用 (open+actual_close)/2 (已知最终方向)
        - v5.7 无偏差(旧): 用前一根5min K线body模拟
        - **v5.8 (本脚本): 用真实1min K线前2根** ← 唯一正确方法
      
      用法:
        python run_backtest_v58.py                    # 完整回测
        python run_backtest_v58.py --sample 6         # 每6根取1根 (30分钟间隔)
        python run_backtest_v58.py --fast             # 快速模式: sample=6 + 180天
        python run_backtest_v58.py --compare          # 同时跑v5.6 baseline
        python run_backtest_v58.py --half 3           # 用前3根1min (默认2)
      """
      
      import json, math, os, sys, time, importlib.util, argparse
      from collections import defaultdict
      from datetime import datetime, timezone, timedelta
      
      SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
      SKILL_DIR = os.path.dirname(SCRIPT_DIR)
      ENGINE_V57 = os.path.join(SKILL_DIR, "5minbtc-engine-v6.0.py")
      ENGINE_V56 = os.path.join(SKILL_DIR, "archive", "engines", "5minbtc-engine-v5.py")
      DATA_5M = os.path.join(SCRIPT_DIR, "data", "btcusdt_5m.json")
      DATA_1M = os.path.join(SCRIPT_DIR, "data", "btcusdt_1m.json")
      RESULTS_DIR = os.path.join(SCRIPT_DIR, "results")
      
      CST = timezone(timedelta(hours=8))
      WARMUP = 200
      
      
      # ======================== Engine Import ========================
      
      def load_engine(path):
          """动态导入引擎模块"""
          if not os.path.exists(path):
              print(f"❌ 引擎文件不存在: {path}")
              sys.exit(1)
          spec = importlib.util.spec_from_file_location("engine", path)
          mod = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(mod)
          return mod
      
      
      # ======================== 1min -> 5min Half-Candle ========================
      
      def build_1m_index(candles_1m):
          """构建 1min open_time -> candle 的索引"""
          idx = {}
          for c in candles_1m:
              idx[c["ot"]] = c
          return idx
      
      
      def find_1m_for_5m(candle_5m, idx_1m):
          """找到对应5分钟K线的所有1分钟K线 (5根)
          
          5min K线 open_time 总是 5的整数倍分钟
          对应的1min K线: open_time, open_time+60000, ... open_time+240000
          """
          base_ot = candle_5m["ot"]
          result = []
          for offset_min in range(5):
              ot = base_ot + offset_min * 60000
              if ot in idx_1m:
                  result.append(idx_1m[ot])
          return result
      
      
      def build_half_candle_from_1m(candle_5m, candles_1m_slice, n_half=2):
          """用前n_half根1分钟K线构建半K线状态
          
          这完全模拟实盘: 在5分钟K线的第3分钟, 你已经看到了前2根1分钟K线的完整数据
          
          Args:
              candle_5m: 当前5分钟K线原始数据
              candles_1m_slice: 对应的5根1分钟K线 (按时间排序)
              n_half: 用前几根1分钟K线 (默认2 = 前2分钟)
          
          Returns:
              模拟的半K线 OHLCV
          """
          if not candles_1m_slice:
              # 没有对应的1分钟数据, fallback到open状态
              return None
          
          half_1m = candles_1m_slice[:n_half]
          
          open_price = candle_5m["o"]  # 5min open 就是第一根1min的open
          
          # 用1分钟数据构建半程OHLCV
          all_highs = [c["h"] for c in half_1m]
          all_lows = [c["l"] for c in half_1m]
          last_close = half_1m[-1]["c"]
          total_vol = sum(c["v"] for c in half_1m)
          
          half_high = max(open_price, max(all_highs))
          half_low = min(open_price, min(all_lows))
          
          return {
              "o": open_price,
              "h": half_high,
              "l": half_low,
              "c": last_close,
              "v": total_vol,
              "ct": half_1m[-1]["ct"],
              "ot": candle_5m["ot"],
          }
      
      
      def simulate_open_candle(raw):
          """模拟K线刚开盘(v5.6 baseline用)"""
          open_price = raw["o"]
          return {
              "o": open_price, "h": open_price, "l": open_price,
              "c": open_price, "v": 0.0, "ct": raw["ct"], "ot": raw["ot"],
          }
      
      
      # ======================== Backtest Core ========================
      
      def _ts(ms):
          return datetime.fromtimestamp(ms / 1000, tz=CST).strftime("%Y-%m-%d %H:%M")
      
      
      def run_backtest_single(engine, candles_5m, idx_1m, sample_rate=1,
                              max_days=None, n_half=2, mode="half_1m",
                              verbose=True):
          """单引擎回测
          
          mode:
            "half_1m"  — 用前n_half根1min K线 (零前视偏差)
            "open"     — progress=0.01 (v5.6 baseline)
          """
          # 时间范围筛选
          start_idx = WARMUP
          if max_days and len(candles_5m) > WARMUP + 100:
              cutoff_ms = candles_5m[-1]["ct"] - max_days * 24 * 3600 * 1000
              for i in range(WARMUP, len(candles_5m)):
                  if candles_5m[i]["ot"] >= cutoff_ms:
                      start_idx = i
                      break
              start_idx = max(start_idx, WARMUP)
      
          total = len(candles_5m) - start_idx
          total_samples = (total + sample_rate - 1) // sample_rate
      
          if verbose:
              label = {
                  "half_1m": f"v5.7+1min半K线(前{n_half}根1min)",
                  "open": "v5.6 开盘baseline",
              }[mode]
              print(f"\n🔄 开始回测 ({label})")
              print(f"   数据范围: {_ts(candles_5m[start_idx]['ot'])} ~ {_ts(candles_5m[-1]['ct'])}")
              print(f"   总5min K线: {total:,} | 采样: {total_samples:,} (rate={sample_rate})")
              print()
      
          results = []
          t0 = time.time()
          skipped_no_1m = 0
      
          for idx, i in enumerate(range(start_idx, len(candles_5m), sample_rate)):
              window = candles_5m[i - WARMUP:i]
              raw = candles_5m[i]
      
              if mode == "half_1m":
                  # 用真实1分钟K线构建半K线
                  all_1m = find_1m_for_5m(raw, idx_1m)
                  simulated = build_half_candle_from_1m(raw, all_1m, n_half=n_half)
                  if simulated is None:
                      skipped_no_1m += 1
                      continue
                  progress = 0.5  # 引擎内half_body因子在≥0.45激活
              elif mode == "open":
                  simulated = simulate_open_candle(raw)
                  progress = 0.01
      
              full_candles = window + [simulated]
              closes = [c["c"] for c in full_candles]
      
              atr_val = engine.atr_wilder(full_candles)
              vr = engine.vol_regime_ratio(closes)
      
              bias, strength, confidence, score, factors, regime = engine.direction_rule_v5(
                  full_candles, closes, atr_val, vr,
                  depth_data=None, candle_progress=progress
              )
      
              actual_close = raw["c"]
              prev_close = candles_5m[i - 1]["c"]
              price_chg = actual_close - prev_close
              actual_dir = "UP" if price_chg > 0 else ("DOWN" if price_chg < 0 else "FLAT")
      
              if bias == "neutral":
                  dir_correct = None
              elif bias == "bull" and actual_dir == "UP":
                  dir_correct = True
              elif bias == "bear" and actual_dir == "DOWN":
                  dir_correct = True
              else:
                  dir_correct = False
      
              mom_val = factors.get("momentum", 0)
              decel_val = factors.get("decel", 0)
              conflict = abs(mom_val) > 0.7 and abs(decel_val) > 0.8 and mom_val * decel_val < 0
      
              half_body_val = factors.get("half_body", 0)
      
              results.append({
                  "idx": i,
                  "ts": raw["ot"],
                  "open": raw["o"],
                  "sim_close": simulated["c"],
                  "sim_high": simulated["h"],
                  "sim_low": simulated["l"],
                  "actual_close": actual_close,
                  "prev_close": prev_close,
                  "price_chg": price_chg,
                  "actual_dir": actual_dir,
                  "bias": bias,
                  "strength": strength,
                  "confidence": confidence,
                  "score": score,
                  "regime": regime,
                  "dir_correct": dir_correct,
                  "conflict": conflict,
                  "half_body": round(half_body_val, 4),
                  "factors": {k: round(v, 4) if isinstance(v, float) else v
                              for k, v in factors.items()},
              })
      
              if verbose and (idx + 1) % 2000 == 0:
                  elapsed = time.time() - t0
                  rate = (idx + 1) / elapsed
                  eta = (total_samples - idx - 1) / rate if rate > 0 else 0
                  directional = sum(1 for r in results if r["dir_correct"] is not None)
                  correct = sum(1 for r in results if r["dir_correct"] is True)
                  acc = correct / directional * 100 if directional > 0 else 0
                  print(f"  [{idx+1}/{total_samples}] acc={acc:.1f}% "
                        f"({correct}/{directional}) | "
                        f"{rate:.0f}根/s | ETA {eta:.0f}s"
                        f"{f' | skip_1m={skipped_no_1m}' if skipped_no_1m else ''}")
      
          elapsed = time.time() - t0
          if verbose:
              if skipped_no_1m:
                  print(f"   ⚠️ 跳过{skipped_no_1m}根(无对应1min数据)")
              print(f"   ✅ 回测完成: {len(results):,}根 | {elapsed:.1f}s")
      
          return results
      
      
      # ======================== Analysis ========================
      
      def analyze_results(results, label=""):
          """分析回测结果"""
          if not results:
              return {}
      
          directional = [r for r in results if r["dir_correct"] is not None]
          correct = sum(1 for r in directional if r["dir_correct"] is True)
          total_dir = len(directional)
          accuracy = correct / total_dir * 100 if total_dir > 0 else 0
      
          # 连胜/连败
          streaks = []
          current_streak = 0
          last_correct = None
          for r in directional:
              if r["dir_correct"] == last_correct:
                  current_streak += 1
              else:
                  if last_correct is not None:
                      streaks.append((last_correct, current_streak))
                  current_streak = 1
                  last_correct = r["dir_correct"]
          if last_correct is not None:
              streaks.append((last_correct, current_streak))
      
          win_streaks = [s for c, s in streaks if c]
          lose_streaks = [s for c, s in streaks if not c]
      
          # 按月分析
          monthly = defaultdict(lambda: {"total": 0, "correct": 0, "neutral": 0})
          for r in results:
              dt = datetime.fromtimestamp(r["ts"] / 1000, tz=CST)
              key = dt.strftime("%Y-%m")
              if r["dir_correct"] is not None:
                  monthly[key]["total"] += 1
                  if r["dir_correct"]:
                      monthly[key]["correct"] += 1
              else:
                  monthly[key]["neutral"] += 1
      
          # half_body因子分析
          hb_active = sum(1 for r in results if abs(r.get("half_body", 0)) > 0.05)
          hb_directional = [r for r in results
                            if r["dir_correct"] is not None and abs(r.get("half_body", 0)) > 0.05]
          hb_correct = sum(1 for r in hb_directional if r["dir_correct"] is True)
      
          # confidence分析
          conf_buckets = defaultdict(lambda: {"total": 0, "correct": 0})
          for r in directional:
              bucket = round(r["confidence"] * 10) / 10  # 0.1步长
              conf_buckets[bucket]["total"] += 1
              if r["dir_correct"]:
                  conf_buckets[bucket]["correct"] += 1
      
          # 因子贡献分析
          factor_names = set()
          for r in results:
              factor_names.update(r["factors"].keys())
          factor_correlation = {}
          for fn in sorted(factor_names):
              vals = [(r["factors"].get(fn, 0), r["dir_correct"]) for r in directional]
              # 简单: 因子方向与实际方向一致的比例
              factor_dir_correct = 0
              factor_dir_total = 0
              for fv, dc in vals:
                  if dc is None or abs(fv) < 0.1:
                      continue
                  factor_dir_total += 1
                  if (fv > 0 and dc) or (fv < 0 and not dc):
                      factor_dir_correct += 1
              if factor_dir_total > 0:
                  factor_correlation[fn] = {
                      "acc": round(factor_dir_correct / factor_dir_total * 100, 1),
                      "count": factor_dir_total,
                  }
      
          stats = {
              "label": label,
              "total_candles": len(results),
              "directional": total_dir,
              "correct": correct,
              "wrong": total_dir - correct,
              "neutral": len(results) - total_dir,
              "accuracy": round(accuracy, 1),
              "edge_vs_random": round(accuracy - 50, 1),
              "max_win_streak": max(win_streaks) if win_streaks else 0,
              "max_lose_streak": max(lose_streaks) if lose_streaks else 0,
              "avg_win_streak": round(sum(win_streaks) / len(win_streaks), 1) if win_streaks else 0,
              "avg_lose_streak": round(sum(lose_streaks) / len(lose_streaks), 1) if lose_streaks else 0,
              "half_body_active": hb_active,
              "half_body_active_pct": round(hb_active / len(results) * 100, 1),
              "half_body_dir": len(hb_directional),
              "half_body_correct": hb_correct,
              "half_body_acc": round(hb_correct / len(hb_directional) * 100, 1) if hb_directional else 0,
              "monthly": dict(sorted(monthly.items())),
              "factor_correlation": factor_correlation,
          }
      
          return stats
      
      
      def print_report(stats, compact=False):
          """打印报告"""
          if not stats:
              print("❌ 无结果")
              return
      
          print(f"\n{'='*60}")
          print(f"📊 {stats['label']}")
          print(f"{'='*60}")
          print(f"  总K线: {stats['total_candles']:,}")
          print(f"  方向性: {stats['directional']:,} | 中性: {stats['neutral']:,}")
          print(f"  正确: {stats['correct']:,} | 错误: {stats['wrong']:,}")
          print(f"  ★ 准确率: {stats['accuracy']}%")
          print(f"  Edge vs random: {stats['edge_vs_random']:+.1f}pp")
          print(f"  最长连胜: {stats['max_win_streak']} | 最长连败: {stats['max_lose_streak']}")
          print(f"  平均连胜: {stats['avg_win_streak']} | 平均连败: {stats['avg_lose_streak']}")
          print(f"  half_body激活: {stats['half_body_active']} ({stats['half_body_active_pct']}%)")
      
          if stats['half_body_dir'] > 0:
              print(f"  half_body方向性: {stats['half_body_dir']} | "
                    f"准确率: {stats['half_body_acc']}%")
      
          if not compact:
              print(f"\n  📅 月度准确率:")
              for month, data in stats["monthly"].items():
                  if data["total"] > 0:
                      acc = data["correct"] / data["total"] * 100
                      bar = "█" * int(acc / 2) + "░" * (50 - int(acc / 2))
                      print(f"    {month}: {acc:5.1f}% ({data['correct']:3d}/{data['total']:3d}) {bar}")
      
              # 因子贡献排名
              fc = stats.get("factor_correlation", {})
              if fc:
                  print(f"\n  🔬 因子方向准确率排名:")
                  sorted_factors = sorted(fc.items(), key=lambda x: x[1]["acc"], reverse=True)
                  for name, info in sorted_factors:
                      marker = "✅" if info["acc"] > 52 else ("⚠️" if info["acc"] < 48 else "  ")
                      print(f"    {marker} {name:18s}: {info['acc']:5.1f}% (n={info['count']})")
      
          print(f"{'='*60}")
      
      
      # ======================== Main ========================
      
      def main():
          parser = argparse.ArgumentParser(description="v5.8 1min真实半K线回测")
          parser.add_argument("--sample", type=int, default=1, help="采样率")
          parser.add_argument("--days", type=int, default=None, help="只回测最近N天")
          parser.add_argument("--fast", action="store_true", help="快速: sample=6 + 180天")
          parser.add_argument("--compare", action="store_true", help="同时跑v5.6 baseline")
          parser.add_argument("--half", type=int, default=2, help="用前几根1min (默认2)")
          parser.add_argument("--full-report", action="store_true", help="完整报告")
          parser.add_argument("--save", action="store_true", default=True, help="保存结果")
          args = parser.parse_args()
      
          if args.fast:
              args.sample = max(args.sample, 6)
              args.days = args.days or 180
      
          # 加载数据
          print("📂 加载5分钟K线数据...")
          with open(DATA_5M) as f:
              candles_5m = json.load(f)
          print(f"   5min: {len(candles_5m):,} 根")
      
          print("📂 加载1分钟K线数据...")
          with open(DATA_1M) as f:
              candles_1m = json.load(f)
          print(f"   1min: {len(candles_1m):,} 根")
      
          # 构建1min索引
          print("🔨 构建1min索引...")
          idx_1m = build_1m_index(candles_1m)
          print(f"   索引大小: {len(idx_1m):,}")
      
          # 验证覆盖
          sample_5m = candles_5m[WARMUP]
          sample_1m = find_1m_for_5m(sample_5m, idx_1m)
          print(f"   覆盖验证: 5min {_ts(sample_5m['ot'])} → 找到{len(sample_1m)}根1min")
      
          # 加载引擎
          print("⚙️ 加载v5.7引擎...")
          engine_57 = load_engine(ENGINE_V57)
      
          # ---- Run 1: v5.7 + 真实1min半K线 ----
          results_1m = run_backtest_single(
              engine_57, candles_5m, idx_1m,
              sample_rate=args.sample, max_days=args.days,
              n_half=args.half, mode="half_1m", verbose=True,
          )
      
          stats_1m = analyze_results(results_1m,
                                     label=f"v5.7 + 真实1min半K线(前{args.half}根)")
      
          compact = not args.full_report
          print_report(stats_1m, compact=compact)
      
          # ---- Run 2: v5.6 baseline (可选) ----
          stats_open = None
          if args.compare:
              print("\n⚙️ 加载v5.6引擎...")
              engine_56 = load_engine(ENGINE_V56)
      
              results_open = run_backtest_single(
                  engine_56, candles_5m, idx_1m,
                  sample_rate=args.sample, max_days=args.days,
                  mode="open", verbose=True,
              )
              stats_open = analyze_results(results_open, label="v5.6 开盘baseline")
              print_report(stats_open, compact=compact)
      
          # ---- 对比摘要 ----
          if stats_open:
              print(f"\n{'='*60}")
              print(f"📊 对比摘要")
              print(f"{'='*60}")
              print(f"  {'':30s} {'v5.7+1min':>10s} {'v5.6开盘':>10s} {'差异':>8s}")
              print(f"  {'-'*58}")
              print(f"  {'准确率':30s} {stats_1m['accuracy']:>9.1f}% {stats_open['accuracy']:>9.1f}% "
                    f"{stats_1m['accuracy']-stats_open['accuracy']:>+7.1f}pp")
              print(f"  {'方向性':30s} {stats_1m['directional']:>10,} {stats_open['directional']:>10,}")
              print(f"  {'Edge vs random':30s} {stats_1m['edge_vs_random']:>+9.1f}pp "
                    f"{stats_open['edge_vs_random']:>+9.1f}pp "
                    f"{stats_1m['edge_vs_random']-stats_open['edge_vs_random']:>+7.1f}pp")
              print(f"  {'最长连胜':30s} {stats_1m['max_win_streak']:>10} {stats_open['max_win_streak']:>10}")
              print(f"  {'最长连败':30s} {stats_1m['max_lose_streak']:>10} {stats_open['max_lose_streak']:>10}")
              print(f"{'='*60}")
      
          # ---- 保存结果 ----
          if args.save:
              os.makedirs(RESULTS_DIR, exist_ok=True)
              ts = datetime.now(CST).strftime("%Y%m%d_%H%M")
              
              full_result = {
                  "version": "v5.8-1min-half",
                  "timestamp": ts,
                  "n_half": args.half,
                  "sample_rate": args.sample,
                  "max_days": args.days,
                  "stats_1m": stats_1m,
              }
              if stats_open:
                  full_result["stats_open"] = stats_open
      
              path_full = os.path.join(RESULTS_DIR, f"backtest_v58_1min_{ts}.json")
              with open(path_full, "w") as f:
                  json.dump(full_result, f, indent=2, ensure_ascii=False, default=str)
              print(f"\n💾 已保存: {path_full}")
      
              # summary
              summary = {
                  "version": "v5.8-1min-half",
                  "timestamp": ts,
                  "n_half": args.half,
                  "accuracy_1m": stats_1m["accuracy"],
                  "edge_vs_random": stats_1m["edge_vs_random"],
                  "directional": stats_1m["directional"],
                  "correct": stats_1m["correct"],
                  "half_body_acc": stats_1m["half_body_acc"],
                  "max_win_streak": stats_1m["max_win_streak"],
                  "max_lose_streak": stats_1m["max_lose_streak"],
              }
              if stats_open:
                  summary["accuracy_open"] = stats_open["accuracy"]
                  summary["edge_diff"] = round(stats_1m["accuracy"] - stats_open["accuracy"], 1)
      
              path_summary = os.path.join(RESULTS_DIR, "latest_v58_1min_summary.json")
              with open(path_summary, "w") as f:
                  json.dump(summary, f, indent=2, ensure_ascii=False)
              print(f"💾 摘要: {path_summary}")
      
      
      if __name__ == "__main__":
          main()
      
  • logs
    • archive
      • 5minbtc-log.2026-08.jsonl.gz 429.2 KB · in bundle
  • references
    • archive.md 7.9 KB
      # 5minbtc 版本归档
      
      > **原则:旧版本引擎源码不留在本地**(不建 `archive/engines/`),只保留这份归档说明 + git 历史。
      > 需要旧代码时从 git 取,不要在本目录堆积废弃 `.py`。
      
      ## 版本沿革
      
      | 版本 | 日期 | 核心变化 | 源码能否取回 |
      |------|------|---------|------------|
      | v5.0 | ~2026-05 | 基础版(R14 审查 14 项修复) | ❌ 已不可考 |
      | v5.1 | ~2026-05 | 动量窗口 30→15 防饱和;tanh 软饱和替代硬截断 | ❌ 已不可考 |
      | v5.5 | 2026-05-26 | 116 轮 v5.4 复盘:Platt 置信度校准、中性区收缩、bull×0.92 惩罚 | ❌ 已不可考 |
      | v5.6 | 2026-05-27 | R1–R4:趋势衰竭检测、volume 因子修复、V 型反转、Chainlink 对齐 | ❌ 已不可考 |
      | v5.7 | 2026-05-28 | **半 K 线预测策略**(ATR×0.55,只预测剩余 ~55% 时间) | ❌ 已不可考 |
      | v5.7.1 | 2026-06 | 黑天鹅防护(ATR spike + FNG<25 + news) | ❌ 已不可考 |
      | v5.7.2 | 2026-06-17 | half_body vs imbalance 冲突裁决 | ❌ 已不可考 |
      | v5.7.3 | 2026-06-17 | 引擎 HTTP 并行化(9 路) | ❌ 已不可考 |
      | v5.7.4 | 2026-06-18 | TREND 强趋势 decel 极值约束 | ❌ 已不可考 |
      | v5.8.0 | 2026-08-11 | `taker_buy` 因子 + 订单簿多时刻采样去噪 | ⚠️ 见下方说明 |
      | v5.9.0 | 2026-08-13 | **对抗式审查重构**:13 因子收敛到 3 信号 + 三层过滤 + 多周期/跨资产/OFI | ✅ `50a4815` 前后 |
      | v5.10.0 | 2026-08-17 | **概率套利**:去中性二选一 + `probability` 字段 + EV 下单 + 时段过滤 | ✅ `50a4815` |
      | v6.0.0 | 2026-08-18 | **真 OFI 驱动**:方向一票交给订单流 + `P(close>open\|ofi)` + 错价 EV | ✅ `e893d31` |
      | v6.0.0 (改名) | 2026-09-10 | 文件改名 `-v5.7.py` → `-v6.0.py`,版本标签对齐 | ✅ `dfa3047` |
      
      各版本的详细变更见 [changelog.md](changelog.md)。
      
      ## ⚠️ v5.8 以前的源码已无法取回
      
      两个原因叠加:
      
      1. **本地不留旧版**(本文件开头的原则),只在改版时覆盖同名文件。
      2. **仓库 git 历史被截断** —— `~/AGI-Super-Team` 是从 GitHub 重新 clone 的(旧的 `/tmp/AGI-Super-Team` 曾被系统清理损坏)。`skills/5minbtc/` 在仓库里**只有 7 个提交**,最早是 `d149f56`(2026-08-17)。
      
      所以 git 里能查到的最早引擎快照 ≈ **v5.8 时期**,v5.0–v5.7 的代码**既不在本地也不在 git**,只在
      [changelog.md](changelog.md) 的文字记录里留有行为描述。
      
      > 原先还有一份 `performance-history.md` 记录 v4.x→v5.7.1 的逐日战绩,因其 83.0% 等数字已被
      > [对抗式审查报告](strategy-adversarial-review.md) 证伪(真实 non-neutral 57.7% / bear 50.0%),
      > 已于 2026-09-10 按最小无用原则删除;需要时从 git 历史取回。
      
      ## 取回历史版本
      
      ```bash
      cd ~/AGI-Super-Team
      
      # 列出引擎文件的所有历史提交
      git log --oneline --all -- skills/5minbtc/5minbtc-engine-v5.7.py
      
      # 取某个版本的引擎内容 (注意旧文件名)
      git show 50a4815:skills/5minbtc/5minbtc-engine-v5.7.py > /tmp/engine-v5.10.py
      
      # 对比两版差异
      git diff e893d31 50a4815 -- skills/5minbtc/5minbtc-engine-v5.7.py
      ```
      
      ## 日志归档
      
      `logs/` 采用「**当月 live + 历史按月压缩**」:
      
      ```
      logs/
      ├── 5minbtc-log.jsonl                    # live,只含当月(不入库)
      └── archive/
          └── 5minbtc-log.YYYY-MM.jsonl.gz     # 按月切分,写后不变(入库)
      ```
      
      - 压缩归档**会同步进仓库**(`.gitignore` 里对 `logs/archive/*.gz` 做了逐级反忽略)。
      - live 的 `*.jsonl` **不入库**(每 5 分钟追加,进 git 会产生巨大且无意义的 diff)。
      - 轮转方法见 [setup-from-scratch.md](setup-from-scratch.md#日志轮转)。
      
      ## 2026-09-10 文档清理记录
      
      按「最小无用原则」删除 11 份文档(references/ 由 34 份 → 23 份)。全部可从 git 历史取回:
      `git show <sha>:skills/5minbtc/references/<文件名>`(删除前的提交见该文件的 git log)。
      
      | 删除的文件 | 理由 |
      |-----------|------|
      | `performance-history.md` | 声称 v5.7.1 达 83.0%,已被[对抗审查](strategy-adversarial-review.md)证伪(真实 57.7% / bear 50.0%);且引用 9 个不存在的 `review-*.md` |
      | `architecture.md` | 仍把「13 正交因子」当方向模型(v5.9 已清零)、把 OFI 列为未来路线图(v6.0 已实现);且代码围栏未闭合导致整篇被当代码渲染 |
      | `decel-collapse-pattern.md` | 主张「LLM 应覆盖引擎方向」,与 v6.0「OFI 一票决定」+ 铁律 #4 直接冲突;decel 因子已清零 |
      | `black-swan-defense-v571.md` | 补丁对象 `v_reversal`/`decel` 已清零;幸存内容(ATR spike + FNG<25)已在 SKILL.md 架构行 |
      | `engine-parallelization-v573.md` | 描述「4 路并行」,现为 9 路;唯一有效的 pitfall 已存于 [pitfalls.md](pitfalls.md) #5 |
      | `cron-setup.md` | 薄壳:版本同步规则与 pitfalls #7 重复,job 表已在 [cron-llm-provider-failure.md](cron-llm-provider-failure.md) §8(本次补入了原始 cron 表达式) |
      | `polymarket-data-source.md` | 交易场所已换成币安 Web3 预测市场,Polymarket 盘口框架过时;Chainlink 价差风险仍在 pitfalls 的 `chainlink_offset` 条目 |
      | `dreaming-cron-recovery.md` | 2026-06 单次事故记录,其数据源 fallback 建议从未落地;job 仍在,说明见 cron-llm-provider-failure.md §8 |
      | `session-2026-06-17.md`、`session-2026-06-18.md` | 结论已 100% 蒸馏进 `lessons.md` #12/#14/#18 与 `changelog.md` v5.7.4 |
      | `daily-stock-analysis-data-sources.md` | 无关内容:A 股/港股/美股股票项目的 fetcher 评估 |
      
      同时删除 **6 个死函数**(详见 [output-template.md](output-template.md#13-已删除--仍残留的死代码)):
      引擎的 `calibrate_confidence`(v5.5 Platt 残骸)、realtime 的 `record_limit` / `fmt_limit` / `fmt_skip_knife`(已废弃的「甜区限价挂单」)与 `fmt_no_edge` / `fmt_signal`。
      
      **同时修正的(不删)**:
      
      | 项 | 处理 |
      |---|---|
      | `quant-knowledge-index.md` | 原始路径 `~/.hermes/profiles/cqo/quant-knowledge/` **实测已不存在** → 改为指向本 skill `reports/`(唯一存活副本);修正 R07/R10 两个写错的文件名;标注 P0#1(OFI)已落地、R13 已过时 |
      | `telegram-push-monitor.md` | 内容仍准确(描述正在跑的 watch daemon)→ **补进 SKILL.md 索引**(此前未被索引),并修正其中「neutral 事件」「每日推送时机」两处过时描述 |
      | `scripts/test_engine_v58.py` | **改名为 `scripts/test_engine.py`**(不带版本号 → 不再漂移)+ 修 2 个必失败的断言(version 5.8.0→6.0.0、`BASE_W['taker_buy']` >0→==0)+ 补 `ofi` 块契约测试 + 加**免 pytest 的内置 runner**。现 13 项全绿 |
      
      **保留判断**(看似重复但经复核不删):Binance 三份网络文档是**三种不同故障模式**;
      `reports/` R01–R14 被 [quant-knowledge-index.md](quant-knowledge-index.md) 索引;
      `lessons.md` / `pitfalls.md` / `strategy-adversarial-review.md` 是「历史叙事 / 行动索引 / 权威结论」三种角色,互补而非重复。
      
      ## 归档 SOP(每次引擎升版时做)
      
      1. 改版本号:引擎 docstring、`SKILL.md` frontmatter 的 `version:`、`README.md` badge。
      2. **文件改名跟随版本**(如 `-v6.1.py`),避免再次出现「文件名 / 文档 / 代码」三个版本号打架。
      3. 更新全量引用:`grep -rn "5minbtc-engine-v" --include='*.py' --include='*.md' .`
      4. 在 [changelog.md](changelog.md) 顶部**新增**一节(不要改历史节)。
      5. 在本文件的「版本沿革」表补一行,填上 commit hash。
      6. 重启守护进程(`ENGINE` 是模块级常量,进程不会自动感知改名):
         ```bash
         launchctl kickstart -k gui/$(id -u)/com.daniel.5minbtc-realtime
         launchctl kickstart -k gui/$(id -u)/com.daniel.5minbtc-watch
         launchctl kickstart -k gui/$(id -u)/com.daniel.5minbtc-paper
         ```
      7. 同步仓库,见 [sync-procedure.md](sync-procedure.md)。
      
    • backtest-findings.md 4.7 KB
      # 365天回测深度复盘 (2026-05-27)
      
      > 105,120根5min K线 | 2025-05-28 ~ 2026-05-27 | 回测引擎: `backtest/run_backtest.py`
      
      ## 一、核心结论
      
      **v5.6引擎的11个因子在公平条件下无预测力。实盘66.1%的edge来自前视偏差。**
      
      ## 二、A/B测试三模式对比
      
      | 模式 | 当前K线处理 | 方向准确率 | vs Baseline |
      |------|-----------|-----------|-------------|
      | `historical_only` | c=h=l=open, v=0 | **47.4%** | -1.4pp |
      | `fair_live_sim` | c=h=l=open, v=0 | **48.8%** | 0.0pp |
      | `full_lookahead` | 含真实close | **61.1%** | +12.3pp |
      | Baseline (always-bull) | — | **48.8%** | — |
      | **实盘** (298轮) | progress=0.9+ | **66.1%** | +17.3pp |
      
      ## 三、因子逐一分析
      
      | 因子 | 权重 | 方向一致率 | 判定 |
      |------|------|-----------|------|
      | momentum | 1.0 (最高) | **48.1%** | ❌ 反向! |
      | rsi | 0.8 | **47.3%** | ❌ 反向! |
      | decel | 0.7 | **50.17%** | ≈ 随机 |
      | meanrev | 0.7 | **51.7%** | ⚠️ 唯一正向但极弱 |
      | volume | 0.5 | ~50% | ≈ 随机 |
      | imbalance | 0.6 | N/A | 实盘日志中始终=0.000 |
      | microprice | 0.6 | N/A | 实盘日志中始终=0.000 |
      | v_reversal | 0.8 | ~50% | 新因子无贡献 |
      | vol_breakout | 0.4 | ~50% | 新因子无贡献 |
      
      **关键发现**:momentum和RSI是反向的——这意味着"顺势"在5分钟尺度上是亏损策略。价格在5分钟内的运动更像均值回归(随机游走+微小漂移),而不是趋势延续。
      
      ## 四、前视偏差诊断
      
      引擎实盘运行流程:
      1. Cron每5分钟触发,通常在K线进行到90%+时执行
      2. `fetch_candles()` 取最近200根K线,最后一根是**当前未完成K线**
      3. `compute_factors()` 中 `closes[-1]` 就是当前K线的实时close
      4. momentum = `tstat(closes[-20:])` → 当K线完成90%时,这个值已经非常接近最终值
      5. RSI/meanrev同理——都基于接近最终的close计算
      
      **本质**:引擎不是在"预测5分钟后",而是在"确认当前已有走势"。这在Polymarket上有效是因为:如果当前K线已经涨了$50,剩余30秒不太可能全部回撤,所以"确认当前方向"≈"预测最终方向"。
      
      ## 五、置信度校准失败
      
      | 置信度区间 | 样本数 | 准确率 |
      |-----------|--------|--------|
      | 40-44 | 17,025 | **50.2%** |
      | 45-49 | 29,147 | **47.6%** |
      | 50-54 | 7,795 | **48.6%** |
      | 55-59 | 532 | **49.7%** |
      
      **结论**:Platt Scaling在回测中完全无效。高置信度没有带来更高准确率。实盘中的校准效果可能也是小样本假象。
      
      ## 六、Regime分析
      
      | Regime | 样本数 | 准确率 |
      |--------|--------|--------|
      | TREND | 30,621 | **47.5%** |
      | RANGE | 15,379 | **50.0%** |
      | LOW_VOL | 8,499 | **49.9%** |
      
      所有regime下均无显著预测力。TREND regime反而最差(47.5%)——说明引擎的"趋势跟踪"在5分钟尺度上是亏损的。
      
      ## 七、实盘edge来源假说
      
      1. **前视偏差(最可能)**:progress=0.9+时closes[-1]≈最终close,momentum/RSI间接含未来信息
      2. **近因偏差**:实盘数据仅298轮(约2周),BTC可能恰好处于趋势行情
      3. **小样本偏差**:66.1% ± 5.5% (95% CI: 55.6%~76.6%),真值可能在50%附近
      4. **LLM增量**:LLM综合分析可能提供了引擎因子之外的增量信息(新闻+K线形态判断)
      
      ## 八、战略建议
      
      ### 不应该做的
      - ❌ 继续调参(权重/阈值) — 因子本身无信息量
      - ❌ 增加更多纯价格因子 — 价格衍生因子在5分钟尺度上已饱和
      - ❌ 用实盘66%作为策略预期 — 大概率会回归
      
      ### 应该做的
      - ✅ **引入Orderbook数据**:OFI/Stoikov microprice是唯一在学术文献中有5分钟预测力的信号(R²=15-25%)
      - ✅ **新闻脉冲信号**:突发事件(Big发布/SEC/ETF)可在5分钟内产生方向性冲击
      - ✅ **改变预测范式**:从"因子加权打分"→"实时Orderbook流+事件驱动"
      - ✅ **降低仓位**:在edge来源明确之前,不应按66%胜率下注
      
      ## 九、回测系统
      
      ```
      backtest/
      ├── fetch_data.py       # Binance数据下载器(106批请求)
      ├── run_backtest.py     # 回测引擎(导入v5.6因子模块)
      ├── run.sh              # 一键运行(--fast/--sample/--days/--skip-fetch)
      ├── README.md           # 使用文档
      ├── data/
      │   └── btcusdt_5m.json # 105,120根K线(12MB)
      └── results/
          ├── backtest_20260527_2328.json  # 完整结果(17MB)
          └── latest_summary.json          # 摘要(5.2KB)
      ```
      
      运行命令: `cd backtest && python3 -u run_backtest.py --progress=0.5 --full-report`
      
      ## 十、方法论说明
      
      回测公平性设计:
      - `progress=0.01`(默认):当前K线价格信息完全屏蔽(c=h=l=open, v=0)
      - `progress=0.5`:允许50%的当前K线信息
      - `progress=1.0`:完整前视(full_lookahead模式)
      - 引擎的200根K线窗口中,仅前199根(已完成)提供有效信息
      
    • backtest-v58-1min-findings.md 3.1 KB
      # v5.8 真实1min半K线回测发现 (2026-05-28)
      
      ## 方法论
      
      **核心改进**: 不再模拟半K线状态,而是用对应5min K线时间段内的**真实1分钟K线**构建半程状态。
      
      之前的错误方法:
      - `(open + actual_close) / 2` → 前视偏差(已知最终方向)
      - 前一根5min K线的body → 太粗糙,无法模拟当前K线内状态
      
      正确方法(Daniel提出):
      - 5min K线的open_time总是5的整数倍分钟
      - 对应的1min K线: open_time, open_time+60s, ... open_time+240s
      - 取前N根1min K线的真实OHLCV构建半K线状态
      - 完全模拟实盘: 在第N分钟时你确实看到了这些数据
      
      ## 数据
      
      - 5min: 105,120根 (2025-05-27 ~ 2026-05-27), `data/btcusdt_5m.json`
      - 1min: 259,210根 (半年), `data/btcusdt_1m.json` (29MB)
      - 覆盖率: 99.5%的5min K线能找到对应的1min数据
      
      ## 结果
      
      ### 前N根1min → 准确率 (半年, sample=6, ~8600轮)
      
      | 前1根(20%) | 前2根(40%) | 前3根(60%) | 前4根(80%) | 前5根(100%) |
      |:---:|:---:|:---:|:---:|:---:|
      | 56.7% | 61.7% | 65.8% | 69.5% | 76.1% |
      
      ### 前2根完整回测 (sample=1, 51,591轮)
      
      - 方向性: 27,237 | 中性: 24,354
      - 正确: 16,727 | 错误: 10,510
      - **准确率: 61.4%**, edge +11.4pp
      - 最长连胜: 22 | 最长连败: 12
      - half_body激活: 82.4%, 方向准确率: 63.8%
      
      ### 月度准确率 (前2根1min)
      
      | 月份 | 准确率 | 方向性 |
      |------|--------|--------|
      | 2025-11 | 63.0% | 165 |
      | 2025-12 | 62.9% | 4,720 |
      | 2026-01 | 61.7% | 4,742 |
      | 2026-02 | 60.8% | 4,191 |
      | 2026-03 | 61.0% | 4,735 |
      | 2026-04 | 61.0% | 4,568 |
      | 2026-05 | 60.8% | 4,116 |
      
      ### 因子方向准确率排名
      
      | 因子 | 准确率 | 备注 |
      |------|--------|------|
      | volume | 58.1% | ✅ 唯一有独立预测力 |
      | rsi | 50.5% | 无 |
      | momentum | 50.3% | 无 |
      | vol_breakout | 50.1% | 无 |
      | v_reversal | 50.0% | 无 |
      | meanrev | 49.6% | 无 |
      | decel | 49.4% | 无 |
      | fatigue | 49.2% | 无 |
      | position | 49.2% | 无 |
      | **half_body** | **48.8%** | ❌ 因子本身无预测力 |
      
      ## 关键洞察
      
      1. **半K线延续性是真正的alpha** — 价格在前2分钟建立的动量有61.4%概率延续到K线结束
      2. **线性增长关系** — 更多1min数据 = 更高准确率,说明是纯粹的延续性效应
      3. **half_body因子的真正贡献** — 不是方向预测(48.8%),而是触发更紧的ATR阈值×0.55,让延续性效应更好地转化为预测区间
      4. **volume因子** — 唯一有独立alpha的技术因子(58%),可能反映大单的方向性冲击
      5. **其他因子全是噪声** — momentum/RSI/meanrev/decel/position在5分钟尺度上无预测力
      
      ## 战略含义
      
      - 半K线延续性是可利用的真实市场微结构效应(类似order flow continuation)
      - 更优策略: 直接做1分钟级别的动量延续策略,而非5分钟
      - 当前引擎的价值: half_body因子+紧ATR阈值的组合,而非12个因子的信号本身
      - 可以考虑: 简化因子组合,只保留volume+half_body+延续性机制
      
      ## 回测脚本
      
      - `backtest/run_backtest_v58.py` — 支持参数:
        - `--half N` 用前N根1min (默认2)
        - `--fast` sample=6 + 180天
        - `--compare` 同时跑v5.6 baseline
        - `--full-report` 含月度和因子分析
      
    • binance-api-geo.md 2.4 KB
      # Binance API Geo-Restriction Fix
      
      ## Problem
      From China mainland, ALL standard Binance API endpoints return **HTTP 451** (Unavailable for Legal Reasons):
      - `api.binance.com` → 451
      - `api.binance.me` → 451
      - `api1.binance.com` → 451
      
      ## Working Endpoints (tested 2026-05-22, updated 2026-06-14)
      
      | Endpoint | Status | Notes |
      |----------|--------|-------|
      | `api.binance.us` | ✅ **Primary** | Most reliable across network conditions, including high-latency networks |
      | `data-api.binance.vision` | ⚠️ Fallback | Works on low-latency networks; **SSL handshake times out on high-latency networks** (ping >250ms) |
      
      ### 2026-06-14 Update: SSL Timeout on data-api.binance.vision
      
      On networks with high latency (ping to 8.8.8.8 ~300ms), `data-api.binance.vision` consistently fails with:
      ```
      TimeoutError: _ssl.c:999: The handshake operation timed out
      ```
      Even with Python `urlopen(..., timeout=20)`, the TLS handshake can't complete. `api.binance.us` handles the same network conditions without issues.
      
      ### Endpoint Quick Test
      ```bash
      for ep in "api.binance.us" "data-api.binance.vision"; do
        code=$(curl -s --connect-timeout 10 --max-time 15 -o /dev/null -w "%{http_code}" \
          "https://$ep/api/v3/klines?symbol=BTCUSDT&interval=5m&limit=1")
        echo "$ep → HTTP $code"
      done
      ```
      
      ### Engine Endpoint Switch Commands
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc
      
      # Switch to api.binance.us
      sed -i 's|BINANCE_KLINES = .*|BINANCE_KLINES = "https://api.binance.us/api/v3/klines"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      sed -i 's|BINANCE_DEPTH = .*|BINANCE_DEPTH = "https://api.binance.us/api/v3/depth"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      
      # Switch to data-api.binance.vision
      sed -i 's|BINANCE_KLINES = .*|BINANCE_KLINES = "https://data-api.binance.vision/api/v3/klines"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      sed -i 's|BINANCE_DEPTH = .*|BINANCE_DEPTH = "https://data-api.binance.vision/api/v3/depth"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      ```
      
      ## Files Using Binance API
      - `5minbtc-engine-v6.0.py` — `BINANCE_KLINES` and `BINANCE_DEPTH` constants (klines + depth fetch)
      - `5minbtc-log.py` — hardcoded URL in `settle_candle()` and `settle-all` (2 occurrences)
      
      ## Migration Context
      - Original OpenClaw cron ID: `14880142-c824-41cc-b41b-6079b072e322`
      - Hermes cron ID: `d8058223a1e0`
      - Schedule: `2,7,...,57 20-22 * * *` (UTC) = CST 04:02-06:57
      - WORKSPACE hardcoded as `workspace-cqo` → changed to `SKILL_DIR = os.path.dirname(os.path.abspath(__file__))`
      
    • binance-endpoint-flapping.md 3.4 KB
      # Binance Endpoint Flapping Pattern
      
      > 2026-06-18 session observation
      
      ## The Pattern
      
      Both `api.binance.us` and `data-api.binance.vision` exhibit **intermittent flapping** — neither is permanently reliable. The failure is **bi-directional** and appears to be related to WireGuard tunnel instability rather than a problem with any specific Binance endpoint.
      
      ## Observed Behavior (2026-06-18)
      
      | Time | api.binance.us | data-api.binance.vision | curl | Python urllib |
      |------|---------------|------------------------|------|--------------|
      | 20:10 | ❌ | ❌ | ❌ (exit 28 timeout) | ❌ |
      | 20:11 | ✅ | — | ✅ (HTTP 200) | — |
      | 20:12 | — | ❌ | ❌ (exit 28) | ❌ |
      | 20:13 | ✅ | — | ✅ (HTTP 200) | — |
      
      ## Key Observations
      
      1. **Both endpoints fail at the same time** when the WireGuard tunnel drops — this rules out endpoint-specific issues
      2. **curl and Python urllib fail simultaneously** — the issue is at the kernel/network level, not the SSL library
      3. **Retrying 1-2 minutes later often works** — the tunnel self-heals within minutes
      4. **Error signature**: `curl: (28) Operation timed out` or `SSL_ERROR_SYSCALL` — no DNS or routing issue, just a dropped connection at the tunnel layer
      
      ## Recovery Procedure
      
      ```bash
      # Step 1: Test both endpoints
      for ep in "api.binance.us" "data-api.binance.vision"; do
        code=$(curl -s --connect-timeout 5 --max-time 10 -o /dev/null -w "%{http_code}" \
          "https://$ep/api/v3/klines?symbol=BTCUSDT&interval=5m&limit=1" 2>&1)
        echo "$ep → $code"
      done
      
      # Step 2: If both fail, wait 1-2 minutes and retry (tunnel self-heals)
      sleep 90
      # retry Step 1
      
      # Step 3: If one endpoint works and the other doesn't, switch engine to the working one
      # (see binance-api-geo.md for switch commands)
      
      # Step 4: If both still fail after 5 minutes, check WireGuard tunnel
      ping -c 3 8.8.8.8
      ping -c 3 198.18.0.10  # WireGuard virtual IP
      ```
      
      ## Diagnosis Commands
      
      ```bash
      # Test basic network (if this fails, it's not Binance-specific)
      curl -s --connect-timeout 5 --max-time 10 "https://google.com" -o /dev/null -w "%{http_code}"
      
      # Check if the issue is DNS or tunnel
      nslookup api.binance.us
      nslookup data-api.binance.vision
      
      # Verbose SSL diagnostics
      curl -v --connect-timeout 10 "https://api.binance.us/api/v3/klines?symbol=BTCUSDT&interval=5m&limit=1" 2>&1 | grep -E "SSL|connect|time|error|HTTP"
      
      # Test with resolved IP (bypass DNS)
      curl -s --connect-timeout 5 --max-time 10 \
        --resolve "api.binance.us:443:208.115.61.204" \
        "https://api.binance.us/api/v3/klines?symbol=BTCUSDT&interval=5m&limit=1" \
        -o /dev/null -w "%{http_code}"
      ```
      
      ## What NOT To Do
      
      - **Don't switch back and forth rapidly** — flapping makes the endpoint the wrong variable to optimize. Wait for the tunnel to stabilize instead.
      - **Don't file a bug against a specific endpoint** — both fail together; it's not endpoint-specific.
      - **Don't increase timeout beyond 15s** — if the tunnel is down, a longer timeout just delays recovery.
      - **Don't try api.binance.com** — will return HTTP 451 from this network.
      
      ## Lesson for the Skill
      
      The correct response to an "all endpoints down" scenario is:
      1. Confirm the pattern (test both endpoints)
      2. Wait 1-2 minutes
      3. Retry
      4. If still down: report to user, suggest checking WireGuard tunnel
      
      The incorrect response is: rotating endpoints rapidly, increasing timeouts, or trying unblocked-but-blocked endpoints (api.binance.com).
      
    • changelog.md 13 KB
      # 5minbtc 版本变更日志
      
      > 详细 changelog。SKILL.md 只保留当前版本亮点。
      >
      > 版本号分两条线:**引擎文件内的 docstring 版本**与**策略版本**曾长期脱节(文件名 `-v5.7.py` 里跑的是 v6.0 代码),v6.0.0 起已对齐。
      
      ## v6.0.0 变更 (2026-08-18, commit e893d31) — 真 OFI 驱动: 方向一票交给订单流
      
      **背景**:v5.9 的对抗式审查已经证明"方向预测"没有 alpha(bear 50.0% = 纯硬币,non-neutral 57.7%)。
      v6.0 不再假装能预测方向,改为**检测错价**:真 OFI 净流方向 = 真实资金流方向,
      若净流已经发生、而 UP/DOWN token 价还没跟上,中间那段 gap 才是真正的钱。
      
      | 变更项 | 详情 |
      |--------|------|
      | **方向判定** | `ofi_direction()`: bias 由**真 OFI 净流一票决定**(`ofi_n>0`→bull / `<0`→bear),替代 v5.10 的 `body>0→bull` 纯延续统计。**二选一无中性**;`ofi_n` 缺失/为 0 时用 body 符号兜底并标 `meta.body_fallback`(弱信号/流量不足只让概率趋近 0.5 → EV 过滤不下单,不产生 neutral) |
      | **OFI 主源** | 当前 K 线**原生 in-candle** `ofi_n = 2*(tb/v) − 1` ∈ [−1,1] — 取 REST `klines[9]`(taker buy base volume)做原生 aggressor 聚合,**天然按 K 线对齐,零 WS 依赖** |
      | **OFI 辅源** | WS `~/bb-auto/ofi.json`(`ofi_candle` / `ofi_60`)用于**新鲜度反转保护**与交叉校准;`load_ofi()` 新增 **ts 保鲜校验**(age > 30s → `feed_fresh=False`,WS 分量自动降级,引擎只信 REST 主源) |
      | **概率** | `ofi_probability()`: `confidence = P(close>open \| ofi)`,替代 `close_direction_confidence()` 的延续概率。三层 = ① 经验校准表(按 `ofi_n` 分桶 + **Bayesian shrink**,`OFI_BAYES_N=30` 向 0.5 收缩)② flow-gap(净流已发生但价格未定价)③ 最近 60s 流(按剩余时间衰减) |
      | **edge** | 改为**错价检测** `EV = p − ask`(真 OFI 概率 vs token 市场价),由 `5minbtc_realtime.py` 执行 |
      | **风控** | realtime 新增 **D2 有效性闸**:滚动 200 笔已结算 OFI 方向胜率 < 0.55 → 自动暂停 + 告警(防第二次 52.6%) |
      | **保留** | ATR spike / FNG<25 / news 黑天鹅断路器;MTF 4h **只降权不翻方向**;EV 下单框架;**13 因子仍计算并输出,但不再参与 bias** |
      | **门限** | `T_OFI_GATE=0.20` / `T_OFI_60=0.35` / WS 质量闸 `OFI_CR_MIN=0.80` |
      
      **契约**:输出 `"version": "6.0.0"`,`factors` / `score` 字段保留(仅作 `strength` 标签与 LLM 参考)。
      
      **命名修正**:引擎文件自本版起为 `5minbtc-engine-v6.0.py`。此前文件名为 `5minbtc-engine-v5.7.py`,与其中运行的 v6.0 代码完全脱节——`scripts/*.py` 的 `ENGINE` 常量、`SKILL.md`、`README.md`、`references/` 中的引用已全量更新。
      ⚠️ 注意:由此产生一个副作用——`references/` 里的**历史文档**(pitfalls / binance-api-geo / high-latency 等)中的旧文件名也被一并替换为 v6.0,阅读时请知悉那些命令当时对应的其实是旧名文件。
      
      ## v5.10.0 变更 (2026-08-17, commit 50a4815) — 概率套利策略 + pnl 修复 + 时段过滤
      
      **从"猜方向"转向"比价格"的转折点** —— 第一次把引擎输出当成概率去和市场报价比较。
      
      | 变更项 | 详情 |
      |--------|------|
      | 方向去中性 | 删掉 `neutral` 阈值分支(原 v5.5 的 `[-1,1]` 中性区),改为 `bias = "bull" if body > 0 else "bear"` **二选一**,方向由半 K 线 body 决定 |
      | 概率字段 | `prediction` 新增 `probability = round(confidence/100, 3)`(此时仍为 v5.9.2 的**半 K 线延续概率**) |
      | 概率套利 | realtime: 引擎概率 > 市场 ask + `min-edge(0.03)` → 买(`EV = p − P > 0`);否则无 edge 跳过 |
      | 时段过滤 | 新增 `--active-hours`(默认 `20,21,22,23`)——只在历史高胜率时段下单 |
      | pnl 修复 | 输单应亏**全额** `-amount`,此前错算为"亏入场价 `-amount*ask`"(系统性低估亏损) |
      | 稳定性 | 崩溃循环修复 + 台账去重 + 精简推送 |
      
      ## v5.9.0 变更 (2026-08-13) — 对抗式审查重构: 13 因子收敛到 3 信号
      
      **依据**:[strategy-adversarial-review.md](strategy-adversarial-review.md)(5 位对抗审查专家的结论综合)。核心结论——**方向准确率不是 edge,`EV = p − P − 成本` 才是**。
      
      | 变更项 | 详情 |
      |--------|------|
      | 因子证伪 | 公平回测显示 13 个因子里 **11 个只有 47–49%**(等同抛硬币),其中 `momentum` 48.1%、`rsi` 47.3% **反向**;唯一有独立 alpha 的是 `volume` 58.1%,而它权重只有 0.3 —— **权重与证据完全倒挂** |
      | 权重收敛 | `BASE_W` 中 10 个因子清零,只留 `half_body 1.2`(延续主信号)+ `volume 0.8`(唯一独立 alpha)+ `meanrev 0.3`(唯一正向价格因子 51.7%);未验证的 `taker_buy` 也清零 |
      | 新增三层过滤 | ① **多周期结构** 4h/1h/15m(linreg slope + ADX + %B)② **跨资产广度** ETH/SOL 5m 动量(趋势日逆势信号打折)③ **真订单流 OFI** |
      | 移除 | `bull×0.92` 惩罚、Platt 置信度门控(v5.5 引入,被证为反校准) |
      | v5.9.2 | 收盘方向置信度改用**半 K 线延续概率**(`close_direction_confidence`),替代 Platt Scaling |
      
      **认知转变**:从前视偏差回测的 71.2% 幻觉中退出——零前视 v5.8 只有前 2 根 1min 61.4%,且 61–70% 是"看着 K 线走完再确认"的延续性,做市商早已定价进 token 价。
      
      ## v5.8.0 变更 (2026-08-11) — 主动买量因子 + 订单簿多时刻去噪
      
      **量能/订单簿信号质量升级 — 对齐 OFI 路线图的第一步**
      
      | 变更项 | 详情 | 效果 |
      |--------|------|------|
      | `taker_buy` 因子 (因子13) | `fetch_klines` 解析 Binance klines 索引9 (taker buy base volume),新增 `tb` 字段;`taker_buy_signal()` 取最近5根已完成K线 tb/v 占比均值映射 [-1,1] | 区分主动买/主动卖,替代「投影放量」的近似。`tb=0` 是有效信号(全主动卖)不跳过;`tb>v` 容错钳制 |
      | 权重接线 | BASE_W 加 `taker_buy=0.7`,REGIME_ADJ 各 regime 对应加值;`combine_factors` 动态查权重自动计入 score | 新因子立即生效,无需硬编码白名单 |
      | `fetch_depth_avg` 多时刻采样 | 订单簿 3 次采样 ×1s,同价位 bids/asks 按量平均合并;单次失败跳过,全失败返回 None | 降低单次快照的瞬时报单噪声。合并结果按 limit 截断避免层数膨胀 |
      | 契约保持 | `prediction` 子对象结构不变,version → 5.8.0 | 监控脚本零改动兼容 |
      
      **关键 pitfall (测试暴露)**: 初始实现用 `v>0 and tb>0` 守卫,把 `tb=0` 当作「无数据跳过」——但实盘 `tb=0` 是强信号(主动买量为零=全主动卖),被误跳过会导致看空信号丢失。已改为「tb 字段缺失才跳过」。
      
      **验证**: `scripts/test_engine_v58.py` 12 项测试 (契约/数值/多时刻采样/稳定性) 全绿。
      (注:该文件已于 2026-09-10 改名为 `scripts/test_engine.py` 并升级为 13 项 —— 见 [archive.md](archive.md) 清理记录)
      
      
      
      ## v5.7.4 变更 (2026-06-18) — TREND强趋势decel极值约束
      
      
      **2026-06-18 00:10 candle mispredict复盘驱动**
      
      | 变更项 | 详情 | 根因 |
      |--------|------|------|
      | TREND decel约束 | EMA delta>$100的强TREND中,\|decel\|>0.7必须配合\|half_body\|>0.15同向才可翻转方向 | 00:10 candle: EMA delta=+$188, decel=-0.801, half_body=+0.009十字星, engine=neutral。LLM基于decel+position覆盖bear→实盘收涨❌。强趋势中decel减速是正常呼吸,十字星=空方无力 |
      | LLM覆盖纪律 | engine=neutral/weak(score≈0)且regime=TREND时,decel极值不能单独作为方向翻转依据 | 仅decel+position在强TREND中是不够的——需要half_body实体确认 |
      
      **关键洞察**: decel测量的是价格变化率的变化——在强趋势中,短暂减速后继续同向是常态而非反转。仅当half_body确认实际方向已改变(实体>ATR×0.15)时,decel极值才构成有效反转信号。
      
      
      
      ## v5.7.3 变更 (2026-06-17) — 引擎HTTP并行化
      
      
      **用户反馈"预测太慢"→引擎4路并行HTTP提速 4-5x**
      
      | 变更项 | 详情 | 效果 |
      |--------|------|------|
      | ThreadPoolExecutor(max_workers=4) | 4个HTTP调用(klines+depth+FNG+chainlink)并行发射 | 引擎12-18s→~3.0s |
      | `_fetch_fng()` 独立函数 | 并行化需要picklable的函数引用 | 不破坏原有逻辑 |
      | 全链路提速 | 引擎+新闻+搜索并行→全链路~7s | 之前22-29s,提速~3x |
      
      **关键pitfall**: `ex.submit(fetch_klines, 200)` 把 `200` 传给第一个参数 `symbol`→HTTP 400。必须用 `ex.submit(fetch_klines, limit=200)`。
      (该 pitfall 现存于 [pitfalls.md](pitfalls.md) #5;原 `engine-parallelization-v573.md` 已于 2026-09-10 删除,因其"4 路并行"已被 v6.0 的 9 路取代。)
      
      
      
      ## v5.7 变更 (2026-05-28)
      
      
      **核心策略变更: 在K线第4分钟执行(progress~80%),利用前半根K线实体方向确认延续**
      
      设计思路(Daniel提出): 实盘66%准确率的edge来自"确认当前K线已有走势"(progress=0.9+),v5.7把这个隐性优势显性化。
      
      | 变更项 | 详情 | 效果 |
      |--------|------|------|
      | S1 `half_body_momentum()` | 新因子,progress≥0.45时激活,ATR归一化实体+tanh压缩 | 将实盘edge显性建模 |
      | S2 ATR乘数×0.55 | pred_close乘数 0.20/0.12→0.11/0.066 | 只预测剩余~2.5分钟 |
      | S3 half_range缩窄 | 0.65→0.40 | 区间更紧凑 |
      | Cron调度 | 第2分钟执行(`2,7,12...`) | progress~40%,前2根1min构建半K线 |
      | `BASE_W['half_body']=1.2` | 所有因子中最高权重 | 核心edge因子 |
      | `REGIME_ADJ` 更新 | 所有4个regime添加half_body权重 | regime-aware调整 |
      
      `half_body_momentum()` 关键设计:
      - 激活阈值: progress≥0.45 (给K线足够时间形成实体)
      - 进度加权: `min(1.0, (progress-0.3)/0.7)` — 0.45→0.2, 0.8→0.71, 1.0→1.0
      - ATR归一化: body/ATR → tanh(×2.0) → [-1, 1]
      - progress<0.45时返回0.0 (不干扰其他因子)
      
      
      
      ## v5.8 回测方法论突破 (2026-05-28)
      
      
      **关键改进: 用真实1分钟K线构建半K线状态,零前视偏差**
      
      之前v5.7回测用 `(open+actual_close)/2` 模拟中点→71.2%但含前视偏差;用前一根5min body→48.1%无预测力。
      **正确方法(Daniel提出)**: 用对应5min K线的前2-3根真实1分钟K线的OHLCV构建半程状态。
      
      回测结果(半年, 27,237轮方向性):
      
      | 前1根1min (20%) | 前2根 (40%) | 前3根 (60%) | 前4根 (80%) | 前5根 (100%) |
      |:---:|:---:|:---:|:---:|:---:|
      | 56.7% | **61.4%** | 65.8% | 69.5% | 76.1% |
      
      核心发现:
      1. 半K线延续性是真正的alpha来源 — 越接近完成越准,近似线性增长
      2. 前2根(当前cron配置): 27,237轮→61.4%, edge +11.4pp, 最长连胜22
      3. half_body因子方向准确率仅48.5% — alpha不在因子本身,在于触发了更紧的ATR阈值
      4. volume是唯一有独立预测力的因子(56-58%),其他因子全部49-51%
      5. 之前的实盘66%准确率谜团已解开 — 就是半K线延续性效应
      
      回测文件: `backtest/run_backtest_v58.py` (数据: `backtest/data/btcusdt_1m.json`, 259k根)
      用法: `python3 run_backtest_v58.py --fast --compare` 或 `--half 3` (前3根1min)
      
      
      
      ## v5.6 变更 (2026-05-27)
      
      
      基于3次方向错误深度复盘的4项修复(已实施验证):
      
      | 修复项 | 变更 | 根因 |
      |--------|------|------|
      | R1 momentum/decel冲突检测 | \|mom\|>0.7且\|decel\|>0.8且方向相反时,动态降权mom 1.0→0.4, 升权decel 0.7→0.9 | V型反转点momentum锁定错误方向 |
      | R2 V型反转因子 | `v_reversal_detect()` 检测低点抬高模式 [-1,1], BASE_W=0.8 | 捕捉结构性的反转信号 |
      | R3 放量突破因子 | `vol_breakout_signal()` 用最近3根完整K线的最大量K线方向 [-1,1], BASE_W=0.4 | 避免未完成K线的量价误导 |
      | R4 Chainlink价格对齐 | `fetch_chainlink_ref()` Coinbase BTC-USD作参考,自动补偿Binance偏移 | Polymarket结算价≠Binance价格 |
      
      关键代码模式:
      - **冲突降权用saved_base模式**: `saved_base = BASE_W.copy()` → 临时改权重 → `combine_factors()` → `BASE_W.update(saved_base)` 恢复。避免跨调用污染全局权重。
      - **Chainlink偏移安全边界**: |offset|>300时不补偿(防止API异常导致预测价飞出合理范围)
      - **V反转用已完成K线**: 取 `candles[-4:-1]`(最近3根完成K线),不用当前未完成K线
      - **输出新增字段**: `chainlink_offset` 追踪每次偏移量, `v_reversal`/`vol_breakout` 因子值
      
      
      
      ## v5.5 变更 (2026-05-26)
      
      
      基于120轮v5.4实战复盘的4项优化:
      
      | 优先级 | 修复项 | 变更 | 影响 |
      |--------|--------|------|------|
      | P0-1 | 置信度校准 | `40+abs(score)` → Platt Scaling sigmoid | conf与准确率正相关 |
      | P0-2 | 新闻因子 | 98% NEUTRAL死代码,保留扫描给LLM | 减少噪声 |
      | P1-1 | Bull bias | score>0时×0.92衰减 | bear 70.2% > bull 65.4%修正 |
      | P1-2 | 高vol惩罚 | HIGH_VOL 0.6→0.45 | 放量准确率更低 |
      
      附加优化:
      - neutral区收缩 [-2,2]→[-1,1],减少无信息预测
      - 置信度上限 80→85,让强信号有区分度
      - `calibrate_confidence()` 独立函数,midpoint=15, steepness=0.10
      
      
      
      
    • cron-llm-provider-failure.md 12.4 KB
      # Cron Job LLM Provider 失效诊断与切换
      
      > 2026-06-21 实战案例 + 操作模板
      
      ## 0. 一句话摘要
      
      看到 `RuntimeError: Error code: 401 - Invalid token` ≠ token 失效。最常见的真实根因是 **主 provider 账户欠费 (HTTP 402)**,fallback 链上某个 provider 的 API key 未注入到 environment,最后一个哑弹 provider 才报的 401。
      
      **先看 errors.log 瀑布,再决定行动。**
      
      ---
      
      ## 1. 故障现象(2026-06-21 实际案例)
      
      3 个 5minbtc 相关 cron job 全部失败:
      
      | Job ID | Name | 表面错误 | 实际错误链 |
      |--------|------|----------|-----------|
      | `d8058223a1e0` | 5minbtc v5.7 半K线策略 | HTTP 401 (yiyong) | deepseek 402 → minimax-cp key 未注入 → yiyong key 未注入 |
      | `3016e27ddefa` | Dreaming 夜间进化 | HTTP 401 (yiyong) | 同上 |
      | `9b07cd139f70` | 5minbtc 每日复盘 23:15 | HTTP 401 (yiyong) | 同上 |
      
      `last_error` 字段全部一致:
      ```
      RuntimeError: Error code: 401 - {'error': {'code': '', 'message': 'Invalid token
      (request id: 202606211217275441733248268d9d6elLFxYw9)'}}
      ```
      
      注意 request id 含字母 `lLFxYw9` — 这是 **yiyong provider** 的格式。DeepSeek 的 request id 是纯 25-26 位数字(无字母)。从 request id 即可反推是哪个 provider。
      
      ---
      
      ## 2. 错误链还原(从 `hermes logs errors` 看到的事实)
      
      ```
      1. provider=deepseek model=deepseek-v4-pro
         → HTTPError 402: {"error":{"message":"Insufficient Balance","type":"..."}}
         【真因:账户欠费,token 本身有效】
      
      2. Fallback attempt: chain entry deepseek/deepseek-v4-pro matches current provider/model
         → Skip(fallback 短路,无法自我 fallback)
      
      3. provider=minimax-cp model=MiniMax-M3
         → 401: login fail (1004) — request will be sent with placeholder no-key-required
         【key 未注入 environment】
      
      4. provider=yiyong model=gpt-5.4
         → 401: Invalid token (request id: 20260621...268d9d6elLFxYw9)
         【key 未注入 environment】
      
      5. 抛出 RuntimeError (用户看到的 401)
      ```
      
      ---
      
      ## 3. 诊断步骤(精确命令)
      
      ### 3.1 第一步:看完整错误瀑布
      
      ```bash
      hermes logs errors -n 30
      ```
      
      **为什么必须先看这个**:`jobs.json` 的 `last_error` 只记录**最后一跳**的异常。如果只看 last_error 看到 yiyong 的 401,会误以为是 yiyong token 失效,开始去重置 yiyong 的 key——浪费大量时间。
      
      ### 3.2 第二步:看 cron 组件日志
      
      ```bash
      hermes logs --component cron -n 50
      ```
      
      看每次 job 触发时加载的 provider / credential pool 信息。`credential pool for provider X with N entries` 这一行能告诉你哪些 provider 的 key 实际被读到了。
      
      ### 3.3 第三步:识别关键错误模式
      
      | 错误模式 | 真实根因 | 修复方向 |
      |----------|----------|----------|
      | `HTTPError 402: Insufficient Balance` | 主 provider 欠费 | 充值 或 换 provider |
      | `HTTPError 429: 使用上限` | 周/月 token 配额耗尽 | 等待重置时间 或 切备用provider |
      | `has no resolvable api_key` | fallback provider 的 env var 未注入 | 检查 `auxiliary_client` 的 `api_key_env` 映射逻辑 |
      | `placeholder no-key-required` | 同上,但发生在请求发出时 | 检查 `code path` 里 `auxiliary_client.build()` 的 key 注入分支 |
      | `Invalid token (request id 含字母)` | 哑弹 provider 的 401 | 找到主因,绕开这个 provider |
      | `Invalid token (request id 纯数字)` | 真实 token 失效/欠费 | 重置或换 |
      | `Stream stale 180s / CloudFront` | fallback provider 网络层断连 | 切到网络稳定的 provider(非 CloudFront 后端) |
      
      ### 3.4 第四步:独立验证可疑 provider
      
      ```bash
      # 验证 DEEPSEEK token 本身是否有效(不依赖 hermes)
      python3 -c "
      import urllib.request, os, json
      key = os.environ['DEEPSEEK_API_KEY']
      req = urllib.request.Request(
          'https://api.deepseek.com/v1/models',
          headers={'Authorization': f'Bearer {key}'}
      )
      try:
          r = json.loads(urllib.request.urlopen(req, timeout=15).read())
          print('OK:', [m['id'] for m in r.get('data', [])][:5])
      except urllib.error.HTTPError as e:
          print(f'HTTP {e.code}:', e.read().decode())
      "
      # 200 OK = token 有效,问题在余额
      # 401 = token 失效
      # 402 = 余额不足
      ```
      
      ---
      
      ## 4. 切换到替代 Provider(操作模板)
      
      ### 4.1 选择替代 provider
      
      当主 provider 欠费时,最快的修复是切换到一个**已验证可用**的备用 provider。
      
      **已知可用 provider 清单(2026-06-21 验证)**:
      
      | Provider | Model | 验证方法 |
      |---|---|---|
      | `zai` | `glm-5.2` | `GET https://open.bigmodel.cn/api/coding/paas/v4/models` 返回 8 个模型 |
      
      **关键约束**:必须先用上面的独立验证脚本确认 provider 可用,再切。不要假设 fallback 链里的 provider 都可用。
      
      ### 4.2 批量更新 cron job
      
      ```bash
      # 单个更新
      hermes cron update --job-id d8058223a1e0 --provider zai --model glm-5.2
      
      # 批量(for 循环)
      for jid in d8058223a1e0 3016e27ddefa 9b07cd139f70; do
        hermes cron update --job-id $jid --provider zai --model glm-5.2
      done
      
      # 验证修改
      python3 -c "
      import json
      jobs = json.load(open('/home/aa/.hermes/profiles/cqo/cron/jobs.json'))
      for j in jobs['jobs']:
          print(f\"{j['id']} {j['name']:30s} provider={j.get('model', {}).get('provider', 'N/A')} model={j.get('model', {}).get('model', 'N/A')}\")
      "
      ```
      
      注意:`hermes cron update` 的 `--model` 参数实际是 `{"provider": "zai", "model": "glm-5.2"}` 的简写(CLI 解析时拼装成 model dict)。如果直接编辑 `jobs.json`,需要写成:
      
      ```json
      "model": {"provider": "zai", "model": "glm-5.2"}
      ```
      
      ### 4.3 立即验证(不等 schedule)
      
      ```bash
      hermes cron run --job-id d8058223a1e0
      # 5minbtc 全链路约 30-50s(含引擎+新闻+3组搜索+LLM)
      # 等待 ~50s 后检查
      
      python3 -c "
      import json, time
      time.sleep(50)
      jobs = json.load(open('/home/aa/.hermes/profiles/cqo/cron/jobs.json'))
      for j in jobs['jobs']:
          if j['id'] == 'd8058223a1e0':
              print(f'last_status={j[\"last_status\"]}')
              print(f'last_run_at={j[\"last_run_at\"]}')
              if j.get('last_error'): print(f'last_error: {j[\"last_error\"][:200]}')
      "
      # 期望输出: last_status=ok
      ```
      
      如果 `last_status=ok`,说明切换成功。如果仍 `error`,看新的 `last_error`(可能是不同的 provider 失败)。
      
      ---
      
      ## 5. 后续修复(可选,本次未做)
      
      ### 5.1 fallback 链重排
      
      当前 `config.yaml` 的 `fallback_providers` 顺序:
      ```
      1. deepseek-v4-pro (欠费,失效)
      2. minimax-cp (key 解析失败)
      3. yiyong (key 解析失败)
      ```
      
      **建议改成**:
      ```
      1. zai/glm-5.2 (已验证可用)
      2. deepseek-v4-pro (欠费,备用)
      3. minimax-cp / yiyong (key 解析问题未根治,置后)
      ```
      
      但**不要**仅依赖 fallback 链 — 每个关键 cron job 都应该显式 pin 一个确认可用的 provider/model,避免 fallback 链故障时全盘崩溃。
      
      ### 5.2 排查 key 解析 bug
      
      `MINIMAX_CP_API_KEY` 和 `YIYONG_API_KEY` 在 `.env` 里都非空(len=125 / 51),但 `agent.auxiliary_client` 报 `has no resolvable api_key`。**根因**:`config.yaml` 里 `providers.minimax-cp.api_key_env` 可能拼写错误或未定义。检查路径:`/home/aa/.hermes/profiles/cqo/config.yaml` → `providers:` 节。
      
      ### 5.3 充值 DeepSeek 账户
      
      DeepSeek token 有效但欠费(HTTP 402)。充值后可以把 fallback 链恢复成 DeepSeek-first(成本最低)。
      
      ---
      
      ## 6. 经验教训
      
      1. **`last_error` 是最后一跳的异常,不是第一跳的根因** — 看到 401 不一定是 token 失效
      2. **fallback 链的每个 provider 都必须独立验证可用** — 不能假设 chain 里某个 provider 工作
      3. **provider 账户余额是常见的隐式失败** — 没有预警,只在请求时返回 402
      4. **request id 的格式能反推 provider** — 数字 = DeepSeek,字母混合 = yiyong/anthropic 等
      5. **每个 cron job 显式 pin provider/model** — 不要依赖全局 fallback 链的默认行为
      6. **收到 cron 失败告警,先 `hermes logs errors -n 30` 再行动** — 不要急于重置 token 或重启服务
      7. **月末是 provider 限额耗尽的高危窗口** — 每月25日后主动检查余额/额度,不等 cron 静默才发现
      
      ---
      
      ## 6b. 第三种失效模式:HTTP 429 周/月使用上限 (2026-06-27)
      
      **与 402 (欠费) 和 401 (token失效) 的区别**:
      
      | 错误码 | 含义 | 修复方式 | 恢复时间 |
      |--------|------|----------|----------|
      | 401 | Token失效/未注入 | 重置token或修复env var | 即时 |
      | 402 | 账户欠费 | 充值 | 充值后即时 |
      | **429** | **周/月使用上限** | **等待重置或换provider** | **有明确重置时间,不可加速** |
      
      ### 6b.1 故障现象
      
      zai provider (glm-5.2) 返回:
      ```
      HTTP 429: 您已达到每周/每月使用上限,您的限额将在 2026-06-28 11:41:02 重置。
      ```
      
      **关键特征**:错误信息中**直接给出了重置时间**。这意味着不需要任何操作——到时间自动恢复。但如果正好赶上交易日,会丢失一整天的预测数据。
      
      ### 6b.2 诊断
      
      ```bash
      # 在 errors.log 中搜索 429
      hermes logs errors -n 30 | grep "429"
      
      # 或直接搜中文关键词
      hermes logs errors -n 30 | grep "使用上限"
      ```
      
      ### 6b.3 应对策略
      
      | 策略 | 何时用 | 操作 |
      |------|--------|------|
      | **等待重置** | 重置时间在数小时内 | 不做任何操作,到时间自动恢复 |
      | **切备用provider** | 重置时间>12小时 或 当日有交易窗口 | `hermes cron update --job-id <JID> --provider <ALT> --model <M>` |
      
      **备用provider选择**(当前2026-06-27验证状态):
      - `zai/glm-5.2` — ⚠️ 限额耗尽,06-28 11:41重置
      - `deepseek/deepseek-v4-pro` — ⚠️ CloudFront断连(stream stale 180s)
      - 需要提前验证备用provider可用,见第4节「已知可用 provider 清单」
      
      ### 6b.4 预防
      
      1. **每月25日主动检查**:在cron静默之前发现限额问题
      2. **追踪月消耗趋势**:如果本月25日耗尽、上月28日耗尽 → 下月预计24日耗尽
      3. **备用provider预验证**:不要等主provider耗尽才去验证备用——提前一个月确认备用可用
      
      ---
      
      ## 7. 诊断命令速查
      
      ```bash
      # 看瀑布错误
      hermes logs errors -n 30
      
      # 看 cron 组件日志(含 provider 加载记录)
      hermes logs --component cron -n 50
      
      # 看所有 cron job 状态
      python3 -c "import json; [print(j['id'], j['name'], j.get('last_status','?'), j.get('last_error','')[:100]) for j in json.load(open('/home/aa/.hermes/profiles/cqo/cron/jobs.json'))['jobs']]"
      
      # 看 .env 里某个 key 是否非空
      grep -c "ZAI_API_KEY=..*" /home/aa/.hermes/profiles/cqo/.env
      
      # 验证 DeepSeek token 本身是否有效
      python3 -c "import urllib.request,json,os; req=urllib.request.Request('https://api.deepseek.com/v1/models',headers={'Authorization':f'Bearer {os.environ[\"DEEPSEEK_API_KEY\"]}'}); print(json.loads(urllib.request.urlopen(req,timeout=15).read()))"
      
      # 验证 zai/glm-5.2 可用
      python3 -c "
      import urllib.request, json
      key = open('/home/aa/.hermes/profiles/cqo/.env').read().split('ZAI_API_KEY=')[1].split(chr(10))[0]
      req = urllib.request.Request('https://open.bigmodel.cn/api/coding/paas/v4/models', headers={'Authorization': f'Bearer {key}'})
      print([m['id'] for m in json.loads(urllib.request.urlopen(req, timeout=15).read())['data']])
      "
      
      # 单个 cron job 切 provider
      hermes cron update --job-id <JID> --provider zai --model glm-5.2
      
      # 触发单个 job 立即跑(不等 schedule)
      hermes cron run --job-id <JID>
      ```
      
      ---
      
      ## 8. 涉及到的 Job 列表
      
      | Job ID | Name | 调度 (cron 表达式) | 当前 model | 状态 |
      |--------|------|-------------------|------------|------|
      | `d8058223a1e0` | 5minbtc v5.7 半K线策略 | `2,7,12,17,22,27,32,37,42,47,52,57 20-22 * * *` | `zai/glm-5.2` | ✅ ok @ 2026-06-21 20:21 |
      | `3016e27ddefa` | Dreaming 夜间进化 | 每日 04:25 | `zai/glm-5.2` | 待首次验证 |
      | `9b07cd139f70` | 5minbtc 每日复盘 23:15 | `15 23 * * *` | `zai/glm-5.2` | 待首次验证 |
      
      > 这些是 **hermes 机**(Linux)上的 cron job,不在 Mac 上(Mac 用 launchd,见 [setup-from-scratch.md](setup-from-scratch.md#4-launchd-常驻服务6-个))。
      > ⚠️ job name 里的 `v5.7` 已是旧标签 —— 正是"引擎升级后必须同步更新 job name"这条规则的反面案例(见 [pitfalls.md](pitfalls.md) #7)。
      >
      > 📌 `3016e27ddefa`(Dreaming 夜间进化)曾于 2026-06 连续失败(最后成功 06-11、最后失败 06-13),
      > 当时的恢复流程记在 `references/dreaming-cron-recovery.md`,该文件已于 2026-09-10 按最小无用原则删除
      > (属 6 月单次事故记录,其数据源 fallback 建议从未落地)—— 需要时从 git 历史取回。
      
    • execution.md 2 KB
      # 5minbtc 执行步骤 (LLM 工作流)
      
      ## 铁律
      1. 每次必须重新执行引擎脚本 — 不缓存
      2. 每次必须重新搜索3组新闻
      3. 先 settle 上一根, 再 log 新预测
      4. LLM 可微调引擎的 bias/pred_close/range, 但必须说明理由
      5. 输出 15-25 行 (平衡深度和 Telegram 可读性)
      
      ## 执行步骤
      
      ### Step 1: 并行启动 (5个调用同时发出)
      ```
      并行组:
      ├── exec: settle-all + 引擎脚本 + 新闻扫描 (合并一条命令)
      ├── web_search: "Bitcoin BTC breaking news price" count=3 freshness=day
      ├── web_search: "crypto market macro stocks today" count=3 freshness=day
      └── web_search: "比特币 BTC 最新 晚间" count=3 freshness=day
      ```
      
      ### 引擎命令 (绝对路径)
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc && \
        python3 $SKILL_DIR/5minbtc-log.py settle-all 2>&1; \
        echo "---ENGINE---"; \
        python3 $SKILL_DIR/5minbtc-engine-v6.0.py 2>&1; \
        echo "---NEWS---"; \
        python3 $SKILL_DIR/5minbtc-news.py 2>&1
      ```
      
      ### Step 2: LLM 完整分析
      详见: [output-template.md](output-template.md)
      
      ### Step 3: 记录日志
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc && \
        python3 $SKILL_DIR/5minbtc-log.py log \
          "<engine.candle.iso>" \
          <final_pred_close> \
          <final_pred_high> \
          <final_pred_low> \
          <confidence> \
          <final_bias> \
          <news_sentiment> \
          <engine.indicators.vol_pct>
      ```
      
      ## 超出设计时间窗口的处理
      本 skill 设计预测**单根 5 分钟 K线**。当用户问更宽的时间窗口 (如"20:00-24:00 up or down"), 不能直接套用单根 5min 预测。
      - 仍按标准流程跑当前 5min K线完整预测
      - 基于当前快照的宏观读数给宽窗口**方向倾向**, 标注"超出引擎置信区间"
      - 建议在该窗口起始时间 (如 20:00) 再次触发 5minbtc
      
      ## 搜索优化
      3 组通用搜索常返回首页/目录页。如果初始结果不具体, 追加 1-2 组上下文定制搜索 (用当前价格方向 + 关键事件)。LLM 分析前发起, 延迟 <30s 但信息价值显著。
      
    • high-latency-network-handling.md 3.3 KB
      # High-Latency Network Handling for 5minbtc
      
      > 2026-06-24 session findings | ping ~260ms to 8.8.8.8
      
      ## Problem
      
      When network latency exceeds ~250ms (ping 8.8.8.8), three failures occur simultaneously:
      
      1. **Binance API SSL timeout**: Both `api.binance.us` and `data-api.binance.vision` return HTTP 000
         - Root cause: Python `urlopen(timeout=10)` insufficient for TLS handshake at >250ms
         - Curl with `--connect-timeout 20` works fine → proves it's a timeout, not endpoint outage
         
      2. **Web search unavailable**: DuckDuckGo/Startpage backend times out
         - All `web_search` calls fail regardless of query
         
      3. **Engine pred_close distortion at extreme progress**: If engine runs late (>80% progress), the half_range×ATR formula overestimates remaining movement
      
      ## Diagnostic Flow
      
      ```bash
      # 1. Check network latency
      ping -c 2 -W 3 8.8.8.8
      
      # 2. Test Binance with extended timeout
      for ep in "api.binance.us" "data-api.binance.vision"; do
        code=$(curl -s --connect-timeout 20 --max-time 30 -o /dev/null -w "%{http_code}" \
          "https://$ep/api/v3/klines?symbol=BTCUSDT&interval=5m&limit=1")
        echo "$ep → HTTP $code"
      done
      
      # 3. If curl works (HTTP 200) but Python fails → timeout patching needed
      #    If curl also fails → true network outage, skip this round
      ```
      
      ## Fix: Engine Timeout Patching
      
      When curl with extended timeout works but Python `urlopen(timeout=10)` fails:
      
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc
      cd "$SKILL_DIR"
      
      # Backup
      cp 5minbtc-engine-v6.0.py 5minbtc-engine-v6.0.py.bak-net
      
      # Increase timeouts: klines 10→25s, others 5→15s
      sed -i 's|timeout=10|timeout=25|g' 5minbtc-engine-v6.0.py
      sed -i 's|timeout=5|timeout=15|g' 5minbtc-engine-v6.0.py
      
      # Run engine
      python3 5minbtc-engine-v6.0.py
      
      # ALWAYS restore
      cp 5minbtc-engine-v6.0.py.bak-net 5minbtc-engine-v6.0.py
      rm 5minbtc-engine-v6.0.py.bak-net
      ```
      
      ⚠️ **Never leave the patched timeouts in place** — the engine is designed for 3s parallel execution; 25s timeouts would make it unusable in normal conditions.
      
      ## Extreme Progress Edge Case
      
      Session ran at 20:14 for a 20:10→20:15 candle (progress=94%, 18s remaining).
      Engine pred_close=$62,745 vs current=$62,900 (gap: -$155).
      
      The half_range×ATR formula assumes ~2.5min remaining. At 18s remaining,
      the realistic price movement is at most ~$10-20, not $155.
      
      **LLM adjustment formula**:
      ```
      pred_close = current + (engine_pred_close - current) × (remaining_sec / 150)
      ```
      
      Where 150 = approximate seconds at 50% progress (design target).
      At 94% progress with 18s remaining: factor = 18/150 = 0.12
      → pred_close = 62900 + (62745 - 62900) × 0.12 = $62,881
      
      This is a common-sense adjustment — the LLM should note it explicitly in output.
      
      ## Web Search Fallback
      
      When web_search fails due to network:
      - Don't retry more than 2 rounds total
      - Use `5minbtc-news.py` output (RSS-based, doesn't use search backend)
      - Note in output: "新闻搜索不可用(网络高延迟),已用引擎内置RSS扫描"
      
      ## Session Log (2026-06-24)
      
      - Time: 20:14 CST
      - Candle: 20:10→20:15, progress 94.0%, 18s remaining
      - Price: $62,900 | Open: $62,883.5 | Body: +$16
      - Engine: neutral/weak, conf=44, pred_close=$62,745
      - LLM adjustment: pred_close→$62,895 (noted extreme progress)
      - Network: ping 260ms, both endpoints SSL timeout, web search dead
      - News: 0 articles from all 3 sources
      - Outcome: logged as neutral, conf=44
      
    • lessons.md 14.7 KB
      # 5minbtc 核心教训 (25条)
      
      > 从 273 笔实盘 (2026-05-05 ~ 2026-07-02) + 365 天回测提炼的核心教训。
      > 每条标注首次出现日期 + 累计确认次数。
      
      ### 核心教训
      
      1. **🔴 纯价格因子无预测力,但半K线延续性有真实edge** — 11因子全部49-51%,但用真实1min K线构建半K线后准确率61-70%。edge来自价格动量延续,不是技术指标。回测时必须用真实1min数据,不能用模拟中点
      2. **半K线延续性是可利用的真实效应** — v5.8回测(27k轮)证实: 前2根1min=61.4%, 前4根=69.5%。越接近K线完成越准。实盘66%准确率已解释
      3. **过度自信是最致命问题** — conf≥60准确率56.8% < conf<60的65.4%,极端信号=行情末端
      4. **放量≠确认方向,放量=反转预警** — 放量(≥80%)准确率仅50%
      5. **bull偏向严重** — bull准确率57.3% < bear 63.4%,引擎过度解读EMA金叉
      6. **线性打分是初学者错误** — 多个共线指标叠加不增加信息量
      7. **🔴 缩量场景(<50% vol) = 半K线策略最强edge — 2026-06-03实盘7/7 100%准确率** — 缩量意味着没有突发因素干扰半K线延续性,half_body因子在低vol市场中被锁定。这从v5.4时代"低vol微波动准确率最高"(66%)的观察,升级为半K线策略的核心优势。v5.8回测中volume因子是唯一有独立预测力的因子(56-58%),与实盘互为印证。**这条经验来自2026-06-03实盘复盘**。
         - **⚠️ 前提条件修正 (2026-06-21)**: "缩量=强edge"仅在**有趋势/延续环境**下成立。周日紧缩纯震荡(session波幅<0.5%)中, 低量(<40%)准确率仅58.3%反成噪声——均值回归击败半K线延续性。见教训20。
      8. **EMA在底部反弹期天然滞后** — VWAP因子部分修复
      9. **momentum/decel冲突时,decel通常是正确的** — 当 |momentum_tstat|>0.7 且 |decel|>0.8 且方向相反时,长期斜率锁定错误方向,短期反转才是真相(V型反转场景)
      10. **volume因子使用未完成K线判断方向是致命缺陷** — 预测时点candle仅完成60-70%,应改为用最近3根完整K线的量价关系
      11. **bear方向在中量区(40-80%)是v5.7.1最大盲区 (2026-06-12)** — 缩量场景(<40%)和bull方向表现优异(87-100%),但bear中量区准确率仅44.4%(低于随机)。趋势市场中的区间震荡导致半K线延续性失效。**修复方向**: bear中量区需增加额外确认规则(如连续2根同向半K线才出方向)
          - **变体: 持续下跌后的弱反弹是bear方向另一盲区 (2026-06-14)**: 32轮方向65.6%,bear仅57.9%(11/19)。8次bear误判全部是"预测跌但实际涨" — 持续下跌后BTC出现弱反弹时,半K线延续性错误地将反弹视为继续下跌。21:00-22:00区间6次bear全部打脸。根因: 下跌半K线延续性过强,无法区分"延续下跌"vs"震荡筑底反弹"。**修复方向**: 连续3根以上bear完成K线+当前半K线收涨时,bear需降权或升至中立
          - **第3次确认 (2026-06-17) — 当前最大系统性失分点**: 反弹日(BTC +0.58%)bear方向准确率仅66.7%(14/21),9次miss中7次是bear。三次实盘链 06-12(bear中量44.4%) → 06-14(bear弱反弹57.9%) → 06-17(bear 66.7%) 稳定确认该盲区。共同特征: 低vol(1-6%)却收涨——缩量环境的均值回归击败了下跌半K线延续性。21:xx小时准确率仅71%(反弹启动点最弱)。**优先于所有其他修复项落地反弹确认机制**
          - **第4次确认 (2026-06-19) — 最强单日案例**: 上涨日(BTC +1.3%)方向仅52.6%(10/19),6月最低。bear方向5/11=45%,全天19轮误差全部为正(见教训19)——引擎系统性低估价格。四次实盘链 06-12 → 06-14 → 06-17 → 06-19 稳定锁定: 上涨趋势日bear必然系统性失分。**反弹确认机制已成为最高优先级未落地的引擎修复项**
          - **第5+6次确认 (2026-06-27 / 06-28) — 反弹段是教训11与教训19的合流点**: 06-27(bear 37.5%, 开盘段22.2%) 与 06-28(bear 73.3%整体尚可, 但22:00-22:35反弹段3连bear miss) 再次确认。**新合成洞察**: 本轮(06-28)的3大正向误差(+0.290/+0.150/+0.150)全部出自急跌后的反弹段——教训11(bear弱反弹盲区)与教训19(误差同号系统性低估)并非独立教训,而是**同一失效模式的两个观测面**: 引擎在急跌后惯性押bear过久,既触发bear盲区(方向❌)又制造系统性低估(误差全正)。两教训在**post-crash反弹段合流放大**。**结论**: 反弹确认机制落地时,其触发条件应同时降低bear置信度上限(教训19的系统性低估修复)——单纯翻转方向不够,需压制bear方向的pred_close估值。六次实盘链 06-12 → 06-14 → 06-17 → 06-19 → 06-27 → 06-28 跨震荡日/上涨日/下跌后反弹日全部稳定再现
          - **第7次确认 (2026-07-01)**: 上涨日(+1.72%, session波幅2.87%)bear方向5/15全miss且均为"预测跌实际涨". 五次bear miss全部出自上涨段. 误差同号+18/-6=SYSTEMATIC_BIAS与教训19合流. 七次实盘链06-12→06-14→06-17→06-19→06-27→06-28→07-01跨震荡/上涨/反弹日稳定再现. **反弹确认机制7次确认仍待落地(最高优先级)**
      12. **imbalance/microprice 因子滞后于半K线实时动量 (v5.7.2)** — 当当前半K线实体方向与 imbalance/microprice 冲突时,已完成K线的 orderbook 因子是滞后指标,半K线实时动量是即时指标。裁决: |half_body|>0.25 且 |imbalance|>0.5 时优先 half_body。2026-06-04 实盘: 引擎判 bear(imbalance=-0.82),但半K线 bull 实体+$376 正确
      13. **周末低流动性放大误差 (2026-06-14)**: 周日(06-14)的MAE=0.045%与平日相当,但22:35出现了-0.22%的极端误差(pred=$64,020 actual=$63,880)。周末BTC波动率降低但偶发性价差放大,建议周末自行降低ATR乘数或减少预测频率
      14. **🔴 fatigue≥0.8 = 均值回归预警 (2026-06-17)**: 22:40 candle — 全因子bear共振(momentum=-0.85/imbalance=-0.73/MACD hist=-26.6/vol=3%),LLM判bear medium,但实际收涨(+$87)。根因: fatigue=0.8表示连续下跌后市场疲劳,缩量环境下的均值回归概率显著升高。**裁决规则**: fatigue≥0.8时,即使全因子同向,也需将置信度上限压至40,并提升反向情景概率至少10pp。fatigue=0.8是本轮唯一反向信号,却被忽略,导致方向误判。**与06-14 bear弱反弹盲区(教训11)的关联**: 两者都是\"连续下跌后的反弹\"模式——06-14是半K线延续性过强无法区分筑底,06-17是fatigue信号被全因子压制。共同修复方向: 连续下跌≥3根K线后,需增加反弹确认机制
      15. **🔴 vol_pct>200% 视为无效数据点,必须过滤 (2026-06-17)**: 实盘出现 vol_pct=595%(20:30)/1322%(21:00) 异常值——根因是未完成K线的成交量除以接近0的历史均量导致爆炸性比值。21:00那次(vol=1322%)触发1次bull方向误判(err+0.06%)。**规则**: vol_pct>200% 时判定为数据质量缺陷而非真实放量,降级处理(沿用上一根有效vol或转neutral)。与教训4(真实放量=反转预警)不同——这是0除近似导致的glitch,不是市场行为。
      16. **🔴 Session尾部(23:00+)动量延续性断裂 (2026-06-17)**: 23:10两轮bear全部miss(+0.38%/+0.27%),是当日最大单点误差。收盘前流动性衰减,半K线延续性失效。**修复方向**: session最后30min降低ATR乘数(如×0.4)或直接减少预测频率。与教训13(周末低流动性)同源——都是流动性下降导致延续性信号失效。复盘统计时建议把23:00后轮次单独拆分评估,避免污染当日MAE
      17. **🔴 decel 极值崩塌 = V反加速阶段结束 (2026-06-17)**: 23:10 candle — decel从+1.0崩塌至+0.011(22:55=+1.0 → 23:00=+1.0 → 23:10=+0.011),同时MACD hist从-32急速修复(-32→-5.7)、RSI从43跳升至55。decel=+1.0 表示"价格正在减速下跌",当这个极值瞬间崩塌(而非渐进下降),代表减速已完成——V反的加速反弹阶段已经到来。此时 engine 仍在消化 bear momentum 滞后,正确做法是: **decel从+1.0崩塌+MACD hist急速修复 = 趋势逆转信号,LLM应覆盖引擎bear方向为bull**。与教训9(R1冲突降权)的区别: R1是mom/decel冲突→decel优先(捕捉即将发生的反转),现在是 decel 已完成使命→反转已在实际发生。两者互补: R1捕即将反转,本规则捕已发生反转
      18. **🔴 TREND强趋势中decel极值≠反转信号 — 需要half_body确认 (2026-06-18)**: 00:10 candle — EMA delta=+$188(强上升趋势),decel=-0.801(极值减速),half_body=+0.009(十字星),engine判neutral/weak(score=0)。LLM基于decel极值+position=-0.868覆盖为bear→实盘收涨$65,848 ❌。根因: 在强TREND中(decel绝对值虽然大),价格减速是正常的趋势呼吸,不等于反转。十字星(half_body≈0)说明空方无力推动实际下跌。**裁决规则**: EMA delta>$100的TREND regime中,decel极端值(|decel|>0.7)必须配合half_body同向(|half_body|>0.15且方向一致)才可触发方向翻转。仅有decel+position的组合在强趋势中是不可靠的反转信号。与教训9(R1降权)的区别: R1是mom/decel冲突→decel优先,但本规则限制了decel优先的适用范围——强TREND中decel需要half_body佐证,否则维持引擎方向
      19. **🔴 全天误差同号 = 系统性方向偏差信号 (2026-06-19)**: 复盘06-19发现19轮误差**全部为正**(+0.00%~+0.28%)——连bull方向的2次miss,实际收盘也都高于预测。这说明引擎在趋势日系统性低估价格(整体偏bear),不是个别预测失误而是整个方向系统偏差。这比"bear准确率低"是更强的诊断信号。**复盘必查**: stats输出的"Max error: +X% / +Y%"两个值同号时(全正或全负),第一反应应是"引擎在对抗趋势"。**可作meta特征**: 连续N轮(N≥5)误差同号 → 引擎正在逆势,应整体上调趋势方向权重(是对抗bear弱反弹盲区教训11的新思路)。06-19 BTC单边上涨+1.3%,engine 11轮bear中6次miss,是bear趋势盲区最强单日案例。**第3次确认 (2026-07-01)**: 上涨日(+1.72%)误差+18/-6=SYSTEMATIC_BIAS, 三大正向误差(+0.42/+0.32/+0.20)集中冲高段, 引擎系统性低估价格
      20. **🔴 低量edge在紧缩周末震荡中反转 (2026-06-21)**: 周日session波幅仅0.37%($235)纯震荡, 低量(<40%)准确率仅58.3%(7/12), 5次miss中4次vol<40%——历史教训7「缩量=最强edge」在此环境失效。根因: 紧缩纯震荡+低量 → 均值回归击败半K线延续性。**修正**: 教训7的前提是「有趋势/延续环境」; 当session波幅<0.5%且vol持续<40%时, 缩量反而是噪声。需新增「紧缩震荡识别」: session波幅<0.5%时降方向置信度倾向neutral。与教训13(周末低流动性放大误差)、教训16(session尾部)同源——都是流动性下降导致延续性信号失效
      21. **🔴 上涨日开盘段是bear方向系统性死亡区 (2026-06-27)**: 今日20:00-20:40开盘段方向仅2/9=22.2%, 其中bear 0/6=0%。上涨日开盘前40分钟引擎默认空方倾向被反复打脸。与教训11(bear弱反弹盲区)不同——教训11是"持续下跌后的反弹,"而这是"上涨日开盘即涨"——引擎尚未确认当日方向便预设bear。**规则**: 开盘前30分钟若价格未跌破开盘价且微幅走高(bull倾向),bear方向应至少降至neutral。这比教训11更根本——不是反弹确认机制可以解决的,而是开盘段的方向先验问题。**第2次确认 (2026-07-01)**: 上涨日(+1.72%)开盘20:00-20:20 bear 0/2, 引擎未确认方向便预设bear
      22. **🔴 成交量信号是 regime-dependent — 高量含义取决于session类型 (2026-07-02)**: 三大成交量教训(4/7/20)看似矛盾,实统一于"session regime":
          - **强趋势session(波幅>1.5%单边)**: 高量(>80%)反而确认方向 — 2026-07-02 high vol 11/11=100%; 低量(<40%)是延续性edge(教训7)
          - **紧缩震荡session(波幅<0.5%)**: 高量=反转预警(教训4); 低量=噪声(教训20, 低vol仅40-58%)
          - **判定规则**: 判断高量含义前先看session波幅。session波幅>1.5%且单边时,高量可上调方向conf(=确认而非反转); session波幅<0.5%紧缩震荡时,高量仍按反转预警处理。**不是推翻教训4,而是给教训4加"震荡市"限定**——强趋势中放量=资金涌入推动延续,震荡中放量=多空分歧=反转
      
      ---
      
      ## 2026-07-05 整理后新增教训 (会话内验证)
      
      23. **🔴 并行工具调用的耗时分析 = max() 而非 sum() (2026-07-05)** — 5minbtc 全链路分析时, 误将 3 路 web_search (3-5s) 当作"可节省 5-10s", 实际它们与 engine 是同一 multi-tool-use 并行组, wall-clock = max(individual)。Daniel 指出后修正为"砍掉 0-2s"且需重新评估价值而非仅看速度。**规则**: 分析 multi-tool-use 并行执行时, 永远是 max() 不是 sum(), 只在能替换为串行时才有完整节省。
      
      24. **🔴 速度优化 vs 数据源质量, 优先质量 (2026-07-05)** — 5minbtc 架构审查时, 我提出"砍 web_search 节省 5-10s"作为 P0 优化, Daniel 正确指出"web_search 偶尔抓 SEC/ETF 突发的价值 >> 节省的 2s"。**规则**: 优化前先评估数据源价值, 不要为了延迟数字而砍掉有概率捕获关键信号的源。优化对象应是"低信噪比 + 无 fallback 价值"的死源 (如 0 文章的 RSS)。
      
      25. **🔴 SKILL.md 应保持 INDEX 风格 (2026-07-05)** — 5minbtc SKILL.md 从 887 行 (7 个 changelog + 22 pitfalls + 22 lessons + 复盘流程全塞一起), Daniel 两次反馈"SKILL 太长", 应主动重构为 INDEX 模式: SKILL.md (150-200 行) + references/ (按主题分文件)。**规则**: SKILL.md 包含 触发 + 何时用 + quick-start + 铁律 + 关键规则 (5-7 条) + 性能快照 + references 索引。详细 changelog/lessons/pitfalls/执行步骤一律入 references/。详见 references/skill-organization.md。
      
      ### 2026-06-03 复盘新增经验
      
      **v5.7.1首日实盘(6月3日)**: 24轮已结算,方向准确率79.2%(19/24),区间54.2%(13/24),MAE 0.106%。详见 `review-2026-06-03.md`。
      
      **5个方向误判的根因模式**:
      | 时间 | 引擎方向 | 实际方向 | 误差 | vol | 根因模式 |
      |------|---------|---------|-----|-----|---------|
      | 20:04 | bear | +0.30% | ❌❌ | 163% | 巨量开盘+冲突降权未完全覆盖 |
      | 20:37 | bear | +0.21% | ❌❌ | 83% | 开仓延迟: 20:36 bull→20:37 bear转多 |
      | 20:54 | bear | +0.16% | ❌❌ | 34% | 纯噪声区间: 缩量反向锁死 |
      | 20:57 | bull | -0.06% | ❌❌ | 37% | 量价背离: 缩量bear bull锁死 |
      | 21:06 | bull | -0.08% | ❌❌ | 81% | Pattern A: momentum 15-candle OLS滞后 |
      | 21:28 | bull | -0.01% | ❌❌ | 156% | 缩量变放量+开仓末期反转 |
      
      **缩量场景(<50% vol)表现**: 7/7方向100%准确率 — 半K线策略的核心优势区域。
      **FNG=11极端恐惧下表现**: 黑天鹅防护正常激活,v_reversal×0.5+decel×0.7,但ATR正常+news=NORMAL未触发熔断 — 设计合理。
      
      
    • monitoring-claude-code.md 4.9 KB
      # 5minbtc × Claude Code 监控集成
      
      > 把 5minbtc 从「cron 单次预测」升级为「Claude Code 会话内实时监控」。
      > 核心: `scripts/5minbtc-monitor.py` 流式输出事件, 配合 Claude Code 的
      > **Monitor 工具**把每行事件转为实时通知, 直到命中明确信号或用户喊停。
      
      ## 一、为什么需要这个
      
      原版 5minbtc 是 cron 驱动的: 每根 K 线第 4 分钟跑一次引擎 → LLM 裁决 → 写日志。
      在 Claude Code 会话里, 用户想要的是**盯着盘, 直到出现可交易信号**:
      
      | 需求 | cron 原版 | Claude Code 监控版 |
      |------|-----------|-------------------|
      | 持续盯盘 | 每 5min 硬跑, 无信号门 | 事件流, 方向翻转即通知 |
      | 明确信号 | LLM 事后看 | 脚本内置判定, 命中即停 |
      | 用户控制 | 手动 cron | 用户一句话 start/stop |
      | 采样时机 | 固定第 4 分钟 | 第 2/3 分钟 (更早捕捉) |
      
      ## 二、脚本设计 (scripts/5minbtc-monitor.py)
      
      **契约**: 一行一条事件到 stdout; 由 Monitor 工具逐行转发。
      
      | 事件 | 含义 | 处理 |
      |------|------|------|
      | `START` | 首次采样基线 bias | 记录 |
      | `DIR-CHANGE` | bias 在 bull/neutral/bear 间翻转 | 即时汇总 |
      | `CLEAR-SIGNAL` | 达到明确信号门槛 | **退出** (exit 0) |
      | `ENGINE-ERR` | 引擎调用失败 | 打印后继续, 不崩溃 |
      | `MAX-RUNS` / `TIMEOUT` | 达到采样/时长上限 | 退出 (exit 2) |
      
      ### 明确信号判定 (默认)
      ```
      bias != neutral  AND  strength ∈ {medium, moderate, strong}  AND  confidence ≥ 50
      ```
      > ⚠️ 实测引擎输出的 strength 是 `weak` / `medium` (SKILL.md 文档写 `moderate`) —
      > 判定集合必须同时包含 `medium` 和 `moderate`, 否则 CLEAR-SIGNAL 永不触发。
      
      ### 采样时机
      每根 5min K 线的**第 2、3 分钟** (progress ~40-70%): half_body 已激活, 又留有足够剩余时间。
      
      ### 关键参数
      ```
      --max-runs N    采样次数上限 (默认 20; 0 = 无限, 由用户/会话结束)
      --min-conf N    明确信号置信度门槛 (默认 50)
      --hours H       时长上限 (默认无)
      --settle        每次采样前先 settle 上一根 (配合 --max-runs 2 的短监控用)
      --dry-run       单次采样打印当前状态后退出 (验证用)
      ```
      
      ## 三、Claude Code 用法
      
      ### 启动 (后台持续监控)
      ```bash
      # 无限持续, 直到用户喊停 (推荐用于会话内盯盘)
      Monitor(command: "python3 <SKILL>/scripts/5minbtc-monitor.py --max-runs 0",
              persistent: true)
      
      # 有限次数 (默认 20 次, 约 100 分钟)
      Monitor(command: "python3 <SKILL>/scripts/5minbtc-monitor.py")
      
      # 更保守: 需 55 置信度才叫明确信号
      Monitor(command: "python3 <SKILL>/scripts/5minbtc-monitor.py --min-conf 55")
      ```
      
      ### 事件响应协议 (Claude 侧)
      1. `DIR-CHANGE` → 简短汇总: 新 bias、conf、现价、关键因子变化, **说明方向翻转含义**。
      2. `CLEAR-SIGNAL` → 立即拉完整引擎快照确认, 输出: bias/strength/conf、现价、关键多空因子、pred 区间、**反向风险** (如 meanrev 托底)。
      3. `ENGINE-ERR` 连续 ≥3 次 → 停监控, 诊断网络/端点 (见 binance-endpoint-flapping.md)。
      4. 用户「stop / 结束」→ `TaskStop` 停 Monitor。
      
      ### 停止
      ```bash
      TaskStop(task_id: <monitor-id>)
      # 或用户直接说「停」/「结束」即触发
      ```
      
      ### 一次「单次判断」 (非持续)
      ```bash
      python3 <SKILL>/5minbtc-engine-v6.0.py     # 拉一次预测
      python3 <SKILL>/scripts/5minbtc-monitor.py --dry-run   # 等价, 带事件格式
      ```
      
      ### 引擎测试
      ```bash
      cd <SKILL>
      python3 scripts/test_engine.py          # 自带 runner, 无需 pytest (慢测加 --slow)
      # 装了 pytest 也可以: python3 -m pytest scripts/test_engine.py -v
      # 13 项: 版本/契约/ofi块/taker_buy 数值与权重/深度采样合并截断/稳定性/监控兼容
      ```
      
      ## 四、铁律 (从原 SKILL.md 继承)
      
      1. **不缓存**: 每次采样都重新执行引擎脚本。
      2. 每次采样后若 bias 或 conf 有实质变化, 视为新信息, 向用户重新汇总。
      3. `CLEAR-SIGNAL` 后必须拉一次完整快照二次确认 (监控事件只是触发, 快照才是证据)。
      4. **非投资建议**: 明确信号只是引擎统计输出, 不代表可下注; 输出必须带置信度和反向风险。
      5. 兜底: 无限模式 (`--max-runs 0`) 依赖用户喊停或会话结束, 脚本本身不设时限。
      
      ## 五、与 cron 原版的取舍
      
      - **用监控版**: 会话内盯盘、等信号、需要方向翻转即时感知。
      - **用 cron 原版**: 无人值守、每 5min 固定记录预测 + 新闻 + settle 的完整流水线。
      - 两者共享同一引擎, 不冲突; 可同时跑 (cron 写日志, 会话内监控看信号)。
      
      ## 六、已知边界
      
      - 引擎 ~3s/次 (4 路并行 HTTP), 采样错峰不会重叠。
      - 每根 K 线采 2 次 → 20 次 ≈ 7-8 根 K 线 ≈ ~40 分钟 (`--max-runs` 按采样次数计, 不是 K 线数)。
      - 若引擎端点故障 (Binance 双向切换后仍失败), 监控进入 ENGINE-ERR 循环, 需人工诊断。
      
    • news-sources.md 812 B
      # 5minbtc 新闻数据源 (2026-07-05 清理后)
      
      ## 当前在用 (1个)
      | 源 | 类型 | 延迟 | 状态 |
      |----|------|------|------|
      | CoinDesk RSS | RSS | ~14min | 唯一稳定源 |
      
      ## 已移除源 (验证失效, 2026-07-05 清理)
      - Cointelegraph RSS - 144min+ 延迟 / TG 脚本不存在
      - TreeNews TG - 脚本不存在 (`scripts/telegram-treenews.py` missing)
      - TheBlock RSS - SSL 封锁
      - BitcoinMagazine RSS - 连接重置
      - NewsData.io API - 无 API key
      - CryptoCompare API - 无 API key
      - Binance Blog RSS - 未在 scan_all 调用 (与 Binance 平台新闻重叠)
      - alternative.me FGI - 引擎 `_fetch_fng()` 已独立获取, 引擎自带
      
      ## 评估中 (待调研)
      - CryptoPanic API (免费层) - 待实测延迟
      - The Block RSS 新端点
      - 中文: PANews, Foresight News, 巴比特, 吴说区块链
      
    • output-template.md 12.2 KB
      # 5minbtc 输出模板
      
      > ⚠️ **本文件是 2026-09-10 补写的。** 此前 `SKILL.md` 与 `execution.md` 都引用它("LLM 输出模板见 output-template.md"),
      > 但**这个文件从未存在过** —— 也就是说"LLM 该输出成什么样"从来没有书面规范,只有代码里的推送格式。
      > 本文件把**实际在跑的格式**(从代码逐条抄出)与**新拟的 LLM 输出模板**分开标注,不要把两者混为一谈。
      
      ## 0. 两个流、三个输出面(不要混)
      
      ### 命名约定(2026-09-11 起)
      
      推送文案与报表标题带前缀标签,一眼区分两条流:
      
      | 标签 | 是什么 | 程序 | 记录 | 报表 | 频率 |
      |------|--------|------|------|------|------|
      | **🌤 全天哨兵** | 全天候**只记录不下注**的预测 | `5minbtc_watch.py` | `logs/5minbtc-log.jsonl` | `scripts/5minbtc_day_stats.py` | 288/天 |
      | **🎯 夜盘狙击** | 仅 20–23 点、过 3 道闸才**真下注** | `5minbtc_realtime.py` | `~/bb-auto/5minbtc-paper.json` | `~/bb-auto/5minbtc_paper_stats.py` | 0–16/天 |
      
      **两者是独立程序、独立记录、独立报表** —— 所以它们的胜率**不可直接比较**:
      哨兵在 K 线第 2 分钟采样(progress≈42%,趋势已显现,方向更易对);狙击还要过价格闸,方向对了买贵照样输。
      实测口径:哨兵方向命中 60–66%,狙击胜率 39–42% —— **这个落差本身就是项目要解决的问题**。
      
      ### 三个输出面
      
      | 输出面 | 谁生成 | 能否手改 |
      |--------|--------|---------|
      | **系统推送** | `realtime` / `watch` / `trader` 的 f-string | ❌ 改代码才变;本文只做记录 |
      | **LLM 综合报告** | 你 / LLM 按 §5 模板写 | ✅ 这是本文件存在的意义 |
      | **日志写入** | `5minbtc-log.py` 追加 jsonl | 固定 8 个参数,见 `execution.md` |
      
      ---
      
      ## 1. realtime 推送(5 秒刷新,系统自动)
      
      > 代码:`scripts/5minbtc_realtime.py` · 全部硬编码 f-string,**没有配置文件**
      
      ### 1.1 每根 K 线的预测快照 ← 最常收到的那条
      
      `fmt_prediction()` L327:
      
      ```
      🌤 全天哨兵 · 预测 {方向中文} | {K线起始} p{进度}%
      概率 {该腿概率} | 市场 {UP|DOWN} {ask} | edge {±差值}
      → {动作}
      [上一轮 {时间} {方向} → {收阳|收阴} ✅|❌]
      ```
      
      字段语义(这几条不看代码猜不出来):
      
      | 字段 | 含义 |
      |------|------|
      | `p4%` | **K 线进度**百分比,**不是概率** |
      | `概率 0.59` | **该腿**的概率:UP 腿 = `P(close>open)`;DOWN 腿 = `1 − P` |
      | `edge -0.07` | `该腿概率 − 该腿 ask`(负 = 无套利空间) |
      | `→ …` | 四种动作之一(下表) |
      | `上一轮 …` | 上一根 K 线的方向 vs 实际收盘,仅在有跨轮状态时出现 |
      
      `→` 后的动作与判定顺序(L457-464):
      
      | 动作 | 条件 |
      |------|------|
      | `非活跃时段` | 当前小时 ∉ `--active-hours`(默认 `20,21,22,23`) |
      | `市场价不可用` | 取不到该腿 ask |
      | `🎯 下单` | `edge ≥ --min-edge`(默认 `0.05`) |
      | `⏭️ 跳过(无edge)` | 其余 |
      
      #### ⚠️ 快照与下单可能是**同根 K 线的两次不同运行**,方向会打架
      
      主循环结构(`5minbtc_realtime.py`):
      
      ```
      while True:
          d = run_engine()                      # ← 每 5 秒重跑一次引擎
          if candle != last_candle:             # ← 仅本根K线的【第一次】循环
              push(fmt_prediction(...))         #   📊 快照: 每根K线只推一次 (p≈0-3%)
          if 活跃时段 and bias in (bull,bear):   # ← 【每次】循环都检查
              if not has_auto_bet(candle):
                  push(fmt_order(...))          #   🎯 下单: 可能发生在稍后的循环 (p=10%、20%…)
      ```
      
      所以同一根 K 线可能出现两条方向相反的消息 —— 快照在开局(p3%)推,下单在稍后某次循环推,
      **中间引擎又跑了很多次,`bias` 会翻**。实盘已出现过:22:05 快照「看空·收阴 → 跳过(无edge)」,
      同分钟随后下单「看多·收阳 UP @0.31」。
      
      **怎么读**:
      - `概率` **精确等于 0.50** ⇒ 几乎可以断定是 `ofi_n is None` 的兜底(当前 K 线还没成交),
        此时 `bias` 来自 **body 符号兜底**(`meta.body_fallback`)—— 即"OFI 没数据,拿 body 猜的"。
        实测占总记录 2.0%。
      - **方向与概率可以互相矛盾**:`bias=bull` 但 `概率 0.45`(<0.5)是合法的。因为 `bias` 只看
        `ofi_n` 的**符号**,而 `p` 来自校准表 `p_cal` 再叠加 flow-gap 项(`gap` 可为负)。
        实测 **9.7%**(805/8340)的记录存在这种矛盾。
        两者不是同一个量:**方向 = "OFI 往哪边流",概率 = "历史上这种流有多少次收阳"**,并列展示容易误读。
      - 想判断"到底下没下单",**看有没有 🎯 那条**,不要只看快照的 `→`。
      
      ### 1.2 其余存活格式
      
      | 触发 | 函数 | 格式 |
      |------|------|------|
      | 下单成交 | `fmt_order()` L315 | `🎯 夜盘狙击 · 下单 \| {方向}` / `{K线} \| {side} @ {ask} \| 1U` / `概率 … vs 市场 … \| edge … \| 已记录` |
      | 结算 | inline L262 | `🎯 夜盘狙击 · 结算 {时间} {side} ✅中\|❌未中` + `买入 …U @… \| 涨跌 …% \| PnL …$` + 账户块 |
      | 限价未成交 | inline L289 | `🎯 夜盘狙击 · 未成交 \| {时间} {side} 限价… 收盘未触及` + 账户块 |
      | 有效性闸 | inline L483 | `🎯 夜盘狙击 · ⛔有效性闸触发: {n}笔 OFI 方向胜率 {x}% < {阈值} → 暂停下单`(1h 节流) |
      
      ### 1.3 已删除 / 仍残留的死代码
      
      **已于 2026-09-10 删除**(按最小无用原则,均无调用点;需要时从 git 历史取回):
      
      | 函数 | 原属功能 |
      |------|---------|
      | `record_limit()` / `fmt_limit()` / `fmt_skip_knife()` | 「甜区限价挂单」—— 已确认废弃。`record_limit` 是该功能唯一的 pending 单产生方 |
      | `fmt_no_edge()` / `fmt_signal()` | 已被 v5.10 起的 inline action 文案取代 |
      
      **仍残留(未删,等你确认)**:
      
      | 位置 | 为什么是死的 |
      |------|-------------|
      | `bias == "neutral"` 分支(`🧭 预测 中性…`) | v6.0 `ofi_direction()` **二选一无中性**,该分支不可达 |
      | `watch.py` 里 `CLEAR-SIGNAL` 的 `bias != "neutral"` 判断 | 同上,该条件恒为真 |
      
      > ⚠️ **不要连带删** `realtime` 的 pending 单结算段(`status == "pending"` 的处理):
      > `record_limit` 虽已删,但 `5minbtc_keyless_paper.py` 与历史台账仍会产生/存在 pending 单。
      >
      > 教训:改推送格式时**先 grep 调用点**再改,否则会改到不存在的路径上。
      
      ### 1.4 ⚠️ 快照里的概率在 K 线开局阶段不可信
      
      每根 K 线的**第一秒** realtime 就会推一条 `p1%` 快照,但那个概率是从刚开几秒的成交里算出来的:
      
      - 主源 `ofi_n = 2*(tb/v)−1` 取**进行中** K 线,`_ofi_native()` 只挡 `v<=0`,**无最小样本量保护**
      - `vol_gate`(本该拦这个的闸门)**算了不用** —— 详见 [pitfalls.md #18](pitfalls.md)
      - 实测:某根 K 线开局 3.4 BTC(均量 39.85 的 8.5%)就能得到 `ofi_n = +0.87`
      
      **因此**:
      
      | 进度 | 怎么看待这条快照 |
      |------|----------------|
      | `< 20%` | **不要当信号**。只说明"盘口在动",概率/edge 一律标注数据不足 |
      | `20–40%` | 谨慎参考,仍以观察为主 |
      | `≥ 40%` | 可采信(日志里 99.7% 的已验证样本都在 40–60% 这个区间) |
      
      引用概率时**必须带上进度**,别把 `p1%` 的 0.70 和 `p45%` 的 0.70 当成一回事。
      
      ---
      
      ## 2. watch 事件推送(事件驱动 + 每小时心跳)
      
      > 代码:`scripts/5minbtc_watch.py` · 采样分钟 `SAMPLE_MINUTES=(2,3,4)`,秒 `+5`
      
      ### 2.1 通用事件体
      
      `fmt_event(tag, d)` L244:
      
      ```
      {emoji} [🌤 全天哨兵] {tag} {K线ISO} p{进度}%
      方向: {方向中文} ({strength}) | 置信 {confidence}
      现价 {当前价} | 预测收 {pred_close} (低{pred_low}/高{pred_high})
      regime {regime}[ | FNG {value} {label}][ | 买力 tb={±}]
      [信号源: {多周期/跨资产/OFI 一行}]
      [tag==CLEAR-SIGNAL → "⚠️ 达到明确信号门槛 — 人工复核后再考虑动作, 非投资建议"]
      [black_swan_warning → "🚨 黑天鹅警告: 方向不可靠"]
      ── 预测市场 ──
      {盘口 + 模拟持仓}
      ```
      
      - `emoji`:`bull 🟢 / neutral ⚪ / bear 🔴`
      - `信号源` 行由 `_mtf_line()` 生成(4h 斜率 / 1h ADX / 15m %B / ETH+SOL 广度 / OFI)
      - 盘口块 `market_line()`:真实价优先,取不到则标 `(模拟)`,再取不到标 `(不可用)`;后接模拟持仓
      
      ### 2.2 tag 与触发条件
      
      | tag | 触发 |
      |-----|------|
      | `START` | 首次采样(`last_bias is None`) |
      | `DIR-CHANGE` | `bias` 相对上一次变化 |
      | `CLEAR-SIGNAL` | `bias != "neutral"` 且 `strength ∈ {medium,moderate,strong}` 且 `confidence ≥ --min-conf`(默认 50) |
      | `TB-FLIP` | `taker_buy` 正负号翻转 |
      | `PREDICT` | 仅 `--every-candle` 时 |
      | `心跳` | 每 `--heartbeat` 秒(默认 3600) |
      | `引擎采样失败 xN` | 连续失败第 1/3/6/10 次各推一次 |
      
      > ⚠️ `CLEAR-SIGNAL` 的 `bias != "neutral"` 判断在 v6.0 下**恒为真**(引擎无中性)—— 与 §1.3 同源的遗留。
      > 另:生产环境 `5minbtc-watch` 用 `--mute` 启动,**事件推送整体关闭**,只保留预测记录 + 结算 + 每日战绩;
      > 事件推送实际由 realtime 承担。调试时才去掉 `--mute`。
      
      ---
      
      ## 3. trader(paper 模拟盘)
      
      > 代码:`scripts/5minbtc_trader.py --paper-monitor` · 由 `com.daniel.5minbtc-paper` 守护
      
      | 触发 | 格式 |
      |------|------|
      | 账户总览 | `🎯 夜盘狙击 · 实测记录` + 注单统计 + 胜率 + `账户: $100 → $xx.xx (+x.xx%)` + 最近 5 笔 |
      | 结算 | `🏁 模拟结算{时间} {side} @{ask} ✅\|❌` |
      | 账户变动 | `💼 模拟账户${br} → ${equity}` |
      | 成交 | `✅ 模拟成交 \| {时间} {tag} UP` |
      | 第 3 分钟预测 | `🔎 第3分钟预测(模拟) \| {时间} …` |
      | 未成交 | `❌ 未成交 \| {时间} {side}` |
      | 挂单/盯盘 | `📡 模拟{side} @{ask} → 现价 {cur}` |
      
      ---
      
      ## 4. 每日战绩
      
      > `scripts/5minbtc_day_stats.py --push`,由 watch 在跨日时自动触发
      
      ```
      🌤 全天哨兵 · 战绩 | {日期}
      ━━━━━━━━━━━━━━━━━━━━
      记录预测 N 条
      已结算 M 条 | 方向命中 x/M = P%
      收盘在预测区间 y/M = P%
      MAE z% | 方向分布 {…}
      ━━━━━━━━━━━━━━━━━━━━
      最近结算: …
      ```
      
      ---
      
      ## 5. LLM 综合报告模板
      
      > 🆕 **本模板是本次新拟的**,此前不存在书面版本。它必须与 v6.0 的现实一致:
      > **方向由 OFI 一票决定,不再由因子打分** —— 所以报告里不要再出现"因子打分/13 因子"的推理链。
      > 铁律:输出 **15–25 行**(`SKILL.md` 铁律 #5)。
      
      ```
      📊 BTC 5min | {K线时间} | p{进度}%
      
      方向: {看多/看空} ({strength}) | 概率 {P(该腿)} | 置信 {confidence}
      现价 {current} | 预测收 {pred_close} ({pred_low}–{pred_high})
      regime {regime} | FNG {value} {label}
      
      【为什么】
      · OFI: ofi_n={±x.xx} ({净流入/流出}) | flow-gap {±x.xx} | 校准概率 {p_cal} (n={cal_n})
      · 过滤: 4h {↑/↓/—} | 1h ADX {x} | 15m %B {x} | ETH/SOL 广度 {±x}
      · [反向保护] {reversed_60 / ws_conflict 时说明}
         ⚠️ 不要写"流量不足所以概率不可信"—— `vol_gate=false` 时引擎**不会**降低概率,
            它只是个装饰字段(见 §1.4)
      · 新闻: {3 组搜索结果一句话结论}
      
      【市场】
      · UP {x.xx} / DOWN {x.xx} | 我买 {UP/DOWN} @ {ask}
      · edge = {p_side} − {ask} = {±x.xx} → {下单 / 跳过 / 非活跃时段}
      
      【风险】
      · {黑天鹅警告 / ATR spike / 流量不足 / 无}
      
      ⚠️ 非投资建议, 仅量化研究
      ```
      
      填写纪律:
      
      1. **方向必须与引擎一致**。bias 是 OFI 一票决定的;只有在 `meta.body_fallback=True` 或 OFI 数据缺失时才可以改,且必须写明原因(见 `SKILL.md` 铁律 #4)。
      2. **概率要写清是哪条腿**:UP = `P(close>open)`,DOWN = `1 − P`。写错方向会让整篇报告反向。
      3. `edge` 为负就明确说"跳过",不要用"倾向"这类模糊词掩盖没有套利空间的事实。
      4. 超出单根 K 线窗口的提问("今晚涨跌")按 `execution.md` 处理:给倾向 + 标注"超出引擎置信区间"。
      5. 有 `black_swan_warning` 时,方向结论必须标注不可靠。
      
    • pitfalls.md 29.9 KB
      # 5minbtc Pitfalls (20条)
      
      > 全部 pitfalls 集中索引。每条给出: 触发条件 / 症状 / 修复规则。
      > 部分有专门 reference: 标 [→ refs/file.md]。
      
      ## 目录
      
      1. Binance API 端点选择与故障切换
      2. 路径硬编码
      3. 全局字典临时修改模式
      4. 🔴 CRITICAL: 回测证明因子无预测力,但半K线延续性有真实edge (2026-05-28更新)
      5. 🔴 CRITICAL: ThreadPoolExecutor.submit() 的 positional arg 陷阱 (v5.7.3)
      6. 🔴 Python 3.11 Unicode Strictness in Engine Source Files
      7. Cron Job 版本同步
      8. 🔴 CRITICAL: Cron Job LLM Provider 失效的诊断与切换 (2026-06-21)
      9. 复盘日志数据质量检查 (2026-06-19)
      10. 内联 python3 -c 脚本中的 emoji/状态字符
      11. 🔴 极端进度执行 (>80%) — 引擎pred_close偏离当前价
      12. 🔴 Body=0 持续模式 (冻结K线) — 引用 5minbtc-v57-optimization §7
      13. 🔴 chainlink_offset 可使引擎 pred_close 与 bias 方向矛盾 (2026-07-05)
      14. 🔴 高延迟网络 — 引擎timeout临时修补 (2026-06-24)
      15. 🔴 Web搜索不可用时的降级策略
      16. 🔴 并行工具调用延迟评估 = max() not sum() (2026-07-05)
      17. 🔴 SKILL.md 维护: 定期重构为 INDEX 风格 (2026-07-05)
      18. 🔴 CRITICAL: K 线开局阶段 OFI 概率不可信 — vol_gate 算了不用 (2026-09-10)
      19. 🔴 CRITICAL: "真OFI" 其实只覆盖单一市场 — 主源(现货) vs 辅源(合约) 口径不一致 (2026-09-10)
      20. 🔴 CRITICAL: 端点 SSL 失败导致 6 小时静默断档 — 记录与推送一起停, 日志无痕 (2026-09-11)
      
      ---
      
      ### Binance API 端点选择与故障切换
      
      
      **三层端点策略**(按优先级):
      
      | 优先级 | 端点 | 场景 | 状态 |
      |--------|------|------|------|
      | 1 | `api.binance.us` | 通用首选(低延迟网络稳定;2026-06-17间歇超时) | ⚠️ |
      | 2 | `data-api.binance.vision` | 备选(低延迟可用,高延迟SSL超时;2026-06-17反向可用) | ⚠️ |
      | 3 | `api.binance.com` | 仅供非中国大陆网络 | ❌ 中国大陆451 |
      
      **高延迟网络下的已知问题**:
      - 当 ping 8.8.8.8 >250ms 时,`data-api.binance.vision` 的 SSL 握手经常超时(Python `urlopen timeout=10` 不够)
      - `api.binance.us` 在此类网络下仍然可用
      - **双向故障切换** (2026-06-17): `api.binance.us` 也可能间歇性超时(URLError: timed out),此时 `data-api.binance.vision` 反而可用。两个端点都可能出问题,故障切换必须是双向的——哪个通就用哪个,不要假设某一端总是稳定
      - **高延迟SSL超时** (2026-06-24): 当ping>250ms时,两个端点都可能返回HTTP 000(SSL握手超时)——此时端点切换无效,需临时扩大引擎timeout(见下方pitfall「高延迟网络 — 引擎timeout临时修补」)
      - 详见 `references/binance-api-geo.md`
      
      **引擎 + 日志模块端点快速切换**(⚠️ **人工操作**,引擎没有自动故障切换:
      
      `BINANCE_KLINES` / `BINANCE_DEPTH` 是**硬编码单端点**,全代码库无 fallback 逻辑。
      所谓"双向故障切换"指的是**这两个方向的 sed 命令都备好了**,需要人来跑。
      → 2026-09-11 的 6 小时静默断档就是后果,见 #20):
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc
      
      # 方向A: 切换到 api.binance.us (当 data-api 不可用时)
      sed -i 's|BINANCE_KLINES = .*|BINANCE_KLINES = "https://api.binance.us/api/v3/klines"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      sed -i 's|BINANCE_DEPTH = .*|BINANCE_DEPTH = "https://api.binance.us/api/v3/depth"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      sed -i 's|data-api.binance.vision|api.binance.us|g' $SKILL_DIR/5minbtc-log.py
      
      # 方向B: 切换到 data-api.binance.vision (当 api.binance.us 不可用时)
      sed -i 's|BINANCE_KLINES = .*|BINANCE_KLINES = "https://data-api.binance.vision/api/v3/klines"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      sed -i 's|BINANCE_DEPTH = .*|BINANCE_DEPTH = "https://data-api.binance.vision/api/v3/depth"|' $SKILL_DIR/5minbtc-engine-v6.0.py
      sed -i 's|api.binance.us|data-api.binance.vision|g' $SKILL_DIR/5minbtc-log.py
      
      # 验证
      python3 $SKILL_DIR/5minbtc-log.py settle-all 2>&1 | head -3
      ```
      
      **验证命令**:
      ```bash
      # 快速测试哪个端点可用
      for ep in "api.binance.us" "data-api.binance.vision"; do
        code=$(curl -s --connect-timeout 10 --max-time 15 -o /dev/null -w "%{http_code}" "https://$ep/api/v3/klines?symbol=BTCUSDT&interval=5m&limit=1")
        echo "$ep → HTTP $code"
      done
      ```
      
      
      
      ### 路径硬编码
      
      不要用 `WORKSPACE = dirname(dirname(dirname(...)))` 指向旧 OpenClaw workspace。用 `SKILL_DIR = os.path.dirname(os.path.abspath(__file__))`。
      
      
      
      ### 全局字典临时修改模式
      
      当需要临时修改 `BASE_W` / `REGIME_ADJ` 等模块级字典做单次计算时,必须用 saved_base 模式:
      ```python
      saved_base = BASE_W.copy()
      BASE_W['momentum'] = 0.4  # 临时修改
      raw = combine_factors(factors, regime)
      BASE_W.update(saved_base)  # 立即恢复
      ```
      **不要**直接修改后不恢复——Python 模块级字典是全局可变的,下次调用会读到脏数据。
      
      
      
      ### 🔴 CRITICAL: 回测证明因子无预测力,但半K线延续性有真实edge (2026-05-28更新)
      
      
      365天105,120根K线回测(`backtest/`目录)揭示的真相:
      
      **v5.6纯价格因子(开盘预测)**:
      | 模式 | 当前K线数据 | 方向准确率 | 本质 |
      |------|-----------|-----------|------|
      | historical_only | 屏蔽 | 47.4% | 低于随机50% |
      | fair_live_sim | 屏蔽 | 48.8% | 等于随机 |
      | full_lookahead | 含close | 61.1% | 前视偏差 |
      | 实盘 (298轮) | progress=0.9+ | 66.1% | 间接前视 |
      
      **v5.8真实1min半K线回测(零前视偏差)**:
      | 前2根1min(40%进度) | 前3根(60%) | 前4根(80%) |
      |:---:|:---:|:---:|
      | **61.4%** (+11.4pp) | 65.8% (+15.8pp) | 69.5% (+19.5pp) |
      
      **结论更新**:
      1. 11个技术因子(momentum/RSI/meanrev等)在5分钟尺度上**确实无预测力**(49-51%)
      2. volume因子是唯一有独立alpha的(56-58%)
      3. **但半K线延续性效应是真实的alpha** — 用真实1min K线构建半K线状态后,准确率61-70%
      4. 实盘66%的谜团已解开: 不是前视偏差,是价格动量延续性
      5. half_body因子的贡献机制: 不是因子本身有方向预测力(48.5%),而是它触发了更紧的ATR阈值,让延续性效应更好地转化为预测区间
      
      **回测方法论教训**: 模拟半K线状态时,绝不能用 `(open+actual_close)/2`(前视偏差)或前一根5min body(太粗糙)。必须用对应时间段的**真实1分钟K线**数据。详见 `backtest/run_backtest_v58.py`。
      
      
      
      ### 🔴 CRITICAL: ThreadPoolExecutor.submit() 的 positional arg 陷阱 (v5.7.3)
      
      
      ```python
      # ❌ WRONG: 200 被当作第一个参数 symbol 传入
      ex.submit(fetch_klines, 200)
      # URL: ?symbol=200&interval=5m&limit=200 → HTTP 400
      
      # ✅ CORRECT: keyword arg
      ex.submit(fetch_klines, limit=200)
      # URL: ?symbol=BTCUSDT&interval=5m&limit=200
      ```
      
      `ex.submit(fn, arg)` 按函数签名位置顺序匹配参数。如果函数定义为 `def fetch_klines(symbol="BTCUSDT", ...)`,那么 `ex.submit(fn, 200)` 会把 200 传给 `symbol`。
      
      **规则**: submit() 中非第一个位置参数的调用必须用 keyword args。
      
      
      
      ### 🔴 Python 3.11 Unicode Strictness in Engine Source Files
      
      
      Python 3.11.15 (uv cpython build) rejects **any** non-ASCII character in `.py` files — not just in code, but in **comments and docstrings too**. This includes:
      - Full-width punctuation: `,:;()!?、。` → must use ASCII `,;:()!?.,`
      - Math symbols: `≤ ≥ × ÷ ± ★` → `<= >= * / +- *`
      - Arrows/dashes: `→ ← ∞ — –` → `> < inf -- -`
      - Smart quotes: `' ' " "` → `' "`
      - Tilde as "approximately" before decimals: `~2.5` → Python parses `~` as bitwise NOT, giving "invalid decimal literal"
      - **Hidden Unicode quotes**: Three `"""` that look correct but contain Unicode left/right double quotes (U+201C/U+201D) instead of ASCII 0x22 — causes "unterminated triple-quoted string"
      
      **Fix strategy** (in order):
      1. `sed -i` or Python script for bulk replacements of known character classes
      2. Regex: insert space between CJK chars and operators/digits (`CJK+*` → `CJK + *`)
      3. If still failing, use `xxd` or `hexdump` to inspect the raw bytes around the error line
      4. Nuclear option: strip ALL non-CJK non-ASCII with `content = ''.join(c if ord(c) < 128 or 0x4E00 <= ord(c) <= 0x9FFF else '<REPLACED>' for c in content)`
      
      **Prevention**: When writing Chinese comments/docstrings in engine `.py` files, never mix full-width punctuation or math symbols. Use ASCII punctuation only.
      
      
      
      ### Cron Job 版本同步
      
      升级引擎后必须同步更新 cron job 的 `name` 字段(如 `"5minbtc v5.6"`)。cron 不自动感知引擎文件版本——它只执行 `5minbtc-engine-v6.0.py`(旧版在 `archive/engines/5minbtc-engine-v5.py`),文件内容变了 cron 就跑新代码,但 job name 仍是旧标签,导致复盘时混淆实际运行的引擎版本。
      **操作**: 每次 engine 升级后,执行 `cronjob(action='update', job_id=..., name='5minbtc vX.Y')`。
      
      
      
      ### 🔴 CRITICAL: Cron Job LLM Provider 失效的诊断与切换 (2026-06-21)
      
      
      **症状**: cron job `last_status=error`,`last_error` 形如 `RuntimeError: Error code: 401 - {'error': {'code': '', 'message': 'Invalid token (request id: 2026...268d9d6XXX)'}}`
      
      **致命陷阱**: `last_error` 字段**会误导**!真实根因可能是 LLM provider 账户**欠费(HTTP 402)**或**fallback 链上某个 provider 的 api_key 解析失败**,最终 fallback 落到一个哑弹 provider 才报的 401。仅看 `last_error` 会以为是 token 失效。
      
      **真实案例 (2026-06-21)** — 3 个 5minbtc cron job 全部 `401 Invalid token`:
      - 表面错误: yiyong provider 的 401 (request id 含字母 `lLFxYw9`)
      - 真实根因: `deepseek-v4-pro` HTTP **402 Insufficient Balance**(账户欠费) → 触发 fallback → `minimax-cp` key 解析失败 → `yiyong` 也解析失败 → 最终 401
      - 验证方法: `python3 -c "import urllib.request,json,os; req=urllib.request.Request('https://api.deepseek.com/v1/models',headers={'Authorization':f'Bearer {os.environ[\"DEEPSEEK_API_KEY\"]}'}); print(json.loads(urllib.request.urlopen(req).read()))"` 返回 200 ≠ token 失效
      
      **诊断步骤(必须按顺序)**:
      
      ```bash
      # 1. 看 errors.log 完整瀑布(last_error 是最后一跳,不是第一跳)
      hermes logs errors -n 30
      
      # 2. 看 cron 组件日志(含每次 job 的 provider 加载记录)
      hermes logs --component cron -n 50
      
      # 3. 在日志中搜索关键模式:
      #    - "HTTP 402" / "Insufficient Balance" → 主 provider 欠费
      #    - "Fallback skip: chain entry X matches current provider/model" → fallback 短路
      #    - "has no resolvable api_key" → 某个 fallback provider 的 env var 解析失败
      #    - "request will be sent with placeholder no-key-required" → key 未注入
      ```
      
      **切换到替代 provider 的操作模板**:
      
      ```bash
      # 验证替代 provider 可用(必须先验证!)
      python3 -c "
      import urllib.request, json
      key = open('/home/aa/.hermes/profiles/cqo/.env').read().split('ZAI_API_KEY=')[1].split(chr(10))[0]
      req = urllib.request.Request(
          'https://open.bigmodel.cn/api/coding/paas/v4/models',
          headers={'Authorization': f'Bearer {key}'}
      )
      print([m['id'] for m in json.loads(urllib.request.urlopen(req, timeout=15).read())['data']])
      "
      # 确认 glm-5.2 (或其他目标模型) 在列表中
      
      # 批量切换所有受影响的 cron job
      for jid in d8058223a1e0 3016e27ddefa 9b07cd139f70; do
        hermes cron update --job-id $jid --model glm-5.2 --provider zai
      done
      
      # 立即验证(不要等下一次 schedule 触发)
      hermes cron run --job-id d8058223a1e0
      # 等待 ~40s(5minbtc 全链路约 30-50s)
      # 读 last_status
      python3 -c "
      import json
      jobs = json.load(open('/home/aa/.hermes/profiles/cqo/cron/jobs.json'))
      for j in jobs['jobs']:
          if j['id'] == 'd8058223a1e0':
              print(f'last_status={j[\"last_status\"]} last_run_at={j[\"last_run_at\"]}')
              if j.get('last_error'): print(f'last_error: {j[\"last_error\"][:200]}')
      "
      # 期望: last_status=ok
      ```
      
      **已知可用替代方案 (2026-06-21 验证)**:
      
      | Provider | Model | Base URL | 验证命令 |
      |---|---|---|---|
      | `zai` | `glm-5.2` | `https://open.bigmodel.cn/api/coding/paas/v4` | `GET /models` 返回 8 个模型含 glm-5.2 |
      | `zai` | `glm-5-turbo` | 同上 | 同上 |
      | `minimax-cp` | `MiniMax-M3` | `https://api.minimax.chat/v1` | ⚠️ 当前 key 解析有问题,报 "no resolvable api_key",待排查 |
      | `yiyong` | `gpt-5.4` | `https://cloud.yiyongai.cn/v1` | ⚠️ 当前 key 解析有问题,报 "no resolvable api_key",待排查 |
      
      **预防措施**:
      1. **每个 cron job 显式 pin 一个确认可用的 provider/model**(不要只靠 fallback 链)
      2. **fallback 链第一条必须是已验证的 provider**(不是默认配置的)
      3. **每月检查账户余额** — DeepSeek/Anthropic/OpenAI 都按 token 计费,欠费前无预警
      4. **收到 cron 失败告警时**,先 `hermes logs errors` 再下结论,不要被 last_error 的字面错误带偏
      
      详见 `references/cron-llm-provider-failure.md`。
      
      
      
      ### 复盘日志数据质量检查 (2026-06-19)
      
      复盘读取 `5minbtc-log.jsonl` 原始数据时,先做三项数据质量校验,避免把数据缺陷误读为行情误判:
      1. **bias 字段合法值**: 只允许 `bull`/`bear`/`neutral`。若出现 `weak`/`strong`/`medium`,说明 engine→log 映射写错了字段(把 strength 写进了 bias)。06-19 出现 bias="weak"(21:35),需排查 `5minbtc-log.py log` 的参数顺序。
      2. **重复记录去重**: 同一根K线(candle_start 相同)出现两条记录 = cron 双触发或手动+自动双跑。06-19 的 20:00 candle 被记录两次(conf 44 和 45)。统计前按 candle_start 去重,取后一条。
      3. **vol_pct 异常值过滤**: vol_pct>200% 是未完成K线成交量/接近0的历史均量导致的爆炸性 glitch(见教训15),非真实放量。统计 vol 分布时先剔除 >200% 的点,否则高量区准确率被噪声污染。
      
      
      
      ### 内联 python3 -c 脚本中的 emoji/状态字符
      
      复盘/分析时手写 `python3 -c "..."` 一次性脚本,若字符串里含 emoji(如 `✅❌⚠️🟢🟡🔴`)会触发 Hermes 安全扫描的 variation-selector / 隐写检测,导致命令被拒。
      - **规避**: 内联脚本只用 ASCII 状态词(`OK`/`X`/`POS`/`NEG`);需要 emoji 时用 `write_file` 写成独立 `.py` 再 `python3 file.py`。
      - **优先**: 按维度拆解的复盘统计直接用 `scripts/daily-review-stats.py`,不要每次重写内联脚本。
      
      
      
      ### 🔴 极端进度执行 (>80%) — 引擎pred_close偏离当前价
      
      
      当cron延迟执行导致progress>80%(如K线第4分钟甚至第5分钟才触发),引擎的pred_close会严重偏离当前价。根因:half_range×ATR公式设计用于~40%进度(剩余~2.5分钟),在>80%进度时高估剩余波幅。实测案例:progress=94%(剩18s),当前价$62,900,引擎pred_close=$62,745(偏差-$155)。
      
      **识别信号**: `progress_pct > 75` 且 `|pred_close - current| > ATR * 0.5`
      
      **LLM处理规则**:
      1. 引擎pred_close不可直接采用 — 剩余时间不足以覆盖该偏差
      2. LLM调整可**突破±ATR×0.3上限**,理由:该上限假设~2.5min窗口,不适用于极端进度
      3. 调整策略: `pred_close = current + (engine_pred_close - current) * (remaining_sec / 150)` — 按剩余时间比例缩放
      4. 置信度保持引擎值不变(因子计算不受进度影响)
      5. **方向判断仍需执行裁决规则**(R1/v5.7.2/v5.7.4),但收盘价预测独立处理
      
      **文档化要求**: LLM输出中必须注明"⚠️ 极端进度(XX%),pred_close已按剩余时间比例调整"
      
      
      
      ### 🔴 Body=0 持续模式 (冻结K线) — 引用 5minbtc-v57-optimization §7
      
      
      当K线开盘后 body=0 (O=H=L=C) 持续2分钟以上,引擎因子(尤其是half_body)无法提供有效方向信号。此时引擎 pred_close 严重不可靠——half_range×ATR公式在body=0时仍产生拟合值,加上chainlink_offset影响,预测价可能偏离当前价$100+。
      
      **两种子模式** (详见技能 `5minbtc-v57-optimization`):
      
      | 模式 | 特征 | LLM应对 |
      |------|------|---------|
      | **模式A: body=0 2m+后选方向** | progress>40%, 临close前50-90s突破 | 区间扩大≥±$80, 等待half_body突破0.25再下方向 |
      | **模式B: body=0 整根5min** | O=H=L=C 持续300s, microprice巨翻但无实际突破 | 方向跟随orderflow但conf降至35-42%, 区间收窄±$30-50 |
      
      **LLM处理规则**:
      1. body=0模式优先级高于标准pred_close微调规则 — 引擎pred_close不可直接采用
      2. pred_close应覆盖为接近当前价($current ± ATR×0.2以内)
      3. 方向判断: 若score≈0(因子完美平衡)且无half_body信号,维持neutral
      4. 即使microprice极值(±0.9+),在body=0模式下也不构成独立方向信号 — 历史上orderflow巨翻不必然触发实际突破
      5. 输出中必须注明"⚠️ Body=0持续X.Xmin"
      
      **实战案例 (2026-06-27 21:10)**: body=0持续3.5min(progress=73.5%), microprice=+0.997 extreme bull, 引擎pred_close=$60,313(偏离-$110)。LLM覆盖为$60,420(≈当前价)并扩宽区间至±$50。
      
      
      
      ### 🔴 并行工具调用延迟评估 = max() not sum() (2026-07-05)
      
      **错误**: 在 multi-tool-use 并行块中估算"砍掉 N 路节省多少"用 sum()。
      
      **正确**: 并行块总 wall-clock = max(各调用延迟), 不是 sum()。砍掉非最慢的 source 实际节省 0s, 只有砍最慢那个才生效。
      
      **5minbtc 案例 (2026-07-05)**: 我算"砍 3 路 web_search 节省 5-10s" 错误。用户指出 web_search 与 engine/news 是同一并行组, 总耗时 ~3-5s (engine 决定), 砍 web_search 实际只省 0-2s, 不是 5-10s。
      
      **判断规则**:
      1. 识别并行组 (同一 multi-tool-use 调用块)
      2. 找出组内最慢调用
      3. 砍其他调用节省 = 0s, 砍最慢调用节省 = (max - second_max)
      4. 评估"是否值得"应基于 signal-to-noise, 不是 wall-clock 节省
      
      **例**: 5minbtc 并行组 [settle 1s, engine 3s, news 1s, ws1 3s, ws2 3s, ws3 3s] -> 总耗时 3s (engine/任意 ws 决定)。砍 ws1+ws2+ws3 省 0-2s, 不是 6-9s。真正可省的是把 engine 从 3s 优化到 1.5s。
      
      ### 🔴 chainlink_offset 可使引擎 pred_close 与 bias 方向矛盾 (2026-07-05)
      
      
      引擎的 pred_close 公式对 chainlink_offset 做了补偿:当 Binance 低于 Chainlink 参考价(offset<0)时,pred_close 会被向下补偿,即使引擎因子给出 bull 方向,pred_close 也可能低于当前价。反之 offset>0 时 bear 方向的 pred_close 可能高于当前价。
      
      **识别信号**: `prediction.bias=bull 但 pred_close < current`(或 `bias=bear 但 pred_close > current`)
      
      **LLM处理规则**:
      1. 方向判断以 `bias` / `score` / `strength` 为准(来自因子打分,反映真实动量方向)
      2. pred_close 的绝对值不可直接采用——它被 chainlink_offset 扭曲
      3. pred_close 微调时,应以 current 为锚点,按 bias 方向小幅调整(±ATR×0.3),而非从 engine pred_close 出发
      4. 输出中注明"pred_close已按方向修正(chainlink_offset扭曲)"
      
      **实战案例 (2026-07-05 18:45)**: current=$62,710, bias=bull medium, engine pred_close=$62,651(低于当前价$59), chainlink_offset=-$59。LLM以current为锚+bull方向微调至$62,720。注意:这不是引擎bug,而是 Binance↔Chainlink 价差补偿的设计副作用——factor score 给出方向,chainlink 给出绝对价位偏移,两者维度不同时即出现表面矛盾。
      
      
      
      ### 🔴 高延迟网络 — 引擎timeout临时修补 (2026-06-24)
      
      
      当ping 8.8.8.8 >250ms 时,两个Binance端点都可能返回HTTP 000(SSL握手超时),而非HTTP错误。此时端点切换无效——需要临时增加引擎内部`urlopen`的timeout参数。
      
      **症状**: `urllib.error.URLError: <urlerror _ssl.c:999: The handshake operation timed out>`,但curl用`--connect-timeout 20`可通。
      
      **操作步骤**:
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc
      cd "$SKILL_DIR"
      
      # 备份
      cp 5minbtc-engine-v6.0.py 5minbtc-engine-v6.0.py.bak-net
      
      # 扩大timeout: klines 10→25s, 其他 5→15s
      sed -i 's|timeout=10|timeout=25|g' 5minbtc-engine-v6.0.py
      sed -i 's|timeout=5|timeout=15|g' 5minbtc-engine-v6.0.py
      
      # 验证
      grep -n "timeout" 5minbtc-engine-v6.0.py
      
      # 运行引擎后务必恢复
      cp 5minbtc-engine-v6.0.py.bak-net 5minbtc-engine-v6.0.py
      rm 5minbtc-engine-v6.0.py.bak-net
      ```
      
      **关键**: 这是临时workaround,不是永久修改。引擎执行完成后**必须恢复**原始timeout值。该修补仅在`curl --connect-timeout 20`可通但Python `timeout=10`超时时使用。两个端点都HTTP 000且curl加大timeout仍不通时,是真正的网络中断,此时应报告并跳过本轮预测。
      
      
      
      ### 🔴 Web搜索不可用时的降级策略
      
      
      当3组web_search全部超时(高延迟网络常见),不要反复重试——DuckDuckGo/Startpage在高延迟网络下几乎一定超时。降级策略:
      1. **引擎内置新闻扫描** (`5minbtc-news.py`) 仍然可用(走RSS,不走搜索后端)
      2. 直接用 `news-risk-level.json` 的结构化结果作为新闻输入
      3. 输出中注明"新闻搜索不可用(网络高延迟),已用引擎内置RSS扫描"
      4. **不要重试web_search超过2轮** — 第1轮初始搜索+第2轮上下文定制搜索后仍失败即放弃
      
      ---
      
      ## 2026-07-05 整理后新增 pitfall (会话内验证)
      
      ### 🔴 SKILL.md 维护: 定期重构为 INDEX 风格 (2026-07-05)
      
      **症状**: SKILL.md 单文件膨胀, 涵盖 changelog + lessons + pitfalls + 复盘流程, 超过 300 行时触发用户"太长了"反馈。
      
      **规则**:
      1. **定期检查**: SKILL.md > 250 行应主动提议拆出 references/
      2. **拆分原则**: changelog/lessons/pitfalls/执行步骤/复盘流程/session 记录/数据源评估/Cron 配置, 每类一个 references/<topic>.md
      3. **SKILL.md 只保留**: 触发 + 何时用 + quick-start + 铁律 (3-7 条) + 关键规则 (5-7 条精简) + 性能快照 + references 索引
      4. **避免**: 把 7 个 changelog + 22 lessons + 22 pitfalls + 复盘流程全塞 SKILL.md
      
      **5minbtc 实战 (2026-07-05)**: 887 → 152 行 (-82%) 重构。详见 `references/skill-organization.md`。
      
      ### 🔴 CRITICAL: K 线开局阶段 OFI 概率不可信 — vol_gate 算了不用 (2026-09-10)
      
      **症状**: 每 5 分钟推送的预测快照在 `p1%`(K 线刚开几秒)时就给出很自信的概率,例如
      `📊 预测 看多·收阳 | 14:20 p1% / 概率 0.70 | 市场 UP 0.55 | edge +0.15`。
      K 线只走了几秒,这个 0.70 其实来自极少量的成交。
      
      **根因**(三条叠加,都在 v6.0 引擎内):
      
      1. **主源取自进行中的 K 线**:`ofi_n = 2*(tb/v) − 1`,`tb`/`v` 来自**当前未完成** K 线的 `klines[5]/[9]`。
      2. **没有最小样本量保护**:`_ofi_native()` 只挡 `v <= 0`。开局 3 秒、成交 3.4 BTC(当天实测,
         仅为已完成 K 线均量 39.85 的 **8.5%**)就能算出 `tb/v = 0.935` → `ofi_n = +0.87`(强看多)。
         少量成交天然偏向单边,`tb/v` 极易饱和到 0 或 1。
      3. **`vol_gate` 是死字段**:引擎 L692 按 `MIN_VOL_FRAC = 0.25` 算出 `vol_gate`(上面那根实测应为
         `False`),L701 写进 meta、L1190 导出到 JSON —— **但从头到尾没有任何代码读它**。
         它既不改方向,也不降概率。
      
      **放大效应**: 概率公式 `p = 0.5 + (p_cal−0.5) + 0.30·sign(E_rem)·min(|E_rem|,1)·rem_frac`,
      其中 `rem_frac = 剩余时间/300` 在 `p1%` 时 ≈ **0.99(几乎满权重)**,而 `E_rem ≈ ofi_n`
      同样来自那几秒的成交 —— **越早,噪声项反而被放得越大**。
      
      **校准口径不匹配**(更隐蔽的一层): `ofi_calibration_table()` 用 **`candles[:-1]`(已完成 K 线)的
      最终 `tb/v`** 建表,而 `ofi_probability()` 拿 **进行中 K 线的部分 `tb/v`** 去查同一张表。
      两者分布不同(部分值更噪、更易饱和),表是按前者标定的。
      
      **验证风险**: 日志 8250 条已结算预测里,**8229 条(99.7%)是 40–60% 进度**的(watch daemon 只在
      每根 K 线的第 2/3/4 分钟采样),`0–20%` 进度**只有 5 条** —— 也就是说 `p1%` 这个 regime
      **基本没被验证过**,而它的概率看起来和 45% 进度时一样自信。
      
      **规则**:
      1. **只在进度 ≥ 40% 后采信概率/edge/下单**。`p < 20%` 的快照只当"盘口在动"的提示,不要当信号。
      2. 报告里引用概率时,**必须附上进度**;进度很低时明确标注"数据不足,概率不可信"。
      3. 若要修引擎(**尚未修**,需先回测验证):给 `_ofi_native()` 加最小成交量门槛(用现成的
         `vol_gate` 逻辑),并在 `ofi_probability()` 里按 `vol_gate=False` 把 `p` 向 0.5 收缩。
         ⚠️ 改前必须用 `backtest/` 验证,别直接改生产引擎。
      
      **影响面**: 非活跃时段(当前配置 `--active-hours 20,21,22,23`)`p1%` 快照只用于**展示**,不下单;
      但**活跃时段内同一套概率会驱动 EV 下单** → 会在几秒的数据上开 paper 仓。
      
      ### 🔴 CRITICAL: "真OFI" 其实只覆盖单一市场 — 主源(现货) vs 辅源(合约) 口径不一致 (2026-09-10)
      
      **先澄清"真"到什么程度**:
      
      | 问题 | 答案 |
      |------|------|
      | 是真成交吗? | ✅ 是。主源 `klines[9]` 是 **Binance 官方计算**的 taker buy base volume,不是自己用 tick rule 猜的 |
      | WS 侧是真主动方吗? | ✅ 是。`@trade` 消息自带原生 `m`(buyer is maker)字段,`agg_native: true` 表示全程用原生标记、未退回 tick 规则 |
      | 是学术意义的 OFI 吗? | ❌ **不是**。公式 `2*(tb/v)−1` 是 **taker 买卖失衡率(trade imbalance)**;Cont et al. (2014) 的 OFI 是从 **L2 订单簿事件**(最优买卖档挂单量变化)算的。准确叫法是"真实主动成交失衡" |
      | 覆盖全市场吗? | ❌ **不**。见下 |
      
      **核心问题:引擎的"主源"和"辅源"根本不是同一个市场**
      
      | | 用途 | 端点 | 市场 |
      |---|---|---|---|
      | 主源(**决定方向**) | `ofi_n = 2*(tb/v)−1` | `data-api.binance.vision` | Binance **现货** |
      | 辅源(仅冲突校验) | `reversed_60` / `ws_conflict` | `fstream.binance.com` | Binance **合约永续** |
      
      **实测(2026-09-10,20 根已完成 5m K 线)**:
      
      ```
      方向一致 16/20 = 80%      方向相反 4/20 = 20%
      平均 5m 成交量:  现货 191.0 BTC   合约 2248.6 BTC   合约/现货 = 11.8x
      ```
      
      - **1/5 的 K 线上两个市场方向相反**,而决定引擎方向的是**成交量小 11.8 倍的现货市场**。
        BTC 的价格发现在合约端(量大一个数量级),现货主动成交与合约主动成交是两拨资金。
      - 引擎把两者当"同一件事的两次观测"来做冲突校验 —— 但 20% 的基线分歧意味着
        `ws_conflict` **大部分时间是噪声,不是发现**。
      
      **还有第三个市场(结算口径错配)**:
      `fetch_chainlink_ref()` 抓 **Coinbase 现货** 作为 Chainlink(多 CEX 中位数)的代理,
      即**预测市场的结算价**。而引擎用来预测的 klines 是 **Binance 现货**。于是:
      **用 Binance 现货的主动成交,去预测一个按多交易所中位数结算的 5 分钟涨跌。**
      
      **一个不对称(与 #18 同源)**:WS 辅源有 `MIN_VOL_BTC = 20.0` 的最小量门槛
      (不足则 `ofi_candle` 置 0),**引擎主源完全没有**(只挡 `v <= 0`)。
      
      **规则**:
      1. 描述这个信号时,**不要**说"全市场订单流"/"OFI(Cont 2014)",准确说法是
         **"Binance 现货的主动成交失衡"**。
      2. 看到 `ws_conflict: true` **不要当成强信号** —— 20% 的基线分歧率下它近似噪声。
      3. **若要改**(未做,需先回测):把主源换成**合约** klines,与自己的 WS 辅源对齐市场,
         也更贴近价格发现端。但 ⚠️ **`fapi.binance.com` 只是间歇可达**:
         2026-09-10 22:00 实测返回正常 K 线,2026-09-11 17:55 再测为 **HTTP 403**。
         早期记录的 451 地区限制与现在的 403 都属于**代理链路相关的间歇失败**,
         不能假设它稳定可用 —— 换源前必须先解决可达性。
      
      ### 🔴 CRITICAL: 端点 SSL 失败 → 6 小时静默断档, 记录与推送一起停, 日志无痕 (2026-09-11)
      
      **症状**: 用户以为"预测只是没推送过来,记录应该还在"。**实际记录也没了。**
      
      **实测断档**(`logs/5minbtc-log.jsonl` 按小时计数,正常应 12 条/小时):
      
      ```
      00:00–10:00  每小时满 12 条
      11:00        7 条        ← 11:30 断
      12:00–16:00  0 条        ← 完全空白
      17:00        1 条        ← 17:45 恢复
      断档时长: 11:30 → 17:45 ≈ 6 小时 15 分
      ```
      
      **根因链**(旁证充分,非推测):
      
      1. 机器**没休眠** —— `pmset -g` 显示 `sleep 0 (sleep prevented by configd, ToDesk, UURemote, powerd)`,
         uptime 连续无重启,6 个守护进程 `ELAPSED` 连续。
      2. 独立 daemon `bb_paper.log` **全程未断档**(每小时 290–450 条)→ **不是整机断网**。
      3. 但它从 **11:24:43 起**开始报错:`data-api.binance.vision` 的
         `SSLError(SSLEOFError(8, 'EOF occurred in violation of protocol'))`。
      4. **`data-api.binance.vision` 正是 5minbtc 引擎唯一的 klines 来源** → 引擎取不到数据 →
         `run_engine()` 返回 `None` → **记录与推送同时停**(两者是同一条链路的下游)。
      5. `bb` 没事是因为它有 **WS 主源 + REST 兜底**;5minbtc 引擎是 **REST 单端点硬依赖**。
      
      **为什么排查时看不见**:`5minbtc_realtime.py` 的 `run_engine()` 是
      `except Exception: return None` —— **静默吞异常**。所以这次 6 小时断档在
      `launchd-realtime.err.log` 里留下的是 **0 字节**。这是本条最该修的地方。
      
      **规则**:
      1. **不要把"没推送"当成"只是没推送"** —— 推送和记录共用引擎这一条上游,
         引擎挂了就是两个都没有。要判断,看 `logs/5minbtc-log.jsonl` 的**按小时记录密度**,
         而不是看有没有收到消息。
      2. **排查顺序**:① 记录密度(定位断档区间)→ ② `bb_paper.log` 同期是否有同端点 SSL 报错
         (区分"整机断网"与"单端点故障")→ ③ `pmset -g`(排除休眠)。
      3. **建议修法(未做)**:给引擎加**端点自动故障切换**
         (`data-api.binance.vision` ↔ `api.binance.us`,2026-09-11 实测两者均 HTTP 200),
         并让 `run_engine()` 连续失败 N 次后**推 Telegram 告警**而不是静默返回 None。
         ⚠️ 注意 `fapi.binance.com` 只是间歇可达(见 #19),不要拿它当兜底。
      
    • prediction-market-strategy.md 6.7 KB
      # 预测市场盈利策略 — 5minbtc 方向信号 × 币安 Up/Down 5m
      
      > 把 5minbtc 的方向预测 (bull/bear) 用到币安 Web3 预测交易 (BTC Up/Down 5m,
      > Polymarket 式二元市场) 的**完整策略**。引擎方向 + 价格门控 + 凯利仓位。
      > ⚠️ 预测交易无 test 模式 — paper 模拟先用, 实盘前务必先跑 paper。
      
      ## 一、唯一公式
      
      ```
      每 $1 下注 EV = p − P
      p = 你该方向的真实概率(条件胜率,非引擎 conf)
      P = 你买入 token 的价位(市场 ask)
      只当 p > P + 手续费 才下注
      ```
      
      **利润不是来自"方向准",是来自"你比市场定价准"。**
      
      ## 二、p 校准(2026-08-12 实测,第 2 分钟预测)
      
      | 信号 | 条数 | 胜率 p | 盈亏平衡入场价 |
      |------|------|--------|--------------|
      | **bull → UP** | 23 | **74%** | 0.70 |
      | bear → DOWN | 7 | 57% | 0.57 |
      
      **关键事实**(详见 `5minbtc_day_stats.py` / `logs/5minbtc-log.jsonl`):
      - 预测全部发生在**第 2 分钟 (progress ~40%)** —— watch 每根 K 线只记录首次采样。所以 p 是**早期预测**的准确率,不是"看了一半结果"刷出来的。
      - **引擎 confidence 反校准**:conf<50 胜率 72% > conf 50-59 的 67%。**不要用 conf 当门槛**(trader 默认 MIN_CONF=55 实测反而砍样本降准确率)。
      - DOWN 样本仅 7 笔,57% 不可靠 → **先只做 bull/UP**。
      
      ## 三、入场规则(价格门控 = 一切)
      
      ```
      第 2 分钟 bull 信号:
        实时 ask ≤ 0.65  → 买 UP  (p=0.74 − 9% margin − 手续费)
        实时 ask ≥ 0.70  → 不碰
      bear 信号: ask ≤ 0.50 才买,否则跳过
      ```
      
      - **limit-price = 入场上限**(UP 0.65 / DOWN 0.50,已是脚本默认),行为 = "按最优价买,绝不高于上限",秒成交不追高。
      - 不要用 ask×0.98 省 2% —— 5 分钟市场波动快,挂低 2% 大概率不成交,**错过整笔 EV+0.15~0.19 的交易**。
      - LIMIT 单 ≥0.5U(交易所仅 MARKET 强制 ≥1.5U;脚本已放开 LIMIT 下限)。
      
      ## 四、仓位(1/4 凯利)
      
      ```
      凯利 f* = (p − P)/(1 − P)
      p=0.74, P=0.55 → f*≈42% → 实盘用 1/4 ≈ 10% 账户/注
      每根 K 线最多 1 注 | 账户 10U 用 1U/注
      ```
      
      74% 胜率 = 约 3/10 注会全亏本金 —— **控制仓位是活命前提**。
      
      ## 五、持有到结算,无 TP/SL
      
      二元市场几分钟结算成 1 或 0:
      - **TP 关**:中途 1.3× 止盈是拿确定的钱换掉大概率到手的 1.0,负期望。
      - **SL 关**:错时 token 结算到 0,0.4× 止损和持有到 0 差别极小。
      - 真正的"止损" = **入场价纪律**(ask 超上限就不买)。
      
      ## 六、paper 模拟(先用这个,绝不下单)
      
      ```bash
      # 单次:结算已收盘 → 引擎信号 → 真实报价 → 价格门控记录 → 报告
      python3 scripts/5minbtc_trader.py --paper --paper-up-max 0.65 --paper-push
      
      # 实时监控:LIMIT 单模拟 → 每根K线精确节奏 → 推 Telegram
      python3 scripts/5minbtc_trader.py --paper-monitor --paper-up-max 0.65 \
        --paper-p-down 0.50 --paper-push --amount 1 --paper-bankroll 100
      ```
      
      ### 免密钥模拟盘(keyless, 无需币安 API 密钥)
      ```bash
      python3 scripts/5minbtc_keyless_paper.py --up-max 0.65 --down-max 0.50 \
        --amount 1 --bankroll 100 --push
      ```
      - 用**公开 BTC 数据**(`data-api.binance.vision` spot + 引擎)模拟 UP/DOWN token 价:
        `P_up = sigmoid(1.7 × 当前涨跌幅 / ATR)`(纯价格走势模型 = "市场"定价),DOWN = 1−P_up。
      - 引擎信号(含订单簿/taker 信息)= 你的 edge;`P_up < p` 时买入 = EV+。
      - 与真实 paper 监控同一套 LIMIT/成交/结算/账户节奏(台账 `~/bb-auto/5minbtc-paper-keyless.json`)。
      - 真实市场价源(需密钥)就绪后切回 `--paper-monitor`。`SIGMOID_K` 可调灵敏度。
      
      ### 每根 5min K 线节奏(--paper-monitor / keyless)
      | 时刻 | 动作 |
      |------|------|
      | 第0分钟 | 结算上一轮 → 推送获利% + 账户权益 |
      | 第2分钟 | 引擎确认方向+入场价 → 设纸面 LIMIT(限价=入场上限, 方向锁定) |
      | 第2分钟后 | 轮询 order-book: ask≤限价 → 成交 → 记录持仓 + 实时涨跌幅推送 |
      | 第3分钟 | **若第2分钟中性无单 → 第二次机会下单**; 若有单则方向锁定不改变; 仍无信号才只预测 |
      | 收盘未触及 | 未成交 → 放弃 |
      
      - **账户**:`--paper-bankroll` 模拟本金(默认 $100),每注 `--amount`(默认 1U),`paper_report`/推送显示 账户余额+收益率。
      - 状态文件 `~/bb-auto/5minbtc-paper.json`(含入场 P / 成交价 / 结算 PnL / 未成交记录)
      - 只 get-quote / order-book,**从不 place-order**
      - 需要的环境变量:`BINANCE_API_KEY / BINANCE_API_SECRET / BINANCE_PREDICT_WALLET / BINANCE_PREDICT_WALLET_ID`
      - **实时价格**:`scripts/prediction_ws_feed.py` 用币安 **w3w-prediction WebSocket API**
        (`wss://api.binance.com/sapi/wss?topic=web3_prediction_orderbook_data`,<200ms)
        写 `~/bb-auto/prediction-ws.json` 缓存;watch/paper 读缓存即秒级实时。不用 RSS。
        认证: HMAC SHA256 签名 + `X-MBX-APIKEY` header; 30s PING; 断线重连。
      
      ## 七、实时推送格式(--paper-monitor, LIMIT 单模拟)
      
      ```
      🏁 结算 19:30 UP @0.60 ✅ 中            ← 第0分钟: 上一轮获利
      获利 +66.7% | +$0.67
      💼 账户 $100 → $100.67 (+0.67%)
      
      📋 LIMIT | 19:35 第2min                  ← 第2分钟: 设单 (方向锁定)
      UP 限价 0.65 (p=0.74) | 现价 0.72
      成交时 EV +0.09 | 等回调 | 1U
      
      ✅ 成交 | UP @ 0.60 (限价 0.65)          ← 成交: 记录持仓
      持仓 1U | 涨跌幅 0% 起算 | 等结算
      
      🔎 第3分钟预测 | 19:35                   ← 第3分钟: 只预测
      现价 64,000 | bull/medium conf 55 | (仅预测, 不设LIMIT)
      
      📡 UP @0.60 → 现价 0.78                  ← 盘中: 实时涨跌幅
      涨跌幅 +30.0% | 剩余 2m40s
      
      🏁 结算 19:35 UP @0.60 ✅ 中             ← 收盘: 最终获利%
      获利 +66.7% | +$0.67 | 💼 账户 $101.34
      ```
      
      ## 八、诚实局限
      
      - p 是**单日 30 笔**样本,会回归;数据积累几百根后重估。
      - **盈利取决于真实 ask vs p 的差距**——paper 模拟(真实报价)是唯一能验证"市场是否低估你的信号"的手段。实测第 2 分钟市场通常还没充分定价,这是 edge 所在,但必须量化确认。
      - 首次实盘前:paper 连续证明 EV+ 至少 2-3 天。
      
      ## 相关
      - [prediction-trading-cli.md](prediction-trading-cli.md) — CLI 全参数/安全契约/API端点
      
      > 原 `polymarket-data-source.md`(Polymarket 用 Chainlink 结算 + 与 Binance 价差风险)已于 2026-09-10 删除:
      > 交易场所现已换成**币安 Web3 预测市场**(见 prediction-trading-cli.md),Polymarket 盘口框架过时;
      > Chainlink 价差风险本身仍在 [pitfalls.md](pitfalls.md) 的 `chainlink_offset` 条目保留。
      
    • prediction-trading-cli.md 7.5 KB
      # 币安预测交易 CLI — 5minbtc_trader.py 使用文档
      
      > 5minbtc 引擎信号 → 币安「Web3 Wallet 预测交易」(BTC Up or Down 5m, Polymarket 式二元市场) 桥接。
      > 引擎 `bias=bull` → 买 UP token; `bias=bear` → 买 DOWN token。
      > ⚠️ **预测交易没有 test 模式 — 下单即真实花钱。** 非投资建议,仅供量化研究。
      
      ## 定位
      
      - 脚本: `scripts/5minbtc_trader.py`(纯 Python 标准库, 直接签名调用币安 SAPI)
      - 引擎: `5minbtc-engine-v6.0.py`(预测方向/置信度/taker_buy)
      - 监控: `scripts/5minbtc-monitor.py`(看信号事件, 不交易; 见 monitoring-claude-code.md)
      
      ## 环境变量
      
      | 变量 | 必填 | 说明 |
      |---|---|---|
      | `BINANCE_API_KEY` / `BINANCE_API_SECRET` | ✅ | 签名用 (HMAC SHA256) |
      | `BINANCE_PREDICT_WALLET` | ✅ | 预测钱包地址 (从环境变量注入, 代码不含默认值) |
      | `BINANCE_PREDICT_WALLET_ID` | ✅ | 预测钱包 ID (同上) |
      
      > 钱包地址/ID 属于账户敏感信息, 不走代码/仓库, 必须由运行方 export。
      > 查询方式: 用 key 调 `GET /sapi/v1/w3w/wallet/prediction/wallet/list` 获取。
      
      ## 核心参数
      
      | 参数 | 默认 | 说明 |
      |---|---|---|
      | `--once` | - | 单次: 读引擎→判信号→发现市场→报价→(确认后)下单 |
      | `--loop` | - | 持续监控开仓 (每根K线第2/3/4分钟采样) |
      | `--monitor` | - | 持仓监控: 实时面板 + 自动止盈止损 |
      | `--live` | off | 实盘 (双确认: 启动 yes + 每单 y) |
      | `--amount` | 2.0 | 下单金额 USDT (LIMIT ≥0.5, MARKET ≥1.5) |
      | `--order-type` | `LIMIT` | 买入单类型: LIMIT / MARKET |
      | `--limit-price` | 默认=入场上限 | LIMIT 限价 (token 价 0~1); 不传则用该方向入场上限 (`--limit-up-max`/`--limit-down-max`) |
      | `--limit-up-max` / `--limit-down-max` | 0.65 / 0.50 | 该方向入场上限 = LIMIT 默认限价 (实测盈亏平衡 UP 0.70 / DOWN 0.57) |
      | `--tp-mult` | 0(不启用) | 止盈: 现价/成本 ≥ 此值自动 SELL 锁利 (如 1.4=+40%) |
      | `--sl-mult` | 0(不启用) | 止损: 现价/成本 ≤ 此值自动 SELL (如 0.4=跌60%) |
      | `--monitor-interval` | 10 | 持仓监控轮询间隔秒 |
      | `--monitor-iters` | 无限 | 持仓监控轮数上限 |
      | `--rounds` | 无限 | loop 轮数上限 |
      
      ## 用法示例
      
      ```bash
      # 1. 单次 LIMIT 低挂买入 (只报价, 确认后才成交)
      python3 scripts/5minbtc_trader.py --once --order-type LIMIT --amount 2.0
      
      # 2. 持续监控开仓 (每根K线第2/3/4分钟, LIMIT 低挂)
      python3 scripts/5minbtc_trader.py --loop
      
      # 3. 持仓监控 + 自动止盈止损 (需显式 TP/SL + --live 才真实卖出)
      python3 scripts/5minbtc_trader.py --monitor --tp-mult 1.4 --sl-mult 0.4 --live
      
      # 4. 只跑 1 轮面板看当前持仓 (不自动卖)
      python3 scripts/5minbtc_trader.py --monitor --monitor-iters 1
      
      # 5. 实盘单次 (启动输 yes, 每单输 y)
      python3 scripts/5minbtc_trader.py --once --live --amount 2.0
      ```
      
      ## 安全契约 (铁律)
      
      1. **默认只报价不成交** — get-quote 不花钱, 真实花钱的是 place-order-bundle
      2. **买入需人工确认** — 每单输入 y; `--live` 另需启动时输入 yes (双闸门)
      3. **自动卖出需显式 TP/SL** — 未设 `--tp-mult`/`--sl-mult` 禁止自动 SELL, 只监控提示
      4. **自动止盈止损**: 现价/成本 ≥tp 自动 SELL 锁利; ≤sl 自动 SELL 止损 (用户授权自动卖出)
      5. **重试机制**: `_retry` 默认 10 次指数退避, 覆盖持仓查询/盘口/卖出, 避免单次失败
      6. **余额校验**: 下单前检查, 不足即拒; 金额默认小注
      7. **市场过期保护**: 每根K线只对当前 OPEN 的 btc-updown-5m 市场取价/下单 (endDate 硬校验)
      8. **token 映射防错**: order-book outcome 强制校验 + 词首匹配, 不符拒单
      
      ## 币安预测 API 端点 (已验证)
      
      | 端点 | 用途 |
      |---|---|
      | `GET /sapi/v1/w3w/wallet/prediction/wallet/list` | 预测钱包列表 |
      | `GET /sapi/v1/w3w/wallet/prediction/balance/payment-options` | 余额 (CeDeFi/SPOT/FUNDING) |
      | `GET /sapi/v1/w3w/wallet/prediction/market/list` | BTC 5m 涨跌市场列表 |
      | `GET /sapi/v1/w3w/wallet/prediction/order-book` | token 盘口价 (需 marketId+tokenId+vendor=predict_fun) |
      | `POST /sapi/v1/w3w/wallet/prediction/trade/get-quote` | 报价 (BUY/SELL, LIMIT/MARKET) |
      | `POST /sapi/v1/w3w/wallet/prediction/trade/place-order-bundle` | 下单 (真实花钱, FOK/GTC) |
      | `GET /sapi/v1/w3w/wallet/prediction/position/list` | 持仓查询 |
      | `GET /api/v3/ticker/price?symbol=BTCUSDT` | BTC 实时价 (免签名) |
      
      ## 已知坑 (教训)
      
      - **签名**: POST 用原始字节 body (`(qs+"&signature="+sig).encode()`), 库自动转义 `[]` 导致 -1022
      - **amountIn**: 18 位 wei 字符串; MARKET 单 ≥1.5e18 (~1.5 USDT), LIMIT 单不受此限
      - **LIMIT 单**: 需 `priceLimit` (token 价 0~1) + place-order 用 `GTC`; MARKET 用 `FOK`
      - **SELL 无持仓**报 `-9000 exceeded available shares` = 报价参数正确, 只是无持仓可卖 (正常)
      - **市场收盘**: 接近结算时 UP/DOWN token 价会极端 (0.98/0.01), 此时买入性价比差, 引擎 conf 通常也会拦截
      
      ## 验证快照 (2026-08-12)
      
      - 余额: `payment-options` 返回 CeDeFi/SPOT/FUNDING 三账户 (具体数额运行时查询, 不写入文档)
      - 市场: BTC Up or Down 5m, `tradingStatus=OPEN`, chainId 56 (BSC), vendor PREDICT_FUN
      - 止盈止损触发逻辑 dry-run: 1.44x→止盈 ✅, 0.36x→止损 ✅, TP/SL 未设→不触发 ✅
      - LIMIT 报价: `orderType=LIMIT`+`priceLimit` 返回 quoteId ✅
      - SELL 无持仓时: `-9000 exceeded available shares` = 报价参数正确, 只是无持仓可卖 ✅
      
      ## Paper 模拟 & 实时监控 (2026-08-12 新增, 绝不下单)
      
      > 预测交易无 test 模式, 用 `--paper` / `--paper-monitor` 先做真实报价模拟。
      > 完整盈利策略见 [prediction-market-strategy.md](prediction-market-strategy.md)。
      
      ### `--paper` 单次模拟
      ```bash
      python3 scripts/5minbtc_trader.py --paper [--paper-up-max 0.65] [--paper-down-max 0.55] \
        [--paper-fee 0.01] [--paper-push]
      ```
      流程: 结算已收盘注单 → 引擎信号 → 真实 ask → **价格门控** (ask≤胜率-费才记录) → 报告。
      状态文件 `~/bb-auto/5minbtc-paper.json`。参数:
      | 参数 | 默认 | 说明 |
      |---|---|---|
      | `--paper-up-max` | 0.65 | UP 入场价上限 (实测盈亏平衡 ~0.70, 留 margin) |
      | `--paper-down-max` | 0.55 | DOWN 入场价上限 (实测盈亏平衡 ~0.57) |
      | `--paper-fee` | 0.01 | 单笔手续费比例 |
      | `--paper-push` | off | 报告推 Telegram |
      | `--paper-min-conf` | 0 | 置信度门槛 (实测 confidence 反校准, 默认不过滤) |
      | `--paper-strength-gate` / `--paper-tb-filter` | off | 未经验证的过滤, 需单独开启 |
      
      ### `--paper-monitor` 实时监控
      ```bash
      python3 scripts/5minbtc_trader.py --paper-monitor --paper-up-max 0.65 \
        --paper-p-down 0.50 --paper-push [--paper-poll 20] \
        [--paper-push-every 60] [--paper-push-delta 5]
      ```
      每根 K 线: 信号触发→推送入场 P+机会判断(EV=p−P) → 盘中轮询实时盈亏% → 收盘推最终获利%。
      | 参数 | 默认 | 说明 |
      |---|---|---|
      | `--paper-p-up` / `--paper-p-down` | 0.74 / 0.57 | 该方向预估概率 p (今日实测 bull/bear 胜率) |
      | `--paper-poll` | 20 | 轮询秒数 |
      | `--paper-push-every` | 60 | 实时盈亏最少推送间隔秒 |
      | `--paper-push-delta` | 5 | 实时盈亏变化 ≥ 此百分比才推 |
      
      ### 信号判定差异 (paper 版)
      `_signal()` 已参数化: `min_conf` / `tb_filter` / `strength_gate` 均可关。
      **实测洞察**: 引擎 confidence 反校准 (conf<50 胜率 72% > conf 50-59 的 67%), 默认不设 conf 门槛;
      预测全在第 2 分钟 (progress~40%), bull 74% / bear 57%。
      
    • quant-knowledge-index.md 3.5 KB
      # Quant Knowledge Bank Index
      
      > 50 轮深度蒸馏(2026-05-23),14 份报告(R01–R14,约 80 KB)。
      >
      > ⚠️ **原始路径 `~/.hermes/profiles/cqo/quant-knowledge/` 已不存在**(2026-09-10 实测)。
      > **本 skill 的 `reports/` 是唯一存活副本**,也是权威位置;AGI-Super-Team 仓库同步同一份。
      > 下表文件名已与 `reports/` 实际文件核对一致(原先 R07/R10 两行写错了文件名)。
      
      | 文件 | 主题 | 对 5minbtc 的可用点 | 状态 |
      |------|------|-------------------|------|
      | R01-factor-theory.md | Alpha101, IC衰减, Barra | IC 衰减监控方法;LLM 因子挖掘需防拥挤度 | 未落地 |
      | R02-strategy-theory.md | OU过程, 协整, Kalman, OFI | Kalman 滤波动态参数 → 替代固定 EMA 权重 | 未落地 |
      | R03-crypto-quant-defi.md | 资金费率, 链上, MEV | 资金费率极值=过热信号;HODL Waves 长周期 | 未落地 |
      | R04-portfolio-theory.md | BL/HRP/Kelly | Kelly 分数下注 → 最优仓位大小框架 | 未落地 |
      | R05-ml-quant-trading.md | LightGBM, TFT, RL | TFT 多时间尺度注意力;Purged K-Fold 防泄露 | 未落地 |
      | R06-data-sources.md | Alt Data全景, 加密数据 | P0 免费源: Arkham(鲸鱼), Binance WS(OFI), Deribit(IV) | 部分(OFI) |
      | R07-frontier-research-2024-2026.md | LLM因子, AI Agent | AlphaAgent 开源可用;RFT 技术;拥挤度风险 | 未落地 |
      | R08-options-volatility.md | Greeks, 曲面, 隐含分布 | DVOL-RV spread → 波动率 regime 信号 | 未落地 |
      | R09-market-microstructure.md | LOB, OFI, Kyle, Almgren | **OFI 5min R²~15-25%**;Microprice 优于 mid | **✅ 已落地(v6.0)** |
      | R10-risk-management-frontier.md | CVaR, EVT, Regime, 压测 | HMM regime 检测 → regime-aware 仓位;GPD 尾部止损 | 未落地 |
      | R11-institutional-methodology.md | Simons, Citadel, Two Sigma | 信号仪表板 + 衰减检测 + 弱信号组合;DSR 防过拟合 | 未落地 |
      | R12-integration-blueprint.md | 知识图谱+升级蓝图 | 完整 5 阶段升级路径,P0→P2 优先级 | 路线图 |
      | R13-accuracy-optimization-plan.md | 178笔回盘+优化方案 | bull bias 根因分析;Phase1 修复 | ⚠️ 针对 v4.x,已过时 |
      | R14-engine-audit-report.md | 顶尖量化审查 | 4 个 Critical 缺陷(共线指标/过拟合/regime盲区/阈值硬编码) | 历史:v5.0 架构基础 |
      
      ## P0 升级优先级(来自 R12)—— 进度
      
      1. ✅ **OFI 微结构因子** —— **已落地**:v6.0 引擎方向由真 OFI 净流一票决定。
         ⚠️ 但落地的是 **taker 买卖失衡**(`2*(tb/v)−1`),**不是** R09 说的订单簿 OFI;
         且只覆盖 Binance 现货单一市场 —— 见 [pitfalls.md #19](pitfalls.md)
      2. ⬜ **免费数据源接入** —— Arkham(鲸鱼追踪) / Binance WS(LOB) / Deribit API(IV)
      3. ⬜ **信号仪表板** —— 监控所有因子 IC/衰减率(Simons 哲学核心实践)
      
      > **这份索引的价值 = 一份尚未做完的升级路线图**,不是历史存档。
      > P0#1 已兑现但打了折扣(见 pitfalls #19);P0#2/#3 与 R02/R04/R05/R10 仍是有效方向。
      > **例外**:R13 是针对 v4.x 的优化方案,其结论已被 v5.9 对抗审查取代,不要当作现行依据。
      
      ## Delegate Research Pattern(研究委派经验,仍有效)
      
      - **超时风险**:>35 次 web_search 的委派研究容易撞 600s 超时。缓解:拆成更小的批次,
        或直接用领域知识 + 已收集的搜索结果写报告。
      - **产出不完整**:委派有时只回搜索轨迹、不回成文报告。缓解:基于已收集的搜索上下文自行成文。
      
    • review-procedure.md 1.6 KB
      # 5minbtc 每日复盘流程
      
      ## 快速统计
      ```bash
      SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc
      python3 $SKILL_DIR/5minbtc-log.py stats
      ```
      
      ## 数据充足性预检
      `stats` 显示当天有效轮数 < 10 时 (健康 20:00-23:00 交易日应有 30-40 轮), 几乎总是 prediction cron 在当日大部分时段静默失败 (provider 欠费/熔断/fallback 链断) 后自恢复, 而非真实低活跃。
      
      **两条失效分支**:
      - 0 轮 / 当日无任何记录 → 先查守护进程, 不是 provider (pgrep, systemd unit, scheduler missed)
      - 1-9 轮 → provider 静默熔断 (原诊断路径)
      
      ## 按维度拆解统计
      ```bash
      python3 $SKILL_DIR/scripts/daily-review-stats.py [YYYY-MM-DD]  # 默认当天
      ```
      脚本内置数据质量处理: 按 candle 去重 (取后一条) + 剔除 vol_pct>200% glitch + 误差同号检测 + <10轮覆盖率告警。
      
      ## 日志数据质量检查 (3项必查)
      1. **bias 字段合法值**: 只允许 bull/bear/neutral。出现 weak/strong/medium = engine→log 映射写错
      2. **重复记录去重**: 同一根 K线出现两条 = cron 双触发或手动+自动双跑, 统计前按 candle_start 去重
      3. **vol_pct 异常值过滤**: vol_pct>200% 是未完成 K线 / 0 历史均量导致的 glitch, 统计时剔除
      
      ## 单位陷阱
      `5minbtc-log.jsonl` 的 `error_pct` 字段**已是百分比单位** (-0.32 表示 -0.32%, 不是 -0.0032)。手写统计脚本时不要再乘 100, 否则 MAE 变 10%+。
      
      ## 内联脚本 emoji 陷阱
      `python3 -c "..."` 含 emoji (✅❌⚠️🟢🟡🔴) 会触发 Hermes 安全扫描被拒。用 ASCII (OK/X/POS/NEG) 或 write_file 独立 .py 文件再执行。
      
    • scheduled-tasks.md 13.5 KB
      # 系统级定时任务设置教程
      
      > **给 agent 的操作手册**:如何在 macOS(launchd)/ cc-connect / hermes 上建立本 skill 的定时任务。
      > 本文覆盖 **2 个任务**(见 §1)与 **3 种实现路线**(见 §2),以及全部踩坑点(§5)。
      >
      > 环境实测(2026-09-10):本机 `uid=503`,系统时区 **Asia/Shanghai (CST +0800)**,
      > 与引擎内部 `CST = timezone(timedelta(hours=8))` **一致** → **所有 cron 表达式直接用本地时间写,不需要换算**。
      > 本机 `~/Library/LaunchAgents/` 现有 6 个 job **全是常驻型(KeepAlive)**,定时型无先例。
      
      ---
      
      ## 0. 先分清:常驻型 vs 定时型(**最容易搞错的地方**)
      
      | | **常驻型**(现有 6 个) | **定时型**(本文要建的) |
      |---|---|---|
      | 用途 | 7×24 持续跑(采集/监控/交易) | 到点跑一次就退出 |
      | 关键键 | `KeepAlive=true` + `RunAtLoad=true` | **`StartCalendarInterval`,且不要配 `KeepAlive`** |
      | 例子 | `ofi-feed`、`5minbtc-realtime`、`5minbtc-watch` | 每日战绩推送 |
      | 配错的后果 | — | 配了 `KeepAlive` → **任务一退出就被立刻重启,变成疯狂循环** |
      
      > ⚠️ **定时任务配 `KeepAlive` 是头号事故**。它意味着"只要进程不在了就拉起来",
      > 而定时任务的正常行为恰恰是"跑完退出" —— 两者语义直接冲突。
      > 定时型只需要 `StartCalendarInterval`(可加 `RunAtLoad=false`)。
      
      ---
      
      ## 1. 这 2 个任务是什么
      
      ### 任务 A:每 5 分钟「预测 + 推送」
      
      | 项 | 值 |
      |---|---|
      | hermes 机上的 job | `d8058223a1e0`「5minbtc v5.7 半K线策略」 |
      | cron 表达式 | `2,7,12,17,22,27,32,37,42,47,52,57 20-22 * * *`(仅 20–22 点,每小时 12 次) |
      | 干什么 | 跑完整 LLM 工作流(引擎 + 新闻 + 3 组 web_search)→ 按 `output-template.md` 出报告 → 推送 |
      
      > ⚠️ **在 Mac 上,这个任务大概率不需要建**。因为常驻的 `com.daniel.5minbtc-realtime`
      > 已经**每 5 秒**跑一次引擎并推预测快照,`--active-hours 20,21,22,23` 的时段过滤也已内建。
      > **两者的区别是**:realtime 的推送是**纯规则型**(不经过 LLM,直接格式化引擎输出);
      > 定时任务 A 是**LLM 工作流型**(会调 web_search + 生成分析文字)。
      > **先问清楚要哪一种,再动手** —— 只是要数据/快照的话,别建,会重复推送。
      
      ### 任务 B:每日「复盘 / 战绩」推送
      
      | 项 | 值 |
      |---|---|
      | hermes 机上的 job | `9b07cd139f70`「5minbtc 每日复盘 23:15」 |
      | cron 表达式 | `15 23 * * *` |
      | 干什么 | 推送当日战绩统计(或 LLM 复盘) |
      
      > ⚠️ **这个任务在 Mac 上"已经有了",只是藏在一个常驻进程里**:
      > `com.daniel.5minbtc-watch` 在主循环里检测**日期切换**,然后 `subprocess` 调
      > `5minbtc_day_stats.py --date <昨天> --push`(`5minbtc_watch.py` 约 L356)。
      >
      > **两件必须知道的事**:
      > 1. **`--mute` 不会静音每日推送** —— 它是 subprocess 直调,不走 watch 的 `push()` 包装器。
      >    所以生产环境的 `--mute` 只关事件推送,每日战绩照推。
      > 2. **`last_day` 边界 bug**:`last_day` 在**进程启动时**初始化为当天。若守护进程在 00:00
      >    之后才启动,当天**不会**推前一天战绩,要等下一个 00:00。当前 KeepAlive 常驻不重启所以碰不到 ——
      >    **但如果你以后改成定时重启 watch,每日推送会静默消失。**
      >
      > **若要建独立的定时任务 B,必须给 watch 加 `--no-daily-stats`**(改 plist 的
      > `ProgramArguments` 再加一个 `<string>--no-daily-stats</string>`),否则**每天推两次**。
      
      ---
      
      ## 2. 三种实现路线
      
      ### 路线 1:macOS launchd `StartCalendarInterval`(推荐,真正的系统级)
      
      **优点**:与常驻进程解耦、开机自启、可单独测试、有独立日志、系统原生。
      
      **步骤**:
      
      ```bash
      # ① 写 plist(模板见 §3)
      vi ~/Library/LaunchAgents/com.daniel.5minbtc-daily-report.plist
      
      # ② 语法校验(必做,plist 一个括号错就整个静默失败)
      plutil -lint ~/Library/LaunchAgents/com.daniel.5minbtc-daily-report.plist
      
      # ③ 确保日志目录存在(launchd 不会自动创建!)
      mkdir -p ~/bb-auto/logs
      
      # ④ 加载
      launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.daniel.5minbtc-daily-report.plist
      
      # ⑤ 确认已注册
      launchctl print gui/$(id -u)/com.daniel.5minbtc-daily-report | head -20
      
      # ⑥ 立即测试一次(不必等到点)—— 这是唯一可靠的验证方式
      launchctl kickstart -k gui/$(id -u)/com.daniel.5minbtc-daily-report
      
      # ⑦ 看结果
      tail -30 ~/bb-auto/logs/launchd-daily-report.out.log
      tail -30 ~/bb-auto/logs/launchd-daily-report.err.log
      
      # 回退
      launchctl bootout gui/$(id -u)/com.daniel.5minbtc-daily-report
      ```
      
      ### 路线 2:cc-connect cron(一条命令,能推到当前 Telegram 会话)
      
      **优点**:最快、能在群里收到结果、有 `/cron` 可查可停。
      **缺点**:依赖 cc-connect 服务存活。
      
      ```bash
      # 任务 B(shell 直跑,不走 LLM)
      cc-connect cron add --cron "15 23 * * *" \
        --exec "/usr/bin/python3 /Users/daniel/.claude/skills/5minbtc/scripts/5minbtc_day_stats.py --push" \
        --desc "5minbtc 每日战绩"
      
      # 任务 A(LLM 工作流型)
      cc-connect cron add --cron "2,7,12,17,22,27,32,37,42,47,52,57 20-22 * * *" \
        --prompt "按 5minbtc skill 的标准流程跑一次预测并按 output-template.md 推送报告" \
        --desc "5minbtc 5min 预测" \
        --session-mode new-per-run --timeout-mins 10
      
      # 管理
      cc-connect cron list
      cc-connect cron info <job-id>
      cc-connect cron exec <job-id>          # 立即跑一次
      cc-connect cron edit <job-id> enabled false   # 暂停(别删了重建)
      cc-connect cron del <job-id>
      ```
      
      > ⚠️ `--session-mode new-per-run` 让每次跑在**全新会话**里,避免上下文越滚越长。
      > 任务 A 建议用它;任务 B 是 shell 命令,不涉及会话。
      
      ### 路线 3:hermes cron(Linux 机)
      
      只有 hermes 机上才有的调度器(Mac 上 `which hermes` 为空)。用法见
      [cron-llm-provider-failure.md](cron-llm-provider-failure.md):
      `hermes cron update --job-id <id> --model <m> --provider <p>` / `hermes cron run --job-id <id>`。
      **注意**:这两个 job 的调度表达式与状态都记在该文件 §8,改动后要同步更新那张表。
      
      ---
      
      ## 3. plist 模板(定时型)
      
      ### 任务 B:每天 23:15 推**当天**战绩
      
      ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
      <dict>
          <key>Label</key>
          <string>com.daniel.5minbtc-daily-report</string>
      
          <key>ProgramArguments</key>
          <array>
              <string>/usr/bin/python3</string>
              <string>/Users/daniel/.claude/skills/5minbtc/scripts/5minbtc_day_stats.py</string>
              <string>--push</string>
          </array>
      
          <!-- 每天 23:15 (系统本地时间 = CST) -->
          <key>StartCalendarInterval</key>
          <dict>
              <key>Hour</key><integer>23</integer>
              <key>Minute</key><integer>15</integer>
          </dict>
      
          <!-- ⚠️ 定时任务不要配 KeepAlive -->
          <key>RunAtLoad</key>
          <false/>
      
          <key>ProcessType</key>
          <string>Background</string>
      
          <key>StandardOutPath</key>
          <string>/Users/daniel/bb-auto/logs/launchd-daily-report.out.log</string>
          <key>StandardErrorPath</key>
          <string>/Users/daniel/bb-auto/logs/launchd-daily-report.err.log</string>
      </dict>
      </plist>
      ```
      
      `day_stats.py` **不带 `--date` 时默认推「今日」**,所以 23:15 直接 `--push` 即可。
      
      ### 变体:00:05 推**前一天**
      
      launchd **不做命令替换**,所以要用 shell 包一层(注意 macOS 的 date 语法是 `-v-1d`):
      
      ```xml
          <key>ProgramArguments</key>
          <array>
              <string>/bin/bash</string>
              <string>-c</string>
              <string>/usr/bin/python3 /Users/daniel/.claude/skills/5minbtc/scripts/5minbtc_day_stats.py --date $(date -v-1d +%F) --push</string>
          </array>
          <key>StartCalendarInterval</key>
          <dict><key>Hour</key><integer>0</integer><key>Minute</key><integer>5</integer></dict>
      ```
      
      注意事项:
      
      - macOS 的 `date` 用 `-v-1d` 取前一天(GNU 的 `-d yesterday` 在 macOS 上**不可用**)。
      - plist 里 `%` 是**普通字符**,**不要**写成 `%%` —— 写成 `%%F` 会让 `date` 输出字面量 `%F`,
        结果 `--date %F` 查不到任何记录。这是本条变体最容易踩的坑。
      - 加载后务必 `kickstart` 跑一次,确认日志里日期正确、不是 `%F`。
      
      ### 一天跑多次
      
      `StartCalendarInterval` 接受**数组**:
      
      ```xml
      <key>StartCalendarInterval</key>
      <array>
          <dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
          <dict><key>Hour</key><integer>15</integer><key>Minute</key><integer>0</integer></dict>
          <dict><key>Hour</key><integer>21</integer><key>Minute</key><integer>0</integer></dict>
      </array>
      ```
      
      ---
      
      ## 4. 只跑一次 / 延迟一次(不要用 cron 表达这个)
      
      - **cc-connect**:用 `cc-connect timer add --delay 2h --prompt "..."`(一次性,触发后自动归档)。
        **不要**用 cron 表达"只跑一次"——`30 14 10 9 *` 意思是"每年 9 月 10 日 14:30",不是"今年这一次"。
      - **launchd**:定时型 launchd 本身就是**长期重复**语义,没有"只跑一次"的原生表达。
        一次性任务建议用 `cc-connect timer`,或临时 plist + 跑完 `bootout`。
      
      ---
      
      ## 5. 坑清单(每条都实际踩过或实测过)
      
      | # | 坑 | 说明与对策 |
      |---|----|-----------|
      | 1 | **定时型配了 `KeepAlive`** | 任务退出即被重启 → 疯狂循环。定时型**只配 `StartCalendarInterval`** |
      | 2 | **launchd 不展开 `~`、不读 shell rc、PATH 极简** | 所有路径写**绝对路径**;python 用 `/usr/bin/python3`(引擎零第三方依赖,别用 brew/venv 的 python) |
      | 3 | **日志目录不存在 → 静默失败** | launchd **不创建** `StandardOutPath`/`StandardErrorPath` 的父目录 → 先 `mkdir -p ~/bb-auto/logs` |
      | 4 | **plist 语法错 → 静默不加载** | 每次改完必跑 `plutil -lint <plist>` |
      | 5 | **机器睡眠错过时间点** | launchd 行为:**唤醒后补跑一次**(不像传统 cron 直接丢弃)。但可能延迟数分钟,别把它当精确时钟 |
      | 6 | **重复推送** | watch 内置每日推送 + 独立定时任务 = 每天两次 → 给 watch 加 `--no-daily-stats` |
      | 7 | **watch 的 `last_day` 启动边界** | 若改成定时重启 watch,每日推送会静默消失(见 §1 任务 B) |
      | 8 | **`--mute` 不静音每日推送** | 它是 subprocess 直调,不走 `push()` 包装器。别以为 `--mute` 就什么都不会推 |
      | 9 | **引擎/脚本改名后路径失效** | 2026-09-10 引擎已改名 `-v5.7.py` → `-v6.0.py`。任何 plist/脚本里的旧路径都要同步;launchd 不会报错,只会静默失败 |
      | 10 | **静默失败最难查** | `5minbtc_realtime.py` 的 `run_engine()` 是 `except: return None` —— 路径错了**不报错、只不下单**。判断是否真在跑:`ps aux \| grep <脚本名>` 看子进程是否反复出现 |
      | 11 | **新任务日志会一直长** | 定时任务每次追加日志 → 用 [setup-from-scratch.md §7](setup-from-scratch.md#7-日志轮转) 的月度轮转脚本一起归档 |
      | 12 | **cron 表达式语义** | 5 段是 `分 时 日 月 周`。`*/5 * * * *` 才是"每 5 分钟";`5 * * * *` 是"每小时的第 5 分钟" |
      | 13 | **cc-connect cron ≠ 本会话的 CronCreate** | `cc-connect cron` 是持久化的 CLI 调度;会话内 CronCreate 的 job 随会话结束消失。做长期任务用前者 |
      
      ---
      
      ## 6. 验证清单(建完必须逐项过)
      
      ```bash
      L=com.daniel.5minbtc-daily-report
      
      # 1. plist 语法
      plutil -lint ~/Library/LaunchAgents/$L.plist
      
      # 2. 已注册且状态正常
      launchctl print gui/$(id -u)/$L | grep -E "state|path|program"
      
      # 3. 立即跑一次(不等时间点)
      launchctl kickstart -k gui/$(id -u)/$L
      
      # 4. 看输出(任务 B 应出现"📈 5minbtc 预测战绩")
      tail -20 ~/bb-auto/logs/launchd-daily-report.out.log
      tail -20 ~/bb-auto/logs/launchd-daily-report.err.log   # 应为空
      
      # 5. 确认 Telegram 真的收到
      
      # 6. 隔天回来确认它自己按时跑过(看日志时间戳)
      ls -la ~/bb-auto/logs/launchd-daily-report.out.log
      ```
      
      **判据**:`kickstart` 能跑通 **≠** 到点会自动跑。第 6 步(隔天看时间戳)才是真正的验收。
      
      ---
      
      ## 7. 给 agent 的执行顺序
      
      1. **先确认要哪个任务**(§1)。任务 A 在 Mac 上可能已被 realtime 覆盖 → **问清楚再动手**。
      2. **选路线**:要系统级 / 与常驻解耦 → launchd;要最快且能进 Telegram → cc-connect cron。
      3. **建之前先查重复**:`launchctl list | grep daniel` + `cc-connect cron list`,
         确认没有同类任务;建任务 B 前检查 watch 是否已带 `--no-daily-stats`。
      4. **写配置 → `plutil -lint` → `bootstrap` → `kickstart` 测一次 → 看日志 → 确认收到**。
      5. **隔天验收**(§6 第 6 步)。
      6. **更新文档**:把新任务登记到本文件 §1 的表里,并在
         [setup-from-scratch.md §4](setup-from-scratch.md#4-launchd-常驻服务6-个) 的 job 表补一行。
      
      ---
      
      ## 8. 相关文档
      
      - [setup-from-scratch.md](setup-from-scratch.md) — 从零搭建(依赖/目录/密钥/常驻服务/日志轮转)
      - [cron-llm-provider-failure.md](cron-llm-provider-failure.md) — hermes cron 的 provider 失效诊断;§8 是 job 列表
      - [output-template.md](output-template.md) — 推送内容格式(任务 A 的输出规范)
      - [pitfalls.md](pitfalls.md) — #7 Cron Job 版本同步 / #18 K线开局概率不可信
      
    • setup-from-scratch.md 8.9 KB
      # 从零搭建 5minbtc
      
      > 目标:在一台干净的 macOS 上,把 5minbtc 的引擎 + 6 个常驻服务跑起来。
      > **验证状态**:本文依据当前**正在运行**的配置转录(2026-09-10,已核对 6 个 plist 的实际参数与进程)。
      > 尚未在干净机器上实测,首次照做时请以「§5 验证清单」逐项确认。
      
      ## 1. 前置条件
      
      | 项 | 要求 | 说明 |
      |----|------|------|
      | 系统 | macOS | 用 launchd 托管,Linux 需改 systemd |
      | Python | 系统自带 `/usr/bin/python3` | **零第三方依赖**(`dependencies-stdlib only`)。不要用 brew/venv 的 python,plist 里写死了 `/usr/bin/python3` |
      | 网络 | 能访问 `data-api.binance.vision` | `fapi.binance.com` 在本机返回 **451**(地区限制),见 §6 |
      | 可选 | 币安 API key/secret | 仅预测市场下单/查价需要;纯 paper 也建议配(查盘口要签名) |
      
      ## 2. 目录布局
      
      ```
      ~/.claude/skills/5minbtc/          # skill 本体(引擎 + 脚本 + 文档)
      ├── 5minbtc-engine-v6.0.py         # 主引擎
      ├── 5minbtc-log.py                 # 日志/settle
      ├── 5minbtc-news.py                # CoinDesk RSS
      ├── scripts/                       # 监控/交易/采集脚本
      ├── references/                    # 专题文档(含本文件)
      └── logs/                          # 运行时日志(见 §7)
      
      ~/bb-auto/                          # 运行时状态(不在 skill 内)
      ├── prediction.env                 # 密钥(600 权限)
      ├── ofi.json                       # OFI 采集缓存
      ├── prediction-ws.json             # 预测市场 WS 价缓存
      ├── 5minbtc-paper.json             # paper 台账
      ├── paper_monitor.sh               # trader 的守护壳
      ├── bb_paper.sh                    # bb-scalper 的守护壳
      └── logs/                          # launchd stdout/stderr
      
      ~/AGI-Super-Team/                   # 共享仓库(git,分支 main)
      ```
      
      ## 3. 密钥与环境
      
      ```bash
      mkdir -p ~/bb-auto/logs
      cat > ~/bb-auto/prediction.env <<'EOF'
      BINANCE_API_KEY=xxx
      BINANCE_API_SECRET=xxx
      EOF
      chmod 600 ~/bb-auto/prediction.env
      ```
      
      - 字段名以 skill 内 `.env.example` 为准(`scripts/*.py` 用 `load_env()` 读它)。
      - **引擎本身不读环境变量**(`.env.example` 里有说明)。
      - Telegram 推送不需要单独配:统一走 `scripts/telegram_push.py`,从 `~/.cc-connect/config.toml` 读 bot token + chat_id。
      
      ## 4. launchd 常驻服务(6 个)
      
      > 📌 本节是**常驻型**(`KeepAlive`,7×24 跑)。
      > 要建**定时型**任务(如每日战绩推送)请看 [scheduled-tasks.md](scheduled-tasks.md) ——
      > **定时型绝不能配 `KeepAlive`**,两者语义冲突。
      
      | Label | 执行体 | 作用 |
      |-------|--------|------|
      | `com.daniel.ofi-feed` | `python3 .../scripts/ofi_feed.py` | 真订单流采集 → `~/bb-auto/ofi.json` |
      | `com.daniel.prediction-ws-feed` | `python3 .../scripts/prediction_ws_feed.py` | 预测市场 WS 实时价(<200ms) |
      | `com.daniel.5minbtc-realtime` | `python3 .../scripts/5minbtc_realtime.py --conf 70 --refresh 5 --active-hours 20,21,22,23 --push` | 5s 刷新,推预测快照 + EV 下单 |
      | `com.daniel.5minbtc-watch` | `python3 .../scripts/5minbtc_watch.py --mute` | 事件驱动推送 + 预测记录/结算 |
      | `com.daniel.5minbtc-paper` | `/bin/bash ~/bb-auto/paper_monitor.sh` | 跑 `5minbtc_trader.py --paper-monitor` |
      | `com.daniel.bb-paper` | `/bin/bash ~/bb-auto/bb_paper.sh` | bb-scalper 模拟盘(另一个 skill) |
      
      ### plist 模板
      
      `ThrottleInterval` 防止崩溃循环打爆 CPU;`KeepAlive` 保证崩溃自启。把 `__LABEL__` / `__ARGS__` 换掉即可:
      
      ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
      <dict>
          <key>Label</key>            <string>__LABEL__</string>
          <key>ProgramArguments</key>
          <array>
              <string>/usr/bin/python3</string>
              <string>__SCRIPT__</string>
              <!-- 其余参数依次一行一个 <string> -->
          </array>
          <key>RunAtLoad</key>        <true/>
          <key>KeepAlive</key>        <true/>
          <key>ProcessType</key>      <string>Background</string>
          <key>ThrottleInterval</key> <integer>5</integer>
          <key>StandardOutPath</key>  <string>/Users/<你>/bb-auto/logs/launchd-__TAG__.out.log</string>
          <key>StandardErrorPath</key><string>/Users/<你>/bb-auto/logs/launchd-__TAG__.err.log</string>
      </dict>
      </plist>
      ```
      
      ### 加载
      
      ```bash
      cp com.daniel.xxx.plist ~/Library/LaunchAgents/
      launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.daniel.xxx.plist
      ```
      
      ### 管理命令
      
      ```bash
      launchctl list | grep daniel                                  # 总览
      launchctl kickstart -k gui/$(id -u)/com.daniel.5minbtc-watch  # 重启(改代码后用 -k)
      launchctl bootout gui/$(id -u)/com.daniel.5minbtc-watch       # 停止
      ```
      
      > ⚠️ **改了引擎文件名必须重启**:`ENGINE` 是模块级常量,进程不会自动感知新路径;
      > 且 `run_engine()` 是 `except: return None` **静默吞异常** —— 路径错了不会报错,只会安静地不下单。
      > 判断是否在跑:`ps aux | grep 5minbtc-engine` 应能看到子进程反复出现。
      
      ## 5. 验证清单
      
      ```bash
      S=~/.claude/skills/5minbtc
      
      # 1. 引擎能跑出 JSON,且 version 正确
      $S/5minbtc-engine-v6.0.py | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['version'], d['prediction']['bias'])"
      # 期望: 6.0.0 <bull|bear>
      
      # 2. 方向来自 OFI(bias 应与 ofi.direction 一致)
      $S/5minbtc-engine-v6.0.py | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['prediction']['bias'], d['ofi']['direction'], d['ofi']['ofi_n'])"
      
      # 3. 6 个服务都在
      launchctl list | grep daniel
      
      # 4. 无引擎路径错误
      grep -il "No such file\|FileNotFound" ~/bb-auto/logs/launchd-*.log || echo "✅ 无路径错误"
      
      # 5. OFI 缓存新鲜(引擎要求 <30s,否则 WS 分量降级)
      python3 -c "import json,time; d=json.load(open('$HOME/bb-auto/ofi.json')); print('age(s)=', round(time.time()-d['ts']/1000,1))"
      ```
      
      ## 6. 网络/地区修复(geo)
      
      - `fapi.binance.com` REST 在本机 **451**(地区限制)。已改用:
        - REST 行情 → **`data-api.binance.vision`**(引擎内默认)
        - WS → `fstream.binance.com`
      - 详见 [binance-api-geo.md](binance-api-geo.md) 与 [binance-endpoint-flapping.md](binance-endpoint-flapping.md)。
      - ⚠️ `fstream` 的 `kline` / `aggTrade` 流在本网络**不推送**,只有 `trade` / `bookTicker` 流可用 —— 这正是 `ofi_feed.py` 用 `trade+bookTicker` 组合的原因(`@trade` 自带原生 `m` 主动买卖标记)。
      - 高延迟网络下的 SSL 超时处理见 [high-latency-network-handling.md](high-latency-network-handling.md)。
      
      ## 7. 日志轮转
      
      `logs/` 采用「当月 live + 历史按月压缩」;压缩归档入库,live 不入库(见 [archive.md](archive.md#日志归档))。
      轮转脚本(幂等,可重复跑):
      
      ```bash
      cd ~/.claude/skills/5minbtc && python3 - <<'PY'
      import gzip, json, os, tempfile
      from collections import defaultdict
      from pathlib import Path
      
      LIVE, ARCH = Path("logs/5minbtc-log.jsonl"), Path("logs/archive")
      ARCH.mkdir(parents=True, exist_ok=True)
      
      by_month, bad = defaultdict(list), 0
      for line in LIVE.read_text(encoding="utf-8").splitlines():
          if not line.strip():
              continue
          try:
              by_month[json.loads(line).get("ts", "")[:7]].append(line)
          except Exception:
              bad += 1
      
      CURRENT = max(by_month)
      for month, lines in sorted(by_month.items()):
          if month == CURRENT:
              continue
          out = ARCH / f"5minbtc-log.{month}.jsonl.gz"
          with gzip.open(out, "wt", encoding="utf-8") as f:
              f.write("\n".join(lines) + "\n")
          print(f"归档 {month}: {len(lines)} 行 → {out}")
      
      keep = by_month[CURRENT]
      tmp = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, dir="logs")
      tmp.write("\n".join(keep) + "\n"); tmp.close()
      os.replace(tmp.name, LIVE)          # 原子替换;写日志的是短命子进程(O_APPEND),不会踩坏
      print(f"live 保留 {CURRENT}: {len(keep)} 行 | 解析失败 {bad} 行")
      PY
      ```
      
      轮转后**校验行数守恒**(归档行数 + live 行数 == 轮转前总行数)再提交。
      
      ## 8. 常见故障
      
      | 现象 | 原因 | 处理 |
      |------|------|------|
      | 引擎跑但一直不下单,日志无报错 | `ENGINE` 路径失效(静默吞异常) | `ps aux \| grep 5minbtc-engine` 看子进程是否在起 |
      | 改了引擎但不生效 | 守护进程持旧常量 | `launchctl kickstart -k` 重启 |
      | 预测市场价拿不到 | WS 未连上或签名失效 | 查 `launchd-ws-feed.err.log`;确认 key/secret |
      | 引擎卡 30s+ | Binance SSL 超时 | 见 [high-latency-network-handling.md](high-latency-network-handling.md) |
      | `OFI feed_fresh=false` | `ofi.json` 超过 30s 未更新 | 查 `com.daniel.ofi-feed` 是否存活 |
      | cron/launchd 报 451 | geo 限制 | 改用 `data-api.binance.vision` |
      
      ## 9. 同步到共享仓库
      
      见 [sync-procedure.md](sync-procedure.md)(含路径映射与 `-c` 校验说明)。
      
    • skill-organization.md 3.6 KB
      # Skill 项目文件结构 (5minbtc 整理后模式, 2026-07-05)
      
      > 从 5minbtc skill 整理 (logs/reviews 归档 + SKILL.md 减负) 提炼的可复用模式。
      > 适用于任何有运行时产物 + 复盘产出 + 长文档的 skill。
      
      ## 核心原则
      1. **SKILL.md = INDEX** (~150-200 行) — 触发 + 何时用 + quick-start + 铁律 + 关键规则 + 性能 + references 索引
      2. **references/ = 详情** — 每个 ref 一个专题, SKILL.md 用一行 + 链接引用
      3. **产出物 = 专用子目录** — 日志/复盘/报告各归各位, 根目录只放源文件
      
      ## 推荐目录结构
      ```
      skill-name/
      ├── SKILL.md                      # INDEX, ~150-200 行
      ├── *.py                          # 源文件 (与 SKILL.md 同级, 易找)
      ├── references/                   # 专题详细文档
      │   ├── lessons.md                # 教训集
      │   ├── pitfalls.md               # 陷阱集
      │   ├── changelog.md              # 版本变更
      │   ├── architecture.md           # 架构说明
      │   ├── execution.md              # 执行步骤
      │   ├── review-procedure.md       # 复盘流程
      │   ├── cron-setup.md             # Cron/调度配置
      │   └── <topic>.md                # 其他专题
      ├── logs/                         # 运行时日志 (rsync 排除)
      │   ├── current.jsonl             # 当前活跃
      │   └── <archive>/                # 归档 (按需)
      ├── reviews/                      # 复盘产出 (按月归档)
      │   └── YYYY-MM/
      ├── backtest/ 或 data/            # 数据/回测产物
      ├── scripts/                      # 工具脚本
      └── reports/                      # 蒸馏报告 (R01-R14 之类)
      ```
      
      ## INDEX 模式 SKILL.md 结构
      1. 标题 + 版本号 + 一句话描述
      2. 触发词
      3. 何时使用 (含边界场景 + 错误用法)
      4. 快速开始 (3-5 行命令)
      5. 架构 (1 行/组件)
      6. 铁律 (3-7 条)
      7. 关键规则 (裁决规则, 5-7 条精简)
      8. 性能快照 (1 行最新数据)
      9. references 索引 (10-25 个, 1 行/项)
      10. 文件结构 (树状图)
      11. 同步/部署说明 (1-3 行)
      
      ## 整理 SOP (发现 SKILL.md > 250 行时触发)
      1. 扫描, 找出从未被调用的函数/从未被引用的文件
      2. 提取冗长章节 (changelog/lessons/pitfalls/执行步骤) → references/<topic>.md
      3. 按月归档 review-*.md 到 reviews/YYYY-MM/
      4. 按时间归档 jsonl/gz 到 logs/archive/
      5. 删除 __pycache__/ 和明确无用的源文件
      6. log.py 等写文件脚本, 加 LOG_DIR = logs/ 子目录, 自动 makedirs
      7. 验证: smoke test 每个 .py, 确认 cron 仍可用
      8. 更新 SKILL.md 反映新结构 (压缩到 ~150-200 行)
      9. 更新 sync procedure 反映新 exclude (加 logs/, reviews/)
      
      ## 同步到 git/共享仓库
      rsync exclude 必须包含:
      - 运行时产物 (data/, __pycache__/)
      - 归档产物 (logs/, reviews/)
      - 大文件 (*.jsonl, *.gz)
      - 密钥 (.env, *credentials*)
      
      ## 常见错误
      - **保留所有 .py 在根目录** — 引擎/日志/新闻源都进 src/ 会改 cron 路径, 改造成本高。建议根目录保留 .py + scripts/ 工具脚本。
      - **SKILL.md 写超过 300 行** — 触发"用户说 SKILL 太长"反弹信号
      - **references/ 文件超过 30 个** — 考虑合并相似专题 (如 session-*.md 可合并为 sessions/)
      - **忘记在 references 删除时同步更新 SKILL.md 索引** — 链接断了用户找不到
      
      ## 验证清单
      - [ ] SKILL.md 行数 < 250
      - [ ] 根目录只放源文件 (.py) + SKILL.md + 子目录
      - [ ] 所有 .py 都有 smoke test
      - [ ] references/ 文件命名一致 (kebab-case, 无日期后缀除非是 session)
      - [ ] sync procedure 列出所有 exclude
      
    • strategy-adversarial-review.md 18.9 KB
      # 5minbtc 方向预测策略 — 对抗式审查报告
      
      > 这份报告是 5 位对抗审查专家(因子有效性、校准与信号生成、市场微观结构、缺失因素、执行时序)的结论综合。所有数字都来自你们自己的回测、日志和 paper 交易记录,不是谁的想象。说人话:**
      > 你们其实没在做"预测",你们在做"确认"——而且大部分确认结果已经被市场提前定价了。真正的钱不在方向准不准,在"市场定价和真实概率之间的缝隙"里。**
      
      ---
      
      ## 一、第一性原理: 我们在赌什么
      
      先把这场赌局拆到底。
      
      **这个预测市场的结算规则是:K线收盘时,close 比 open 高 = UP,低 = DOWN。** 我们的任务是:在 5 分钟走完前 40% 左右(第 2 分钟),用已经实现的路径,估计"剩下 60% 时间里净涨/净跌的条件概率 P"。然后去买一个价格已经定好的 token。
      
      这里有三个不能混为一谈的量:
      
      1. **方向准确率 p**:我们猜得准不准——这是你们引擎 13 个因子在算的东西。
      2. **市场价格 P**:UP/DOWN token 的盘口价,由做市商和所有参与者定出来的。它本质上就是市场对 P(close>open) 的条件概率估计。
      3. **真实盈亏 EV = p − P − 点差 − 手续费**。
      
      一句话:**方向准确率不是 edge,EV 才是。** 如果你的 p 是 64%,但买 UP 的成交价是 0.65,那你的期望是 64−65 = −1,亏钱。只有 p 高、而且买得便宜,才赚钱。
      
      然后是最关键的第一性事实:**5 分钟级别的"方向预测",其信息结构只有三部分——随机游走 + 微弱的均值回归 + 盘内延续。** 其中唯一被反复验证有统计基础的是"盘内延续":已经形成的方向,倾向于延续到收盘。引擎自己的回测精确地证明了这一点——准确率随 K 线流逝的时间单调上升:
      
      > 前 1 根 1 分钟 K 线 → 56.7%,前 2 根 → 61.7%,前 3 根 → 65.8%,前 4 根 → 69.5%,5 分钟走完 → 76.1%。
      
      **越晚看越准。** 这不是预测力,这是"看着 K 线走完了大部分再下结论"。任何一条"当前方向"规则都能拿到这个数字——它已经写进行情里了,不是 alpha。
      
      而做市商几乎把这个延续效应完整地定价进了 token 价。所以你们真正的可套利空间,只剩下"市场还锚定在旧价位、延续效应还没被完全定价"的那一小段窗口——这就是为什么 paper 交易里只有 UP ask 在 0.40~0.50 区间才赚钱(实测 60% 胜率、+$1.39),其他价位全部无利可图甚至亏钱。
      
      **结论:这个策略本质在赌的是"错价",不是"方向"。** 五个专家里四个用了同一个公式收尾:**EV = p − P − 成本 > 0 才下单,否则一律跳过。**
      
      ---
      
      ## 二、现状胜率的真相
      
      ### 1. 你们引以为傲的"74%"是个幻影
      
      - v5.7 回测的 71.2% 在文件里**自己标注了"含前视偏差,已知"**。
      - 去掉前视后,v5.8 前 2 根 1 分钟只有 **61.4%**。
      - 回测准确率随流逝分钟数单调上升(56.7→76.1%),这就是前视污染最硬的证据——它在"看完K线再确认方向"。
      - 实盘 bull 63.9% 和零前视 61.4% 几乎重合,说明**实盘其实没跑输,是回测数字自己虚高了**。
      
      ### 2. 13 个"正交因子"既不正交,也基本没有预测力
      
      - 13 个因子里有 11 个公平回测只有 **47–49%**(等于猜硬币),其中 momentum 48.1% **反向**、rsi 47.3% **反向**(顺势在 5 分钟尺度亏钱)。
      - 唯一有独立 alpha 的是 **volume 58.1%**——但它的权重只有 0.3。
      - 反过来,权重最高的 half_body(1.2)因子本身 48.8% 无预测力,权重第二的 momentum(1.0)是反向因子。**权重分配和实测证据完全倒挂。**
      - "13 正交"在数学上不可能成立:趋势组(decel/momentum/meanrev/position/rsi)全部从同一串 closes 导出,两两高度相关;meanrev 和 position 是同一个"价格位置"信号的近似单调变换。加权平均它们等于把同一份信息算好几遍再稀释。
      - 还有 3 个因子(taker_buy 0.7 / imbalance 0.8 / microprice 0.6,名义权重合计 25%)**从未被任何回测验证过**——公平回测里订单簿数据是 None,恒为 0;实盘日志里 imbalance/microprice 始终是 0.000(死代码)。更致命的是:当前引擎 `direction_rule_v5` 返回 7 元组,而两个回测脚本只解包 6 个——**当前引擎根本跑不了自带的回测**,所谓"v5.8 回测 61–70% 有真实 edge"测的是旧引擎快照。
      
      ### 3. 真正的胜率长什么样(逐项剔掉水分)
      
      | 口径 | 数字 | 说明 |
      |---|---|---|
      | 总准确率(含 neutral) | ~64–76% | **虚高**,因为 neutral 142 笔全部恒记"对" |
      | non-neutral 方向胜率 | **57.7%** (101/175) | 这才是真实交易方向的基准 |
      | bull | **63.9%** (97笔,95%CI 54–73%) | 唯一统计显著区别于硬币的方向 |
      | bear | **50.0%** (78笔) | 精确等于随机,是硬币 |
      | paper 全样本 UP 成交 | **44%** (+$0.24) | 价格已经把方向信息定价进去了 |
      | paper 甜区 UP ask 0.40–0.50 | **60%** (+$1.39) | 唯一正 EV 区间 |
      | paper DOWN | **22%** (−$4.78~−$5.09) | 确定性亏损,趋势日 7 次实盘链确认 |
      | 价格区间预测 in_range | **10.7%** | 完全无效,该下线 |
      
      注意那组吓人的对比:**方向 raw 胜率 63.9%,但真实成交胜率只有 44%。** 中间差的就是价格——能按 ask≤门槛成交,恰恰说明市场没有低估你(逆向选择)。这是"能预测方向"和"能赚钱"是两回事的量化证明。
      
      ### 4. 置信度(Platt Scaling)是死的
      
      - 输出被压扁在 **35–57**(设计 35–85),均值 45.5。原因:组合 raw 分被数学限制在 ±3,过 sigmoid 最多 ±34,再被 HIGH_VOL×0.45、TREND 逆势×0.25、bull×0.92 层层压扁。
      - 实测校准**非单调反校准**:conf 40–45 → 62.5%,45–50 → 52.9%(占 58% 样本的大桶反而最差!),50–55 → 65.5%,55–60 → 63.6%。
      - 所以任何单调 sigmoid 结构上都表示不了这条曲线,而且"越高越准"的假设根本不成立。**用 confidence 当门槛,只会砍掉正样本还反向分配资金。**
      
      ### 5. 收益天花板:方向 edge 真实但极薄
      
      bull 64% 确实统计显著(二项 SE≈4.9%,CI 排除 50%),且与 7 个月月度 60.8–63.0% 的稳定区间、零前视回测 61.4% 三方吻合。**但它是市场微观结构的基础概率,不是因子 alpha。** 而且样本只有约 2 天单一行情,价格门控后 paper 净收益仅 +$1.39——**经济意义接近零。** 方向 edge 真实存在 ≠ 能赚钱。
      
      ---
      
      ## 三、执行计算步骤(现状 vs 优化后)
      
      ### 现状(现在的引擎在做什么)
      
      1. **采数据**:klines + 深度快照 + FNG + taker,4 路并行。
      2. **算 13 因子**:closes 派生的动量/RSI/均值回归/位置/反转/放量/疲劳等 + half_body 延续 + volume + 订单簿(taker_buy/imbalance/microprice 多数时候恒 0)。
      3. **加权平均 → raw 分**(数学上被钳在 ±3)。
      4. **sigmoid ×3 → 强度分**。
      5. **补丁叠补丁**:regime 惩罚、bull×0.92、FNG 衰减、冲突降权、ATR spike 减半、ATR 乘数 ×0.55——参数数量远超可验证信息。
      6. **Platt sigmoid(midpoint=15, steepness=0.10)→ confidence 35–57**——这个"置信度"是手设参数,从没拟合过真实胜率。
      7. **中性带判定**:|score|<1 → neutral(45% 的 K 线都躺在这里)。
      8. **方向判定** → 7 元组 → trader 以 MIN_CONF=55、入场价 ask≤0.65(UP)/≤0.50(DOWN)下单。
      
      ### 优化后(应该怎么做)
      
      1. **采数据 + 全量落地**:每根 K 线在第 2/3/4 分钟各记一次——方向(half-K 已形成方向)、volume、**该时刻真实的 UP/DOWN ask**、regime、时段、最终结算。这是所有后续步骤的地基,现在日志里连 score/regime/factors 都没记。
      2. **估计 p(经验条件胜率)**:不用因子堆叠,直接用已结算日志,按(方向, 价位桶)分箱统计实证延续概率。例:P(UP 结算 | 第2分钟已收绿,ask∈0.40–0.50)。
      3. **算 EV**:`EV = p(该桶) − ask − 半价差 − 手续费`。
      4. **硬门控**:
         - 只做 **bias=bull 且 UP ask ∈ [0.40, 0.50]** 才入场;
         - **永不交易 DOWN/bear**(有数据支撑的硬禁用);
         - EV < 0.05 一律跳过;
         - 首次入场锁定方向,后续采样方向翻转 = **跳过,绝不反手**(显式抑制追涨杀跌);
         - 第 4 分钟 ask 已 >0.50 → 放弃该 K 线,不因"方向更准"而追高。
      5. **仓位 1/4 凯利**:`f* = (p−P)/(1−P)`,P=0.50 时 f*≈7%,P=0.60 时 f*≈2.5%。禁止 ask≥0.60 还平注 1U/10U(那是 4 倍超配)。
      6. **校准**:用保序回归(isotonic regression)在已结算日志上拟合经验可靠性表 P(win|分箱),confidence = 平滑后的经验胜率,不再是参数映射。threshold 设在 P(win)≈50% 处。
      7. **回测口径对齐**:目标改为 close vs open(与结算一致),重跑全量;price-range 输出直接下线。
      
      ---
      
      ## 四、该保留 / 该删除 / 缺失
      
      ### 该保留(有证据的)
      
      - **盘内延续作为主信号**:"当前已形成的半K线方向"按每 progress 桶的原始延续概率建模(61–69%),权重 ≥1.0。注意是保留这个**机制**,不是保留 tanh(body/ATR) 的退化编码。
      - **volume_conditional**:唯一验证过的独立 alpha(58.1%),权重从 0.3 提到 ≥0.8。
      - **meanrev**:唯一方向为正的价格因子(51.7%),保留但低权重(0.3),且与 position 合并为一个信号。
      - **EV = p − P − 成本 框架**和**价格门控**(只做 UP 甜区)。
      - **P_up/P_down token 价**:基础设施已建好(<200ms WS),现在只差接进引擎当校准/门控维度。
      - **ATR 乘数 ×0.55** 作为触发机制可以保留,但**不作为预测力来源**。
      - **bull 方向的偏差识别**——但要从冻结的先验改为滚动窗口实时估计的方向校准偏移。
      
      ### 该删除(无证据 / 反向 / 死代码)
      
      - **momentum(1.0)、rsi(0.4)**:回测反向 47–48%,权重清零或翻转符号。
      - **decel、position、fatigue、v_reversal、vol_breakout**(合计 37% 权重):全部实测 48–50% 噪声,且两两重叠,清零。
      - **taker_buy(0.7)、imbalance(0.8)、microprice(0.6)** 的权重:未验证之前全部置 0。当前多数时候本来就是死代码。
      - **Platt 置信度作为交易门槛**(MIN_CONF=55):反校准、砍样本、反向分配资金,从一切闸门移除。
      - **bull×0.92 冻结惩罚**:前提(bear>bull)已被当前数据推翻,它现在压制唯一真 edge。
      - **HIGH_VOL 三重惩罚**(score×0.45 + conf×0.85 + ATR-spike 减半):三层去掉一层;且 ATR-spike 减半可能正在扔掉最准的信号(实测低 conf 高 vol 桶胜率 62.5%)。
      - **价格区间预测**:命中率 10.7%,无效输出,下线。
      - **neutral 计入头条胜率**:45% 的 K 线恒记"对",虚高统计口径;以后只报 non-neutral。
      - **bear/DOWN 侧交易**:硬币 + 确定性亏损,直接砍掉。
      - **"13 正交因子加权"和**:本质是噪声堆叠,收敛为 2–3 个有证据的信号。
      - **FNG/冲突/ATR-spike 等 post-hoc 补丁的继续叠加**:冻结新增,改为留出法验证。
      
      ### 缺失的因素(按性价比排序)
      
      1. **P_up/P_down 作为门控**(P0,零新增成本):盈利机制就是 EV=p−P,paper 已证明 0.45–0.50 甜区 +$1.68 vs >0.55 死亡区 −$2.32。
      2. **跨资产广度 / 发散因子**(P0):ETH/SOL 5 分钟动量,Binance 免费 API。趋势日(跨资产同向)强制压制 bear——这是 7 次实盘链确认的"上涨日 bear 必失分"的根因修复。
      3. **真 OFI 订单流**(P1):引擎自评 R²=15–25% 的 5 分钟尺度唯一学术支撑信号,但现在实现的是静态 20 档快照(≠订单流)。用 depth-diff WS + 成交带 aggressor + spoof 检测,并用 OFI 方向做"延续是否有真实资金支撑"的确认。
      4. **时段 / session 维度**(P1,零成本):日志已经自己证明了强时段依赖——00–01 点方向 36–37%,10 点 14.3%,而 08 点 85.7%、17 点 88.9%。加上周末、funding 结算、期权到期的判断。
      5. **funding rate + open interest**(P1):OI 与价同涨=新资金趋势延续,OI 平=换手反转;极端正 funding + 快速下跌 = 清算瀑布触发器。
      6. **宏观日历**(P2,纯防御):FOMC/CPI/NFP 事件窗口前降仓/转 neutral,不预测方向,只防最坏尾部。
      7. **score/regime/factors 落地到日志**(P0):现在只有 confidence + vol_pct,没有这些数据,上面所有校准都无从谈起。
      8. **明确不做**:链上净流入/稳定币铸造/巨鲸转账(观察延迟在 5 分钟级即失效)、BTC.D(日级背景对 5 分钟无独立信息)。别把工程浪费在信噪比已被延迟抹平的维度上。
      
      ---
      
      ## 五、提升胜率行动清单(P0 / P1 / P2)
      
      ### P0 — 先止血,优先级最高
      
      1. **砍掉 bear/DOWN,策略改为 UP-only。**
         数据:bear 50% 精确随机,paper DOWN 22% 全亏 −$4.78。预期:立即消除确定性亏损,胜率集中在唯一正 EV 侧(63–64%)。
      
      2. **把交易逻辑从"方向预测"重构为"错价检测",写死双门控:**
         `仅 bias=bull 且 UP ask ∈ [0.40,0.50] 才入场;EV=p−ask−半价差−费<0.05 跳过;永不 DOWN;方向锁定,翻转=跳过而非反手;第4分钟 ask>0.50 放弃。`
         预期:每次交易期望从约 0 变为 +0.05~+0.14,结构性消灭两类亏损源(UP>0.50 −$1.14、DOWN −$4.78)。
      
      3. **从一切交易闸门移除 Platt 置信度。** 恢复 MIN_CONF=0,删除"conf≥50 才算明确信号"的阈值叙事;改用按(方向, 价位桶)的实证条件胜率校准,或保序回归拟合经验可靠性表。
         预期:消除"越自信越亏"的反向资金分配。
      
      4. **修验证管线(先于任何调权重)。** 修复回测脚本与当前引擎 7 元组/6 元组不兼容;把 taker_buy/imbalance/microprice 纳入公平回测(下一根 K 线目标);任何因子非零权重的前提是通过下一根 K 线回测的 IC/方向一致率门槛。
         预期:消除 25% 组合权重建立在零验证之上的风险;阻断"因子先上线再验证"的过拟合流程。
      
      5. **先修数据地基:引擎输出与日志同时记录 score、regime、factors 快照 + 每根 K 线第 2/3/4 分钟的预测与真实 ask。**
         没有这些,一切校准、regime 解剖、时序结论都无法验证。同时一次性回答"第 3/4 分钟胜率更高但被价格吃掉"这个目前完全无数据支撑的悬案。
      
      6. **把 P_up/P_down 接进引擎当门控。** 只在 p−P−手续费>0 时出方向,用 token 价替代反校准的 confidence 做仓位依据。
         预期:零新增成本、最高 ROI——直接兑现盈利机制。
      
      7. **收敛因子集到 3 个有证据的信号:延续(≥1.0)+ volume(≥0.8)+ meanrev(0.3);momentum/rsi/decel/position/fatigue/v_reversal/vol_breakout 权重清零或翻转。**
         预期:消除 37% 权重无信息、25% 权重未验证的现状,这是唯一能同时解释"回测 74% vs 实盘 46–65%"缺口的根因修复。
      
      8. **接 ETH/SOL 5m 动量做跨资产广度,趋势日强制压制 bear 至 neutral 或降权。**
         预期:针对实测最差 failure(DOWN 22%/−$4.78),在涨势日关闭"逆势押空"是最高单项收益。
      
      ### P1 — 建立可信的测量与扩展
      
      9. **重定回测目标为 close vs open**(与预测市场结算一致),重跑全量;price-range 输出下线。
         预期:消除目标错位导致的系统性"预测正确但口径不符"失真(100% 进度也仅 76.1% 的固有上限)。
      
      10. **统一战绩口径,停止引用虚高数字。** 所有报告只报 non-neutral 方向胜率(57.7%)与按(方向,价位桶)的 EV + 95%CI;任何"总准确率"禁止用于决策。甜区 n=10 的 60% 置信区间约 [30%,90%],不足以支撑实盘仓位,先积累样本。
      
      11. **A/B 朴素基线**:规则"第 2 分钟 close>open 就买 UP"与引擎对比。若追平 13 因子引擎,就把它压成延续 + volume 两因子,砍掉全部噪声因子与区间预测。
      
      12. **仓位 1/4 凯利并随入场价缩量**:`f*=(p−P)/(1−P)`。
          预期:把单笔 36% 全损的高方差拖累压到可承受范围,避免在 6+ 连败中回吐全部正期望。
      
      13. **订单簿升级为真 OFI**:depth-diff WS + 成交带 aggressor + spoof 检测,用 OFI 做延续的"流确认";否则 imbalance/microprice 权重维持 0。
      
      14. **删除冻结的 bull×0.92 先验**,改为滚动窗口重新估计 bull/bear 偏差作为方向独立的仓位乘数。
      
      15. **接入时段/session 因子**(UTC 整点、session 三段、周末、funding 00/08/16 UTC、期权周五到期),零成本编码已记录的教训(周末回归、23:00+ 延续断裂、紧缩低量=噪声)。
      
      16. **上 15m(再测 1h)Up/Down 对比实验**:更长周期=延续更强、点差占比更小、锚定窗口更大。以 EV/单位方差 而非胜率为评估指标,先 paper 满 30 个结算再定仓——这是唯一的规模化路径。
      
      17. **加 funding rate + OI**:强趋势放量日确认方向,清算期避免接飞刀。
      
      ### P2 — 打磨与防御
      
      18. **保序回归校准落地后**,从经验可靠性表重新导出 neutral 阈值(在 P(win)≈50% 处),替代目测的 ±1。在此之前不要盲缩 neutral 带。
      
      19. **用已结算数据验证 ATR-spike conf 减半与 HIGH_VOL 惩罚**:若"低 conf 高 vol 桶"(conf 40–45,vol_pct 86%,胜率 62.5%)确实更高,取消或放宽该黑天鹅减半,止损改为独立仓位规则。
      
      20. **硬编码 FOMC/CPI/NFP 日历**,事件窗口前 30–60 分钟对方向信号降级/转 neutral——纯防御,不预测方向。
      
      21. **冻结因子/补丁新增**,改为留出法(out-of-sample)压力测试,量化每个补丁对样本外胜率的独立贡献,无贡献者删除。
      
      22. **延长实盘样本并分 regime 记录**,按 TREND/HIGH_VOL/RANGE 分开统计 bull 延续率,确认 64% 不是某段单边行情的产物。
      
      23. **用真实盘口价对比 keyless 假设 `P_up=sigmoid(1.7×move/ATR)`**,确认"第 2 分钟最便宜"是市场真实结构还是模拟价模型的人为产物。
      
      ---
      
      ## 六、诚实结论
      
      把五份审查剥到最硬,结论就五条:
      
      1. **你们的真 edge 是"半K线延续"这个市场微观结构基础概率,不是 13 因子预测能力。** 回测越往后越准(76.1% vs 前 2 根 61.4%),就是"确认已发生的事",而市场已经把它定价进了 token 价。
      
      2. **能赚钱的唯一路径是"错价检测",不是方向准确率。** 只做 bull 且 UP ask 落在 0.40–0.50 的甜区,EV=p−P−成本>0 才下单。方向 raw 胜率 63.9% vs 成交胜率 44%,这个落差就是执行层吃掉的 edge——价格门控比任何因子都值钱。
      
      3. **bear/DOWN 是硬币,趋势日系统性失分,直接砍掉。** 这是五个方向共同、数据反复确认的结论。别修了,放弃它。
      
      4. **置信度(Platt Scaling)已死且反校准,从一切交易闸门移除。** 用按价位桶的经验胜率校准替代,让数据说话。
      
      5. **单资产 5 分钟价量因子已经榨干了。** 11 个因子 47–49% 是铁证。要继续提升,只有两条路:引入统计独立的新信息(订单流 OFI、跨资产广度、时段结构、token 定价 P),以及在更长周期(15m/1h)里找延续效应和错价窗口。而诚实的另一面是:即使方向 edge 真实存在(64%,统计显著),样本只有 2 天、门控后 paper 净收益只有 +$1.39——**这是一个边缘到经济意义接近零的优势**。它值得用纸面仓位慢慢积累验证,不值得用 1U/10U 平注去赌 36% 的单笔全损。
      
      一句话送给以后的自己:**先证明"你比市场定价准",再谈权重、时序和仓位。**
    • sync-procedure.md 4.5 KB
      # 5minbtc 仓库同步 (AGI-Super-Team)
      
      ## 路径映射 (两台机器)
      
      | 角色 | hermes 机 (Linux) | Mac (daniel 本机) |
      |------|------------------|------------------|
      | 运行副本 (源) | `/home/aa/.hermes/profiles/cqo/skills/5minbtc/` | `~/.claude/skills/5minbtc/` |
      | 共享仓库 | `/home/aa/clawd/repos/AGI-Super-Team/` | `~/AGI-Super-Team/` |
      
      ⚠️ 仓库分支是 **`main`**(不是 master)。下面命令按 hermes 机写,Mac 上把两个路径换成上表右侧即可。
      
      ## 同步命令
      
      ```bash
      # 1. rsync 源码到共享仓库
      #    -c 按「内容校验和」比较, 不看 mtime
      rsync -avc \
        --exclude='data/' --exclude='/archive/' --exclude='reviews/' --exclude='__pycache__/' \
        --exclude='logs/*.jsonl' --exclude='logs/*.jsonl.*' \
        --exclude='backtest/results/' \
        /home/aa/.hermes/profiles/cqo/skills/5minbtc/ \
        /home/aa/clawd/repos/AGI-Super-Team/skills/5minbtc/
      
      # 2. 校验:必须只有「刻意排除的东西」有差异
      diff -rq ~/.claude/skills/5minbtc/ ~/AGI-Super-Team/skills/5minbtc/ \
        | grep -v 'logs/5minbtc-log.jsonl'     # 只应剩下 live 日志这一条
      
      # 3. 提交推送
      cd /home/aa/clawd/repos/AGI-Super-Team
      git add -A skills/5minbtc/
      git commit -m "sync(skills/5minbtc): <变更简述>"
      git pull --rebase origin main    # 另一台机器可能已推过
      git push origin main
      ```
      
      ### 为什么用 `-c` 而不是 `-u`
      
      `-u`(只允许源更新覆盖)是为了防止本机旧副本反向覆盖仓库里更新的文件 —— **这个动机没错,但判据错了**:
      `-u` 比的是 **mtime**,而 git 的 `rebase` / `checkout` / `clone` 都会**重写工作区文件**,
      把仓库副本的 mtime 顶得比源还新。此时 `-u` 会**静默跳过真实改动**,只打印 `Skip newer`,看起来一切正常。
      
      实际踩过(2026-09-10):`git pull --rebase` 后 rsync 对 5 个 `scripts/*.py` 报 `Skip newer`,
      但 `diff` 显示内容其实一致 —— 那次侥幸没事,但同一机制会在真有改动时静默漏同步。
      
      `-c` 用内容校验和判断,与 mtime 无关,既保留"不反向覆盖"的效果,又不会漏。
      代价是要读全部文件(本 skill 约 1MB,可忽略)。
      
      **兜底原则:无论用什么参数,同步后必须跑一次 `diff -rq` 校验。**
      
      ## 重命名/删除文件时
      
      rsync **不带 `--delete`**,所以删掉/改名的文件不会自动从仓库移除,必须显式删:
      
      ```bash
      git rm skills/5minbtc/<旧文件名>
      ```
      
      (2026-09-10 引擎改名 `-v5.7.py` → `-v6.0.py` 时即如此处理。)
      
      ## 同步内容 / 排除
      
      **同步**:引擎 `5minbtc-engine*.py`、`5minbtc-log.py`、`5minbtc-news.py`、`SKILL.md`、
      `README.md`、`.env.example`、`.gitignore`、`references/`、`scripts/`、`backtest/`、`reports/`、
      **`logs/archive/*.jsonl.gz`**(月度压缩归档)。
      
      **排除**:
      
      | 排除项 | 原因 |
      |--------|------|
      | `logs/*.jsonl`(live) | 每 5 分钟追加,进 git 会产生巨大且无意义的 diff |
      | `data/`、`backtest/results/` | 运行时产物 / 回测产物 |
      | `reviews/` | 该目录实际不存在(见 [archive.md](archive.md)) |
      | `/archive/` | 旧版本引擎(本地不保留,只留 archive.md 文字归档) |
      | `__pycache__/` | Python 缓存 |
      
      > ⚠️ `--exclude='/archive/'` **必须带前导 `/`**(锚定到传输根)。
      > 写成 `--exclude='archive/'` 会把 `logs/archive/` 一起排掉,日志归档就再也同步不上去了。
      
      ## 日志归档入库的 .gitignore 要点
      
      仓库根的 `.gitignore` 有 `logs/`(会匹配任意层级的 `logs/` 目录)。**父目录被排除时,只写 `!文件` 是无效的**,必须逐级反忽略目录本身:
      
      ```gitignore
      !skills/5minbtc/logs/
      skills/5minbtc/logs/*
      !skills/5minbtc/logs/archive/
      !skills/5minbtc/logs/archive/*.gz
      ```
      
      skill 内还有一份嵌套 `.gitignore`(同样忽略 `*.gz`),也需加 `!logs/archive/` + `!logs/archive/*.gz`。两处都改完,用下面命令确认(`git add -n` 是权威判据,`check-ignore` 的退出码在负向规则下会误导):
      
      ```bash
      cd ~/AGI-Super-Team
      git add -n skills/5minbtc/logs/          # 应只列出 .gz;live jsonl 不应出现
      ```
      
      ## Commit message 惯例
      
      `sync(skills/5minbtc): <版本> 全量同步 — 引擎+回测+复盘+参考文档`
      
      ## 推送前检查
      
      - 不含敏感数据(密钥、API 凭证)—— 特别确认没把 `~/bb-auto/prediction.env` 带进来
      - 不含市场数据(`data/`、`backtest/results/`)
      - 不含 live 日志(只应有 `logs/archive/*.jsonl.gz`)
      - `diff -rq` 校验通过(见上文第 2 步)
      
    • telegram-push-monitor.md 2.8 KB
      # 5minbtc → Telegram 推送监控(含预测记录与结算)
      
      本地持久化监控 daemon,把 5minbtc 引擎预测实时推送到 Telegram,并**记录预测 + 收盘结算**,
      用于验证每日预测战绩。
      
      ## 脚本(本 skill `scripts/`)
      
      | 脚本 | 作用 |
      |------|------|
      | `5minbtc_watch.py` | 持续监控 daemon:每根 5min K 线第 2/3/4 分钟采样引擎,事件驱动推送 + 记录 + 结算 |
      | `5minbtc_day_stats.py` | 预测战绩查询(今日/指定日期/全部),`--push` 推送到 Telegram |
      | `telegram_push.py` | 通用 Telegram 推送助手(从 `~/.cc-connect/config.toml` 读 bot token + chat_id) |
      
      ## 用法
      
      ```bash
      # 持续监控(事件驱动 + 每小时心跳 + 每日战绩推送)
      python3 scripts/5minbtc_watch.py
      
      # 每根K线都推完整预测(约 288 条/天,慎用)
      python3 scripts/5minbtc_watch.py --every-candle
      
      # 查询今日战绩 / 指定日期 / 全部,推送到 Telegram
      python3 scripts/5minbtc_day_stats.py
      python3 scripts/5minbtc_day_stats.py --date 2026-08-12 --push
      python3 scripts/5minbtc_day_stats.py --all
      ```
      
      ## 记录与结算(验证以最终确认结果为准)
      
      - **记录**:每根 K 线记录首次成功采样的预测到 `logs/5minbtc-log.jsonl`(经 `5minbtc-log.py log`)
        - 字段:candle ISO / pred_close / pred_range / confidence / bias / news / vol_pct
      - **结算**:新 K 线开始时对上一根已收盘 K 线执行 `settle-all`,写入最终确认的
        actual_close / actual_high / actual_low,并计算:
        - `direction_correct` — 方向命中(看多→实际收>开,看空→实际收<开)
        - `in_range` — 收盘价是否落在预测区间
        - `error_pct` — 预测收盘误差 %
      - **每日 00:00**(CST)自动推送前一天战绩汇总:方向命中率 / 区间命中率 / MAE
      
      ## 事件推送类型
      
      | 事件 | 含义 |
      |------|------|
      | START | 首次采样基线方向 |
      | DIR-CHANGE | 方向翻转(⚠️ v6.0 起引擎**二选一无中性**,只会 bull↔bear) |
      | CLEAR-SIGNAL | 达到明确信号门槛(strength∈medium+ + conf≥50;`bias!=neutral` 条件在 v6.0 下恒为真) |
      | TB-FLIP | 主动买卖力 taker_buy 正负翻转 |
      | 心跳 | 每小时一次,确认 daemon 存活 |
      
      > ⚠️ 生产环境的 `com.daniel.5minbtc-watch` 用 **`--mute`** 启动 → **事件推送整体关闭**,
      > 只保留预测记录 + 结算 + 每日战绩;事件推送实际由 `5minbtc-realtime` 承担(见
      > [output-template.md](output-template.md) §1–2)。
      
      ## 依赖
      
      纯标准库(引擎只依赖 stdlib,无 pip 依赖)。`telegram_push.py` 需要本机装有 cc-connect 配置。
      
      ## 持久化(launchd 示例)
      
      ```bash
      # ~/Library/LaunchAgents/com.daniel.5minbtc-watch.plist
      # ProgramArguments: /usr/bin/python3 <SKILL>/scripts/5minbtc_watch.py
      # RunAtLoad=true, KeepAlive=true(开机自启 + 崩溃自动重启)
      ```
      
  • reports
    • R01-factor-theory.md 4.5 KB
      # R1: 量化因子理论深度蒸馏
      
      > 2026-05-23 | 来源: 16次 web_search + 3次 web_extract
      
      ## 1. WorldQuant Alpha101 进阶分析
      
      ### 1.1 Kakushadze 因子分组逻辑
      
      四大因子族(基于 Coriva 2026 逆向分析):
      
      - **价量背离因子 — 32个**:`correlation(close, volume, d)`, `rank(close) * rank(volume)`。IC衰减最慢(半衰期5-15交易日),知情交易者价量行为是结构性的。
      - **动量与反转 — 23个**:`ts_delta(close, d)`, `rank(ts_delta(close,5)) - rank(ts_delta(close,20))`。IC衰减最快,因子拥挤度消耗。
      - **波动率与日内结构 — 23个**:`high/low - 1`, `stddev(returns, d)`。alpha收益与波动率强相关(`return ~ σ^α, α≈1`),与换手率无显著依赖。
      - **流动性/复合/多因子 — 23个**:`adv{d}`,引入 `IndClass` 截面排序。
      
      **关键统计**:平均持仓0.6~6.4天,平均两两相关系数15.9%,80%因子在WQ实盘使用。
      
      ### 1.2 加密市场因子验证
      
      - **Liu, Tsyvinski & Wu (2022)** *JF*: 加密三因子模型(市场+规模+动量)
      - **Liu & Tsyvinski (2021)** *RFS*: 加密网络因子+生产因子,强时间序列动量,Google搜索量预测收益
      - **lansetaowa/alpha101-crypto**: 全部Alpha101迁移到Binance,价量背离+动量类在加密有效
      - **ACM 2025**: 高频(1h)IC衰减极快,日级仍有效;Gas因子在高拥堵期突出
      - **不适用**: 涉及`IndClass`的因子(加密行业分类体系不成熟)
      
      ## 2. WorldQuant BRAIN平台 & Alpha扩展
      
      - Alpha201 **非公开发表**,BRAIN平台因子数万+(持续增长),仅操作符公开
      - **Alpha191**:国泰君安2017年A股版,用申万行业分类,增加技术指标类因子
      - **BRAIN操作符扩展**: `group_neutralize`, `vector_neut`, `regression_neut`, `decay_exp`
      - **LLM因子生成**: `ritchie27/worldquant-miner-remote` 已实现Ollama本地LLM自动生成测试提交alpha
      
      ## 3. 因子动物园与多重检验
      
      ### 3.1 Harvey-Liu-Zhu (2016) 里程碑
      - 截至2015年已发表316个因子,提出 **t > 3.0** 新标准
      - Bonferroni/Holm/BH三种校正方法
      
      ### 3.2 Feng-Giglio-Xiu (2020) "Taming the Factor Zoo"
      - **双选择准则(Double Selection)**:检验因子A时先选控制变量再做条件检验
      - 从"t阈值"到"模型选择"的范式转变
      
      ### 3.3 Jensen-Kelly-Pedersen (2023) 反驳
      - 对两万亿个模型估计,**大多数因子可以复制**
      - 聚类为13个主题簇,**不存在广泛的复制危机**
      
      ### 3.4 Publication Bias修正
      - p-curving、HARKing检测、经济显著性vs统计显著性、跨市场样本外验证
      
      ## 4. MSCI Barra风险模型
      
      ### USE4结构
      - 10个风格因子: Beta, Momentum, Size, Earnings Yield, Residual Volatility, Growth, Book-to-Price, Leverage, Liquidity, Non-linear Size
      - **关键创新**: Country因子分离、Eigenfactor偏差校正(蒙特卡洛估计)、VRA(EWMA半衰期90日)、优化偏差调整
      - **因子回归**: WLS回归,权重=总市值平方根倒数
      - **正交流程**: 行业内Z-score → 对行业因子正交 → Gram-Schmidt风格因子间正交(优先级: Size→Beta→Momentum→...)
      
      ### CNE5(中国版)
      - 同10因子但参数本地化,专有中国行业分类,A股特有建模(涨跌停板、T+1)
      
      ## 5. 前沿因子研究 (2024-2026)
      
      ### ML生成因子
      - **Gu, Kelly & Xiu (2020)**: 神经网络+GBDT夏普翻倍,94个因子中动量/波动率/流动性最重要
      - **AlphaForge (AAAI 2025)**: 生成式-预测式NN→公式化因子→动态组合,保留可解释性
      - **QuantFactor REINFORCE (IEEE TKDE 2025)**: 方差有界REINFORCE解决梯度估计方差
      - **Alpha-GPT (2023)**: 自然语言→数学公式→自动回测
      - **AlphaLogics (arXiv 2603.20247, 2026)**: 多Agent系统,从Alpha101反向提取市场逻辑
      - **FactorMiner (arXiv 2602.14670, 2026)**: 组合式技能架构+经验记忆,攻克"相关性红海"问题
      
      ### ESG因子
      - Pedersen et al. (2021): ESG-有效前沿,ESG Improvers比ESG水平更有预测力
      - 能源转型因子比宽基ESG更稳定
      - 对加密市场:能源效率/共识机制可能成为类似ESG因子
      
      ---
      
      ## 核心参考文献
      1. Kakushadze (2016) arXiv:1601.00991
      2. Kakushadze & Tulchinsky (2015) SSRN 2657603
      3. Liu, Tsyvinski & Wu (2022) JF, NBER WP 25882
      4. Harvey, Liu & Zhu (2016) NBER WP 20592
      5. Feng, Giglio & Xiu (2020) JF, NBER WP 25481
      6. Jensen, Kelly & Pedersen (2023) JF 78(5)
      7. Menchero, Orr & Wang (2011) MSCI USE4 Notes
      8. Gu, Kelly & Xiu (2020) RFS 33(5)
      9. Shi & Luo et al. (2025) AAAI AlphaForge
      10. Chen et al. (2026) arXiv:2603.20247 AlphaLogics
      11. Li et al. (2026) arXiv:2602.14670 FactorMiner
      
    • R02-strategy-theory.md 4.9 KB
      # R2: 量化交易顶尖策略理论深度蒸馏
      
      > 2026-05-23 | 来源: 28次 web_search + 3次 web_extract
      
      ## 一、统计套利 / 配对交易
      
      ### OU过程数学核心
      $$dX_t = \theta(\mu - X_t)dt + \sigma dW_t$$
      - θ: 均值回归速度 | μ: 长期均衡 | σ: 扩散系数
      - **半衰期**: $t_{1/2} = \ln 2 / \theta$ — 决定持仓周期
      
      ### 参数估计
      - **OLS**: 简单但Hurwicz bias($\hat{b}$向上偏误,低估θ,高估半衰期)
      - **MLE**: 渐近有效,转移密度 $X_{t+\Delta t}|X_t \sim N(\mu + (X_t-\mu)e^{-\theta\Delta t}, \frac{\sigma^2}{2\theta}(1-e^{-2\theta\Delta t}))$
      - **实践**: 先OLS初始值,再MLE精细化,日频需200+观测
      
      ### 协整检验
      - **Engle-Granger**: 因变量选择不对称,只能一个协整向量,检验势低
      - **Johansen**: 基于VECM,对称,可检测多个协整关系,直接提取对冲权重
        - 迹检验: $LR_{tr} = -T\sum_{i=r+1}^{n}\ln(1-\hat{\lambda}_i)$
        - 最大特征值检验: $LR_{max} = -T\ln(1-\hat{\lambda}_{r+1})$
      
      ### 最优阈值 (Leung & Li 2015)
      - 基于OU参数的最优停止问题
      - θ快→阈值窄(回归快);σ大→阈值宽(补偿波动)
      - 数值解:有限差分法求解Stefan problem
      
      ### 失效场景
      - 协整关系破裂(2008危机)、制度切换、交易成本吞噬、拥挤交易、gap risk
      
      ### 核心文献
      1. Avellaneda & Lee (2010) QF 10(7)
      2. Leung & Li (2015) IJTAF 18(3)
      3. Johansen (1991) Econometrica 59(6)
      4. d'Aspremont (2011) QF 11(3)
      
      ---
      
      ## 二、做市策略
      
      ### Avellaneda-Stoikov (2008)
      - **预约价格**: $r(s,q,t) = s - q\gamma\sigma^2(T-t)$ — 效用调整mid
      - **最优价差**: $\delta^b + \delta^a = \frac{2}{\gamma}\ln(1+\frac{\gamma}{\kappa}) + \gamma\sigma^2(T-t)$
      - 到达率: $\Pr(\text{fill at }S\pm\delta) \approx A\exp(-\kappa\delta)$
      - 核心洞见: 库存偏移自动倾斜报价,κ大→价差小
      
      ### GLF扩展 (2013, 2015)
      - 多资产做市(向量库存$\mathbf{q}$,协方差$\Sigma$)
      - 近封闭解: $\delta^b \approx \frac{1}{\kappa} + \frac{2q+1}{2}\frac{\gamma\sigma^2(T-t)}{2} + \frac{\epsilon}{\gamma}$
      - 跨资产库存耦合: Risk = $\mathbf{q}'\Sigma\mathbf{q}(T-t)$
      
      ### Alpha-AS (Schulz 2022)
      - RL替换AS固定参数,保留理论框架
      - Alpha-AS-1: RL直接输出δ | Alpha-AS-2: RL输出超参(γ,κ)
      - Alpha-AS-2在加密市场显著优于原始AS
      
      ### 失效场景
      - 毒性订单流、极端波动、LOB结构突变、延迟竞争、库存极限
      
      ### 核心文献
      1. Avellaneda & Stoikov (2008) QF 8(3)
      2. Guéant, Lehalle & Fernandez (2013) MFE 7(4)
      3. Cartea, Jaimungal & Penalva (2015) Cambridge UP
      4. Schulz et al. (2022) PLoS ONE 17(12)
      
      ---
      
      ## 三、高频信号
      
      ### OFI (Cont, Kukanov & Stoikov 2014)
      - 订单流不平衡 = best bid事件 - best ask事件
      - $\Delta p_n = \alpha + \beta \cdot OFI_n + \epsilon_n$, $R^2$=0.35-0.65
      - OFI比纯成交量预测力更强,平方根冲击定律的微观基础
      
      ### 价格冲击模型分层
      1. 线性(小单): $\Delta p = \lambda Q$
      2. 平方根(中单): $\Delta p = \sigma\sqrt{Q/V}$
      3. 对数(大单): $\Delta p = k\ln(Q)$
      
      ### Kyle's Lambda估计
      - 高频回归: $\Delta p_\tau = \alpha + \lambda \cdot \text{sign}(v_\tau)\sqrt{|v_\tau|} + \epsilon$
      - LOB快照: $\hat{\lambda} \approx \frac{1}{2}\frac{\text{spread}}{\text{depth at best}}$
      - VPIN: volume bucket中估计知情交易概率
      
      ### 核心文献
      1. Cont, Kukanov & Stoikov (2014) JFE 12(1)
      2. Kyle (1985) Econometrica 53(6)
      3. Easley, López de Prado & O'Hara (2012) RFS 25(5)
      4. Almgren & Chriss (2001) JOR 3(2)
      
      ---
      
      ## 四、动量策略前沿
      
      ### TS动量 vs 截面动量
      - **截面动量**(Jegadeesh & Titman 1993): 相对排名博弈,做多top做空bottom
      - **TS动量**(Moskowitz, Ooi & Pedersen 2012): 独立判断趋势方向,单资产可实施
        - 信号: $\text{sign}(r_{t-12,t-1})$
        - 波动率缩放: $\frac{40\%}{\sigma_t}$
        - 58个期货合约夏普~1.2
      
      ### 动量崩溃与对冲
      - **2009年3月**: 截面动量3个月亏73.42%
      - **Barroso & Santa-Clara (2015)**: 风险管理动量
        - $w_t^{RM} = \frac{\sigma_{target}}{\sigma_t^{realized}} w_t^{original}$
        - 夏普0.8→1.5,最大回撤-73%→-20%
      
      ### BTC动量特殊性
      - 高频显现(小时/分钟级,Corbet et al. 2020)
      - 散户主导→趋势更强更持久
      - 资金费率动量、链上动量、波动率调整动量
      
      ### 核心文献
      1. Moskowitz, Ooi & Pedersen (2012) JFE
      2. Barroso & Santa-Clara (2015) JFE
      3. Daniel & Moskowitz (2016) JF
      4. Liu, Tsyvinski & Wu (2022) JF
      
      ---
      
      ## 五、均值回归
      
      ### Kalman Filter动态均值回归
      - 状态方程: $\beta_t = \beta_{t-1} + w_t$, $w_t \sim N(0,Q)$
      - 观测方程: $y_t = x_t\beta_t + v_t$, $v_t \sim N(0,R)$
      - 时变对冲比 vs 静态OLS:动态适应regime变化
      - 核心: 预测→更新循环,R/Q比控制适应性vs稳定性
      
      ### Z-Score陷阱
      - 非平稳序列的Z-Score无意义(均值和标准差时变)
      - 必须先用OU过程确认均值回归性,再做Z-Score
      - 正确做法: 用滚动OU参数计算动态Z-Score
      
    • R03-crypto-quant-defi.md 4 KB
      # R3: 加密货币专属量化策略与预测市场理论深度蒸馏
      
      > 2026-05-23 | 来源: 24次 web_search + 2次 web_extract
      
      ## 一、加密资金费率策略进阶
      
      ### 费率核心公式
      $$F = \text{clamp}(P_{premium} + I_{rate},\ -0.75\%,\ 0.75\%)$$
      $$P_{premium} = \frac{\max(0, B_{bid} - I) + \min(0, B_{ask} - I)}{I}$$
      
      ### 跨所套利基差风险
      $$\Delta P = (P_{perp}^A - P_{oracle}^A) + (P_{oracle}^A - P_{oracle}^B) + (P_{oracle}^B - P_{spot}^B)$$
      = 溢价偏差 + Oracle差异 + 现货滑点
      
      ### 费率预测模型
      $$F_t = \alpha + \beta_1 L_t + \beta_2 R_{ls,t} + \beta_3 \sigma_t + \beta_4 M_t + \epsilon_t$$
      - L: 全市场杠杆率(OI/MCap) | R_ls: 多空比 | σ: 24h RV | M: 动量因子
      - BTC费率显著均值回复(半衰期36-72h)
      
      ### 牛熊市差异
      - 牛市: 92%时间为正,均值0.01%-0.03%(8h),右偏
      - 熊市: 频繁转负,波动率放大2-3x,负费率=轧空前兆
      - 结构性不对称: 正费率被钳位0.75%上限,负费率无下限
      
      ### 费率异常检测
      $$Z_t = \frac{F_t - \mu_{rolling}}{\sigma_{rolling}}$$
      - |Z|>2触发: Z>2做空永续+多现货 | Z<-2做多永续+空现货
      - 费率Z-Score均值回复: 夏普1.5-2.8, 最大回撤8-12%
      
      ---
      
      ## 二、链上数据量化
      
      ### HODL Waves信号
      $$\text{Signal}_{HODL}: H_{1y+}>0.60且dH/dt转正=BUY | H_{1y+}<0.40且dH/dt转负=SELL$$
      - >40%供应量持有>1年 ≈ 接近底部
      
      ### 交易所净流入/流出
      - Glassnode实体调整: 识别交易所地址集群
      - 单日净流入>30d均值+2σ → 未来7d下跌概率65-70%
      - **突然性比绝对量更重要**
      
      ### Puell Multiple
      $$\text{Puell} = \frac{\text{Daily Issuance Value}}{\text{365d MA}}$$
      - <0.5 矿工投降(底部) | >3.0 异常高收入(顶部)
      
      ### Whale Alert有效性
      - 单独使用IR约0.2-0.4,**不足以独立构成alpha**
      - 需组合: $\text{Composite} = w_1 \cdot \text{NetFlow} + w_2 \cdot \text{Whale Score} + w_3 \cdot \text{MPI}$
      
      ### MVRV Z-Score
      $$\text{MVRV Z-Score} = \frac{\text{Market Cap} - \text{Realized Cap}}{\sigma(\text{Mkt Cap - Realized Cap})}$$
      - >7.0 周期顶部 | <0.0 周期底部 | 1.0-3.0 正常
      - MVRV>NVT: 成本基础难操纵,信号噪音比更高
      
      ---
      
      ## 三、DeFi量化
      
      ### 三明治攻击最优Front-run
      $$x_f^* = \sqrt{\frac{k(x_0+x_v)}{y_0/x_0}} - x_0 - x_v$$
      - 利润函数对front-run量求导=0的最优解
      
      ### 闪电贷套利边界
      $$\Delta P_{A,B} > 0.09\% + \frac{\text{Gas}}{\text{Borrowed}} \approx 0.12\%-0.15\%$$
      - Aave V3费用0.09% + Gas
      - Flashbots Protect防front-run
      
      ### Uniswap V3集中流动性
      - **资本效率**: ±10%范围约5倍效率提升
      - **IL放大**: $IL_{V3} = IL_{V2} / \text{range\_factor}$(范围越窄IL越大)
      - 策略: 波动率预测→选范围→费率收益覆盖IL
      
      ---
      
      ## 四、预测市场微结构
      
      ### LMSR数学 (Hanson 2003)
      - **成本函数**: $C(\mathbf{q}) = b\ln(\sum e^{q_i/b})$
      - **价格**: $P_i = \frac{e^{q_i/b}}{\sum e^{q_j/b}}$ = **Softmax!**
      - **有界损失**: 做市商最大损失 = $b\ln n$
      - **流动性参数**: $b$大→价格变动小→适合大额
      
      ### Polymarket架构
      - V1: LMSR AMM → V2: CLOB混合(离链撮合+链上结算)
      - YES+NO=$1(无风险套利约束)
      - 2024大选期间日交易量>$3亿
      
      ### 信息聚合理论
      $$P^* = \frac{\sum_j w_j \cdot E_j[\text{Event}]}{\sum_j w_j}$$
      - 财富加权平均信念,噪声交易者最终被套利者纠正
      
      ---
      
      ## 五、加密市场微结构
      
      ### 24/7影响
      - 资金利用率100% vs TradFi 19%,需5x风控覆盖
      
      ### 周末效应
      - 周末日收益: -0.05%至-0.15%(统计显著)
      - 波动率低15-25%,交易量低30-40%
      - 做市: 收窄价差 | 统计套利: 调低仓位
      
      ### 时段差异
      - 美国时段波动率最高(+20-35%):美股开盘+宏观事件+ETF资金流
      - 亚洲时段:泡菜溢价,散户活跃
      - 欧洲时段最低:午餐低谷12:00-14:00 UTC
      
      ### 减半周期信号
      - Puell<0.5持续30d → 矿工投降底部
      - S2F模型: $\ln(\text{MC}) = a + b\ln(S2F)$
      - 减半后时间轴: 积累→反弹→盘整→牛市→周期顶部
      - 2024特殊: ETF提前吸收供给冲击,突破时间表延后
      
    • R04-portfolio-theory.md 4.8 KB
      # R4: 顶尖投资组合理论深度蒸馏
      
      > 2026-05-23 | 来源: 30次 web_search + 3次 web_extract
      
      ## 一、Black-Litterman 模型
      
      ### Markowitz困境
      - 最优解 $w^* = \frac{1}{\delta}\Sigma^{-1}\mu$,当$\Sigma$条件数大时,$\mu$微小扰动→极端权重
      - 实践中MVO可能将61%资本集中于单一资产
      
      ### BL Bayesian框架
      **先验**: 市场隐含均衡收益 $\Pi = \delta\Sigma w_{mkt}$
      - $\mu \sim \mathcal{N}(\Pi, \tau\Sigma)$,τ通常取0.05
      
      **似然**: 投资者观点 $P\mu = Q + \epsilon, \epsilon \sim \mathcal{N}(0, \Omega)$
      
      **后验**:
      $$E[R] = [(\tau\Sigma)^{-1} + P'\Omega^{-1}P]^{-1}[(\tau\Sigma)^{-1}\Pi + P'\Omega^{-1}Q]$$
      
      精度加权平均:高置信度(低Ω)的观点获得更大权重。
      
      ### Omega矩阵设定
      - **He-Litterman (1999)**: $\Omega = \text{diag}(P(\tau\Sigma)P')$ — 观点不确定性与先验成比例
      - **Idzorek (2005)**: 用户指定置信度$c_k \in [0\%, 100\%]$,通过优化确定Ω
      
      ### 为什么BL避免极端权重
      $$E[R] = \Pi + \tau\Sigma P'(\Omega + \tau P\Sigma P')^{-1}(Q - P\Pi)$$
      $\Sigma$将偏离映射到协方差结构中,高相关资产间偏离被分散。
      无观点时$E[R]=\Pi$,$w_{BL}=w_{mkt}$ — 自然基准锚定。
      
      ### 加密组合挑战
      1. 无CAPM"市场组合" — BTC主导>50%
      2. Σ极度非平稳(BTC/ETH相关0.3→0.9)
      3. 缺乏无风险利率(DeFi收益率波动巨大)
      4. 解决: DCC-GARCH估计Σ + 链上指标作观点 + 稳定币收益率作基准
      
      ### 核心文献
      1. Black & Litterman (1992) FAJ 48(5)
      2. He & Litterman (1999) Goldman Sachs
      3. Idzorek (2005) Zephyr Associates
      4. Palomar (2025) *Portfolio Optimization*, Cambridge UP
      
      ---
      
      ## 二、Hierarchical Risk Parity (HRP)
      
      ### 核心算法 (López de Prado 2016)
      1. **距离矩阵**: $d_{i,j} = \sqrt{\frac{1}{2}(1-\rho_{i,j})}$
      2. **树形聚类**: 凝聚层次聚类
      3. **准对角化**: dendrogram中序遍历产生排序π
      4. **二分递归分配**: 逆方差权重递归分割
      
      ### 链接方法选择
      - **Single Linkage**: 易链式效应(chaining) — 不推荐
      - **Average Linkage**: 金融数据中最稳定(Papenbrock 2021)— **推荐**
      - **Complete Linkage**: 产生最平衡聚类
      
      ### 递归分配数学
      $$w_{C_1} = \frac{\text{IVP}_{C_1}}{\text{IVP}_{C_1} + \text{IVP}_{C_2}}$$
      $$\text{IVP}_{C_j}^{-1} = \frac{1}{|C_j|^2}\mathbf{1}'_{C_j}\Sigma_{C_j}\mathbf{1}_{C_j}$$
      
      ### 为什么HRP优于传统风险平价
      1. **无需矩阵求逆** — 仅使用方差和子矩阵的迹
      2. **聚类结构比精确协方差值更稳定** — 排序关系vs绝对值
      3. **鲁棒性** — Σ奇异或病态时仍产出有意义权重
      4. 更均匀的权重分布,避免低波动资产获得极端高权重
      
      ### 增强: Ledoit-Wolf收缩 + 时间衰减相关矩阵 + bootstrap聚类稳定性检验
      
      ### 核心文献
      1. López de Prado (2016) JPM 42(4)
      2. Raffinot (2018) JPM 44(2)
      3. Papenbrock et al. (2021) Physica A
      
      ---
      
      ## 三、Kelly Criterion 最优下注
      
      ### 连续时间Kelly
      $$f^* = \frac{\mu - r}{\sigma^2}$$
      最大增长率: $g(f^*) = r + \frac{S^2}{2}$(S为夏普比率)
      
      ### 分数Kelly三角关系
      - $f^* = S/\sigma$ — 最优下注 = 夏普/波动率
      - 半Kelly保留75%增长率,波动率减半
      - 高波动→Kelly比例减小;高夏普→Kelly比例增大
      
      ### Thorp实战扩展
      - **多资产Kelly**: $\mathbf{f}^* = \Sigma^{-1}(\boldsymbol{\mu} - r_f\mathbf{1})$ = Markowitz δ=1特例
      - **边注Kelly**: 独立有利机会不应放弃,多策略同时持有
      - **相关性修正**: 必须用完整协方差矩阵,不能简单叠加
      
      ### Kelly在加密交易的问题
      1. **μ过估计致命** — 牛市历史均值得f*>1,回归均值后爆仓
      2. **非平稳** — 2024-2026市场≠2017-2020
      3. **肥尾** — 用Cornish-Fisher展开修正
      4. **实际建议**: μ向0收缩50%+(半Kelly),滚动窗口σ,加密用1/4 Kelly,硬性f≤0.5
      
      ### Kelly ≡ Markowitz条件
      对数效用(γ=1) + 连续时间 + 正态分布 → Kelly最优组合 = Markowitz δ=1
      
      ### 核心文献
      1. Kelly (1956) BSTJ 35(4)
      2. Thorp (2006) *Handbook of ALM* Vol.1
      3. Merton (1969) REStat 51(3)
      
      ---
      
      ## 四、前沿组合优化 (2024-2026)
      
      ### 可微分组合优化 (DFL)
      - **问题**: 最小化MSE ≠ 最大化Sharpe
      - **解决**: 端到端训练,损失函数直接 = -Sharpe(w(θ))
      - **Moreau包络方法** (Zhang 2026): 近端算子使非凸/不可微约束可微分
      - **Lee et al. (2024)** "Anatomy of Machines for Markowitz"
      
      ### RL动态资产配置
      - PPO用于组合权重更新,SAC用于连续仓位
      - 多智能体RL (MARL): 每个资产一个agent + 全局协调器
      - Graph Attention + Heterogeneous MARL (Nature Sci.Rep. 2025)
      
      ### 图方法组合构建
      - MST → TMFG → Network Risk Parity
      - GNN组合: 资产为节点,捕获非对称关系(BTC→altcoin溢出)
      - Core-Periphery构建: 核心资产(BTC/ETH)基础权重 + 外围图距离加权
      
    • R05-ml-quant-trading.md 4.5 KB
      # R5: 机器学习量化交易最前沿
      
      > 2026-05-23 | 来源: 18次 web_search | 聚焦BTC 5min预测场景
      
      ## 一、GBDT因子挖掘 (LightGBM)
      
      ### BTC 5min适配度: ★★★★★
      表格型数据(OHLCV+衍生因子)最优选择,秒级训练。
      
      ### 关键实践
      - **时序交叉验证**: 严禁`train_test_split(shuffle=True)`,必须用`TimeSeriesSplit`或Purged K-Fold
      - Purge窗口≥12根K线(1h)
      - **Point-in-Time对齐**: 因子在t仅用≤t-1数据,标签用未来收益
      - **SHAP**: 因子筛选(≈0剔除) + 因子监控(随时间变化=regime shift预警)
      - **Optuna**: TPESampler, n_trials=200
      
      ### BTC 5min关键超参
      - `num_leaves`: 31-63 | `max_depth`: 5-8 | `learning_rate`: 0.01-0.05
      - `min_child_samples`: 50-200 | `early_stopping_rounds=50`
      
      ### 失败模式
      - 过拟合高频噪声 → 增大min_child_samples, 减小num_leaves
      - 因子共线性 → SHAP + PCA/因子正交化
      - Regime变化因子失效 → 分层训练/条件因子模型
      
      ### 核心文献
      1. Ke et al. (2017) LightGBM NeurIPS
      2. López de Prado (2018) *Advances in Financial ML* — Purged K-Fold
      
      ---
      
      ## 二、深度学习时序预测
      
      ### BTC 5min适配度: ★★★★☆
      多步预测和多模态融合有不可替代优势,建议与GBDT混合架构。
      
      ### 前沿Transformer (2023-2025)
      1. **PatchTST** (ICLR 2023): 时序分Patch降低复杂度O(L²)→O((L/P)²)
      2. **TimesNet** (ICLR 2023): FFT→2D张量→2D卷积捕捉多周期
      3. **iTransformer** (ICLR 2024)🏆: 反转!变量为Token,注意力建模变量间相关性
      4. **TFT** (2021): 多Horizon + 分位数预测 + 可解释注意力
      
      ### BTC 5min关键超参 (iTransformer)
      - seq_len=96(8h), pred_len=12(1h), d_model=64, n_heads=4
      - dropout=0.2, RevIN=True, loss=MSE+Huber
      
      ### 金融时序vs自然语言差异
      | 维度 | NLP | 金融 |
      |------|-----|------|
      | 平稳性 | 稳定 | 高度非平稳 |
      | 信噪比 | 高 | 极低 |
      | 分布偏移 | 缓慢 | 剧烈(黑天鹅) |
      | 多周期 | 层次结构 | 多周期叠加 |
      
      应对: RevIN + 滚动标准化 + 对比学习预训练 + 在线微调
      
      ### 核心文献
      1. Nie et al. (2023) PatchTST ICLR
      2. Liu et al. (2024) iTransformer ICLR
      3. Lim et al. (2021) TFT Int.J.Forecasting
      4. Kim et al. (2022) RevIN ICLR
      
      ---
      
      ## 三、强化学习做市与交易
      
      ### BTC 5min适配度: ★★★☆☆
      高度实验性,仓位管理潜力巨大。
      
      ### 算法选择
      - 方向决策 → **PPO**(离散买/卖/平)
      - 仓位管理 → **SAC**(连续比例,推荐首选)
      - 做市 → **SAC** 或 **TD3**
      
      ### 奖励函数设计(核心!)
      - **P&L-Based**: 直接但忽略风险
      - **Sharpe-Based**: 考虑风险但稀疏奖励
      - **Differential Sharpe** (Moody & Saffell 2001)🌟: 在线递推,即时反馈+风险调整
        - $dS = (B_t \cdot \Delta A - 0.5 A_t \cdot \Delta B) / (B_t - A_t^2)^{1.5}$
      
      ### 状态空间设计
      - 市场数据(归一化OHLCV) + 技术指标 + 持仓状态 + 市场状态(spread/OB imbalance/费率) + 外部信号(贝叶斯概率/ML置信度)
      
      ### RL过拟合: Sim2Real Gap
      - 缓解: 市场模拟器增强 + 域随机化 + 模拟→纸盘→实盘渐进
      - **FinRL框架**: AI4Finance开源,集成PPO/SAC/DDPG
      
      ### 核心文献
      1. Haarnoja et al. (2018) SAC ICML
      2. Moody & Saffell (2001) Differential Sharpe NeurIPS
      3. Liu et al. (2020) FinRL IJCAI
      
      ---
      
      ## 四、AutoML与自动特征工程
      
      ### BTC 5min适配度: ★★★★☆
      自动化因子挖掘显著提升Alpha发现效率,需强过滤机制。
      
      ### 遗传规划 (gplearn)
      - 函数集: add/sub/mul/div/sqrt/log/rank/delay/delta/ts_mean/ts_std
      - `parsimony_coefficient=0.01-0.1`(复杂度惩罚,关键防过拟合)
      - 2024-2025前沿: AlphaForge(多目标优化) → AlphaCFG(语法引导) → FactorMiner(自进化Agent)
      
      ### Featuretools时序特征
      - Deep Feature Synthesis + cutoff_time确保PiT + 多窗口[12,48,96,288]
      
      ### 联邦学习跨交易所
      - FedAvg/FedProx + 差分隐私 → 数据不出本地,只传模型梯度
      - 当前实验阶段,跨交易所合规需求推动落地
      
      ---
      
      ## 五、混合架构: 与贝叶斯引擎v3.5.1集成
      
      ```
      贝叶斯概率引擎(P=0.77) → LightGBM因子筛选(Top 20) → SAC仓位管理 → 执行风控层
      ```
      
      ### 实施优先级
      - **P0**: LightGBM + Optuna + SHAP
      - **P1**: iTransformer/TFT深度模型
      - **P2**: SAC仓位管理RL + gplearn自动因子
      - **P3**: 联邦学习(研究)
      
      ### 技术栈
      - 因子: LightGBM 4.x + Optuna 3.x + SHAP 0.45+
      - 深度学习: PyTorch 2.x + Time-Series-Library (thuml)
      - RL: FinRL + Stable-Baselines3
      - AutoML: gplearn + Featuretools
      - 部署: ONNX Runtime
      
    • R06-data-sources.md 5.1 KB
      # R6: 顶尖数据源全景
      
      > 2026-05-23 | 来源: 48次 web_search + 2次 web_extract
      
      ## 一、替代数据(Alt Data)全景
      
      | 类型 | 代表平台 | Alpha衰减 | 成本/年 | 最佳市场 |
      |------|---------|----------|--------|---------|
      | 卫星图像 | Orbital Insight | 3-6月 | $50K-200K | 大宗商品/零售 |
      | 信用卡 | Second Measure(Bloomberg) | 2-4月 | $100K-300K | 零售股 |
      | 网页流量 | SimilarWeb | 6-12月 | $2K-240K | 电商/SaaS |
      | APP下载 | Sensor Tower | 6-12月 | $12K-120K | 社交/游戏 |
      | 地理位置 | Placer.ai | 3-6月 | $25K-100K | 零售/旅游 |
      | 供应链 | Panjiva(S&P Global) | 6-12月 | $1K-50K | 制造/贸易 |
      
      **对加密市场**:替代数据直接Alpha有限,但可追踪CEX/DEX流量、加密钱包APP下载量(散户入场信号)。
      
      ---
      
      ## 二、加密专属数据源(核心!)
      
      ### 链上数据
      - **Glassnode** — MVRV/SOPR/NUPL/Hash Ribbon | Pro $39/月 | ★★★★★
      - **CoinMetrics** — 学术级数据质量 | Pro $50-500/月 | ★★★★
      - **Dune Analytics** — SQL直接查询原始链上,最灵活 | Pro $29/月 | ★★★★★
      - **Token Terminal** — 加密"Bloomberg",协议基本面 | Pro $49/月 | ★★★★
      
      ### DEX数据
      - **DeFi Llama** — 完全免费,无需API Key | TVL/Volume/APY | ★★★★★
      - **The Graph** — GraphQL查询DeFi原始事件 | 免费 | ★★★★
      
      ### 社交情绪
      - **Santiment** — 链上+社交融合最强 | Pro $49/月 | ★★★★★
      - **CryptoQuant** — Exchange Flow最强短期Alpha | Pro $29/月 | ★★★★★
      - **LunarCrush** — Galaxy Score/AltRank | Pro $99/月 | ★★★★
      
      ### Whale追踪
      - **Arkham Intelligence** — 实体去匿名化独一无二 | 免费/定制 | ★★★★★
      - **Whale Alert** — >$500K链上转账实时监控 | Pro $99/月 | ★★★
      
      ### 衍生品
      - **Coinglass** — OI/Funding/Liquidation/L-S Ratio | Pro $49/月 | ★★★★★
      - **Laevitas** — 期权/波动率曲面/Deribit | Pro $29/99/月 | ★★★★
      
      ### MEV/Mempool
      - **Flashbots** — MEV-Boost数据,免费开源 | ★★★★★
      - **Blocknative** — 实时mempool监控+Gas预测 | Pro $99-499/月 | ★★★★
      
      ---
      
      ## 三、宏观数据API
      
      ### 免费
      - **FRED** — GDP/CPI/PCE/非农/DXY/M2 | `fredapi` Python | ★★★★
      - **Yahoo Finance** — yfinance跨资产 | ★★★
      - **GDPNow(Atlanta Fed)** — 实时GDP预测,领先官方4-6周 | ★★★★
      
      ### 中国专属
      - **Wind** — ¥30K-100K+/年,A股全覆盖,机构标配 | ★★★★★
      - **同花顺iFinD** — ¥5K-50K/年,Python API友好 | ★★★★
      - **AKShare** — 完全免费开源,东方财富/新浪数据 | ★★★★
      - **Tushare Pro** — 免费积分制,A股+宏观+期货 | ★★★★
      
      ### 宏观→加密传导机制
      1. **利率→美元→BTC**(最强传导,2-4周延迟)
      2. **M2流动性→BTC**(中期最强,6-12月领先)
      3. 中国M2增速领先BTC约3-6个月
      
      ### BTC 5min模型推荐宏观因子
      `DXY` | `VIX` | `US10Y` | `M2_YoY` | `Funding_Rate` | `Stablecoin_MCap_Change`
      
      ---
      
      ## 四、新闻与NLP
      
      ### 商业平台
      - **RavenPack** — ESS事件情感,延迟<50ms,$25K-100K+/年 | ★★★★★
      - **Bloomberg NLP** — 含Terminal | $20K-25K/年 | ★★★★
      
      ### 自建Pipeline(推荐!)
      ```
      Scrapy(50+站点) → SimHash去重 → spaCy NER → FinBERT/GPT-4o-mini情感 → MongoDB+Redis
      ```
      - GPT-4o-mini: ~$5-20/月(日均1000条),输出 sentiment/confidence/affected_coins/event_type
      - FinBERT(ProsusAI/finbert): 免费,金融领域专用
      
      ### 免费新闻API
      CryptoPanic API | NewsAPI.org | Twitter/X API v2 | Reddit API | CryptoCompare
      
      ---
      
      ## 五、高频/Level 2数据
      
      ### 交易所WebSocket对比
      
      | 维度 | Binance | OKX | Bybit |
      |------|---------|-----|-------|
      | 最快更新 | 100ms | 100ms | 100ms |
      | 最大深度档 | 100 | 4000(TBT) | 500 |
      | 稳定性 | ★★★★★ | ★★★★★ | ★★★★ |
      | 流动性 | 最深 | 深 | 深(合约) |
      
      ### LOB重建方法
      1. REST全量快照 + WS增量更新 + 每分钟REST校准
      2. **Cryptofeed库**(bmoscon/cryptofeed) — 统一接口30+交易所,自动LOB重建
      3. **Tardis.dev** — 历史tick级数据回测 | Pro $100/月
      
      ### Tick存储
      - **QuestDB** — >100万行/秒写入,列式压缩10:1-100:1,免费开源(推荐)
      - **kdb+/q** — 华尔街标准,$10K+/年
      - **ClickHouse** — 大规模历史分析
      
      推荐架构: `Exchange WS → Cryptofeed → QuestDB(热) → ClickHouse(冷/回测)`
      
      ---
      
      ## 六、BTC 5min系统升级优先级
      
      ### P0 立即实施(~$78/月)
      1. Coinglass $49 → Funding Rate + OI + Liquidation
      2. CryptoQuant $29 → Exchange Flow
      3. DeFi Llama 免费 → DEX Volume + TVL
      4. FRED 免费 → DXY + VIX + US10Y
      5. Binance WS 免费 → L2 Order Book Imbalance
      
      ### P1 短期(+$78/月)
      6. Santiment $49 → 链上+社交融合
      7. Glassnode Pro $39 → 链上宏观
      8. 自建Scrapy+FinBERT → 升级情绪分析
      
      ### 关键新因子
      - **衍生品**: funding_rate, oi_change_5m, liq_volume_5m, long_short_ratio
      - **微观结构**: obi(前5档), spread, trade_imbalance, vpin
      - **链上**: exchange_net_flow, whale_txn_count, stablecoin_supply_change
      
    • R07-frontier-research-2024-2026.md 5.3 KB
      # R7: 2024-2026年量化金融最前沿研究
      
      > 2026-05-23 | 来源: 23次 web_search + 1次 web_extract
      
      ## 一、因子投资新发现:LLM驱动Alpha挖掘革命
      
      ### 核心突破
      - **AlphaAgent (2025)** — LLM+正则化探索,多Agent系统自动生成抗衰减因子
        - 创新点:"Alpha衰减对抗"机制,RL引导LLM避免冗余因子
        - 开源可用,需IC/IR检验后才能用
      - **QuantaAlpha (2025)** — LLM+进化策略,闭环因子发现优化
      - **AlphaAgentEvo (2025)** — "Agentic RL"概念,进化式因子挖掘
      - **"Automate Strategy Finding with LLM" (ACL EMNLP 2025)** — 系统性让LLM生成Alpha101风格多样化因子
      - **"Reinforcement Fine-Tuning for Alpha" (arXiv 2026.05)** — RFT技术应用于LLM因子发现,最新前沿
      
      ### ⚠️ 拥挤度风险
      2026年研究指出:多团队独立用LLM挖因子→因子相关性急剧上升→拥挤度问题
      
      ### 核心文献
      1. AlphaAgent: arXiv:2502.16789 (2025)
      2. "Automate Strategy Finding with LLM": ACL 2025.findings-emnlp.1005
      3. "Reinforcement Fine-Tuning for Alpha": arXiv:2605.15412 (2026)
      
      ---
      
      ## 二、AI Agent交易系统
      
      ### FinRobot (2024)
      - 多Agent协作交易框架
      - Data Agent + Strategy Agent + Execution Agent分工
      - 已开源,可复现
      
      ### FinCon (2024)
      - LLM驱动的金融对话Agent
      - 自然语言→策略代码生成→回测→优化
      
      ### TradingGPT (2024)
      - GPT架构改造为交易决策系统
      - 多时间尺度注意力机制
      
      ### 多Agent协作交易架构(2025趋势)
      ```
      ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
      │ Data Agent   │  │ Strategy    │  │ Execution   │
      │ (数据采集    │→ │ Agent       │→ │ Agent       │
      │  清洗 特征)  │  │ (因子+信号) │  │ (下单+风控) │
      └─────────────┘  └─────────────┘  └─────────────┘
              ↑               ↑               ↑
          ┌───────────────────────────────────────────┐
          │          Risk Manager Agent               │
          │     (全局风险监控 + 仓位约束)               │
          └───────────────────────────────────────────┘
      ```
      
      ### 实际可用性
      - FinRobot框架可直接使用,但需自定义策略逻辑
      - 回测表现依赖市场状态,无万能架构
      - **建议**:以FinRobot为骨架,嵌入自研因子和信号
      
      ---
      
      ## 三、LLM+量化融合前沿
      
      ### 金融LLM进化路线
      BloombergGPT(2023) → FinGPT(2023) → **FinRobot(2024)** → **Agent-based系统(2025-2026)**
      
      ### 四大应用方向
      
      **1. 因子挖掘**
      - GPT-4生成Alpha101风格因子表达式
      - RL fine-tuning优化因子质量
      - AlphaAgent已实现自动化闭环
      
      **2. 情感分析(超越传统NLP)**
      - 理解加密行业特定语境("diamond hands", "HODL", "rug pull")
      - 多语言(中/英/韩/日)加密社交媒体分析
      - 实时事件分类+影响评估
      
      **3. 市场状态判断(Regime Detection via LLM)**
      - LLM阅读宏观经济报告→判断risk-on/risk-off
      - Fed声明文本分析→利率预期→加密方向
      - 2025研究显示LLM regime判断准确率>传统HMM
      
      **4. 策略代码生成**
      - 自然语言描述→Python策略代码
      - 结合回测验证→迭代优化
      - 风险:生成代码的前视偏差需人工审核
      
      ---
      
      ## 四、加密量化新范式
      
      ### 机构化后的市场新特征
      - BTC/ETH ETF通过后,价格发现从原生交易所转移到ETF市场
      - 期权市场深度显著增加(Deribit + CME)
      - 机构资金导致BTC波动率系统性压缩(年化60-80% → 40-60%)
      
      ### ETF通过后的微结构变化
      - **价格发现转移**: ETF逐步取代原生交易所成为主导
      - **套利机会**: ETF NAV vs 现货价差 + 创建/赎回机制
      - **资金流**: ETF净流入/出成为最强日级信号
      
      ### MEV与AI结合
      - **MEV-Share**: 用户可选择分享MEV收益
      - **MEV-Blocker**: 保护交易免受三明治攻击
      - **AI+MEV**: 用ML预测mempool中的MEV机会,优化Gas竞拍
      - 2026前沿:"MEV Is the PFOF of Crypto" — MEV量化为每笔交易的隐性税收
      
      ---
      
      ## 五、多模态AI在交易中的应用
      
      ### 文本+图表+音频融合
      - **财报会议语音分析**: CEO语气/语速/犹豫 → 真实信心水平
      - **K线图视觉理解**: Vision Transformer直接从K线图提取形态信号
      - **社交媒体多模态**: 文本+图片+视频内容联合分析
      
      ### 实际可用性
      - 财报语音分析已有商业产品(Bounding.ai)
      - K线视觉理解仍处学术阶段
      - 对BTC 5min预测价值有限(更适合中长期)
      
      ---
      
      ## 六、总结:2024-2026关键趋势
      
      1. **LLM因子挖掘**是最大趋势,但需警惕拥挤度
      2. **AI Agent多协作架构**是交易系统设计方向
      3. **ETF价格发现转移**改变了加密市场微结构
      4. **MEV量化**为DeFi交易成本提供新视角
      5. **金融LLM**从BloombergGPT到专用Agent的快速进化
      6. 多模态AI(语音+图表)在特定场景有独特Alpha
      
      ### 对5minbtc v4.0的启示
      - Phase 1: 接入LLM因子挖掘(AlphaAgent思路)
      - Phase 2: 构建Data Agent + Strategy Agent架构
      - Phase 3: 加入ETF资金流作为日级信号
      - 长期:多Agent协作交易系统
      
    • R08-options-volatility.md 6.8 KB
      # R08: 期权与波动率理论深度蒸馏
      
      > 蒸馏范围:R26-30 | 期权Greeks进阶 → 波动率曲面 → Variance Swap → 隐含分布 → 加密期权策略
      
      ---
      
      ## 1. Greeks进阶
      
      ### Delta对冲的离散化误差
      - **连续对冲假设**:Black-Scholes假设无限频率再平衡,现实中每日/每小时再平衡
      - **离散化误差**:Δt越大,对冲误差越大,误差方差 ∝ Γ²σ⁴Δt
      - **最优再平衡频率**:交易成本 vs 对冲误差的权衡 — Whalley & Wilmott (1993) 渐近分析:对冲带宽 ∝ (Γ²σ²S²/κ)^(1/3),κ=交易成本率
      - **加密实践**:Deribit期权Delta对冲需考虑现货T+0、合约杠杆,高频再平衡成本较低
      
      ### Gamma-Theta权衡(核心关系)
      ```
      BS框架:Θ + ½σ²S²Γ + rSΔ = rV(持有成本等式)
      简化:Θ ≈ -½σ²S²Γ(平值附近)
      ```
      - **Gamma Scalping**:做多Gamma → 每次价格波动赚 ½Γ(ΔS)²,但每天付|Θ|
      - **盈亏平衡波动**:实现波动率 > 隐含波动率时Gamma多头盈利
      - **加密特殊性**:BTC实现波动率经常超过隐含(周末跳空),Gamma多头在周五尾盘建仓有统计优势
      
      ### 高阶Greeks
      | Greek | 定义 | 加密实战意义 |
      |-------|------|-------------|
      | **Vanna** | ∂Δ/∂σ = ∂Vega/∂S | 波动率变化时Delta偏移,影响对冲稳定性 |
      | **Volga (Vomma)** | ∂Vega/∂σ | 波动率凸性暴露,long volga = 做多波动率波动率 |
      | **Charm** | ∂Δ/∂t | 到期日附近Delta漂移加速,影响对冲频率决策 |
      | **Color** | ∂Γ/∂t | Gamma的时间衰减速率,短期期权Gamma衰减极快 |
      | **Speed** | ∂Γ/∂S | Gamma随标的变化,衡量对冲再平衡紧迫性 |
      
      ---
      
      ## 2. 波动率曲面建模
      
      ### 局部波动率(Dupire 2024框架)
      - **Dupire公式**:σ²_loc(K,T) = [2∂C/∂T] / [K²∂²C/∂K²]
      - **核心思想**:从市场期权价格反推局部波动率函数 σ(S,t)
      - **优点**:完美拟合所有观察到的期权价格
      - **致命缺陷**:产生"负波动率斜率动态" — 当现货上涨时模型预测波动率下降,与实证相悖
      - **实战定位**:用于奇异期权定价的基准模型,不用于交易
      
      ### 随机波动率模型
      **Heston模型**(最广泛使用):
      ```
      dS = μS dt + √v S dW₁
      dv = κ(θ-v) dt + ξ√v dW₂    (CIR过程)
      Corr(dW₁, dW₂) = ρ           (杠杆效应)
      ```
      - 5参数(κ,θ,ξ,ρ,v₀),半解析解(特征函数+FFT)
      - ρ<0 产生volatility skew(下跌时波动率上升)
      - ξ 控制vol-of-vol(波动率的波动率)
      - **加密应用**:BTC的ρ约-0.3~-0.5(弱于股票的-0.7),ξ约0.8~1.2(远高于股票的0.3)
      
      **SABR模型**(利率市场标准,可适配加密):
      ```
      dF = σ F^β dW₁
      dσ = α σ dW₂
      ```
      - Hagan近似公式直接给出IV → 快速校准
      - β=1时为lognormal SABR,适合BTC(对数正态特征)
      - **实战**:Deribit期权做市商常用SABR族进行快速报价
      
      ### 加密波动率曲面特殊性
      1. **极端Skew**:BTC期权25Δ RR (Risk Reversal) 可达±15 vol points(标普约±5)
      2. **周末效应**:周五→周一跳空,周末IV系统性低估 → 周五尾盘做多Gamma统计盈利
      3. **ETF上市后变化**:2024.1 BTC ETF上市后,短期IV下降约5-8个点,skew变平
      4. **期限结构倒挂**:恐惧事件时短期IV > 长期IV,与VIX类似但幅度更大
      
      ---
      
      ## 3. Variance Swap与波动率交易
      
      ### VIX计算原理
      ```
      σ² = (2/T) Σᵢ [ΔKᵢ/Kᵢ²] e^(RT) Q(Kᵢ)  — 对所有OTM期权求和
      ```
      - 本质是所有OTM期权价格的加权平均,权重 1/K²
      - **模型无关**:不依赖任何波动率模型假设
      
      ### 方差互换定价(Log Contract复制)
      - **核心公式**:Var Swap = E[log²(R)] = 2/T ∫₀ᵀ (1/S_t) dS_t - 2/T log(S_T/S_₀) 的风险中性期望
      - **复制组合**:持有连续K的OTM strip,权重 2/(TK²) dK
      - **离散实现**:选取有限strike的期权近似,误差 ∝ (ΔK)²
      - **方差互换 vs 波动率互换**:
        - 方差互换:可直接用期权strip复制,流动性好
        - 波动率互换:需要凸性调整 E[σ] < √(E[σ²]),无法静态复制
      
      ### 加密波动率指数
      - **BVIV (Binance)**:基于Binance期权,计算类似VIX
      - **DVOL (Deribit)**:基于Deribit期权数据的波动率指数
      - **实战应用**:DVOL-RV spread作为波动率交易信号(DVOL > 历史RV → 卖波动率)
      
      ---
      
      ## 4. 隐含分布提取(Breeden-Litzenberger)
      
      ### 核心公式
      ```
      f(K) = e^(rT) ∂²C/∂K²  — 风险中性概率密度
      ```
      - 从期权价格二阶导数提取隐含PDF
      - **数值实现**:插值IV → 构建光滑IV曲面 → 数值二阶导
      - **关键挑战**:稀疏strike → 插值不稳定 → 需要正则化(Tikhonov/B-spline平滑)
      
      ### 模型无关尾部风险
      - **Left tail mass**:P(S_T < K) = e^(rT) ∂P(K)/∂K(从put导数)
      - **Conditional Value at Risk**:从隐含PDF直接积分计算
      - **加密实战**:Deribit BTC期权提取隐含分布:
        - 观察到左尾肥于右尾(崩盘恐惧)
        - ETF上市后左尾概率下降但未消失
        - 隐含分布的偏度变化领先于现货价格
      
      ---
      
      ## 5. 加密期权实战策略
      
      ### 波动率卖出策略(统计表现)
      | 策略 | 构造 | 典型胜率 | 加密实证 |
      |------|------|---------|---------|
      | **Iron Condor** | 卖OTM call+put + 买更OTM保护 | 70-80% | 月度IC在BTC上月均收益2-5% IV,尾部风险需严格止损 |
      | **Short Strangle** | 卖OTM call + 卖OTM put | 65-75% | BTC周度strangle历史回测:盈利期数>70%,但亏损期数单次可吃掉5-8次盈利 |
      | **Calendar Spread** | 买远月卖近月同strike | 60-65% | 利用期限结构contango,近月IV衰减更快 |
      
      ### 波动率套利(IV-RV Spread)
      - **核心逻辑**:长期看,IV > RV(波动率风险溢价,VRP)
      - **BTC VRP**:历史均值约3-5个vol points,但时而转负(如2024.3突破行情)
      - **交易方法**:卖出方差互换(或近似用short straddle)+ Delta对冲
      - **加密优势**:高VRP + 做市商较少 = 更大的结构性套利机会
      
      ### 期权+现货组合
      - **Covered Call**:持有BTC + 卖虚值call → 增强收益,放弃上行
      - **Protective Put**:持有BTC + 买虚值put → 尾部保险,成本约年化5-10% IV
      - **Collar**:持有BTC + 卖call + 买put → 零成本保险,限制上下行
      
      ---
      
      ## 关键参考文献
      - Dupire (1994) "Pricing with a Smile" — 局部波动率奠基
      - Heston (1993) "A Closed-Form Solution for Options with Stochastic Volatility"
      - Hagan et al. (2002) "Managing Smile Risk" — SABR模型
      - Breeden & Litzenberger (1978) "Prices of State-Contingent Claims Implicit in Option Prices"
      - Carr & Madan (2001) "Optimal Positioning in Market Completeness" — Variance Swap理论
      - Demeterfi et al. (1999) "More Than You Ever Wanted to Know About Volatility Swaps"
      - Alexander & Imeraj (2023) "Crypto Options and Volatility" — 加密期权实证
      
    • R09-market-microstructure.md 8.6 KB
      # R09: 市场微结构理论深度蒸馏
      
      > 蒸馏范围:R31-35 | 订单簿动力学 → 价格发现 → Kyle Lambda → 最优执行 → 加密微结构
      
      ---
      
      ## 1. 订单簿动力学
      
      ### 限价订单簿(LOB)随机模型
      
      **Cont & de Larrard (2013) — Markovian LOB模型**:
      - 将LOB建模为多层泊松过程:每个price level的到达/取消是独立的泊松事件
      - **中间价(Mid Price)**:m_t = (best_bid + best_ask) / 2
      - **LOB动态**:
        - 限价单到达率:λ (距最优价的距离函数)
        - 市价单到达率:μ
        - 取消率:θ × 排队深度
      - **关键结论**:在μ≈λ的平衡状态下,spread ≈ 常数,价格扩散近似布朗运动
      
      ### 微观价格(Microprice)
      
      **Stoikov (2018) — The Microprice**:
      ```
      M = bid + s × ρ_bid / (ρ_bid + ρ_ask)
      ```
      - s = ask - bid (spread)
      - ρ_bid, ρ_ask = 最优买卖队列深度
      - **直觉**:如果买方队列深度远大于卖方 → microprice偏向bid → 卖方压力更大
      - **实战**:比mid price更好的短期价格预测器,尤其在高频场景
      
      **扩展 — 加权微观价格**:
      ```
      M_weighted = bid + s × Σ(w_i × ρ_bid_i) / Σ(w_i × (ρ_bid_i + ρ_ask_i))
      ```
      - 考虑多层LOB信息,w_i为衰减权重
      
      ### 订单流不平衡(OFI) — 预测力核心
      
      **Cont, Kukanov & Stoikov (2014)**:
      ```
      OFI_t = ΔV_t^B × I{ΔP_t^B ≥ 0} - ΔV_t^A × I{ΔP_t^A ≤ 0}
      ```
      - V^B = bid side累计深度, V^A = ask side累计深度
      - **核心发现**:OFI对下一时刻的价格变化有显著预测力(R² ≈ 30-70% in high-freq)
      - **多层OFI**:将OFI扩展到LOB的多层深度,预测力从单层R²~40%提升到多层R²~65%
      - **加密实战**:BTC永续合约的OFI在5分钟频率R²约15-25%(低于股票微秒级,因为加密市场更噪声)
      
      ### LOB弹性与恢复速度
      - **LOB恢复时间**:大单冲击后,LOB恢复到稳态的时间
      - **估计方法**:观察大单后的depth profile变化,拟合指数恢复模型
      - **加密实证**:BTC市场LOB恢复约2-5秒(Binance永续),传统市场约0.1-0.5秒
      
      ---
      
      ## 2. 价格发现机制
      
      ### 信息不对称模型
      
      **Glosten-Milgrom (1985) — 信息不对称做市模型**:
      - 做市商面对知情交易者(informed)和噪声交易者(uninformed)
      - 做市商的最优bid/ask:
      ```
      ask = E[V | 有人在买] = [α·V̄_high + (1-α)·V̄] / [α + (1-α)]
      bid = E[V | 有人在卖] = [α·V̄_low + (1-α)·V̄] / [α + (1-α)]
      ```
      - α = 知情交易者比例,V̄ = 无条件期望,V̄_high/low = 利好/利空时的条件期望
      - **核心结论**:spread = 信息不对称补偿 + 订单处理成本 + 逆向选择风险
      
      ### PIN (Probability of Informed Trading)
      
      **Easley, Hvidkjaer & O'Hara (2002)**:
      ```
      PIN = α·μ / (α·μ + 2α·δ·ε_b + 2(1-α)·ε_b)
      ```
      - α = 信息事件发生概率
      - μ = 知情交易者到达率
      - ε_b, ε_s = 买/卖噪声交易者到达率
      - δ = 好消息概率
      - **估计**:用EM算法从每日买卖单量序列估计参数
      - **实证**:高PIN股票有更高预期收益(逆向选择溢价)
      
      ### VPIN (Volume-Synchronized PIN)
      
      **Easley, López de Prado & O'Hara (2012)**:
      - 用成交量切片(volume clock)替代时间切片,更稳定
      - **核心优势**:不需要区分买卖单(用bulk volume classification近似)
      - **实战应用**:预测"毒性订单流"(toxic flow) → 闪崩预警
      - **加密应用**:BTC永续合约VPIN在极端事件前显著上升(如2024.8.5日圆套利平仓)
      
      ---
      
      ## 3. Kyle Lambda与价格影响
      
      ### Kyle's Lambda (λ)
      
      **Kyle (1985) — Insider Trading模型**:
      ```
      ΔP = λ·Q + ε    (Q = 净订单流)
      ```
      - λ = Kyle's Lambda = 价格影响系数
      - **Kyle深度**:1/λ = 需要多少订单流才能移动价格1单位
      - **估计方法**:
        1. **回归法**:ΔP_t = λ·OFI_t + ε_t (简单OLS)
        2. **高频法**:用tick-by-tick数据拟合
        3. **Amihud ILLIQ**:|r_t| / (V_t × P_t) — 非流动性代理
      
      ### 价格影响的平方根定律
      
      **Almgren-Thum (2005) / Toth et al. (2011)**:
      ```
      临时影响 = η × σ × √(Q/V) × (Q/V)^γ
      ```
      - η = 模型常数, σ = 日波动率, Q = 订单量, V = 日均成交量
      - γ ≈ 0.5(平方根定律)→ γ ∈ [0.3, 0.7] 根据市场
      - **关键洞察**:影响与订单规模成亚线性关系(不是线性的!)
      
      **临时 vs 永久影响**:
      - **临时影响**:来源于LOB消耗,随时间恢复(衰减时间 = LOB恢复时间)
      - **永久影响**:来源于信息效应,不恢复
      - **典型比例**:永久/临时 ≈ 0.2-0.4(大部分冲击是临时的)
      
      ### 加密市场中的Kyle Lambda估计
      - **BTC永续合约**:λ ≈ 0.5-2 bp per $1M 净订单流(正常市场)
      - **极端市场**:λ可放大5-10倍(如闪崩时LOB被抽空)
      - **跨交易所差异**:Binance λ最小(流动性最深),OKX/Bybit略高
      
      ---
      
      ## 4. 最优执行理论
      
      ### Almgren-Chriss模型完整推导
      
      **目标函数**:
      ```
      min_{n_1,...,n_N} E[Cost] + λ_risk × Var[Cost]
      ```
      - n_k = 第k个时间间隔的执行量
      - Cost = 执行价格偏离VWAP的部分
      
      **冲击模型**:
      ```
      执行价格: S̃_k = S_k + η_temp × (n_k / τ) + γ_perm × Σ_{j<k} n_j
      ```
      - η_temp = 临时冲击系数(消耗LOB)
      - γ_perm = 永久冲击系数(信息效应)
      - τ = 时间间隔长度
      
      **最优轨迹(闭式解)**:
      ```
      n_k* = (1/2N) × (S_end - S_start) + (κ/2) × tanh(κ(T-t_k)) × position
      ```
      - κ = √(λ_risk × γ_perm / (2 × η_temp²))
      - **特征**:两端加速、中间减速的U型轨迹
      - **加密特殊考量**:
        1. 24/7市场无收盘 → 需自定义执行窗口
        2. 流动性碎片化跨所 → 需多 Venue优化
        3. 波动率日内pattern → BTC凌晨4-8 UTC流动性最差
      
      ### VWAP vs TWAP vs 最优执行
      
      | 方法 | 策略 | 优势 | 劣势 |
      |------|------|------|------|
      | **TWAP** | 均匀分时 | 简单,无预测依赖 | 不考虑成交量pattern |
      | **VWAP** | 按历史成交量比例分时 | 适应成交量日内pattern | 假设历史pattern延续 |
      | **Almgren-Chriss** | 风险厌恶最优轨迹 | 理论最优,平衡成本与风险 | 需估计冲击参数 |
      
      ### 加密市场执行特殊挑战
      1. **流动性碎片化**:BTC流动性分散在Binance/OKX/Coinbase/Bybit等 → 多venue路由
      2. **滑点模型**:加密滑点比传统市场更大(做市商较少、LOB较薄)
      3. **资金费率影响**:永续合约持仓成本需纳入执行模型
      4. **延迟套利**:跨所价格差异在ms级消失,需co-location
      
      ---
      
      ## 5. 加密微结构特殊现象
      
      ### 加密 vs 传统市场微结构差异
      
      | 维度 | 传统市场 | 加密市场 |
      |------|---------|---------|
      | **交易时间** | 交易所限时(9:30-16:00) | 24/7/365 |
      | **做市商** | 指定做市商(DMM) | 自发做市商,无义务 |
      | **价格发现** | 集中(交易所) | 碎片化(CEX+DEX) |
      | **熔断** | 有(如NYSE LULD) | 无 |
      | **结算** | T+1/T+2 | 即时(on-chain) |
      | **透明度** | LOB公开,大宗延迟报告 | LOB公开,链上可追踪 |
      
      ### 资金费率对价格发现的贡献
      - **资金费率(Funding Rate)**:永续合约的多空平衡指标
      - **价格发现贡献**:永续合约价格领先现货约100-500ms(研究发现)
      - **套利机制**:当funding rate极端(>0.1%)时,套利者涌入压平 → 包含信息量
      - **实战信号**:极端funding + 大仓位变化 = 强方向信号
      
      ### 跨交易所价格发现
      - **价格领先关系**:Binance > OKX > Bybit(按流动性排序)
      - **信息份额模型(Hasbrouck 1995)**:用VECM分解跨所价格发现的贡献度
      - **BTC实证**:Binance贡献约40-50%价格发现,Coinbase约20-25%(美元对)
      - **ETF影响**:2024年后ETF成为BTC价格发现的重要贡献者(约15-20%)
      
      ### ETF后的微结构变化
      1. **流动性提升**:ETF引入传统做市商,BTC现货spread收窄约20%
      2. **相关性增加**:BTC与纳指相关性从0.2升至0.4-0.5
      3. **波动率下降**:短期实现波动率下降约5-10个点
      4. **价格发现迁移**:部分价格发现从Deribit/Binance迁移到CME/ETF
      
      ---
      
      ## 关键参考文献
      - Cont & de Larrard (2013) "Price Dynamics in a Markovian Limit Order Book Market"
      - Stoikov (2018) "The Micro-Price: A High Frequency Estimator of Future Prices"
      - Cont, Kukanov & Stoikov (2014) "The Price Impact of Order Book Events"
      - Kyle (1985) "Continuous Auctions and Insider Trading"
      - Glosten & Milgrom (1985) "Bid, Ask and Transaction Prices in a Specialist Market"
      - Easley et al. (2012) "Flow Toxicity and Liquidity in a High-Frequency World" (VPIN)
      - Almgren & Chriss (2001) "Optimal Execution of Portfolio Transactions"
      - Hasbrouck (1995) "One Security, Many Markets: Determining the Contributions to Price Discovery"
      - Cartea, Jaimungal & Penalva (2015) "Algorithmic and High-Frequency Trading" (教科书)
      
    • R10-risk-management-frontier.md 7.3 KB
      # R10: 风险管理前沿深度蒸馏
      
      > 蒸馏范围:R36-40 | CVaR → EVT极值理论 → 下行风险 → Regime Detection → 压力测试
      
      ---
      
      ## 1. CVaR与尾部风险建模
      
      ### VaR vs CVaR (Expected Shortfall)
      
      **VaR_α(P&L)** = inf{l : P(L > l) ≤ 1-α} — 损失分布的α分位数
      **CVaR_α** = E[L | L > VaR_α] — 超过VaR的条件期望损失
      
      关键差异:
      - VaR只告诉"最多亏多少"(在置信水平内) — 不关心尾部形状
      - CVaR告诉"如果突破VaR,平均亏多少" — 捕捉尾部信息
      - **CVaR是一致性风险度量**(满足次可加性),VaR不是
      - Basel III已从VaR转向ES(CVaR)作为银行监管标准
      
      ### Rockafellar-Uryasev线性规划方法
      
      **核心突破**:CVaR可以通过线性规划直接优化
      
      ```
      min_{x,ζ} F_α(x,ζ) = ζ + (1/(1-α)) · E[max(L(x) - ζ, 0)]
      ```
      - ζ = VaR候选值,x = 组合权重
      - **线性化**:引入辅助变量 u_i ≥ 0, u_i ≥ L_i - ζ
      - 离散场景下变为标准线性规划 → 可嵌入组合优化
      - **实用价值**:不需要知道分布的具体形式,只需场景样本
      
      ### 加密市场CVaR特征
      - BTC日度95% CVaR约5-8%(传统股票市场约2-3%)
      - 99% CVaR可达15-25%(2020.3.12单日-40%)
      - CVaR/VaR比值(尾部厚度指标):BTC约1.5-2.0,标普约1.2-1.4
      
      ---
      
      ## 2. 极值理论(EVT)
      
      ### POT (Peaks Over Threshold)方法
      
      **核心定理(Balkema-de Haan-Pickands)**:
      对于足够高的阈值u,超出量的分布收敛于广义Pareto分布(GPD):
      
      ```
      P(X-u ≤ y | X > u) ≈ G(y; ξ, β) = 1 - (1 + ξy/β)^(-1/ξ)    for y > 0
      ```
      - ξ = shape parameter (尾部厚度)
        - ξ < 0: 薄尾(有上界)
        - ξ = 0: 指数尾部(正态类)
        - ξ > 0: 肥尾(Pareto类) — **金融市场典型**
      - β = scale parameter
      
      **阈值u的选择**:
      - 经验法则:u ≈ 90-95%分位数
      - 精确方法:平均超出量图(Mean Excess Plot) — 在超过u后应为线性
      
      ### Hill估计器
      
      用于估计shape parameter ξ:
      ```
      ξ_Hill = (1/k) Σ_{i=1}^{k} [log(X_(i)) - log(X_(k+1))]
      ```
      - X_(i) = 第i个顺序统计量(降序)
      - k = 使用的尾部观测数
      - **k的选择**:k太小→方差大,k太大→偏差大 → 平衡点约为n^0.6
      
      ### 加密市场EVT参数估计
      
      **BTC日度收益率EVT估计**:
      - ξ ≈ 0.25-0.45 (显著肥尾,高于标普的0.15-0.25)
      - 右尾略薄于左尾(崩盘恐惧不对称性)
      - 极端事件(>10σ)出现频率远高于正态预测 — EVT比正态更准确
      
      **实战应用**:
      1. **极端VaR/CVaR计算**:用GPD拟合尾部 → 外推计算99.9% VaR
      2. **压力损失估计**:基于EVT估计单日最大可能损失
      3. **尾部风险定价**:期权隐含分布 vs EVT拟合分布的差异
      
      ---
      
      ## 3. 下行风险(DSR)进阶
      
      ### Sortino Ratio
      
      ```
      Sortino = (R_p - R_f) / σ_d
      σ_d = √(E[min(R_p - MAR, 0)²])  — 下行半标准差
      ```
      - MAR = Minimum Acceptable Return (通常取0或无风险利率)
      - **vs Sharpe**:Sharpe惩罚所有波动(包括上行),Sortino只惩罚下行
      - **适用场景**:非对称收益分布(如期权策略、趋势跟踪)
      
      ### Omega函数
      
      ```
      Ω(r) = ∫_r^∞ [1 - F(x)] dx / ∫_{-∞}^r F(x) dx
      ```
      - r = 收益率阈值
      - **Omega > 1**:获得超过r的概率加权收益高于低于r的损失
      - **Omega是完整的分布描述** — 比任何单一统计量包含更多信息
      - **优化应用**:最大化Omega(r) → 不需要假设分布形态
      - **加密实战**:BTC策略的Omega(0)在牛市区间可达3-5,熊市可降至0.5
      
      ### MAR设定方法
      1. **零值MAR**:最简单,确保正期望
      2. **无风险利率**:与Sharpe等价
      3. **目标收益率**:基于资金成本/机会成本
      4. **动态MAR**:随市场regime调整(牛市更高/熊市更低)
      
      ---
      
      ## 4. Regime Detection
      
      ### Hidden Markov Model (HMM)
      
      **模型设定**:
      ```
      隐状态 S_t ∈ {1,...,K} (K=2-4个regime)
      P(S_t = j | S_{t-1} = i) = p_ij (转移概率矩阵)
      观测 r_t | S_t = j ~ N(μ_j, σ_j²) (regime-dependent分布)
      ```
      - **估计方法**:Baum-Welch算法(EM)估计参数,Viterbi解码最可能状态路径
      - **BTC典型regime**:
        - Regime 1 (低波动牛市):μ>0, σ~2-3%
        - Regime 2 (高波动熊市):μ<0, σ~5-8%
        - Regime 3 (震荡):μ≈0, σ~3-4%
      
      ### Markov Switching模型 (Hamilton 1989)
      
      - HMM在计量经济学的经典实现
      - **扩展**:允许regime-dependent的AR系数 → 不同regime下动量/反转强度不同
      - **滤波**:实时估计当前处于各regime的概率 P(S_t=j | r_1,...,r_t)
      - **实战**:用滤波概率作为策略的条件开关
      
      ### Change-Point Detection (PELT算法)
      
      **PELT (Pruned Exact Linear Time)** — Killick et al. (2012):
      - 精确检测时间序列的结构断点
      - 时间复杂度O(n)(剪枝加速)
      - **vs HMM**:不需要预设regime数量和参数
      - **应用**:检测BTC波动率regime切换点,用于:
        1. 自适应止损调整
        2. 仓位大小动态调整
        3. 策略参数切换触发器
      
      ### 加密市场Regime特征
      - **切换频率**:BTC平均每30-60天切换一次regime(传统市场约90-180天)
      - **可预测性**:基于链上指标(活跃地址、交易所净流入)可提前1-3天预警regime切换
      - **ETF影响**:2024年后BTC regime与美股regime相关性增加
      
      ---
      
      ## 5. 压力测试与极端场景
      
      ### 历史场景回放
      
      | 事件 | BTC跌幅 | 持续时间 | 特征 |
      |------|--------|---------|------|
      | **2020.3.12** (COVID) | -40% | 1天 | 流动性枯竭,spread扩大10x |
      | **2021.5.19** (中国禁矿) | -35% | 3天 | 政策冲击,矿工抛售 |
      | **2022.11.9** (FTX崩盘) | -25% | 2天 | 交易所信任危机 |
      | **2024.8.5** (日圆套利平仓) | -18% | 1天 | 宏观跨市场传染 |
      
      ### 反向压力测试(Reverse Stress Testing)
      1. **定义**:从"什么会让策略崩溃"倒推
      2. **方法**:
         - 识别策略的关键风险因子暴露
         - 找到使策略亏损超过阈值的因子变动组合
         - 评估该组合发生的概率(结合EVT)
      3. **加密示例**:
         - 资金费率策略:交易所同时暂停提币 + funding rate极端
         - 做市策略:连续闪崩 + 流动性蒸发 + 交易所宕机
      
      ### 蒙特卡洛压力测试
      - **相关矩阵扰动**:将正常相关矩阵乘以扰动因子,模拟极端相关性变化
        - 正常BTC-SPX相关性~0.3 → 压力下可升至0.7+
        - 相关性趋同(correlation breakdown)是最致命的压力效应
      - **肥尾注入**:用t分布(自由度3-5)替代正态 → 生成极端场景
      - **条件场景**:在特定宏观事件(如美联储紧急降息)条件下模拟
      
      ### 加密特有风险因子
      1. **交易所风险**:交易所暂停/倒闭(FTX教训) → 分散托管
      2. **链上风险**:DeFi协议被黑/预言机操纵 → 审计+限额
      3. **监管风险**:主要国家禁止/限制 → 地理分散
      4. **技术风险**:网络拥堵/分叉 → 多链备份
      5. **稳定币风险**:USDT/USDC脱锚 → 多稳定币分散
      
      ---
      
      ## 关键参考文献
      - Rockafellar & Uryasev (2000) "Optimization of Conditional Value-at-Risk"
      - Balkema & de Haan (1974) / Pickands (1975) — POT定理
      - Hill (1975) — Hill估计器
      - Hamilton (1989) "A New Approach to the Economic Analysis of Nonstationary Time Series"
      - Killick, Fearnhead & Eckley (2012) "Optimal Detection of Changepoints With a Linear Computational Cost"
      - Sortino & Price (1994) "Performance Measurement in a Downside Risk Framework"
      - Acerbi & Tasche (2002) "On the Coherence of Expected Shortfall"
      - Basel III FRTB (Fundamental Review of the Trading Book)
      
    • R11-institutional-methodology.md 5.4 KB
      # R11: 顶尖量化机构方法论深度蒸馏
      
      > 蒸馏范围:R41-45 | Renaissance → Citadel → Two Sigma → DE Shaw → 通用方法论 → Simons精华
      
      ---
      
      ## 1. Renaissance Technologies / Jim Simons
      
      ### Medallion Fund 核心数据
      - 年化66%(费前)/39%(费后),1988-2018
      - 费率:5%管理费 + 44%业绩提成
      - 自2005年仅对员工开放,管理~$10B
      - 每年交易量超全美股市总量
      
      ### "壁虎式交易"策略核心
      - **极短持仓**:秒级到数天,大部分1-3天平仓
      - **高频换仓**:大量小额利润复利增长
      - **不预测,只交易**:不预测方向,寻找统计规律
      - **信号组合>单一预测**:数百个弱信号组合形成统计优势
      
      > "If Medallion discovered a profitable signal — the question was never whether this signal made intuitive sense. The question was whether the statistical evidence was overwhelming." — Zuckerman
      
      ### 信号衰减管理框架
      - 实时监控每个信号的IC/预测力
      - 衰减检测系统:识别信号何时失去预测力
      - 信号寿命分布:毫秒级(LOB不平衡) → 小时级(统计套利) → 天级(动量) → 月级(价值)
      - **关键**:当信号衰减时,必须已有新信号准备替代
      
      ### Simons的数学哲学
      1. **数学作为共同语言** — 数学家/物理学家/语言学家用数学协作
      2. **不讲故事,只看数据** — 完全拒绝叙事,只信统计证据
      3. **模式识别>因果解释** — 不需要理解"为什么",只要"是什么"
      4. **概率思维** — 接受近半数交易亏损,统计优势确保整体盈利
      
      ### 组织结构:科学家>金融人
      - 刻意避开华尔街背景,招聘STEM博士
      - 开放协作文化,员工共享想法
      - Peter Brown & Robert Mercer来自IBM语音识别团队,将NLP应用于市场
      
      ---
      
      ## 2. Citadel Securities
      
      - 2025年交易收入$122亿,处理~35%美国散户股票交易
      - **定价引擎**:多资产统一定价(股票/期权/FICC)
      - **PFOF战略**:年投~$10亿获取订单流
      - **风险管理**:库存翻转(flip fast) + 逆向选择防范 + 跨资产对冲
      - **Ken Griffin哲学**:"We're in the research business first"
      - **Pod模式**:~200个独立交易团队,中央风险管理,动态资本分配
      
      ---
      
      ## 3. Two Sigma
      
      - 2001年创立,管理~$600B,员工2000+
      - **ML优先**:深度学习/随机森林系统化挖掘alpha,不依赖人工设计因子
      - **分布式计算**:Apache Mesos + Google Cloud混合架构,处理PB级数据
      - **Overdeck哲学**:"数据中蕴含的信号比人类直觉更可靠"
      - 技术基础设施是核心竞争力,不是成本中心
      
      ---
      
      ## 4. DE Shaw
      
      - 1988年创立,量化对冲基金先驱,管理~$600B
      - David Shaw双重身份:投资公司 + DESRES(开发Anton超级计算机做分子动力学)
      - **科学研究方法论**:假设可证伪 → 严格样本外测试 → 持续监控
      - **系统化因子研究Pipeline**:假设→数据→因子→IC检验→衰减分析→Walk-forward→组合整合→渐进部署
      - **混合模式**:系统化 + 自由裁量 + 混合策略
      - Oculus Fund 2024年回报36.1%
      
      ---
      
      ## 5. 可提炼的通用方法论
      
      ### 信号衰减测量框架
      ```
      滚动IC分析 → 指数衰减拟合 IC(t) = IC₀·e^(-λt)
      → 结构断点检测 → 衰减归因(竞争/制度/噪声)
      ```
      - 建立信号仪表板:实时监控IC/IR
      - 设衰减阈值:IC降X%触发审查
      - 维护信号储备池:始终有新信号在测试
      
      ### 回测过拟合防范(Bailey/Borwein/Lopez de Prado)
      
      **PBO (Probability of Backtest Overfitting)**:
      - CSCV方法:将数据分M段,检查训练/测试排名一致性
      
      **DSR (Deflated Sharpe Ratio)**:
      - 考虑试验次数N、样本长度T、偏度γ₃、峰度γ₄
      - 修正公式确保观测到的夏普不是多重检验的假阳性
      
      **实用措施**:限制超参数空间、预注册假设、20-30%严格样本外、Walk-forward、多市场验证
      
      ### 科学方法在量化中的应用
      ```
      假设(先于数据,可证伪) → 严格检验(预定方法) → 独立复现
      → 部署决策(组合相关性) → 持续监控(衰减检测)
      ```
      
      ---
      
      ## 6. Jim Simons访谈精华
      
      ### 六大核心哲学
      
      1. **"我们不预测,我们交易"** — 寻找统计规律而非方向预测
      2. **"从数据开始,不从模型开始"** — 不预设市场应该怎样,让数据说话
      3. **"数学是共同语言"** — 跨学科协作的基础
      4. **"信号vs噪声"** — 99%噪声,1%信号,工作核心是分离
      5. **"永远不要信任单一信号"** — 组合提供鲁棒性
      6. **"我们不覆盖模型"** — 系统化交易的纪律:模型决策,人不干预
      
      ### 信号组合层次架构
      ```
      Layer 1: 单信号 — IC ~0.01-0.05,独立计算衰减/稳健性
      Layer 2: 信号聚类 — 相关性高的合并为族,去冗余
      Layer 3: 组合优化 — 预期收益+风险+交易成本+滑点
      Layer 4: 风险控制 — 仓位限制/因子暴露/极端减仓
      ```
      
      **弱信号组合效应**:100个IC=0.02的独立信号 → 组合IC可达0.20
      
      ### 持续迭代哲学
      - "好的科学家永远不会停止改进模型"
      - Medallion从来不是同一个基金 — 底层模型在不断演进
      - 永远不要停止研究,市场在变模型必须变
      
      ---
      
      ## 参考文献
      - Zuckerman (2019) "The Man Who Solved the Market"
      - Bailey, Borwein, Lopez de Prado & Zhu (2016) "The Probability of Backtest Overfitting"
      - Bailey & Lopez de Prado (2014) "The Deflated Sharpe Ratio"
      - Lopez de Prado (2018) "Advances in Financial Machine Learning"
      - Simons MIT Lecture / TED Talk / Various speeches
      
    • R12-integration-blueprint.md 9.9 KB
      # R12: 50轮蒸馏整合 — 知识图谱与策略框架升级蓝图
      
      > 蒸馏范围:R46-50 | 11份报告 → 知识图谱 → 升级蓝图 → 知识库索引
      
      ---
      
      ## 一、量化知识图谱
      
      ### 1. 核心知识域关系图
      
      ```
                          ┌─────────────────┐
                          │  数据层 (R06)    │
                          │ Alt Data / 链上  │
                          │ / 宏观 / NLP    │
                          └────────┬────────┘
                                   │ 输入
                          ┌────────▼────────┐
                          │  因子层 (R01)    │
                          │ Alpha101 / IC   │
                          │ / Barra / LLM   │
                          └────────┬────────┘
                                   │ 信号
                    ┌──────────────┼──────────────┐
                    │              │              │
           ┌────────▼──┐  ┌───────▼──────┐  ┌───▼────────┐
           │ 统计套利   │  │ ML因子挖掘   │  │ 微结构信号  │
           │ (R02)     │  │ (R05)       │  │ (R09)      │
           │ OU过程    │  │ LightGBM    │  │ OFI/Micro  │
           │ 协整/KF   │  │ Transformer │  │ price      │
           └────────┬──┘  └───────┬──────┘  └───┬────────┘
                    │              │              │
                    └──────────────┼──────────────┘
                                   │ 组合
                          ┌────────▼────────┐
                          │  组合层 (R04)    │
                          │ BL/HRP/Kelly   │
                          │ / 风险平价     │
                          └────────┬────────┘
                                   │ 优化
                    ┌──────────────┼──────────────┐
                    │              │              │
           ┌────────▼──┐  ┌───────▼──────┐  ┌───▼────────┐
           │ 期权策略   │  │ 风险管理     │  │ 执行层     │
           │ (R08)     │  │ (R10)       │  │ (R09)      │
           │ 波动率曲面 │  │ CVaR/EVT    │  │ Almgren    │
           │ 隐含分布   │  │ Regime/压力 │  │ -Chriss    │
           └────────┬──┘  └───────┬──────┘  └───┬────────┘
                    │              │              │
                    └──────────────┼──────────────┘
                                   │
                          ┌────────▼────────┐
                          │  机构方法论     │
                          │  (R11)          │
                          │ Simons哲学     │
                          │ 信号组合/衰减  │
                          └─────────────────┘
      
           跨域连接:
           ├── 加密专属 (R03): 资金费率 ↔ 统计套利 ↔ 微结构
           ├── 前沿研究 (R07): LLM因子 ↔ 因子层, AI Agent ↔ 执行层
           └── 数据→因子→策略→风控→执行 完整链条
      ```
      
      ### 2. 数学工具依赖图
      
      ```
      概率论 ─→ 随机过程 ─→ ┬─ OU过程 (R02: 统计套利均值回归)
                             ├─ CIR过程 (R08: Heston随机波动率)
                             └─ 泊松过程 (R09: LOB动力学)
      
      线性代数 ─→ ┬─ 协整/Johansen (R02: 配对交易)
                   ├─ PCA (R01: Barra风险模型)
                   └─ 协方差矩阵优化 (R04: BL/HRP)
      
      凸优化 ─→ ┬─ Rockafellar-Uryasev (R10: CVaR优化)
                 ├─ Almgren-Chrass (R09: 最优执行)
                 └─ Kelly criterion (R04: 最优下注)
      
      贝叶斯推断 ─→ ┬─ Black-Litterman (R04: 观点注入)
                     ├─ Kalman滤波 (R02: 动态对冲比)
                     └─ HMM (R10: Regime Detection)
      
      极值理论 ─→ ┬─ GPD/POT (R10: 尾部风险)
                   └─ Hill估计 (R10: shape parameter)
      
      信息论 ─→ ┬─ Breeden-Litzenberger (R08: 隐含分布)
                 └─ PIN/VPIN (R09: 知情交易概率)
      ```
      
      ---
      
      ## 二、策略框架升级蓝图
      
      ### 阶段1: 数据管道升级 (优先级P0)
      
      **现状**:Twitter情绪 + Reddit + Cointelegraph + CoinDesk 4源
      **升级目标**:
      
      | 新增数据源 | 来源 | 月成本 | 预期Alpha增量 |
      |-----------|------|-------|-------------|
      | 链上鲸鱼追踪 | Arkham API | 免费 | 高(提前1-3天信号) |
      | 链上指标 | Glassnode Pro | $39 | 中(长周期regime) |
      | DEX链上 | Dune Pro | $29 | 中(DEX价格发现) |
      | 衍生品数据 | Coinglass API | $49 | 高(资金费率/清算) |
      | OFI微结构 | Binance WebSocket | 免费 | 高(5min级预测) |
      | 隐含波动率 | Deribit API | 免费 | 中(波动率regime) |
      
      **实施**:每个数据源写独立采集脚本 → Redis缓存 → 统一特征工程接口
      
      ### 阶段2: 因子库升级 (优先级P0)
      
      **现状**:5minbtc v3.5.1, 8因子(EMA/RSI/MACD/BB/ATR等)
      **升级方案**:
      
      1. **微结构因子** (基于R09):
         - OFI(订单流不平衡) → 5分钟预测R²可达15-25%
         - Microprice偏离 → 中间价vs微观价格差作为动量因子
         - LOB深度不平衡 → 买/卖压力指标
      
      2. **波动率因子** (基于R08):
         - IV-RV spread → 波动率风险溢价信号
         - 波动率期限结构斜率 → 短期vs长期波动率预期差
         - 隐含分布偏度 → 尾部风险方向
      
      3. **链上因子** (基于R03+R06):
         - 交易所净流入/流出 → 卖压/买压指标
         - 鲸鱼钱包活跃度 → 大户行为信号
         - 资金费率极值 → 过热/过冷信号
      
      4. **LLM增强因子** (基于R07):
         - 新闻情绪score → 替代当前手动搜索
         - LLM提取的事件类型 → 分类情绪(监管/技术/宏观)
      
      ### 阶段3: 预测模型升级 (优先级P1)
      
      **现状**:贝叶斯log-odds → P(UP/DOWN/NEUTRAL)
      **升级方案**:
      
      1. **多模型集成**:
         - LightGBM因子挖掘 → 输出因子重要性排名
         - TFT(Temporal Fusion Transformer) → 多时间尺度注意力
         - 贝叶斯引擎 → 保留作为uncertainty quantification
         - **集成**:加权平均 or Stacking
      
      2. **Regime-aware预测** (基于R10):
         - HMM检测当前regime → 每个regime独立模型
         - Regime概率作为模型权重 → 平滑切换
         - 预期提升:减少regime转换期间的误判
      
      3. **信号组合框架** (基于R11/Simons):
         - 每个因子独立计算IC/衰减率
         - 信号聚类去冗余
         - 组合优化考虑交易成本
      
      ### 阶段4: 风险管理升级 (优先级P1)
      
      **现状**:方向准确率62.7%, MAE 0.069%
      **升级方案**:
      
      1. **CVaR止损** (基于R10):
         - 用GPD拟合BTC收益尾部 → 计算实时CVaR
         - 动态止损 = CVaR_95% × 仓位调整系数
      
      2. **Regime-aware仓位** (基于R10):
         - 低波动regime → 仓位放大1.5x
         - 高波动regime → 仓位缩小0.5x
         - 震荡regime → 仓位0.8x
      
      3. **压力测试框架** (基于R10):
         - 历史场景回放(4个极端事件)
         - 蒙特卡洛相关矩阵扰动
         - 每日自动运行,超限报警
      
      ### 阶段5: 期权策略扩展 (优先级P2)
      
      **现状**:无期权能力
      **升级方案**:
      
      1. **波动率信号** (基于R08):
         - DVOL-RV spread → 卖波动率时机
         - 隐含分布偏度变化 → 方向辅助
      
      2. **期权+现货组合**:
         - 方向预测+covered call → 增强收益
         - 极端fear时买protective put → 尾部保险
      
      3. **执行路径**:
         - 接入Deribit API → 实时IV/ Greeks
         - 先做数据采集和分析 → 再做纸面交易 → 最后实盘
      
      ---
      
      ## 三、知识库文件索引
      
      | 文件 | 大小 | 核心内容 | 对应阶段 |
      |------|------|---------|---------|
      | R01-factor-theory.md | 4.7KB | Alpha101, IC衰减, Barra, BRAIN | 阶段2 |
      | R02-strategy-theory.md | 5.0KB | OU过程, Avellaneda-Stoikov, OFI, Kalman | 阶段3 |
      | R03-crypto-quant-defi.md | 4.1KB | 资金费率, 链上, MEV | 阶段1+2 |
      | R04-portfolio-theory.md | 4.9KB | Black-Litterman, HRP, Kelly | 阶段3 |
      | R05-ml-quant-trading.md | 4.6KB | LightGBM, iTransformer, TFT, RL | 阶段3 |
      | R06-data-sources.md | 5.2KB | Alt Data全景, 加密数据源, 宏观API | 阶段1 |
      | R07-frontier-research.md | 5.4KB | LLM因子, AI Agent, 顶刊论文 | 阶段2+3 |
      | R08-options-volatility.md | 7.0KB | Greeks, 波动率曲面, Variance Swap | 阶段5 |
      | R09-market-microstructure.md | 8.8KB | LOB动力学, OFI, Kyle, Almgren-Chriss | 阶段2+3 |
      | R10-risk-management.md | 7.5KB | CVaR, EVT, Regime, 压力测试 | 阶段4 |
      | R11-institutional-methodology.md | 5.6KB | Simons哲学, 信号组合, 机构方法论 | 全阶段 |
      | **R12-integration.md** | **本文** | **知识图谱 + 升级蓝图** | **总览** |
      
      **总计**:12份报告, ~67KB, 覆盖量化交易完整知识栈
      
      ---
      
      ## 四、优先级排序(Simons哲学)
      
      > "永远不要信任单一信号" — 多样化信号来源是首要任务
      
      **P0 (立即实施)**:
      1. OFI微结构因子接入 → 5minbtc预测力直接提升
      2. 数据管道升级(免费源优先: Arkham/Binance WS/Deribit)
      3. 信号仪表板(监控所有因子IC/衰减)
      
      **P1 (2-4周内)**:
      4. Regime-aware仓位管理 → HMM检测
      5. LightGBM因子挖掘 → 自动化因子筛选
      6. CVaR止损替代固定止损
      
      **P2 (1-2月内)**:
      7. TFT多时间尺度模型 → 替代贝叶斯引擎
      8. 期权数据接入(Deribit IV信号)
      9. 完整压力测试框架
      
      ---
      
      *50轮蒸馏完成。12份报告构成完整量化交易知识体系。*
      *下一步:按蓝图阶段逐步实施,每个阶段独立验证。*
      
    • R13-accuracy-optimization-plan.md 8.8 KB
      # R13: 5minbtc 方向准确率优化方案
      
      > 基于177笔结算数据的深度复盘 + 50轮蒸馏知识的交叉验证
      > 目标:方向准确率从当前60.5%提升至70%+
      
      ---
      
      ## 一、问题诊断(数据驱动)
      
      ### 1.1 核心数据
      
      | 指标 | 值 | 评价 |
      |------|-----|------|
      | 总结算 | 177笔 | 样本充足 |
      | **方向准确率** | **60.5% (109/177)** | ❌ 接近随机 |
      | 区间命中率 | 62.1% | 一般 |
      | MAE | 0.064% | ✅ 精度好 |
      | 误差偏差 | +0.0025% | ✅ 无系统性偏 |
      | 最长连错 | 4笔 | 可接受 |
      
      ### 1.2 五大致命问题
      
      **问题1:强信号反而更差(最致命)**
      ```
      强信号(conf≥65): 50.8% 方向准确率 ← 比随机还差!
      弱信号(conf<65): 67.2% 方向准确率 ← 反而更好
      ```
      **根因**:线性打分系统在极端值时恰恰是行情末端。SKILL.md已记录"引擎越自信越错"但未修复。这是**过度自信反转效应**——当多个指标同向打分到极端时,市场大概率在末端,下一步是反转。
      
      **问题2:bull预测明显弱于bear**
      ```
      bull: 57.3% (51/89)
      bear: 63.0% (51/81)
      neutral: 100% (7/7)
      ```
      **根因**:BTC在样本期内偏向震荡/下行,但引擎bull倾向过强(89次bull vs 81次bear)。引擎对EMA金叉的bullish解读过激进。
      
      **问题3:放量时准确率暴跌**
      ```
      缩量(<40):    66.1%
      正常(40-80):  56.1%
      放量(80-120): 50.0%
      巨量(120+):   50.0%
      ```
      **根因**:放量意味着大单进入/清算事件,此时传统技术指标失效。当前引擎只是把放量当成"确认方向",但放量常常意味着**方向即将反转**(大量清算=方向极端)。
      
      **问题4:近期表现下滑**
      ```
      早期(127笔): 64.6% MAE=0.070%
      近期(50笔):  54.0% MAE=0.048%
      ```
      **根因**:近期市场regime变化(可能是震荡市),引擎的trend-following性质在震荡中失效。
      
      **问题5:高confidence区间70-79最差(44.4%)**
      ```
      conf 70-79: 16/36 = 44.4% ← 最差
      conf 80-89: 5/7  = 71.4% ← 样本小
      conf 40-59: 62/97 = 63.9% ← 基准
      ```
      **根因**:conf 70-79是"score=30-39"区间,刚好是direction_rule中"score>30"进入bull/bear的阈值。这个阈值设计不合理,太多噪声信号被判定为"有方向"。
      
      ---
      
      ## 二、优化方案
      
      ### 优化1:反转过度自信信号(预计+5-8%准确率)
      
      **原理**:当引擎score极端(|score|>60)时,反转信号或降级为neutral。
      
      **实施**:
      ```python
      # 修改 direction_rule() 函数
      if abs(score) > 60:
          # 极端信号 → 反转概率高
          # 降级strength为weak,或反转方向
          strength = "reversal_alert"
          confidence = min(70, 40 + abs(score) // 2)  # 压低confidence
      ```
      
      **验证**:回测177笔数据中score>60的样本,看反转后的准确率。
      
      ### 优化2:引入OFI微结构因子(预计+5-10%准确率)
      
      **原理**:OFI(订单流不平衡)对5分钟价格变化R²~15-25%,是目前缺失的最强预测因子。
      
      **数据源**:Binance WebSocket `btcusdt@depth20@100ms`
      - 最优买/卖价及深度
      - 计算实时OFI和microprice
      
      **实施**:
      ```python
      def compute_ofi(depth_snapshots):
          """订单流不平衡 — Cont, Kukanov & Stoikov (2014)"""
          ofi = 0
          for i in range(1, len(depth_snapshots)):
              prev, curr = depth_snapshots[i-1], depth_snapshots[i]
              # Bid side
              if curr['bid_price'] > prev['bid_price']:
                  ofi += curr['bid_qty']
              elif curr['bid_price'] == prev['bid_price']:
                  ofi += curr['bid_qty'] - prev['bid_qty']
              else:
                  ofi -= prev['bid_qty']
              # Ask side (mirror)
              if curr['ask_price'] > prev['ask_price']:
                  ofi -= prev['ask_qty']
              elif curr['ask_price'] == prev['ask_price']:
                  ofi -= (curr['ask_qty'] - prev['ask_qty'])
              else:
                  ofi += curr['ask_qty']
          return ofi
      
      def compute_microprice(bid_price, bid_qty, ask_price, ask_qty):
          """Stoikov (2018) microprice"""
          spread = ask_price - bid_price
          return bid_price + spread * bid_qty / (bid_qty + ask_qty)
      ```
      
      **权重**:OFI作为独立信号,权重40%,与现有指标组合。
      
      ### 优化3:Regime-aware方向判定(预计+3-5%准确率)
      
      **原理**:不同市场regime下,技术指标的有效性不同。
      - 趋势市:EMA/MACD有效
      - 震荡市:RSI/BB反转有效
      - 高波动市:所有指标失效,应降权
      
      **实施**:用波动率和趋势强度判断regime
      ```python
      def detect_regime(candles, atr_val, closes):
          """简单的3-regime分类"""
          # 波动率
          vol = atr_val / closes[-1] * 100
          
          # 趋势强度 (EMA9 vs EMA21 角度)
          ema9_slope = (ema(closes[-10:], 9) - ema(closes[-20:-10], 9))
          
          if vol > 0.25:  # 高波动
              return "HIGH_VOL"
          elif abs(ema9_slope) > atr_val * 0.3:  # 有趋势
              return "TREND"
          else:
              return "RANGE"
      
      # 然后根据regime调整权重
      def adjusted_direction(regime, base_score):
          if regime == "HIGH_VOL":
              return "neutral", "weak", 40  # 高波动不判方向
          elif regime == "RANGE":
              # 震荡市反转策略
              if base_score > 20: return "bear", "weak", 45
              elif base_score < -20: return "bull", "weak", 45
              else: return "neutral", "weak", 40
          else:
              return None  # 使用原始逻辑
      ```
      
      ### 优化4:修正bull偏向(预计+2-3%准确率)
      
      **原理**:当前score>0就判bull,score>-30也判bull(weak)。阈值不对称。
      
      **实施**:
      ```python
      # 修改阈值
      if score > 25:      # 原来30,提高bull门槛
          bias = "bull"
      elif score > 5:     # 原来0,小幅bullish不判方向
          bias = "neutral"  # 改为中性
          strength = "slight_bull"
      elif score > -5:    # 新增中性区间
          bias = "neutral"
      elif score > -25:   # 原来-30
          bias = "neutral"
          strength = "slight_bear"
      else:
          bias = "bear"
      ```
      
      **扩大neutral区间**:score在[-5, +5]范围判定neutral,避免对微弱信号下方向判断。
      
      ### 优化5:放量反转因子(预计+2-4%准确率)
      
      **原理**:放量不是确认方向,而是**反转预警**。
      
      **实施**:
      ```python
      # 修改volume权重逻辑
      if vol_pct > 120:
          # 巨量 → 反转信号
          score -= 15 * (1 if score > 0 else -1)  # 反转当前方向
      elif vol_pct > 80:
          # 放量 → 减弱方向
          score *= 0.7
      elif vol_pct < 40:
          # 缩量 → 维持当前判断(缩量时准确率最高66%)
          pass
      ```
      
      ### 优化6:连续K线疲劳因子(预计+1-2%准确率)
      
      **原理**:连续3+根同方向K线后,下一根反转概率显著升高。
      
      **实施**:
      ```python
      # 增强 consecutive candle 逻辑
      if consecutive_bull >= 3:
          score -= 15  # 原来只有-10,加强反转力度
          if consecutive_bull >= 5:
              score -= 25  # 5根以上强烈反转
      elif consecutive_bear >= 3:
          score += 15
          if consecutive_bear >= 5:
              score += 25
      ```
      
      ---
      
      ## 三、实施优先级与验证方法
      
      ### Phase 1: 立即修复(今天)
      
      | # | 优化 | 预期提升 | 复杂度 |
      |---|------|---------|--------|
      | 1 | 反转过度自信信号 | +5-8% | 低(改5行代码) |
      | 4 | 修正bull偏向+扩大neutral | +2-3% | 低(改阈值) |
      | 5 | 放量反转因子 | +2-4% | 低(改volume逻辑) |
      | 6 | 连续K线疲劳 | +1-2% | 低(改权重) |
      
      **合计预期**: +10-17% → 方向准确率 70-77%
      
      ### Phase 2: 一周内
      
      | # | 优化 | 预期提升 | 复杂度 |
      |---|------|---------|--------|
      | 2 | OFI微结构因子 | +5-10% | 中(需WebSocket) |
      | 3 | Regime-aware判定 | +3-5% | 中(需regime分类) |
      
      ### 验证方法
      
      1. **历史回测**:用177笔已有数据的指标值(不含OFI),模拟Phase 1修改后的判定结果
      2. **Walk-forward**:Phase 1修改后,运行7天实盘纸盘,跟踪准确率
      3. **A/B对比**:保留旧引擎逻辑,新旧引擎并行运行,对比准确率差异
      
      ---
      
      ## 四、与蒸馏知识的对齐
      
      | 优化方案 | 蒸馏来源 | 核心理论 |
      |---------|---------|---------|
      | OFI因子 | R09微结构 | Cont, Kukanov & Stoikov (2014) |
      | Microprice | R09微结构 | Stoikov (2018) |
      | Regime检测 | R10风险管理 | HMM/波动率regime |
      | 反转信号 | R01因子理论 | 动量因子的IC衰减与反转 |
      | 信号组合 | R11机构方法论 | Simons: 每个信号独立验证IC |
      | Neutral区间 | R02策略理论 | OU过程的均值回归区域 |
      | 放量反转 | R09微结构 | Kyle Lambda: 大单=信息交易 |
      
      ---
      
      ## 五、长期目标(v5.0架构)
      
      ```
      当前v4.0:  线性打分(EMA+RSI+MACD+Vol) → LLM覆盖
                方向准确率: 60.5%
      
      Phase 1:   反转+修正+扩大neutral
                目标: 70-77%
      
      Phase 2:   +OFI+Regime
                目标: 75-85%
      
      v5.0:      多模型集成(LightGBM+TFT+贝叶斯)
                +OFI+Microprice+Regime+IV
                目标: 80%+
      ```
      
      > Simons哲学: "不要相信单一信号。每个信号独立验证,只组合IC>0.05的因子。"
      > 优先级: 夏普>回撤>年化 → 方向准确率是夏普的基础
      
    • R14-engine-audit-report.md 4.2 KB
      # R14: 5minbtc Engine 顶尖量化审查报告
      
      > 审查标准: Renaissance Medallion / Jim Simons 级别
      > 审查日期: 2026-05-23
      > 样本绩效: 178笔交易,方向准确率60.5%
      > 统计显著性: t≈1.4,远未达Harvey标准t>3.0
      
      ---
      
      ## 🔴 Critical — 直接导致引擎失效的根本性缺陷
      
      ### C-1: 指标间严重共线性,有效独立信号仅2-3维
      
      EMA delta (EMA9-EMA21) 和 MACD histogram (EMA12-EMA26) 数学上高度相关:
      ```
      EMA9-EMA21 ≈ α₁ · P'(t)  (一阶导数近似)
      EMA12-EMA26 ≈ α₂ · P'(t)  (同一导数近似)
      Correlation ≈ 0.85-0.95
      ```
      两者占score 50/80=62.5%,但提供几乎相同信息。**有效独立信号维度仅2-3维**。
      → "噪音共振":共线指标同时极端≠高确信度,只=噪音放大。
      → 这解释了强信号准确率(56.8%)反而更差。
      
      **修复**: PCA去相关或替换为正交因子(动量t-stat、Z-score均值回归、波动率regime ratio)
      
      ### C-2: 绝对价格阈值完全不具尺度不变性
      
      所有阈值(ema_delta>100, macd_hist>50)是绝对价格差,非标准化。
      BTC $30k时ATR≈$300,$100k时ATR≈$1000。
      → 信号变成价格代理变量,非预测因子。
      → 直接导致bull准确率(57.3%)低于bear(63.0%)——高价时EMA_delta天然大→过多bull信号。
      
      **修复**: 所有阈值用ATR归一化: `norm_ed = ema_delta / atr_val`
      
      ### C-3: 放量反转逻辑与微观结构理论矛盾
      
      当前逻辑:放量(vol>120%)→反转方向。
      但Cont, Kukanov & Stoikov (2014)证明OFI在高成交量时段预测力更强。
      BTC高成交量主要在美盘/欧盘开盘——恰恰是趋势延续而非反转。
      代码不区分"突破放量"和"衰竭放量"。
      
      **修复**: 用price acceleration+OFI区分结构性放量vs衰竭性放量
      
      ### C-4: 完全缺失订单簿微结构信息
      
      | 因子 | 5min R² | 文献 |
      |------|---------|------|
      | OFI | 15-25% | Cont et al. (2014) |
      | Microprice | 优mid 5-10bp | Stoikov (2018) |
      | Bid-Ask Imbalance | 短期方向预测 | Bouchaud et al. (2009) |
      
      当前引擎所有信号加总R²估计<5%,**OFI单独就强3-5倍**。
      
      ---
      
      ## 🟠 High — 严重损害性能的设计缺陷
      
      ### H-1: "过度自信压制"是非线性跳变hack
      
      score=69→69, score=70→38。**1分变化导致31分跳跃**。
      0.55/0.75系数无统计依据。
      
      **修复**: sigmoid压缩: `compressed = max_score * (2 / (1 + exp(-score/20)) - 1)`
      
      ### H-2: MACD signal line O(n²)且EMA初始化不一致
      
      每次`ema(data[:i], ...)`从头计算,前period个数据用不同长度SMA初始化→EMA值不一致。
      100根K线→O(7500)次运算。
      
      **修复**: 计算完整EMA序列,O(n)
      
      ### H-3: 用绝对价格差而非log return
      
      BTC $30k→$100k过程中,$100变动含义完全不同(0.33% vs 0.10%)。
      违反Campbell, Lo & MacKinlay (1997)基本规范。
      
      ### H-4: 波动率Regime未显式建模
      
      亚洲时段ATR≈$100-200,美盘开盘ATR跳升5-8x,引擎用相同阈值。
      Medallion为不同regime建独立预测模型。
      
      ---
      
      ## 🟡 Medium
      
      - **M-1**: BB用总体标准差(÷n)而非样本标准差(÷n-1) → 2.6%偏差
      - **M-2**: 100根K线对EMA26不足(需~78根预热,仅剩22根有效)
      - **M-3**: vol_pct用未完成K线volume→系统性偏差(取决于查询时间点)
      - **M-4**: predict_close系数(0.25/0.15/0.1/0.5)全硬编码无理论依据
      - **M-5**: RSI超买/超卖反转逻辑在5min频率失效(应改为动量模式)
      - **M-6**: consecutive candle排除当前K线(若已走4分钟明显阴线则信息丢失)
      
      ## 🟢 Low
      
      - L-1: ATR用SMA而非Wilder's RMA
      - L-2: 无信号衰减管理(Medallion核心竞争力)
      - L-3: confidence区分度不足(40-70集中)
      
      ---
      
      ## 与顶尖基金对比
      
      | 维度 | Medallion | 本引擎 |
      |------|-----------|--------|
      | 信号融合 | 贝叶斯/正交因子 | 加法打分,共线 |
      | Alpha衰减 | 独立半衰期+IC监控 | 无 |
      | 微结构 | tick级order flow | 仅OHLCV |
      | 波动率 | regime-switching | 未建模 |
      | 标准化 | 波动率标度化 | 绝对阈值 |
      
      ---
      
      ## 三阶段修复路线图
      
      **Phase 1 (+5-10%)**: ATR归一化 + 移除/修正放量反转 + sigmoid压缩
      **Phase 2 (+5-8%)**: OFI/Microprice + regime检测 + 因子正交化
      **Phase 3 (机构级)**: log return + ML因子权重 + 信号衰减 + 500根K线
      
      **目标**: 70-75%准确率 (t≈2.7-3.5,接近Harvey标准)
      
  • scripts
    • 5minbtc-monitor.py 6.8 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """
      5minbtc-monitor.py — BTC 5min 预测监控 (Claude Code 适配版 v1.0)
      
      在每根 5min BTC K线的第 2/3/4 分钟采样 5minbtc-engine-v6.0.py 的预测,
      把事件以「一行一条」流式输出到 stdout — Claude Code 的 Monitor 工具
      会把每行转发为实时通知;命中「明确信号」即自动退出。
      
      事件行:
        START        首次采样,记录基线 bias
        DIR-CHANGE   bias 在 bull/neutral/bear 间翻转
        CLEAR-SIGNAL 达到明确信号门槛 → 退出 (exit 0)
        ENGINE-ERR   引擎调用失败 (打印后继续, 不崩溃)
        TIMEOUT      达到 --hours 上限仍无明确信号 → 退出 (exit 2)
      
      用法:
        python3 scripts/5minbtc-monitor.py                 # 默认: 最多 20 次采样, 命中明确信号即停
        python3 scripts/5minbtc-monitor.py --max-runs 100  # 采样次数上限
        python3 scripts/5minbtc-monitor.py --hours 2 --min-conf 55
        python3 scripts/5minbtc-monitor.py --settle        # 每次采样前 settle 上一根
        python3 scripts/5minbtc-monitor.py --dry-run       # 单次采样打印当前状态后退出
      
      路径: 脚本用 __file__ 定位引擎, 与 cwd 无关; 任意目录可调用。
      退出码: 0=CLEAR-SIGNAL, 2=达上限/超时, 1=异常。
      
      Claude Code 用法 (后台持续监控, 每根K线第2/3分钟采样):
        Monitor 工具 挂起:  python3 scripts/5minbtc-monitor.py --max-runs 20
        配合 --persistent 可无限运行, 由用户/会话结束或 --max-runs 兜底停止。
      """
      import argparse
      import datetime
      import json
      import subprocess
      import sys
      import time
      from pathlib import Path
      
      ROOT = Path(__file__).resolve().parent.parent
      ENGINE = ROOT / "5minbtc-engine-v6.0.py"
      SETTLE = ROOT / "5minbtc-log.py"
      
      # 每根 5min K线内的采样分钟 (progress ~40-80%: 第2/3分钟早捕捉, 第4分钟=原版半K线成熟期)
      DEFAULT_SAMPLE_MINUTES = (2, 3, 4)
      # 明确信号的 strength 集合 — 实测引擎会输出 weak / medium (而非文档的 moderate), 两者都收
      DEFAULT_STRENGTHS = ("medium", "moderate", "strong")
      SAMPLE_SECOND = 5
      ENGINE_TIMEOUT = 45
      
      
      def run_engine():
          try:
              out = subprocess.run(["python3", str(ENGINE)], capture_output=True,
                                   text=True, timeout=ENGINE_TIMEOUT)
          except subprocess.TimeoutExpired:
              print("ENGINE-ERR TimeoutExpired(45s)", flush=True)
              return None
          except Exception as e:  # noqa: BLE001
              print(f"ENGINE-ERR {type(e).__name__}: {e}", flush=True)
              return None
          try:
              return json.loads(out.stdout)
          except json.JSONDecodeError as e:
              print(f"ENGINE-ERR JSONDecode {e}; stderr={out.stderr.strip()[:200]}", flush=True)
              return None
      
      
      def emit(tag, d):
          p = d["prediction"]
          c = d["candle"]
          tb = d["factors"].get("taker_buy")
          tb_s = f" tb={tb:+.2f}" if tb is not None else ""
          print(f"{tag} [{c.get('iso')} p{c.get('progress_pct', 0):.0f}%] "
                f"{p['bias']}/{p['strength']} conf={p['confidence']} "
                f"px={d['price']['current']} regime={d.get('regime')} "
                f"fng={d.get('fng', {}).get('value')}{tb_s}", flush=True)
      
      
      def next_sample(now):
          """返回下一个采样时刻: minute%5 ∈ SAMPLE_MINUTES 且秒=SAMPLE_SECOND, 至少 6s 后。"""
          for i in range(0, 12):
              t = now + datetime.timedelta(minutes=i)
              if t.minute % 5 in SAMPLE_MINUTES:
                  t = t.replace(second=SAMPLE_SECOND, microsecond=0)
                  if t - now >= datetime.timedelta(seconds=6):
                      return t
          return now + datetime.timedelta(minutes=12)
      
      
      def main():
          ap = argparse.ArgumentParser(description="BTC 5min 引擎监控 (Claude Code 适配)")
          ap.add_argument("--max-runs", type=int, default=20,
                          help="采样次数上限(默认 20; 0=无限, 配 --persistent 由用户/会话结束)")
          ap.add_argument("--hours", type=float, default=None, help="监控时长上限(小时); 默认无")
          ap.add_argument("--min-conf", type=int, default=50, help="明确信号置信度门槛, 默认 50")
          ap.add_argument("--strengths", nargs="*", default=list(DEFAULT_STRENGTHS),
                          help="明确信号 strength 集合")
          ap.add_argument("--sample-minutes", type=int, nargs="*", default=list(DEFAULT_SAMPLE_MINUTES),
                          help="每根K线内采样分钟 (默认 2 3 4)")
          ap.add_argument("--settle", action="store_true", help="每次采样前 settle 上一根 K线")
          ap.add_argument("--dry-run", action="store_true", help="单次采样打印当前状态后退出")
          ap.add_argument("--loop-rounds", type=int, default=0,
                          help="命中 CLEAR-SIGNAL 后自动进入下一轮(每轮重计 max-runs); 0=命中即退出; -1=无限续轮直到手动停")
          args = ap.parse_args()
      
          strengths = set(args.strengths)
          global SAMPLE_MINUTES
          SAMPLE_MINUTES = tuple(args.sample_minutes)
          deadline = time.time() + args.hours * 3600 if args.hours else None
          last_bias = None
          last_tb = None
          runs = 0
      
          def settle():
              if args.settle:
                  subprocess.run(["python3", str(SETTLE), "settle-all"],
                                 capture_output=True, text=True, timeout=30)
      
          while True:
              if args.max_runs and runs >= args.max_runs:
                  print(f"MAX-RUNS reached ({runs}) no-clear-signal", flush=True)
                  return 2
              if deadline and time.time() > deadline:
                  print("TIMEOUT no-clear-signal", flush=True)
                  return 2
              runs += 1
              settle()
              d = run_engine()
              if d is None:
                  time.sleep(15)
                  continue
              p = d["prediction"]
              if args.dry_run:
                  emit("DRY", d)
                  return 0
              clear = (p["bias"] != "neutral" and p["strength"] in strengths
                       and p["confidence"] >= args.min_conf)
              if clear:
                  emit("CLEAR-SIGNAL", d)
                  loop = args.loop_rounds  # -1=无限, N>0=再续N轮, 0=退出
                  if loop != 0:
                      if loop > 0:
                          args.loop_rounds -= 1
                      runs = 0
                      emit("ROUND", d)
                      now = datetime.datetime.now()
                      t = next_sample(now)
                      time.sleep(max(10, (t - now).total_seconds()))
                      continue
                  return 0
              if p["bias"] != last_bias:
                  emit("DIR-CHANGE" if last_bias is not None else "START", d)
                  last_bias = p["bias"]
              # v5.8: taker_buy 正负翻转报警 (方向未变但主动买卖力翻转)
              tb = d["factors"].get("taker_buy")
              if tb is not None and last_tb is not None:
                  if (tb >= 0) != (last_tb >= 0):
                      emit("TB-FLIP", d)
              if tb is not None:
                  last_tb = tb
              now = datetime.datetime.now()
              t = next_sample(now)
              time.sleep(max(10, (t - now).total_seconds()))
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • 5minbtc_day_stats.py 4.2 KB
      #!/usr/bin/env python3
      """5minbtc 预测战绩查询 → 可选推送 Telegram (可移植版, 放在本 skill scripts/ 内)
      
      记录由 5minbtc_watch.py 写入 SKILL/logs/5minbtc-log.jsonl;
      结算(actual close/direction/range)由 watch 在新K线时对已收盘K线自动结算,
      也可手动: python3 <SKILL>/5minbtc-log.py settle-all
      
      用法:
        python3 scripts/5minbtc_day_stats.py              # 今日战绩
        python3 scripts/5minbtc_day_stats.py --date 2026-08-12
        python3 scripts/5minbtc_day_stats.py --all        # 全部历史
        python3 scripts/5minbtc_day_stats.py --push       # 推送到 Telegram
      """
      import argparse
      import gzip
      import json
      import os
      import subprocess
      from collections import Counter
      from datetime import datetime, timezone, timedelta
      from pathlib import Path
      
      SCRIPT_DIR = Path(__file__).resolve().parent
      SKILL = SCRIPT_DIR.parent
      LOG = SKILL / "logs" / "5minbtc-log.jsonl"
      ARCHIVE = SKILL / "logs" / "archive"
      PUSH = SCRIPT_DIR / "telegram_push.py"
      CST = timezone(timedelta(hours=8))
      
      
      def iter_log_lines():
          """按月归档(logs/archive/*.jsonl.gz, 老→新) + 当月 live。
      
          日志 2026-09-10 起按月轮转: 历史月份只在 archive 里,
          只读 live 会让 --all / --date <旧月份> 查不到数据。
          """
          for gz in sorted(ARCHIVE.glob("*.jsonl.gz")):
              try:
                  with gzip.open(gz, "rt", encoding="utf-8") as f:
                      for line in f:
                          yield line
              except Exception:
                  continue
          if LOG.exists():
              with open(LOG, encoding="utf-8") as f:
                  for line in f:
                      yield line
      
      
      def load():
          out = []
          for line in iter_log_lines():
              line = line.strip()
              if not line:
                  continue
              try:
                  out.append(json.loads(line))
              except Exception:
                  continue
          return out
      
      
      def fmt_stats(entries, label):
          settled = [e for e in entries if e.get("settled")]
          lines = [f"🌤 全天哨兵 · 战绩 | {label}"]
          lines.append("━" * 24)
          if not entries:
              lines.append("暂无预测记录")
              return "\n".join(lines)
          lines.append(f"记录预测 {len(entries)} 条")
          if not settled:
              lines.append("暂无已结算(收盘确认)记录")
              lines.append("(K线收盘后自动结算, 以最终确认结果为准)")
              return "\n".join(lines)
          total = len(settled)
          dir_ok = sum(1 for e in settled if e.get("direction_correct"))
          in_rng = sum(1 for e in settled if e.get("in_range"))
          errs = [e.get("error_pct", 0) for e in settled]
          mae = sum(abs(x) for x in errs) / total
          pred_dir = Counter(e.get("bias", "?").split("-")[0] for e in settled)
          lines.append(f"已结算 {total} 条 | 方向命中 {dir_ok}/{total} = {dir_ok / total * 100:.0f}%")
          lines.append(f"收盘在预测区间 {in_rng}/{total} = {in_rng / total * 100:.0f}%")
          lines.append(f"MAE {mae:.3f}% | 方向分布 {dict(pred_dir)}")
          lines.append("━" * 24)
          lines.append("最近结算:")
          for e in settled[-3:]:
              d = "✅" if e.get("direction_correct") else "❌"
              r = "✅" if e.get("in_range") else "❌"
              lines.append(
                  f"  {e.get('candle', '?')[5:16]} {e.get('bias', '?'):6s} "
                  f"pred {e.get('pred_close', 0):.0f} → 实际 {e.get('actual_close', 0):.0f} "
                  f"({e.get('error_pct', 0):+.2f}%) 方向{d} 区间{r}"
              )
          return "\n".join(lines)
      
      
      def main():
          ap = argparse.ArgumentParser(description="5minbtc 预测战绩查询")
          ap.add_argument("--date", help="YYYY-MM-DD")
          ap.add_argument("--all", action="store_true")
          ap.add_argument("--push", action="store_true")
          args = ap.parse_args()
      
          entries = load()
          today = datetime.now(CST).strftime("%Y-%m-%d")
          if args.all:
              label, sel = "全部历史", entries
          elif args.date:
              label, sel = args.date, [e for e in entries if e.get("candle", "").startswith(args.date)]
          else:
              label, sel = f"今日 {today}", [e for e in entries if e.get("candle", "").startswith(today)]
      
          msg = fmt_stats(sel, label)
          print(msg)
          if args.push:
              subprocess.run(["/usr/bin/python3", str(PUSH), msg], capture_output=True, timeout=20)
      
      
      if __name__ == "__main__":
          main()
      
    • 5minbtc_keyless_paper.py 12.7 KB
      #!/usr/bin/env python3
      """5minbtc 预测市场 · 免密钥模拟盘 (keyless paper) — 无需币安 API 密钥.
      
      用公开 BTC 数据 (data-api.binance.vision) 模拟 UP/DOWN token 价:
          P_up = sigmoid(1.7 × 当前涨跌幅 / ATR)     # 纯价格走势模型 = "市场"定价
      引擎信号 (含订单簿/taker 信息) = 你的 edge;  当 P_up < p 时买入 = EV+.
      
      与真实 paper 监控同一套 LIMIT 单/成交/结算/账户节奏, 只是价格源换成模拟价.
      真实市场价源 (需要密钥) 就绪后, 切换回 5minbtc_trader.py --paper-monitor.
      
      用法:
        python3 5minbtc_keyless_paper.py [--up-max 0.65] [--down-max 0.50]
            [--amount 1] [--bankroll 100] [--push]
      
      每根 5min K 线节奏:
        第0分钟: 结算上一轮 → 推送获利 + 账户权益
        第2分钟: 引擎确认方向+入场价 → 设模拟 LIMIT (方向锁定)
        第2分钟后: 轮询 spot → P_up 更新 → ask≤限价 → 成交记录 + 实时涨跌幅
        第3分钟: 只预测, 不改变 LIMIT 方向
        收盘: 按实际 BTC 收盘结算
      """
      import argparse
      import importlib
      import json
      import math
      import os
      import subprocess
      import sys
      import time
      import urllib.request
      from datetime import datetime, timezone, timedelta
      from pathlib import Path
      
      SCRIPTS = Path(__file__).resolve().parent
      SKILL = SCRIPTS.parent
      PAPER_FILE = Path.home() / "bb-auto" / "5minbtc-paper-keyless.json"
      PUSH = SCRIPTS / "telegram_push.py"
      SPOT_URL = "https://data-api.binance.vision/api/v3/ticker/price?symbol=BTCUSDT"
      SAMPLE_SECOND = 5
      SIGMOID_K = 1.7          # 走势→价格灵敏度 (可调: 越大价格越随走势走)
      
      
      def _trader():
          if str(SCRIPTS) not in sys.path:
              sys.path.insert(0, str(SCRIPTS))
          return importlib.import_module("5minbtc_trader")
      
      
      def _sigmoid(x):
          return 1.0 / (1.0 + math.exp(-x))
      
      
      def _spot():
          try:
              req = urllib.request.Request(SPOT_URL, headers={"User-Agent": "keyless/1"})
              with urllib.request.urlopen(req, timeout=8) as r:
                  return float(json.loads(r.read())["price"])
          except Exception:
              return None
      
      
      def _sim_up_price(open_, atr, spot):
          """模拟 UP token 价 = P(收阳 | 当前走势). DOWN 价 = 1 − UP 价."""
          move_price = spot - open_
          z = (move_price / max(atr, 1e-9)) * SIGMOID_K
          return max(0.02, min(0.98, _sigmoid(z)))
      
      
      def _push(msg, push=True):
          if not push:
              return
          try:
              subprocess.run(["/usr/bin/python3", str(PUSH), msg], capture_output=True,
                             timeout=20)
          except Exception:
              pass
      
      
      def _load_state():
          t = _trader()
          state = t.load_paper(str(PAPER_FILE))
          return state
      
      
      def _save_state(state):
          _trader().save_paper(str(PAPER_FILE), state)
      
      
      def _settle(state, fee):
          return _trader().settle_paper(state, fee)
      
      
      def _equity(state, bankroll):
          return bankroll + state.get("realized", 0.0)
      
      
      def main():
          ap = argparse.ArgumentParser(description="5minbtc 免密钥预测市场模拟盘")
          ap.add_argument("--up-max", type=float, default=0.65, help="UP 模拟价上限")
          ap.add_argument("--down-max", type=float, default=0.50, help="DOWN 模拟价上限")
          ap.add_argument("--amount", type=float, default=1.0, help="每注 USDT")
          ap.add_argument("--bankroll", type=float, default=100.0, help="模拟本金")
          ap.add_argument("--fee", type=float, default=0.0, help="手续费比例")
          ap.add_argument("--p-up", type=float, default=0.74, help="UP 信号预估概率 p")
          ap.add_argument("--p-down", type=float, default=0.57, help="DOWN 信号预估概率 p")
          ap.add_argument("--poll", type=int, default=10, help="轮询秒数 (keyless 用 spot)")
          ap.add_argument("--push", action="store_true", help="推送到 Telegram")
          ap.add_argument("--push-every", type=int, default=60)
          ap.add_argument("--push-delta", type=float, default=5.0)
          args = ap.parse_args()
      
          t = _trader()
          state = _load_state()
          if state.get("bankroll", 0) <= 0:
              state["bankroll"] = args.bankroll
      
          cur_candle = None
          did_2 = did_3 = False
          candle_open = None
          candle_atr = None
          last_pnl = {}
          last_live_ts = {}
      
          _push(f"🟢 免密钥模拟盘启动 | 本金 ${args.bankroll:.0f} 每注 {args.amount}U "
                f"| UP≤{args.up_max:.2f} DOWN≤{args.down_max:.2f} | 模拟价(无需密钥)", args.push)
      
          def _active_for(candle):
              return any(b.get("candle") == candle and b.get("status") in ("open", "pending")
                         for b in state["bets"])
      
          def _candle_remain(b):
              try:
                  close_ts = datetime.fromisoformat(b["candle"]).timestamp() + 300
                  return close_ts - time.time()
              except Exception:
                  return 999.0
      
          def _token_price_for(b, spot, open_, atr):
              up = _sim_up_price(open_, atr, spot)
              return up if b.get("side") == "UP" else (1 - up)
      
          while True:
              now = datetime.now()
              minute = now.minute % 5
      
              # 第0分钟: 结算 + 账户
              if minute == 0 and now.second < 20:
                  settled = _settle(state, args.fee)
                  if settled:
                      for b in state["bets"]:
                          if b.get("status") == "settled" and not b.get("pushed_final"):
                              pnl_pct = ((1 - b["ask"]) / b["ask"] * 100 - args.fee * 100
                                         if b.get("outcome") == "win" else -100 - args.fee * 100)
                              mark = "✅ 中" if b.get("outcome") == "win" else "❌ 未中"
                              _push(f"🏁 结算 {b['candle'][5:16]} {b['side']} @{b['ask']:.2f} {mark}\n"
                                    f"获利 {pnl_pct:+.1f}% | ${b.get('pnl', 0):+.2f}", args.push)
                              b["pushed_final"] = True
                      eq = _equity(state, args.bankroll)
                      br = state.get("bankroll", args.bankroll)
                      _push(f"💼 账户 ${br:.0f} → ${eq:.2f} "
                            f"({(eq - br) / br * 100:+.2f}%)", args.push)
                      _save_state(state)
      
              # 第2/3分钟: 引擎
              if minute in (2, 3) and now.second >= SAMPLE_SECOND:
                  d = t._load_engine()
                  if d:
                      candle = d["candle"]["iso"]
                      if candle != cur_candle:
                          cur_candle = candle
                          did_2 = did_3 = False
                          candle_open = d["price"]["open"]
                          candle_atr = d["indicators"].get("atr") or 40.0
                      side, reason = t._signal(d, min_conf=0, tb_filter=False,
                                               strength_gate=False)
                      spot = _spot()
      
                      def _place(side_, reason_, tag):
                          """设模拟 LIMIT 单 (立即成交或挂单). 返回是否下单."""
                          if not side_ or _active_for(candle) or spot is None:
                              return False
                          p_est = args.p_up if side_ == "UP" else args.p_down
                          limit = args.up_max if side_ == "UP" else args.down_max
                          p_up = _sim_up_price(candle_open, candle_atr, spot)
                          ask = p_up if side_ == "UP" else (1 - p_up)
                          if ask <= limit:
                              state["bets"].append({
                                  "candle": candle, "side": side_, "ask": round(ask, 4),
                                  "amount": args.amount, "fee": args.fee, "status": "open",
                                  "p_est": p_est,
                                  "ts": datetime.now().isoformat(), "reason": reason_,
                              })
                              _save_state(state)
                              _push(f"✅ 立即成交 | {candle[5:16]} {tag} {side_}\n"
                                    f"模拟价 {ask:.2f} (限价 {limit:.2f}内) | p={p_est:.2f}\n"
                                    f"UP {p_up:.2f} | DOWN {1 - p_up:.2f}\n"
                                    f"持仓 {args.amount}U | 涨跌幅 0% 起算", args.push)
                          else:
                              state["bets"].append({
                                  "candle": candle, "side": side_,
                                  "limit": round(limit, 4), "status": "pending",
                                  "p_est": p_est,
                                  "ts": datetime.now().isoformat(), "reason": reason_,
                              })
                              _save_state(state)
                              _push(f"📋 LIMIT | {candle[5:16]} {tag}\n"
                                    f"{side_} 限价 {limit:.2f} (p={p_est:.2f}) | "
                                    f"模拟现价 {ask:.2f}\n"
                                    f"UP {p_up:.2f} | DOWN {1 - p_up:.2f}\n"
                                    f"成交时 EV {p_est - limit:+.2f} | 等回调 | {args.amount}U", args.push)
                          return True
      
                      if minute == 2 and not did_2:
                          did_2 = True
                          _place(side, reason, "第2min")
                      elif minute == 3 and not did_3:
                          did_3 = True
                          if not _active_for(candle):
                              # 第2分钟中性 → 第3分钟第二次机会下单
                              if not _place(side, reason, "第3min"):
                                  p = d["prediction"]
                                  cc = d["candle"]
                                  f = d["factors"]
                                  tb = f.get("taker_buy", 0.0)
                                  fng = d.get("fng", {})
                                  fng_s = (f" | FNG {fng.get('value')} {fng.get('label', '')}"
                                           if fng.get("value") is not None else "")
                                  p_up = (_sim_up_price(candle_open, candle_atr, spot)
                                          if spot is not None else None)
                                  updown = (f"UP {p_up:.2f} | DOWN {1 - p_up:.2f}"
                                            if p_up is not None else "UP/DOWN: spot不可用")
                                  _push(f"🔎 第3分钟预测(模拟) | {candle[5:16]} "
                                        f"p{cc.get('progress_pct', 0):.0f}%\n"
                                        f"方向: {p['bias']} ({p['strength']}) | 置信 {p['confidence']}\n"
                                        f"现价 {d['price']['current']:,.2f} | "
                                        f"预测收 {p['pred_close']:,} "
                                        f"(低{p['pred_low']:,}/高{p['pred_high']:,})\n"
                                        f"regime {d.get('regime')}{fng_s} | 买力 tb={tb:+.2f}\n"
                                        f"模拟价: {updown}\n"
                                        f"(仍无信号, 不设LIMIT)", args.push)
                          # 已有活跃单: LIMIT 方向锁定, 静默
      
              # 成交检测 + 实时涨跌幅 (用 spot → 模拟价)
              now_ts = time.time()
              spot = _spot()
              has_pending = False
              if spot is not None and candle_open is not None:
                  for b in state["bets"]:
                      if b.get("status") == "pending":
                          has_pending = True
                          ask = _token_price_for(b, spot, candle_open, candle_atr)
                          if ask <= b["limit"]:
                              b["status"] = "open"
                              b["ask"] = round(ask, 4)
                              b["amount"] = args.amount
                              b["fee"] = args.fee
                              _save_state(state)
                              _push(f"✅ 成交 | {b['candle'][5:16]} {b['side']} @ {ask:.2f} "
                                    f"(限价 {b['limit']:.2f})\n"
                                    f"持仓 {args.amount}U | 涨跌幅 0% 起算 | 等结算", args.push)
                          elif _candle_remain(b) <= 0:
                              b["status"] = "unfilled"
                              _save_state(state)
                              _push(f"❌ 未成交 | {b['candle'][5:16]} {b['side']}\n"
                                    f"限价 {b['limit']:.2f} 收盘未触及 | 放弃", args.push)
                          continue
                      if b.get("status") != "open":
                          continue
                      ask = b["ask"]
                      cur = _token_price_for(b, spot, candle_open, candle_atr)
                      pnl_pct = (cur - ask) / ask * 100
                      last = last_pnl.get(str(b.get("token_id") or b.get("candle")))
                      key = str(b.get("candle")) + b.get("side", "")
                      last = last_pnl.get(key)
                      if (last is None or abs(pnl_pct - last) >= args.push_delta
                              or now_ts - last_live_ts.get(key, 0) >= args.push_every):
                          remain = _candle_remain(b)
                          _push(f"📡 {b['side']} @{ask:.2f} → 模拟价 {cur:.2f}\n"
                                f"涨跌幅 {pnl_pct:+.1f}% | 剩余 "
                                f"{int(max(remain, 0)) // 60}m{int(max(remain, 0)) % 60:02d}s", args.push)
                          last_pnl[key] = pnl_pct
                          last_live_ts[key] = now_ts
      
              time.sleep(10 if has_pending else args.poll)
      
      
      if __name__ == "__main__":
          main()
      
    • 5minbtc_order.py 4 KB
      #!/usr/bin/env python3
      """5minbtc 模拟下单 (paper) — 用户确认重大信号后买入 1U.
      
      跑引擎拿当前方向 + UP/DOWN 真实价, 记录到 paper 台账.
      🔒 纯模拟: 只写台账, 绝不调 place_order (LIVE_GATE 硬闸门).
      
      用法:
        python3 5minbtc_order.py --amount 1   # 按当前引擎方向模拟买入
      """
      import argparse
      import json
      import os
      import subprocess
      import sys
      from datetime import datetime, timezone, timedelta
      from pathlib import Path
      
      SCRIPTS = Path(__file__).resolve().parent
      SKILL = SCRIPTS.parent
      ENGINE = SKILL / "5minbtc-engine-v6.0.py"
      PAPER = Path.home() / "bb-auto" / "5minbtc-paper.json"
      PUSH = SCRIPTS / "telegram_push.py"
      PY = "/usr/bin/python3"
      CST = timezone(timedelta(hours=8))
      
      
      def load_env():
          envf = Path.home() / "bb-auto" / "prediction.env"
          if envf.exists():
              for line in envf.read_text().splitlines():
                  line = line.strip()
                  if not line or line.startswith("#") or "=" not in line:
                      continue
                  k, v = line.split("=", 1)
                  if v.strip():
                      os.environ.setdefault(k.strip(), v.strip())
      
      
      def run_engine():
          try:
              out = subprocess.run([PY, str(ENGINE)], capture_output=True,
                                   text=True, timeout=45)
              return json.loads(out.stdout)
          except Exception:
              return None
      
      
      def get_up_down_price():
          """拉当前 BTC 5m 预测市场 UP/DOWN 真实价. 失败返回 (None, None)."""
          try:
              sys.path.insert(0, str(SCRIPTS))
              import importlib
              trader = importlib.import_module("5minbtc_trader")
              found = trader.find_btc_5m_market()
              if not found:
                  return None, None
              _, mid, up_tok, dn_tok, _, _ = found
              return trader._token_price(mid, up_tok), trader._token_price(mid, dn_tok)
          except Exception:
              return None, None
      
      
      def load_paper():
          if PAPER.exists():
              try:
                  return json.loads(PAPER.read_text())
              except Exception:
                  pass
          return {"bets": [], "realized": 0.0, "bankroll": 100.0,
                  "started": datetime.now(CST).isoformat()}
      
      
      def save_paper(state):
          PAPER.write_text(json.dumps(state, indent=2, ensure_ascii=False))
      
      
      def push(msg):
          try:
              subprocess.run([PY, str(PUSH), msg], capture_output=True, timeout=20)
          except Exception:
              pass
      
      
      def main():
          ap = argparse.ArgumentParser(description="5minbtc 模拟下单 (paper)")
          ap.add_argument("--amount", type=float, default=1.0, help="下单金额 USDT (默认1)")
          args = ap.parse_args()
      
          load_env()
          d = run_engine()
          if not d:
              print("❌ 引擎失败, 无法下单")
              return 1
          p = d["prediction"]
          c = d["candle"]
          if p["bias"] == "neutral":
              print(f"❌ 当前方向中性, 不下单 (bias={p['bias']})")
              return 0
      
          side = "UP" if p["bias"] == "bull" else "DOWN"
          up_price, down_price = get_up_down_price()
          ask = up_price if side == "UP" else down_price
          if ask is None:
              print(f"⚠️ 无法获取 {side} 真实价, 用引擎预测价代替")
              ask = round((p["pred_close"] / d["price"]["current"]) - 1 + 0.5, 4)
              ask = max(0.05, min(0.95, ask))
      
          state = load_paper()
          bet = {
              "candle": c["iso"], "side": side, "ask": round(ask, 4),
              "amount": args.amount, "fee": 0.01, "status": "open", "mode": "paper",
              "p_est": p["confidence"] / 100,
              "manual": True,
              "ts": datetime.now(CST).isoformat(),
              "reason": f"手动确认下单 conf={p['confidence']}",
          }
          state["bets"].append(bet)
          save_paper(state)
      
          dir_cn = "看多·收阳" if side == "UP" else "看空·收阴"
          msg = (f"✅ 模拟下单已确认\n"
                 f"{dir_cn} | {side} @ {ask:.2f} | {args.amount}U\n"
                 f"{c['candle_start']} progress {c['progress_pct']:.0f}% | "
                 f"置信 {p['confidence']}\n"
                 f"(纯模拟, 台账记录, 绝不下真实单)")
          print(msg)
          push(msg)
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • 5minbtc_realtime.py 18.4 KB
      #!/usr/bin/env python3
      """5minbtc 实时监控 — 5秒刷新, 真 OFI 驱动下单.
      
      每 5 秒跑一次引擎 (v6.0 真 OFI 驱动):
        - 方向: 引擎 bias 来自真 OFI 净流 (净流入→UP, 净流出→DOWN, 无信号→neutral 不交易)
        - 下单: OFI 概率 p vs UP/DOWN token 市场价 ask → EV = p − ask ≥ min_edge 才买
                (OFI 已净流入但 UP token 价未涨 = gap>0 = edge)
        - D2 有效性闸: 滚动 200 笔 OFI 方向胜率 < 0.55 → 自动暂停 + 告警 (防第二次 52.6%)
      命中时推送 Telegram. 同一根K线去重. 只写 paper 台账, 绝不调 place_order (LIVE_GATE 硬闸门在 trader).
      
      用法:
        python3 5minbtc_realtime.py [--conf 70] [--refresh 5] [--push] [--min-edge 0.05]
      """
      import argparse
      import json
      import os
      import subprocess
      import sys
      import time
      import urllib.request
      from datetime import datetime, timezone, timedelta
      from pathlib import Path
      
      SKILL = Path(__file__).resolve().parent.parent
      ENGINE = SKILL / "5minbtc-engine-v6.0.py"
      PUSH = SKILL / "scripts" / "telegram_push.py"
      PY = "/usr/bin/python3"
      PAPER = Path.home() / "bb-auto" / "5minbtc-paper.json"
      CST = timezone(timedelta(hours=8))
      
      DIR_CN = {"bull": "看多·收阳", "bear": "看空·收阴"}
      
      
      def load_env():
          envf = Path.home() / "bb-auto" / "prediction.env"
          if envf.exists():
              for line in envf.read_text().splitlines():
                  line = line.strip()
                  if not line or line.startswith("#") or "=" not in line:
                      continue
                  k, v = line.split("=", 1)
                  if v.strip():
                      os.environ.setdefault(k.strip(), v.strip())
      
      
      def get_up_down_price():
          """拉当前 BTC 5m 预测市场 UP/DOWN 真实价. 失败返回 (None, None)."""
          try:
              load_env()
              sys.path.insert(0, str(SKILL / "scripts"))
              import importlib
              trader = importlib.import_module("5minbtc_trader")
              found = trader.find_btc_5m_market()
              if not found:
                  return None, None
              _, mid, up_tok, dn_tok, _, _ = found
              return trader._token_price(mid, up_tok), trader._token_price(mid, dn_tok)
          except Exception:
              return None, None
      
      
      def load_paper():
          if PAPER.exists():
              try:
                  return json.loads(PAPER.read_text())
              except Exception:
                  pass
          return {"bets": [], "realized": 0.0, "bankroll": 100.0,
                  "started": datetime.now(CST).isoformat()}
      
      
      def save_paper(state):
          PAPER.write_text(json.dumps(state, indent=2, ensure_ascii=False))
      
      
      def has_auto_bet(candle):
          """台账去重: 该 K线是否已自动下单/挂单 (进程重启也不重复)."""
          state = load_paper()
          return any(b.get("candle") == candle and b.get("auto")
                     for b in state.get("bets", []))
      
      
      def ofi_snapshot(d):
          """从引擎输出提取真 OFI 入场快照 (D2 有效性闸与分桶复盘用)."""
          o = d.get("ofi") or {}
          return {
              "ofi_n": o.get("ofi_n"), "ofi_60": o.get("ofi_60"),
              "ofi_gap": o.get("gap"), "ofi_p_cal": o.get("p_cal"),
              "ofi_cal_n": o.get("cal_n"),
              "ofi_cr": o.get("cr"), "ofi_feed_fresh": o.get("feed_fresh"),
              "ofi_agg_native": o.get("agg_native"), "ofi_vol_gate": o.get("vol_gate"),
          }
      
      
      def ofi_win_rate(state):
          """D2 实盘有效性闸: 已结算 auto 单中带 OFI 快照的最近 200 笔的 OFI 方向胜率.
          防第二次 52.6% — 只在真 OFI 驱动后产生的新单上统计.
          返回 (n, win_rate)."""
          bets = [b for b in state.get("bets", [])
                  if b.get("status") == "settled" and b.get("auto")
                  and b.get("entry", {}).get("ofi_n") is not None]
          recent = bets[-200:]
          if not recent:
              return 0, 0.0
          wins = sum(1 for b in recent if b.get("outcome") == "win")
          return len(recent), wins / len(recent)
      
      
      def record_paper(d, up=None, down=None):
          """重大信号自动记录一笔模拟单到台账 (完整入场快照 + 后续结算回测)."""
          p = d["prediction"]
          c = d["candle"]
          price = d["price"]
          side = "UP" if p["bias"] == "bull" else "DOWN"
          ask = up if side == "UP" else down
          if ask is None:
              # 真实价拿不到, 用引擎预测方向折算近似价
              ask = round(max(0.05, min(0.95, p["confidence"] / 100)), 4)
          state = load_paper()
          bet = {
              "candle": c["iso"], "side": side, "ask": round(ask, 4),
              "amount": 1.0, "fee": 0.01, "status": "open", "mode": "paper",
              "p_est": (p["confidence"] / 100) if side == "UP" else (1 - p["confidence"] / 100), "auto": True,
              "ts": datetime.now(CST).isoformat(),
              "reason": f"真OFI信号自动记录 ofi_n={ofi_snapshot(d).get('ofi_n')}",
              # 入场完整快照 (回测用) + 真 OFI 快照 (D2 分桶复盘用)
              "entry": {
                  **ofi_snapshot(d),
                  "progress": c.get("progress_pct"),
                  "open": price["open"],
                  "current": price["current"],
                  "body": price.get("body"),
                  "confidence": p["confidence"],
                  "strength": p["strength"],
                  "regime": d.get("regime"),
                  "mtf": d.get("mtf", {}),
              },
          }
          state["bets"].append(bet)
          save_paper(state)
          return side, ask
      
      
      def run_engine():
          try:
              out = subprocess.run([PY, str(ENGINE)], capture_output=True,
                                   text=True, timeout=45)
              return json.loads(out.stdout)
          except Exception:
              return None
      
      
      def push(msg, enabled=True):
          if not enabled:
              return
          try:
              subprocess.run([PY, str(PUSH), msg], capture_output=True, timeout=20)
          except Exception:
              pass
      
      
      # 2026-09-10 按最小无用原则删除的死代码 (均无调用点, 可从 git 历史取回):
      #   record_limit / fmt_limit / fmt_skip_knife —— 「甜区限价挂单」功能, 已确认废弃
      #     (record_limit 是该功能唯一的 pending 单产生方; 台账 148 笔全 settled, 无 pending)
      #   fmt_no_edge / fmt_signal —— 已被 v5.10 起的 inline action 文案取代
      # ⚠️ 遗留: settle 逻辑仍会消费 status=="pending" 的单 (见下方 bet 结算段),
      #   那是给 5minbtc_keyless_paper.py 与历史台账用的, 不要误删。
      
      
      def fetch_close(candle_iso):
          """拉该 5min K线的 (open, close). 返回 (open, close) 或 None."""
          try:
              dt = datetime.fromisoformat(candle_iso)
              start_ms = int(dt.timestamp() * 1000)
              url = (f"https://data-api.binance.vision/api/v3/klines"
                     f"?symbol=BTCUSDT&interval=5m&startTime={start_ms}&limit=1")
              req = urllib.request.Request(url, headers={"User-Agent": "realtime/1"})
              data = json.loads(urllib.request.urlopen(req, timeout=10).read())
              if data:
                  return float(data[0][1]), float(data[0][4])
          except Exception:
              pass
          return None
      
      
      def fmt_account(state, last_pnl=None):
          """账户总览: 总资金(权益) + 本次盈亏 + 总盈亏(已实现)."""
          bankroll = state.get("bankroll", 100.0)
          realized = state.get("realized", 0.0)
          up, down = get_up_down_price()
          unrealized = 0.0
          for b in state.get("bets", []):
              if b.get("status") == "open":
                  ask = b.get("ask", 0)
                  cur = up if b["side"] == "UP" else down
                  if cur is not None and ask and ask > 0:
                      unrealized += b.get("amount", 1) * (cur - ask) / ask
          equity = bankroll + realized + unrealized
          last_s = f" | 本次 ${last_pnl:+.2f}" if last_pnl is not None else ""
          return (f"💰 总资金 ${equity:.2f}{last_s} | 总盈亏 ${realized:+.2f} "
                  f"| 持仓浮盈 ${unrealized:+.2f}")
      
      
      def settle_open(state, push_enabled=True):
          """结算已收盘的 open 单, 推送结算结果 + 账户总览. 返回结算数."""
          now_ms = int(time.time() * 1000)
          settled = 0
          for b in state.get("bets", []):
              if b.get("status") != "open" or not b.get("auto"):
                  continue
              try:
                  dt = datetime.fromisoformat(b["candle"])
                  close_ms = int(dt.timestamp() * 1000) + 5 * 60 * 1000
              except Exception:
                  continue
              if now_ms < close_ms:
                  continue
              ohlc = fetch_close(b["candle"])
              if ohlc is None:
                  continue
              o, c = ohlc
              won = (b["side"] == "UP" and c > o) or (b["side"] == "DOWN" and c < o)
              b["status"] = "settled"
              b["outcome"] = "win" if won else "loss"
              b["actual_open"] = o
              b["actual_close"] = c
              b["direction_correct"] = won
              amount = b.get("amount", 1.0)
              ask = b.get("ask", 0.0)
              fee = b.get("fee", 0.0)
              # 投入 amount 买 token @ ask: 赢→token变1,赚 amount*(1-ask)/ask; 输→token变0,亏 amount(全额)
              if won:
                  b["pnl"] = round(amount * (1 - ask) / ask - amount * fee, 4)
              else:
                  b["pnl"] = round(-amount - amount * fee, 4)
              b["pnl_pct"] = round(b["pnl"] / amount * 100, 2) if amount else 0.0
              settled += 1
          # 重建 realized (所有 settled 单 pnl 总和)
          state["realized"] = round(sum(x.get("pnl", 0) for x in state.get("bets", [])
                                        if x.get("status") == "settled"), 4)
          if settled:
              save_paper(state)
              for b in state.get("bets", []):
                  if b.get("status") == "settled" and b.get("auto") and not b.get("pushed_final"):
                      mark = "✅ 中" if b.get("direction_correct") else "❌ 未中"
                      acct = fmt_account(state, b["pnl"])
                      push(f"🎯 夜盘狙击 · 结算 {b['candle'][5:16]} {b['side']} {mark}\n"
                           f"买入 {b.get('amount', 1):.0f}U @{b['ask']:.2f} | "
                           f"涨跌 {b.get('pnl_pct', 0):+.1f}% | PnL {b['pnl']:+.2f}$\n"
                           f"{acct}", push_enabled)
                      b["pushed_final"] = True
              save_paper(state)
          return settled
      
      
      def check_pending(push_enabled=True):
          """检查挂单: 回调到限价→成交, 收盘未触及→未成交. 返回是否有变化."""
          up, down = get_up_down_price()
          state = load_paper()
          changed = False
          for b in state.get("bets", []):
              if b.get("status") != "pending" or not b.get("auto"):
                  continue
              try:
                  close_ts = datetime.fromisoformat(b["candle"]).timestamp() + 300
              except Exception:
                  continue
              # 收盘未触及 → 未成交
              if time.time() >= close_ts:
                  b["status"] = "unfilled"
                  changed = True
                  print(f"❌ 未成交: {b['side']} 限价{b.get('limit')}", flush=True)
                  acct = fmt_account(state)
                  push(f"🎯 夜盘狙击 · 未成交 | {b['candle'][5:16]} {b['side']} 限价{b.get('limit')} 收盘未触及\n"
                       f"{acct}", push_enabled)
                  continue
              # 回调到限价 → 成交 (静默, 结算时统一推送账户)
              ask = up if b["side"] == "UP" else down
              if ask is not None and ask <= b["limit"]:
                  b["status"] = "open"
                  b["ask"] = round(ask, 4)
                  changed = True
                  print(f"✅ 成交: {b['side']} @ {ask:.2f} (限价{b.get('limit')})", flush=True)
          if changed:
              save_paper(state)
          return changed
      
      
      
      
      def fmt_order(d, side, ask, probability=None):
          """概率套利下单推送."""
          p = d["prediction"]
          c = d["candle"]
          dir_cn = DIR_CN.get(p["bias"], p["bias"])
          edge_s = f" | edge {probability - ask:+.2f}" if probability is not None and ask is not None else ""
          prob_s = f"概率 {probability:.2f}" if probability is not None else f"置信 {p['confidence']}"
          return (f"🎯 夜盘狙击 · 下单 | {dir_cn}\n"
                  f"{c['candle_start']} | {side} @ {ask:.2f} | 1U\n"
                  f"{prob_s} vs 市场 {ask:.2f}{edge_s} | 已记录")
      
      
      def fmt_prediction(d, side, ask, probability, edge, action):
          """每根K线的预测快照 (方向+概率+市场价+edge+是否下单)."""
          p = d["prediction"]
          c = d["candle"]
          dir_cn = DIR_CN.get(p["bias"], p["bias"])
          ask_s = f"{ask:.2f}" if ask is not None else "?"
          edge_s = f"edge {edge:+.2f}" if edge is not None else "edge ?"
          return (f"🌤 全天哨兵 · 预测 {dir_cn} | {c['candle_start']} p{c.get('progress_pct', 0):.0f}%\n"
                  f"概率 {probability:.2f} | 市场 {side} {ask_s} | {edge_s}\n"
                  f"→ {action}")
      
      
      
      
      
      
      
      
      def main():
          ap = argparse.ArgumentParser(description="5minbtc 实时监控(5秒刷新, 捕捉重大信号)")
          ap.add_argument("--conf", type=int, default=70, help="重大信号置信度阈值(默认70)")
          ap.add_argument("--refresh", type=int, default=5, help="刷新间隔秒(默认5)")
          ap.add_argument("--push", action="store_true", help="推送 Telegram")
          ap.add_argument("--active-hours", type=str, default="20,21,22,23",
                          help="只在此时段下单(逗号分隔CST小时)")
          ap.add_argument("--min-edge", type=float, default=0.05,
                          help="最小edge(引擎OFI概率-市场价), 低于此不下单(默认0.05, 覆盖0.01U手续费+校准噪声)")
          ap.add_argument("--ofi-min-n", type=int, default=100,
                          help="OFI有效性闸最少样本数(默认100)")
          ap.add_argument("--ofi-min-win", type=float, default=0.55,
                          help="OFI有效性闸最低方向胜率, 低于则暂停下单(默认0.55, 防第二次 52.6 硬币)")
          args = ap.parse_args()
      
          active_hours = set(int(h) for h in args.active_hours.split(",") if h.strip())
      
          last_candle = None
          last_signal_conf = 0
          last_bias = None
          prev_candle = None   # 上一根K线 (结算上一轮预测结果用)
          prev_bias = None     # 上一根K线的预测方向
          _ofi_gate_alert_ts = 0.0  # D2 有效性闸告警节流 (1h 一次)
          print(f"🟢 5minbtc 实时监控启动(真OFI驱动) | {args.refresh}s刷新 | conf≥{args.conf} | "
                f"min-edge {args.min_edge} | 下单时段 {sorted(active_hours)}点", flush=True)
      
          while True:
              d = run_engine()
              if d is None:
                  time.sleep(args.refresh)
                  continue
              p = d["prediction"]
              c = d["candle"]
              candle = c["iso"]
              bias = p["bias"]
              conf = p["confidence"]
      
              now_hour = datetime.now(CST).hour
      
              # 新K线 → 结算上一轮预测结果 + 推预测快照
              if candle != last_candle:
                  last_candle = candle
                  last_signal_conf = 0
                  last_bias = None
      
                  # 上一轮方向预测结果 (上一根K线的方向 vs 实际收盘)
                  prev_line = ""
                  if prev_candle is not None and prev_bias in ("bull", "bear"):
                      ohlc = fetch_close(prev_candle)
                      if ohlc:
                          o, cl = ohlc
                          actual = "收阳" if cl > o else "收阴"
                          correct = (prev_bias == "bull" and cl > o) or (prev_bias == "bear" and cl < o)
                          prev_line = (f"上一轮 {prev_candle[11:16]} "
                                       f"{DIR_CN.get(prev_bias, prev_bias)} → {actual} "
                                       f"{'✅' if correct else '❌'}")
      
                  # 记录当前预测为下一轮的"上一轮"
                  prev_candle = candle
                  prev_bias = bias
      
                  if bias == "neutral":
                      # 真 OFI 无显著信号 → 宁缺毋滥不交易
                      push(f"🧭 预测 中性(OFI无显著信号/流量不足) | {c['candle_start']} "
                           f"p{c.get('progress_pct', 0):.0f}% | ofi_n {(d.get('ofi') or {}).get('ofi_n')} "
                           f"| 不交易(宁缺毋滥)" + (f"\n{prev_line}" if prev_line else ""), args.push)
                  else:
                      up0, down0 = get_up_down_price()
                      side0 = "UP" if bias == "bull" else "DOWN"
                      ask0 = up0 if side0 == "UP" else down0
                      prob0 = p.get("probability", p.get("confidence", 50) / 100)
                      # 引擎 probability 恒为 P(close>open); DOWN 腿用 P(close<open)=1-P
                      prob_side0 = prob0 if side0 == "UP" else (1 - prob0)
                      edge0 = prob_side0 - ask0 if ask0 is not None else None
                      if now_hour not in active_hours:
                          action = "非活跃时段"
                      elif ask0 is None:
                          action = "市场价不可用"
                      elif edge0 >= args.min_edge:
                          action = "🎯 下单"
                      else:
                          action = "⏭️ 跳过(无edge)"
                      msg = fmt_prediction(d, side0, ask0, prob_side0, edge0, action)
                      if prev_line:
                          msg += f"\n{prev_line}"
                      push(msg, args.push)
      
              # 活跃时段内才下单 (真 OFI 方向 vs token 价: 引擎 OFI 概率 > 市场价 且方向明确才买)
              if now_hour in active_hours and bias in ("bull", "bear"):
                  if not has_auto_bet(candle):
                      side = "UP" if bias == "bull" else "DOWN"
                      up, down = get_up_down_price()
                      ask = up if side == "UP" else down
                      probability = p.get("probability", p.get("confidence", 50) / 100)
                      # D2 实盘有效性闸: 滚动 OFI 方向胜率过低 → 自动暂停 (防第二次 52.6%)
                      ofi_n_samples, ofi_wr = ofi_win_rate(load_paper())
                      if (ofi_n_samples >= args.ofi_min_n
                              and ofi_wr < args.ofi_min_win):
                          if time.time() - _ofi_gate_alert_ts > 3600:
                              _ofi_gate_alert_ts = time.time()
                              push(f"🎯 夜盘狙击 · ⛔有效性闸触发: {ofi_n_samples}笔 OFI 方向胜率 "
                                   f"{ofi_wr:.1%} < {args.ofi_min_win:.0%} → 暂停下单", args.push)
                          print(f"⛔ OFI有效性闸: {ofi_n_samples}笔胜率 {ofi_wr:.1%} < "
                                f"{args.ofi_min_win:.0%}, 暂停", flush=True)
                      elif ask is not None and (probability if side == "UP" else (1 - probability)) - ask >= args.min_edge:
                          # 真 OFI 方向 vs token 价: EV = p - P > 0 才买
                          # (DOWN 腿用 P(close<open)=1-P, 否则 bear 永远不触发)
                          prob_side = probability if side == "UP" else (1 - probability)
                          record_paper(d, up, down)
                          print(f"📝 真OFI下单: {side} 概率{prob_side:.2f} > 市场{ask:.2f} "
                                f"(edge {prob_side-ask:+.2f}) ofi_n={(d.get('ofi') or {}).get('ofi_n')}",
                                flush=True)
                          push(fmt_order(d, side, ask, prob_side), args.push)
      
              # 检查挂单 + 结算已收盘持仓 (每5秒)
              check_pending(args.push)
              settle_open(load_paper(), args.push)
      
              time.sleep(args.refresh)
      
      
      if __name__ == "__main__":
          main()
      
    • 5minbtc_signal_stats.py 7.2 KB
      #!/usr/bin/env python3
      """重大信号模拟单 — 结算 + 按时间分类回测.
      
      读 paper 台账的 auto 单 (realtime 重大信号自动记录):
      1. 结算已收盘的 open 单 (拉真实 K线收盘价, 判定方向对错)
      2. 按时间分类统计胜率/PnL (日期 / 小时)
      3. 可选推送到 Telegram
      
      用法:
        python3 5minbtc_signal_stats.py            # 结算 + 统计
        python3 5minbtc_signal_stats.py --push     # 推送 Telegram
        python3 5minbtc_signal_stats.py --by-hour  # 按小时分桶
      """
      import argparse
      import json
      import os
      import subprocess
      import sys
      import urllib.request
      from collections import defaultdict
      from datetime import datetime, timezone, timedelta
      from pathlib import Path
      
      PAPER = Path.home() / "bb-auto" / "5minbtc-paper.json"
      PUSH = Path(__file__).resolve().parent / "telegram_push.py"
      PY = "/usr/bin/python3"
      CST = timezone(timedelta(hours=8))
      KLINE_URL = "https://data-api.binance.vision/api/v3/klines"
      
      
      def fetch_close(candle_iso):
          """拉该 5min K线的 open/close. 返回 (open, close) 或 None."""
          try:
              dt = datetime.fromisoformat(candle_iso)
              start_ms = int(dt.timestamp() * 1000)
              url = f"{KLINE_URL}?symbol=BTCUSDT&interval=5m&startTime={start_ms}&limit=1"
              req = urllib.request.Request(url, headers={"User-Agent": "signal-stats/1"})
              data = json.loads(urllib.request.urlopen(req, timeout=10).read())
              if data:
                  return float(data[0][1]), float(data[0][4])
          except Exception:
              pass
          return None
      
      
      def settle_bets(state):
          """结算已收盘的 open 单 (pending 单由 realtime check_pending 处理). 返回本次结算数."""
          now_ms = int(datetime.now(CST).timestamp() * 1000)
          settled = 0
          for b in state.get("bets", []):
              if b.get("status") != "open":
                  continue
              try:
                  dt = datetime.fromisoformat(b["candle"])
                  close_ms = int(dt.timestamp() * 1000) + 5 * 60 * 1000
              except Exception:
                  continue
              if now_ms < close_ms:
                  continue
              ohlc = fetch_close(b["candle"])
              if ohlc is None:
                  continue
              o, c = ohlc
              side = b["side"]
              won = (side == "UP" and c > o) or (side == "DOWN" and c < o)
              b["status"] = "settled"
              b["outcome"] = "win" if won else "loss"
              b["actual_open"] = o
              b["actual_close"] = c
              b["direction_correct"] = won
              amount = b.get("amount", 1.0)
              ask = b.get("ask", 0.0)
              fee = b.get("fee", 0.0)
              # 投入 amount 买 token @ ask: 赢→token变1,赚 amount*(1-ask)/ask; 输→token变0,亏 amount(全额)
              if won:
                  b["pnl"] = round(amount * (1 - ask) / ask - amount * fee, 4)
              else:
                  b["pnl"] = round(-amount - amount * fee, 4)
              b["pnl_pct"] = round(b["pnl"] / amount * 100, 2) if amount else 0.0
              settled += 1
          # 重建 realized = 所有 settled 单 pnl 总和 (修正历史遗漏)
          state["realized"] = round(sum(b.get("pnl", 0) for b in state.get("bets", [])
                                        if b.get("status") == "settled"), 4)
          return settled
      
      
      def load_env():
          envf = Path.home() / "bb-auto" / "prediction.env"
          if envf.exists():
              for line in envf.read_text().splitlines():
                  line = line.strip()
                  if not line or line.startswith("#") or "=" not in line:
                      continue
                  k, v = line.split("=", 1)
                  if v.strip():
                      os.environ.setdefault(k.strip(), v.strip())
      
      
      def get_up_down_price():
          """拉当前 BTC 5m 预测市场 UP/DOWN 真实价 (算持仓未实现盈亏)."""
          try:
              load_env()
              sys.path.insert(0, str(Path(__file__).resolve().parent))
              import importlib
              trader = importlib.import_module("5minbtc_trader")
              found = trader.find_btc_5m_market()
              if not found:
                  return None, None
              _, mid, up_tok, dn_tok, _, _ = found
              return trader._token_price(mid, up_tok), trader._token_price(mid, dn_tok)
          except Exception:
              return None, None
      
      
      def load_state():
          if PAPER.exists():
              try:
                  return json.loads(PAPER.read_text())
              except Exception:
                  pass
          return {"bets": []}
      
      
      def save_state(state):
          PAPER.write_text(json.dumps(state, indent=2, ensure_ascii=False))
      
      
      def fmt_report(state, by_hour=False):
          auto = [b for b in state.get("bets", []) if b.get("auto")]
          settled = [b for b in auto if b.get("status") == "settled"]
          pending = [b for b in auto if b.get("status") == "pending"]
          unfilled = [b for b in auto if b.get("status") == "unfilled"]
          open_ = [b for b in auto if b.get("status") == "open"]
          lines = ["📊 重大信号回测 | " + ("按小时" if by_hour else "按日期")]
          lines.append("━" * 26)
          lines.append(f"信号 {len(auto)} | 已结算 {len(settled)} | 挂单 {len(pending)} | "
                       f"未成交 {len(unfilled)} | 持仓 {len(open_)}")
      
          # 账户总览 (本金 + 已实现 + 持仓浮盈 + 总权益)
          bankroll = state.get("bankroll", 100.0)
          realized = state.get("realized", 0.0)
          up_px, down_px = get_up_down_price()
          unrealized = 0.0
          for b in open_:
              ask = b.get("ask", 0)
              cur = up_px if b["side"] == "UP" else down_px
              if cur is not None and ask and ask > 0:
                  unrealized += b.get("amount", 1) * (cur - ask) / ask
          equity = bankroll + realized + unrealized
          lines.append(f"💰 本金 ${bankroll:.0f} | 已实现 ${realized:+.2f} | "
                       f"持仓浮盈 ${unrealized:+.2f} | 权益 ${equity:.2f}")
      
          # 下单统计
          if settled:
              wins = sum(1 for b in settled if b.get("direction_correct"))
              pnl = sum(b.get("pnl", 0) for b in settled)
              avg_ask = sum(b.get("ask", 0) for b in settled) / len(settled)
              lines.append(f"✅下单: 胜率 {wins}/{len(settled)} = {wins/len(settled)*100:.0f}% | "
                           f"PnL ${pnl:+.2f} | 均ask {avg_ask:.2f}")
      
          if not settled:
              lines.append("暂无已结算信号")
              return "\n".join(lines)
      
          # 按时间分类 (仅下单)
          key_fn = (lambda b: b["candle"][11:13] + ":00") if by_hour else (lambda b: b["candle"][:10])
          groups = defaultdict(list)
          for b in settled:
              groups[key_fn(b)].append(b)
          if groups:
              lines.append("━" * 26)
              for k in sorted(groups):
                  g = groups[k]
                  w = sum(1 for b in g if b.get("direction_correct"))
                  p = sum(b.get("pnl", 0) for b in g)
                  avg_ask = sum(b.get("ask", 0) for b in g) / len(g)
                  lines.append(f"{k}: {len(g)}笔 胜率{w/len(g)*100:.0f}% PnL${p:+.2f} 均ask{avg_ask:.2f}")
          return "\n".join(lines)
      
      
      def main():
          ap = argparse.ArgumentParser(description="重大信号回测(结算+按时间分类)")
          ap.add_argument("--push", action="store_true")
          ap.add_argument("--by-hour", action="store_true")
          args = ap.parse_args()
      
          state = load_state()
          settled = settle_bets(state)
          save_state(state)
          if settled:
              print(f"本次结算 {settled} 笔")
      
          report = fmt_report(state, by_hour=args.by_hour)
          print(report)
          if args.push:
              subprocess.run([PY, str(PUSH), report], capture_output=True, timeout=20)
      
      
      if __name__ == "__main__":
          main()
      
    • 5minbtc_trader.py 59.6 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """
      scripts/5minbtc_trader.py - 5minbtc 预测 → 币安「Web3 Wallet 预测交易」桥接
      
      读取 5minbtc 引擎 JSON 预测, 判定交易信号, 直接签名调用币安预测市场 API
      (BTC 5min 涨跌市场, Polymarket 式 outcome token, 非合约).
      引擎 bias=bull → 买 UP token; bias=bear → 买 DOWN token.
      
      风险契约 (铁律, 预测交易没有 test 模式 — 下单即真实花钱):
      - 默认行为 = 只取报价 (get-quote 不花钱), 绝不自动成交
      - 成交必须人工确认: 每单输入 y 同意, 且 --live 另需启动时的 yes 双闸门
      - 真实花钱的是 place-order-bundle (FOK 市价), 确认闸门就放在它前面
      - 金额默认小注 2.0 USDT, 下单前校验余额, 不足即拒
      - 市场过期保护: 每根K线只对当前 OPEN 的 btc-updown-5m 市场取价/下单
      
      用法:
        # 单次: 读引擎 → 判定信号 → 发现市场 → 报价(不成交, 除非确认)
        python3 scripts/5minbtc_trader.py --once [--amount 2.0]
        # 持续监控 (每根K线第2/3/4分钟采样)
        python3 scripts/5minbtc_trader.py --loop [--rounds N]
        # 实盘 (危险! 启动需输 yes, 每单再输 y)
        python3 scripts/5minbtc_trader.py --once --live --amount 2.0
      
      环境变量:
        BINANCE_API_KEY / BINANCE_API_SECRET   (必填, 签名用)
        BINANCE_PREDICT_WALLET                  (必填, 预测钱包地址, 从环境变量注入)
        BINANCE_PREDICT_WALLET_ID               (必填, 预测钱包 ID)
      
      依赖: 纯 Python 标准库 (urllib/hmac/hashlib/json). 不打印密钥.
      非投资建议 — 仅供量化研究, 市场风险自负.
      """
      import argparse
      import datetime
      import hashlib
      import hmac
      import json
      import os
      import sys
      import time
      import urllib.error
      import urllib.parse
      import urllib.request
      from pathlib import Path
      
      ROOT = Path(__file__).resolve().parent.parent          # = skill 根 (5minbtc/)
      ENGINE = ROOT / "5minbtc-engine-v6.0.py"
      
      # ---- 币安预测 API ----
      API_BASE = "https://api.binance.com"
      API_KEY = os.environ.get("BINANCE_API_KEY", "")
      API_SECRET = os.environ.get("BINANCE_API_SECRET", "")
      WALLET = os.environ.get("BINANCE_PREDICT_WALLET", "")          # 预测钱包地址 (必填, 从环境变量注入)
      WALLET_ID = os.environ.get("BINANCE_PREDICT_WALLET_ID", "")    # 预测钱包 ID (必填)
      ACCOUNT_TYPE = os.environ.get("BINANCE_PREDICT_ACCOUNT", "SPOT")  # SPOT|FUNDING
      
      # ---- 信号门槛 ----
      MIN_CONF = 55          # conf >= 55 才触发 (低于此区间横跳无意义)
      ALLOWED_STRENGTHS = ("medium", "moderate", "strong")
      TB_FILTER = True       # 方向必须与 taker_buy 一致
      
      # ---- 下单参数 (预测交易, 无杠杆无止损) ----
      DEFAULT_AMOUNT = 1.0      # 默认金额 USDT (模拟/实盘均每注 1U, 小注)
      MIN_ORDER_WEI = 1500000000000000000   # MARKET 单 ~1.5 USDT = 1.5e18 wei
      SLIPPAGE_BPS = 200        # 滑点容忍 2%
      WEI_18 = 10 ** 18
      CLOSE_MARGIN_SEC = 45     # 距K线收盘 <45s 警告, <=0 拒绝 (防买已过期token)
      
      # ── 真实下单硬闸门 (安全铁律) ──
      # place_order 是唯一会花钱的函数. 默认 LIVE_GATE=False → 任何情况都拒绝真实下单.
      # 只有 --live 且用户双确认后才置 True. paper/模拟 模式永远为 False.
      LIVE_GATE = False
      
      SAMPLE_MINUTES = (2, 3, 4)   # 每根 5min K线采样分钟
      SAMPLE_SECOND = 5
      
      
      # ======================== 引擎 ========================
      
      def _load_engine():
          """运行引擎, 返回 dict 或 None."""
          if not ENGINE.exists():
              print(f"ERROR engine not found: {ENGINE}", file=sys.stderr)
              return None
          try:
              out = subprocess_run()
              return json.loads(out)
          except Exception as e:  # noqa: BLE001
              print(f"engine error: {e}", file=sys.stderr)
              return None
      
      
      def subprocess_run():
          import subprocess
          return subprocess.run(["python3", str(ENGINE)],
                                capture_output=True, text=True, timeout=45).stdout
      
      
      def _signal(d, min_conf=MIN_CONF, tb_filter=TB_FILTER, strength_gate=True):
          """从引擎输出判定预测交易信号. 返回 (token_side, reason) 或 (None, reason).
          token_side: "UP" | "DOWN".
          min_conf: 置信度门槛 (paper 模式可关掉, 实测 confidence 校准不可靠)
          tb_filter: 是否要求方向与 taker_buy 一致 (paper 可关掉, 未经验证)
          strength_gate: 是否要求 strength∈medium+ (paper 可关掉)"""
          p = d["prediction"]
          f = d["factors"]
          bias, strength, conf = p["bias"], p["strength"], p["confidence"]
          tb = f.get("taker_buy", 0.0)
      
          if bias == "neutral":
              return None, "neutral"
          if conf < min_conf:
              return None, f"conf {conf} < {min_conf}"
          if strength_gate and strength not in ALLOWED_STRENGTHS:
              return None, f"strength {strength} 不足"
      
          if tb_filter:
              if bias == "bear" and tb >= 0:
                  return None, f"bear 但 taker_buy {tb:+.2f} ≥ 0, 方向不一致"
              if bias == "bull" and tb <= 0:
                  return None, f"bull 但 taker_buy {tb:+.2f} ≤ 0, 方向不一致"
      
          return ("DOWN" if bias == "bear" else "UP"), f"{bias}/{strength} conf={conf} tb={tb:+.2f}"
      
      
      # ======================== 币安预测 API 客户端 (纯标准库) ========================
      
      def _signed_request(method, path, params=None):
          """签名并发送币安 SAPI 请求. 返回解析后的 JSON dict.
      
          签名约定 (SAPI): 签名串 = 原始 urlencoded 参数串.
          GET  -> 签名串为 query string;  POST -> 签名串为 form body (原始串),
                  服务器按原始字节验签 (见 batch-cancel 已知问题: 库自动转义括号导致 -1022).
          """
          if not API_KEY or not API_SECRET:
              raise RuntimeError("缺少 BINANCE_API_KEY / BINANCE_API_SECRET 环境变量")
          params = dict(params or {})
          params.setdefault("timestamp", int(time.time() * 1000))
          params.setdefault("recvWindow", 5000)
          # 排序保证签名与发送串一致 (服务器接受任意顺序, 只要匹配)
          qs = urllib.parse.urlencode(sorted(params.items()))
          sig = hmac.new(API_SECRET.encode(), qs.encode(), hashlib.sha256).hexdigest()
          headers = {"X-MBX-APIKEY": API_KEY, "User-Agent": "5minbtc-trader/1.0"}
      
          if method == "GET":
              url = API_BASE + path + "?" + qs + "&signature=" + sig
              data = None
          else:
              headers["Content-Type"] = "application/x-www-form-urlencoded"
              url = API_BASE + path
              data = (qs + "&signature=" + sig).encode()
      
          req = urllib.request.Request(url, data=data, headers=headers, method=method)
          try:
              with urllib.request.urlopen(req, timeout=15) as resp:
                  raw = resp.read().decode("utf-8")
          except urllib.error.HTTPError as e:
              body = e.read().decode("utf-8", "replace")
              raise RuntimeError(f"HTTP {e.code} {path}: {body}") from e
          return json.loads(raw) if raw else {}
      
      
      def _parse_amount(v):
          """解析余额/金额字段为 USDT float. 兼容 显示值 / 18位wei / hexwei."""
          if v is None:
              return 0.0
          s = str(v).strip()
          if not s:
              return 0.0
          try:
              if s.lower().startswith("0x"):
                  return int(s, 16) / WEI_18
              f = float(s)
          except ValueError:
              return 0.0
          return f / WEI_18 if f > 1e12 else f
      
      
      def get_payment_balance():
          """GET balance/payment-options -> {accountType: item}. item 含
          availableBalanceDisplay / enabled / accountType."""
          data = _signed_request("GET", "/sapi/v1/w3w/wallet/prediction/balance/payment-options")
          items = data.get("items") or data.get("data") or []
          if isinstance(items, dict):      # 容错: 可能是 {accountType: ...} 映射
              items = items.values()
          return {str(it.get("accountType")): it for it in items if isinstance(it, dict)}
      
      
      def _usable_balance(bal):
          """可用余额 = 各启用 accountType 的 availableBalanceDisplay 之和 (USDT)."""
          return sum(_parse_amount(it.get("availableBalanceDisplay"))
                     for it in bal.values() if it.get("enabled", True))
      
      
      def _extract_outcome_tokens(market):
          """容错扫描 market dict 里的 UP/DOWN outcome token. 返回 (up_id, down_id)."""
          up = down = None
          lists = ("tokens", "tokenList", "outcomes", "variants", "outcomeTokens",
                   "resultTokens", "underlyingTokens")
          for key in lists:
              for t in (market.get(key) or []):
                  if not isinstance(t, dict):
                      continue
                  tid = (t.get("tokenId") or t.get("token_id") or t.get("token")
                         or t.get("resultToken") or t.get("id") or t.get("outcomeToken"))
                  if not tid:
                      continue
                  name = " ".join(str(t.get(k) or "") for k in
                                  ("name", "title", "outcome", "outcomeName",
                                   "displayName", "side", "type", "symbol"))
                  nl = name.lower().strip()
                  # 方向判定用「词首/精确匹配」, 避免 "uptime"/"updatedAt" 等子串误判
                  up_hit = (nl in ("up", "bull", "bullish", "y", "yes", "1")
                            or nl.startswith("up ") or nl.startswith("bull")
                            or nl.startswith("yes"))
                  down_hit = (nl in ("down", "bear", "bearish", "n", "no", "0")
                              or nl.startswith("down ") or nl.startswith("bear")
                              or nl.startswith("no"))
                  if up_hit:
                      if up is None:
                          up = str(tid)
                  elif down_hit:
                      if down is None:
                          down = str(tid)
          # 兜底: 浅层递归扫描嵌套 dict 中形如 {tokenId, name含Up/Down} 的条目
          if up is None or down is None:
              stack = list(market.values())
              while stack and (up is None or down is None):
                  v = stack.pop()
                  if isinstance(v, dict):
                      tid = (v.get("tokenId") or v.get("token_id") or v.get("token")
                             or v.get("resultToken") or v.get("id"))
                      if tid:
                          name = " ".join(str(v.get(k) or "") for k in
                                          ("name", "title", "outcome", "outcomeName", "side"))
                          nl = name.lower().strip()
                          if up is None and (nl in ("up", "bull", "bullish", "y", "yes")
                                             or nl.startswith("up ") or nl.startswith("bull")):
                              up = str(tid)
                          elif down is None and (nl in ("down", "bear", "bearish", "n", "no")
                                                 or nl.startswith("down ") or nl.startswith("bear")):
                              down = str(tid)
                      stack.extend(v.values())
                  elif isinstance(v, (list, tuple)):
                      stack.extend(x for x in v if isinstance(x, (dict, list, tuple)))
          return up, down
      
      
      def find_btc_5m_market():
          """发现当前 OPEN 的 btc-updown-5m 市场.
          返回 (marketTopicId, marketId, up_token_id, down_token_id) 或 None."""
          data = _signed_request("GET", "/sapi/v1/w3w/wallet/prediction/market/list",
                                 {"pageSize": 50, "pageNum": 1})
          topics = data.get("marketTopics") or data.get("data") or []
          if isinstance(topics, dict):
              topics = topics.get("marketTopics") or topics.get("list") or []
          for t in topics:
              if not isinstance(t, dict):
                  continue
              slug = str(t.get("slug") or "")
              if not slug.startswith("btc-updown-5m"):
                  continue
              topic_id = t.get("marketTopicId")
              title = t.get("title") or slug
              for m in (t.get("markets") or []):
                  if not isinstance(m, dict):
                      continue
                  if m.get("tradingStatus") != "OPEN":
                      continue
                  market_id = m.get("marketId") or m.get("id")
                  up_tok, down_tok = _extract_outcome_tokens(m)
                  if up_tok is None or down_tok is None:
                      # 市场列表没带 token, 尝试 market/detail 补全
                      try:
                          detail = _signed_request(
                              "GET", "/sapi/v1/w3w/wallet/prediction/market/detail",
                              {"marketId": market_id})
                          for cand in (detail.get("variants") or detail.get("data") or [detail]):
                              u, d = _extract_outcome_tokens(cand)
                              if up_tok is None:
                                  up_tok = u
                              if down_tok is None:
                                  down_tok = d
                      except Exception as e:  # noqa: BLE001
                          print(f"market/detail 补全 token 失败: {e}", file=sys.stderr)
                  if up_tok and down_tok:
                      # 收盘时间: topic.endDate (epoch ms), market 层无此字段
                      return (topic_id, market_id, up_tok, down_tok,
                              title, t.get("endDate") or m.get("endDate"))
          return None
      
      
      def get_quote(wallet, token_id, side, amount_usdt, order_type="LIMIT",
                    price_limit=None):
          """POST get-quote. 返回响应 dict (含 quoteId).
          side="BUY"|"SELL"; amountIn 用 18位 wei 字符串.
          order_type="LIMIT"(默认, 低挂) | "MARKET"(市价).
          LIMIT 需 price_limit (token 价格, 0~1), 默认 None 则用市价. """
          body = {
              "walletAddress": wallet,
              "tokenId": token_id,
              "side": side,
              "amountIn": str(int(round(amount_usdt * WEI_18))),
              "orderType": order_type,
              "slippageBps": SLIPPAGE_BPS,
          }
          if order_type == "LIMIT":
              if price_limit is None:
                  raise ValueError("LIMIT 单需提供 price_limit (token 价格 0~1)")
              body["priceLimit"] = str(int(round(price_limit * WEI_18)))
          return _signed_request("POST", "/sapi/v1/w3w/wallet/prediction/trade/get-quote", body)
      
      
      def place_order(wallet, wallet_id, quote_id, amount_usdt=None, account_type=ACCOUNT_TYPE,
                      order_type="LIMIT"):
          """POST place-order-bundle (真实花钱). 返回响应 dict (含 orderId).
          LIMIT → timeInForce=GTC (挂单等成交); MARKET → FOK.
      
          🔒 硬闸门: LIVE_GATE 必须为 True (仅 --live 双确认后), 否则拒绝.
          paper/模拟 模式绝不允许调用此函数."""
          if not LIVE_GATE:
              raise RuntimeError(
                  "🔒 真实下单被硬性禁用 (LIVE_GATE=False). 当前为模拟模式, 绝不下单.")
          body = {
              "walletAddress": wallet,
              "walletId": wallet_id,
              "quoteId": quote_id,
              "timeInForce": "GTC" if order_type == "LIMIT" else "FOK",
              "accountType": account_type,
              "orderType": order_type,
              "slippageBps": SLIPPAGE_BPS,
          }
          if order_type == "LIMIT":
              # 限价挂单不自动划转 CEX 资金 (资金来自 MPC 钱包)
              pass
          else:
              if amount_usdt is not None:
                  body["fundingSource"] = "CEX"
                  body["fundTransferAmount"] = str(int(round(amount_usdt * WEI_18)))
          return _signed_request("POST", "/sapi/v1/w3w/wallet/prediction/trade/place-order-bundle",
                                 body)
      
      
      def _order_book(market_id, token_id=None, limit=5):
          """GET order-book: 确认 outcome token 的价格 (up+down≈1).
          需 marketId (+ tokenId). vendor 必填 (服务端约定 predict_fun)."""
          params = {"marketId": market_id, "limit": limit, "vendor": "predict_fun"}
          if token_id:
              params["tokenId"] = token_id
          data = _signed_request("GET", "/sapi/v1/w3w/wallet/prediction/order-book", params)
          return data
      
      
      def get_positions():
          """GET position/list -> {summary, positions[]}. 当前活跃持仓."""
          data = _signed_request("GET", "/sapi/v1/w3w/wallet/prediction/position/list",
                                 {"walletAddress": WALLET, "pageSize": 100, "pageNum": 1})
          return data
      
      
      def sell_token(token_id, amount_usdt, market_id=None):
          """SELL 卖出持仓 token 锁利/止损 (真实成交). 返回响应 dict."""
          quote = get_quote(WALLET, token_id, "SELL", amount_usdt, order_type="MARKET")
          qid = quote.get("quoteId")
          if not qid:
              raise RuntimeError(f"SELL 报价失败: {quote}")
          return place_order(WALLET, WALLET_ID, qid, order_type="MARKET")
      
      
      def get_btc_price():
          """GET 公开 ticker/price (免签名) -> BTC 实时价 (float)."""
          url = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT"
          req = urllib.request.Request(url, headers={"User-Agent": "5minbtc-trader/1.0"})
          with urllib.request.urlopen(req, timeout=10) as resp:
              data = json.loads(resp.read().decode())
          return float(data.get("price", 0))
      
      
      # ======================== 重试机制 ========================
      
      def _retry(fn, retries=10, base_delay=1.0, desc="操作"):
          """带退避的重试. 单次失败不致命, 重试 retries 次 (默认10), 间隔 1,2,4...s.
          全失败抛最后一个异常."""
          last = None
          for i in range(retries):
              try:
                  return fn()
              except Exception as e:  # noqa: BLE001
                  last = e
                  if i < retries - 1:
                      delay = base_delay * (2 ** i)
                      print(f"  {desc} 失败 (第{i+1}次): {str(e)[:120]}, {delay:.0f}s 后重试",
                            file=sys.stderr)
                      time.sleep(delay)
          raise last if last else RuntimeError(f"{desc} 重试耗尽")
      
      
      # ======================== 持仓监控 + 止盈止损 ========================
      
      def _pos_field(p, *keys, default=None):
          """容错取持仓字段 (多 key 兼容不同响应结构)."""
          for k in keys:
              if k in p and p[k] not in (None, ""):
                  return p[k]
          return default
      
      
      def _pos_token_id(p):
          v = _pos_field(p, "tokenId", "token_id", "resultToken", "outcomeToken")
          return str(v) if v is not None else None
      
      
      def _pos_market_id(p):
          return _pos_field(p, "marketId", "market_id")
      
      
      def _pos_qty(p):
          v = _pos_field(p, "quantity", "qty", "amount", "shareQty", "makerShareQty")
          if v is None:
              return 0.0
          try:
              return float(v) / WEI_18 if float(v) > 1e12 else float(v)
          except (TypeError, ValueError):
              return 0.0
      
      
      def _pos_price(p, *keys):
          v = _pos_field(p, *keys)
          if v is None:
              return None
          try:
              return float(v) / WEI_18 if float(v) > 1e12 else float(v)
          except (TypeError, ValueError):
              return None
      
      
      def _pos_side(p):
          s = str(_pos_field(p, "side", "outcome", "outcomeName", "tokenSide", default="") or "").lower()
          if "down" in s or "bear" in s:
              return "DOWN"
          if "up" in s or "bull" in s:
              return "UP"
          return "?"
      
      
      def monitor_positions(tp_mult=1.4, sl_mult=0.4, interval=10, live=False,
                            max_iters=None):
          """实时监控持仓: 面板 + 止盈/止损自动卖出.
          止盈/止损只在 tp_mult/sl_mult 都 >0 且显式启用时自动执行.
          全部卖出走 SELL, 带重试 (避免单次失败).
          返回: 卖出次数."""
          print(f"\n=== 持仓监控开始 (tp={tp_mult:.2f}x, sl={sl_mult:.2f}x, "
                f"interval={interval}s, live={'YES' if live else 'NO'}) ===")
          if not (tp_mult > 0 and sl_mult > 0):
              print("⚠️  未设定 TP/SL, 仅监控不自动卖出", file=sys.stderr)
          iters = 0
          sold = 0
          while max_iters is None or iters < max_iters:
              iters += 1
              try:
                  # BTC 实时价
                  try:
                      btc = get_btc_price()
                  except Exception as e:  # noqa: BLE001
                      btc = None
                      print(f"  BTC 价获取失败: {e}", file=sys.stderr)
                  # 持仓
                  data = _retry(get_positions, desc="查询持仓")
                  summary = data.get("summary") or {}
                  positions = data.get("positions") or []
                  if isinstance(positions, dict):
                      positions = positions.values()
      
                  # 面板
                  print("\n" + "-" * 56)
                  print(f"[#{iters}] BTC={btc if btc else 'N/A'}"
                        f"  总资产={summary.get('totalValue', 'N/A')}"
                        f"  今日已实现盈亏={summary.get('todayRealizedPnl', 'N/A')}"
                        f" ({summary.get('todayRealizedPnlPercent', 'N/A')}%)")
                  if not positions:
                      print("  无活跃持仓")
                  for p in positions:
                      tid = _pos_token_id(p)
                      mkt = _pos_market_id(p)
                      side = _pos_side(p)
                      qty = _pos_qty(p)
                      cost = _pos_price(p, "avgPrice", "averagePrice", "entryPrice")
                      cur = _pos_price(p, "currentPrice", "lastPrice", "price")
                      upnl = _pos_price(p, "unrealizedPnl", "unrealizedProfit", "pnl")
                      # 现价缺失则从 order-book 补
                      if cur is None and mkt is not None and tid is not None:
                          try:
                              ob = _retry(lambda: _order_book(mkt, tid), desc="盘口")
                              asks = ob.get("asks") or []
                              bids = ob.get("bids") or []
                              cur = float(asks[0]["price"]) if asks else (
                                  float(bids[0]["price"]) if bids else None)
                          except Exception as e:  # noqa: BLE001
                              print(f"  [盘口] {e}", file=sys.stderr)
                      print(f"  [{side}] qty={qty:.4f}  cost={cost}  cur={cur}  "
                            f"uPnL={upnl}")
                      # 止盈/止损判断
                      if not (tp_mult > 0 and sl_mult > 0):
                          continue
                      if cost is None or cur is None or qty <= 0:
                          continue
                      mult = cur / cost
                      action = None
                      if mult >= tp_mult:
                          action = f"止盈 (现价/成本 = {mult:.2f} ≥ {tp_mult})"
                      elif mult <= sl_mult:
                          action = f"止损 (现价/成本 = {mult:.2f} ≤ {sl_mult})"
                      if action:
                          print(f"  ⚡ 触发{action} → 自动卖出 {side} qty={qty:.4f} "
                                f"@ {cur}")
                          if not live:
                              print("  (演练模式, 不实际卖出)")
                              continue
                          amt = max(cur * qty, 1.5)
                          try:
                              res = _retry(lambda: sell_token(tid, amt, mkt),
                                           desc=f"卖出 {side}")
                              sold += 1
                              print(f"  ✅ 卖出完成: {res}")
                          except Exception as e:  # noqa: BLE001
                              print(f"  ❌ 卖出失败: {e}", file=sys.stderr)
              except Exception as e:  # noqa: BLE001
                  print(f"监控轮次异常: {e}", file=sys.stderr)
              if max_iters is not None and iters >= max_iters:
                  break
              time.sleep(interval)
          return sold
      
      
      # ======================== 闸门 / 输出 ========================
      
      def _market_expiry_ok(d, close_time=None):
          """市场过期校验. 返回 remaining_sec (>=0).
          优先用 market closeTime (epoch ms 或 ISO), 否则用引擎 remaining_sec.
          remaining_sec <= 0 表示已收盘, 应拒绝下单.
          """
          now_epoch = time.time()
          if close_time:
              try:
                  if isinstance(close_time, str) and not close_time.isdigit():
                      # ISO 时间 (如 2026-08-11T12:35:00Z)
                      from datetime import timezone
                      import datetime as _dt
                      dt = _dt.datetime.fromisoformat(close_time.replace("Z", "+00:00"))
                      close_epoch = dt.timestamp()
                  else:
                      close_epoch = float(close_time) / 1000  # epoch ms
                  return close_epoch - now_epoch
              except Exception:  # noqa: BLE001
                  pass
          return float(d.get("candle", {}).get("remaining_sec", 300))
      
      
      def _confirm_place(title, token_side, token_id, amount_usdt, quote, live):
          """place-order 前的人工确认闸门. 返回 True=同意."""
          qid = quote.get("quoteId")
          print("\n--- 待确认下单 (预测交易, 无test模式, 下单即真实花钱) ---")
          print(f"  市场: {title}")
          print(f"  买入: {token_side} token  {token_id}")
          print(f"  金额: {amount_usdt:.2f} USDT")
          print(f"  quoteId: {qid}")
          for k in ("chance", "averagePrice", "minReceive", "totalFee", "maxAmountIn"):
              if quote.get(k) is not None:
                  print(f"  {k}: {quote[k]}")
          if live:
              print("  ⚠️  实盘模式已开启")
          try:
              ans = input("  确认成交? [y/N]: ").strip().lower()
          except EOFError:
              ans = ""
          return ans in ("y", "yes")
      
      
      # ======================== 主流程 ========================
      
      def _print_quote_summary(title, token_side, token_id, amount_usdt, quote):
          print("\n=== 报价摘要 ===")
          print(f"  市场: {title}")
          print(f"  方向: BUY {token_side} token  {token_id}")
          print(f"  金额: {amount_usdt:.2f} USDT  (={int(round(amount_usdt*WEI_18))} wei)")
          print(f"  quoteId: {quote.get('quoteId')}")
          for k in ("chance", "averagePrice", "minReceive", "totalFee", "slippageBps"):
              if quote.get(k) is not None:
                  print(f"  {k}: {quote[k]}")
      
      
      def run_once(live=False, amount=DEFAULT_AMOUNT, order_type="LIMIT",
                   limit_price=None, limit_up_max=0.65, limit_down_max=0.50):
          """单次: 读引擎 → 判信号 → 发现市场 → 校验余额 → 报价 → (确认后)下单.
          LIMIT 默认限价 = 该方向入场上限 (limit_up_max/limit_down_max),
          而非 ask×0.98 — 5分钟市场波动快, 挂低 2% 常不成交, 错过整笔 EV."""
          d = _load_engine()
          if not d:
              return 1
          side, reason = _signal(d)
          c = d["candle"]
          p = d["prediction"]
          print(f"[{c.get('iso')} p{c.get('progress_pct', 0):.0f}%] "
                f"{p['bias']}/{p['strength']} conf={p['confidence']} "
                f"px={d['price']['current']} tb={d['factors'].get('taker_buy'):+.2f}")
          print(f"信号: {side or '无'}  ({reason})")
          if not side:
              return 0
      
          # 金额下限: LIMIT 单可低至 0.5U (交易所仅 MARKET 强制 ≥1.5U)
          min_amt = 0.5 if order_type == "LIMIT" else 1.5
          if amount < min_amt:
              print(f"金额 {amount:.2f} USDT < {order_type} 单最低 {min_amt:.1f} USDT, 拒绝",
                    file=sys.stderr)
              return 1
      
          # 余额校验 (get-quote 前先看钱够不够)
          try:
              bal = get_payment_balance()
          except Exception as e:  # noqa: BLE001
              print(f"查询余额失败: {e}", file=sys.stderr)
              return 1
          usable = _usable_balance(bal)
          print("\n=== 余额 (payment-options) ===")
          for acc, it in bal.items():
              print(f"  {acc}: {_parse_amount(it.get('availableBalanceDisplay')):.2f} USDT"
                    f"  enabled={it.get('enabled', True)}")
          print(f"  可用合计: {usable:.2f} USDT")
          if usable < amount:
              print(f"余额不足: 可用 {usable:.2f} < 需要 {amount:.2f}, 拒绝", file=sys.stderr)
              return 1
      
          # 发现当前 OPEN 市场 + token 映射
          try:
              found = find_btc_5m_market()
          except Exception as e:  # noqa: BLE001
              print(f"发现市场失败: {e}", file=sys.stderr)
              return 1
          if not found:
              print("未发现 OPEN 的 btc-updown-5m 市场 (可能已结算/暂停)", file=sys.stderr)
              return 1
          topic_id, market_id, up_tok, down_tok, title, close_time = found
          token_id = up_tok if side == "UP" else down_tok
          print("\n=== 市场 ===")
          print(f"  topic: {title}  (topicId={topic_id})")
          print(f"  marketId={market_id}  closeTime={close_time}")
          print(f"  UP token:   {up_tok}")
          print(f"  DOWN token: {down_tok}")
          print(f"  → 买入 {side}: {token_id}")
      
          # order-book 双重确认 token 映射 (响应带 outcome 字段, 最可靠)
          # 同时捕获目标 token 的 ask 价, 用于 LIMIT 低挂
          tok_ok = True
          token_ask = None
          for label, tid in (("UP", up_tok), ("DOWN", down_tok)):
              try:
                  ob = _order_book(market_id, tid)
                  bids = ob.get("bids") or []
                  asks = ob.get("asks") or []
                  b0 = bids[0].get("price") if bids else None
                  a0 = asks[0].get("price") if asks else None
                  outcome = ob.get("outcome")
                  print(f"  [{label}] token={tid}  outcome={outcome}"
                        f"  top_bid={b0}  top_ask={a0}")
                  if label == side and a0:
                      token_ask = float(a0)
                  # 强制校验: order-book outcome 必须与目标方向一致, 否则拒绝 (防误映射)
                  if outcome:
                      ol = str(outcome).lower()
                      if label == "UP" and not (ol.startswith("up") or ol.startswith("bull") or ol == "y"):
                          tok_ok = False
                      if label == "DOWN" and not (ol.startswith("down") or ol.startswith("bear") or ol == "n"):
                          tok_ok = False
              except Exception as e:  # noqa: BLE001
                  print(f"  [{label}] order-book 查询失败: {e}", file=sys.stderr)
          if not tok_ok:
              print("order-book outcome 与目标方向不符, 拒绝下单 (token 映射可能错)", file=sys.stderr)
              return 1
      
          # 市场过期保护: 用 market closeTime (真实) + 引擎 remaining_sec 双校验
          remain = _market_expiry_ok(d, close_time)
          if remain <= 0:
              print(f"市场已收盘 (remaining_sec={remain:.0f}), 拒绝下单", file=sys.stderr)
              return 1
          if remain < CLOSE_MARGIN_SEC:
              print(f"⚠️  距收盘仅 {remain}s (< {CLOSE_MARGIN_SEC}s), 风险高, 请自行判断",
                    file=sys.stderr)
      
          # 报价 (不花钱) — 默认 LIMIT 价格 = 该方向入场上限
          lp = limit_price
          if lp is None and order_type == "LIMIT":
              lp = round(limit_up_max if side == "UP" else limit_down_max, 4)
          if order_type == "LIMIT":
              _st = ""
              if token_ask is not None:
                  _st = "→ 立即成交" if token_ask <= lp else "→ 等回调(可能不成交)"
              print(f"\n限价单: 买入 {side} token @ {lp} (当前 ask={token_ask}) {_st}")
          try:
              quote = get_quote(WALLET, token_id, "BUY", amount,
                                order_type=order_type, price_limit=lp)
          except Exception as e:  # noqa: BLE001
              print(f"获取报价失败: {e}", file=sys.stderr)
              return 1
          qid = quote.get("quoteId")
          _print_quote_summary(title, side, token_id, amount, quote)
          if not qid:
              print("get-quote 未返回 quoteId (可能余额不足或市场关闭)", file=sys.stderr)
              return 1
      
          # 确认闸门 (place-order 前, 即真实花钱前)
          if not _confirm_place(title, side, token_id, amount, quote, live):
              print("已取消下单")
              return 0
          try:
              res = place_order(WALLET, WALLET_ID, qid, amount_usdt=amount,
                                order_type=order_type)
          except Exception as e:  # noqa: BLE001
              print(f"下单失败: {e}", file=sys.stderr)
              return 1
          print("\n=== 下单结果 ===")
          print(json.dumps(res, ensure_ascii=False, indent=2))
          if res.get("orderId"):
              print(f"成交! orderId={res['orderId']}")
          return 0
      
      
      def run_loop(live=False, amount=DEFAULT_AMOUNT, rounds=None,
                   order_type="LIMIT", limit_price=None,
                   limit_up_max=0.65, limit_down_max=0.50):
          """持续监控: 每根K线第2/3/4分钟采样. 每根K线只处理一次."""
          seen = set()
          n = 0
          while rounds is None or n < rounds:
              now = datetime.datetime.now()
              if now.minute % 5 in SAMPLE_MINUTES and now.second >= SAMPLE_SECOND:
                  d = _load_engine()
                  if d:
                      c = d["candle"]
                      key = (c["iso"], now.minute // 5)
                      if key not in seen:
                          seen.add(key)
                          n += 1
                          run_once(live=live, amount=amount, order_type=order_type,
                                   limit_price=limit_price, limit_up_max=limit_up_max,
                                   limit_down_max=limit_down_max)
                  time.sleep(20)
              else:
                  time.sleep(10)
          return 0
      
      
      # ======================== 预测市场 Paper 模拟 (真实报价, 绝不下单) ========================
      # 策略: 引擎方向信号 + 入场价门控 (ask < 该方向胜率-手续费) → 记录假设成交 → K线收盘结算.
      # 与实盘共用同一套 API 客户端, 但只 get-quote/order-book, 从不 place-order (不花钱).
      # 实测教训 (2026-08-12): 引擎 confidence 校准不可靠 (反相关), 默认不设 conf 门槛;
      # strength/taker_buy 过滤未经验证, 默认关闭 — 用 --paper-strength-gate / --paper-tb-filter 开启.
      
      PAPER_FILE_DEFAULT = os.path.expanduser("~/bb-auto/5minbtc-paper.json")
      
      
      def _fetch_klines(symbol="BTCUSDT", interval="5m", start_ms=None, limit=1):
          """免签名拉取 K 线 (结算用). 返回 [(open, close)]."""
          base = "https://data-api.binance.vision/api/v3/klines"
          params = {"symbol": symbol, "interval": interval, "limit": str(limit)}
          if start_ms:
              params["startTime"] = str(int(start_ms))
          url = base + "?" + urllib.parse.urlencode(params)
          req = urllib.request.Request(url, headers={"User-Agent": "5minbtc-paper/1"})
          with urllib.request.urlopen(req, timeout=15) as resp:
              data = json.loads(resp.read().decode())
          return [(float(k[1]), float(k[4])) for k in data]
      
      
      def load_paper(path):
          if os.path.exists(path):
              try:
                  with open(path) as f:
                      return json.load(f)
              except Exception:
                  pass
          return {"bets": [], "realized": 0.0, "bankroll": 100.0,
                  "started": datetime.datetime.now().isoformat()}
      
      
      def save_paper(path, state):
          with open(path, "w") as f:
              json.dump(state, f, indent=2, ensure_ascii=False)
      
      
      def settle_paper(state, fee=0.0):
          """结算所有已收盘的未结算纸面注单. 返回本次结算数."""
          now_ms = int(time.time() * 1000)
          settled = 0
          for b in state["bets"]:
              if b.get("status") != "open":
                  continue
              try:
                  dt = datetime.datetime.fromisoformat(b["candle"])
                  close_ms = int(dt.timestamp() * 1000) + 5 * 60 * 1000
              except Exception:
                  continue
              if now_ms < close_ms:
                  continue
              ohlc = _fetch_klines(start_ms=int(dt.timestamp() * 1000))
              if not ohlc:
                  continue
              o, c = ohlc[0]
              side = b["side"]
              won = (side == "UP" and c > o) or (side == "DOWN" and c < o)
              ask, amt, f = b["ask"], b["amount"], b.get("fee", fee)
              pnl = (amt * (1 - ask) - amt * f) if won else (-amt * ask - amt * f)
              b["status"] = "settled"
              b["outcome"] = "win" if won else "loss"
              b["actual_open"], b["actual_close"] = o, c
              b["pnl"] = round(pnl, 4)
              state["realized"] = round(state["realized"] + pnl, 4)
              settled += 1
          return settled
      
      
      def paper_report(state):
          settled = [b for b in state["bets"] if b.get("status") == "settled"]
          open_ = [b for b in state["bets"] if b.get("status") == "open"]
          pending = [b for b in state["bets"] if b.get("status") == "pending"]
          unfilled = [b for b in state["bets"] if b.get("status") == "unfilled"]
          wins = sum(1 for b in settled if b.get("outcome") == "win")
          total = len(settled)
          bankroll = state.get("bankroll", 100.0)
          realized = state.get("realized", 0.0)
          equity = bankroll + realized
          lines = ["🎯 夜盘狙击 · 实测记录"]
          lines.append(f"注单 {len(state['bets'])} (已结算 {total} / 成交持仓 {len(open_)} "
                       f"/ 挂单 {len(pending)} / 未成交 {len(unfilled)})")
          if total:
              lines.append(f"胜率 {wins}/{total} = {wins / total * 100:.0f}% | "
                           f"已实现 ${realized:+.2f}")
          lines.append(f"账户: ${bankroll:.0f} → ${equity:.2f} "
                       f"({realized / bankroll * 100:+.2f}%)")
          for b in settled[-5:]:
              mark = "✅" if b.get("outcome") == "win" else "❌"
              lines.append(f"  {mark} {b['candle'][5:16]} {b['side']:4s} @{b['ask']:.2f} "
                           f"PnL {b.get('pnl', 0):+.2f}$")
          for b in open_:
              lines.append(f"  ⏳ {b['candle'][5:16]} {b['side']:4s} @{b['ask']:.2f} 待结算")
          for b in pending:
              lines.append(f"  📋 {b['candle'][5:16]} {b['side']:4s} 限价 {b['limit']:.2f} 挂单中")
          for b in unfilled:
              lines.append(f"  💨 {b['candle'][5:16]} {b['side']:4s} 限价 {b['limit']:.2f} 未成交")
          return "\n".join(lines)
      
      
      def run_paper(amount=2.0, fee=0.0, up_max=0.65, down_max=0.55,
                    paper_file=None, push=False, tb_filter=False, min_conf=0,
                    strength_gate=False):
          """一次 paper 周期: 结算已收盘 → 引擎信号 → 真实报价 → 价格门控记录 → 报告."""
          paper_file = paper_file or PAPER_FILE_DEFAULT
          state = load_paper(paper_file)
          settled = settle_paper(state, fee)
      
          d = _load_engine()
          if not d:
              return 1
          side, reason = _signal(d, min_conf=min_conf, tb_filter=tb_filter,
                                 strength_gate=strength_gate)
          c = d["candle"]
          candle = c.get("iso")
          print(f"[{candle} p{c.get('progress_pct', 0):.0f}%] "
                f"bias={d['prediction']['bias']} conf={d['prediction']['confidence']} "
                f"信号={side or '无'} ({reason})")
          print(f"本次结算 {settled} 笔")
      
          if side and not any(b.get("candle") == candle and b.get("status") == "open"
                              for b in state["bets"]):
              try:
                  found = find_btc_5m_market()
              except Exception as e:
                  print(f"发现市场失败: {e}", file=sys.stderr)
                  found = None
              if found:
                  _, market_id, up_tok, down_tok, title, _ = found
                  tok = up_tok if side == "UP" else down_tok
                  ask = None
                  try:
                      ob = _order_book(market_id, tok)
                      asks = ob.get("asks") or []
                      if asks:
                          ask = float(asks[0]["price"])
                  except Exception as e:
                      print(f"盘口失败: {e}", file=sys.stderr)
                  max_price = up_max if side == "UP" else down_max
                  if ask is not None:
                      if ask <= max_price:
                          state["bets"].append({
                              "candle": candle, "side": side, "ask": round(ask, 4),
                              "amount": amount, "fee": fee, "status": "open",
                              "market_id": market_id, "token_id": tok,
                              "ts": datetime.datetime.now().isoformat(), "reason": reason,
                          })
                          print(f"✅ Paper 记录: BUY {side} @ {ask:.4f} (≤{max_price:.2f}) "
                                f"{amount} USDT")
                      else:
                          print(f"⏭️ 价格门控跳过: {side} ask {ask:.4f} > 上限 {max_price:.2f}")
                  else:
                      print("无法获取 ask, 跳过")
          save_paper(paper_file, state)
      
          report = paper_report(state)
          print("\n" + report)
          if push:
              try:
                  import subprocess
                  subprocess.run(["python3", str(ROOT / "scripts" / "telegram_push.py"), report],
                                 capture_output=True, timeout=20)
              except Exception:
                  pass
          return 0
      
      
      def run_paper_loop(amount=2.0, fee=0.0, up_max=0.65, down_max=0.55,
                         paper_file=None, push=False, tb_filter=False, min_conf=0,
                         strength_gate=False, rounds=None):
          """持续 paper 模拟: 每根K线第2/3/4分钟采样, 每根K线处理一次."""
          seen = set()
          n = 0
          while rounds is None or n < rounds:
              now = datetime.datetime.now()
              if now.minute % 5 in SAMPLE_MINUTES and now.second >= SAMPLE_SECOND:
                  key = now.strftime("%H:%M")
                  if key not in seen:
                      seen.add(key)
                      n += 1
                      run_paper(amount=amount, fee=fee, up_max=up_max, down_max=down_max,
                                paper_file=paper_file, push=push, tb_filter=tb_filter,
                                min_conf=min_conf, strength_gate=strength_gate)
                  time.sleep(20)
              else:
                  time.sleep(10)
          return 0
      
      
      def _token_price(market_id, token_id):
          """查询 token 当前 ask 价 (实时 PnL 用). 返回 float 或 None."""
          try:
              ob = _order_book(market_id, token_id)
              asks = ob.get("asks") or []
              bids = ob.get("bids") or []
              if asks:
                  return float(asks[0]["price"])
              if bids:
                  return float(bids[0]["price"])
          except Exception:
              pass
          return None
      
      
      def run_paper_monitor(amount=1.0, fee=0.0, up_max=0.50, down_max=0.55,
                            paper_file=None, push=True, tb_filter=False, min_conf=0,
                            strength_gate=False, poll=20, p_up=0.74, p_down=0.57,
                            push_every=60, push_delta=5.0, bankroll=100.0,
                            up_min=0.40, skip_trend=True):
          """实时 paper 监控: LIMIT 单模拟, 每根K线精确节奏 (绝不下单, 真实报价).
      
          第0分钟: 结算上一轮 → 推送获利 + 账户权益 (本金 bankroll, 每注 amount)
          第2分钟: 引擎确认方向+入场价 → 设纸面 LIMIT (限价=入场上限, 方向锁定)
          第2分钟后: 轮询 order-book, ask≤限价 → 成交 → 记录持仓 + 实时涨跌幅推送
          第3分钟: 只预测 (无活跃单时推送引擎读数), 不改变 LIMIT 方向
          收盘未触及限价 → 未成交 (放弃)
          """
          import subprocess
          paper_file = paper_file or PAPER_FILE_DEFAULT
          state = load_paper(paper_file)
          if state.get("bankroll", 0) <= 0:
              state["bankroll"] = bankroll
          cur_candle = None
          did_2 = False
          did_3 = False
          last_pnl = {}
          last_live_ts = {}
      
          def _push(msg):
              if not push:
                  return
              try:
                  subprocess.run(["python3", str(ROOT / "scripts" / "telegram_push.py"), msg],
                                 capture_output=True, timeout=20)
              except Exception:
                  pass
      
          def _equity():
              return state.get("bankroll", bankroll) + state.get("realized", 0.0)
      
          def _candle_remain(b):
              try:
                  close_ts = datetime.datetime.fromisoformat(b["candle"]).timestamp() + 300
                  return close_ts - time.time()
              except Exception:
                  return 999.0
      
          def _active_for(candle):
              return any(b.get("candle") == candle
                         and b.get("status") in ("open", "pending")
                         for b in state["bets"])
      
          print(f"📡 模拟预测市场实时 paper 监控 (LIMIT模拟) 🔒 纯模拟, 绝不下单 | "
                f"本金 ${bankroll:.0f} 每注 {amount}U "
                f"| UP限价≤{up_max:.2f} DOWN≤{down_max:.2f} | poll={poll}s", flush=True)
          while True:
              now = datetime.datetime.now()
              minute = now.minute % 5
      
              # ── 第0分钟: 结算上一轮 + 推送获利/账户 ──
              if minute == 0 and now.second < 20:
                  settled = settle_paper(state, fee)
                  if settled:
                      for b in state["bets"]:
                          if b.get("status") == "settled" and not b.get("pushed_final"):
                              pnl_pct = ((1 - b["ask"]) / b["ask"] * 100 - b.get("fee", fee) * 100
                                         if b.get("outcome") == "win"
                                         else -100 - b.get("fee", fee) * 100)
                              mark = "✅ 中" if b.get("outcome") == "win" else "❌ 未中"
                              _push(f"🏁 模拟结算{b['candle'][5:16]} {b['side']} @{b['ask']:.2f} {mark}\n"
                                    f"获利 {pnl_pct:+.1f}% | ${b.get('pnl', 0):+.2f}")
                              b["pushed_final"] = True
                      br = state.get("bankroll", bankroll)
                      _push(f"💼 模拟账户${br:.0f} → ${_equity():.2f} "
                            f"({(_equity() - br) / br * 100:+.2f}%)")
                      save_paper(paper_file, state)
      
              # ── 第2分钟: 确认方向+入场价, 设 LIMIT (方向锁定) ──
              # ── 第3分钟: 只预测, 不改变 LIMIT 方向 ──
              if minute in (2, 3) and now.second >= SAMPLE_SECOND:
                  d = _load_engine()
                  if d:
                      candle = d["candle"]["iso"]
                      if candle != cur_candle:
                          cur_candle = candle
                          did_2 = did_3 = False
                      side, reason = _signal(d, min_conf=min_conf, tb_filter=tb_filter,
                                             strength_gate=strength_gate)
                      def _place(side_, reason_, tag):
                          """最小规则集下单: 只做 UP + 甜区价立即成交 (禁 DOWN/趋势停手)."""
                          if not side_ or _active_for(candle):
                              return False
                          if side_ == "DOWN":
                              return False   # 规则1: 禁 DOWN (实测21%胜率, 全亏 -5.09)
                          if skip_trend and d.get("regime") == "TREND":
                              print(f"跳过: TREND 趋势市停手 (规则3, regime={d.get('regime')})")
                              return False
                          p_est = p_up
                          try:
                              found = find_btc_5m_market()
                          except Exception as e:
                              print(f"发现市场失败: {e}", file=sys.stderr)
                              return False
                          if not found:
                              return False
                          _, market_id, up_tok, down_tok, title, _ = found
                          up_ask = _token_price(market_id, up_tok)
                          down_ask = _token_price(market_id, down_tok)
                          if up_ask is None:
                              print("无法取 UP ask, 跳过")
                              return False
                          if not (up_min <= up_ask <= up_max):
                              print(f"跳过: UP ask {up_ask:.2f} 不在甜区 "
                                    f"[{up_min:.2f},{up_max:.2f}] (规则2)")
                              return False
                          ud = (f"UP {up_ask:.2f} | DOWN {down_ask:.2f}"
                                if down_ask is not None else f"UP {up_ask:.2f} | DOWN ?")
                          state["bets"].append({
                              "candle": candle, "side": "UP",
                              "ask": round(up_ask, 4), "amount": amount,
                              "fee": fee, "status": "open", "mode": "paper",
                              "market_id": market_id, "token_id": up_tok,
                              "p_est": p_est,
                              "ts": datetime.datetime.now().isoformat(),
                              "reason": reason_,
                          })
                          save_paper(paper_file, state)
                          _push(f"✅ 模拟成交 | {candle[5:16]} {tag} UP\n"
                                f"@ {up_ask:.2f} (甜区[{up_min:.2f},{up_max:.2f}]) | p={p_est:.2f}\n"
                                f"{ud}\n"
                                f"持仓 {amount}U | 涨跌幅 0% 起算")
                          return True
      
                      if minute == 2 and not did_2:
                          did_2 = True
                          _place(side, reason, "第2min")
                      elif minute == 3 and not did_3:
                          did_3 = True
                          if not _active_for(candle):
                              # 第2分钟中性 → 第3分钟第二次机会下单
                              if not _place(side, reason, "第3min"):
                                  p = d["prediction"]
                                  cc = d["candle"]
                                  f = d["factors"]
                                  tb = f.get("taker_buy", 0.0)
                                  fng = d.get("fng", {})
                                  fng_s = (f" | FNG {fng.get('value')} {fng.get('label', '')}"
                                           if fng.get("value") is not None else "")
                                  ud = "UP/DOWN: 不可用"
                                  try:
                                      f3 = find_btc_5m_market()
                                      if f3:
                                          _, mid3, u3, dn3, _, _ = f3
                                          ua = _token_price(mid3, u3)
                                          da = _token_price(mid3, dn3)
                                          if ua is not None and da is not None:
                                              ud = f"UP {ua:.2f} | DOWN {da:.2f}"
                                  except Exception:
                                      pass
                                  _push(f"🔎 第3分钟预测(模拟) | {candle[5:16]} "
                                        f"p{cc.get('progress_pct', 0):.0f}%\n"
                                        f"方向: {p['bias']} ({p['strength']}) | 置信 {p['confidence']}\n"
                                        f"现价 {d['price']['current']:,.2f} | "
                                        f"预测收 {p['pred_close']:,} "
                                        f"(低{p['pred_low']:,}/高{p['pred_high']:,})\n"
                                        f"regime {d.get('regime')}{fng_s} | 买力 tb={tb:+.2f}\n"
                                        f"预测市场: {ud}\n"
                                        f"(仍无信号, 不设LIMIT)")
                          # 已有活跃单: LIMIT 方向锁定, 静默
      
              # ── 成交检测 + 盘中实时涨跌幅 ──
              now_ts = time.time()
              has_pending = False
              for b in state["bets"]:
                  if b.get("status") == "pending":
                      # 先判蜡烛是否已收盘 → 未成交自动清理 (不依赖盘口价,
                      # 防挂单存的是已轮换的老 market, 取价失败导致假挂单积压)
                      if _candle_remain(b) <= 0:
                          b["status"] = "unfilled"
                          save_paper(paper_file, state)
                          _push(f"❌ 未成交 | {b['candle'][5:16]} {b['side']}\n"
                                f"限价 {b['limit']:.2f} 收盘未触及 | 放弃")
                          continue
                      has_pending = True
                      tid, mid = b.get("token_id"), b.get("market_id")
                      if not tid or not mid:
                          continue
                      cur = _token_price(mid, tid)
                      if cur is None:
                          continue
                      if cur <= b["limit"]:
                          b["status"] = "open"
                          b["ask"] = round(cur, 4)
                          b["amount"] = amount
                          b["fee"] = fee
                          save_paper(paper_file, state)
                          _push(f"✅ 成交 | {b['candle'][5:16]} {b['side']} @ {cur:.2f} "
                                f"(限价 {b['limit']:.2f})\n"
                                f"持仓 {amount}U | 涨跌幅 0% 起算 | 等结算")
                      continue
                  if b.get("status") != "open":
                      continue
                  tid, mid = b.get("token_id"), b.get("market_id")
                  if not tid or not mid:
                      continue
                  cur = _token_price(mid, tid)
                  if cur is None:
                      continue
                  pnl_pct = (cur - b["ask"]) / b["ask"] * 100
                  last = last_pnl.get(tid)
                  if (last is None or abs(pnl_pct - last) >= push_delta
                          or now_ts - last_live_ts.get(tid, 0) >= push_every):
                      remain = _candle_remain(b)
                      _push(f"📡 模拟{b['side']} @{b['ask']:.2f} → 现价 {cur:.2f}\n"
                            f"涨跌幅 {pnl_pct:+.1f}% | 剩余 "
                            f"{int(max(remain, 0)) // 60}m{int(max(remain, 0)) % 60:02d}s")
                      last_pnl[tid] = pnl_pct
                      last_live_ts[tid] = now_ts
      
              time.sleep(10 if has_pending else poll)
      
      
      def main():
          ap = argparse.ArgumentParser(
              description="5minbtc→币安预测交易 桥接 (默认只报价不成交)")
          ap.add_argument("--once", action="store_true", help="单次: 判断+报价+确认+可选下单")
          ap.add_argument("--loop", action="store_true", help="持续监控(每根K线2/3/4分钟)")
          ap.add_argument("--monitor", action="store_true",
                          help="持仓监控: 实时面板 + 止盈止损自动卖出 (需显式 --tp-mult/--sl-mult)")
          ap.add_argument("--live", action="store_true", help="实盘模式(危险! 双确认)")
          ap.add_argument("--amount", type=float, default=DEFAULT_AMOUNT,
                          help=f"下单金额 USDT (默认 {DEFAULT_AMOUNT})")
          ap.add_argument("--rounds", type=int, default=None, help="loop 轮数上限(默认无限)")
          ap.add_argument("--order-type", choices=("LIMIT", "MARKET"), default="LIMIT",
                          help="买入单类型 (默认 LIMIT)")
          ap.add_argument("--limit-price", type=float, default=None,
                          help="LIMIT 单限价 (token 价格 0~1; 默认 = 该方向入场上限, 即 --limit-up-max/--limit-down-max)")
          ap.add_argument("--limit-up-max", type=float, default=0.65,
                          help="UP 入场上限 (默认 0.65; 实测盈亏平衡 ~0.70, 留 margin)")
          ap.add_argument("--limit-down-max", type=float, default=0.50,
                          help="DOWN 入场上限 (默认 0.50; 实测盈亏平衡 ~0.57)")
          ap.add_argument("--tp-mult", type=float, default=0,
                          help="止盈倍率: 现价/成本 ≥ 此值自动卖出锁利 (如 1.4=+40%%; 0=不启用)")
          ap.add_argument("--sl-mult", type=float, default=0,
                          help="止损倍率: 现价/成本 ≤ 此值自动卖出 (如 0.4=跌60%%; 0=不启用)")
          ap.add_argument("--monitor-interval", type=int, default=10,
                          help="持仓监控轮询间隔秒 (默认 10)")
          ap.add_argument("--monitor-iters", type=int, default=None,
                          help="持仓监控轮数上限 (默认无限)")
          ap.add_argument("--paper", action="store_true",
                          help="预测市场 paper 模拟 (真实报价, 绝不下单)")
          ap.add_argument("--paper-file", default=None,
                          help="paper 状态文件 (默认 ~/bb-auto/5minbtc-paper.json)")
          ap.add_argument("--paper-up-min", type=float, default=0.40,
                          help="UP 甜区下限 (默认 0.40; 实测 <0.40 接飞刀, <0.30 必亏)")
          ap.add_argument("--paper-up-max", type=float, default=0.50,
                          help="UP 甜区上限 (默认 0.50; 实测 >0.50 买贵, 胜率对但亏大)")
          ap.add_argument("--paper-down-max", type=float, default=0.50,
                          help="DOWN 已禁用(实测21%%胜率全亏); 此参数保留兼容")
          ap.add_argument("--paper-trend-ok", action="store_true",
                          help="允许 TREND 趋势市交易 (默认趋势市停手)")
          ap.add_argument("--paper-fee", type=float, default=0.01,
                          help="单笔手续费比例 (默认 0.01=1%%)")
          ap.add_argument("--paper-push", action="store_true",
                          help="paper 报告推送到 Telegram")
          ap.add_argument("--paper-min-conf", type=float, default=0,
                          help="paper 置信度门槛 (默认 0=不过滤; 实测 confidence 校准不可靠)")
          ap.add_argument("--paper-strength-gate", action="store_true",
                          help="paper 开启 strength≥medium 过滤 (未经验证)")
          ap.add_argument("--paper-tb-filter", action="store_true",
                          help="paper 开启 taker_buy 方向一致性过滤 (未经验证)")
          ap.add_argument("--paper-monitor", action="store_true",
                          help="实时 paper 监控: 信号→入场P→实时盈亏%%→结算(推送)")
          ap.add_argument("--paper-p-up", type=float, default=0.74,
                          help="UP 信号预估概率 p (默认 0.74, 今日实测 bull 胜率)")
          ap.add_argument("--paper-p-down", type=float, default=0.57,
                          help="DOWN 信号预估概率 p (默认 0.57, 今日实测 bear 胜率)")
          ap.add_argument("--paper-poll", type=int, default=20,
                          help="实时监控轮询秒数 (默认 20)")
          ap.add_argument("--paper-push-every", type=int, default=60,
                          help="实时盈亏最少推送间隔秒 (默认 60)")
          ap.add_argument("--paper-push-delta", type=float, default=5.0,
                          help="实时盈亏变化≥此百分比才推送, 单位%% (默认 5)")
          ap.add_argument("--paper-bankroll", type=float, default=100.0,
                          help="模拟本金 USDT (默认 100, 每注 --amount)")
          args = ap.parse_args()
      
          if not os.environ.get("BINANCE_API_KEY") or not os.environ.get("BINANCE_API_SECRET"):
              print("需要 BINANCE_API_KEY/BINANCE_API_SECRET 环境变量", file=sys.stderr)
              return 1
          paper_mode = args.paper or args.paper_monitor
          if not WALLET or not WALLET_ID:
              if not paper_mode:
                  print("需要 BINANCE_PREDICT_WALLET / BINANCE_PREDICT_WALLET_ID 环境变量"
                        " (预测钱包地址/ID, 安全起见不从代码读取)", file=sys.stderr)
                  return 1
              print("⚠️ 预测钱包未设置 — paper 模拟模式无需钱包 (只读报价, 绝不下单)")
      
          print("\n⚠️  币安预测交易无 test 模式, 确认下单即真实花钱. 默认只报价不成交。\n")
      
          # 自动卖出闸门: 止盈/止损都必须显式设定, 否则禁止自动 SELL
          auto_sell = (args.tp_mult > 0 and args.sl_mult > 0)
          if args.monitor and not auto_sell:
              print("⚠️  未显式设定 --tp-mult 和 --sl-mult, 自动卖出未启用。")
              print("    将仅显示实时面板, 不执行止盈/止损卖出。")
              print("    启用自动卖出需: --monitor --tp-mult 1.4 --sl-mult 0.4 --live")
              if not args.live:
                  pass  # 非 live 本就不会真实卖出
      
          if args.live:
              global LIVE_GATE
              print("\n⚠️⚠️  实盘模式! 将真实下单。\n")
              try:
                  confirm = input("确认真实下单? 输入 yes: ").strip().lower()
              except EOFError:
                  confirm = ""
              if confirm != "yes":
                  print("已取消")
                  return 0
              LIVE_GATE = True   # 唯一放行真实下单的地方 (双确认后)
      
          if args.paper_monitor:
              return run_paper_monitor(amount=args.amount, fee=args.paper_fee,
                                       up_max=args.paper_up_max, down_max=args.paper_down_max,
                                       paper_file=args.paper_file, push=args.paper_push,
                                       tb_filter=args.paper_tb_filter,
                                       min_conf=args.paper_min_conf,
                                       strength_gate=args.paper_strength_gate,
                                       poll=args.paper_poll, p_up=args.paper_p_up,
                                       p_down=args.paper_p_down,
                                       push_every=args.paper_push_every,
                                       push_delta=args.paper_push_delta,
                                       bankroll=args.paper_bankroll,
                                       up_min=args.paper_up_min,
                                       skip_trend=not args.paper_trend_ok)
          if args.paper:
              if args.loop:
                  return run_paper_loop(amount=args.amount, fee=args.paper_fee,
                                        up_max=args.paper_up_max, down_max=args.paper_down_max,
                                        paper_file=args.paper_file, push=args.paper_push,
                                        tb_filter=args.paper_tb_filter, min_conf=args.paper_min_conf,
                                        strength_gate=args.paper_strength_gate,
                                        rounds=args.rounds)
              return run_paper(amount=args.amount, fee=args.paper_fee,
                               up_max=args.paper_up_max, down_max=args.paper_down_max,
                               paper_file=args.paper_file, push=args.paper_push,
                               tb_filter=args.paper_tb_filter, min_conf=args.paper_min_conf,
                               strength_gate=args.paper_strength_gate)
          if args.monitor:
              return monitor_positions(tp_mult=args.tp_mult, sl_mult=args.sl_mult,
                                       interval=args.monitor_interval, live=args.liv
    • 5minbtc_watch.py 13 KB
      #!/usr/bin/env python3
      """5minbtc 持续监控 → Telegram 推送 (可移植版, 放在本 skill scripts/ 内)
      
      每根 5min BTC K线的第 2/3/4 分钟运行 5minbtc-engine-v6.0.py 采样:
      
      - 记录: 每根K线记录第一次成功采样的预测到 SKILL/logs/5minbtc-log.jsonl
              (通过 5minbtc-log.py log, 供日后验证战绩)
      - 结算: 新K线开始时 settle-all 上一根已收盘K线, 写入实际收盘/方向/区间命中
      - 推送: 事件驱动 (START/DIR-CHANGE/CLEAR-SIGNAL/TB-FLIP) + 每小时心跳
              + 每日 00:00 前后推送前一天战绩汇总 (5minbtc_day_stats.py --push)
      - 可选: --every-candle 每根K线都推完整预测
      
      用法:
        python3 scripts/5minbtc_watch.py                 # 默认: 事件+心跳+每日战绩
        python3 scripts/5minbtc_watch.py --every-candle  # 每次采样都推完整预测
        python3 scripts/5minbtc_watch.py --min-conf 60
      """
      import argparse
      import datetime
      import json
      import math
      import os
      import subprocess
      import sys
      import time
      from pathlib import Path
      
      SCRIPTS = Path(__file__).resolve().parent
      SKILL = SCRIPTS.parent
      ENGINE = SKILL / "5minbtc-engine-v6.0.py"
      SETTLE = SKILL / "5minbtc-log.py"
      PUSH = SCRIPTS / "telegram_push.py"
      DAY_STATS = SCRIPTS / "5minbtc_day_stats.py"
      PY = "/usr/bin/python3"
      CST = datetime.timezone(datetime.timedelta(hours=8))
      
      SAMPLE_MINUTES = (2, 3, 4)      # 每根K线内采样分钟
      SAMPLE_SECOND = 5
      STRENGTHS = {"medium", "moderate", "strong"}
      MIN_CONF = 50
      HEARTBEAT_SEC = 3600
      
      EMOJI = {"bull": "🟢", "neutral": "⚪", "bear": "🔴"}
      DIR_CN = {"bull": "看多", "neutral": "中性", "bear": "看空"}
      
      
      # ── 预测市场 UP/DOWN 实时价 + 模拟持仓 (可选增强, 无密钥优雅降级) ──
      
      def _load_predict_env():
          """从 ~/bb-auto/prediction.env 载入币安预测API密钥 (供取 UP/DOWN 实时价)."""
          envf = Path.home() / "bb-auto" / "prediction.env"
          if not envf.exists():
              return
          try:
              for line in envf.read_text().splitlines():
                  line = line.strip()
                  if not line or line.startswith("#") or "=" not in line:
                      continue
                  k, v = line.split("=", 1)
                  os.environ.setdefault(k.strip(), v.strip())
          except Exception:
              pass
      
      
      def fetch_predict_prices():
          """返回 (up_price, down_price) 或 (None, None).
          优先读 WS 实时缓存 (~/bb-auto/prediction-ws.json, <200ms), 失败则 REST 轮询兜底."""
          try:
              cache = json.loads((Path.home() / "bb-auto" / "prediction-ws.json").read_text())
              cur = cache.get("current") or {}
              if cur.get("up_ask") is not None and cur.get("down_ask") is not None:
                  return float(cur["up_ask"]), float(cur["down_ask"])
          except Exception:
              pass
          try:
              import importlib
              _load_predict_env()
              if str(SCRIPTS) not in sys.path:
                  sys.path.insert(0, str(SCRIPTS))
              trader = importlib.import_module("5minbtc_trader")
              found = trader.find_btc_5m_market()
              if not found:
                  return None, None
              _, market_id, up_tok, down_tok, _, _ = found
              return trader._token_price(market_id, up_tok), trader._token_price(market_id, down_tok)
          except Exception:
              return None, None
      
      
      def paper_positions():
          """读取 paper 台账, 返回当前模拟持仓/挂单行列表 (无则 None)."""
          pf = Path.home() / "bb-auto" / "5minbtc-paper.json"
          if not pf.exists():
              return None
          try:
              state = json.loads(pf.read_text())
          except Exception:
              return None
          open_ = [b for b in state.get("bets", []) if b.get("status") == "open"]
          pending = [b for b in state.get("bets", []) if b.get("status") == "pending"]
          if not open_ and not pending:
              return None
          lines = []
          for b in open_:
              lines.append(f"  ✅ {b.get('side')} @{b.get('ask', 0):.2f} 待结算")
          for b in pending:
              lines.append(f"  📋 {b.get('side')} 限价 {b.get('limit', 0):.2f} 挂单")
          return lines
      
      
      def _sim_prices(d):
          """从引擎输出计算模拟 UP 价 (无真实价源时回退, 同 keyless 模型)."""
          try:
              move_price = d["price"]["current"] - d["price"]["open"]
              atr = d["indicators"].get("atr") or 40.0
              z = (move_price / max(atr, 1e-9)) * 1.7
              p_up = 1.0 / (1.0 + math.exp(-z))
              return max(0.02, min(0.98, p_up))
          except Exception:
              return None
      
      
      def market_line(d):
          """组合 UP/DOWN 价 (真实优先, 无则模拟) + 模拟持仓 文本块."""
          up, down = fetch_predict_prices()
          out = []
          if up is not None:
              out.append(f"预测市场: UP {up:.2f} | DOWN {down:.2f}")
          else:
              p_up = _sim_prices(d)
              if p_up is not None:
                  out.append(f"预测市场: UP {p_up:.2f} | DOWN {1 - p_up:.2f} (模拟)")
              else:
                  out.append("预测市场: (不可用)")
          pos = paper_positions()
          if pos:
              out.append("模拟持仓:")
              out.extend(pos)
          else:
              out.append("模拟持仓: 无")
          return "\n".join(out)
      
      
      MUTE = False  # 全局静默开关 (--mute 时关闭事件推送, 保留记录+结算)
      
      
      def push(msg):
          if MUTE:
              return
          try:
              subprocess.run([PY, str(PUSH), msg], timeout=20, capture_output=True)
          except Exception:
              pass
      
      
      def run_engine():
          try:
              out = subprocess.run([PY, str(ENGINE)], capture_output=True,
                                   text=True, timeout=45)
          except subprocess.TimeoutExpired:
              return None, "TimeoutExpired(45s)"
          except Exception as e:
              return None, f"{type(e).__name__}: {e}"
          try:
              return json.loads(out.stdout), None
          except json.JSONDecodeError as e:
              return None, f"JSONDecode {e}; stderr={out.stderr.strip()[:200]}"
      
      
      def run_settle_all():
          try:
              subprocess.run([PY, str(SETTLE), "settle-all"], capture_output=True,
                             text=True, timeout=90)
          except Exception:
              pass
      
      
      def already_logged(candle_iso):
          logf = SKILL / "logs" / "5minbtc-log.jsonl"
          if not logf.exists():
              return False
          try:
              with open(logf) as f:
                  for line in f:
                      line = line.strip()
                      if not line:
                          continue
                      try:
                          e = json.loads(line)
                      except Exception:
                          continue
                      if e.get("candle") == candle_iso and not e.get("settled"):
                          return True
          except Exception:
              return False
          return False
      
      
      def log_prediction(d, candle_iso):
          p = d["prediction"]
          ind = d["indicators"]
          # v5.9: 补记 score/regime/factors/mtf 快照 (对抗审查 P0 数据地基)
          extra = {
              "score": p.get("score"),
              "strength": p.get("strength"),
              "regime": d.get("regime"),
              "mtf": d.get("mtf", {}),
              "half_body": round(d.get("factors", {}).get("half_body", 0), 3),
              "volume": round(d.get("factors", {}).get("volume", 0), 3),
              "meanrev": round(d.get("factors", {}).get("meanrev", 0), 3),
          }
          try:
              subprocess.run([PY, str(SETTLE), "log", candle_iso,
                              str(p["pred_close"]), str(p["pred_high"]), str(p["pred_low"]),
                              str(p["confidence"]), p["bias"],
                              str(d.get("news_risk", "UNKNOWN")), str(ind.get("vol_pct", 0)),
                              json.dumps(extra)],
                             capture_output=True, text=True, timeout=30)
          except Exception:
              pass
      
      
      def _mtf_line(mtf):
          """多周期/跨资产/OFI 状态行 (v5.9)."""
          if not mtf:
              return ""
          parts = []
          tf4 = mtf.get("tf_4h_slope", 0)
          adx = mtf.get("tf_1h_adx", 0)
          if tf4:
              parts.append(f"4h{'↓' if tf4 < -0.0004 else '↑' if tf4 > 0.0004 else '→'}")
          if adx:
              parts.append(f"ADX{adx:.0f}")
          eth = mtf.get("ca_eth_mom", 0)
          sol = mtf.get("ca_sol_mom", 0)
          if eth or sol:
              parts.append(f"ETH{eth*100:+.1f}% SOL{sol*100:+.1f}%")
          if "ofi" in mtf:
              ofi = mtf["ofi"]
              parts.append(f"OFI{ofi:+.2f}")
          return " | ".join(parts)
      
      
      def fmt_event(tag, d):
          p = d["prediction"]
          c = d["candle"]
          px = d["price"]["current"]
          tb = d["factors"].get("taker_buy")
          tb_s = f" | 买力 tb={tb:+.2f}" if tb is not None else ""
          fng = d.get("fng", {})
          fng_s = f" | FNG {fng.get('value')} {fng.get('label', '')}" if fng.get("value") is not None else ""
          mtf_line = _mtf_line(d.get("mtf", {}))
          head = f"{EMOJI.get(p['bias'], '⚪')} [🌤 全天哨兵] {tag} {c.get('iso')} p{c.get('progress_pct', 0):.0f}%"
          line = (
              f"{head}\n"
              f"方向: {DIR_CN.get(p['bias'], p['bias'])} ({p['strength']}) | 置信 {p['confidence']}\n"
              f"现价 {px:,.2f} | 预测收 {p['pred_close']:,} (低{p['pred_low']:,}/高{p['pred_high']:,})\n"
              f"regime {d.get('regime')}{fng_s}{tb_s}"
          )
          if mtf_line:
              line += f"\n信号源: {mtf_line}"
          if tag == "CLEAR-SIGNAL":
              line += "\n⚠️ 达到明确信号门槛 — 人工复核后再考虑动作, 非投资建议"
          if d.get("black_swan_warning"):
              line += "\n🚨 黑天鹅警告: 方向不可靠"
          line += "\n── 预测市场 ──\n" + market_line(d)
          return line
      
      
      def next_sample(now):
          for i in range(0, 12):
              t = now + datetime.timedelta(minutes=i)
              if t.minute % 5 in SAMPLE_MINUTES:
                  t = t.replace(second=SAMPLE_SECOND, microsecond=0)
                  if t - now >= datetime.timedelta(seconds=6):
                      return t
          return now + datetime.timedelta(minutes=12)
      
      
      def main():
          ap = argparse.ArgumentParser(description="5minbtc 监控→Telegram daemon (含记录/结算)")
          ap.add_argument("--every-candle", action="store_true",
                          help="每次采样都推完整预测 (默认仅事件+心跳)")
          ap.add_argument("--min-conf", type=int, default=MIN_CONF)
          ap.add_argument("--heartbeat", type=int, default=HEARTBEAT_SEC)
          ap.add_argument("--no-daily-stats", action="store_true",
                          help="关闭每日战绩汇总推送")
          ap.add_argument("--mute", action="store_true",
                          help="关闭事件推送(保留预测记录+结算+每日战绩), 推送交给实时重大信号监控")
          args = ap.parse_args()
      
          global MUTE
          MUTE = args.mute
      
          mode = "每根K线推预测" if args.every_candle else \
              f"事件驱动+每{args.heartbeat // 3600}h心跳"
          push(f"🟢 5minbtc 监控已启动 | 每5min采样引擎 | {mode}\n"
               f"已启用预测记录+收盘结算 | 每日战绩推送"
               f"{' (已关闭)' if args.no_daily_stats else ''}")
      
          last_bias = None
          last_tb = None
          last_heart = time.time()
          err_streak = 0
          cur_candle = None
          logged_this_candle = False
          last_day = datetime.datetime.now(CST).strftime("%Y-%m-%d")
      
          while True:
              d, err = run_engine()
              if d is None:
                  err_streak += 1
                  if err_streak in (1, 3, 6, 10):
                      push(f"⚠️ [5minbtc] 引擎采样失败 x{err_streak}: {err}")
                  time.sleep(15)
                  continue
              err_streak = 0
              p = d["prediction"]
              candle_iso = d["candle"]["iso"]
      
              # ── 新K线: 结算上一根 + 重置记录标记 ──
              if candle_iso != cur_candle:
                  if cur_candle is not None:
                      run_settle_all()   # 上一根已收盘 → 写入实际结果
                  cur_candle = candle_iso
                  logged_this_candle = False
      
              # ── 记录预测: 每根K线一次 (首次成功采样) ──
              if not logged_this_candle and not already_logged(candle_iso):
                  log_prediction(d, candle_iso)
                  logged_this_candle = True
      
              # ── 事件推送 ──
              clear = (p["bias"] != "neutral" and p["strength"] in STRENGTHS
                       and p["confidence"] >= args.min_conf)
              if clear:
                  push(fmt_event("CLEAR-SIGNAL", d))
              if p["bias"] != last_bias:
                  tag = "START" if last_bias is None else "DIR-CHANGE"
                  push(fmt_event(tag, d))
                  last_bias = p["bias"]
              tb = d["factors"].get("taker_buy")
              if tb is not None and last_tb is not None and (tb >= 0) != (last_tb >= 0):
                  push(fmt_event("TB-FLIP", d))
              if tb is not None:
                  last_tb = tb
              if args.every_candle:
                  push(fmt_event("PREDICT", d))
      
              # ── 每小时心跳 ──
              if time.time() - last_heart >= args.heartbeat:
                  push(fmt_event("心跳", d))
                  last_heart = time.time()
      
              # ── 每日战绩汇总 (日期切换时推前一天) ──
              today = datetime.datetime.now(CST).strftime("%Y-%m-%d")
              if not args.no_daily_stats and today != last_day:
                  try:
                      subprocess.run([PY, str(DAY_STATS), "--date", last_day, "--push"],
                                     capture_output=True, timeout=60)
                  except Exception:
                      pass
                  last_day = today
      
              t = next_sample(datetime.datetime.now())
              time.sleep(max(10, (t - datetime.datetime.now()).total_seconds()))
      
      
      if __name__ == "__main__":
          main()
      
    • daily-review-stats.py 7.7 KB
      #!/usr/bin/env python3
      """5minbtc daily review stats analyzer.
      
      Reads 5minbtc-log.jsonl, filters to a single trading day, and computes the
      per-dimension breakdown that the daily-review task needs (per-bias accuracy,
      per-vol-band accuracy, MAE, max error, win streak, error-sign / systematic-bias
      check). This is the structured analysis that `5minbtc-log.py stats` does NOT
      provide (it only gives aggregate direction/range/MAE/max).
      
      Built-in data-quality handling:
        * dedup by candle start (cron double-fire / manual+auto overlap -> keep last)
        * filter vol_pct > 200  (lesson 15: 0/near-0-denominator explosive glitch,
          NOT real volume)
        * error-sign analysis   (lesson 19: all-same-sign => engine fighting trend)
      
      IMPORTANT UNIT CONVENTION
      -------------------------
      `error_pct` in the JSONL is ALREADY in percent units. -0.32 means -0.32%,
      NOT -0.0032. Do NOT multiply by 100 a second time when computing MAE.
      
      Field names are read defensively via alias lookup so the script survives small
      changes to the log schema. Run on first use to confirm keys match.
      
      Usage:
          python3 scripts/daily-review-stats.py            # today
          python3 scripts/daily-review-stats.py 2026-06-23 # specific date
      """
      import json
      import sys
      import os
      from collections import defaultdict
      
      # 2026-09-10 修: 原路径少了一层 logs/ (指向 skill 根), 该脚本从未真正读到过日志
      _SKILL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
      LOG = os.path.join(_SKILL, "logs", "5minbtc-log.jsonl")
      ARCHIVE = os.path.join(_SKILL, "logs", "archive")
      
      VOL_GLITCH_THRESHOLD = 200.0  # lesson 15
      SYSTEMATIC_BIAS_MIN_RUNS = 5  # lesson 19
      
      
      def _get(r, *keys, default=None):
          """Return the first present, non-None key among aliases."""
          for k in keys:
              if k in r and r[k] is not None:
                  return r[k]
          return default
      
      
      def _is_settled(r):
          """A record is settled if it has an actual close price or an explicit flag."""
          if _get(r, "settled"):
              return True
          if _get(r, "actual_close", "actual", "actual_price", "close") is not None:
              return True
          return False
      
      
      def _iter_lines():
          """已归档月份 (logs/archive/*.jsonl.gz) + live。"""
          import glob, gzip
          for gz in sorted(glob.glob(os.path.join(ARCHIVE, "*.jsonl.gz"))):
              try:
                  with gzip.open(gz, "rt", encoding="utf-8") as f:
                      for line in f:
                          yield line
              except Exception:
                  continue
          if os.path.exists(LOG):
              with open(LOG) as f:
                  for line in f:
                      yield line
      
      
      def load_day(log_path, day):
          recs = []
          for line in _iter_lines():
              line = line.strip()
              if not line:
                  continue
              try:
                  r = json.loads(line)
              except Exception:
                  continue
              hay = " ".join(str(r.get(k, "")) for k in ("ts", "candle", "date"))
              if day in hay and _is_settled(r):
                  recs.append(r)
          return recs
      
      
      def dedup(records):
          """Same candle start twice => keep the last record."""
          by_candle = {}
          for r in records:
              key = _get(r, "candle", "candle_start", "iso", default=r.get("ts"))
              by_candle[key] = r
          return sorted(by_candle.values(),
                        key=lambda x: _get(x, "candle", "ts", default=""))
      
      
      def vol_band(v):
          if v < 40:
              return "low(<40%)"
          if v < 80:
              return "mid(40-80%)"
          return "high(>80%)"
      
      
      def pct(ok, n):
          return 100.0 * ok / n if n else 0.0
      
      
      def main():
          day = sys.argv[1] if len(sys.argv) > 1 else None
          if day is None:
              import datetime
              day = datetime.date.today().isoformat()
      
          if not os.path.exists(LOG):
              print("Log not found: %s" % LOG)
              return
      
          records = load_day(LOG, day)
          if not records:
              print("No settled records for %s" % day)
              print("If a full trading day was expected (30-40 rounds), this is a")
              print("cron coverage anomaly -> check provider balance / fallback chain")
              print("See references/cron-llm-provider-failure.md")
              return
      
          records = dedup(records)
          valid = [r for r in records
                   if (_get(r, "vol_pct", "vol", default=0) or 0) <= VOL_GLITCH_THRESHOLD]
          glitch = [r for r in records
                    if (_get(r, "vol_pct", "vol", default=0) or 0) > VOL_GLITCH_THRESHOLD]
      
          print("=== %s ===" % day)
          print("deduped=%d  valid=%d  vol_glitch_filtered=%d"
                % (len(records), len(valid), len(glitch)))
          for r in glitch:
              print("  VOL_GLITCH: %s vol_pct=%s"
                    % (_get(r, "candle", "ts", default="?"),
                       _get(r, "vol_pct", "vol")))
          print()
      
          n = len(valid)
          if n == 0:
              print("No valid records after filtering.")
              return
      
          def dir_ok(r):
              return bool(_get(r, "direction_correct", "dir_correct", default=False))
      
          def rng_ok(r):
              return bool(_get(r, "in_range", "range_correct", default=False))
      
          dir_n = sum(1 for r in valid if dir_ok(r))
          rng_n = sum(1 for r in valid if rng_ok(r))
          errs = [abs(_get(r, "error_pct", "err_pct", default=0.0)) for r in valid]
          raw = [_get(r, "error_pct", "err_pct", default=0.0) for r in valid]
          mae = sum(errs) / n
          max_pos = max(raw)
          max_neg = min(raw)
      
          print("--- HEADLINE ---")
          print("Direction: %d/%d = %.1f%%" % (dir_n, n, pct(dir_n, n)))
          print("Range:     %d/%d = %.1f%%" % (rng_n, n, pct(rng_n, n)))
          print("MAE: %.3f%%   (error_pct is already in percent units)" % mae)
          print("Max err: +%.3f%% / %.3f%%" % (max_pos, max_neg))
          print()
      
          # bias breakdown
          print("--- BY BIAS ---")
          bg = defaultdict(list)
          for r in valid:
              bg[_get(r, "bias", default="?")].append(r)
          for b in ("bull", "bear", "neutral"):
              g = bg.get(b, [])
              if g:
                  c = sum(1 for r in g if dir_ok(r))
                  print("  %-7s %d/%d = %.1f%%" % (b, c, len(g), pct(c, len(g))))
          print()
      
          # vol breakdown
          print("--- BY VOLUME ---")
          vg = defaultdict(list)
          for r in valid:
              vg[vol_band(_get(r, "vol_pct", "vol", default=0) or 0)].append(r)
          for v in ("low(<40%)", "mid(40-80%)", "high(>80%)"):
              g = vg.get(v, [])
              if g:
                  c = sum(1 for r in g if dir_ok(r))
                  print("  %-13s %d/%d = %.1f%%" % (v, c, len(g), pct(c, len(g))))
          print()
      
          # streak
          best = cur = 0
          for r in valid:
              if dir_ok(r):
                  cur += 1
                  best = max(best, cur)
              else:
                  cur = 0
          print("Max win streak: %d" % best)
      
          # error-sign / systematic-bias (lesson 19)
          pos = sum(1 for e in raw if e > 0)
          neg = sum(1 for e in raw if e < 0)
          flag = ("SYSTEMATIC_BIAS (engine fighting trend -> lesson 19)"
                  if (pos >= SYSTEMATIC_BIAS_MIN_RUNS or neg >= SYSTEMATIC_BIAS_MIN_RUNS)
                  else "ok")
          print("Error signs: +%d / -%d  =>  %s" % (pos, neg, flag))
      
          # coverage warning
          if n < 10:
              print()
              print("WARN: only %d valid rounds. A healthy trading day has 30-40." % n)
              print("      Likely silent cron/provider failure -> check provider balance.")
          print()
      
          print("--- PER-ROUND (chronological) ---")
          for r in valid:
              dc = "OK" if dir_ok(r) else "X"
              rc = "OK" if rng_ok(r) else "X"
              ctime = str(_get(r, "candle", "ts", default=""))[11:16]
              print("%s | %-7s conf=%3s | pred=%s act=%s err=%+.3f%% | dir%s rng%s | vol=%s%%"
                    % (ctime, _get(r, "bias", default="?"), _get(r, "confidence", default="?"),
                       _get(r, "pred_close", "pred", default="?"),
                       _get(r, "actual_close", "actual", default="?"),
                       _get(r, "error_pct", "err_pct", default=0.0),
                       dc, rc, _get(r, "vol_pct", "vol", default="?")))
      
      
      if __name__ == "__main__":
          main()
      
    • fetch-github-repo.sh 2.3 KB
      #!/usr/bin/env bash
      # fetch-github-repo.sh — 受限网络下下载GitHub仓库的通用脚本
      #
      # 用法: ./fetch-github-repo.sh <owner/repo> [output_dir] [branch]
      # 示例: ./fetch-github-repo.sh ZhuLinsen/daily_stock_analysis ./repos/daily_stock_analysis main
      #
      # 背景:
      #   在中国大陆网络下,git clone / gh repo clone / codeload.zip 经常因TLS握手超时失败
      #   (典型错误: GnuTLS recv error (-110), Operation timed out, unexpected EOF)
      #   本脚本绕过git协议,走 api.github.com tarball端点,断点续传+多重重试
      #
      # 注意:
      #   - 不需要GitHub auth (匿名tarball支持)
      #   - 不需要git客户端
      #   - 适合单仓库下载,不需要commit历史
      #   - 默认下载 tarball,但若仅需几个文件见fetch-github-files.sh
      
      set -e
      
      REPO="${1:?Usage: $0 owner/repo [output_dir] [branch]}"
      OUTDIR="${2:-$(basename "$REPO")}"
      BRANCH="${3:-main}"
      
      # 规范化 output dir 为绝对路径
      mkdir -p "$OUTDIR"
      OUTDIR="$(cd "$OUTDIR" && pwd)"
      
      TARBALL="/tmp/github-$(echo "$REPO" | tr '/' '-').tgz"
      WORKDIR="$(mktemp -d)"
      
      echo "==> 下载 $REPO @ $BRANCH → $TARBALL"
      
      # wget -c 断点续传,--tries=30 长重试链,120s单次超时适合慢网
      wget --tries=30 --timeout=120 --read-timeout=120 -c \
        -O "$TARBALL" \
        "https://api.github.com/repos/$REPO/tarball/$BRANCH" 2>&1 | tail -8
      
      # 完整性校验
      SIZE=$(stat -c%s "$TARBALL")
      echo "==> 下载完成: $SIZE bytes"
      echo "==> 验证tarball完整性..."
      
      if ! tar -tzf "$TARBALL" >/dev/null 2>&1; then
        echo "⚠️  tarball不完整(EOF截断),但可能部分文件仍可解压" >&2
      fi
      
      # 列出文件数
      FILE_COUNT=$(tar -tzf "$TARBALL" 2>/dev/null | wc -l)
      echo "==> tarball内文件数: $FILE_COUNT"
      
      # 解压
      cd "$WORKDIR"
      tar -xzf "$TARBALL"
      
      # GitHub tarball 解压到 "{owner}-{repo}-{sha}/" 目录
      SRC_DIR=$(find . -maxdepth 1 -type d -not -name '.' | head -1)
      if [ -z "$SRC_DIR" ]; then
        echo "❌ 解压后未找到源码目录" >&2
        exit 1
      fi
      
      # 移动到目标位置
      if [ -d "$OUTDIR" ] && [ "$(ls -A "$OUTDIR" 2>/dev/null)" ]; then
        echo "==> 目标目录已存在非空,合并内容到 $OUTDIR"
        cp -r "$SRC_DIR"/. "$OUTDIR"/
      else
        rm -rf "$OUTDIR"
        mv "$SRC_DIR" "$OUTDIR"
      fi
      
      # 清理
      rm -rf "$WORKDIR"
      
      echo ""
      echo "✅ 完成: $REPO → $OUTDIR"
      echo "   解压后文件数: $(find "$OUTDIR" -type f | wc -l)"
      echo "   占用空间: $(du -sh "$OUTDIR" | cut -f1)"
    • ofi_feed.py 8.1 KB
      #!/usr/bin/env python3
      """真订单流 OFI 采集 v2 — 原生 aggressor (futures @trade 流 m 标记).
      
      v2 相对 v1 的关键修复 (对抗审查 F1-F8):
      - 原生 aggressor: futures @trade 每条 msg 自带 m (buyer is maker). m=False → 主动买(taker buy),
        m=True → 主动卖(taker sell). 100% 精确. 弃用 v1 的 tick 规则推断
        (实测仅 62.6% / 量加权 66.3%, 37% 成交判错边). @trade 流在本网络确认推送且带 m.
      - 蜡烛对齐窗口: 从 trade 的 T (ms) 推导 UTC 5min 边界, 边界处重置 in-candle 累计量,
        不再用进程启动对齐的固定 300s 滚动桶 (修时间错配 F5).
      - ofi_60: 最近 60s 净流环形缓冲, 作领先/新鲜度指标 (供引擎做新鲜度反转保护).
      - 质量闸: classification_ratio + last_trade_ts 保鲜 + in-candle 最低量门槛.
      - WS 稳定性: ping_interval 降到 15s + 指数退避重连 (1s/3s/10s) (修 1011 keepalive timeout).
      - bookTicker 保留订阅 (网络零改动) 但不再参与 aggressor 判定, 仅作兜底与留档.
      
      用法: python3 ofi_feed.py  (launchd 托管: com.daniel.ofi-feed)
      """
      import asyncio
      import json
      import sys
      import time
      from collections import deque
      from datetime import datetime, timezone
      from pathlib import Path
      
      import websockets
      
      WS = "wss://fstream.binance.com/ws/btcusdt@trade/btcusdt@bookTicker"
      CACHE = Path.home() / "bb-auto" / "ofi.json"
      CANDLE_MS = 300_000       # 5分钟蜡烛
      OFI60_MS = 60_000         # 最近60s净流窗口
      TRADE_KEEP_MS = 120_000   # 环形缓冲保留时长 (算 ofi_60 需要)
      MIN_WINDOW_SEC = 45       # in-candle 决策至少 45s 窗口
      MIN_VOL_BTC = 20.0        # in-candle 最低总成交量 (BTC), 防窗口重置后单笔噪声
      BACKOFF = [1, 3, 10]      # 重连退避 (秒)
      
      
      class OfiFeed:
          def __init__(self):
              self.candle_buy = 0.0
              self.candle_sell = 0.0
              self.candle_amb = 0.0          # 无法判定的量 (m 缺失时兜底, 正常为 0)
              self.candle_start_ms = 0       # 当前 5m 蜡烛起点 (UTC ms)
              self.candle_iso = ""
              self.trades = deque()          # (ts_ms, qty, is_buy), 最近 ~120s
              self.last_trade_ms = 0
              self.last_write = 0.0
              self.agg_native = True         # m 字段可用 → 真 OFI
              self.best_bid = 0.0            # 仅兜底 classify 用
              self.best_ask = 0.0
              # 对齐到当前 5m 边界, 保证首写就有正确的 window_sec
              now_ms = int(time.time() * 1000)
              self.candle_start_ms = (now_ms // CANDLE_MS) * CANDLE_MS
              self.candle_iso = self._iso(self.candle_start_ms)
      
          @staticmethod
          def _iso(ms):
              return datetime.fromtimestamp(ms / 1000, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
      
          # ---- 蜡烛对齐 (v2: 从 trade 的 T 推导 5min 边界) ----
          def _sync_candle(self, ts_ms):
              boundary = (ts_ms // CANDLE_MS) * CANDLE_MS
              if boundary != self.candle_start_ms:
                  self.candle_start_ms = boundary
                  self.candle_buy = self.candle_sell = self.candle_amb = 0.0
                  self.candle_iso = self._iso(boundary)
      
          # ---- 最近60s 净流 (领先/新鲜度指标) ----
          def _ofi_60(self, now_ms):
              while self.trades and now_ms - self.trades[0][0] > TRADE_KEEP_MS:
                  self.trades.popleft()
              buy = sum(q for _, q, is_buy in self.trades if is_buy)
              sell = sum(q for _, q, is_buy in self.trades if not is_buy)
              total = buy + sell
              if total <= 0:
                  return 0.0, 0.0
              return (buy - sell) / total, total
      
          def write(self):
              now_ms = int(time.time() * 1000)
              ofi_60, vol_60 = self._ofi_60(now_ms)
              total_c = self.candle_buy + self.candle_sell
              window_sec = round((now_ms - self.candle_start_ms) / 1000) if self.candle_start_ms else 0
              low_vol = total_c < MIN_VOL_BTC
              ofi_candle = (self.candle_buy - self.candle_sell) / total_c if total_c > 0 else 0.0
              if low_vol:
                  ofi_candle = 0.0  # 流量不足 → 信号置中性, 引擎不会把它当冲突
              fresh = (now_ms - self.last_trade_ms) < 10_000
              cr = total_c / (total_c + self.candle_amb) if (total_c + self.candle_amb) > 0 else 1.0
              data = {
                  "ofi": round(ofi_candle, 4),             # 向后兼容旧字段
                  "ofi_candle": round(ofi_candle, 4),      # 蜡烛对齐净流 (主字段)
                  "ofi_60": round(ofi_60, 4),              # 最近60s净流 (领先指标)
                  "buy_vol": round(self.candle_buy, 2),
                  "sell_vol": round(self.candle_sell, 2),
                  "net": round(self.candle_buy - self.candle_sell, 2),
                  "window_sec": window_sec,
                  "candle_iso": self.candle_iso,
                  "classification_ratio": round(cr, 4),
                  "agg_native": self.agg_native,
                  "feed_fresh": fresh,
                  "low_vol": low_vol,
                  "last_trade_ts": self.last_trade_ms,
                  "ts": now_ms,
              }
              try:
                  CACHE.write_text(json.dumps(data))
              except Exception as e:
                  print(f"写缓存失败: {e}", file=sys.stderr)
      
          def classify(self, price):
              """兜底 tick 规则 (仅当 msg 无 m 字段时). 正常路径用原生 m, 不走这里."""
              if self.best_ask > 0 and price >= self.best_ask:
                  return "buy"
              if self.best_bid > 0 and price <= self.best_bid:
                  return "sell"
              if self.best_bid > 0 and self.best_ask > 0:
                  mid = (self.best_bid + self.best_ask) / 2
                  return "buy" if price >= mid else "sell"
              return None
      
          def _on_trade(self, msg):
              ts = int(msg.get("T", 0) or 0)
              qty = float(msg.get("q", 0) or 0)
              if qty <= 0:
                  return
              if ts > 0:
                  self._sync_candle(ts)  # 先对齐蜡烛 (边界处重置 in-candle 累计)
              m = msg.get("m")
              if m is False:
                  # 原生: m=False → 主动买 (taker buy)
                  self.candle_buy += qty
                  is_buy = True
              elif m is True:
                  # 原生: m=True → 主动卖 (taker sell)
                  self.candle_sell += qty
                  is_buy = False
              else:
                  # m 缺失 (理论不发生) → 兜底 tick 规则
                  side = self.classify(float(msg.get("p", 0) or 0))
                  if side == "buy":
                      self.candle_buy += qty
                      is_buy = True
                  elif side == "sell":
                      self.candle_sell += qty
                      is_buy = False
                  else:
                      self.candle_amb += qty
                      self.agg_native = False
                      self.last_trade_ms = ts
                      return
              self.trades.append((ts, qty, is_buy))
              self.last_trade_ms = ts
      
          async def run(self):
              print(f"真OFI 采集启动: {WS} → {CACHE} (原生 m aggressor)", flush=True)
              attempt = 0
              while True:
                  try:
                      async with websockets.connect(WS, ping_interval=15, ping_timeout=20) as ws:
                          print("真OFI WS 已连接 (trade+bookTicker, 原生 m)", flush=True)
                          attempt = 0
                          while True:
                              try:
                                  raw = await asyncio.wait_for(ws.recv(), timeout=60)
                              except asyncio.TimeoutError:
                                  continue
                              msg = json.loads(raw)
                              e = msg.get("e", "")
                              if e == "bookTicker":
                                  # 仅兜底用, 不参与原生 m 判定
                                  self.best_bid = float(msg.get("b", 0) or 0)
                                  self.best_ask = float(msg.get("a", 0) or 0)
                              elif e == "trade":
                                  self._on_trade(msg)
                              # 节流写缓存 (1s)
                              if time.time() - self.last_write >= 1.0:
                                  self.write()
                                  self.last_write = time.time()
                  except Exception as e:
                      delay = BACKOFF[min(attempt, len(BACKOFF) - 1)]
                      print(f"真OFI WS 断开: {e}, {delay}s 重连", file=sys.stderr)
                      attempt += 1
                      await asyncio.sleep(delay)
      
      
      if __name__ == "__main__":
          asyncio.run(OfiFeed().run())
      
    • prediction_ws_feed.py 7.2 KB
      #!/usr/bin/env python3
      """币安 w3w-prediction WebSocket 实时价格源 → 本地 JSON 缓存.
      
      订阅 orderbook 流 (聚合所有市场 topic=web3_prediction_orderbook_data, <200ms 实时),
      维护每个 marketId 的最新盘口, 并周期性 (每根 5min K线) 用 REST 快照刷新当前
      BTC Up/Down 市场的 UP/DOWN token 权威价。消费方 (5minbtc_watch / paper-monitor)
      直接读 ~/bb-auto/prediction-ws.json 即可, 无需各自连 WS 或轮询 REST。
      
      密钥: 从 ~/bb-auto/prediction.env 或 launchctl getenv 加载 (未就绪则等待)。
      用法:
        python3 prediction_ws_feed.py            # 前台
        (launchd 托管: com.daniel.prediction-ws-feed)
      """
      import asyncio
      import hashlib
      import hmac
      import json
      import os
      import random
      import subprocess
      import sys
      import time
      import urllib.parse
      from pathlib import Path
      
      ENV_FILE = Path.home() / "bb-auto" / "prediction.env"
      CACHE = Path.home() / "bb-auto" / "prediction-ws.json"
      BASE = "wss://api.binance.com/sapi/wss"
      TOPIC = "web3_prediction_orderbook_data"   # 聚合所有市场
      REST_REFRESH_SEC = 300                      # 每根 5min K线刷新一次 UP/DOWN 权威价
      WS_PING = 30                                # 心跳秒
      
      
      def _load_env():
          if ENV_FILE.exists():
              try:
                  for line in ENV_FILE.read_text().splitlines():
                      line = line.strip()
                      if not line or line.startswith("#") or "=" not in line:
                          continue
                      k, v = line.split("=", 1)
                      os.environ.setdefault(k.strip(), v.strip())
              except Exception:
                  pass
          for v in ("BINANCE_API_KEY", "BINANCE_API_SECRET"):
              if not os.environ.get(v):
                  val = _launchctl_getenv(v)
                  if val:
                      os.environ[v] = val
      
      
      def _launchctl_getenv(k):
          try:
              out = subprocess.run(["launchctl", "getenv", k], capture_output=True,
                                   text=True, timeout=5).stdout.strip()
              return out or None
          except Exception:
              return None
      
      
      def keys_ready():
          return bool(os.environ.get("BINANCE_API_KEY")
                      and os.environ.get("BINANCE_API_SECRET"))
      
      
      def sign_url(topic):
          params = {"random": str(random.random()), "topic": topic,
                    "recvWindow": "30000", "timestamp": str(int(time.time() * 1000))}
          qs = urllib.parse.urlencode(sorted(params.items()))
          sig = hmac.new(os.environ["BINANCE_API_SECRET"].encode(), qs.encode(),
                         hashlib.sha256).hexdigest()
          return f"{BASE}?{qs}&signature={sig}"
      
      
      def rest_current():
          """REST 快照当前 BTC 5m 预测市场的 UP/DOWN token 价 (权威)."""
          try:
              sys.path.insert(0, str(Path.home() / ".claude/skills/5minbtc/scripts"))
              import importlib
              trader = importlib.import_module("5minbtc_trader")
              found = trader.find_btc_5m_market()
              if not found:
                  return None
              _, market_id, up_tok, down_tok, _, _ = found
              return {"market_id": market_id, "up_token": up_tok, "down_token": down_tok,
                      "up_ask": trader._token_price(market_id, up_tok),
                      "down_ask": trader._token_price(market_id, down_tok),
                      "ts": int(time.time() * 1000)}
          except Exception as e:
              print(f"REST 快照失败: {e}", file=sys.stderr)
              return None
      
      
      class Feed:
          def __init__(self):
              self.cache = {"updated_ms": 0, "markets": {}, "current": None}
              self.last_write = 0.0
      
          def write(self):
              now = time.time()
              if now - self.last_write < 0.2:      # ~200ms 节流
                  return
              self.last_write = now
              self.cache["updated_ms"] = int(time.time() * 1000)
              try:
                  CACHE.write_text(json.dumps(self.cache, ensure_ascii=False))
              except Exception as e:
                  print(f"写缓存失败: {e}", file=sys.stderr)
      
          async def run(self):
              import websockets
              while True:
                  try:
                      url = sign_url(TOPIC)
                      print(f"WS 连接: {url.split('?')[0]} (topic={TOPIC})", flush=True)
                      async with websockets.connect(
                              url, additional_headers={"X-MBX-APIKEY": os.environ["BINANCE_API_KEY"]},
                              ping_interval=WS_PING, ping_timeout=WS_PING * 2) as ws:
                          # 连接后立即 REST 刷新当前市场 UP/DOWN 权威价 (不等 300s)
                          cur = rest_current()
                          if cur:
                              self.cache["current"] = cur
                              self.write()
                              print(f"REST 初始刷新: market={cur['market_id']} "
                                    f"UP={cur['up_ask']} DOWN={cur['down_ask']}", flush=True)
                          last_rest = time.time()
                          while True:
                              # 收 WS 消息 (orderbook 流 — 可能不推送, 但不阻塞刷新)
                              try:
                                  raw = await asyncio.wait_for(ws.recv(), timeout=30)
                                  try:
                                      env = json.loads(raw)
                                  except Exception:
                                      env = None
                                  if env and env.get("type") == "TOPIC":
                                      try:
                                          data = json.loads(env.get("data") or "{}")
                                      except Exception:
                                          data = {}
                                      if data.get("msgType") == "orderbook":
                                          mid = data.get("marketId")
                                          asks = data.get("asks") or []
                                          bids = data.get("bids") or []
                                          self.cache["markets"][str(mid)] = {
                                              "best_ask": asks[0][0] if asks else None,
                                              "best_bid": bids[0][0] if bids else None,
                                              "asks": asks, "bids": bids,
                                              "ts": data.get("updateTimestampMs"),
                                          }
                                          self.write()
                              except asyncio.TimeoutError:
                                  pass
      
                              # 周期性 REST 刷新 UP/DOWN 权威价 (不依赖 WS 消息)
                              if time.time() - last_rest >= REST_REFRESH_SEC:
                                  cur = rest_current()
                                  if cur:
                                      self.cache["current"] = cur
                                      last_rest = time.time()
                                      self.write()
                                      print(f"REST 刷新: market={cur['market_id']} "
                                            f"UP={cur['up_ask']} DOWN={cur['down_ask']}", flush=True)
                  except Exception as e:
                      print(f"WS 断开/异常: {e}, 3s 后重连", file=sys.stderr)
                      await asyncio.sleep(3)
      
      
      def main():
          _load_env()
          while not keys_ready():
              print(f"[{time.strftime('%F %T')}] ⏳ 等待币安预测API密钥 "
                    f"(写入 {ENV_FILE})...", flush=True)
              time.sleep(30)
              _load_env()
          print(f"✅ 密钥就绪, 启动 WS 实时价格源 → {CACHE}", flush=True)
          asyncio.run(Feed().run())
      
      
      if __name__ == "__main__":
          main()
      
    • telegram_push.py 1.7 KB
      #!/usr/bin/env python3
      """Push a message to the user's Telegram via the cc-connect bot.
      
      Reads bot token + chat id from ~/.cc-connect/config.toml (no duplicated secrets).
      Usage:
          python3 telegram_push.py "message text"
          echo "message" | python3 telegram_push.py
      """
      import json
      import os
      import re
      import sys
      import time
      import urllib.parse
      import urllib.request
      
      CONFIG = os.path.expanduser("~/.cc-connect/config.toml")
      
      
      def load_telegram_creds():
          with open(CONFIG) as f:
              text = f.read()
          m = re.search(r'^\s*token\s*=\s*"(\d+:[A-Za-z0-9_-]+)"\s*$', text, re.M)
          token = m.group(1) if m else None
          chat = None
          for key in ("allow_from", "admin_from"):
              m = re.search(rf'^\s*{key}\s*=\s*"?(\d+)"?\s*$', text, re.M)
              if m:
                  chat = m.group(1).split(",")[0].strip()
                  break
          return token, chat
      
      
      def send(token, chat_id, text):
          url = f"https://api.telegram.org/bot{token}/sendMessage"
          data = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode()
          for attempt in range(3):
              try:
                  req = urllib.request.Request(url, data=data)
                  with urllib.request.urlopen(req, timeout=15) as resp:
                      return resp.status
              except Exception as e:
                  if attempt == 2:
                      return f"ERR {e}"
                  time.sleep(2)
          return "ERR"
      
      
      def main():
          msg = sys.argv[1] if len(sys.argv) > 1 else sys.stdin.read()
          if not msg.strip():
              return
          token, chat = load_telegram_creds()
          if not token or not chat:
              sys.stderr.write("no token/chat in config\n")
              sys.exit(1)
          print(send(token, chat, msg), flush=True)
      
      
      if __name__ == "__main__":
          main()
      
    • test_engine.py 9.6 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """引擎契约测试 — 契约、因子数值、多时刻采样、稳定性
      
      ⚠️ 2026-09-10 前名为 `test_engine_v58.py`:版本号写进文件名 → 引擎升到 v6.0 后即漂移。
      现改名 `test_engine.py`(不带版本号),内容已对齐 v6.0。
      
      引擎零第三方依赖,**测试也不应强制装 pytest**:
      - 有 pytest:`python3 -m pytest scripts/test_engine.py -v`
      - 没有 pytest:`python3 scripts/test_engine.py`(自带极简 runner,慢测默认跳过)
      """
      import importlib.util
      import json
      import subprocess
      import sys
      from pathlib import Path
      
      try:
          import pytest
      except ImportError:                      # 无 pytest → 退化为直通装饰器
          pytest = None
          def mark_slow(fn):
              fn._slow = True
              return fn
      else:
          def mark_slow(fn):
              fn._slow = True
              return pytest.mark.slow(fn)
      
      HERE = Path(__file__).resolve().parent
      SKILL = HERE.parent  # scripts/ 的上一级 = skill 根
      ENGINE = SKILL / "5minbtc-engine-v6.0.py"
      MONITOR = HERE / "5minbtc-monitor.py"
      
      spec = importlib.util.spec_from_file_location("eng", ENGINE)
      eng = importlib.util.module_from_spec(spec)
      spec.loader.exec_module(eng)
      
      
      # ---------------- 契约 ----------------
      
      def run_engine_subprocess(timeout=60):
          """真实跑一次引擎(不缓存)—— 稳定性测试用。"""
          out = subprocess.run(["python3", str(ENGINE)], capture_output=True,
                               text=True, timeout=timeout)
          assert out.returncode == 0, f"engine exit={out.returncode} stderr={out.stderr}"
          return json.loads(out.stdout)
      
      
      _SNAPSHOT = {}
      
      
      def engine_snapshot():
          """契约测试共用的引擎输出(缓存一次,避免每个测试都跑一遍 9 路 HTTP)。"""
          if "d" not in _SNAPSHOT:
              _SNAPSHOT["d"] = run_engine_subprocess()
          return _SNAPSHOT["d"]
      
      
      def test_engine_version_is_600():
          assert eng.__file__ or True  # module loads
          d = engine_snapshot()
          assert d["version"] == "6.0.0"
      
      
      def test_prediction_contract():
          d = engine_snapshot()
          pred = d["prediction"]
          for k in ("bias", "strength", "confidence", "score",
                    "pred_close", "pred_high", "pred_low"):
              assert k in pred, f"prediction missing {k}"
          # v6.0: 方向二选一无中性 (neutral 分支不可达)
          assert pred["bias"] in ("bull", "bear"), f"bias 必须二选一, 实际 {pred['bias']}"
          assert pred["strength"] in ("weak", "medium", "moderate", "strong")
          assert 0 <= pred["confidence"] <= 100
      
      
      def test_top_level_contract():
          d = engine_snapshot()
          for k in ("version", "candle", "price", "recent_candles", "indicators",
                    "fng", "factors", "regime", "chainlink_offset", "atr_spike",
                    "fng_black_swan", "news_risk", "prediction"):
              assert k in d, f"top-level missing {k}"
          p = d["price"]
          assert p["low"] <= p["current"] <= p["high"]
      
      
      def test_ofi_block_contract():
          """v6.0 新增: 顶层 ofi 块是方向与概率的来源, 必须存在且 direction 与 bias 一致。"""
          d = engine_snapshot()
          assert "ofi" in d, "顶层缺 ofi 块"
          o = d["ofi"]
          for k in ("direction", "ofi_n"):
              assert k in o, f"ofi 块缺 {k}"
          assert o["direction"] in ("bull", "bear")
          assert d["prediction"]["bias"] == o["direction"], "bias 必须等于 ofi.direction"
      
      
      def test_taker_buy_in_factors():
          d = engine_snapshot()
          assert "taker_buy" in d["factors"]
          assert -1.0 <= d["factors"]["taker_buy"] <= 1.0
      
      
      def test_taker_buy_weight_is_zeroed():
          """v5.9 起 BASE_W['taker_buy'] 被**有意清零**(该因子从未被公平回测验证)。
      
          这里断言 == 0 是为了**防止它被误重新启用** —— 若要恢复,请先补公平回测,
          并同时改这条断言(别只把 0 改成非 0 就上)。
          """
          assert "taker_buy" in eng.BASE_W
          assert eng.BASE_W["taker_buy"] == 0.0, "taker_buy 权重应保持清零 (v5.9 决定)"
          for regime, adj in eng.REGIME_ADJ.items():
              assert "taker_buy" in adj, f"REGIME_ADJ[{regime}] missing taker_buy"
      
      
      # ---------------- 因子数值 ----------------
      
      def test_taker_buy_signal_math():
          # 全主动买: tb=v → ratio=1 → 2*(1-0.5)*3=3 → clamp 1.0
          candles = [{"o": 100, "h": 101, "l": 99, "c": 101, "v": 10, "tb": 10, "ct": 0}] * 6
          assert eng.taker_buy_signal(candles) == 1.0
          # 全主动卖: tb=0 → ratio=0 → 2*(0-0.5)*3=-3 → clamp -1.0 (v>0 有效)
          candles0 = [{"o": 100, "h": 101, "l": 99, "c": 99, "v": 10, "tb": 0, "ct": 0}] * 6
          assert eng.taker_buy_signal(candles0) == -1.0
          # 均衡 0.5 → 0
          candles_mid = [{"o": 100, "h": 101, "l": 99, "c": 100, "v": 10, "tb": 5, "ct": 0}] * 6
          assert eng.taker_buy_signal(candles_mid) == 0.0
          # 用已完成 K线 (排除当前未完成)
          done = [{"o": 100, "h": 101, "l": 99, "c": 100, "v": 10, "tb": 10, "ct": 0}] * 5
          cur = [{"o": 100, "h": 101, "l": 99, "c": 100, "v": 10, "tb": 0, "ct": 0}]
          assert eng.taker_buy_signal(done + cur) == 1.0  # 当前 tb=0 被排除
          # 数据容错: tb>v 时钳制到 v → ratio=1
          c_over = [{"o": 100, "h": 101, "l": 99, "c": 101, "v": 10, "tb": 99, "ct": 0}] * 6
          assert eng.taker_buy_signal(c_over) == 1.0
      
      
      def test_taker_buy_signal_edges():
          # 空列表
          assert eng.taker_buy_signal([]) == 0.0
          # v=0 全跳过
          c = [{"o": 100, "h": 101, "l": 99, "c": 100, "v": 0, "tb": 0, "ct": 0}] * 6
          assert eng.taker_buy_signal(c) == 0.0
          # 无 tb 字段向后兼容 (fetch_klines 守卫不足时)
          c2 = [{"o": 100, "h": 101, "l": 99, "c": 100, "v": 10}] * 6
          assert eng.taker_buy_signal(c2) == 0.0
      
      
      # ---------------- 订单簿多时刻采样 ----------------
      
      def test_fetch_depth_avg_merges_and_truncates(monkeypatch):
          calls = {"n": 0}
          snaps = [
              {"bids": [(100.0, 1.0), (99.5, 2.0)], "asks": [(100.5, 3.0)]},
              {"bids": [(100.0, 3.0), (99.5, 1.0)], "asks": [(100.5, 1.0)]},
              None,  # 失败采样应被跳过
          ]
          def fake_depth(*a, **k):
              s = snaps[calls["n"] % len(snaps)]
              calls["n"] += 1
              return s
          monkeypatch.setattr(eng, "fetch_depth", fake_depth)
          monkeypatch.setattr(eng, "time", _FakeTime())
          res = eng.fetch_depth_avg(limit=20, samples=3, interval_s=0.0)
          assert res is not None
          # 同价位取平均: (1+3)/2=2 (第3次None被跳过 → got=2)
          assert res["bids"][0] == (100.0, 2.0)
          assert res["bids"][1] == (99.5, 1.5)
          assert res["asks"][0] == (100.5, 2.0)
      
      
      def test_fetch_depth_avg_all_fail(monkeypatch):
          monkeypatch.setattr(eng, "fetch_depth", lambda *a, **k: None)
          monkeypatch.setattr(eng, "time", _FakeTime())
          assert eng.fetch_depth_avg(samples=3, interval_s=0.0) is None
      
      
      def test_fetch_depth_avg_limit_truncation(monkeypatch):
          # 3 次采样价位漂移 → 合并后层数 > limit, 必须截断到 limit
          def fake_depth(*a, **k):
              return {"bids": [(float(i), 1.0) for i in range(100, 0, -1)],
                      "asks": [(float(i), 1.0) for i in range(101, 201)]}
          monkeypatch.setattr(eng, "fetch_depth", fake_depth)
          monkeypatch.setattr(eng, "time", _FakeTime())
          res = eng.fetch_depth_avg(limit=20, samples=3, interval_s=0.0)
          assert len(res["bids"]) == 20
          assert len(res["asks"]) == 20
      
      
      class _FakeTime:
          def sleep(self, *_a, **_k):
              return None
      
      
      # ---------------- 稳定性 (可跳过) ----------------
      
      @mark_slow
      def test_multiple_runs_stable():
          biases = []
          for _ in range(3):
              d = run_engine_subprocess()
              biases.append(d["prediction"]["bias"])
              assert "taker_buy" in d["factors"]
          # 只断言不崩, 不做 bias 一致性断言 (market 实时变化)
      
      
      def test_monitor_dry_run_compatible():
          out = subprocess.run(["python3", str(MONITOR), "--dry-run"],
                               capture_output=True, text=True, timeout=60)
          assert out.returncode == 0, f"monitor dry-run failed: {out.stderr}"
          assert "DRY" in out.stdout
          assert "[20" in out.stdout or "progress" in out.stdout or "conf=" in out.stdout
      
      
      def _run_without_pytest(argv):
          """极简 runner —— 引擎零依赖, 不该为了跑测试强装 pytest。
      
          提供 monkeypatch 的最小替身(只实现 setattr/undo),慢测默认跳过(加 --slow 才跑)。
          """
          class _MonkeyPatch:
              def __init__(self):
                  self._undo = []
      
              def setattr(self, obj, name, value):
                  self._undo.append((obj, name, getattr(obj, name)))
                  setattr(obj, name, value)
      
              def undo(self):
                  for obj, name, old in reversed(self._undo):
                      setattr(obj, name, old)
                  self._undo.clear()
      
          want_slow = "--slow" in argv
          fns = [v for k, v in sorted(globals().items())
                 if k.startswith("test_") and callable(v)]
          passed, failed, skipped = [], [], []
      
          for fn in fns:
              if getattr(fn, "_slow", False) and not want_slow:
                  skipped.append(fn.__name__)
                  continue
              mp = _MonkeyPatch()
              try:
                  if "monkeypatch" in fn.__code__.co_varnames[:fn.__code__.co_argcount]:
                      fn(mp)
                  else:
                      fn()
                  passed.append(fn.__name__)
                  print(f"  ✅ {fn.__name__}")
              except Exception as e:
                  failed.append((fn.__name__, e))
                  print(f"  ❌ {fn.__name__}: {type(e).__name__}: {e}")
              finally:
                  mp.undo()
      
          print(f"\n通过 {len(passed)} | 失败 {len(failed)} | 跳过 {len(skipped)}"
                + (f"(慢测: {' '.join(skipped)},加 --slow 可跑)" if skipped else ""))
          return 1 if failed else 0
      
      
      if __name__ == "__main__":
          if pytest is not None:
              sys.exit(pytest.main([__file__, "-v"]))
          print("未装 pytest → 使用内置 runner\n")
          sys.exit(_run_without_pytest(sys.argv[1:]))
      
  • .env.example 3.6 KB · in bundle
  • .gitignore 626 B · in bundle
  • 5minbtc-engine-v6.0.py 49.9 KB
    #!/usr/bin/env python3
    """5minbtc Engine v6.0 -- 真 OFI 驱动 (替代方向延续统计) + 正交因子体系 + Regime感知
    
    v6.0 核心改造 (对抗审查: 方向预测已证硬币 52.6%, 彻底转真订单流驱动):
    - 方向: bias 由真 OFI 净流方向一票决定 (净流入→bull / 净流出→bear, **二选一无中性**;
      ofi_n 缺失或为 0 时用 body 符号兜底, 并标 meta.body_fallback).
      弱信号/流量不足/反向冲突只记 meta 供概率层降权 (p 趋近 0.5 → EV 过滤不买), 不把方向变中性.
      替代 v5.10 的 body>0→bull/body<0→bear 纯延续统计.
      主源 = 当前K线原生 in-candle ofi_n = 2*(tb/v)-1 (REST kline[9] taker buy base volume,
      原生 aggressor 聚合, 天然按K线对齐, 零 WS 依赖); 辅源 = WS ofi.json (ofi_candle/ofi_60)
      做新鲜度反转保护与交叉校准.
    - 概率: confidence = P(close>open | ofi), 三层 = 经验校准表(Bayesian shrink) + flow-gap
      (净流已发生价格未定价) + 最近60s流, 替代 close_direction_confidence 的延续概率.
    - edge: 错价检测 EV = p − ask (真 OFI 概率 vs UP/DOWN token 市场价), 由 realtime 执行.
    - 保留: ATR spike / FNG<25 / news 黑天鹅断路器, MTF 4h 降权(只降权不翻方向), EV 下单框架.
    
    v5.8 优化 -- taker buy量能因子(区分主动买/主动卖) + 订单簿多时刻采样去噪
    
    v5.7 优化 -- 半K线预测策略 (Daniel提议 2026-05-28):
    - S1: 半K线body动量因子(half_body_momentum) -- K线过半后,已形成的body方向在剩余时间内延续
          核心逻辑: 在progress>=45%时激活,body方向用ATR归一化到[-1,1]
          这是实盘edge的正式建模:回测证明 11因子无预测力(47-49%),但实盘 66%的edge来自
          progress=0.9+ 时"确认已有走势">现在把这个逻辑变成显式因子
    - S2: 预测范围收窄 -- ATR乘数 *0.55(只预测剩余~55%时间),half_range 0.65>0.40
    - S3: Cron调度从每根K线第 2分钟改为第 4分钟(progress~60%>80%)
          给前半K线充分形成信号,再预测后半段
    
    v5.6 优化 (基于v5.5 3轮方向错误复盘):
    - R1: 趋势衰竭检测 -- momentum/decel方向冲突时动态降权
    - R2: volume因子修复 -- vol_breakout_signal用已完成K线
    - R3: V型反转因子 -- 低点抬高 + 收>开反转模式
    - R4: Chainlink价格对齐 -- Coinbase BTC-USD参考
    
    v5.5 优化 (基于 116轮v5.4实战复盘):
    - P0-1: 置信度Platt Scaling校准 -- sigmoid(score)替代线性 40+abs(score)
    - P0-2: 新闻因子移除 -- 98%NEUTRAL死代码
    - P1-1: Bull bias惩罚 -- bear 69.1% > bull 63.5%, bull*0.92
    - P1-2: 高vol惩罚增强 -- HIGH_VOL score衰减 0.45
    - P1-3: neutral区收缩 [-1,1]
    
    v5.0 基础(基于R14审查 14项修复):
    - C-1: 正交因子替代共线指标(momentum t-stat, Z-score, vol ratio)
    - C-2: 所有阈值ATR归一化
    - C-3: 条件化volume信号(区分突破vs衰竭)
    - C-4: 订单簿深度信号(imbalance + microprice)
    - H-1: Sigmoid压缩替代ad-hoc压制
    - H-3: 百分比化信号(不再用绝对价格差)
    - H-4: 波动率Regime检测(4态)
    - M-1~M-6: 数学修正(BB std, 200K线, vol投影, RSI动量, Wilder ATR, O(n) MACD)
    """
    import json, math, os, time, urllib.request
    import ssl
    ssl._create_default_https_context = ssl._create_unverified_context
    from concurrent.futures import ThreadPoolExecutor, as_completed
    from datetime import datetime, timezone, timedelta
    
    CST = timezone(timedelta(hours=8))
    BINANCE_KLINES = "https://data-api.binance.vision/api/v3/klines"
    BINANCE_DEPTH = "https://data-api.binance.vision/api/v3/depth"
    
    # ======================== Data Fetching ========================
    
    def fetch_klines(symbol="BTCUSDT", interval="5m", limit=200):
        url = f"{BINANCE_KLINES}?symbol={symbol}&interval={interval}&limit={limit}"
        req = urllib.request.Request(url, headers={"User-Agent": "5minbtc/5.0"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            raw = json.loads(resp.read())
        return [{"o": float(c[1]), "h": float(c[2]), "l": float(c[3]), "c": float(c[4]),
                 "v": float(c[5]), "ct": int(c[6]),
                 "tb": float(c[9]) if len(c) > 9 else 0.0} for c in raw]
    
    def fetch_depth(symbol="BTCUSDT", limit=20):
        try:
            url = f"{BINANCE_DEPTH}?symbol={symbol}&limit={limit}"
            req = urllib.request.Request(url, headers={"User-Agent": "5minbtc/5.0"})
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read())
            bids = [(float(b[0]), float(b[1])) for b in data["bids"][:limit]]
            asks = [(float(a[0]), float(a[1])) for a in data["asks"][:limit]]
            return {"bids": bids, "asks": asks}
        except Exception:
            return None
    
    def fetch_depth_avg(symbol="BTCUSDT", limit=20, samples=3, interval_s=1.0):
        """v5.8 -- 订单簿多时刻采样去噪
        循环 samples 次调用 fetch_depth, 每次间隔 interval_s,
        同价位 bids/asks 取平均合并, 降低单次快照的瞬时报单噪声.
        全部失败返回 None, 单次失败跳过.
        """
        acc = {"bids": {}, "asks": {}}
        got = 0
        for i in range(samples):
            snap = fetch_depth(symbol, limit)
            if snap is not None:
                got += 1
                for side in ("bids", "asks"):
                    for price, qty in snap[side]:
                        acc[side][price] = acc[side].get(price, 0.0) + qty
            if i < samples - 1:
                time.sleep(interval_s)
        if got == 0:
            return None
        bids = sorted(((p, q / got) for p, q in acc["bids"].items()), reverse=True)[:limit]
        asks = sorted(((p, q / got) for p, q in acc["asks"].items()))[:limit]
        return {"bids": bids, "asks": asks}
    
    # ======================== O(n) Indicators ========================
    
    def ema(data, period):
        """Single-point EMA"""
        if len(data) < period:
            return sum(data) / len(data) if data else 0
        k = 2 / (period + 1)
        e = sum(data[:period]) / period
        for v in data[period:]:
            e = v * k + e * (1 - k)
        return e
    
    def ema_series(data, period):
        """Full EMA series O(n) -- 所有EMA基于同一初始化"""
        n = len(data)
        if n < period:
            avg = sum(data) / n if n > 0 else 0
            return [avg] * n
        k = 2 / (period + 1)
        result = [0.0] * n
        init = sum(data[:period]) / period
        for i in range(period):
            result[i] = init
        result[period - 1] = init
        for i in range(period, n):
            result[i] = data[i] * k + result[i - 1] * (1 - k)
        return result
    
    def rsi(data, period=14):
        """Wilder's RSI"""
        if len(data) < period + 1:
            return 50
        gains, losses = [], []
        for i in range(1, len(data)):
            d = data[i] - data[i - 1]
            gains.append(max(d, 0))
            losses.append(max(-d, 0))
        ag = sum(gains[:period]) / period
        al = sum(losses[:period]) / period
        for i in range(period, len(gains)):
            ag = (ag * (period - 1) + gains[i]) / period
            al = (al * (period - 1) + losses[i]) / period
        if al == 0:
            return 100
        return 100 - 100 / (1 + ag / al)
    
    def compute_macd(data, fast=12, slow=26, sig=9):
        """O(n) MACD -- 统一EMA初始化"""
        ema_f = ema_series(data, fast)
        ema_s = ema_series(data, slow)
        macd_vals = [ema_f[i] - ema_s[i] for i in range(len(data))]
        valid = macd_vals[slow - 1:]
        if len(valid) < sig:
            return macd_vals[-1], 0, macd_vals[-1]
        k = 2 / (sig + 1)
        sv = sum(valid[:sig]) / sig
        for v in valid[sig:]:
            sv = v * k + sv * (1 - k)
        return macd_vals[-1], sv, macd_vals[-1] - sv
    
    def bollinger(data, period=20, std_mult=2):
        """BB with sample std (/n-1)"""
        if len(data) < period:
            return data[-1], data[-1], data[-1]
        subset = data[-period:]
        sma = sum(subset) / period
        var = sum((x - sma) ** 2 for x in subset) / (period - 1)
        std = math.sqrt(var)
        return sma + std_mult * std, sma, sma - std_mult * std
    
    def atr_wilder(candles, period=14):
        """Wilder's ATR (RMA-based)"""
        if len(candles) < 2:
            return 0
        trs = []
        for i in range(1, len(candles)):
            c = candles[i]
            prev_c = candles[i - 1]["c"]
            tr = max(c["h"] - c["l"], abs(c["h"] - prev_c), abs(c["l"] - prev_c))
            trs.append(tr)
        if len(trs) < period:
            return sum(trs) / len(trs) if trs else 0
        rma = sum(trs[:period]) / period
        for tr in trs[period:]:
            rma = (rma * (period - 1) + tr) / period
        return rma
    
    # ======================== 6 Orthogonal Factors ========================
    
    def momentum_tstat(closes, window=15):
        """因子 1: 动量t-stat -- 线性回归斜率统计显著性
        v5.1修复: window 30>15防止饱和; 自适应归一化
        正交于均值回归;scale-invariant(log price)
        返回 [-1, 1]
        """
        if len(closes) < window:
            return 0
        prices = closes[-window:]
        n = len(prices)
        log_p = [math.log(p) for p in prices]
        x_mean = (n - 1) / 2
        x_sq = sum((i - x_mean) ** 2 for i in range(n))
        if x_sq == 0:
            return 0
        y_mean = sum(log_p) / n
        xy = sum((i - x_mean) * (log_p[i] - y_mean) for i in range(n))
        slope = xy / x_sq
        intercept = y_mean - slope * x_mean
        resid = [log_p[i] - (intercept + slope * i) for i in range(n)]
        se = math.sqrt(sum(r ** 2 for r in resid) / max(1, n - 2) / max(1, x_sq))
        if se == 0:
            return 0
        t = slope / se
        # v5.1: softer saturation with tanh instead of hard cap
        return max(-1, min(1, math.tanh(t / 2.5)))
    
    def zscore_meanrev(closes, period=20):
        """因子 2: 均值回归 -- 高Z=超买=看空, 低Z=超卖=看多
        * 关键: 方向反转! 高Z > 返回负值(bearish)
        正交于动量: 提供counter-trend信号
        返回 [-1, 1]
        """
        if len(closes) < period:
            return 0
        subset = closes[-period:]
        sma = sum(subset) / period
        std = math.sqrt(sum((x - sma) ** 2 for x in subset) / (period - 1))
        if std == 0:
            return 0
        z = (closes[-1] - sma) / std
        # * 反转: 高Z(超买)>负值(bearish), 低Z(超卖)>正值(bullish)
        return max(-1, min(1, -z / 2.5))
    
    def vol_regime_ratio(closes, short=20, long=60):
        """因子 3: 波动率比率 > regime检测
        返回 raw ratio
        """
        if len(closes) < long + 1:
            return 1.0
        def rvol(prices):
            if len(prices) < 2:
                return 0
            lr = [math.log(prices[i] / prices[i - 1]) for i in range(1, len(prices))]
            m = sum(lr) / len(lr)
            return math.sqrt(sum((r - m) ** 2 for r in lr) / len(lr))
        sv = rvol(closes[-short:])
        lv = rvol(closes[-long:])
        return sv / lv if lv > 0 else 1.0
    
    def rsi_momentum(rsi_val):
        """因子 4: RSI动量模式 -- 5min频率下动量>反转
        Connors & Alvarez (2012): RSI>70继续看涨
        返回 [-1, 1]
        """
        return (rsi_val - 50) / 50
    
    def volume_conditional(vol_now, vol_avg, progress, candles):
        """因子 5: 条件化volume信号
        投影未完成K线; 区分突破放量vs衰竭放量
        返回 [-1, 1]
        """
        if vol_avg <= 0 or progress <= 0:
            return 0
        projected = vol_now / progress if progress < 1.0 else vol_now
        vr = projected / vol_avg
    
        if vr < 0.5:
            return 0.1
        elif vr < 0.8:
            return 0.15
        elif vr < 1.3:
            return 0
        elif vr < 2.0:
            if len(candles) >= 2:
                cur_r = candles[-1]["h"] - candles[-1]["l"]
                prev_r = candles[-2]["h"] - candles[-2]["l"]
                d = 1 if candles[-1]["c"] > candles[-1]["o"] else -1
                return 0.3 * d if cur_r > prev_r * 1.2 else -0.2 * d
            return 0
        else:
            if candles:
                d = 1 if candles[-1]["c"] > candles[-1]["o"] else -1
                return -0.25 * d
            return 0
    
    def consecutive_fatigue(candles):
        """因子 6: 连续K线疲劳
        返回 [-1, 1] (正=看bullish reversal)
        """
        bull = bear = 0
        for c in reversed(candles[:-1]):
            if c["c"] > c["o"]:
                if bear > 0: break
                bull += 1
            else:
                if bull > 0: break
                bear += 1
        if bull >= 5: return -0.8
        if bull >= 3: return -0.5
        if bear >= 5: return 0.8
        if bear >= 3: return 0.5
        return 0
    
    def momentum_deceleration(closes):
        """因子 7: 动量减速 -- 短期vs中期动量差
        捕获趋势内拐点: 短期动量<长期动量 > 减速 > 可能回调
        返回 [-1, 1]
        """
        if len(closes) < 20:
            return 0
        short_mom = momentum_tstat(closes[-8:], 5) if len(closes) >= 8 else 0
        long_mom = momentum_tstat(closes[-20:], 15) if len(closes) >= 20 else 0
        decel = short_mom - long_mom
        return max(-1, min(1, decel * 2.5))
    
    def price_position(candles):
        """因子 8: 价格在近期区间的位置 (类Stochastic)
        接近高点 > 可能回调; 接近低点 > 可能反弹
        返回 [-1, 1] (正=接近高点=看空; 负=接近低点=看多)
        """
        if len(candles) < 20:
            return 0
        highs = [c["h"] for c in candles[-20:]]
        lows = [c["l"] for c in candles[-20:]]
        hh = max(highs)
        ll = min(lows)
        if hh == ll:
            return 0
        pos = (candles[-1]["c"] - ll) / (hh - ll)
        # 反转: 高位=看空, 低位=看多
        return -(pos * 2 - 1)  # [-1, 1], 正=bullish(低位), 负=bearish(高位)
    
    def orderbook_signals(depth_data, mid_price):
        """因子 7+8: 订单簿 imbalance + microprice"""
        if not depth_data or not depth_data.get("bids") or not depth_data.get("asks"):
            return 0, 0
        bids, asks = depth_data["bids"], depth_data["asks"]
        bq = sum(b[1] for b in bids[:5])
        aq = sum(a[1] for a in asks[:5])
        total = bq + aq
        imb = (bq - aq) / total if total > 0 else 0
        bb, ba = bids[0][0], asks[0][0]
        bbq, baq = bids[0][1], asks[0][1]
        spread = ba - bb
        if spread > 0 and (bbq + baq) > 0:
            mp = bb + spread * bbq / (bbq + baq)
            mid = (bb + ba) / 2
            dev = (mp - mid) / spread * 2
        else:
            dev = 0
        return max(-1, min(1, imb)), max(-1, min(1, dev))
    
    def v_reversal_detect(candles):
        """因子 10: V型反转检测 -- 捕获急跌后快速反转
        检测逻辑: 最近 3根K线低点逐渐抬高 + 当前K线收>开
        返回 [-1, 1] (正=看bullish reversal)
        v5.6新增: 解决Case#1 momentum=-0.98但实盘bull的根因
        """
        if len(candles) < 4:
            return 0
        # 取最近 3根完成的K线 (不含当前未完成的)
        recent = candles[-4:-1]  # -4, -3, -2 (3根完成的)
        cur = candles[-1]        # 当前未完成的
    
        # 条件 1: 最近 3根低点逐渐抬高 (V底)
        lows = [c["l"] for c in recent]
        ascending_lows = all(lows[i] <= lows[i+1] for i in range(len(lows)-1))
    
        # 条件 2: 当前K线是阳线 (收>开)
        cur_bull = cur["c"] > cur["o"]
    
        # 条件 3: 最近 3根中有至少 2根是阴线 (先下跌)
        bear_count = sum(1 for c in recent if c["c"] < c["o"])
    
        if ascending_lows and cur_bull and bear_count >= 2:
            # 强V反转
            return 1.0
        elif ascending_lows and cur_bull:
            # 弱V反转
            return 0.6
        # 对称检测: 高点降低 + 阴线 = 倒V (bearish reversal)
        highs = [c["h"] for c in recent]
        descending_highs = all(highs[i] >= highs[i+1] for i in range(len(highs)-1))
        cur_bear = cur["c"] < cur["o"]
        bull_count = sum(1 for c in recent if c["c"] > c["o"])
        if descending_highs and cur_bear and bull_count >= 2:
            return -1.0
        elif descending_highs and cur_bear:
            return -0.6
        return 0
    
    def vol_breakout_signal(candles):
        """因子 11: 突破放量信号 -- 用已完成K线判断方向
        v5.6新增: 解决volume因子用未完成K线的bug
        逻辑: 最近 3根完成K线中,量最大的一根的方向决定信号
        """
        if len(candles) < 4:
            return 0
        recent = candles[-4:-1]  # 3根完成的K线
        vols = [(c["v"], 1 if c["c"] > c["o"] else -1) for c in recent]
        # 找最大量的一根
        max_vol_bar = max(vols, key=lambda x: x[0])
        vol_max, direction = max_vol_bar
        avg_vol = sum(v for v, _ in vols) / len(vols)
        if avg_vol == 0:
            return 0
        ratio = vol_max / avg_vol
        if ratio > 1.5:
            # 突破放量: 方向跟随最大量K线
            return direction * min(1.0, (ratio - 1.0) * 0.5)
        return 0
    
    def taker_buy_signal(candles):
        """v5.8 -- 因子 13: taker buy 量能(区分主动买/主动卖)
        用最近 5根已完成K线(candles[-6:-1], 不含当前未完成K线),
        每根 ratio = tb/v (主动买入占比, 0.5=均衡), 取均值.
        映射到 [-1,1]: 2*(mean_ratio - 0.5)*3, clamp.
        正值=主动买占优=看多. v=0 或 tb 字段缺失的K线跳过, 全部跳过返回 0.
        tb=0 是有意义信号(主动买量为零=全主动卖), 不跳过.
        """
        ratios = []
        for c in candles[-6:-1]:
            v = c.get("v", 0)
            if "tb" not in c or v <= 0:
                continue
            tb = c["tb"]
            if tb > v:
                tb = v  # 数据容错: 主动买量不可能超过总成交量
            ratios.append(tb / v)
        if not ratios:
            return 0
        mean_ratio = sum(ratios) / len(ratios)
        return max(-1.0, min(1.0, 2 * (mean_ratio - 0.5) * 3))
    
    def half_body_momentum(candles, progress, atr_val):
        """因子 12: 半K线body动量 -- v5.7核心新增
        逻辑: K线进行到 45%+ 后,已形成的body方向有延续倾向
        这是"确认已有走势"的显式建模 -- 回测证明这才是真正的edge来源
        
        返回 [-1, 1]
        - 正值=阳线body>bullish延续
        - 负值=阴线body>bearish延续
        - progress<45%时返回 0(信号不足)
        """
        if progress < 0.45 or atr_val <= 0 or not candles:
            return 0
        cur = candles[-1]
        body = cur["c"] - cur["o"]
        # ATR归一化: body/atr > 典型body约 0.3-0.8 ATR
        normalized = body / atr_val
        # 进度加权: 越接近完成,信号越强
        # progress 0.45>权重 0.4, 0.8>权重 0.85, 1.0>权重 1.0
        weight = min(1.0, (progress - 0.3) / 0.7)
        # 非线性压缩: 避免极端body主导
        signal = math.tanh(normalized * 2.0) * weight
        return max(-1.0, min(1.0, signal))
    
    def atr_spike_detect(candles, atr_val, lookback=5, spike_threshold=1.5, consecutive=3):
        """v5.7.1 -- P0-1: ATR异常检测(黑天鹅防护)
        连续N根K线实际range > ATR * threshold > 判定为spike环境
        返回: (is_spike: bool, spike_ratio: float, consecutive_count: int)
        """
        if atr_val <= 0 or len(candles) < lookback:
            return False, 0.0, 0
        count = 0
        max_ratio = 0.0
        for c in candles[-lookback:]:
            actual_range = c["h"] - c["l"]
            ratio = actual_range / atr_val if atr_val > 0 else 0
            if ratio > spike_threshold:
                count += 1
                max_ratio = max(max_ratio, ratio)
            else:
                count = 0  # 重置连续计数
        is_spike = count >= consecutive
        return is_spike, round(max_ratio, 2), count
    
    def fetch_chainlink_ref():
        """R4: 抓取Coinbase BTC-USD作为Chainlink参考价
        Chainlink Data Streams聚合 3+CEX中位数价格
        Coinbase是其中权重最大的成分交易所之一
        返回 (price, source) 或 (None, None)
        """
        try:
            url = "https://api.coinbase.com/v2/prices/BTC-USD/spot"
            req = urllib.request.Request(url, headers={"User-Agent": "5minbtc/5.6"})
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read())
            return float(data["data"]["amount"]), "coinbase"
        except Exception:
            return None, None
    
    # ======================== Multi-Timeframe Structure (v5.9) ========================
    
    def _linreg_slope(values, n=10):
        """线性回归斜率, 按均值归一化 (返回 %/根). 纯 Python 无 numpy."""
        y = values[-n:]
        m = len(y)
        if m < 2:
            return 0.0
        x_mean = (m - 1) / 2
        y_mean = sum(y) / m
        num = sum((i - x_mean) * (y[i] - y_mean) for i in range(m))
        den = sum((i - x_mean) ** 2 for i in range(m))
        slope = num / den if den > 0 else 0.0
        return slope / y_mean if y_mean > 0 else 0.0
    
    
    def wilder_adx(candles, period=14):
        """Wilder's ADX — 趋势强度(0-100). 纯 Python.
        <20 震荡 / 20-25 过渡 / >25 明确趋势."""
        if len(candles) < period + 1:
            return 0.0
        trs, pdm, ndm = [], [], []
        for i in range(1, len(candles)):
            h, l, pc = candles[i]["h"], candles[i]["l"], candles[i - 1]["c"]
            ph, pl = candles[i - 1]["h"], candles[i - 1]["l"]
            trs.append(max(h - l, abs(h - pc), abs(l - pc)))
            up, dn = h - ph, pl - l
            pdm.append(up if (up > dn and up > 0) else 0.0)
            ndm.append(dn if (dn > up and dn > 0) else 0.0)
    
        def _wilder(vals, p):
            s = sum(vals[:p])
            out = [s]
            for i in range(p, len(vals)):
                s = s - s / p + vals[i]
                out.append(s)
            return out
    
        tr_s = _wilder(trs, period)
        pdm_s = _wilder(pdm, period)
        ndm_s = _wilder(ndm, period)
        dxs = []
        for i in range(len(tr_s)):
            pdi = 100 * pdm_s[i] / tr_s[i] if tr_s[i] > 0 else 0.0
            ndi = 100 * ndm_s[i] / tr_s[i] if tr_s[i] > 0 else 0.0
            s = pdi + ndi
            dxs.append(100 * abs(pdi - ndi) / s if s > 0 else 0.0)
        if len(dxs) < period:
            return sum(dxs) / len(dxs) if dxs else 0.0
        s = sum(dxs[:period])
        for i in range(period, len(dxs)):
            s = s - s / period + dxs[i]
        return s / period
    
    
    def multi_timeframe_signal(k_4h, k_1h, k_15m):
        """多周期结构: 4h大方向斜率 + 1h趋势强度(ADX) + 15m区间位置(%B).
        参数为完整 candle 字典列表 (含 h/l/c). 返回 dict."""
        mtf = {}
        if k_4h and len(k_4h) >= 8:
            mtf["tf_4h_slope"] = round(_linreg_slope([c["c"] for c in k_4h], 10), 6)
        if k_1h and len(k_1h) >= 20:
            mtf["tf_1h_adx"] = round(wilder_adx(k_1h, 14), 1)
            mtf["tf_1h_slope"] = round(_linreg_slope([c["c"] for c in k_1h], 20), 6)
        if k_15m and len(k_15m) >= 20:
            closes = [c["c"] for c in k_15m]
            sma = sum(closes) / 20
            var = sum((x - sma) ** 2 for x in closes) / 19
            std = var ** 0.5
            up, lo = sma + 2 * std, sma - 2 * std
            if up > lo:
                mtf["tf_15m_pctb"] = round((closes[-1] - lo) / (up - lo), 3)
        return mtf
    
    
    def cross_asset_signal(k_eth, k_sol):
        """跨资产广度 (v5.9): ETH/SOL 5m 动量方向 = BTC 趋势的确认.
        对抗审查 P0: 趋势日(跨资产同向)强制压 bear 是最差 failure 的根因修复."""
        ca = {}
        for name, k in (("eth", k_eth), ("sol", k_sol)):
            if k and len(k) >= 6:
                closes = [c["c"] for c in k]
                if closes[-6] > 0:
                    ca[f"ca_{name}_mom"] = round((closes[-1] - closes[-6]) / closes[-6], 6)
        return ca
    
    
    def load_ofi():
        """读真订单流 OFI 缓存 (ofi_feed.py v2 用原生 m 聚合的净主动买卖流).
        v6.0: 加 ts 保鲜校验 — 过期(>30s)时 feed_fresh=False, 引擎只信任 REST 原生主源,
        WS 分量 (ofi_60/ofi_candle 交叉) 自动降级. 失败返回 {}."""
        try:
            cache = json.load(open(os.path.expanduser("~/bb-auto/ofi.json")))
            if not isinstance(cache, dict):
                return {}
            now_ms = int(time.time() * 1000)
            ts = cache.get("ts", 0) or 0
            age_ms = now_ms - ts
            fresh = bool(ts) and 0 < age_ms <= 30_000
            cr = cache.get("classification_ratio")
            return {
                "ofi": cache.get("ofi", 0.0) or 0.0,
                "ofi_candle": cache.get("ofi_candle", cache.get("ofi", 0.0)) or 0.0,
                "ofi_60": cache.get("ofi_60", 0.0) or 0.0,
                "ofi_window_sec": cache.get("window_sec", 0) or 0,
                "feed_fresh": fresh,
                "ts_age_ms": int(age_ms) if ts else None,
                "classification_ratio": cr if isinstance(cr, (int, float)) else 1.0,
                "agg_native": cache.get("agg_native"),
                "low_vol": bool(cache.get("low_vol", False)),
                "last_trade_ts": cache.get("last_trade_ts", 0) or 0,
            }
        except Exception:
            return {}
    
    
    # ======================== 真 OFI 方向与概率 (v6.0 核心改造) ========================
    # 原则 (对抗审查结论): 方向预测在 5min 尺度接近硬币 (body 延续 52.6% = 硬币已证).
    # edge 定义改为错价检测: 真 OFI 净流方向 = 真实资金流方向 (净流入→bull/净流出→bear),
    # 概率 = P(close>open | ofi), 与 UP/DOWN token 价比较决定 EV = p − ask.
    # OFI 领先 token 价: 净流已发生但价格未定价 (flow-gap) = 真正的 edge 来源.
    
    T_OFI_GATE = 0.20        # |ofi_n| 方向门限 (校准参数; 对应 ≥60% 量单边)
    T_OFI_60 = 0.35          # 最近60s 新鲜度反转门限
    MIN_VOL_FRAC = 0.25      # in-candle 成交量 ≥ 平均已完成K线量*该比例 才认可流量强度
    MIN_PROGRESS = 0.15      # progress ≥ 15% (≥45s) 才判方向, 防 K线开头噪声
    OFI_CR_MIN = 0.80        # classification_ratio 质量闸 (WS 辅源)
    OFI_BAYES_N = 30         # Bayesian shrink 先验样本数 (向 0.5)
    
    _OFI_BUCKETS = [(-1.0, -0.5), (-0.5, -0.25), (-0.25, -0.1),
                    (-0.1, 0.1), (0.1, 0.25), (0.25, 0.5), (0.5, 1.0)]
    
    
    def _ofi_native(candle):
        """当前K线原生 in-candle OFI: ofi_n = 2*(tb/v) − 1 ∈ [-1,1].
        原生 aggressor 聚合 (kline[9] = taker buy base volume, 已在 fetch_klines 取到),
        天然按K线对齐、零 WS 依赖. 无有效量返回 None."""
        if not candle:
            return None
        v = candle.get("v", 0.0) or 0.0
        if v <= 0:
            return None
        tb = candle.get("tb", 0.0) or 0.0
        tb = min(tb, v)
        return 2.0 * (tb / v) - 1.0
    
    
    def _ofi_bucket(ofi_n):
        for lo, hi in _OFI_BUCKETS:
            if lo <= ofi_n < hi:
                return lo, hi
        return _OFI_BUCKETS[0] if ofi_n < -1.0 else _OFI_BUCKETS[-1]
    
    
    def ofi_calibration_table(candles):
        """真 OFI→方向 经验校准表 (先证后上 D1: 离线校准的在线近似).
        用已完成K线的 in-candle ofi_n (最终 tb/v) 与 close>open 建表,
        每格 Bayesian shrink 向 0.5 (prior n=30), 替代 body 延续概率.
        返回 {(lo, hi): {"n": n, "p": p}}."""
        agg = {k: {"n": 0, "up": 0} for k in _OFI_BUCKETS}
        for c in candles[:-1]:  # 已完成K线 (不含当前未完成)
            ofi_n = _ofi_native(c)
            if ofi_n is None:
                continue
            up = 1 if c["c"] > c["o"] else 0
            agg[_ofi_bucket(ofi_n)]["n"] += 1
            agg[_ofi_bucket(ofi_n)]["up"] += up
        table = {}
        for k, st in agg.items():
            n = st["n"]
            f = st["up"] / n if n > 0 else 0.5
            p = (n * f + OFI_BAYES_N * 0.5) / (n + OFI_BAYES_N)
            table[k] = {"n": n, "p": p}
        return table
    
    
    def ofi_direction(candles, ofi_cache, progress, atr_val=None):
        """真 OFI 方向判定 (v6.0 二选一版, 无中性):
        - 主源: 当前K线原生 in-candle ofi_n (REST, 天然K线对齐)
        - 方向: ofi_n>0→bull, ofi_n<0→bear (永远二选一); ofi_n 无/为0 用 body 符号兜底
        - 弱信号/流量低/反向冲突 记录到 meta, 供概率层降权 (概率近0.5→EV过滤不买)
        返回 (direction, meta)."""
        cur = candles[-1] if candles else None
        if cur is None:
            return "bull", {"no_data": True}
        ofi_n = _ofi_native(cur)
        body = cur.get("c", 0) - cur.get("o", 0)
    
        # 流量强度 (供概率层降权, 不再把方向变中性)
        v = cur.get("v", 0.0) or 0.0
        avg_v = 0.0
        if len(candles) >= 2:
            avg_v = sum(c.get("v", 0.0) or 0.0 for c in candles[:-1]) / (len(candles) - 1)
        vol_gate = avg_v > 0 and v >= avg_v * MIN_VOL_FRAC
    
        cr = 1.0
        if ofi_cache:
            cr = ofi_cache.get("classification_ratio")
            cr = cr if isinstance(cr, (int, float)) else 1.0
        cr_ok = cr >= OFI_CR_MIN
    
        meta = {"ofi_n": round(ofi_n, 4) if ofi_n is not None else None,
                "vol_gate": bool(vol_gate),
                "vol_frac": round(v / avg_v, 3) if avg_v > 0 else 0.0,
                "cr": round(cr, 3), "cr_ok": bool(cr_ok)}
    
        # 方向二选一: 优先 OFI 符号, 无 OFI 用 body 兜底
        if ofi_n is not None and ofi_n != 0:
            direction = "bull" if ofi_n > 0 else "bear"
        else:
            direction = "bull" if body > 0 else "bear"
            meta["body_fallback"] = True
    
        # 60s 反向 / WS 冲突: 记 meta (概率层降权), 不翻方向
        if ofi_cache and ofi_cache.get("feed_fresh"):
            ofi_60 = ofi_cache.get("ofi_60", 0.0) or 0.0
            if abs(ofi_60) >= T_OFI_60 and (ofi_60 > 0) != (ofi_n is not None and ofi_n > 0):
                meta["reversed_60"] = True
            ws_candle = ofi_cache.get("ofi_candle", 0.0) or 0.0
            if abs(ws_candle) >= T_OFI_GATE and (ws_candle > 0) != (ofi_n is not None and ofi_n > 0):
                meta["ws_conflict"] = True
    
        return direction, meta
    
    
    def ofi_probability(candles, ofi_cache, body, atr_val, progress, remaining_sec):
        """真 OFI 概率 (v6.0, 替代 close_direction_confidence 延续概率): P(close>open | ofi).
        三层:
          层1 经验校准表 p_cal = P(close>open | ofi_n 桶) (Bayesian shrink)
          层2 flow-gap: gap = ofi_n − clamp(body_s, −0.5, 0.5) — 净流已发生但价格未定价的缺口
          层3 剩余走势: E_rem = 0.6·ofi_60 + 0.4·gap, 按剩余时间比例衰减
        p = clamp(0.5 + (p_cal−0.5) + 0.30·sign(E_rem)·min(|E_rem|,1)·rem_frac, 0.15, 0.88)
        返回 (p, meta)."""
        ofi_n = _ofi_native(candles[-1] if candles else None)
        if ofi_n is None:
            return 0.5, {}
    
        table = ofi_calibration_table(candles)
        k = _ofi_bucket(ofi_n)
        p_cal = table[k]["p"]
        n_cal = table[k]["n"]
    
        body_s = (body / atr_val) if atr_val and atr_val > 0 else 0.0
        gap = ofi_n - max(-0.5, min(0.5, body_s))
    
        # 最近60s 净流: WS 新鲜用 WS, 否则用主源 ofi_n 近似
        ofi_60 = ofi_n
        if ofi_cache and ofi_cache.get("feed_fresh"):
            ofi_60 = ofi_cache.get("ofi_60", 0.0) or 0.0
    
        rem_frac = max(0.0, min(1.0, (remaining_sec or 0) / 300.0))
        E_rem = 0.6 * ofi_60 + 0.4 * gap
        sign_e = 1 if E_rem > 0 else (-1 if E_rem < 0 else 0)
        p = 0.5 + (p_cal - 0.5) + 0.30 * sign_e * min(abs(E_rem), 1.0) * rem_frac
        p = max(0.15, min(0.88, p))
    
        meta = {"ofi_n": round(ofi_n, 4), "p_cal": round(p_cal, 3), "cal_n": n_cal,
                "cal_bucket": k, "gap": round(gap, 4), "ofi_60": round(ofi_60, 4),
                "rem_frac": round(rem_frac, 3)}
        return p, meta
    
    
    # ======================== Regime Detection ========================
    
    def detect_regime(vol_ratio, candles, atr_val=None):
        """4态: HIGH_VOL / TREND / RANGE / LOW_VOL
        v5.7.1: 增加ATR spike强制HIGH_VOL路径"""
        if vol_ratio > 2.0:
            return "HIGH_VOL"
        # v5.7.1 P0-1: ATR spike检测 -- 连续K线range>ATR*1.5时强制HIGH_VOL
        if atr_val is not None and atr_val > 0:
            is_spike, _, _ = atr_spike_detect(candles, atr_val)
            if is_spike:
                return "HIGH_VOL"
        closes = [c["c"] for c in candles]
        if len(closes) >= 30:
            e9 = ema(closes, 9)
            e9_prev = ema(closes[:-10], 9) if len(closes) > 10 else e9
            atr_v = atr_wilder(candles)
            if atr_v > 0 and closes[-1] > 0:
                norm_slope = (e9 - e9_prev) / closes[-1] / (atr_v / closes[-1])
            else:
                norm_slope = 0
            if vol_ratio < 0.6:
                return "LOW_VOL"
            if abs(norm_slope) > 0.5:
                return "TREND"
        return "RANGE"
    
    # ======================== Factor Combination ========================
    
    # v5.9 对抗式审查修复 (2026-08-13):
    # 实测回测证伪了 11/13 因子(47-49%硬币), 且 momentum/rsi 反向。
    # 权重与证据完全倒挂 —— 收敛到 3 个有证据的信号:
    #   half_body(延续, 主信号) + volume(唯一独立alpha 58%) + meanrev(唯一正向价格因子 51.7%)
    # 其余因子(占37%权重无信息 + 25%权重未验证)清零。
    BASE_W = {'momentum': 0.0, 'meanrev': 0.3, 'rsi': 0.0,
              'volume': 0.8, 'fatigue': 0.0, 'imbalance': 0.0, 'microprice': 0.0,
              'decel': 0.0, 'position': 0.0,
              'v_reversal': 0.0, 'vol_breakout': 0.0,
              'half_body': 1.2,  # 延续主信号 (真正 edge)
              'taker_buy': 0.0}  # 未验证, 清零 (待纳入公平回测后再评估)
    
    REGIME_ADJ = {
        'HIGH_VOL': {'momentum': 0.4, 'meanrev': 0.3, 'rsi': 0.3,
                     'volume': 0.2, 'fatigue': 0.5, 'imbalance': 0.6, 'microprice': 0.5,
                     'decel': 0.8, 'position': 0.6,
                     'v_reversal': 1.0, 'vol_breakout': 0.6,
                     'half_body': 1.0,  # v5.7: 高vol下body延续仍有效
                     'taker_buy': 0.7},  # v5.8: 高vol下主动买卖方向信息量适中
        'TREND':    {'momentum': 1.0, 'meanrev': 0.4, 'rsi': 0.5,
                     'volume': 0.4, 'fatigue': 0.4, 'imbalance': 0.7, 'microprice': 0.5,
                     'decel': 0.9, 'position': 0.6,
                     'v_reversal': 0.9, 'vol_breakout': 0.5,
                     'half_body': 1.0,  # v5.7: 趋势中body延续很强
                     'taker_buy': 0.6},  # v5.8: 趋势中主动买卖略弱于震荡(方向已由趋势表达)
        'RANGE':    {'momentum': 0.4, 'meanrev': 1.5, 'rsi': 0.2,
                     'volume': 0.3, 'fatigue': 0.6, 'imbalance': 0.9, 'microprice': 0.7,
                     'decel': 0.5, 'position': 0.8,
                     'v_reversal': 0.7, 'vol_breakout': 0.4,
                     'half_body': 0.8,  # v5.7: 震荡中body延续稍弱(可能回归)
                     'taker_buy': 0.8},  # v5.8: 震荡中主动买卖主导方向
        'LOW_VOL':  {'momentum': 0.5, 'meanrev': 1.2, 'rsi': 0.3,
                     'volume': 0.2, 'fatigue': 0.4, 'imbalance': 0.8, 'microprice': 0.6,
                     'decel': 0.6, 'position': 0.7,
                     'v_reversal': 0.5, 'vol_breakout': 0.3,
                     'half_body': 0.9,  # v5.7: 低vol下body方向信息量适中
                     'taker_buy': 0.6},  # v5.8: 低vol下主动买卖方向略弱
    }
    
    def combine_factors(factors, regime):
        adj = REGIME_ADJ.get(regime, BASE_W)
        score = total_w = 0
        for name, value in factors.items():
            w = BASE_W.get(name, 0) * adj.get(name, 1.0)
            score += w * value
            total_w += w
        if total_w > 0:
            score = score / total_w * 3
        return score
    
    def sigmoid_compress(score, max_score=45, sensitivity=1.5):
        """Smooth sigmoid: [-inf,+inf] -> [-45, +45]"""
        return max_score * (2 / (1 + math.exp(-score / sensitivity)) - 1)
    
    # ======================== Direction Decision ========================
    
    # v6.0: close_direction_confidence (body 延续概率) 已删除 —
    # 方向预测被证明是硬币 (52.6%), 置信度改由真 OFI 概率 ofi_probability 提供.
    # 2026-09-10: calibrate_confidence (v5.5 Platt Scaling) 也已删除 —— 无任何调用点,
    # 且其"置信度与准确率正相关"的前提已被对抗审查推翻 (实测 conf 35-57 压扁且反校准).
    
    
    def direction_rule_v5(candles, closes, atr_val, vol_ratio,
                          depth_data=None, candle_progress=1.0, fng_value=None,
                          mtf=None):
        """v5.9 -- 增加 mtf(多周期信号)参数用于趋势过滤"""
        mom = momentum_tstat(closes)
        mr = zscore_meanrev(closes)
        rsi_val = rsi(closes)
        rsi_m = rsi_momentum(rsi_val)
    
        vols = [c["v"] for c in candles]
        vol_now = vols[-1]
        vol_avg = sum(vols[-21:-1]) / 20 if len(vols) >= 21 else vol_now
        vol = volume_conditional(vol_now, vol_avg, candle_progress, candles)
        fat = consecutive_fatigue(candles)
    
        imb, mpd = orderbook_signals(depth_data, closes[-1])
    
        decel = momentum_deceleration(closes)
    
        # v5.6 R1: momentum/decel 冲突检测 -- 趋势衰竭信号
        # 当 momentum 方向绝对值大(>0.7)但 decel 方向相反且也大(>0.8),
        # 说明趋势正在衰竭(15根K线斜率还在惯性, 但近 5根已在减速/反转)
        # 此时动态降权 momentum, 升权 decel, 让反转信号穿透
        mom_val = mom  # 保留原始值用于冲突检测
        decel_val = decel
        conflict_detected = False
        if abs(mom_val) > 0.7 and abs(decel_val) > 0.8:
            # 方向相反: 乘积<0 意味着 momentum 和 decel 一个正一个负
            if mom_val * decel_val < 0:
                conflict_detected = True
                # 不修改原始因子值, 而是在 combine_factors 中用动态权重覆盖
                # 记录冲突标记, 供后续使用
    
        factors = {'momentum': mom, 'meanrev': mr, 'rsi': rsi_m,
                   'volume': vol, 'fatigue': fat,
                   'imbalance': imb, 'microprice': mpd,
                   'decel': decel,
                   'position': price_position(candles),
                   'v_reversal': v_reversal_detect(candles),      # v5.6 R2
                   'vol_breakout': vol_breakout_signal(candles),   # v5.6 R3
                   'half_body': half_body_momentum(candles, candle_progress, atr_val),  # v5.7 S1
                   'taker_buy': taker_buy_signal(candles)}  # v5.8 S1: 主动买卖量能因子
    
        regime = detect_regime(vol_ratio, candles, atr_val)  # v5.7.1: 传入atr_val
    
        # v5.7.1 P0-2: FNG<25 黑天鹅过滤 -- 极端恐惧时v_reversal/decel不可信
        # 黑天鹅教训(2026-05-28): 暴跌后缩量被v_reversal=0.6+decel=+1.0误判为企稳
        # 极端恐惧环境下,反转因子缺乏统计基础,大幅衰减
        fng_black_swan = False
        _saved_base_for_fng = None
        if fng_value is not None and fng_value < 25:
            fng_black_swan = True
            _saved_base_for_fng = BASE_W.copy()  # 保存全局权重
            # 动态衰减v_reversal和decel的权重
            # v_reversal: 0.8>0.4 (衰减 50%), decel: 0.7>0.49 (衰减 30%)
            BASE_W['v_reversal'] = round(BASE_W.get('v_reversal', 0.8) * 0.5, 2)
            BASE_W['decel'] = round(BASE_W.get('decel', 0.7) * 0.7, 2)
    
        # v5.6 R1: 冲突时动态调整因子权重
        # momentum 权重从 1.0降至 0.4, decel 权重从 0.7升至 0.9
        # 让反转信号不再是噪声, 而是主导信号
        if conflict_detected:
            saved_base = BASE_W.copy()
            saved_regime = REGIME_ADJ.get(regime, {}).copy() if regime in REGIME_ADJ else {}
            BASE_W['momentum'] = 0.4   # 1.0>0.4: 趋势惯性降权
            BASE_W['decel'] = 0.9      # 0.7>0.9: 减速信号升权
            raw = combine_factors(factors, regime)
            BASE_W.update(saved_base)   # 恢复冲突检测前的权重
            if saved_regime:
                REGIME_ADJ[regime] = saved_regime
        else:
            raw = combine_factors(factors, regime)
    
        # v5.7.1 P0-2: 恢复FNG修改前的全局权重(防止污染下次调用)
        if _saved_base_for_fng is not None:
            BASE_W.update(_saved_base_for_fng)
    
        score = sigmoid_compress(raw)
    
        # ---- Regime-aware score adjustment ----
        if regime == "TREND":
            ts = 1 if factors["momentum"] > 0 else -1
            ss = 1 if score > 0 else -1
            if ss != ts:
                score *= 0.25  # dampen counter-trend (keep 25% for extreme cases)
            if factors.get("decel", 0) * ts < -0.5:
                score *= 0.5   # decel > reduce confidence, not direction
        elif regime == "HIGH_VOL":
            score *= 0.6  # v5.9: 0.45>0.6 放宽 (审查: 三层惩罚去掉一层, 低conf高vol桶实测62.5%不该过度压制)
    
        # ---- P1-1: Bull bias惩罚 (v5.9 移除) ----
        # 原假设 "bear 69.1% > bull 63.5%" 已被最新 317 笔实测推翻 (bull 64% > bear 50%)
        # 该惩罚现在压制唯一真 edge, 移除。方向偏差改由滚动窗口经验胜率校准。
        # if score > 0:
        #     score *= 0.92
    
        # ---- v5.9: 多周期趋势过滤 — 逆大趋势的信号大幅降权 ----
        # 4h 大方向: 明显下跌时压制做多, 明显上涨时压制做空
        # 这是对抗审查 P0: 趋势日逆势押单(尤其 bear)是最大亏损源
        if mtf:
            tf4 = mtf.get("tf_4h_slope", 0.0)
            if tf4 < -0.0004 and score > 0:   # 4h 每根跌 0.04% = 明显下跌
                score *= 0.35
            elif tf4 > 0.0004 and score < 0:  # 4h 每根涨 0.04% = 明显上涨
                score *= 0.35
    
        # ---- v5.9: 跨资产广度过滤 — ETH/SOL 同向涨跌时, BTC 逆势信号不可信 ----
        # 跨资产合力方向 = 市场真实情绪, 单币假信号(尤其震荡日)在此被过滤
        if mtf:
            breadth = mtf.get("ca_eth_mom", 0.0) + mtf.get("ca_sol_mom", 0.0)
            if breadth < -0.001 and score > 0:    # ETH+SOL 同跌, BTC 做多不可信
                score *= 0.5
            elif breadth > 0.001 and score < 0:   # ETH+SOL 同涨, BTC 做空不可信
                score *= 0.5
    
        # ---- v6.0: 真 OFI 方向判定 (替代 v5.10 body 延续) ----
        # 主源 = 当前K线原生 in-candle ofi_n (REST tb 聚合, 天然K线对齐, 零 WS 依赖)
        # 辅源 = WS ofi.json (ofi_candle/ofi_60/feed_fresh) 做新鲜度反转保护与交叉校准
        # 净流入 → bull, 净流出 → bear; **二选一无中性** (ofi_n 缺失/为0 用 body 符号兜底).
        # 弱信号/流量不足只记 meta 供概率层降权 (p→0.5 → EV 过滤不买), 不产生 neutral.
        # 方向一票交给 OFI: body 不再决定 bias, 只作 display.
        body = candles[-1]["c"] - candles[-1]["o"]
        ofi_dir, ofi_meta = ("neutral", {})
        if isinstance(mtf, dict):
            ofi_dir, ofi_meta = ofi_direction(candles, mtf, candle_progress, atr_val)
        bias = ofi_dir  # 'bull' / 'bear' / 'neutral'
        strength = "strong" if abs(score) > 25 else ("medium" if abs(score) > 6 else "weak")
    
        # ---- v6.0: 收盘方向概率 = OFI 校准概率 (替代 close_direction_confidence 延续概率) ----
        remaining_sec = max(0, int(300 - (candle_progress or 0) * 300))
        ofi_p, ofi_p_meta = ofi_probability(
            candles, mtf if isinstance(mtf, dict) else None, body, atr_val,
            candle_progress, remaining_sec)
        confidence = max(35, int(round(ofi_p * 100)))
        if bias == "neutral":
            confidence = min(confidence, 50)  # 中性不装强
        ofi_meta.update(ofi_p_meta)
    
        # v5.7.1 P0-1: ATR spike时confidence减半 -- 黑天鹅防护
        is_spike, spike_ratio, spike_count = atr_spike_detect(candles, atr_val)
        if is_spike:
            confidence = max(35, int(confidence * 0.5))
    
        return bias, strength, confidence, int(score), factors, regime, fng_black_swan, ofi_meta
    
    # ======================== Price Prediction ========================
    
    def predict_close_v5(candles, closes, atr_val, bbu, bbm, bbl, bias, strength):
        """v5.7 -- ATR calib x0.55 (predict approx 2.5 min remaining) + historical percentile range"""
        pred = candles[-1]["c"]
    
        # v5.7: ATR乘数缩至 0.55x -- 半K线策略只需预测剩余约 55%时间
        if bias == "bull":
            pred += atr_val * (0.11 if strength == "strong" else 0.066)
        elif bias == "bear":
            pred -= atr_val * (0.11 if strength == "strong" else 0.066)
    
        e9 = ema(closes[-30:], 9) if len(closes) >= 30 else ema(closes, min(9, len(closes)))
        pred += (e9 - pred) * 0.15
    
        pred = max(bbl + atr_val * 0.05, min(bbu - atr_val * 0.05, pred))
        pred = round(pred)
    
        # v5.1: 历史range/ATR P75校准 + 更宽half_range
        ratios = []
        for c in candles[-50:]:
            r = c["h"] - c["l"]
            if atr_val > 0:
                ratios.append(r / atr_val)
        if ratios:
            sorted_r = sorted(ratios)
            p75 = sorted_r[int(len(sorted_r) * 0.75)]  # P75覆盖多数场景
        else:
            p75 = 1.0
        half = atr_val * p75 * 0.40  # v5.7: 0.65>0.40 缩窄区间(只预测剩余~2.5min)
    
        return pred, round(pred + half), round(pred - half)
    
    # ======================== Candle Info ========================
    
    def current_candle_info():
        now = datetime.now(CST)
        minute = (now.minute // 5) * 5
        cs = now.replace(minute=minute, second=0, microsecond=0)
        ce = cs + timedelta(minutes=5)
        elapsed = (now - cs).total_seconds()
        remain = 300 - elapsed
        return {
            "now": now.strftime("%H:%M:%S"),
            "candle_start": cs.strftime("%H:%M"),
            "candle_end": ce.strftime("%H:%M"),
            "progress_pct": round(elapsed / 300 * 100, 1),
            "remaining_sec": round(remain),
            "iso": cs.strftime("%Y-%m-%dT%H:%M:%S+08:00")
        }
    
    # ======================== P0-3: News Risk Loader ========================
    
    def load_news_risk():
        """P0-3: load data/news-risk-level.json, return (risk_level, is_black_swan)"""
        try:
            news_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                     "data", "news-risk-level.json")
            if not os.path.exists(news_path):
                return "UNKNOWN", False
            with open(news_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            risk = data.get("risk_level", "UNKNOWN")
            # BLACK_SWAN or CRITICAL triggers the circuit breaker
            return risk, risk in ("BLACK_SWAN", "CRITICAL")
        except Exception:
            return "UNKNOWN", False
    
    # ======================== Main ========================
    
    def _safe_result(future, default=None):
        """容错取 future.result(): 请求失败返回 default, 不让单路网络抖动拖垮引擎."""
        try:
            return future.result()
        except Exception:
            return default
    
    
    def _fetch_fng():
        """P0-2黑天鹅过滤: Fetch Fear & Greed Index (parallel-safe)"""
        try:
            req = urllib.request.Request("https://api.alternative.me/fng/?limit=1",
                                         headers={"User-Agent": "5minbtc/5.0"})
            with urllib.request.urlopen(req, timeout=5) as resp:
                fd = json.loads(resp.read())["data"][0]
            return {"value": int(fd["value"]), "label": fd["value_classification"]}
        except Exception:
            return {"value": None, "label": None}
    
    def run():
        info = current_candle_info()
    
        # v5.9: 9路并行HTTP — klines + depth + FNG + chainlink + 多周期(4h/1h/15m) + 跨资产(ETH/SOL)
        with ThreadPoolExecutor(max_workers=9) as ex:
            f_klines = ex.submit(fetch_klines, limit=200)
            f_depth = ex.submit(fetch_depth_avg)  # v5.8: 多时刻采样去噪(3次x1s)
            f_fng = ex.submit(_fetch_fng)
            f_cl = ex.submit(fetch_chainlink_ref)
            f_4h = ex.submit(fetch_klines, interval="4h", limit=30)
            f_1h = ex.submit(fetch_klines, interval="1h", limit=40)
            f_15m = ex.submit(fetch_klines, interval="15m", limit=30)
            f_eth = ex.submit(fetch_klines, symbol="ETHUSDT", limit=20)
            f_sol = ex.submit(fetch_klines, symbol="SOLUSDT", limit=20)
    
            candles = f_klines.result()
            depth = f_depth.result()
            fng = f_fng.result()
            chainlink_ref, cl_source = f_cl.result()
            k_4h = _safe_result(f_4h, [])
            k_1h = _safe_result(f_1h, [])
            k_15m = _safe_result(f_15m, [])
            k_eth = _safe_result(f_eth, [])
            k_sol = _safe_result(f_sol, [])
    
        closes = [c["c"] for c in candles]
        mtf = multi_timeframe_signal(k_4h, k_1h, k_15m)  # v5.9 多周期结构
        mtf.update(cross_asset_signal(k_eth, k_sol))     # v5.9 跨资产广度
        mtf.update(load_ofi())                            # v5.9 真订单流 OFI
    
        e9 = ema(closes, 9)
        e21 = ema(closes, 21)
        rsi_val = rsi(closes)
        macd_val, sig_val, macd_hist = compute_macd(closes)
        bbu, bbm, bbl = bollinger(closes)
        atr_val = atr_wilder(candles)
        vr = vol_regime_ratio(closes)
    
        progress = min(info["progress_pct"] / 100, 1.0)
        (bias, strength, confidence, score, factors, regime, fng_black_swan, ofi_meta) = direction_rule_v5(
            candles, closes, atr_val, vr, depth, progress, fng_value=fng.get("value"),
            mtf=mtf)
    
        # v5.7.1 P0-3: news risk circuit breaker
        news_risk, news_black_swan = load_news_risk()
        if news_black_swan:
            bias = "neutral"
            confidence = 30
            strength = "weak"
    
        pred_close, pred_high, pred_low = predict_close_v5(
            candles, closes, atr_val, bbu, bbm, bbl, bias, strength)
    
        recent = [{"O": round(c["o"]), "H": round(c["h"]),
                   "L": round(c["l"]), "C": round(c["c"])} for c in candles[-4:-1]]
        cur = candles[-1]
    
        # v5.6 R4: Chainlink价格对齐 — 已在并行块中获取
        binance_price = cur["c"]
        cl_offset = 0.0
        if chainlink_ref is not None and binance_price > 0:
            cl_offset = round(chainlink_ref - binance_price)
            if abs(cl_offset) > 300:
                cl_offset = 0.0
            else:
                pred_close = round(pred_close + cl_offset)
                pred_high = round(pred_high + cl_offset)
                pred_low = round(pred_low + cl_offset)
    
        # v5.7.1 P0-1: ATR spike检测
        is_spike, spike_ratio, spike_count = atr_spike_detect(candles, atr_val)
    
        result = {
            "version": "6.0.0",
            "candle": info,
            "price": {
                "current": cur["c"], "open": cur["o"],
                "high": cur["h"], "low": cur["l"],
                "body": round(cur["c"] - cur["o"]),
                "body_pct": round((cur["c"] - cur["o"]) / cur["o"] * 100, 3)
            },
            "recent_candles": recent,
            "indicators": {
                "ema9": round(e9, 1), "ema21": round(e21, 1),
                "ema_delta": round(e9 - e21, 1), "rsi": round(rsi_val, 1),
                "macd": round(macd_val, 2), "macd_signal": round(sig_val, 2),
                "macd_hist": round(macd_hist, 2),
                "bb_upper": round(bbu, 1), "bb_mid": round(bbm, 1), "bb_lower": round(bbl, 1),
                "atr": round(atr_val, 1),
                "vol_pct": round(cur["v"] / (sum(c["v"] for c in candles[-21:-1]) / 20) * 100, 0)
                            if len(candles) >= 21 else 0
            },
            "fng": fng,
            "factors": {k: round(v, 3) if isinstance(v, float) else v for k, v in factors.items()},
            "regime": regime,
            "mtf": mtf,  # v5.9: 多周期结构 (tf_4h_slope/tf_1h_adx/tf_1h_slope/tf_15m_pctb) + ofi 缓存
            "ofi": {  # v6.0: 真 OFI 决策元数据 (realtime 记账与复盘用)
                "direction": bias,
                "ofi_n": ofi_meta.get("ofi_n"),
                "ofi_60": ofi_meta.get("ofi_60"),
                "gap": ofi_meta.get("gap"),
                "p_cal": ofi_meta.get("p_cal"),
                "cal_n": ofi_meta.get("cal_n"),
                "cal_bucket": list(ofi_meta.get("cal_bucket", ())) or None,
                "vol_gate": ofi_meta.get("vol_gate"),
                "vol_frac": ofi_meta.get("vol_frac"),
                "cr": ofi_meta.get("cr"),
                "cr_ok": ofi_meta.get("cr_ok"),
                "feed_fresh": mtf.get("feed_fresh"),
                "agg_native": mtf.get("agg_native"),
                "reversed_60": ofi_meta.get("reversed_60", False),
                "ws_conflict": ofi_meta.get("ws_conflict", False),
                "early": ofi_meta.get("early", False),
            },
            "chainlink_offset": cl_offset,  # v5.6: Binance>Chainlink 价格偏移
            "atr_spike": {"detected": is_spike, "ratio": round(spike_ratio, 2),
                          "consecutive": spike_count},  # v5.7.1 P0-1
            "fng_black_swan": fng_black_swan,  # v5.7.1 P0-2: FNG<25时为True
            "news_risk": news_risk,  # v5.7.1 P0-3: NORMAL/BLACK_SWAN/CRITICAL
            "black_swan_warning": "⚠ News black swan active, direction unreliable" if news_black_swan else None,
            "prediction": {
                "bias": bias, "strength": strength,
                "confidence": confidence, "probability": round(confidence / 100, 3),
                "score": score,
                "pred_close": pred_close, "pred_high": pred_high, "pred_low": pred_low
            }
        }
        print(json.dumps(result, ensure_ascii=False))
    
    if __name__ == "__main__":
        run()
    
  • 5minbtc-log.py 11.8 KB
    #!/usr/bin/env python3
    """5minbtc 预测日志 - 记录预测vs实际,用于回测准确率"""
    import glob, gzip, json, os, sys
    from datetime import datetime, timezone, timedelta
    
    LOG_DIR = os.path.dirname(os.path.abspath(__file__))
    LOGS_DIR = os.path.join(LOG_DIR, "logs")
    LOG_FILE = os.path.join(LOGS_DIR, "5minbtc-log.jsonl")
    CST = timezone(timedelta(hours=8))
    
    def _ensure_logs_dir():
        os.makedirs(LOGS_DIR, exist_ok=True)
    
    def log_prediction(candle_start_iso, predicted_close, predicted_high, predicted_low, confidence, bias, news_sentiment, vol_pct, extra=None):
        """记录预测(在K线进行中调用). extra=dict 存 score/regime/factors/mtf 快照 (v5.9)"""
        _ensure_logs_dir()
        entry = {
            "ts": datetime.now(CST).isoformat(timespec="seconds"),
            "candle": candle_start_iso,
            "pred_close": predicted_close,
            "pred_range": [predicted_low, predicted_high],
            "confidence": confidence,
            "bias": bias,
            "news": news_sentiment,
            "vol_pct": vol_pct,
            "actual_close": None,
            "actual_high": None,
            "actual_low": None,
            "settled": False
        }
        if extra:
            entry["extra"] = extra
        with open(LOG_FILE, "a") as f:
            f.write(json.dumps(entry) + "\n")
        print(f"✅ Logged prediction for {candle_start_iso}: ${predicted_close:.0f}")
    
    def settle_candle(candle_start_iso):
        """从Binance API获取已结算K线,填入actual值"""
        import urllib.request
        _ensure_logs_dir()
        # Convert candle_start to epoch ms
        dt = datetime.fromisoformat(candle_start_iso)
        start_ms = int(dt.timestamp() * 1000)
        url = f"https://data-api.binance.vision/api/v3/klines?symbol=BTCUSDT&interval=5m&startTime={start_ms}&limit=1"
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
        if not data:
            print("❌ No candle data")
            return
        candle = data[0]
        actual_high = float(candle[2])
        actual_low = float(candle[3])
        actual_close = float(candle[4])
        close_time = int(candle[6])
        now_ms = int(datetime.now(CST).timestamp() * 1000)
        if now_ms < close_time:
            print(f"⏳ Candle not yet closed (closes at {datetime.fromtimestamp(close_time/1000, tz=CST).isoformat()})")
            return
    
        # Update log
        if not os.path.exists(LOG_FILE):
            print("❌ No log file")
            return
        lines = []
        updated = False
        with open(LOG_FILE, "r") as f:
            for line in f:
                entry = json.loads(line.strip())
                if entry["candle"] == candle_start_iso and not entry["settled"]:
                    entry["actual_close"] = actual_close
                    entry["actual_high"] = actual_high
                    entry["actual_low"] = actual_low
                    entry["settled"] = True
                    pred = entry["pred_close"]
                    err = actual_close - pred
                    err_pct = err / pred * 100
                    in_range = entry["pred_range"][0] <= actual_close <= entry["pred_range"][1]
                    entry["error"] = round(err, 2)
                    entry["error_pct"] = round(err_pct, 2)
                    entry["in_range"] = in_range
                    direction_ok = (entry["bias"] == "bull" and actual_close > float(candle[1])) or \
                                   (entry["bias"] == "bear" and actual_close < float(candle[1])) or \
                                   (entry["bias"] == "neutral")
                    entry["direction_correct"] = direction_ok
                    updated = True
                    print(f"✅ Settled {candle_start_iso}: pred=${pred:.0f} actual=${actual_close:.0f} err={err:+.0f} ({err_pct:+.2f}%) range={'✅' if in_range else '❌'} dir={'✅' if direction_ok else '❌'}")
                lines.append(json.dumps(entry) + "\n")
        if updated:
            with open(LOG_FILE, "w") as f:
                f.writelines(lines)
        else:
            print(f"❌ No unsettled prediction found for {candle_start_iso}")
    
    def get_last_unsettled():
        """返回最近一个未结算预测的candle时间(供cron使用)"""
        if not os.path.exists(LOG_FILE):
            print("NO_DATA")
            return
        entries = []
        with open(LOG_FILE, "r") as f:
            for line in f:
                e = json.loads(line.strip())
                entries.append(e)
        # 从后往前找最近一个未结算的
        for e in reversed(entries):
            if not e["settled"]:
                candle = e["candle"]
                pred = e["pred_close"]
                bias = e["bias"]
                print(f"{candle}|{pred:.0f}|{bias}")
                return
        # 如果都结算了,找最后一个已结算的(展示结果)
        for e in reversed(entries):
            if e["settled"]:
                candle = e["candle"]
                pred = e["pred_close"]
                actual = e["actual_close"]
                err = e.get("error", 0)
                rng = "✅" if e["in_range"] else "❌"
                dr = "✅" if e["direction_correct"] else "❌"
                bias = e["bias"]
                print(f"ALL_SETTLED|{candle}|{pred:.0f}|{actual:.0f}|{err:+.0f}|{rng}|{dr}|{bias}")
                return
        print("NO_DATA")
    
    def settle_all_unsettled():
        """结算所有未结算的预测(批量)"""
        if not os.path.exists(LOG_FILE):
            print("NO_DATA")
            return
        settled_count = 0
        lines = []
        with open(LOG_FILE, "r") as f:
            for line in f:
                entry = json.loads(line.strip())
                if not entry["settled"]:
                    # 获取该K线数据
                    try:
                        import urllib.request
                        dt = datetime.fromisoformat(entry["candle"])
                        start_ms = int(dt.timestamp() * 1000)
                        url = f"https://data-api.binance.vision/api/v3/klines?symbol=BTCUSDT&interval=5m&startTime={start_ms}&limit=1"
                        req = urllib.request.Request(url)
                        with urllib.request.urlopen(req, timeout=10) as resp:
                            data = json.loads(resp.read())
                        if data:
                            candle = data[0]
                            close_time = int(candle[6])
                            now_ms = int(datetime.now(CST).timestamp() * 1000)
                            if now_ms >= close_time:
                                actual_close = float(candle[4])
                                actual_high = float(candle[2])
                                actual_low = float(candle[3])
                                pred = entry["pred_close"]
                                err = actual_close - pred
                                err_pct = err / pred * 100
                                in_range = entry["pred_range"][0] <= actual_close <= entry["pred_range"][1]
                                direction_ok = (entry["bias"] == "bull" and actual_close > float(candle[1])) or \
                                               (entry["bias"] == "bear" and actual_close < float(candle[1])) or \
                                               (entry["bias"] == "neutral")
                                entry["actual_close"] = actual_close
                                entry["actual_high"] = actual_high
                                entry["actual_low"] = actual_low
                                entry["settled"] = True
                                entry["error"] = round(err, 2)
                                entry["error_pct"] = round(err_pct, 2)
                                entry["in_range"] = in_range
                                entry["direction_correct"] = direction_ok
                                settled_count += 1
                                pred_dir = entry['bias'].split('-')[0]  # bull/bear/neutral
                                actual_dir = 'bull' if actual_close > float(candle[1]) else 'bear'
                                dir_sym = '✅' if direction_ok else '❌'
                                rng_sym = '✅' if in_range else '❌'
                                print(f"✅ {entry['candle']}: pred=${pred:.0f} actual=${actual_close:.0f} err={err:+.0f} ({err_pct:+.2f}%)")
                                print(f"   方向: pred={pred_dir} actual={actual_dir} {dir_sym} | 区间: pred=[{entry['pred_range'][0]:.0f},{entry['pred_range'][1]:.0f}] actual=[{actual_low:.0f},{actual_high:.0f}] {rng_sym}")
                    except Exception as ex:
                        print(f"⚠️ {entry['candle']}: {ex}")
                lines.append(json.dumps(entry) + "\n")
        with open(LOG_FILE, "w") as f:
            f.writelines(lines)
        if settled_count == 0:
            # Show last settled for verification even when nothing new to settle
            last_settled = None
            for line in lines:
                e = json.loads(line.strip())
                if e.get("settled"):
                    last_settled = e
            if last_settled:
                e = last_settled
                rng_sym = '✅' if e['in_range'] else '❌'
                dir_sym = '✅' if e['direction_correct'] else '❌'
                pred_dir = e['bias'].split('-')[0]
                actual_dir = 'bull' if e['actual_close'] > e.get('actual_open', e['pred_close']) else 'bear'
                # Try to infer actual direction from actual_close vs pred_range low (open proxy)
                print(f"📋 Last settled: {e['candle']} pred=${e['pred_close']:.0f} actual=${e['actual_close']:.0f} err={e['error']:+.0f} ({e['error_pct']:+.2f}%)")
                print(f"   方向: pred={pred_dir} actual={actual_dir} {dir_sym} | 区间: pred=[{e['pred_range'][0]:.0f},{e['pred_range'][1]:.0f}] actual=[{e['actual_low']:.0f},{e['actual_high']:.0f}] {rng_sym}")
            else:
                print("No predictions found")
        else:
            print(f"\n📊 Batch settled {settled_count} predictions")
    
    def iter_log_lines():
        """按月归档(logs/archive/*.jsonl.gz, 老→新) + 当月 live。
    
        仅用于**只读统计**。settle 系列不要用 —— 它们要回写 live 文件,
        归档是只读的月度快照。
        """
        for gz in sorted(glob.glob(os.path.join(LOGS_DIR, "archive", "*.jsonl.gz"))):
            try:
                with gzip.open(gz, "rt", encoding="utf-8") as f:
                    for line in f:
                        yield line
            except Exception:
                continue
        if os.path.exists(LOG_FILE):
            with open(LOG_FILE, "r") as f:
                for line in f:
                    yield line
    
    
    def stats():
        """统计预测准确率 (含已归档月份)"""
        entries = []
        for line in iter_log_lines():
            line = line.strip()
            if not line:
                continue
            try:
                e = json.loads(line)
            except Exception:
                continue
            if e.get("settled"):
                entries.append(e)
        if not entries:
            print("No settled predictions yet")
            return
        total = len(entries)
        in_range = sum(1 for e in entries if e["in_range"])
        dir_correct = sum(1 for e in entries if e["direction_correct"])
        errors = [e["error_pct"] for e in entries]
        abs_errors = [abs(e) for e in errors]
        mae = sum(abs_errors) / total
        print(f"📊 5minbtc Prediction Stats ({total} settled)")
        print(f"  Direction accuracy: {dir_correct}/{total} = {dir_correct/total*100:.0f}%")
        print(f"  In range: {in_range}/{total} = {in_range/total*100:.0f}%")
        print(f"  MAE: {mae:.3f}%")
        print(f"  Max error: {max(errors):+.3f}% / {min(errors):+.3f}%")
    
    if __name__ == "__main__":
        if len(sys.argv) < 2:
            print("Usage: 5minbtc-log.py log|settle|stats [args...]")
            sys.exit(1)
        cmd = sys.argv[1]
        if cmd == "log":
            # log <candle_iso> <pred_close> <pred_high> <pred_low> <confidence> <bias> <news> <vol_pct> [extra_json]
            extra = None
            if len(sys.argv) > 10:
                try:
                    extra = json.loads(sys.argv[10])
                except Exception:
                    extra = None
            log_prediction(sys.argv[2], float(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5]), int(sys.argv[6]), sys.argv[7], sys.argv[8], float(sys.argv[9]), extra)
        elif cmd == "settle":
            settle_candle(sys.argv[2])
        elif cmd == "last-unsettled":
            get_last_unsettled()
        elif cmd == "settle-all":
            settle_all_unsettled()
        elif cmd == "stats":
            stats()
    
  • 5minbtc-news.py 7.9 KB
    #!/usr/bin/env python3
    """
    5minbtc-news.py -- BTC 5min prediction news scanner
    
    当前在用源 (scan_all):
      scan_coindesk (RSS fallback) -- ~14min 延迟 (唯一稳定源)
    
    已移除源 (验证失效, 2026-07-05 清理):
      - Cointelegraph RSS/TG  -- 0 results
      - TreeNews TG            -- scripts/telegram-treenews.py 不存在
      - TheBlock RSS           -- SSL 封锁
      - BitcoinMagazine RSS    -- 连接重置
      - NewsData.io API        -- 无 API key
      - CryptoCompare API      -- 无 API key
      - Binance Blog RSS       -- 未在 scan_all 调用
      - alternative.me FGI     -- 引擎 _fetch_fng() 已独立获取, 此处冗余
    """
    import json, os, re
    from datetime import datetime, timezone, timedelta
    
    # ─── 路径配置 ───
    SKILL_DIR = os.path.dirname(os.path.abspath(__file__))
    DATA_DIR = os.path.join(SKILL_DIR, "data")
    NEWS_RISK_FILE = os.path.join(DATA_DIR, "news-risk-level.json")
    NEWS_SIGNALS_FILE = os.path.join(DATA_DIR, "news-signals.jsonl")
    CST = timezone(timedelta(hours=8))
    
    # ─── 工具函数 ───
    def fetch_url(url, headers=None, timeout=10):
        try:
            import urllib.request
            req = urllib.request.Request(url, headers=headers or {"User-Agent": "Mozilla/5.0"})
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return resp.read().decode("utf-8", errors="ignore")
        except:
            return ""
    
    def cst_now():
        return datetime.now(CST)
    
    def parse_rss_time(date_str):
        """解析 RSS 时间,返回 CST datetime 或 None"""
        if not date_str:
            return None
        try:
            dt = datetime.strptime(date_str.strip(), "%a, %d %b %Y %H:%M:%S %z")
            return dt.astimezone(CST)
        except:
            pass
        try:
            dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
            return dt.astimezone(CST)
        except:
            pass
        return None
    
    # ─── 分类引擎 ───
    def classify(headline, body="", categories=""):
        text = (headline + " " + body + " " + categories).lower()
        danger = [
            "hawkish","rate hike","higher for longer","inflation ris","war escalat",
            "military conflict","sanction","hack","exploit","flash crash","bankrupt",
            "sec charg","recession","sell-off","bloodbath","plunge","whale dump",
            "regulation crackdown","iran","conflict","missile","nuclear","attack",
            "arrest","fraud","collapse","default"
        ]
        opp = [
            "dovish","rate cut","etf approved","inflow record","institutional adopt",
            "regulatory clarity","breakout","rally","surge","partnership",
            "bitcoin etf","blackrock","clarity act","bullish","ath","new high",
            "adopted by","legal tender","approval","approve"
        ]
        caution = [
            "cpi","fed meeting","nfp","ppi","nonfarm","awaiting","uncertain",
            "volatile","consolidation","resistance","support","mixed",
            "caution","review","probe","investigation"
        ]
        d = [w for w in danger if w in text]
        o = [w for w in opp if w in text]
        c = [w for w in caution if w in text]
        if d and len(d) >= len(o):
            return "bearish", min(10, 5 + len(d))
        elif o and len(o) > len(d):
            return "bullish", min(10, 4 + len(o))
        elif c:
            return "caution", min(10, 3 + len(c))
        return "neutral", 1
    
    # ─── 新闻源 ───
    def scan_coindesk_rss():
        """CoinDesk RSS -- 唯一在用源, ~14min 延迟"""
        results = []
        url = "https://www.coindesk.com/arc/outboundfeeds/rss/"
        data = fetch_url(url)
        if not data:
            return results
        now = cst_now()
        cutoff = now - timedelta(hours=24)
        items = re.findall(r"<item>(.*?)</item>", data, re.DOTALL)
        for item in items[:30]:
            title_m = re.search(r"<title><!\[CDATA\[([^\]]+)\]\]></title>", item)
            date_m = re.search(r"<pubDate>([^<]+)</pubDate>", item)
            if not title_m:
                continue
            title = title_m.group(1).strip()
            dt = parse_rss_time(date_m.group(1) if date_m else "")
            if not dt or dt < cutoff:
                continue
            sentiment, impact = classify(title)
            results.append({
                "ts": dt.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
                "source": "CoinDesk",
                "title": title[:200],
                "sentiment": sentiment,
                "impact": impact,
                "categories": ""
            })
        return results
    
    def scan_coindesk():
        """CoinDesk -- 当前直接走 RSS (无 API key)"""
        return scan_coindesk_rss()
    
    # ─── 主扫描 ───
    def scan_all():
        sources = [
            ("CoinDesk", scan_coindesk),
        ]
        all_articles = []
        source_stats = {}
        for name, fn in sources:
            try:
                arts = fn()
                source_stats[name] = len(arts)
                all_articles.extend(arts)
            except Exception as e:
                source_stats[name] = f"error: {e}"
        # 去重
        seen = set()
        unique = []
        for a in all_articles:
            key = a["title"][:100]
            if key not in seen:
                seen.add(key)
                unique.append(a)
        unique.sort(key=lambda x: x["ts"], reverse=True)
        return unique, source_stats
    
    # ─── 更新风险文件 ───
    def update_risk(articles, max_age_minutes=30):
        """计算风险等级,只计入 max_age_minutes 内的新文章"""
        now = cst_now()
        fresh = []
        stale = []
        for a in articles:
            try:
                dt = datetime.strptime(a["ts"], "%Y-%m-%dT%H:%M:%S+08:00")
                dt = dt.replace(tzinfo=CST)
                age_min = (now - dt).total_seconds() / 60
                a["_age_min"] = age_min
                if age_min <= max_age_minutes:
                    fresh.append(a)
                else:
                    stale.append(a)
            except Exception:
                stale.append(a)
        bullish = [a for a in fresh if a["sentiment"] == "bullish"]
        bearish = [a for a in fresh if a["sentiment"] == "bearish"]
        high_impact = [a for a in fresh if a["impact"] >= 6]
        nb, nl = len(bearish), len(bullish)
        if nb >= 2 and nb > nl:
            risk_level, sentiment = "HIGH_VOL", "BEARISH"
        elif nl >= 2 and nl > nb:
            risk_level, sentiment = "LOW_RISK", "BULLISH"
        elif nb >= 2 and nl >= 2:
            risk_level, sentiment = "ELEVATED", "NEUTRAL"
        elif nb == 1:
            risk_level, sentiment = "ELEVATED", "NEUTRAL"
        else:
            risk_level, sentiment = "NORMAL", "NEUTRAL"
        risk_data = {
            "risk_level": risk_level,
            "sentiment": sentiment,
            "timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
            "bullish_count": len(bullish),
            "bearish_count": len(bearish),
            "article_count": len(fresh),
            "total_count": len(articles),
            "high_impact_count": len(high_impact),
            "max_age_minutes": max_age_minutes,
            "stale_count": len(stale),
            "newest_article_age_min": fresh[0]["_age_min"] if fresh else None
        }
        os.makedirs(DATA_DIR, exist_ok=True)
        with open(NEWS_RISK_FILE, "w") as f:
            json.dump(risk_data, f, indent=2)
        with open(NEWS_SIGNALS_FILE, "w") as f:
            for a in articles:
                f.write(json.dumps(a) + "\n")
        return risk_data
    
    # ─── CLI ───
    if __name__ == "__main__":
        print(f"5minbtc News Scanner -- {cst_now().strftime('%H:%M:%S')}")
        print("-" * 40)
        articles, stats = scan_all()
        for src, cnt in stats.items():
            mark = "OK" if isinstance(cnt, int) and cnt > 0 else "FAIL"
            print(f"  [{mark}] {src}: {cnt}")
        print("-" * 40)
        risk = update_risk(articles)
        age_info = f"{risk['newest_article_age_min']:.0f}min ago" if risk['newest_article_age_min'] is not None else "no articles"
        print(f"Sentiment: {risk['sentiment']} | Risk: {risk['risk_level']}")
        print(f"Fresh({risk['max_age_minutes']}min): {risk['article_count']} | bull={risk['bullish_count']} | bear={risk['bearish_count']} | newest: {age_info}")
        if risk['stale_count'] > 0:
            print(f"  Note: {risk['stale_count']} articles older than {risk['max_age_minutes']}min (excluded from sentiment)")
        for a in articles[:3]:
            print(f"  - [{a['source']}] {a['title'][:80]}")
    
  • README.md 14.7 KB
    # 5minbtc — BTC 5 分钟实时方向预测引擎
    
    > 引擎 + LLM 混合架构:**真 OFI 净流定方向 + 错价检测定 edge**,LLM 综合裁决,预测 BTC 单根 5 分钟 K 线的方向与收盘价区间。
    >
    > ⚠️ **v6.0 起方向不再来自因子打分** — 13 个价量因子中 11 个经公平回测为 47–49%(等同抛硬币),现仅保留输出供参考,方向由当前 K 线原生 OFI 净流一票决定。详见 [对抗式审查报告](references/strategy-adversarial-review.md)。
    >
    > ⚠️ **NOT FINANCIAL ADVICE / 非投资建议** — 本项目仅为量化研究与学习目的,不构成任何投资、交易或持仓建议。预测存在显著误差,实际交易将面临资金损失风险。详见文末 [免责声明](#免责声明-not-financial-advice)。
    
    [![Engine](https://img.shields.io/badge/engine-v6.0.0-blue)]() [![Python](https://img.shields.io/badge/python-3.8%2B-green)]() [![Deps](https://img.shields.io/badge/dependencies-stdlib%20only-success)]()
    
    ---
    
    ## 目录
    
    - [核心理念](#核心理念)
    - [系统架构](#系统架构)
    - [12 正交因子体系](#12-正交因子体系)
    - [半 K 线预测策略](#半-k-线预测策略half--candle)
    - [黑天鹅三重防护](#黑天鹅三重防护)
    - [数据源](#数据源)
    - [快速开始](#快速开始)
    - [完整运行流程](#完整运行流程)
    - [目录结构](#目录结构)
    - [回测系统](#回测系统)
    - [性能快照](#性能快照)
    - [配置说明](#配置说明)
    - [已知局限](#已知局限)
    - [路线图](#路线图)
    - [免责声明](#免责声明-not-financial-advice)
    - [License](#license)
    
    ---
    
    ## 核心理念
    
    5minbtc 解决一个具体问题:**在每根 5 分钟 BTC K 线进行到 ~60–80% 时,预测这根 K 线收盘时的方向和价格区间**。
    
    它不是全自动交易机器人,而是一个 **引擎 + LLM 混合决策系统**:
    
    ```
       量化引擎 (12 因子 + ATR 归一化)        LLM (新闻 + 因子 + 裁决规则)
       ┌─────────────────────────┐           ┌──────────────────────────┐
       │ 4 路并行拉取市场数据      │  JSON →  │ 3 路并行新闻搜索          │
       │ 12 正交因子打分           │ ──────→  │ 综合裁决 (可微调引擎结论)  │
       │ Platt Scaling 置信度      │           │ 模板化输出 (15–25 行)      │
       └─────────────────────────┘           └──────────────────────────┘
             纯 Python 标准库                       可由 cron 调度
    ```
    
    **为什么要混合?** 回测证明:纯价格因子在公平(无前视)条件下无显著预测力(47–49%)。实盘的 edge 主要来自 K 线后段(progress ≥ 0.9)的"已确认走势延续"以及新闻冲击。把数值因子交给 LLM 做上下文综合,比纯规则或纯 LLM 都更稳健。
    
    ## 系统架构
    
    ### 组件
    
    | 组件 | 文件 | 职责 |
    |------|------|------|
    | **引擎** | `5minbtc-engine-v6.0.py` | 拉数据 → 计算 OFI(`ofi_n=2*(tb/v)-1`) → **OFI 一票定方向** + 概率 `P(close>open\|ofi)` → 输出预测 JSON(stdout)。13 因子仍计算但只作参考 |
    | **新闻** | `5minbtc-news.py` | 抓取 CoinDesk RSS,输出风险等级到 `data/news-risk-level.json` |
    | **日志** | `5minbtc-log.py` | 预测记录追加(jsonl)+ 增量 settle(结算上一根 K 线实际结果) |
    | **回测** | `backtest/*.py` | 历史数据回测,多版本(v5.6 / v5.7 / v5.8)|
    | **LLM** | (外部) | 读引擎 JSON + 新闻,综合裁决并输出 |
    
    ### 数据流(单次预测)
    
    ```
    cron 触发 (每根 5min K线 第4分钟)
       │
       ├─ 5minbtc-log.py settle-all      ← 结算上一根 K 线(写入实际收盘)
       │
       ├─ 5minbtc-engine-v6.0.py         ← 引擎核心
       │     │
       │     ├─ ThreadPoolExecutor(4) 并行:
       │     │    ├─ Binance  Klines   (200 根 5min K线)
       │     │    ├─ Binance  Depth    (订单簿 top-20)
       │     │    ├─ alternative.me FNG (恐惧贪婪指数)
       │     │    └─ Coinbase  BTC-USD  (Chainlink 参考)
       │     │
       │     ├─ 指标: EMA9/21, RSI, MACD, Bollinger, Wilder ATR, Vol-Regime
       │     ├─ 12 正交因子 → score → bias / confidence
       │     ├─ 黑天鹅过滤 (ATR spike / FNG<25 / 新闻熔断)
       │     └─ Chainlink 偏移补偿 (±$300 上限)
       │
       ├─ 5minbtc-news.py                 ← 写 news-risk-level.json
       │
       └─ LLM 读取引擎 JSON + 3 路新闻 → 裁决 → 写日志
    ```
    
    引擎单次执行约 **~3 秒**(v5.7.3 并行化后,原串行 ~11–18 秒)。
    
    ## 12 正交因子体系
    
    v5.0 起用正交因子替代共线指标,所有阈值经 **ATR 归一化**,避免波动率变化导致阈值失效。
    
    | 因子 | 类别 | 含义 |
    |------|------|------|
    | `momentum` | 趋势 | EMA9−EMA21 差值,ATR 归一化的 t-stat |
    | `meanrev` | 均值回归 | 偏离布林带中轨程度 |
    | `rsi` | 动量 | Wilder RSI 的 Z-score |
    | `volume` | 量能 | 条件化放量信号(区分突破 vs 衰竭)|
    | `fatigue` | 衰竭 | 趋势动能衰减检测 |
    | `imbalance` | 微结构 | 订单簿买卖盘失衡 |
    | `microprice` | 微结构 | Stoikov microprice 偏离 mid |
    | `decel` | 衰竭 | 价格变化减速(动量冲突时动态降权)|
    | `position` | 位置 | K 线在布林带中的相对位置 |
    | `v_reversal` | 反转 | 低点抬高 + 收>开 的 V 型反转模式 |
    | `vol_breakout` | 突破 | 最近 3 根完成 K 线的放量突破 |
    | **`half_body`** | **半 K 线** | **核心 edge:progress≥45% 时已形成 body 的延续性** |
    
    权重由 `BASE_W` 表给定,并随波动率 Regime(TREND / RANGE / HIGH_VOL / LOW_VOL)动态调整。
    
    ## 半 K 线预测策略(Half‑Candle)
    
    这是 v5.7 的核心贡献,也是实盘 edge 的正式建模:
    
    - **S1**:在 K 线进度 ≥ 45% 时激活 `half_body` 因子——已经形成的 body 方向在剩余时间内倾向于延续。
    - **S2**:预测范围收窄——ATR 乘数 × 0.55,只预测剩余 ~55% 时间内的波动。
    - **S3**:调度时机从每根 K 线第 2 分钟推迟到第 4 分钟(progress ~60–80%),让前半段充分形成信号再预测后半段。
    
    > 实证:回测中 11 因子组合无预测力(47–49%),但实盘 66% 的胜率主要来自 progress=0.9+ 时"确认已有走势"——`half_body` 把这个隐性逻辑变成了显式因子。
    
    ## 黑天鹅三重防护(v5.7.1)
    
    引擎在输出预测前进行三层过滤,任一触发即压低置信度或转为 neutral:
    
    1. **ATR Spike 检测**:当前 ATR 相对历史出现尖峰时,方向可靠性下降。
    2. **FNG < 25 过滤**:恐惧贪婪指数低于 25(极度恐慌)时标记 `fng_black_swan`。
    3. **新闻冲击熔断**:`5minbtc-news.py` 写入 `BLACK_SWAN`/`CRITICAL` 时,引擎强制 bias=neutral、confidence=30。
    
    ## 数据源
    
    全部为**公开、免认证**接口,零 API Key:
    
    | 数据 | 来源 | 用途 |
    |------|------|------|
    | K 线 | `data-api.binance.vision/api/v3/klines` | 200 根 5min OHLCV |
    | 订单簿 | `data-api.binance.vision/api/v3/depth` | top-20 bids/asks |
    | 恐惧贪婪 | `api.alternative.me/fng/` | 黑天鹅过滤 |
    | 参考价 | `api.coinbase.com/v2/prices/BTC-USD/spot` | Chainlink 偏移补偿 |
    | 新闻 | `www.coindesk.com/.../rss/` | 风险等级(~14min 延迟)|
    
    ## 快速开始
    
    **依赖**:仅 Python 3.8+ 标准库,无需 `pip install` 任何第三方包。
    
    ```bash
    # 单次运行引擎(输出预测 JSON 到 stdout)
    python3 5minbtc-engine-v6.0.py
    
    # 单独跑新闻扫描
    python3 5minbtc-news.py
    
    # 结算上一根 K 线 + 追加新预测
    python3 5minbtc-log.py settle-all
    python3 5minbtc-log.py log "<candle.iso>" <pred_close> <pred_high> <pred_low> \
        <conf> <bias> <news_sent> <vol_pct>
    ```
    
    引擎输出示例(节选):
    
    ```json
    {
      "version": "5.7.3",
      "candle": { "progress_pct": 72.3, "...": "..." },
      "indicators": { "ema9": 61950.1, "rsi": 47.2, "atr": 85.4, "...": "..." },
      "factors": { "half_body": -0.31, "momentum": -0.42, "...": "..." },
      "regime": "TREND",
      "news_risk": "NORMAL",
      "prediction": {
        "bias": "bear", "strength": "moderate",
        "confidence": 48, "pred_close": 61920, "pred_high": 61960, "pred_low": 61870
      }
    }
    ```
    
    ## 完整运行流程
    
    完整的"引擎 + LLM"混合流程由 cron 每 5 分钟触发:
    
    ```bash
    SKILL_DIR=/path/to/5minbtc
    
    # 1. 并行执行:结算 + 引擎 + 新闻
    python3 $SKILL_DIR/5minbtc-log.py settle-all
    python3 $SKILL_DIR/5minbtc-engine-v6.0.py
    python3 $SKILL_DIR/5minbtc-news.py
    
    # 2. LLM 侧:3 路并行新闻搜索
    #    - "Bitcoin BTC breaking news price"
    #    - "crypto market macro stocks today"
    #    - "比特币 BTC 最新 晚间"
    
    # 3. LLM 综合引擎 JSON + 新闻 → 按模板输出(15–25 行)
    
    # 4. 写入预测日志
    python3 $SKILL_DIR/5minbtc-log.py log "<candle.iso>" <pred_close> ...
    ```
    
    > 💡 用 `cp .env.example .env` 并设置 `SKILL_DIR` 指向你的克隆目录,可避免把路径写死。详见 [配置说明](#配置说明)。
    
    ## 目录结构
    
    ```
    5minbtc/
    ├── 5minbtc-engine-v6.0.py     # 主引擎(cron 调用,输出预测 JSON)
    ├── 5minbtc-engine-v5.py       # v5 旧版(回测因子模块 import 用)
    ├── 5minbtc-news.py            # 新闻扫描(CoinDesk RSS)
    ├── 5minbtc-log.py             # 日志记录 + settle
    ├── SKILL.md                   # 技能索引文档(Hermes/Claude skill 格式)
    ├── README.md                  # 本文件
    ├── .env.example               # 配置模板
    ├── backtest/
    │   ├── fetch_data.py          # Binance 历史数据下载
    │   ├── run_backtest.py        # v5.6 回测(公平,因子无预测力)
    │   ├── run_backtest_v57.py    # v5.7 回测(含已知前视偏差)
    │   ├── run_backtest_v58.py    # v5.8 回测(真实 1min 半 K线,零前视)⭐
    │   ├── run.sh                 # 一键运行
    │   ├── data/                  # 缓存的 K 线 JSON
    │   └── results/               # 回测结果(.gitignore)
    ├── references/                # 深度文档(架构/教训/pitfalls/黑天鹅…)
    ├── reviews/                   # 每日复盘报告(按月归档)
    ├── scripts/                   # 辅助脚本
    └── data/                      # 运行时数据(新闻风险等级,.gitignore)
    ```
    
    ## 回测系统
    
    ```bash
    cd backtest
    
    # 一键回测(下载最新数据 + 运行)
    ./run.sh
    
    # 快速模式(每小时采样,最近 180 天)
    ./run.sh --fast
    
    # 最近 90 天,每 6 根 K 线采样
    ./run.sh --days 90 --sample=6
    
    # 完整因子贡献分析
    ./run.sh --full-report
    
    # 模拟 K 线进度(progress=0.5 = 中段)
    python3 run_backtest.py --progress=0.5
    ```
    
    **公平性保障**:
    - 无前视偏差(当前 K 线价格信息被屏蔽:c=h=l=open, v=0)
    - 因子仅基于 200 根已完成 K 线计算
    - 回测中订单簿不可获取,imbalance/microprice 设为 0
    
    详见 [`backtest/README.md`](backtest/README.md)。
    
    ## 性能快照
    
    > 以下为历史实测数据,**过去表现不代表未来收益**,且小样本下统计显著性有限。
    
    | 版本 | 设定 | 方向准确率 |
    |------|------|-----------|
    | v4.x | 实盘 | 62.5% |
    | v5.7.1 | 实盘 273 轮 | **75.7%** (+13.2pp) |
    | v5.8 | 回测(前 2 根 1min) | 61.4% |
    | v5.8 | 回测(前 4 根 1min) | 69.5% |
    
    ⚠️ 见 [`references/backtest-findings.md`](references/backtest-findings.md):公平回测下纯价格因子无预测力。实盘高胜率很可能部分来自小样本 + K 线后段信息优势,并非稳定 edge。**在 edge 来源明确前,不应按高胜率预期下注。**
    
    ## 配置说明
    
    当前引擎**不读取任何环境变量**——所有参数(symbol、interval、HTTP timeout、ATR 乘数、因子权重)均硬编码在 `.py` 源码中,这保证了 cron 调用的零配置可复现性。
    
    `.env.example` 列出了**建议配置化**的项(当前为预留/文档用途),供希望二次开发的用户参考。如需让引擎实际读取这些变量,需要修改对应源码(欢迎 PR)。
    
    主要硬编码常量位置:
    
    | 常量 | 默认值 | 位置 |
    |------|--------|------|
    | `symbol` | `BTCUSDT` | `fetch_klines()` / `fetch_depth()` 形参 |
    | `interval` | `5m` | `fetch_klines()` 形参 |
    | HTTP timeout | `5`–`10` s | `urlopen()` 调用 |
    | ATR 乘数 | `0.55` | `predict_close_v5()` |
    | `BASE_W` | 因子权重表 | 模块级常量 |
    | `REGIME_ADJ` | Regime 权重调整 | 模块级常量 |
    
    ## 已知局限
    
    1. **回测与实盘的差距**:公平回测下因子无预测力,实盘 edge 主要来自 K 线后段信息优势(可能不稳定的"半前视")。
    2. **订单簿简化**:仅用 top-20 深度,未实现完整的 OFI(Cont et al. 2014)与 microprice 模型。
    3. **新闻延迟**:CoinDesk RSS 约 14 分钟延迟,无法捕捉瞬时冲击。
    4. **SSL 验证禁用**:引擎全局设置 `ssl._create_unverified_context`,是为对抗高延迟网络下的 SSL 握手超时(见 `references/high-latency-network-handling.md`)。在可信网络环境下可安全恢复验证。
    5. **无自动交易**:本项目只输出预测,不执行任何下单。
    
    ## 路线图
    
    - ✅ **OFI 微结构因子**(v6.0 已落地):引擎现由真 OFI 净流一票决定方向 + `P(close>open|ofi)` 概率,
      见 [SKILL.md 架构](SKILL.md#架构-1-行组件) 与 [strategy-adversarial-review.md](references/strategy-adversarial-review.md)
    - 🟡 HMM Regime 检测 + 自适应仓位
    - 🟡 LightGBM 自动化因子筛选
    - 🟡 CVaR 动态止损
    - 🟢 TFT 多时间尺度模型替代贝叶斯引擎
    
    > 原 `references/architecture.md`(13 因子模型 + 升级路线图)已于 2026-09-10 删除:
    > 其"13 正交因子"描述被 v5.9 清零、OFI 也已从路线图变为已实现,内容已由
    > [changelog.md](references/changelog.md) 与 [SKILL.md](SKILL.md) 取代。
    - 🟢 Deribit 期权 IV 信号
    
    ## 免责声明 (NOT FINANCIAL ADVICE)
    
    本项目是**量化研究与教育项目**,不构成投资建议、交易信号或任何形式的财务建议。
    
    - 加密货币市场波动剧烈,交易可能导致**全部本金损失**。
    - 引擎预测存在显著误差,历史准确率不代表未来表现。
    - 作者与贡献者对任何基于本项目的交易决策**不承担责任**。
    - 请在了解当地法律法规的前提下自行承担风险。
    
    如本项目对你有帮助,欢迎 Star。但**请勿**将其视为稳赚的交易工具。
    
    ## License
    
    本项目暂未指定开源许可证。如需使用、修改或分发代码,请先联系作者或等待 License 文件添加。在未指定 License 前,根据默认版权法,代码保留全部权利(仅供阅读与学习)。
    
  • SKILL.md 16.9 KB
    ---
    name: 5minbtc
    version: 6.0.0
    description: "BTC 5分钟K线实时方向预测 + 币安预测市场错价套利(paper)。v6.0 真OFI驱动: 方向由当前K线原生 in-candle OFI 净流一票决定(ofi_n=2*(tb/v)-1, REST klines[9] 聚合, 零WS依赖; WS ofi.json 做新鲜度反转保护与交叉校准), 概率=P(close>open|ofi) 经验校准表+Bayesian shrink, edge=EV=p−ask 错价检测, 方向二选一无中性。13因子自v5.9起收敛到3个有证据信号且不再决定bias。黑天鹅防护: ATR spike+FNG<25。launchd 常驻 realtime/watch/trader。⚠️ Binance 端点**人工**双向切换(引擎为单端点硬依赖,无自动故障切换;2026-09-11 曾因 data-api SSL 失败静默断档 6h)。"
    triggers:
      - 5minbtc
      - 5min btc
      - btc 5min
    tools:
      - terminal
      - web
    ---
    
    # 5minbtc — BTC 5分钟实时预测 v6.0.0
    
    > BTC 单根 5min K线 方向 + 收盘价预测。引擎+LLM 混合架构。
    > **SKILL.md 是索引, 详细内容见 `references/`。**
    > **v5.9 认知转变**: 从"13因子预测器"→"3信号错价检测器" — 方向准确率不是 edge, `EV = p − P − 成本` 才是 (见 [对抗式审查报告](references/strategy-adversarial-review.md))
    > **v6.0 执行转变**: 方向从"body 延续统计"改成**真 OFI 净流一票决定** — 赌的是"净流已发生、token 价还没定价"的 flow-gap
    
    > ⚠️ **同名易混**: 本 skill 只做 BTC 单标的 5min 方向 + 预测市场 EV。要做全市场永续的量价突破扫描, 用 [`coin-vp-scanner`](../coin-vp-scanner/SKILL.md) — 两者区别见下方 [与 coin-vp-scanner 的分工](#与-coin-vp-scanner-的分工)。
    
    ## 触发
    `5minbtc` / `5min btc` / `btc 5min` / `监控` (配合持续盯盘)
    
    ## 何时使用
    | 场景 | 做法 |
    |------|------|
    | 当前 5min K线 方向+价位 | ✅ 标准流程, **方向=OFI 净流, edge=错价 EV** (v5.8 零前视回测 61.4% 是延续性不是 alpha, 见性能快照) |
    | 会话内持续盯盘, 等明确信号 | ✅ 监控模式 (scripts/5minbtc-monitor.py + Monitor 工具) |
    | 币安预测市场 Up/Down 5m 交易 | ✅ paper 模拟/实时监控 (见 [预测市场策略](references/prediction-market-strategy.md)) |
    | "今晚 BTC 涨跌" (宽窗口) | ⚠️ 跑当前 K线 + 给方向倾向, 标注"超出引擎置信区间" |
    | "下根 K线" / "1小时后" | 引导在该 K线 起始时间再触发 |
    
    ## 监控模式 (Claude Code)
    
    > 详见 [monitoring-claude-code.md](references/monitoring-claude-code.md)
    
    ```bash
    # 无限持续, 直到用户喊停 (会话内盯盘推荐)
    Monitor(command: "python3 <SKILL>/scripts/5minbtc-monitor.py --max-runs 0", persistent: true)
    
    # 默认 20 次采样 (约 40 分钟)
    python3 <SKILL>/scripts/5minbtc-monitor.py
    
    # 单次判断 (非持续)
    python3 <SKILL>/scripts/5minbtc-monitor.py --dry-run
    ```
    
    - **明确信号** = bias 非中性 + strength∈{medium,moderate,strong} + conf≥50 → 自动停
      (⚠️ 实测引擎 strength 输出 `medium`, 判定集合需同时含 `medium` 和 `moderate`)
    - 每根 K 线第 2/3 分钟采样 (progress ~40-70%), 比 cron 第 4 分钟更早
    - 事件流: `START` / `DIR-CHANGE` / `CLEAR-SIGNAL` / `ENGINE-ERR` / `MAX-RUNS`
    - 停止: 用户说「停/结束」→ `TaskStop` 停 Monitor
    - `CLEAR-SIGNAL` 后必须拉一次完整引擎快照二次确认
    - 引擎验证: `python3 scripts/test_engine.py` (13 项, 自带 runner **无需 pytest**; 慢测加 `--slow`)
      — 装了 pytest 也可 `python3 -m pytest scripts/test_engine.py -v`
      (原文件名 `test_engine_v58.py` 带版本号, 引擎升到 v6.0 后即漂移, 2026-09-10 改为不带版本号)
    
    ## 快速开始
    
    ```bash
    SKILL_DIR=/home/aa/.hermes/profiles/cqo/skills/5minbtc
    
    # 1. 并行: 引擎 + 新闻 + settle (前一根)
    python3 $SKILL_DIR/5minbtc-log.py settle-all 2>&1
    python3 $SKILL_DIR/5minbtc-engine-v6.0.py 2>&1
    python3 $SKILL_DIR/5minbtc-news.py 2>&1
    
    # 2. 3 路 web_search (并行)
    # "Bitcoin BTC breaking news price" / "crypto market macro stocks today" / "比特币 BTC 最新 晚间"
    
    # 3. LLM 分析 → 输出 (见 output-template.md)
    # 4. 写日志
    python3 $SKILL_DIR/5minbtc-log.py log \
      "<candle.iso>" <pred_close> <pred_high> <pred_low> \
      <conf> <bias> <news_sent> <vol_pct>
    ```
    
    ## 架构 (1 行/组件)
    - **引擎** `5minbtc-engine-v6.0.py` (v6.0, 输出 `"version": "6.0.0"`): **bias 由真 OFI 净流一票决定** (ofi_n>0→bull / <0→bear, **二选一无中性**; ofi_n 缺失/为0 用 body 符号兜底并标 `meta.body_fallback`) + **概率 `P(close>open|ofi)`** 三层(经验校准表 Bayesian shrink + flow-gap + 最近60s流) + 三层独立过滤(多周期4h/1h/15m 结构 + 跨资产ETH/SOL 广度 + WS OFI 新鲜度反转保护) + 9路并行HTTP
      - ⚠️ **K 线开局阶段概率不可信**(已知问题,见 [pitfalls](references/pitfalls.md)):主源 `ofi_n = 2*(tb/v)−1` 取自**当前进行中**的 K 线,`_ofi_native()` 只挡 `v<=0`、**无最小样本量保护** → 开局几秒的少量成交就能把 ofi_n 推到 ±0.9。而 `vol_gate`(L692 按 `MIN_VOL_FRAC=0.25` 判断流量是否够)**算出来但从不参与任何决策**,只是输出里的装饰字段。**建议只在进度 ≥40% 后采信概率/下单**
      - 门限: `T_OFI_GATE=0.20` / `T_OFI_60=0.35` / WS 质量闸 `OFI_CR_MIN=0.80` / `OFI_BAYES_N=30`
      - **13 因子仍计算并输出**(JSON 契约保留), 但自 v6.0 起**不参与 bias 决策** — score 只用于 `strength` 标签与 LLM 参考
    - **订单流** `scripts/ofi_feed.py`: trade+bookTicker 组合流 tick规则推断主动买卖, 写 `~/bb-auto/ofi.json`, 带 ts 保鲜(>30s 引擎降级为只信 REST 主源) (launchd: com.daniel.ofi-feed)
    - **日志** `5minbtc-log.py`: jsonl 追加 + 增量 settle (写入 logs/)
    - **新闻** `5minbtc-news.py`: CoinDesk RSS (唯一稳定源, ~14min 延迟)
    - **常驻进程** (launchd): `5minbtc_realtime.py` (5s 刷新, 预测快照+EV下单) / `5minbtc_watch.py` / `5minbtc_trader.py --paper-monitor`
    - **LLM**: 因子打分基准 + LLM 综合裁决 + 模板输出
    
    ## 铁律
    1. 每次必须重新执行引擎脚本 — 不缓存
    2. 每次必须重新搜索3组新闻
    3. 先 settle 上一根, 再 log 新预测
    4. LLM 可微调引擎的 `pred_close`/range, 但必须说明理由
       ⚠️ **bias 不要轻易覆盖** (v6.0): 方向是 OFI 一票决定的, LLM 用因子/新闻翻方向 = 退回被证伪的路径。
       仅在 `meta.body_fallback=True` 或 OFI 数据缺失时才允许改 bias, 并显式写明原因。
    5. 输出 15-25 行 (平衡深度和 Telegram 可读性)
    
    ## 关键裁决规则 (⚠️ 历史规则: v5.7.x 因子打分路径)
    
    > **v6.0 起 bias 由真 OFI 一票决定, 下列规则不再决定方向。** 仅在两条兜底路径上仍有参考价值:
    > ① `meta.body_fallback=True` (ofi_n 缺失/为0, 方向退回 body 符号) ② LLM 复核时解释 `meta` 冲突字段。
    > 引擎的 `raw score` 仍输出, 但只影响 `strength` 标签, 不影响 bias。
    
    - **half_body vs imbalance 冲突** (v5.7.2): |half_body|>0.25 + |imbalance|>0.5 + progress≥45% → 优先 half_body <sub>(注: 两者自 v5.9 起权重已清零)</sub>
    - **TREND 强趋势 decel 约束** (v5.7.4): EMA delta>$100 时 |decel|>0.7 需 |half_body|>0.15 同向确认
    - **fatigue≥0.8**: conf 上限 40, 反向 +10pp
    - **chainlink_offset 矛盾**: bias=bull 但 pred_close<current → 以 current 为锚 ±ATR×0.3
    - **极端进度 (>80%)**: pred_close 按剩余时间比例缩放
    - **Body=0 持续模式**: pred_close → current ± ATR×0.2, conf 降至 35-42%
    
    ## 性能快照 (2026-09-10 更新)
    
    > 数字口径以 [对抗式审查报告](references/strategy-adversarial-review.md) 为准 — 旧数字多数含前视偏差。
    
    - **v5.7 回测 71.2% 含前视偏差**(文件里自己标注"已知"); 零前视 v5.8: 前2根1min=**61.4%**, 前4根=69.5%
    - 但 61–70% 是"看着 K 线走完再确认"的延续性, **不是 alpha** — 做市商已把它定价进 token 价
    - 去掉水分后的真实基准: non-neutral 方向 **57.7%** | bull **63.9%** (唯一显著) | **bear 50.0% = 纯硬币**
    - paper 交易: 全样本 UP 成交 **44%** (+$0.24) | 甜区 UP ask 0.40–0.50 → **60%** (+$1.39) ← 唯一正 EV 区间
    - **结论: edge 不在方向准不准, 在 `EV = p − P − 成本 > 0`。v6.0 的错价检测(flow-gap)就是直接做这件事。**
    - 当前 LLM: `zai/glm-5.2` (8-15s/次) | opencaio 实测 MiniMax-M3 ~2.7s 可作更快选项
    
    ## 📚 引用索引 (references/)
    
    ### 核心方法论
    - [strategy-adversarial-review.md](references/strategy-adversarial-review.md) — **对抗式审查报告: 第一性原理 + 13因子证伪 + 该留/删/缺失 + P0/P1/P2行动清单** (v5.9 依据)
    - [lessons.md](references/lessons.md) — **25 条核心教训** (必读, 含 2026-07-05 新增 23-25)
    - [pitfalls.md](references/pitfalls.md) — **20 条 pitfalls 集中索引** (必读, 含并行 max() 评估陷阱; #18 = K线开局概率不可信; #19 = "真OFI" 只覆盖单一市场; #20 = 端点故障致静默断档)
    - [changelog.md](references/changelog.md) — v5.0 ~ v6.0 详细变更
    - [skill-organization.md](references/skill-organization.md) — **Skill 文件结构模式 (可复用)** — SKILL.md INDEX + references/ 分专题
    
    ### 执行与输出
    - [execution.md](references/execution.md) — 完整执行步骤 + 铁律 + 宽窗口处理
    - [output-template.md](references/output-template.md) — LLM 输出模板 + 裁决规则
    - [monitoring-claude-code.md](references/monitoring-claude-code.md) — **监控模式: Monitor 工具集成 + 事件协议**
    - [telegram-push-monitor.md](references/telegram-push-monitor.md) — **Telegram 推送 daemon: watch 采样节奏/事件类型/记录与结算/launchd 持久化**
    - [prediction-trading-cli.md](references/prediction-trading-cli.md) — **币安预测交易 CLI: 5minbtc_trader.py 用法/参数/安全契约/API端点 (含 --paper/--paper-monitor 模拟)**
    - [prediction-market-strategy.md](references/prediction-market-strategy.md) — **预测市场盈利策略: EV=p−P, 价格门控, 凯利仓位, paper 模拟**
    
    ### 数据源 & 网络
    - [news-sources.md](references/news-sources.md) — 新闻源评估 (清理后只剩 CoinDesk)
    - [binance-api-geo.md](references/binance-api-geo.md) — Binance 端点区域问题
    - [binance-endpoint-flapping.md](references/binance-endpoint-flapping.md) — 端点双向故障切换
    - [high-latency-network-handling.md](references/high-latency-network-handling.md) — 高延迟网络处理 (SSL 超时)
    
    > Binance 三种故障模式各一份(431/451 区域、隧道抖动、高延迟 SSL 超时),**不是重复文档**,见 [setup-from-scratch.md §6](references/setup-from-scratch.md#6-网络地区修复geo)。
    
    ### 引擎专项
    - [cron-llm-provider-failure.md](references/cron-llm-provider-failure.md) — Cron LLM Provider 失效诊断 (含 job 列表 §8) + 版本同步规则
    
    ### 回测 & 复盘
    - [backtest-findings.md](references/backtest-findings.md) — 365 天回测深度复盘 + 因子无预测力
    - [backtest-v58-1min-findings.md](references/backtest-v58-1min-findings.md) — v5.8 真实 1min 半 K线回测
    - [review-procedure.md](references/review-procedure.md) — 每日复盘流程 + 数据质量检查
    
    ### 数据采集 & 仓库
    - [sync-procedure.md](references/sync-procedure.md) — AGI-Super-Team 同步流程 (路径映射 + `-c` 校验 + logs 归档策略)
    - [archive.md](references/archive.md) — **版本归档: v5.0→v6.0 沿革 + 哪些版本已不可取回 + 归档 SOP**
    - [setup-from-scratch.md](references/setup-from-scratch.md) — **从零搭建: 依赖/目录/密钥/6 个 launchd 服务/验证清单/日志轮转/故障表**
    - [scheduled-tasks.md](references/scheduled-tasks.md) — **系统级定时任务教程: 常驻型vs定时型 / 2 个任务 / launchd+cc-connect+hermes 三路线 / plist 模板 / 13 条坑**
    - [quant-knowledge-index.md](references/quant-knowledge-index.md) — 50 轮蒸馏知识库索引
    
    ## 复盘记录
    
    ⚠️ **`reviews/` 目录本地不存在** —— 早期文件树声称有 23 份按月归档的复盘记录(2026-05/06/07),实际从未落盘。
    复盘结论目前散落在 [lessons.md](references/lessons.md)、[pitfalls.md](references/pitfalls.md)、
    [strategy-adversarial-review.md](references/strategy-adversarial-review.md) 与 `reports/` 里。
    若要恢复这个习惯,需先补 `review-procedure.md` 的落盘步骤,别只在文档里声明。
    
    ## 报告库
    14 份深度蒸馏报告在 `reports/` 目录 (也同步在 AGI-Super-Team): R01-R14。
    
    ## 与 coin-vp-scanner 的分工
    
    两个都是交易类 skill, **但赌的不是同一件事, 不要混用**:
    
    | | **5minbtc** (本 skill) | [coin-vp-scanner](../coin-vp-scanner/SKILL.md) |
    |---|---|---|
    | 标的 | **单标的 BTC** | 全量币安 USDT 永续 |
    | 周期 | 单根 5min K线 | 1–30min 短线 |
    | 核心逻辑 | **真 OFI 净流 + 错价检测** (净流已发生、token 价未定价 → flow-gap) | **确定性结构突破** (放量 + 强实体 + 收盘破 1h 结构位 + 趋势同向, 四条全 ✓) |
    | 决策依据 | 概率 `P(close>open\|ofi)` vs 市场 token 价 → `EV = p − P` | 结构规则布尔判定, 非概率 |
    | 输出 | 方向 bull/bear + 概率 + edge | 杠杆 / TP / SL 交易卡 |
    | 落地方式 | 预测市场 Up/Down token, **仅 paper** (`LIVE_GATE` 硬闸门) | 直接做合约短线 |
    | 代码位置 | 本目录自包含 (引擎+脚本+launchd) | 代码在 `~/projects/coin-vp-scanner/`, 本 skill 只是索引 |
    
    **一句话选型**: 要赌"市场定价错了" → 5minbtc; 要抓"价格放量破位了" → coin-vp-scanner。
    
    ## 仓库同步
    详见 [sync-procedure.md](references/sync-procedure.md)。简述:
    ```bash
    rsync -av --exclude='data/' --exclude='__pycache__/' \
      --exclude='*.jsonl' --exclude='*.jsonl.*' --exclude='*.gz' \
      /home/aa/.hermes/profiles/cqo/skills/5minbtc/ \
      /home/aa/clawd/repos/AGI-Super-Team/skills/5minbtc/
    cd /home/aa/clawd/repos/AGI-Super-Team
    git add skills/5minbtc/ && git commit -m "sync(skills/5minbtc): <版本>" && git push origin main
    ```
    
    ## 回测系统
    ```
    backtest/
    ├── fetch_data.py            # Binance 历史数据下载
    ├── run_backtest.py          # v5.6 回测 (因子无预测力, 公平回测)
    ├── run_backtest_v57.py      # v5.7 回测 (含前视偏差, 已知)
    ├── run_backtest_v58.py      # v5.8 回测 (真实 1min 半 K线, 零前视) ← 推荐
    ├── run.sh                   # 一键运行
    ├── data/                    # 5min (105K) + 1min (259K) K线
    └── results/                 # 回测结果 (gitignore)
    ```
    
    ## 文件结构
    ```
    5minbtc/
    ├── SKILL.md                      # 本文件 (~170 行 INDEX)
    ├── 5minbtc-engine-v6.0.py        # 主引擎 (v6.0 真OFI 一票定方向, launchd 调用)
    ├── 5minbtc-news.py               # 新闻扫描 (CoinDesk RSS, 唯一稳定源)
    ├── 5minbtc-log.py                # 日志记录 (写入 logs/)
    ├── logs/                         # 日志 (当月 live + 历史按月压缩)
    │   ├── 5minbtc-log.jsonl         # 当月 live (不入库)
    │   └── archive/*.jsonl.gz        # 月度压缩归档 (入库, 见 archive.md)
    ├── references/                   # 24 份专项 ref (含 skill-organization 模式)
    ├── backtest/                     # 回测系统 (results/ 不入库)
    ├── data/                         # 运行时 (news-risk-level.json 等)
    ├── scripts/                      # 复盘/监控/交易脚本
    │   ├── 5minbtc-monitor.py        # ★ 监控脚本 (Claude Code Monitor 集成, v1.0)
    │   ├── 5minbtc_watch.py          # ★ Telegram 推送监控 daemon (事件驱动+预测记录+收盘结算)
    │   ├── 5minbtc_day_stats.py      # 预测战绩查询 (今日/历史, --push 推送)
    │   ├── 5minbtc_trader.py         # ★ 币安预测交易桥接 (--once/--loop/--monitor/--paper/--paper-monitor)
    │   ├── prediction_ws_feed.py     # 币安 w3w-prediction WS 实时价源 (<200ms)
    │   ├── ofi_feed.py               # ★ 真订单流采集 (trade+bookTicker, launchd com.daniel.ofi-feed)
    │   ├── 5minbtc_keyless_paper.py  # 免密钥模拟盘 (公开BTC数据模拟UP/DOWN价)
    │   ├── telegram_push.py          # 通用 Telegram 推送助手
    │   ├── daily-review-stats.py
    │   └── fetch-github-repo.sh
    ```
    
    > ⚠️ **`archive/` 目录与 `reviews/` 目录本地并不存在** —— 本文件早期版本的文件树里声称有它们(还列了 23 份复盘),属于文档/现实漂移,已更正为上面的实际结构:
    > - 旧版本引擎源码**不保留在本地**,只留文字归档 → [references/archive.md](references/archive.md)
    > - 每日复盘从未落盘到 `reviews/`,复盘结论散在 `references/lessons.md` / `pitfalls.md` 与 `reports/`
    
    
    ---
    最后更新: 2026-09-10 — 引擎 v5.7→**v6.0** 改名 (文件/SKILL/引用全线对齐, 修引擎内"无中性"过期注释) + SKILL.md 版本刷到 6.0.0 + 性能快照改为审查后的诚实口径 + 补 v5.9/v5.10/v6.0 changelog + 新增与 coin-vp-scanner 的分工说明 + **修文件树未闭合的代码块与 `archive/`/`reviews/` 假目录声明** + 新增 [archive.md](references/archive.md) 与 [setup-from-scratch.md](references/setup-from-scratch.md) + logs 月度压缩归档入库
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related