diff --git a/live/setup_wallets.py b/live/setup_wallets.py new file mode 100644 index 00000000..697b5838 --- /dev/null +++ b/live/setup_wallets.py @@ -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 先设 25,sync_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 时看前瞻盈亏决定晋升/降级") diff --git a/sync-upstream.sh b/sync-upstream.sh new file mode 100644 index 00000000..71c54f5b --- /dev/null +++ b/sync-upstream.sh @@ -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" diff --git a/每周Bench-Review流程.md b/每周Bench-Review流程.md new file mode 100644 index 00000000..f429ff98 --- /dev/null +++ b/每周Bench-Review流程.md @@ -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}%') +" +```