Posts 改口径之后重建历史:标记价加权和 Polymarket Protocol V2 接入
Post
Cancel

改口径之后重建历史:标记价加权和 Polymarket Protocol V2 接入

  增量 PnL 引擎把每天的结果登记成带指纹的按天事实。10-10 到 10-12,标记价规则和 V2 视图翻译发生了变化,规则版本也要进入指纹:原始账本行不变,计算结果仍可能改变。V2 接入同时增加了新的账本输入。

一个对不上的钱包

  线索来自一个钱包,我们算出的 30 天盈亏与 Polymarket 的 user-pnl-api 相差很大。调查到的流动形状是:收到 YES + NO 成对份额,在 maker 侧卖出,收盘后把剩余的成对份额转回去。

  直接转账按转账时点的公允价值计入现金流,所以收盘后转回去的那些成对份额,估值高低直接决定它的盈亏。问题出在标记价 mark(t, T) 上:T 之前 2000 块(约 50 分钟)内的成交均价,当时写的是按成交额加权:

1
旧:Σ(usdc · price) / Σusdc      新:Σusdc / Σshares

  price 本身等于 usdc / shares,按成交额加权会让高价成交的权重更大,均价偏高。举个算术例子,同样 100 份,一笔 0.9 成交($90),一笔 0.1 成交($10),按份数加权是 0.5,按成交额加权是 (90×0.9 + 10×0.1) / 100 = 0.82。一个 5 分钟的 BTC 涨跌市场在收盘前后价格剧烈地往两头走,YES 和 NO 两边的均价同时被抬高,收盘后一对份额合计估成 1.716,而一对份额的真实价值最多是 1。改成按份数加权后是 1.026。

  提交 d172bbb 记录,修正后 Top 100 的留存率为 72–76%。

  同一个提交还增加冷清市场的估值处理:两个 token 近期都没有成交时,用两边里更近的那笔成交价;结算 payout 可用时优先读 payout。

估值规则要进指纹

  改的是 markCte 和 valueCte,账本一行没变,按天指纹也就一样,旧的天会一直用旧口径。于是每天的指纹加了一项 val=<VALUATION_VERSION>,当前是 2,规则一改就加一,增量引擎从创世起全部重建。中间表 stats_fpc 的累计列从 cumPV 换成 cumSh,迁移 v26→v27 重建这张表,没有用 ALTER(ALTER UPDATE 属于 mutation,进行中的统计会因此拒发)。长周期收益曲线的点是快照,算完不随后来的重建自动更新,所以另写了 inc-curves 去重算登记的输入已过期的点。

  重建 57 天是用 inc-build --max 6 分段跑的,每天 10–14 分钟,段与段之间歇 15 分钟,避免持续高 CPU 再触发主机商的限流。驱动脚本有个小坑:它的停止条件是「这一段 built 0」,跨过 UTC 午夜时会多建出新的一天,挤掉 01:00 那一轮 30d 的时段。之后的做法是看到 N/N valid 就停,最后一天留给 stats 自己补。

Protocol V2 是什么

  10-10 收到的计划是 Protocol V2 在 10-05 到 10-30 灰度,暂定 11-02 起新建市场走 V2。这里保留当时的计划日期。核算这些市场需要接入新的份额和生命周期事件。

  读完合约之后,第一个发现是:V2 的撮合合约 ExchangeV3(0xe3333700…)就是我们 contracts.ts 里一直标着 pm_unknown_v2 的那个地址。它的 OrderFilled / OrdersMatched / FeeCharged 和 CTF Exchange V2 逐字节相同,我们从创世起就在收它的成交,撮合分组也都校验通过,只是 scorable: false,不进盈亏。代码注释里写的「不发 OrdersMatched/FeeCharged」是错的。

  和 CTF 的主要区别:份额不在 CTF 合约里,在 PositionManager(标准 ERC-1155)上;拆分、合并、兑换由三个模块完成(二元、NegRisk、组合单);抵押品只有 pUSD。position id 的 256 位是结构化的:

1
moduleId 8 | baseHash 128 | arity 16 | reserved 64 | resolutionChain 16 | conditionIndex 16 | outcomeIndex 8

  合约的 bytes31 conditionId 可由 id >> 8 得到;账本统一存 bytes32 时要在右侧补零字节,等价于清零 tokenId 的最低 8 位,不能直接把右移后的数当成账本 conditionId。二元对立结果是 id ^ 1。推导不需要 RPC 反查,但 map-v2 仍写 conditions 和 tokens_backfill,供现有核算与 partialHistory 读取。

  10-10 的只读调查记录,ExchangeV3 成交约占全部成交额的 1%,其中约 99% 是组合单。接入分 A(入账)、B(二元计分)、C(NegRisk 计分)三期,组合单暂不计分。

A、B 两期

  A 期代码在 10-10 提交,项目笔记记录 10-11 起上线:PositionManager 转移进 pm_transfers,模块、自动兑换、合约升级事件进 v2_events,live 加第 4 个 getLogs 过滤器。V2 解码失败记录到 v2_decode_failures 并告警,不阻断 CTF 成交入账;发布侧另检查未修复失败。backfill-v2 从归档补已提交块的 V2 行,不写提交标记,不碰 fills。

  B 期在 10-11 接入二元计分,用四个视图把 V2 翻译成 CTF 形状:pos_transfers_canonical、lifecycle_canonical、redemptions_all_canonical、resolutions_all_canonical。PnL、preflight、增量引擎和曲线改读视图,并增加 V2 成交计分判断。生命周期归因信任 Router 和交易所,自动兑换用自动兑换合约自己的事件单独处理;不能把所有自动兑换模块事件直接当成用户现金流。发布侧检查不计分场所占比,以及窗口内未修复的 V2 解码失败。

  笔记记录,上线时 map-v2 映射了 544,202 个条件,多数是只有结算事件的迁移镜像;增量引擎从 08-29 起重建,每批 6 天约 100 分钟。当时 gamma 未能补齐 V2 名称,10-11 的 5b1b1dc 给 backfill-markets 加了与实时命名相同的 CLOB 回退。

10-12 的两个问题

  10-12,30d 连续 5 轮拒发,stats worker 退出。V2_OPEN_FAILURES_SQL 的 count() 返回数字,代码却与字符串 '0' 严格比较,零失败也被拒绝。01860c9 改成数字比较,ae4b6a3 补上不带 force、没有解码失败时正常发布的测试;此前测试主要覆盖 force 或拒发路径。

  第二个问题是看 30d 结果时发现的:V2 二元的 1,292 行里有 1,229 行被标成 lifecycleUnpaired 排除。ExchangeV3 撮合 MERGE 时,交易所把卖方的份额直接转进模块(operator = ExchangeV3),再由模块的 merge 把钱付给交易所。视图把这些转移改写成了卖方的销毁,而卖方自己并没有 merge 事件,配不上对。只有这类成交的窗口会被管线排除闸门 100% 拒发。修法是 operator 为 ExchangeV3 的转移保留模块地址,按交易所腿归类。

  这次修改同样碰到了指纹的问题:按天指纹哈希的是原始 V2 行,视图怎么翻译它们,指纹看不出来,B 期翻译下建好的天会一直带着 MERGE 的错误排除。做法和估值版本一样,非空的 v2pm / v2ev 指纹分量带上 V2_TRANSLATION_VERSION(=2),空的保持为空,所以只有含 V2 行的天会重建。

C 期和收尾

  C 期的 NegRisk 计分代码在 10-11 提交,10-12 部署并修复 MERGE 翻译。模块侧的铸造和转移通过伪地址 V2_NEGRISK_FLOW 归为 NegRisk 流动,拆分、合并、转换、横向拆合、兑换、自动兑换各自生成期望腿。V2 的 NegRisk 转换不动抵押品。每个自动兑换与同 tx、同 position 的相应模块兑换配对,避免重复使用整笔交易的兑换总额。

  部署时 map-v2 一次拼接整个 V2 时期的条件,撞到 V8 字符串长度上限,报 RangeError: Invalid string length。c52e06b 将 writeV2Conditions 改成每 2 万行一批。笔记记录,59 天重建从 06:00 UTC 跑到 18:32 UTC。

  影子比对先遇到 inputs changed:inc-assemble 不持映射锁,names 持续登记新的 V2 条件。给影子加锁会与正式计算的排队意向锁冲突;只限 block 也挡不住晚登记改变 partialHistory,这两种方案没有采用,最后暂停 names 再比。FULL JOIN 5,360 万行撞上 4 GiB,改为先用行哈希筛候选,再用 clickhouse local 比较候选行。记录中的结果是非 V2 行没有发现差异,V2 二元有 382 行从 unpaired 排除变成计入;哈希筛选有碰撞可能,这不是对全部行的逐字节证明。

  10-13 又出现调度问题:C 期部署脚本把曲线重算 inc-curves 放在启动 stats 前面,每轮约 50 分钟只算 2–3 天,错过了 01:00 UTC 的 30d 时段。后续将曲线重算安排到榜单发布后的空档。

  截至这次接入记录,CTF → V2 迁移镜像配对仍未实现;C 期之后还留有 52 行 V2 二元 lifecycleUnpaired 待查。


pmwallets 工程笔记(7/7)

上一篇:从全量重算到增量 PnL:按天事实、晚到的映射和被限流的 CPU

This post is licensed under CC BY 4.0 by the author.

LPSignal 的卡支付:免费试用、Managed Payments 和从 Stripe 回来按钮全灰

回测沙箱:锁定内置对象、删除 shell 和提交检查