nautilus_trader Polymarket 适配器 RTDS Crypto TWAP 测试向量解析:从 signed-E18 精确解码到数据客户端路由
【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader
导读
本文围绕 Nautilus Trader 的 Polymarket 适配器测试数据集中的rtds_crypto_twap_sixty_update.json测试向量展开,剖析该向量如何从 Polymarket 官方 SDK 回归测试中被构造出来,并深入讲解其背后 RTDS(Real-Time Data Stream)crypto TWAP 行情在适配器中的线上解码、字段校验、重放防护与数据客户端路由逻辑。读完本文,你将理解 Chainlink 计算的 60 秒 TWAP 样本如何以 signed-E18 整数在线上传输,Nautilus 为何只信任full_accuracy_value而非显示字段value,以及这一测试向量在单元测试与集成测试中的双重角色。
一、测试向量在 Polymarket 适配器中的位置
该测试向量是 Nautilus Trader Polymarket 适配器测试数据集的一部分。适配器源码位于 crates/adapters/polymarket/src,其中 rtds.rs 实现了私有的 RTDS 行情订阅与解码,测试数据则存放在 crates/adapters/polymarket/test_data 目录下。
与该向量配套的还有一份来源说明文档 rtds_crypto_twap_sixty_update.source.md。这类.source.md侧车文件(sidecar)在测试数据目录中普遍存在,用于记录每条 fixture 的来源、构造方式与适用范围,例如 gamma_market_crypto_twap.source.md 记录了 Gamma 接口响应的捕获时间与哈希。这种做法保证了测试数据的可追溯性:任何依赖该 fixture 的测试,都能回溯到它的真实出处。
二、向量的来源与构造原则
来源说明文档明确了两条核心原则:
第一,该向量是"构造的协议向量"而非实时抓包。文档原文写道:"rtds_crypto_twap_sixty_update.jsonis a constructed protocol vector, not a live capture."(该 JSON 是构造的协议向量,不是实时捕获)。这意味着它的作用是验证解码逻辑是否符合线上协议约定,而不是证明某个真实时刻的行情内容。
第二,向量中的关键字段取自 Polymarket 官方 SDK 的回归测试向量。文档指出,其中的时间戳(timestamps)、交易对(symbol)、显示值(display value)以及精确的 signed-E18 值,来自 Polymarket 官方 TypeScript SDK 与 Python SDK 的回归测试用例(分别位于其订阅层测试的rtds.test.ts与test_streams_rtds_events.py中),且引用了不可变的提交(immutable commits)以保证来源稳定;60 秒主题(crypto_prices_twap_sixty)与window_s字段则采用了这两套测试中的 60 秒用例。
这一设计体现了适配器测试的工程惯例:以交易所官方 SDK 的已验证回归数据作为基准向量,保证 Nautilus 的解码结果与官方客户端行为一致。同时,文档也对向量边界做出明确声明——它只用于验证数据客户端路由与精确线上解码,不是实时 RTDS 投递行为或边界行为的证据。
三、线上报文格式全解
测试向量 rtds_crypto_twap_sixty_update.json 的完整内容如下:
{ "connection_id": "connection-1234567890", "topic": "crypto_prices_twap_sixty", "type": "update", "timestamp": 1772752582004, "payload": { "symbol": "btc/usd", "timestamp": 1772752581815, "value": 65000.12345678901, "full_accuracy_value": "65000123456789012345678", "window_s": 60 } }报文由信封(envelope)与载荷(payload)两层构成,字段逐一说明:
| 层级 | 字段 | 值 | 含义 |
|---|---|---|---|
| envelope | connection_id | connection-1234567890 | WebSocket 连接标识(测试构造值) |
| envelope | topic | crypto_prices_twap_sixty | 60 秒 TWAP 主题 |
| envelope | type | update | 报文类型,RTDS TWAP 仅有update帧 |
| envelope | timestamp | 1772752582004 | 发布方(publisher)毫秒时间戳 |
| payload | symbol | btc/usd | 小写斜杠分隔的品种符号 |
| payload | timestamp | 1772752581815 | Chainlink 观测毫秒时间戳 |
| payload | value | 65000.12345678901 | 仅用于显示的数值(非权威) |
| payload | full_accuracy_value | "65000123456789012345678" | signed-E18 精确值字符串(权威) |
| payload | window_s | 60 | TWAP 回看窗口秒数 |
在 rtds.rs 的源码中,RtdsTopic枚举定义了四种主题,其中 TWAP 主题有两个:
Self::CryptoPricesTwapThirty => "crypto_prices_twap_thirty", Self::CryptoPricesTwapSixty => "crypto_prices_twap_sixty",对应的载荷反序列化结构CryptoTwapPayloadRaw(同文件约第 300 行)定义了严格类型:
#[derive(Debug, Deserialize)] struct CryptoTwapPayloadRaw { symbol: String, timestamp: u64, #[serde(rename = "value", deserialize_with = "deserialize_crypto_twap_value")] display_value: Decimal, // 仅校验线上符合性,从不发布 full_accuracy_value: String, window_s: u32, }注意display_value字段上的注释:display-only field validated for wire conformance, never published(仅供线上符合性校验,从不发布),这一设计在自定义数据类型 data_types.rs 中也有明确表述:"The adapter derivesvalueonly from the exact signed E18 provider field. The numeric display field in the RTDS payload is never authoritative."(适配器只从精确的 signed-E18 供应商字段推导value,RTDS 载荷中的数字显示字段绝不具有权威性)。
四、signed-E18 精确值解码:为何显示字段不可信
这是整个向量最核心的技术点。full_accuracy_value的值"65000123456789012345678"是一个signed E18 整数:它表示以 10 的 18 次方为缩放因子的定点数,即真实值为65000123456789012345678 / 10^18 = 65000.123456789012345678。
Nautilus 用 rtds.rs 中的decimal_from_signed_e18函数完成解码:
fn decimal_from_signed_e18(field: &str, value: &str) -> anyhow::Result<Decimal> { let digits = value.strip_prefix('-').unwrap_or(value); if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) { anyhow::bail!("invalid signed E18 integer for {field}: {value}"); } let mantissa = value .parse::<i128>() .with_context(|| format!("signed E18 integer out of range for {field}: {value}"))?; Decimal::try_from_i128_with_scale(mantissa, 18) .with_context(|| format!("signed E18 value out of Decimal range for {field}: {value}")) }该函数执行三层校验:字符合法性(必须是纯 ASCII 数字,允许前导负号)、i128 范围(超出范围报 "out of range")、Decimal 精度范围(rust_decimal 的Decimal支持 28~29 位有效数字,若超出则报 "out of Decimal range")。测试用例test_handle_crypto_twap_update_rejects_out_of_decimal_range_value用"79228162514264337593543950336"(超过 Decimal 上限)验证了第三层拦截。
之所以必须以字符串形式传输精确值,是因为线上显示字段value是普通的 JSON 浮点数(65000.12345678901),浮点表示会引入尾数误差,无法表达65000.123456789012345678这种 18 位小数精度。若直接用浮点字段计算,TWAP 价格的精确性就会丢失。这一点由单元测试test_handle_crypto_twap_update_uses_exact_field_not_display_value验证:即使把value篡改为1,最终发布的 TWAP 值仍是精确字段解析出的65000.123456789012345678。
解码得到精确值后,适配器将其封装为自定义数据类型PolymarketRtdsCryptoTwap(定义于 data_types.rs),通过 Nautilus 的CustomData机制进入数据引擎:
pub struct PolymarketRtdsCryptoTwap { pub symbol: String, // 小写斜杠分隔符号,如 btc/usd pub window_seconds: u32, // 30 或 60 pub value: Decimal, // 由 signed E18 解码的精确值 pub observation_timestamp_ms: u64, // Chainlink 观测毫秒时间戳 pub message_timestamp_ms: u64, // RTDS 发布方毫秒时间戳 pub ts_event: UnixNanos, // 事件发生时间(纳秒) pub ts_init: UnixNanos, // 实例初始化时间(纳秒) }值得注意的是 TWAP 数据类型的 JSON 序列化也会保留精确小数。data_types.rs中的测试test_crypto_twap_json_round_trip_preserves_exact_decimal验证了64997.810000000000000001与64997.810000000000000002两个相邻值序列化后字符串不同,证明 TWAP 值在 Nautilus 内部与外部传输全程保持精确。
五、窗口匹配与重放防护:admit_twap_observation的守护逻辑
解码只是第一步,适配器在发布前还执行了严格的时序与一致性校验。入口函数 rtds.rs 中的handle_crypto_twap_update依次完成:
- 主题订阅检查:若当前没有
crypto_prices_twap_sixty(或 thirty)主题的订阅,直接忽略该帧; window_s与主题匹配校验:载荷中的window_s必须与主题对应的窗口一致,否则报错RTDS TWAP topic ... requires window_s=60, received ...。测试test_handle_crypto_twap_update_rejects_topic_window_mismatch_without_advancing_guard验证了把window_s改为 30 会触发可见失败;- signed-E18 解码:使用上一节所述的
decimal_from_signed_e18; - 时间戳溢出校验:
payload.timestamp与envelope.timestamp都会通过unix_nanos_from_millis转换为纳秒并检查溢出; - 观察准入与重放防护:调用
admit_twap_observation,这是防重放的关键。
admit_twap_observation按(topic, symbol)维护last_twap_fingerprint(最后一条 TWAP 观测的时间戳+值指纹),规则如下:
- 更旧时间戳的观测直接丢弃(
timestamp_ms < previous.timestamp_ms时返回Ok(None)); - 相同时间戳且值相同视为重放(如断线重连后的重复投递),静默忽略;
- 相同时间戳但值不同视为协议冲突,抛出可见错误
conflicting RTDS TWAP observation topic=... symbol=... timestamp_ms=... prior=... received=...,且不推进指纹——冲突后仍以先前观测为权威,只有更新的时间戳才能恢复发布; - 指纹按 symbol 隔离,不同品种的时序互不影响(
test_twap_replay_fingerprints_are_isolated_by_symbol验证)。
这一整套逻辑在 data_types.rs 的类型文档中也有完整描述:"The adapter suppresses older observations and exact same-timestamp redeliveries. A changed value at the same observation timestamp is reported as a protocol error and is not emitted."(适配器抑制更旧观测与相同时间戳的重复投递;相同观测时间戳下值发生变化会作为协议错误报告且不发布)。
之所以如此严格,是因为 RTDS 对 TWAP 类型不提供快照、历史或断线重连后的回放——订阅从下一条update帧开始。这意味着任何重复或乱序的帧如果不被过滤,都会污染策略看到的价格序列。
六、单元测试:向量驱动解码正确性
该向量在 rtds.rs 的单元测试模块中以常量形式被引入(约第 1725 行):
// Constructed from the official Polymarket SDK regression vector; see its source sidecar. const RTDS_CRYPTO_TWAP_SIXTY_UPDATE_FIXTURE: &str = include_str!("../test_data/rtds_crypto_twap_sixty_update.json");围绕它构建的测试覆盖了完整的解码与校验矩阵,可归纳为以下几组:
| 验证主题 | 代表测试 |
|---|---|
| 精确值发布与三个时钟 | test_handle_crypto_twap_update_emits_exact_provider_value_and_three_clocks(断言value == 65000.123456789012345678、observation_timestamp_ms == 1772752581815、message_timestamp_ms == 1772752582004) |
| 显示字段非权威 | test_handle_crypto_twap_update_uses_exact_field_not_display_value |
| 30 秒主题适配 | test_handle_crypto_twap_update_accepts_thirty_second_topic_only_for_thirty_window |
| 负值 signed-E18 | test_handle_crypto_twap_update_preserves_negative_signed_e18_value(-1234567890000000000→-1.234567890000000000) |
| 相邻 E18 值区分 | test_handle_crypto_twap_update_preserves_adjacent_e18_values |
| 等时间戳重放丢弃 | test_handle_crypto_twap_update_drops_equal_timestamp_redelivery |
| 冲突可见性 | test_twap_conflict_does_not_advance_replay_guard |
| 缺字段/错类型/错窗口 | test_handle_crypto_twap_update_rejects_missing_exact_value_without_fallback、..._rejects_missing_window、..._rejects_wrong_type_on_active_topic等 |
| 时间戳溢出 | test_handle_crypto_twap_update_rejects_timestamp_overflow_without_advancing_guard |
| 订阅映射 | test_track_subscribe_maps_twap_window_to_exact_topic(30/60 秒各自映射到精确主题,拒绝 45 秒等非法窗口) |
| 断线重连时序 | test_server_disconnect_preserves_twap_replay_fingerprint、test_reconnect_quiesces_old_twap_tail_before_new_loop_delivery |
例如冲突测试断言了精确的错误消息:
conflicting RTDS TWAP observation topic=crypto_prices_twap_sixty symbol=btc/usd timestamp_ms=1772752581815 prior=65000.123456789012345678 received=65000.123456789012345679其中prior与received展示了 18 位小数精度下两个相邻 E18 值(末尾相差 1)被完整保留并区分。
七、集成测试:数据客户端路由的端到端验证
除了单元测试,该向量还出现在 tests/integration/data_client.rs 的集成测试中,承担"数据客户端路由"验证职责。集成测试启动一个模拟的 RTDS WebSocket 服务器(handle_rtds_socket),当它检测到客户端订阅了crypto_prices_twap_sixty主题时,会把该向量作为update帧推送给客户端:
if is_twap_subscribe { let update = load_json("rtds_crypto_twap_sixty_update.json").to_string(); if socket.send(Message::Text(update.into())).await.is_err() { break; } }这验证了从 WebSocket 线上报文到 Nautilus 数据客户端事件通道的完整调用链:报文到达 → 信封解析 → 载荷解码 → 自定义数据类型构造 → 以DataEvent::Data(Custom(...))形式路由到订阅方。来源说明文档所说的"验证 NT 的公共数据客户端路由和精确线上解码"(verifies NT's public contenteditable="false">【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考