# Freqtrade 币安永续量化 — 部署与迁移文档

> 首次部署：2026-07-07，腾讯云利雅得 2C4G Ubuntu 22.04（43.164.75.52）
> 本文档记录从零到可用的全部步骤，迁移到新机器时按此复现。

## 1. 架构总览

```
浏览器 → https://bot.dylanwuzc.uk（Bot Observatory 多 Bot 控制台）
       → Cloudflare Access（邮箱 OTP，允许名单见 §6.2）
       → Cloudflare Tunnel（cloudflared 容器，纯出站连接，服务器零入站端口）
       → bot-observatory:3000（Bun BFF + React）
       → freqtrade:8080（服务端 API 聚合）

bot1 是 RiskCap D48 dry-run，bot2（D36）与 bot3（EntryOther）均停止。RiskCap 保留
D48 全部信号与退出，只将定仓风险分母封顶到实际 5% 硬止损；2026-07-23 用户明确
要求其替换原 D48 定仓实现。EntryOther（`price_side=other`）于 2026-07-28 随漏单
机会成本裁决结案否决并停机，数据库 `tradesv3-entry-other-forward.sqlite` 与日志
只读保留、不伪造平仓；入场执行维持 `price_side=same`。普通
`docker compose up -d` 不会新建研究容器，启动研究 bot 需
`docker compose --profile research up -d freqtrade3`。
当前远端只有一个交易策略实例：RiskCap D48 本身是“波动率突破 + 趋势过滤”，不是
波动率策略与趋势策略两个并行实例。
主机另运行独立 Compose 部署的 `bot-observatory` 与 `cloudflared`。
Bot Observatory 已启用 dry-run 的
逐次确认启动/暂停；live 管理、取消订单与强制退出仍由服务端拒绝。旧 FreqUI 仍在各
Freqtrade 容器内，但不再是正式域名入口。
```

## 2. 服务器要求

- **区域**：必须避开币安 API 屏蔽区（美国、加拿大、荷兰、马来西亚等）。
  验证方法：`curl -s -m 10 https://fapi.binance.com/fapi/v1/ping` 返回 `{}` 即可。
- **配置**：2C4G 足够（bot 运行约 500MB；重度 hyperopt/FreqAI 建议本地跑或升配）。
- **软件**：Docker + Docker Compose v2。
- **磁盘**：建议 40GB+（历史 K 线 + 镜像 + 日志）。
- **当前机器**：`43.164.75.52`，SSH key 已配置，使用 `ubuntu@43.164.75.52`
  登录；远端项目文件在 `/data` 下，当前 Freqtrade 路径为 `/data/freqtrade`。

## 3. 项目结构

```
/data/freqtrade/
├── docker-compose.yml     # 默认 bot1 + cloudflared；bot2/bot3 属于 research profile
├── .env                   # CF_TUNNEL_TOKEN=<tunnel token>，权限 600，勿入 git
├── docs/                  # 本文档
├── scripts/
│   ├── refresh_top_pairs_backtest.py  # 拉最新活跃交易对、补数据、回测并筛白名单
│   │                                  # 内置 5m + fee 0.00035 + cache none 标准协议
│   └── run_black_swan_stress.py       # 历史极端行情窗口，比较 D48/D36 与熔断开关
│   # repo 侧另有一组只读审计脚本（不部署到服务器，用 ssh 管道送进去执行）：
│   #   纯 stdlib，宿主机直跑：slippage_audit.py / forward_compare.py /
│   #     entry_pricing_compare.py
│   #   需 pandas，必须进容器：same_side_opp_cost.py /
│   #     unfilled_entry_opp_cost.py / liquidity_capacity.py
│   #     用法 ssh ... 'docker exec -i freqtrade python3 -' < 脚本
│   #     （宿主机没有 pandas；容器内 user_data 挂在 /freqtrade/user_data）
└── user_data/
    ├── config.json        # bot1 配置（含 FreqUI 密码，勿入公开仓库）
    ├── config-riskcap-forward.json  # 已退役 RiskCap bot3 配置（勿入公开仓库）
    ├── research-entry-other.overlay.json  # 已结束 EntryOther 实验配置，历史保留
    ├── config2.json       # bot2 旧配置（当前 D36 bot2 改为共用 config.json）
    ├── config3.json       # 已退役 bot3 历史配置，不再由 Compose 使用
    ├── strategies/        # 策略族目录；线上 compose 用独立 --strategy-path
    │   ├── volatility_breakout/
    │   │   ├── v5_d48.py       # RiskCap 继承的 D48 信号/退出基类
    │   │   ├── v5_d36.py       # bot2 D36 候选；class VolatilityBreakoutD36
    │   │   ├── risk_cap_math.py
    │   │   ├── entry_block_observability.py  # 限仓因果快照与置换 shadow 纯函数
    │   │   └── v5_d48_riskcap.py  # bot1 生产 dry-run；class VolatilityBreakoutRiskCap
    │   ├── research/
    │   │   └── long_1h.py            # 1h 多头待命变体（repo 版含 funding 过滤，启动前需同步）
    │   ├── HighVolMeanRev.py        # 旧 bot2，已停用
    │   ├── RisingVolTrend.py        # bot3，已停用
    │   └── SmartMoneySwing.py       # 聪明钱归纳（回测未通过）
    ├── regime_check.py    # 市场状态评分器（cron 每小时，见 docs/market-regime.md）
    ├── regime.json        # regime_check 输出快照（gitignore）
    ├── regime_history.jsonl  # regime 每小时历史，供日后回放验证（gitignore）
    ├── data/              # 历史 K 线 + funding rate（download-data 生成）
    ├── logs/              # freqtrade.log / freqtrade-d36-forward.log / regime.log 等
    ├── tradesv3-v5.sqlite      # bot1 dry-run 交易记录（V5 逻辑，2026-07-08 起）
    ├── tradesv3.sqlite         # bot1 旧记录（V1，已下线，留作对比）
    ├── tradesv3-d36-forward.sqlite  # bot2 D36 前向 A/B（2026-07-10 起）
    ├── tradesv3-entry-other-forward.sqlite  # 已结束 bot3；历史库保留
    ├── tradesv3-riskcap-active-forward.sqlite  # 已退役 RiskCap bot3 历史记录
    ├── tradesv3-bot2.sqlite    # 旧 bot2，保留历史
    └── tradesv3-grok-combined-d48-forward.sqlite  # 已退役 bot3 历史前向记录
```

> **多 bot 说明**：bot2 曾与 bot1 共用同一个 `user_data` 卷和 `config.json` 做
> 并行 dry-run A/B；策略、数据库和日志必须独立。bot2 已停止。旧 Grok、RiskCap
> 活跃池和 EntryOther bot3 的历史数据库与日志保留，bot3 当前停止。运行状态以
> docs/strategy-iterations.md 速览为准。
> 2026-07-17 提议复用 bot2 做「12 对 + SUI」扩池前向，但 SUI 在补齐历史数据后的
> 验证段 PF 仅 0.77，SUI-only 两段均负，已在离线门槛否决；没有部署 overlay、没有
> 创建新前向数据库，bot2 继续保持停止。结果仅归档在
> `user_data/backtest_results/sui-expansion/`。
> 同一杠杆账户实盘时不能这样，多策略实盘需交易所子账户，见 §8。bot1（宿主机
> 8080）经 CF 隧道对外；bot2 的旧公网路由不代表容器正在运行。

### docker-compose.yml

完整文件见仓库 `freqtrade/docker-compose.yml`。结构：3 个 freqtrade 服务
（freqtrade / freqtrade2 / freqtrade3，端口 8080/8081/8082 各绑本机）+ 1 个
cloudflared。bot2/bot3 均在 `research` profile，当前都停止。
关键骨架：

```yaml
services:
  freqtrade:            # bot1，经 CF 隧道对外
    image: freqtradeorg/freqtrade:stable
    restart: unless-stopped
    container_name: freqtrade
    volumes:
      - "./user_data:/freqtrade/user_data"
    ports:
      - "127.0.0.1:8080:8080"   # 只绑本机，公网走 CF Tunnel
    command: >
      trade
      --logfile /freqtrade/user_data/logs/freqtrade.log
      --db-url sqlite:////freqtrade/user_data/tradesv3-v5.sqlite
      --config /freqtrade/user_data/config.json
      --strategy-path /freqtrade/user_data/strategies/volatility_breakout
      --strategy VolatilityBreakoutRiskCap

  # freqtrade2: research profile，默认不启动；8081 → 容器 8080
  # strategy-path=/freqtrade/user_data/strategies/volatility_breakout
  # strategy=VolatilityBreakoutD36
  # db=tradesv3-d36-forward.sqlite, log=freqtrade-d36-forward.log

  # freqtrade3: research profile；历史 EntryOther 定义保留但当前不启动
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    container_name: cloudflared
    command: tunnel run --token ${CF_TUNNEL_TOKEN}
    depends_on:
      - freqtrade
```

默认 Compose 集只包含 `freqtrade` + `cloudflared`。研究 bot 必须加入 `research` profile；
加 bot 时复制服务块、
递增宿主机端口并使用独立 strategy/db/log。若希望 FreqUI 账号密码相同，可共用
`config.json`；交易数据库绝不能共用。

策略按“策略族目录 + 版本文件”管理，而不是把所有实验脚本堆在 `strategies/` 顶层。
线上目录只保留基准和正在前向验证的候选；被否决实验的参数与结果记录在
`docs/strategy-iterations.md`，通过 Git 历史复现。派生版本单独成文件，避免 Freqtrade
hyperopt 参数文件发生类名/文件冲突。

### config.json 关键项（完整文件见 user_data/config.json）

```json
{
    "trading_mode": "futures",
    "margin_mode": "isolated",
    "dry_run": true,
    "dry_run_wallet": 1000,
    "timeframe": "5m",
    "order_types": {
        "entry": "limit",
        "exit": "limit",
        "emergency_exit": "market",
        "force_entry": "limit",
        "force_exit": "market",
        "stoploss": "market",
        "stoploss_on_exchange": true,
        "stoploss_on_exchange_interval": 60,
        "stoploss_price_type": "last"
    },
    "exchange": {
        "name": "binance",
        "pair_whitelist": [
            "BTC/USDT:USDT", "ETH/USDT:USDT", "SOL/USDT:USDT", "BNB/USDT:USDT",
            "XRP/USDT:USDT", "DOGE/USDT:USDT", "ADA/USDT:USDT", "ZEC/USDT:USDT",
            "EDGE/USDT:USDT", "POWER/USDT:USDT", "VANRY/USDT:USDT", "UNI/USDT:USDT"
        ]
    },
    "entry_pricing":  { "use_order_book": true },
    "exit_pricing":   { "use_order_book": true },
    "api_server": {
        "enabled": true,
        "listen_ip_address": "0.0.0.0",
        "listen_port": 8080,
        "username": "freqtrader"
    }
}
```

注意：币安期货没有 ticker，`entry_pricing`/`exit_pricing` 必须 `use_order_book: true`。

### 黑天鹅执行保护

- Binance USDT-M isolated futures 支持交易所端 stop-market。成交开仓后由交易所持有条件单，
  不依赖机器人下一次 5 秒轮询或服务器到 Binance 的网络连接。
- `stoploss_price_type: "last"` 使用合约最新成交价触发，优先及时退出；代价是极端插针也可能
  触发。`market` 只保证尽快按当时最优流动性成交，不保证最终成交价等于触发价。
- 交易所止损创建失败时，`emergency_exit: "market"` 是兜底。移动止损启用后，机器人最多
  每 60 秒更新一次交易所端止损，避免触发 API 限频。
- 策略另有 `StoplossGuard`：5m 周期内，2 小时出现两次亏损止损，就全币种、全方向暂停新开仓
  1 小时。它只阻止新仓，不影响已有仓位退出。
- 当前是 dry-run，Freqtrade 只模拟这些订单，不能证明真实 Binance 条件单已经创建。切换实盘时
  必须先用小资金开一笔，登录 Binance 核对对应的 stop-market 条件单、触发方向和数量，再扩大资金。

### 同向限仓观测与 replacement shadow

生产 `max_same_side_open_trades=2` 保持不变。`entry_blocked` 日志在原有
`reason/pair/side/occupied` 后追加三个无空格 JSON 字段：

- `candidate`：决策时已分析 K 线的 close、Donchian 边界、突破绝对距离、ATR 归一化
  突破距离、ADX 与 ATR；
- `occupied_state`：每个占槽仓位的持仓小时数、当前 mark、当前收益率及
  `current_R = current_profit_ratio / abs(initial_stoploss)`；
- `replacement_shadow`：冻结规则的 `eligible/reason/weakest`，只做影子判定。

冻结 shadow 规则：候选 `breakout_atr >= 0.10 && ADX >= 30`，且当前 R 最低的占槽仓
`current_R <= 0`。即使 `eligible=true`，生产仍拒绝候选订单，也不退出原仓。汇总命令：

```bash
python3 scripts/same_side_replacement_shadow.py \
  user_data/logs/freqtrade.log.1 user_data/logs/freqtrade.log \
  --since 2026-08-03T00:00:00Z
```

依据：Freqtrade 官方的 [Stoploss](https://www.freqtrade.io/en/stable/stoploss/)、
[Binance 交易所说明](https://www.freqtrade.io/en/stable/exchanges/#binance) 与
[Protections](https://www.freqtrade.io/en/stable/plugins/#protections)。

## 4. 部署步骤（新机器）

```bash
# 1. 建目录并上传项目文件（从旧机器打包迁移最简单）
ssh <新机器> 'sudo mkdir -p /data/freqtrade && sudo chown $USER /data/freqtrade'
# 旧机器: tar czf freqtrade-backup.tgz -C /data freqtrade（含 user_data、docs、compose、.env）
# 新机器解包后即恢复全部状态（含 dry-run 交易库和已下载数据）

# 2. 验证币安连通性
curl -s -m 10 https://fapi.binance.com/fapi/v1/ping   # 期望 {}

# 3. 启动
cd /data/freqtrade && docker compose up -d freqtrade cloudflared

# 4. 验证
docker compose ps                                  # 只应看到 freqtrade + cloudflared 为 Up
tail -f user_data/logs/freqtrade.log               # 出现 Bot heartbeat
docker logs cloudflared | tail                     # precheck pass / Registered tunnel
curl -sI https://bot.dylanwuzc.uk | head -3        # 302 → cloudflareaccess.com
curl -sI https://bot-2.dylanwuzc.uk | head -3      # 同样应为 Access 302
```

## 5. 常用命令（都在 /data/freqtrade 下）

```bash
# 下载/更新历史数据（5m+1h K线 + funding rate）
docker compose run --rm freqtrade download-data \
  --config user_data/config.json --timerange 20260101- --timeframes 5m 1h

# 回测单个策略
docker compose run --rm freqtrade backtesting \
  --config user_data/config.json \
  --strategy-path user_data/strategies/volatility_breakout \
  --strategy VolatilityBreakout --timerange 20260101-20260701 \
  --timeframe 5m --fee 0.00035 --cache none

# 多策略对比（可插拔核心用法）
docker compose run --rm freqtrade backtesting \
  --config user_data/config.json \
  --strategy-path user_data/strategies/volatility_breakout \
  --strategy-list VolatilityBreakout VolatilityBreakoutD36 \
  --timeframe 5m --fee 0.00035 --cache none

# hyperopt 参数优化
docker compose run --rm freqtrade hyperopt \
  --config user_data/config.json \
  --strategy-path user_data/strategies/volatility_breakout \
  --strategy VolatilityBreakout \
  --hyperopt-loss SharpeHyperOptLoss -e 100

# 拉最新 USDT-M 活跃交易对，增量补 60 天 5m/1d 数据，回测并输出推荐白名单
python3 scripts/refresh_top_pairs_backtest.py --days 60 --max-candidates 30

# 只读查看候选池/现役币流动性；forced 现役币低于 1 亿时会明确告警
python3 scripts/refresh_top_pairs_backtest.py --max-candidates 30 \
  --include BTC,ETH,SOL,BNB,XRP,DOGE,ADA,ZEC,EDGE,POWER,VANRY,UNI \
  --candidates-only

# 将脚本筛出的白名单写入正式 config.json，并重启 bot1（确认回测结果后再用）
python3 scripts/refresh_top_pairs_backtest.py --days 60 --max-candidates 30 --apply --restart

# 日常运维
docker compose logs -f freqtrade      # 看主 bot 日志
docker compose restart freqtrade      # 改策略/配置后重启
docker compose pull && docker compose up -d freqtrade cloudflared  # 只升级必要服务

# 研究前向 bot 默认停止；只有明确批准新一轮前向验证后才显式启动
docker compose --profile research up -d freqtrade2  # 不要在日常启动/升级命令中使用
docker compose stop freqtrade2
docker compose --profile research up -d freqtrade3  # 仅在新研究实验获批后使用
docker compose stop freqtrade3

# 实盘核验容器（live profile，日常 up -d 不会启动它）
docker compose --profile live up -d freqtrade-live
docker compose stop freqtrade-live
```

> ⚠️ `freqtrade-live` 是**真钱**容器：key 从 `.env` 注入、端口只绑本机 8083、
> 不接 Cloudflare 隧道、`force_entry_enable=false`。停容器**不会**撤销交易所上的
> 挂单，停机后必须手工确认挂单与持仓，流程见 `docs/live-verification-runbook.md` §3.1。

### 生产主机资源纪律

- 常驻服务为 `freqtrade` 与 `cloudflared`；bot2、bot3 均为已停止的研究 bot。旧
  Grok、RiskCap 活跃池与 EntryOther 配置、数据库和日志只读保留。
- 唯一保留的宿主机定时任务是每小时 `regime_check.py`，它为主策略的实时开多闸门
  生成 `regime.json`。
- 不在这台 2C4G 主机运行并行 backtesting、hyperopt、download-data 或 FreqAI 任务。
  离线研究使用本地/独立机器；临时 `freqtrade-freqtrade-run-*` 容器不得常驻。
- 日常执行 `docker compose up -d` 时，`research` profile 默认不激活；不要使用
  `--profile research`，除非已经明确批准一次隔离前向验证。

### 市场状态评分器（cron，非 Docker）

`user_data/regime_check.py` 由宿主机 cron 每小时跑一次，不在 compose 里。安装：

```bash
( crontab -l 2>/dev/null; \
  echo "0 * * * * cd /data/freqtrade/user_data && /usr/bin/python3 regime_check.py >> /data/freqtrade/user_data/logs/regime.log 2>&1" \
) | crontab -
```

指标与评分见 [market-regime.md](market-regime.md)。纯 stdlib，无需装依赖。

## 6. Cloudflare 配置（一次性，已完成；换域名/重建时参考）

账号：dylanwuzc@gmail.com；域名：dylanwuzc.uk；Zero Trust team：wild-violet-4135。
控制台：https://one.dash.cloudflare.com

### 6.1 Tunnel（Networks → Tunnels / 连接器）

1. 创建隧道 → 类型选 **Cloudflared** → 命名 `freqtrade` → 保存
2. 复制安装命令里的 **token**（`eyJ...`），写入服务器 `/data/freqtrade/.env`：
   `CF_TUNNEL_TOKEN=<token>`，`chmod 600 .env`
3. 隧道的 **Public Hostname** 添加两条：
   - `bot.dylanwuzc.uk` → `http://bot-observatory:3000`
   - `bot-2.dylanwuzc.uk` → `http://freqtrade2:8080`
   两个 URL 都使用 compose 服务名和容器内端口，不使用宿主机 8080/8081。
4. 服务器 `docker compose up -d freqtrade cloudflared` 后，控制台连接器状态
   应为“已连接/正常”。

> 迁移到新机器：token 不变的话直接把 .env 带过去即可，旧机器容器停掉后
> 新机器的 cloudflared 会自动接管同一隧道；也可以新建隧道换新 token。

### 6.2 Access 应用（访问控制 → 应用程序）

1. 新建应用 → **自托管 (Self-hosted)**
2. 分别为 `bot.dylanwuzc.uk`、`bot-2.dylanwuzc.uk` 创建 Self-hosted 应用，公共
   主机名与 Tunnel 的 Public Hostname 一致。
3. 登录方式：启用 **One-time PIN**，应用的 `allowed_idps` 只绑定 One-time PIN。
   不要使用 Cloudflare 登录方式，否则非账号成员会看到
   `Cloudflare sign-in is restricted to members of the account`，且邮箱 allow policy
   不会被执行。
4. **策略**（重要：新版界面策略要在构建器里单独保存，注意确认策略列表里真的出现了）：
   - 操作 **允许**，规则：选择器 **电子邮件** = `dylanwuzc@gmail.com` 或
     `sharewayer@gmail.com`
   - 需要共享访问时，把对方邮箱加入同一 Allow policy，不需要邀请成 Cloudflare 账号成员
   - 无策略时 Access 默认拒绝所有人
5. 会话持续时间：24 小时
6. 两个域名还各有一个更具体的静态资源应用：`<hostname>/assets/*`，Policy 为
   Bypass + Include Everyone。它只放行控制台的版本化 JS/CSS，页面和
   `/api/*` 仍必须邮箱认证。

Cloudflare 邮箱认证通过后直接进入 Bot Observatory。Freqtrade API 用户名和密码仅由
Bun BFF 在服务端使用，不发送到浏览器。

### 6.3 验证

```bash
curl -sI https://bot.dylanwuzc.uk | head -3
curl -sI https://bot-2.dylanwuzc.uk | head -3
# HTTP/2 302 + location: https://wild-violet-4135.cloudflareaccess.com/... 即正确

curl -s -o /dev/null -w '%{http_code}\n' \
  https://bot-2.dylanwuzc.uk/assets/index-CXSzRURc.js
# 静态资源 bypass 正常时返回 200（文件 hash 随 FreqUI 版本变化）
```

### 6.4 Access 无限重定向排查

如果浏览器完成邮箱 OTP 后又回到 Cloudflare Access 登录页，先区分是 Access
cookie/策略问题，还是后端服务问题：

```bash
# 未登录访问公网域名应返回 Access 302
curl -I https://bot.dylanwuzc.uk

# 在服务器本机验证新控制台与健康探针
ssh ubuntu@43.164.75.52 'curl -sI http://127.0.0.1:3000/ | head; curl -fsS http://127.0.0.1:3000/healthz'

# 观察 tunnel 是否稳定
ssh ubuntu@43.164.75.52 'cd /data/freqtrade && docker compose ps && docker logs --tail 80 cloudflared'
```

当前配置下，公网未登录返回 `302 -> wild-violet-4135.cloudflareaccess.com` 是正常的；
服务器本机 `http://127.0.0.1:3000` 能返回控制台 HTML 时，说明后端没有把请求重定向回
Access。此时优先检查 Cloudflare 控制台：

- Zero Trust → Access → Applications：每个 bot 主机名只能有一个主应用匹配；
  Public hostname 精确为 `bot.dylanwuzc.uk` 或 `bot-2.dylanwuzc.uk`，策略为 Allow，
  Include email 使用上面的两个允许邮箱；登录方式只绑定 One-time PIN。
- Zero Trust → Networks → Tunnels → `freqtrade` → Public Hostname：
  主域名 service 必须是 `http://bot-observatory:3000`，bot2 必须是
  `http://freqtrade2:8080`；不要填公网域名、`cloudflareaccess.com` 或 https 回源。
- Cloudflare 站点规则：不要有 Redirect/Page/Transform Rule 把 `bot.dylanwuzc.uk`
  重定向到自身、`/cdn-cgi/access/*` 或其它受 Access 保护的地址。
- 控制台的 Vite 静态资源标签带 `crossorigin`；如果浏览器控制台看到
  `/assets/*.js` 或 `/assets/*.css` 被重定向到 `cloudflareaccess.com` 后报 CORS，
  检查对应域名是否已有更具体的 `<hostname>/assets/*` self-hosted application，
  policy 为 Bypass + Include Everyone。这样只放行静态资源，`/trade` 和 `/api/*`
  仍由主应用保护。
- 浏览器：清理 `bot.dylanwuzc.uk` 和 `wild-violet-4135.cloudflareaccess.com` 的站点数据，
  允许 cookie，临时关闭广告/隐私插件后重试。认证成功后浏览器应在 `bot.dylanwuzc.uk`
  下出现 `CF_Authorization` cookie；如果没有，通常就是 cookie 被拦截或域名不匹配。

### 6.5 其它项目复用：Access + One-time PIN 模板

后续其它内网站点也按这个模式配置：Cloudflare Tunnel 负责回源，Access 只做邮箱 OTP
认证；共享访问只加邮箱到 Allow policy，不邀请对方成为 Cloudflare 账号成员。

控制台步骤：

1. Zero Trust → Settings → Authentication → Login methods：
   确认存在 **One-time PIN**；如果没有就新增。
2. Zero Trust → Networks → Tunnels：
   在对应 tunnel 下新增 Public Hostname。
   - Hostname：`<subdomain>.<domain>`，例如 `admin.example.com`
   - Service：填内网服务地址，例如 `http://service-name:8080`
   - 不要填同一个公网 hostname，否则会形成回源/认证环路
3. Zero Trust → Access → Applications：
   新建 Self-hosted 应用，Public hostname 与 Tunnel hostname 一致。
4. 应用登录方式只选择 **One-time PIN**。不要选择 **Cloudflare** 登录方式；
   否则用户会看到 `Cloudflare sign-in is restricted to members of the account`。
5. Policy 用 Allow + Email：
   - Include email：项目 owner 邮箱
   - 需要共享访问时继续加对方邮箱
   - Require/Exclude 默认留空，除非明确需要设备姿态、国家、IP 等条件
6. Session duration 按项目需要设置，普通内部工具用 `24h` 即可。

API 修复/复现步骤（只在控制台不方便时使用）：

```bash
# 1. 创建短期 API token，至少需要 Zero Trust Access 编辑权限。
#    不要把 token 写入仓库或聊天记录。
printf '%s' '<temporary-cloudflare-api-token>' > /tmp/cf_api_token
chmod 600 /tmp/cf_api_token

ACCOUNT_ID='<cloudflare-account-id>'
APP_ID='<access-app-id>'
HOSTNAME='<protected-hostname>'

# 2. 查看当前身份提供商。正常应至少有 type=onetimepin。
curl -sS \
  -H "Authorization: Bearer $(cat /tmp/cf_api_token)" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/identity_providers?per_page=100" \
  | jq '.result[] | {id,name,type}'

# 3. 如果没有 One-time PIN，则创建。
OTP_ID="$(curl -sS -X POST \
  -H "Authorization: Bearer $(cat /tmp/cf_api_token)" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/identity_providers" \
  --data '{"name":"One-time PIN","type":"onetimepin","config":{}}' \
  | jq -r '.result.id')"

# 如果第 2 步已经存在 One-time PIN，直接手动设置 OTP_ID 为那个 id：
# OTP_ID='<existing-onetimepin-id>'

# 4. 用完整 PUT 更新 Access app，仅把 allowed_idps 限定为 One-time PIN。
curl -sS \
  -H "Authorization: Bearer $(cat /tmp/cf_api_token)" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/apps/${APP_ID}" \
  | jq --arg otp_id "$OTP_ID" '.result
      | .allowed_idps=[$otp_id]
      | del(.created_at,.updated_at,.aud,.uid)' \
  > /tmp/cf_access_app_payload.json

curl -sS -X PUT \
  -H "Authorization: Bearer $(cat /tmp/cf_api_token)" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/apps/${APP_ID}" \
  --data @/tmp/cf_access_app_payload.json \
  | jq '{success, errors, result: {name: .result.name, domain: .result.domain, allowed_idps: .result.allowed_idps}}'

# 5. 验证入口会跳到 Access 登录页，登录页应出现 Email/验证码入口。
curl -sI "https://${HOSTNAME}" | head

# 6. 收尾：删除本地 token 文件，并到 Cloudflare 后台 revoke 临时 token。
rm -f /tmp/cf_api_token /tmp/cf_access_app_payload.json
```

### 6.6 Bot Observatory 更新与逐页验收

Observatory 使用独立 Compose 文件，不要用父目录的默认 `docker compose up -d` 代替：

```bash
cd /data/freqtrade/bot-observatory
docker compose -f compose.example.yml --env-file .env.observatory up -d --build
docker compose -f compose.example.yml --env-file .env.observatory ps
curl -fsS http://127.0.0.1:3000/healthz
```

> ⚠️ **`--env-file .env.observatory` 不能省。** `compose.example.yml` 里
> `FREQTRADE_USERNAME`/`FREQTRADE_PASSWORD` 是 `${VAR}` 插值，而 Compose 默认只读
> 同目录的 `.env`——该目录没有 `.env`，省掉这个参数会让两个变量静默解析成**空字符串**，
> 容器照常 `healthy`、`/healthz` 返回 ok，但对 Freqtrade 的请求全部 401，
> 前端表现为**所有 bot 显示离线、总权益"不可用"**，很容易误判成 bot 挂了。
> 2026-07-28 因漏这个参数造成过一次；排查口径：
>
> ```bash
> # 1) bot 真的在跑吗（容器与 API 都要看）
> docker ps --format "{{.Names}}|{{.Status}}"
> docker exec -i freqtrade python3 -c "import urllib.request;print(urllib.request.urlopen('http://freqtrade:8080/api/v1/ping',timeout=5).read())"
> # 2) Observatory 侧的真实错误码（401=凭据，network_error=容器没起）
> docker exec -i freqtrade python3 -c "import urllib.request,json;print(json.loads(urllib.request.urlopen('http://bot-observatory:3000/api/bots',timeout=10).read())['bots'][0]['error'])"
> # 3) 凭据是否为空（只看长度，不打印值）
> docker exec bot-observatory sh -c 'echo ${#FREQTRADE_USERNAME} ${#FREQTRADE_PASSWORD}'
> ```
>
> 凭据的唯一真源是 `.env.observatory`（权限 600），值须与 `user_data/config.json`
> 的 `api_server.username/password` 一致。**不要另建 `.env`**——两个 env 文件会让
> 下次排查更难。

2026-07-13 的生产验收覆盖机器人总览、挂单管理及详情、多图工作区、交易筛选器、
市场指标、市场事件、风险与策略、健康状态和策略源码抽屉。桌面 1440px 与移动 390px 均无页面级
横向溢出，移动端 8 个导航入口全部位于视口内；浏览器控制台、失败请求及 HTTP 4xx/5xx 为零。
真实 Freqtrade K 线、持仓、止损订单、策略指标、Binance 资金费率，以及 SEC、CFTC、Fed、
CoinDesk 五个事件来源均已抽样核对。事件源单独失败不得影响 Bot 页面。

Fleet 首屏已按页面拆包，不应请求 ECharts、`lightweight-charts`、挂单工作台或交易筛选器代码；
生产构建主脚本为约 285 kB（优化前约 437 kB）。带哈希资源应返回一年 `immutable`，Cloudflare
应提供 Brotli 压缩并在重复请求时显示缓存命中。每次更新仍应重复这些检查，不能只以容器
`healthy` 作为前端验收完成。

K 线页还应执行一次同标签页刷新验收：首次加载成功后刷新浏览器，应立即恢复该
Bot/交易对/周期最近 30 分钟内的真实 K 线，并显示“已恢复缓存 · 后台更新”，不能重新退回
整块“正在加载 K 线”。切换到未缓存的交易对仍显示同形骨架，接口失败时保留最后一次可用图表
并明确显示失败原因。缓存只用于改善显示，后台仍会向 BFF 重新验证，不能把缓存时间冒充采集时间。

订单详情与策略图还应执行定时刷新视窗验收：将图表缩放并拖动到某一历史挂单附近，跨过至少一次
5 秒订单详情刷新或 15 秒策略订单刷新后，可见起止时间应保持不变；只有切换交易、交易对或周期时
才恢复全图自适应。刷新可以更新 K 线和订单标记，但不能夺走用户正在检查的时间窗口。

## 7. 安全注意事项

- `.env`（tunnel token）和 `config.json`（FreqUI 密码、将来的交易所 API key）**绝不提交到公开仓库**
- 服务器不开任何新入站端口；8080 只绑 127.0.0.1
- 备用访问路径（CF 故障时）：`ssh -i ~/Downloads/tx.pem -L 8080:127.0.0.1:8080 ubuntu@<服务器IP>` 后访问 http://127.0.0.1:8080
- 将来实盘：币安账户需设 **单向持仓 (One-way Mode)** + **单资产模式 (Single-Asset Mode)**；API key 不开提现权限；`dry_run` 改 `false` 前先小资金

## 8. 已知限制

- 回测中早期 funding rate 数据交易所不提供，久远时间段回测会失真（可设 `futures_funding_rate` 近似值）
- 同一杠杆账户不能同时跑多个 bot 实例；多策略实盘需子账户
- 示例策略 VolatilityBreakout 未经调优（2026 上半年回测 -92%），仅作可插拔骨架参考，勿直接实盘
