记录一次模拟盘的排障(2026-07-10)
记录一次模拟盘的排障(2026-07-10)
模拟盘最坑的地方,不是它亏了钱,而是它让你以为策略已经能跑了
这天打开 http://0.0.0.0:8501/,主流币模拟交易那条曲线几乎全是红的。接下来的几个小时基本都在同一个循环里转:发现一处数据不对,翻代码,改掉,重启,然后发现下一处不对。
下面按当时踩到的顺序记。
第一次运行就崩
maincoin_paper.py 里 scan_signals() 返回的是 dict,但 run_tick() 全程用属性访问:sig.confidence、sig.signal.value。所以第一轮 mc_paper_loop 一起来就崩了。
改成 sig["confidence"]、sig["signal"],顺手删掉没用的 Signal import。这个 bug 的性质其实很典型——函数签名和调用方之间没有契约,写的时候两边各自都对,合起来就炸。
开仓时间和平仓时间一模一样
翻交易记录时发现一件事:每一笔的 opened_at 和 closed_at 是同一秒,大量交易 duration=0s。
根因在 close_position 函数(第 193 行):它用 NOW() 同时填了 opened_at 和 closed_at,等于把开仓时间覆盖成了平仓时间。修法是平仓时用原始开仓时间,只把 closed_at 设成 NOW():
1 | # 修复前 |
但 duration=0s 只是症状。真正的原因是同一币种挂着互相冲突的信号:ETH/USDT 同时有 ls_ratio_extreme 的 LONG(0.72)和 orderbook_imbalance 的 SHORT(0.85)。于是 Tick N 按 LONG 开仓,Tick N+1 按 SHORT 平仓,再开再平——XLM/USDT 就这么连续止损了 20 多次。
两处修复一起上:5 分钟平仓冷却(CLOSE_COOLDOWN_SECONDS = 300),以及信号去重——每个 symbol 只留置信度最高的那条。
1 | symbol_signals = {} |
总资产显示 -72.8%
17 号页一度显示总资产 -72.8%。查出来是 total_assets = balance + unrealized_pnl = 2500 + 217 = 2717。
问题在于:balance=2500 时已经扣掉了 7500 的持仓本金,而 unrealized_pnl 只算浮盈、不含本金。所以要把持仓本金加回来:
1 | total_position_value += size + pnl # 本金 + 浮盈 |
total_pnl 也有同样的重复计算。旧公式 closed_pnl + (持仓市值 - 持仓本金) 里,balance 已经含了已平仓盈亏,再加一次 closed_pnl 就多算了。正确写法只有一行:
1 | total_pnl = total_assets - initial # 总资产 - 初始余额 = 真实盈亏 |
表格和数字的观感
两处纯显示层的返工:持仓列表原来用 st.columns + write 拼出来,没有表头,改成 st.dataframe,列是「币种/方向/入场价/现价/仓位/盈亏/盈亏%/策略/止损/止盈」;交易记录同样从文本行改成结构化表格。
价格显示也顺手收拾了:原来一律 8 位小数(1737.60000000)。改成按量级自动取位——≥100 取 2 位,1~100 取 4 位,<1 取 6 位。
稳定币不该开仓
持仓里出现了 RLUSD。它在 mc_top_coins 里(成交量第 8),所以会被当成标的。稳定币根本没有方向性行情,开仓等于白交手续费和滑点。加了一层过滤,把 RLUSD/USDC/USDT/BUSD/DAI/TUSD 全部挡在开仓之前,顺带清掉了过滤生效前开出来的那个 RLUSD 持仓。
从 1h 收盘价换成实时价
最根本的一处:曲线用的还是 1h K 线收盘价。几分钟就开平的交易,拿小时级数据去算盈亏,本身就是错的。
在 maincoin_kline.py 里加了两个函数:fetch_realtime_price(symbol) 走 ccxt 的 fetch_ticker(),fetch_realtime_prices(symbols) 批量拿、失败降级回 1h kline。策略引擎的 check_positions() 也改成开头一次性批量拉价格,止损/止盈/信号反转全用实时价。
批量版本踩了个坑:fetch_tickers(list) 对部分币种返回 None,字符串拼接直接报 can only concatenate str (not "NoneType") to str。最后退回逐个 fetch_ticker() 调用,并加上空值过滤。验证时 BTC $64,212.0、ETH $1,784.1、SOL $79.19 都正常返回。
15 号页面的 K 线停在 7 月 6 日
15 号页的 1h K 线一直没动,停在 7 月 6 日。原因是 run_tick() 只拉 4h,别的周期没管。改成每轮把 1h/4h/1d 全拉一遍。
16 号页的 Tab 3「💰模拟交易」和 17 号页完全重复,而且引用的 load_kline_df 根本没 import,直接删掉,4 个 tab 收敛成 3 个(策略概览/实时信号/回测)。
参数与重启
交易频率太低,把参数重定了一遍:单仓 20% → **15%**,最大持仓 3 → 5,置信度门槛 0.7 → 0.6,止损 -3% → **-2%**,止盈 +8% → **+5%**,并新增信号反转平仓。
后台这边发现同时跑了 11 个 maincoin_paper.py 进程。清干净,用 subprocess.Popen + DETACHED_PROCESS 只起一个,日志写 mc_paper_loop.log,每 5 分钟一轮:拉 K 线 → 查止损止盈 → 扫信号 → 开仓。
验证看的是数据库里的新记录:ID 267,开仓 2026-07-10 16:40:30、平仓 16:42:14,持仓 104 秒。开平仓时间终于不是同一秒了。旧的 263–266 还是 0s,那些是旧代码留下的。
这天后半段还做了账户重置——余额回 $10,000,持仓和交易清空;其中一度明确要求「交易频率保持 1h、不要改成 4h、也不要冷却期机制」,于是那版冷却期又撤了回去。同日还换了侧边栏顺序:把「主流币模拟交易」提到第 1 页,「模拟跟单交易」挪到第 17 页。
最后
收盘前把当天的记录存进了 OpenViking(端点 http://127.0.0.1:1933 正常,向量索引更新完能搜到):一条 cases 记 7 项修复清单和持仓状态,一条 patterns 记 mc_paper_loop 的启动方式、常见问题与数据流。
最表面的收获是那份修复清单——从 dict 访问到 NOW() 覆盖,从 -72.8% 到重复开仓,每一条都具体可查。
中间一层,是「同一个症状可能有两处独立的根因」。duration=0s 看代码是 NOW() 写错,看数据是信号冲突;只修一处,另一处还会换个形式冒出来。排障真正的活,是在症状和根因之间来回对,而不是停在第一处能解释现象的地方。
最里面一层,还是那句:模拟盘的价值,在于它便宜地暴露你不想看的错。 -72.8%、duration=0s、被当成标的的 RLUSD——这些全是模拟盘替我挡下来的。真正让我警惕的不是这些数字,而是:如果这是实盘,我需要多久才会发现?
那天之后给自己立了条规矩:每次修复,都要在下结论之前再跑一遍完整验证,而不是改完就刷新页面。