树莓派上的 Freqtrade 模拟盘/实盘跑了几个月,一切正常。然而一次意外断电后,重启发现 dashboard 上所有数据都不见了——交易记录、持仓信息、K线历史、资金曲线,全部清零。
这个问题很严重:断电是常态(特别是树莓派这种嵌入式设备),如果每次断电后数据都丢失,那整个量化系统就不可用了。
本文记录了完整的排查过程和解决方案,核心思路是数据持久化 + 自动恢复。
问题分析
1. 交易信息丢失
现象:断电重启后,持仓显示为 0,交易记录为空。
原因:
fetch_freqtrade.py 优先从 REST API 读取持仓和交易
- 断电后 freqtrade docker 还没启动,API 不可用
- 直接返回空列表,导致 dashboard 显示为空
根本问题:REST API 数据是易失性的,而 freqtrade 的 SQLite 数据库才是持久化存储。
2. 资金曲线丢失
现象:断电后 equity 曲线从零开始。
原因:
push_worker.py 中 equity_history 只在内存中
- 重启后内存清空,没有持久化到磁盘
3. K线历史丢失
现象:断电后 K线从某个时间点重新开始,没有之前的历史数据。
原因:
download_latest.sh 只下载当前月的数据
- 断电跨月后,之前的月份 K线数据丢失
- feather 文件损坏后无法恢复
解决方案总览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ┌─ 树莓派 ─────────────────────────────────────────┐ │ │ │ 断电重启流程: │ │ │ │ 1. mihomo.timer (4min) → 启动代理 │ │ 2. freqtrade-auto-restart.timer (5min) │ │ → docker stop freqtrade & rm freqtrade │ │ → docker run freqtrade (加载数据库) │ │ 3. freqtrade-push.timer (6min) │ │ → 加载 equity_history.json │ │ → 从 SQLite 读取 trades/positions │ │ → 计算 K线范围(基于运行天数) │ │ │ │ 数据来源策略: │ │ │ │ trades: SQLite (优先) → API (回退) │ │ positions: SQLite (基础) → API (补充实时价格) │ │ equity: equity_history.json (持久化) │ │ klines: feather 文件 (最近3个月) │ │ │ └───────────────────────────────────────────────────┘
|
核心改进:
- 数据来源优先级调整:SQLite 持久化数据优先,API 作为实时补充
- equity 持久化:保存到
equity_history.json
- K线范围扩大:下载最近3个月,显示范围动态计算
- 自动启动:systemd timer 延迟启动,确保网络和代理就绪
一、数据持久化改造
1.1 fetch_freqtrade.py — 持仓获取逻辑优化
修改前:优先 API → 失败回退 SQLite
1 2 3 4 5 6 7
| def get_positions(): try: api_positions = _local_get("/api/v1/status") return result except: return get_positions_from_db()
|
问题:API 返回空(freqtrade 未启动)时,直接返回空列表,SQLite 不会被读取。
修改后:先 SQLite → 再用 API 补充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def get_positions(): db_positions = get_positions_from_db() if not db_positions: return [] try: api_positions = _local_get("/api/v1/status") for pos in db_positions: if api_map[key]: pos["current_rate"] = api_map[key]["current_rate"] except: pass return db_positions
|
效果:
- ✅ 断电重启后,即使 API 不可用,也能显示持仓(来自 SQLite)
- ✅ API 可用时,自动补充实时价格信息
- ✅ 完全解决了”断电后持仓显示为0”的问题
1.2 fetch_freqtrade.py — 交易记录优先 SQLite
1 2 3 4 5 6 7 8
| def get_trades(): db_trades = get_trades_from_db() if db_trades: return db_trades return get_trades_from_api()
|
SQLite 是 freqtrade 的持久化存储,交易记录不会因重启丢失。
1.3 push_worker.py — equity 持久化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| EQUITY_FILE = DASHBOARD_DIR / "equity_history.json"
def _load_equity(): global equity_history if EQUITY_FILE.exists(): with open(EQUITY_FILE, "r") as f: equity_history = json.load(f)
def _save_equity(): with open(EQUITY_FILE, "w") as f: json.dump(equity_history, f)
_load_equity()
while True: equity = build_equity(latest) _save_equity()
|
效果:
- ✅ 断电重启后,equity 曲线自动恢复
- ✅ 资金曲线不再从零开始
二、K线数据优化
2.1 download_latest.sh — 扩大下载范围
修改前:只下载当前月
1 2 3 4 5
| CURRENT_MONTH=$(date +"%Y%m") docker run --rm ... freqtradeorg/freqtrade:stable download-data \ --days 1 \ --timeframes 15m \ --timerange ${CURRENT_MONTH}010000-${CURRENT_MONTH}312359
|
修改后:下载最近3个月
1 2 3 4 5 6 7 8 9 10 11 12
| CURRENT_MONTH=$(date +"%Y%m")
PREV_MONTH1=$(date -d "$(date +%Y-%m-15) -1 month" +"%Y%m") PREV_MONTH2=$(date -d "$(date +%Y-%m-15) -2 month" +"%Y%m")
for MONTH in $PREV_MONTH2 $PREV_MONTH1 $CURRENT_MONTH; do docker run --rm ... freqtradeorg/freqtrade:stable download-data \ --days 1 \ --timeframes 15m \ --timerange ${MONTH}010000-${MONTH}312359 done
|
重要:freqtrade download-data 是增量合并,不会覆盖已有数据。
2.2 push_worker.py — K线范围动态计算
修改前:定死 200 根(约2天)
1
| klines = get_klines("BTCUSDT", "15m", 200)
|
修改后:基于 equity_history 的运行天数动态计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if len(equity_history) >= 2: start_time = equity_history[0]["time"] current_time = int(time.time() * 1000) days_running = (current_time - start_time) / (24 * 60 * 60 * 1000) kline_limit = int(days_running * 96 * 1.1) + 100 kline_limit = max(200, min(kline_limit, 8640)) print(f"[klines] 运行 {days_running:.1f} 天,获取 {kline_limit} 根K线") else: kline_limit = 200
klines = get_klines("BTCUSDT", "15m", kline_limit)
|
效果:
| 策略运行天数 |
K线数量(15分钟) |
| 1天 |
约200根 |
| 7天 |
约800根 |
| 30天 |
约3200根 |
| 90天 |
8640根(上限) |
2.3 fetch_klines.py — feather 损坏容错
1 2 3 4 5 6 7 8 9 10
| def _read_feather_safe(path): """ feather 损坏时自动重试 """ for attempt in range(3): try: return pd.read_feather(path) except Exception as e: if attempt == 2: raise print(f"[fetch_klines] feather 读取失败,重试 {attempt + 1}/3") time.sleep(1)
|
三、Systemd 自动启动配置
树莓派断电重启后,需要按顺序启动服务:
- 4 分钟后:启动 mihomo 代理(网络依赖)
- 5 分钟后:重启 freqtrade docker(代理依赖)
- 6 分钟后:启动 push_worker(freqtrade API 依赖)
3.1 mihomo.timer — 延迟4分钟启动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| sudo systemctl disable mihomo.service sudo systemctl stop mihomo.service
sudo cat > /etc/systemd/system/mihomo.timer << 'EOF' [Unit] Description=Run mihomo proxy 4 minutes after boot
[Timer] OnBootSec=4min Unit=mihomo.service
[Install] WantedBy=timers.target EOF
sudo systemctl daemon-reload sudo systemctl enable mihomo.timer sudo systemctl start mihomo.timer
|
3.2 freqtrade-auto-restart.timer — 延迟5分钟重启
创建重启脚本 ~/github/freqtrade/scripts/restart_freqtrade.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| cat > ~/github/freqtrade/scripts/restart_freqtrade.sh << 'EOF'
set -e
FREQTRADE_DIR="$HOME/github/freqtrade"
echo "[restart_freqtrade] $(date): 开始重启 freqtrade..."
cd "$FREQTRADE_DIR" || exit 1
echo "[restart_freqtrade] 停止旧容器..." docker stop freqtrade 2>/dev/null || true docker rm freqtrade 2>/dev/null || true
echo "[restart_freqtrade] 启动新容器..." docker run -d \ --name freqtrade \ --network host \ -v "$FREQTRADE_DIR/user_data:/freqtrade/user_data" \ freqtradeorg/freqtrade:stable \ trade \ --config /freqtrade/user_data/config.json \ --strategy MACDStrategy
echo "[restart_freqtrade] $(date): freqtrade 已启动" EOF
chmod +x ~/github/freqtrade/scripts/restart_freqtrade.sh
|
创建 systemd 服务和 timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| sudo cat > /etc/systemd/system/freqtrade-auto-restart.service << 'EOF' [Unit] Description=Auto restart freqtrade after boot After=network-online.target docker.service
[Service] Type=oneshot User=zhshang WorkingDirectory=/home/zhshang/github/freqtrade ExecStart=/home/zhshang/github/freqtrade/scripts/restart_freqtrade.sh StandardOutput=journal StandardError=journal
[Install] WantedBy=multi-user.target EOF
sudo cat > /etc/systemd/system/freqtrade-auto-restart.timer << 'EOF' [Unit] Description=Run freqtrade restart 5 minutes after boot
[Timer] OnBootSec=5min Unit=freqtrade-auto-restart.service
[Install] WantedBy=timers.target EOF
sudo systemctl daemon-reload sudo systemctl enable freqtrade-auto-restart.timer sudo systemctl start freqtrade-auto-restart.timer
|
注意:将 zhshang 替换为你的实际用户名。
3.3 freqtrade-push.timer — 延迟6分钟启动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| sudo cat > /etc/systemd/system/freqtrade-push.service << 'EOF' [Unit] Description=Freqtrade Dashboard Push Worker After=network.target
[Service] Type=simple User=zhshang WorkingDirectory=/home/zhshang/github/freqtrade/dashboard ExecStart=/home/zhshang/miniforge3/envs/freqtrade/bin/python push_worker.py Restart=always RestartSec=10
[Install] WantedBy=multi-user.target EOF
sudo cat > /etc/systemd/system/freqtrade-push.timer << 'EOF' [Unit] Description=Run freqtrade push worker 6 minutes after boot
[Timer] OnBootSec=6min Unit=freqtrade-push.service
[Install] WantedBy=timers.target EOF
sudo systemctl daemon-reload sudo systemctl enable freqtrade-push.timer sudo systemctl start freqtrade-push.timer
|
3.4 验证配置
查看所有 timer 状态:
1 2 3
| systemctl status mihomo.timer systemctl status freqtrade-auto-restart.timer systemctl status freqtrade-push.timer
|
应该显示:
1 2
| Active: active (running) Trigger: Boot Xmin later
|
查看日志:
1
| journalctl -u freqtrade-auto-restart.service -f
|
四、完整启动流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 树莓派断电 → 重启 ↓ 0 min: 系统启动 ↓ 4 min: mihomo.timer 触发 → mihomo.service 启动(代理服务) ↓ 5 min: freqtrade-auto-restart.timer 触发 → restart_freqtrade.sh 执行 → docker stop freqtrade & docker rm freqtrade → docker run freqtrade(加载数据库) ↓ 6 min: freqtrade-push.timer 触发 → freqtrade-push.service 启动 → 加载 equity_history.json → 从 SQLite 读取 trades/positions → 计算 K线范围(基于运行天数) → 开始推送数据到 Cloudflare Worker ↓ 最终: 所有数据恢复,dashboard 正常显示
|
五、测试验证
5.1 模拟断电重启
1 2 3 4 5 6 7 8 9 10 11
| docker stop freqtrade sudo systemctl stop freqtrade-push
rm /home/zhshang/github/freqtrade/dashboard/equity_history.json
|
5.2 检查数据来源
查看 push_worker 日志:
1
| journalctl -u freqtrade-push -f
|
正常输出:
1 2 3 4
| [equity] loaded 17 points from equity_history.json [fetch_freqtrade] 持仓:1 个(已更新实时价格) [klines] 运行 0.1 天,获取 200 根K线 [dashboard] HTTP 200
|
5.3 手动测试重启
1 2 3 4 5 6 7 8
| docker stop freqtrade && docker rm freqtrade
sudo systemctl start freqtrade-auto-restart.service
journalctl -u freqtrade-auto-restart.service -f
|
六、常见问题
Q1: 为什么不用 docker 自带的 restart 策略?
A: docker 的 restart: unless-stopped 确实能自动重启容器,但无法保证启动顺序:
- freqtrade docker 启动时,mihomo 代理可能还没就绪
- push_worker 启动时,freqtrade API 可能还没就绪
使用 systemd timer 可以精确控制启动时间和顺序。
Q2: SQLite 数据会丢失吗?
A: 不会。push_worker.py 使用只读模式访问 SQLite:
1
| conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
|
只有 freqtrade docker 容器会写入 SQLite(开仓/平仓时)。
Q3: feather 文件损坏后怎么办?
A: download_latest.sh 每 15 分钟下载最近 3 个月数据,是增量合并。如果 feather 损坏:
fetch_klines.py 自动重试 3 次
- 下一次
download_latest.sh 执行时会重新下载
- 数据会自动恢复
附录:常用命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| systemctl list-timers --all
systemctl status mihomo.timer systemctl status freqtrade-auto-restart.timer systemctl status freqtrade-push.timer
sudo systemctl start freqtrade-auto-restart.service sudo systemctl start freqtrade-push.service
journalctl -u freqtrade-auto-restart.service -f journalctl -u freqtrade-push.service -f
docker stop freqtrade && docker rm freqtrade
docker ps -a
sqlite3 user_data/tradesv3.sqlite "SELECT pair, is_short, is_open FROM trades"
cat dashboard/equity_history.json
curl -u freqtrade:password http://127.0.0.1:8080/api/v1/trades
|
后记
经过这次优化,系统已经具备完整的断电恢复能力:
| 改进项 |
修改前 |
修改后 |
| 交易记录 |
API 优先,断电为空 |
SQLite 优先,持久化 |
| 持仓信息 |
API 优先,断电为空 |
SQLite 基础,API 补充 |
| 资金曲线 |
内存存储,重启清空 |
持久化 JSON,自动恢复 |
| K线数据 |
当月数据,断电丢失 |
3个月数据,动态范围 |
| 自动启动 |
手动执行 |
systemd timer 延迟启动 |
现在即使树莓派意外断电,重启后 6 分钟内所有数据自动恢复,dashboard 正常显示。这对于模拟盘/实盘交易系统来说,是至关重要的可靠性保障。