目前只做工程方面的总结,不做策略数据方面的复盘。这个项目是从一个小的页面开始,逐步发展形成了一套复杂的数据看板。在这个过程中踩了一些有意思的坑,这些坑都不高级,属于一看就知道很简单的问题,但为什么会遇到这么多低级问题呢?因为用了 AI。真正的关键不在于让 AI 写代码,而在于 AI 比较死板,标准比较生硬和严格,尤其是在这次做资金费率套利机会的数据看板中,这种现象体现的更加明显。所以这也是 AI 能力边界的一个体现。AI 是不是比程序员厉害?是。AI 能不能代替程序员?分情况。
一开始只是做了一个简单的资金费率排行榜单,把 Hyperliquid 上的全部美股,按照实时资金费率做了个排行。是一个纯前端页面,到现在都能访问:https://hyper.dark20.xyz/ 。页面上提供了回测资金费率的功能,直接回测榜单前几名,在不同周期内的资金费率情况。关注到美股的资金费率套利,很大一部分原因是今年的美股上链很火,RWA 等概念逐渐正规化,似乎给链上带来了新的活力。
再后来开始往页面上加资金费率套利回测的数据和功能。与之前的区别在于,真正的套利需要开两腿交易,只回测资金费率还不够,需要结合现货价格,把开关仓的成本给算上。当然到这一步的时候,整体回测下来的收益并不高,普遍在 10%~ 20% 的范围。这个收益还不够有吸引力,毕竟资金费率套利是广为人知的策略了。同时在做的是具体的 Hyperliquid Price Spreads 观察,实时检测现货价格和期货价格之间的差异,这个会影响到开关仓的成本估算。
接着从 Hyperliquid 横向扩展到了更多平台,Binance、Bitget、Lighter、DYDX 等,此时也新增了一种策略,就是跨交易所的资金费率套利。与现货+期货的套利策略相比,跨交易所得套利策略收益要高出很多倍,并且当时做了几个回测页面,比如指定同一个代币在不同平台上的费率差距情况、或者同一个平台上不同代币的费率情况等。直到最终觉得可以合并了,干脆就不同平台的全部代币直接交叉,在同一个页面上展示一个表格出来。当时还花了几天时间研究资金费率的 APR 到底应该怎么算等问题,有哪些关键数据、滑点这么考虑等。重
这些都是前置条件,不展开描述。只是在解释经过了这样一些调研的过程,最终确定了想要的网页形态,就是一个跨平台+多币种交叉的榜单。
所以在一开始,页面榜单的数据,都是纯前端加载的。打开页面之后页面实时请求多个平台的全部代币数据,然后计算这些代币在不同费率下的收益情况,最终得到一个排行榜。当然最明显的问题就是,从打开页面到能看到结果,需要长达几分钟的时间,这样使用体验太不好了。
然后改成了手动触发计算任务的方式。打开页面,设定好想要的参数,包括哪些平台、哪些代币,点击计算,会向后端发送一个任务请求,后端持续请求各平台数据、汇总结果并且返回。在计算任务期间就可以离开页面了,请求任务也会按照时期或者任务的 ID 记录到用户的操作记录中,这样用户随时可以查看到以前的任务运行状况,以及最新的计算结果。要强调的是,这一切都运行在一个 1 CPU 2GB Memory 的 AWS 服务器上。
后来因为服务器上的资源竞争,就把这个排行榜程序独立出去,放到了一个 2 CPU 4GB Memory 的 Vultr 服务器上,用的 Cloud Compute 型号(属于廉价低性能款,20u/m)。迁移的同时干脆做了后端程序的重构,从 Nodejs 切换为 Rust 语言。以及从原来的手动触发计算,大幅调整架构,改为滚动采集全部数据+定时发布排行榜快照的方式。
自动采集数据自然是好的,同时采集数据失败也经常发生,举几个典型的例子:
请求 Bitget 接口的时候,多了一个 ?,比如原本计划请求 /api/v3/account/info,但实际构造出来了 /api/v3/account/info?,导致 Bitget 的接口认为传了一个空的字符串作为参数过去,就返回 Failed to fetch。
请求 Kucoin 资金费率接口,会得到 {"code": "200000", "data": null} 这样的响应,当时程序认为 data 是 null 所以报了异常,实际上是 Kucoin 的接口分页结束了就回返回 null。
类似的接口兼容类问题很多,不需要一一列举。这些算是意料之中的问题,采集那么多平台的数据,肯定要做好接口兼容。当时支持的平台一共有这些:Binance、Bitget、Bybit、OKX、Gate、KuCoin、MEXC、HTX、Kraken、Coinbase、Deribit、Hyperliquid、Lighter、dYdX、Aster、 Polymarket、Kalshi。
紧接着问题就出现了,采集数据的进度迟迟不推进。当时使用后端采集任务的数量来评估,比如请求 Binance 的 BTC 在 10月01日 14:00 的资金费率是一个采集任务(一个请求),请求同时间段的价格数据是另一个请求;这样堆积起来,十几个平台 * 一千多个代币 * 不同数据维度,最多堆积到了 15 万个采集任务。这样的数量显然是异常的。
优化方式也很简单,大多数平台的请求都可以合并,比如一次性请求 300 个代币在一天内的资金费率数据,这样一个请求直接合并上万个采集任务。需要进行这种优化的有五六个平台,当全部优化之后,任务数量就降低到了一两万个。
在数据采集有压力的情况下,有几个新的需求:
具体的门槛后来有调整,整体上没差太多。按照这个门槛,需要采集大量历史数据的代币有 100 个左右。同时期,采集任务的积压已经很严重,因为每时每刻都在产生新的采集任务,每过一小时,每个平台都会产生新的资金费率数据,采集任务的数量不降反增。为了缓解 CPU 压力,把服务器升级到了 4 CPU 8GB Memory 的规格(40u/m)。
同时也采用了一个优化手段,给代币数据的采集和快照的发布划分优先级。在用户收藏列表里的代币最高优先级采集全部数据、单独发布每个代币的快照信息。超过设定流动性门槛的代币,第二优先级采集数据、优先发布快照榜单。最后优先级的是其他的大众代币。不过优化的效果不够理想。
为了缓解数据压力、增加出榜单数据的范围,降低了几个数据的要求:
其实这个身份验证给我带来很多麻烦,这个标准是一开始 AI 自己加的,后来数次给很多其他功能带来阻碍。身份验证的意思是,比如在 Binance 看到了叫 CAT 的代币,并且在 Bitget 看到了叫 CAT 的代币,程序要求从两个平台的接口能够直接返回代币的合约地址并且合约地址能对得上,就认为这两个代币的身份是经过验证的、能够安心配对做套利计算的。这个逻辑和要求本身没什么问题,只是由于很多平台不返回合约信息,尤其是很多代币化股票需要传递不同参数的复杂性,导致能通过验证的代币其实不多,榜单上大量缺失正常数据。所以后来尽可能去掉了这层验证。
这个也非常好理解,比如最新数据只采集到 4 个小时前,那么出榜单的时候是忽略这个代币、还是同步采集完最新的数据、还是直接用 4 个小时前的数据做计算?原本的做法是会同步采集最新数据,但是服务器资源不允许,这样不但会拖慢整个榜单产生的速度,还会增加额外的采集压力。于是后来直接降低要求,只要 12 个小时内有数据就直接按照最新数据计算。榜单的数据成功率就高了不少。
这也是一个非常大的坑,很大程度阻碍了采集数据的进度。首先注意到这个问题,是发现 Bitget 对于某些代币,原本想要采集一天的小时级别的 K 线数据,但是实际上接口就是只返回 4 根 K 线。而按照当时的程序设计,程序把一天按照每个小时划分为 24 段,比如 K 线数据,会要求数据库里每个小时都有 K 线数据才算是完整覆盖,而且没数据的地方会反复采集,进度阻塞停止不前。而后全面改成了请求成功率的口径,只要请求成功且不是异常请求,就认为这个时间段内的数据已经覆盖。就这样看起来非常简单的改动,却极大推进了整个系统的采集进度。这个问题够低级吧?AI 是不会主动发现这种问题的,得你告诉他怎么改!
对于套利项目来说,我们肯定不会优先关心那些 APR 最低的代币,是吧?哪怕它只是现在这一刻低,也会意味着如果开仓这个代币,它现在的回撤会非常大,这也不是我们想要面对的风险。所以全部平台的代币有 8000 多个,其中能配对的有 1000 多个,这一千个左右的代币会最轻量级的采集实时资金费率数据,排序后选出 spread 最高的 300 个代币,然后再进一步采集 7d 的资金费率历史、30d 价格历史等数据。在这样的榜单口径下,后端一下子减少了 2/3 的压力,后端的数据逐渐就跟上了。
这也是事先没能料到的,为了观察后端整体数据的采集进度,当时是有一个页面分门别类展示每个平台各类数据的采集进度的。没想到这个请求每次都在扫描全表、还会有一个后台程序不断扫描,就为了在页面上展示一下采集进度。把这个东西删掉后 CPU 一下子释放了 20%。
数据的采集进度跟上以后,紧接着面临的问题有三个:
这几个问题看似属于不同阶段的问题,其实有很多混杂的地方,可以同步优化。当时发现并做了几个代码逻辑方面的调整:
以前把整个榜单的快照数据都放到一个 JSONB 里,程序每次都需要把超过 100M 的数据从数据库里读出来、反序列化解析到数据结构、然后计算、返回给页面(哪怕带缓存),其中光是 json 的反序列化就耗时 5 秒以上。虽然 Rust 里已经用了 Arc.clone 共享同一份内存,但是如果把数据写进 json 再读出来,就是一份新的内存,而程序里存在大量这种反复读取、反复序列化的问题,导致大量的重复计算。我不能断言这个和 Rust 语言有关,只是 Rust 语言的理念就是尽快释放内存,或者说用完就必须释放内存,导致程序里默认存很多这种用完释放然后反复序列化的逻辑。
再一个问题是即使内存里共享了内存,序列化到 json 里,json 里保存了多份数据。这也很好理解,哪怕是同样的数据,不同的程序也没有复用,而是自己单独存了一份,导致 json 的数据体积成倍增加。还有的问题比如 json 里每个快照都保存了完整的全部历史 K 线数据,可以精简。总之在经过优化后,无论是发布快照还是预热、重计算的缓解,json 的体积都降低了至少 1/3,减少到 20M 左右。不过减少体积还不够,后来又进一步把 json 里的关键字段拆开放到数据表上,进一步加快数据读取的速度。
很基础的逻辑,以前每个榜单都是一个单独的快照和数据,可以把每一种类型的数据结果都成块存下来,下次发布快照的时候直接复用上一个快照的增量数据。比如要看 7d 榜单,现在过了一个小时,只需要把最一开始的 1 小时数据拿掉、再把最新的一小时数据放进去,就可以得到最新的 7d 快照。而不再需要每个 7d 榜单都会滚动产生一份快照数据。
而且这样的数据拆分和共享,可以同时使用到快照、预热、重计算三个环节。最终的结果是,页面访问的时候,没有缓存的冷计算大概在 25 秒左右,显然好于原本的 60秒。但是仍然不够理想。当时的统计信息是,300 个代币一共有 27000 种组合,处理每种组合需要 0.6 毫秒,单看 0.6 毫秒不多,27000 个组合一累加就是 25 秒。此时服务器的 CPU 已经常规持续在 90% 以上了。
正当代码逻辑和数据共享层面的优化都已经很难有明显增益的时候,同时新的需求还在增加,要加上模拟盘等其他功能,感觉这个服务器长期下去不够用,因为 CPU 已经接近满载了。首先考虑的是能不能用另一个 4 CPU 8G Memory 的服务器分担压力,但是怎么分担是个问题,我一直想用 Redis 但是 AI 反复告诉我 Redis 没用,解决不了这个 CPU 频繁换页的问题,也解决不了内存不够的问题,Redis 做不到用磁盘换内存。可以考虑的方案是让另一台服务器直链这台服务器的数据库、把预热的 worker 给分散出去。
然后我就看 Vultr 有没有其他高规格的服务器,要是开一个 8 CPU 16G Memory 的服务器把两台服务器合并在一起会不会更好?但是 Vultr 的服务器又贵,80u/m 只能开到 6 CPU 16G Memory 的机器,如果是 vhp(高性能款)则需要 100u。同时我还在搜寻有没有性价比更高的服务器提供商。
性价比高的 VPS 自然是有的,最终直接用了 Hetnzer。虽然我早就知道 Hetnzer,据说有一半的以太坊节点都泡在 Hetnzer 上。在 Hetnzer Cloud 的常规性能机型里,我开了 CPX32 型号的机器,4 CPU 8G Memory。同样是 42u/m 的价格,结果一测试,CPU 性能比 Vultr 的那台机器高出三倍。我折腾了两个小时,把全部服务都迁移了过去。
迁移完之后进一步测试,排行榜打开的速度比原本快了 3 倍!就在当天上午,我实际测试打开网页、切换各种参数的速度,时长在 5s~ 20s 之间波动。昨天下午换了服务器再测试一遍,同样的页面路径(当然是 AI 给我测的),加载数据的时长降低到了 1s~ 5s,最长也就 5 秒钟,这已经完全是一个正常使用能够接受的速度了。
我之前从来没觉得服务器是个问题,也没觉得我应该去折腾服务器,我总觉得服务器环境属于非常基础也没有技术含量的事情,而我长期作为开发人员,也不太关心服务器这回事。直到这次直观体会到了服务器之间的差异。当软件层面已经很难有显著提升的时候,直接升级硬件也是个有效的手段!
另外,Vultr 倒是也提供了 vhp 机型,从网上的测评来看,vhp 的 CPU 能力似乎和 CPX 差不太多,是同一个级别的,也没有很贵,只需要 48u/m。不过我不再需要 Vultr 了,因为我在 Hetzner 的 Dedicated 服务器里,买到了性价比最高的 AX41,只需要 70u/m 的价格,6 CPU 64 GB Memory + 2x 512 SSD。尽管这个 6 核的 CPU 是老款芯片,据说和云服务器上的 CPX32 也差不了太多,但是 64 GB 的内存,这以前在 AWS 上是不敢想的宽裕配置。
你应该看到了,这都是一些简单的技术问题,AI 有能力把每一个具体的技术问题解决好,但还不具备整体布局的能力,软件的演进,依然需要遵守软件工程的客观规律。这也是以后软件开发仍然需要工程师的原因。