feat: add bench review workflow + wallet setup script + sync-upstream fix

- live/setup_wallets.py: configures our 8 follow wallets with Kruto2027
  floor_pin=80 (matching author), plus prints bench status
- 每周Bench-Review流程.md: weekly Friday review process using 5 dimensions
  (forward P&L, copy_pnl, conv_win, seasonal catalyst, floor stability)
  with promote/demote/pin commands
- sync-upstream.sh: recreated with all our custom files including new ones
  (sync_live_floors.py, setup_wallets.py, bench review doc)
- copybot.paper.json: our 8 wallets with Kruto2027 floor_pin=80

bench area (backtest.json) already has 15 wallets from author sync:
  live: 8 (Kruto2027, 0xbadaf319, 1kto1m, Vahan88, LSB1, EdwardIN, lma0o0o0o, Coteykens)
  bench: 7 (gkmgkldfmg, AIcAIc, BikesAreTheBikes, imwalkinghere, leegunner, oliman2, JuiceFarm, 0xb0E43B)
This commit is contained in:
2026-07-21 08:09:44 +08:00
parent 93d7fe76b6
commit 8656e528af
3 changed files with 407 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""配置我们的 8 个跟单钱包到 copybot.paper.json 和 config.live.json。
跟作者的方法论一致:
- Kruto2027 floor_pin=80(钉死,跟作者一样)
- 其它钱包 floor 由 sync_floors.py 每天自动算 p80
- backtest.json 保持作者的 15 个钱包(8 个 live + 7 个 bench 观察区)
"""
import json
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
# 我们的 8 个跟单钱包(基于 watch_sharps.json 排名 + 作者方法论)
OUR_WALLETS = [
# floor_pin=80 跟作者一样,防止 p80 漂移到 125
{"wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb",
"name": "Kruto2027", "class": "volume", "floor": 80.0, "floor_pin": 80.0},
# 其它钱包不钉 pin,让 sync_floors.py 每天算 p80
{"wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874",
"name": "0xbadaf319", "class": "volume", "floor": 42.0},
{"wallet": "0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343",
"name": "1kto1m", "class": "volume", "floor": 332.0},
{"wallet": "0x2d462b29127b919ae4c085e03be44ae7077e3e2d",
"name": "Vahan88", "class": "volume", "floor": 245.0},
{"wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41",
"name": "LSB1", "class": "volume", "floor": 315.0},
# 新加的 3 个,floor 先设 25sync_floors.py 会自动更新
{"wallet": "0x0c3e55cf50a00a7e74fabd8584c6fdde21603c4c",
"name": "EdwardIN", "class": "volume", "floor": 25.0},
{"wallet": "0x82d2e4dbb0a849ff8e2f5380719769145648beea",
"name": "lma0o0o0o", "class": "volume", "floor": 25.0},
{"wallet": "0x69b9bd4fa27813091fcfe726541f2fd7baa3a5d5",
"name": "Coteykens", "class": "volume", "floor": 25.0},
]
# 更新 copybot.paper.json
paper_path = os.path.join(HERE, "copybot.paper.json")
paper = json.load(open(paper_path))
paper["wallets"] = OUR_WALLETS
json.dump(paper, open(paper_path, "w"), indent=2)
print(f"copybot.paper.json: {len(OUR_WALLETS)} wallets")
for w in OUR_WALLETS:
pin = f" (PINNED {w['floor_pin']})" if "floor_pin" in w else ""
print(f" {w['name']:20s} floor=${w['floor']}{pin}")
# 更新 config.live.json(如果存在)
live_path = os.path.join(ROOT, "config.live.json")
if os.path.exists(live_path):
live = json.load(open(live_path))
live["wallets"] = OUR_WALLETS
json.dump(live, open(live_path, "w"), indent=2)
print(f"\nconfig.live.json: {len(OUR_WALLETS)} wallets (synced)")
else:
print(f"\nconfig.live.json: not found (create from config.live.example.json first)")
# 确认 backtest.json 有 bench 观察区
bt_path = os.path.join(HERE, "backtest.json")
bt = json.load(open(bt_path))
bt_names = [w.get("name", w["wallet"][:10]) for w in bt["wallets"]]
our_names = [w["name"] for w in OUR_WALLETS]
bench = [n for n in bt_names if n not in our_names]
print(f"\nbacktest.json: {len(bt_names)} wallets total")
print(f" live (跟单): {len(our_names)} - {', '.join(our_names)}")
print(f" bench (观察): {len(bench)} - {', '.join(bench)}")
print(f"\nbench 区的钱包每天被 portfolio.py 回测,周五 review 时看前瞻盈亏决定晋升/降级")
+110
View File
@@ -0,0 +1,110 @@
#!/bin/bash
# sync-upstream.sh - 同步原作者代码 + 保留我们的定制
#
# 用法:每次原作者有大改动时跑一次
# HTTPS_PROXY=http://127.0.0.1:7890 bash sync-upstream.sh
#
# 做的事:
# 1. 备份我们的定制文件到 /tmp/wwf-ours/
# 2. 拉取原作者最新代码(github/main
# 3. 把我们的定制文件加回去
# 4. 推到 Gitea
#
# 前提:已经 git remote add github https://github.com/jaxperro/winning-wallet-finder.git
set -e
cd "$(git rev-parse --show-toplevel)"
echo "=== 1. 备份我们的定制文件 ==="
BACKUP="/tmp/wwf-ours"
rm -rf "$BACKUP"
mkdir -p "$BACKUP/host" "$BACKUP/live"
# 我们的定制文件清单(新增定制文件必须加到这里!)
OUR_FILES=(
"live/serve_dashboard.py"
"live/bot_dashboard.html"
"live/sync_live_floors.py"
"live/setup_wallets.py"
"host/mihomo-healthcheck.sh"
"sync-upstream.sh"
"DEPLOY.md"
"USAGE.md"
"日常使用.md"
"问题排查与踩坑记录.md"
"mihomo代理部署教程.md"
"云端架构.md"
"实盘配置.md"
"常用命令.md"
"项目流程.md"
"钱包筛选流程.md"
"每周Bench-Review流程.md"
"同步作者代码指南.md"
)
for f in "${OUR_FILES[@]}"; do
if [ -f "$f" ]; then
mkdir -p "$BACKUP/$(dirname "$f")"
cp "$f" "$BACKUP/$f"
echo " 备份: $f"
fi
done
# 也备份我们的钱包配置(要覆盖回作者的)
if [ -f "live/copybot.paper.json" ]; then
cp "live/copybot.paper.json" "$BACKUP/live/copybot.paper.json.ours"
echo " 备份: live/copybot.paper.json (我们的钱包配置)"
fi
echo ""
echo "=== 2. 拉取原作者最新代码 ==="
git fetch github
git checkout -B sync-upstream github/main
echo " 当前代码: $(git log --oneline -1)"
echo ""
echo "=== 3. 加回我们的定制文件 ==="
for f in "${OUR_FILES[@]}"; do
if [ -f "$BACKUP/$f" ]; then
mkdir -p "$(dirname "$f")"
cp "$BACKUP/$f" "$f"
echo " 恢复: $f"
fi
done
echo ""
echo "=== 4. 恢复我们的钱包配置 ==="
if [ -f "$BACKUP/live/copybot.paper.json.ours" ]; then
cp "$BACKUP/live/copybot.paper.json.ours" "live/copybot.paper.json"
echo " 恢复: live/copybot.paper.json (我们的钱包)"
fi
# config.live.json 是 gitignored 的,不会被覆盖
if [ ! -f "config.live.json" ]; then
cp config.live.example.json config.live.json
echo " 创建: config.live.json (从模板,需要填 bankroll + 钱包)"
fi
echo ""
echo "=== 5. 提交 + 推送 ==="
git add -A
git commit -m "sync: upstream + our customizations (dashboard, healthcheck, docs, wallet setup)
Synced from github/main: $(git log --oneline github/main -1)
Our additions: serve_dashboard.py, bot_dashboard.html, mihomo-healthcheck.sh,
sync_live_floors.py, setup_wallets.py, 中文文档, 我们的钱包配置" || echo " 没有改动需要提交"
git push origin sync-upstream:main --force
echo ""
echo "=== 完成 ==="
echo "已推送到 Gitea main 分支"
echo ""
echo "下一步:在服务器上更新"
echo " cd /opt/winning-wallet-finder"
echo " systemctl stop copybot-live copybot-paper"
echo " git fetch origin && git reset --hard origin/main"
echo " rm -f copybot_state*.json copybot_fills*.jsonl live/copybot_live*.json"
echo " cp host/mihomo-healthcheck.sh /opt/mihomo-healthcheck.sh"
echo " cd live && python3 setup_wallets.py"
echo " chown wwf:wwf ../config.live.json copybot.paper.json"
echo " systemctl start copybot-paper copybot-live"
+230
View File
@@ -0,0 +1,230 @@
# 每周 Bench Review 流程
> 跟原作者的方法论一致:每周五对跟单钱包和 bench 观察区钱包做前瞻盈亏 review,决定晋升/降级。
>
> 原作者的原话(HANDOFF_ARCHIVE.md):forward table = portfolio.py resolved rows, two windows: since last review and since the last swap.
---
## 为什么要每周 review
watch_sharps.json 的 copy_pnl 是 **in-sample**(用历史数据算的),只能告诉你"过去跟这个钱包赚没赚"。但市场在变,钱包的表现也会变。
**前瞻盈亏(Forward P&L** 是 out-of-sample 的:看"从某个时间点开始,这个钱包的信念单实际赚了多少"。这才是决定留还是踢的真实依据。
---
## review 时间
**每周五**(跟作者一样)。可以在 1Panel 加一个定时提醒。
---
## review 步骤
### 第 1 步:跑回测(在服务器上)
```bash
cd /opt/winning-wallet-finder/live
# 跑 30 天回测(包含 live 8 个 + bench 8 个,共 16 个钱包)
python3 portfolio.py --days 30
# 看回测结果
python3 -c "
import json
d = json.load(open('portfolio.json'))
print(f'总体: equity \${d[\"equity\"]:,.0f} | realized \${d[\"realized\"]:+,.0f} | {d[\"resolved\"]} resolved')
"
```
### 第 2 步:看每个钱包的前瞻表现
```bash
# 看回测里每个钱包的盈亏
python3 -c "
import json
d = json.load(open('portfolio.json'))
wallets = {}
for b in d.get('bets', []):
name = b.get('name', '?')
if name not in wallets:
wallets[name] = {'n': 0, 'pnl': 0, 'wins': 0}
wallets[name]['n'] += 1
wallets[name]['pnl'] += b.get('pnl', 0)
if b.get('pnl', 0) > 0:
wallets[name]['wins'] += 1
# 按 pnl 排序
for name, stats in sorted(wallets.items(), key=lambda x: x[1]['pnl'], reverse=True):
wr = stats['wins']/stats['n']*100 if stats['n'] > 0 else 0
print(f'{name:20s} {stats[\"n\"]:3d} bets pnl \${stats[\"pnl\"]:+8.2f} 胜率 {wr:.0f}%')
"
```
### 第 3 步:对比 live vs bench
```bash
# 当前跟的 8 个
python3 -c "import json; [print(w['name']) for w in json.load(open('copybot.paper.json'))['wallets']]"
# bench 观察区的(backtest.json 里有但 copybot.paper.json 里没有的)
python3 -c "
import json
live = {w['wallet'].lower() for w in json.load(open('copybot.paper.json'))['wallets']}
bt = json.load(open('backtest.json'))
bench = [w.get('name', w['wallet'][:10]) for w in bt['wallets'] if w['wallet'].lower() not in live]
print('bench:', ', '.join(bench))
"
```
### 第 4 步:决策(5 个维度)
对照 5 个维度做决策:
| 维度 | 怎么看 | 晋升条件 | 降级条件 |
|------|--------|---------|---------|
| **前瞻盈亏** | 第 2 步的 per-wallet pnl | 正盈亏 | 负盈亏(连续 2 周) |
| **copy_pnl** | `cat watch_sharps.json \| grep copy_pnl` | 排名靠前 | 跌出 watch_sharps |
| **信念胜率** | `cat watch_sharps.json \| grep conv_win` | >60% | <30% |
| **季节性** | 看钱包的 niche 有没有赛季/事件 | 赛季开始 | 赛季结束 |
| **floor 稳定** | `cat copybot.paper.json \| grep floor` | 正常 | p80 漂移太大需 pin |
### 第 5 步:执行变更
**晋升**bench -> live):
```bash
cd /opt/winning-wallet-finder
python3 -c "
import json
# 把 bench 钱包加到 copybot.paper.json 和 config.live.json
promote_name = 'AIcAIc' # 改成你要晋升的钱包
# 从 backtest.json 查地址
bt = json.load(open('live/backtest.json'))
promote = next((w for w in bt['wallets'] if w.get('name') == promote_name), None)
if not promote:
print(f'{promote_name} not found in backtest.json')
exit(1)
# 加到 paper 和 live
for path in ['live/copybot.paper.json', 'config.live.json']:
cfg = json.load(open(path))
if not any(w['wallet'].lower() == promote['wallet'].lower() for w in cfg['wallets']):
w = {'wallet': promote['wallet'], 'name': promote['name'],
'class': 'volume', 'floor': 25.0}
cfg['wallets'].append(w)
json.dump(cfg, open(path, 'w'), indent=2)
print(f'{path}: added {promote_name}')
else:
print(f'{path}: {promote_name} already there')
"
chown wwf:wwf live/copybot.paper.json config.live.json
systemctl restart copybot-paper copybot-live
```
**降级**live -> bench):
```bash
cd /opt/winning-wallet-finder
python3 -c "
import json
demote_name = 'BikesAreTheBikes' # 改成你要降级的钱包
for path in ['live/copybot.paper.json', 'config.live.json']:
cfg = json.load(open(path))
old = [w['name'] for w in cfg['wallets']]
cfg['wallets'] = [w for w in cfg['wallets'] if w['name'] != demote_name]
new = [w['name'] for w in cfg['wallets']]
json.dump(cfg, open(path, 'w'), indent=2)
print(f'{path}: {old} -> {new}')
"
chown wwf:wwf live/copybot.paper.json config.live.json
systemctl restart copybot-paper copybot-live
```
> 降级的钱包留在 backtest.json 里继续观察(不用删),下周 review 还能看到它的前瞻表现。
### 第 6 步:钉死 floor_pin(如果需要)
如果某个钱包的 p80 漂移太大(sync_floors.py 警告 `⚠ floor X->Y`),钉死它:
```bash
python3 -c "
import json
pin_name = 'Kruto2027' # 要钉的钱包
pin_value = 80.0 # 钉死的值
for path in ['live/copybot.paper.json', 'config.live.json']:
cfg = json.load(open(path))
for w in cfg['wallets']:
if w['name'] == pin_name:
w['floor'] = pin_value
w['floor_pin'] = pin_value
print(f'{path}: {pin_name} floor_pin = {pin_value}')
json.dump(cfg, open(path, 'w'), indent=2)
"
chown wwf:wwf live/copybot.paper.json config.live.json
systemctl restart copybot-paper copybot-live
```
---
## 当前状态
### live 跟单(8 个)
| 钱包 | floor | floor_pin | niche |
|------|-------|-----------|-------|
| Kruto2027 | $80 | ✅ 80 | 综合 |
| 0xbadaf319 | $42 | - | 综合 |
| 1kto1m | $332 | - | BTC Up/Down |
| Vahan88 | $245 | - | 板球/体育 |
| LSB1 | $315 | - | 瑞典足球 |
| EdwardIN | $25 | - | 待观察 |
| lma0o0o0o | $25 | - | 待观察 |
| Coteykens | $25 | - | 待观察 |
### bench 观察(8 个)
| 钱包 | 状态 | 备注 |
|------|------|------|
| gkmgkldfmg | 被淘汰 | daily.sh 已移出 watch_sharps |
| AIcAIc | 观察 | 作者 07-16 重新加入(电竞赛季) |
| BikesAreTheBikes | 观察 | 信念胜率 21% 偏低 |
| imwalkinghere | 永久排除 | 5min BTC 市场,结算比跟单快 |
| leegunner | 新候选 | 作者 bench rev 5 新加 |
| oliman2 | 新候选 | 作者 bench rev 5 新加 |
| JuiceFarm | 新候选 | 作者 bench rev 5 新加 |
| 0xb0E43B | 新候选 | 作者 bench rev 5 新加 |
---
## 1Panel 快速命令
**每周 review 一键跑回测 + 看排名**
```bash
cd /opt/winning-wallet-finder/live && python3 portfolio.py --days 30 && python3 -c "
import json
d = json.load(open('portfolio.json'))
wallets = {}
for b in d.get('bets', []):
name = b.get('name', '?')
if name not in wallets:
wallets[name] = {'n': 0, 'pnl': 0, 'wins': 0}
wallets[name]['n'] += 1
wallets[name]['pnl'] += b.get('pnl', 0)
if b.get('pnl', 0) > 0:
wallets[name]['wins'] += 1
print('=== 前瞻盈亏排名 ===')
for name, stats in sorted(wallets.items(), key=lambda x: x[1]['pnl'], reverse=True):
wr = stats['wins']/stats['n']*100 if stats['n'] > 0 else 0
print(f'{name:20s} {stats[\"n\"]:3d} bets pnl \${stats[\"pnl\"]:+8.2f} 胜率 {wr:.0f}%')
"
```