记录一次模拟跟单系统的搭建(2026-06-05)
记录一次模拟跟单系统的搭建(2026-06-05)
上午还在修一个报错,下午决定从零搭一整套系统
那天的开始很普通——修 page 8 的一个 format_func 报错。修完顺手看了一眼,页面上是 GMGN 的 trenches 新币数据,一行行刷出来。
然后就有了那个念头:数据都在这儿了,为什么不干脆拿它做一套模拟交易? 拿真金白银试错太贵,但拿一套虚拟账户去跑,成本是零——而且能跑出真实的数据。
于是就有了后面这一整天。
9 号页:4 套策略 + 一套硬风控
新建的页面是 pages/9_sim_trading.py,里面装了四套策略:
- 跟 KOL
- 跟聪明钱
- 避雷
- 综合
起始资金 1000U,纯虚拟。
策略可以有多个,但风控只有一套,而且是硬编码的:
| 规则 | 值 |
|---|---|
| 单笔金额 | 30U |
| 同时持仓上限 | 5 |
| 止损 | 30% |
| 止盈 | 100% |
| rug 比率 | > 0.6 强平 |
| sniper 数量 | > 5 强平 |
| 最长持仓 | 72 小时 |
| 价格超时 | 24 小时未更新强平 |
把风控写成常量而不是配置,是刻意的选择。策略是要被反复比较的对象,风控不是。 如果风控也能随手改,那两版策略的收益差异就说不清是策略变好了还是风控放松了。
调优不是感觉,是 8 张表
这是那天最重要的决定:数据库从 4 张表扩到 8 张。
一开始想的很简单——账户、成交、持仓、净值曲线,4 张就够了。但真要做调优就会发现,4 张表只能告诉你"赚了还是亏了",**告诉不了你"为什么"**。
所以补上了另外 4 张:
| 表 | 存什么 |
|---|---|
sim_account |
账户余额与已实现盈亏 |
sim_trades |
成交明细,开仓和平仓各带一套信号列 |
sim_positions |
持仓,含开仓信号、最高最低价、mark 次数 |
sim_snapshots |
净值时间序列 |
sim_position_marks |
持仓期间的价格序列,后台每 30 秒一次 |
sim_strategy_versions |
策略版本快照,params_json 存风控参数 |
sim_market_snapshots |
市场环境,含 regime 判定 |
sim_decisions |
BUY / SKIP / RISK_BLOCK / CLOSE 的全部决策日志 |
最后那张 sim_decisions 是关键。它记录的不是"做成了什么",而是"当时想做什么、为什么没做"。没有它,调优就只能看到结果,看不到被拒绝掉的那部分——而被拒绝掉的那部分,往往才是问题所在。
迁移写成了 idempotent 的,重复执行也不会出错。
7 类分析
光有数据还不够,得有人把它读出来。所以写了 analysis.py,出七类分析:
- 账户健康度
- 策略版本对比
- 按市场 regime 分组
- 开仓信号分桶
- 平仓预警
- 漏掉的好币
- 持仓 mark 序列
第 6 类"漏掉的好币"是我特意加的。因为策略的失误有两种:一种是买了不该买的,一种是没买该买的。前者亏钱看得见,后者不亏钱,但也在损失——而它只会在"漏掉的好币"里露头。
配套的还有一个 10 号页 pages/10_review.py,9 个分区,专门用来复盘和调参。
6 步工作流
调优这件事如果不定流程,很容易变成"凭感觉改参数"。那天把它固化成了 6 步:
- 先跑够 50 笔以上平仓,积累样本
- 看分桶,找出最赚的桶和最亏的桶
- 改
ACTIVE_VERSION(在 9 号页顶部)和策略评分 - 跑一轮,自动
upsert出新的策略版本 - 用 2 号分析对比新旧版本
- 回到第 1 步
这 6 步里,"先跑够 50 笔"是最容易被跳过、也最不该跳过的一步。样本不够的时候,任何差异都是噪声——你会把运气当成策略。
第一轮数据:难看,但有用
v1.0 跑出来的结果是这样:
- combo 策略**胜率 10%**(1 胜 9 负)
- 累计 PnL −37.78U
- cold 市场几乎没有 KOL 币(
pct_with_kol=0,avg_renowned=0) - KOL=0 的那个桶,把 −37.78U 全亏了
- 平均持仓 不到 6 分钟
- **费用占比 99%**(累计 6.25U 手续费,实收 0U 利润)
单看第一条,这是套失败的策略。但把六条串起来看,它讲了一个完整的故事:
cold 市场里根本没有 KOL 参与,但策略照样开仓——因为"没有 KOL"这件事被当成了"中性",而不是"警告"。 于是所有单子都落在同一个桶里,一起亏。而"持仓不到 6 分钟"说明大部分单子一开仓就被止损或止盈扫掉了,这不是策略在交易,这是在阈值边缘来回摩擦——最直接的证据就是那 99% 的费用占比:钱没亏在判断上,亏在了周转上。
有了这个解释,调参方向就自己浮出来了:cold 时收紧 KOL 阈值或者干脆不交易、把 combo 阈值从 4 提到 5、把"持有人跌 10% 以上"和"rug 涨 0.3 以上"写进强制平仓。
这些结论不是猜的,是从 8 张表里读出来的。 这就是那天坚持把表扩到 8 张的回报。
三个坑
一是 PYTHONPATH 污染(这个最阴)。 环境里给 shell 设了 PYTHONPATH,指向一个 cp311 的 venv。但子进程用的是 conda 的 cp313 Python。结果就是 jsonschema 依赖的 rpds-py——那个 .pyd 是按 cp311 编的,跟 cp313 不兼容,直接报 No module named 'rpds.rpds'。
修法是启动子进程前 unset PYTHONPATH 和 unset PYTHONHOME。但更有价值的是那条诊断经验:conda 进程报"找不到某个子模块",八成是 PYTHONPATH 把别的 venv 插到了 sys.path 顶端——先 env | grep PYTHONPATH,别急着 pip install 重装。 重装是对的包,装在错的环境里,问题一点不会少。
二是 MySQL 没有 MEDIAN()。 想算中位数,得在 Python 客户端自己算。
三是 mysql.connector 返回的是 Decimal 和 None 的混合。 直接丢给 pandas 会出事,计算前必须 astype(float)。
另外还有一条工程上的小事:Streamlit 改了 app.py 在 Windows 上不会自动重启(mtime 偶尔不变),得先 taskkill /F 掉旧进程再启。
顺手接了两个源
那天还把两个数据源接进来了:
一是 GMGN 的 trenches(openapi.gmgn.ai 的 POST /v1/trenches,带 X-APIKEY 和 ts+uuid)。这里有个坑:gmgn.ai 主域和 defi.gmgn.ai 都被 Cloudflare 拦着,只有 openapi.gmgn.ai 这个子域能直连。而且没有走官方的 CLI,直接用 Python requests 打——因为 spawn 一个 CLI 会拖慢前端的自动刷新。
二是 gmgn.ai/ai 那个技能导航站,收录了 40 多个链上技能,列表在 skills_zh-CN.json 里。它的用法是"按需查找按需安装"——需要什么技能,先去检索,确认了再从 url 把 SKILL.md 拉下来装。不一次性全装,因为装了不用的技能,跟没装一样,只是占了位置。
顺带确认了代理配置:Clash 挂在 127.0.0.1:7890,curl -x http://127.0.0.1:7890 走代理。注意优先用 HTTPS——只支持 HTTP 的站走代理会被 Cloudflare 403。
最后
那天还有一条自我约束,我觉得比代码本身更值得记:
会话级的工作进度,不写进长期记忆。
todo 推进到哪了、这个 phase 完成了没、参数调到几了——这些是"当前状态",不是"长期事实"。把它们塞进记忆,7 天之后就是一堆垃圾,还得花时间清理。长期记忆只存环境、偏好、约定这类恒定的东西。
所以那天真正被记下来的,是"数据存在哪台机器""用什么命令启动""中位数要在客户端算"——而不是"今天改到了 v1.0"。
回到系统本身。它第一天跑出来是亏的,而且亏得挺难看。但那天结束时我一点都不沮丧,因为这 −37.78U 是第一次"可归因"的亏损:能说出钱亏在哪个市场状态、哪个信号桶、哪个环节。以前做策略,亏了只能归咎于"运气不好"。
这可能就是模拟盘真正的价值——它不是让你不亏钱,而是让你亏得明白。