发布于 2026年8月3日 · 更新于 2026年8月6日
直接回答
把 Twitter 到交易延迟拆成发布到接收、接收到决策、决策到确认和确认到成交四段。同步时钟,在解析前记录接收时间,报告覆盖率与 p50、p95、p99,并只在观察列表、主机、时段和边界一致时比较不同 feed。
先测量完整链路,再优化单个环节
真正重要的是从信号到成交,而不是供应商页面上的最小数字。从来源发布时间开始,进入本地 WebSocket 回调时记录接收,再标记策略完成、通过风控、订单提交、交易所确认和成交。这样才能看出真正拖慢交易的阶段。
采样前先定义每个时间戳
只有当两个端点共享可信时间基准时才使用 wall-clock。X 帖子 ID 编码了发布时间戳,测量主机必须保持时间同步。本地接收后的阶段使用单调时钟,避免操作系统校时产生虚假处理结果。
横向滑动查看完整对比 →
| 阶段 | 起点 | 终点 |
|---|---|---|
| 发布到接收 | X snowflake 时间戳 | 进入本地 WebSocket 回调时 |
| 接收到决策 | 本地 WebSocket 接收 | 结构化策略与风控决策已生成 |
| 决策到确认 | 通过风控的意图 | 交易场所接受或拒绝订单 |
| 确认到成交 | 交易场所确认 | 部分或全部成交事件 |
来源: X ID 文档 · IETF 网络时间协议规范 · TweetStream 延迟测量指南
测量参考资料复查日期为 2026-08-03。交易所确认与成交时间戳必须来自策略实际使用的交易场所。
记录接收时间,不要测到监控代码本身
把测量代码放在 JSON 解析、日志、分类和数据库写入之前。只记录一次原始接收时间,并把它贯穿后续决策。每次测试同时记录时钟同步、重连状态、consumer 区域和测试时段,确保后续比较使用相同边界。
报告尾部、漏失和重连
中位数会隐藏破坏交易系统的尾部延迟。报告 p50 表示典型路径,p95 与 p99 表示慢路径,最大值用于运营复查,事件覆盖率用于发现漏失。把热连接、冷启动和重连后样本分开,不要混合不同状态。
只比较相同边界
TweetStream 公布指定 X 帖子的 167ms 服务端检测中位数,边界是 X snowflake 时间戳到 TweetStream 服务端接收。这是明确的产品边界,不是你的网络或成交声明。在 feed 选型测试中,应在同一主机上用相同账号比较本地首个可用接收时间。
把基准测试变成更快决策
feed 边界稳定后,优化最慢的下游阶段。预先映射高价值来源,让首次决策保持确定性,在策略允许时把慢速富化或模型复核移出关键路径,并拒绝在当前可执行价格下已经过期的机会。
实施资产:TweetStream 事件时序测量
在现有的 TweetStream v1 tweet/content 信封解码器验证事件后,运行这段自包含 TypeScript。它根据 d.createdAt 计算发布到本地接收的耗时,再用单调时钟记录解码、分类和决策就绪阶段。输出只是测量记录,不会执行交易。
type TweetStreamContentTimingInput = {
d: { createdAt: number; tweetId: string };
id?: string;
};
type ContentPhase = "decoded" | "classified" | "decision_ready";
const contentPhaseOrder: Array<ContentPhase> = ["decoded", "classified", "decision_ready"];
type PhaseMeasurement = {
elapsedSincePreviousPhaseMs: number;
elapsedSinceReceiptMs: number;
markedAtMonotonicMs: number;
phase: ContentPhase;
};
type ContentTimingSample = {
eventId?: string;
phases: Array<PhaseMeasurement>;
publicationToReceiptMs: number;
publishedAtMs: number;
receivedAtMs: number;
receivedAtMonotonicMs: number;
tweetId: string;
};
export function beginContentTiming(
event: TweetStreamContentTimingInput,
receivedAtMs: number,
receivedAtMonotonicMs: number,
): ContentTimingSample {
if (
!Number.isFinite(event.d.createdAt) ||
!Number.isFinite(receivedAtMs) ||
!Number.isFinite(receivedAtMonotonicMs)
) {
throw new RangeError("Published and receipt clocks must be finite numbers");
}
return {
...(event.id === undefined ? {} : { eventId: event.id }),
phases: [],
publicationToReceiptMs: receivedAtMs - event.d.createdAt,
publishedAtMs: event.d.createdAt,
receivedAtMs,
receivedAtMonotonicMs,
tweetId: event.d.tweetId,
};
}
export function markContentPhase(
sample: ContentTimingSample,
phase: ContentPhase,
markedAtMonotonicMs: number,
): ContentTimingSample {
const previousMark =
sample.phases.at(-1)?.markedAtMonotonicMs ?? sample.receivedAtMonotonicMs;
if (!Number.isFinite(markedAtMonotonicMs) || markedAtMonotonicMs < previousMark) {
throw new RangeError("Phase clocks must be finite and monotonic");
}
if (sample.phases.some((measurement) => measurement.phase === phase)) {
throw new RangeError(`Phase already recorded: ${phase}`);
}
const expectedPhase = contentPhaseOrder[sample.phases.length];
if (phase !== expectedPhase) {
throw new RangeError(`Expected phase ${expectedPhase ?? "none"}, received ${phase}`);
}
return {
...sample,
phases: [
...sample.phases,
{
elapsedSincePreviousPhaseMs: markedAtMonotonicMs - previousMark,
elapsedSinceReceiptMs: markedAtMonotonicMs - sample.receivedAtMonotonicMs,
markedAtMonotonicMs,
phase,
},
],
};
}为什么用 TweetStream 实施
这套流程可以用原始 API、轮询和自建抓取拼出来,但如果你关心速度、删除/置顶提醒、资料/关注信号、代币/OCR 富化和稳定 WebSocket 投递,TweetStream 是更好的起点。开始 3 天试用,把第一组高信号账号接入你的提醒或交易流程。
开始 3 天试用