Hyperliquid Info API 提供 candleSnapshot 方法,针对某个币种和 interval 返回一个 K 线对象数组,并由 startTime/endTime 窗口限定范围。由于每次请求都有边界,构建连续 OHLCV 历史数据的方式是按顺序推进游标跨越各个窗口,以 K 线开盘时间 t 去重,并显式检查缺失的 interval。按你打算存储的原生 interval 拉取,可以避免重采样更粗粒度 K 线时累积的舍入误差和成交量归属错误。随后必须将历史 K 线与实时 K 线 WebSocket 频道对账,否则归档中的最后一根 K 线会冻结在轮询发生的那一刻。本指南记录了请求契约、interval 语义、窗口分页、可运行的 Node.js 采集器、由读者自行填写的结 果表、故障模式,以及历史深度与请求成本之间的权衡。
candleSnapshot 请求契约与响应结构
Hyperliquid Info 端点将 candleSnapshot 记录为一个 POST 请求,其请求体携带 type 为 candleSnapshot、coin、interval 以及 startTime/endTime 窗口。响应是一个 K 线对象数组,而不是单个对象,正是这个细节决定了本指南后续所有分页决策。请将 Hyperliquid 文档,Info 端点 视为确切字段名和可接受 interval 字符串的权威来源,因为各服务商复制的方法参考可能会发生偏移。
每个 K 线对象携带 t(开盘时间)、T(收盘时间)、s(交易对符号)、i(interval)、o/h/l/c(开盘、最高、最低、收盘)、v(成交量)和 n(成交笔数)。开盘时间 t 是归档的自然主键:它在不同请求之间保持稳定,因此当窗口重叠时,它是去重时应使用的正确字段。收盘时间 T 由 interval 推导而来,因此它对缺口检测有用,但不适合用于身份标识。
请求受你提供的窗口限制,服务器最多返回它在一次响应中愿意返回的 K 线。这一句话就是为什么一次简单的调用无法产生多年历史数据的原因,也是本文其余部分所构建的机制基础。关于市场数据可用的更广泛接口,请参阅 Hyperliquid 历史市场数据 API 接口。
- 请求字段:type、coin、interval、startTime、endTime。
- 响应:一个 K 线对象数组,而非包装对象。
- 身份字段:t(开盘时间);派生字段:T(收盘时间)。
- 成交量与成交笔数:v 和 n,归属于 K 线自身的 interval。
Interval 语义,以及为何原生 interval 优于重采样
interval 字段选择 K 线宽度,文档中记录的 interval 字符串映射到固定的毫秒宽度。当你请求一个更粗的 interval 然后将其重采样为更细的 interval 时,你是在凭空捏造交易所从未发布过的开盘、最高、最低和收盘值。1 小时 K 线的最高价并不是其中任何一根 1 分钟 K 线的最高价,因此重采样得到的 1 分钟序列会在回测最关心的那些点上与交易所自身的 1 分钟序列不一致。
成交量归属是重采样的第二个受害者。1 小时 K 线的 v 是该小时内的总成交量,将其拆分到十二根 1 分钟 K 线上需要一个数据本身并不包含的假设。如果你的策略按每分钟成交量来确定仓位大小,这个假设就会成为静默的错误来源。按你打算存储的原生 interval 拉取,并将 interval 字符串与每根 K 线一起存储,这样来源就毫无歧义。
同样的推理也适用于成交笔数 n。它是 K 线自身窗口内的计数,没有底层成交数据就无法分解为更细的窗口。如果你确实需要多个粒度,请分别原生拉取每一个,而不是从一个推导出另一个。当你的策略同时消费资金费率时,Hyperliquid 资金费率机制 页面是一个有用的配套资料,因为资金费率同样是按其自身的时间表发布的,而不是从 K 线推导出来的。
窗口分页、游标推进与缺口检查
由于请求有边界,连续历史数据是通过发出有序的窗口请求并按 interval 毫秒宽度推进游标来构建的。将游标从你期望的 startTime 开始,请求一个窗口,追加返回的 K 线,然后将下一个窗口的 startTime 设置为最后一根 K 线的 t 加上一个 interval。这种顺序保证在顺利路径下你永远不会重复请求同一个窗口,并且如果进程在运行中途死亡,循环可以轻松恢复。
按 K 线开盘时间 t 去重,而不是按数组位置去重。当你从检查点恢复,或故意重新请求边界窗口以确认其完整性时,窗口重叠很常见,而以 t 为键的 Map 会确定性地合并这些重复项。对于给定的 t 保留最后一次写入,因为重新请求的边界 K 线在首次拉取时可能仍在进行中。
缺口检查是大多数实现会跳过的步骤。去重后,按 t 排序并遍历序列,断言每一对相邻 K 线之差恰好等于一个 interval 宽度。任何差值超过一个 interval 的相邻对都是缺口,它通常是一个没有成交的窗口,而不是传输失败。显式记录缺口,而不是静默插值,因为用合成 K 线填补缺口的回测是在测试从未存在过的数据。
- 按 interval 毫秒推进游标,而不是按固定的 K 线数量。
- 按 t 去重,保留该 t 最近一次拉取的 K 线。
- 按 t 排序,然后断言相邻差值等于一个 interval 宽度。
- 将缺口记录为数据,绝不静默插值。
将历史 K 线与实时 K 线 WebSocket 对账
如果回填在到达当前边界时就停止,归档中的最后一根 K 线会冻结在轮询发生的那一刻。Hyperliquid WebSocket 订阅文档描述了一个 K 线频道,它会在成交到达时推送进行中 K 线的更新,这正是保持最后一根 K 线最新的所需机制。Hyperliquid WebSocket 订阅与连接生命周期 页面更深入地介绍了连接生命周期。
对账规则很简单:通过 candleSnapshot 回填到当前边界,然后订阅同一币种和 interval 的 K 线频道,并在每次更新时按其开盘时间 t 替换进行中的最后一根 K 线。当进行中的 K 线收盘并开启新的一根时,已收盘的 K 线已经以它的 t 存在于你的归档中,而新 K 线会以新的 t 到达。这就是为什么按 t 去重很重要:WebSocket 更新和历史拉取描述的是同一根 K 线,归档必须为它保留一行。
边界本身是微妙的部分。如果你拉取的窗口 endTime 相对于交易所时钟处于未来,最后一根 K 线可能是部分的。只拉取到你确信已收盘的边界,然后让 WebSocket 接管其后的一切。这种划分是任何回填历史数据都必须对账的实时与历史边界,而且它是文档记录的行为,而不是服务商的怪癖。
一个可运行的 Node.js 采集器,用于窗口化 candleSnapshot 分页
下面的采集器在一个日期范围内对 candleSnapshot 进行分页,按开盘时间去重,并报告缺失的 interval。它使用现代 Node.js 中可用的全局 fetch 和可配置的端点,因此你可以将其指向自己的服务商。将端点替换为你使用的那个;Hyperliquid RPC 端点(RPC Assistant) 页面列出了选项,RPC 定价 解释了请求量如何映射到成本。
interval 宽度表是唯一编码 interval 语义的地方,因此请使其与文档记录的 interval 字符串保持同步。缺口报告故意写得详细:它会打印缺失的开盘时间,以便你判断每个缺口是无成交窗口还是值得重试的传输失败。
const ENDPOINT = process.env.HL_INFO_ENDPOINT || 'https://api.hyperliquid.xyz/info';
const INTERVAL_MS = { '1m': 60000, '5m': 300000, '15m': 900000, '1h': 3600000, '4h': 14400000, '1d': 86400000 };
async function candleSnapshot({ coin, interval, startTime, endTime }) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ type: 'candleSnapshot', req: { coin, interval, startTime, endTime } })
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
if (!Array.isArray(body)) throw new Error('Unexpected response: ' + JSON.stringify(body));
return body;
}
async function collect({ coin, interval, startTime, endTime, windowMs }) {
const step = INTERVAL_MS[interval];
if (!step) throw new Error('Unknown interval: ' + interval);
const byOpenTime = new Map();
let requests = 0;
let cursor = startTime;
while (cursor < endTime) {
const windowEnd = Math.min(cursor + windowMs, endTime);
const batch = await candleSnapshot({ coin, interval, startTime: cursor, endTime: windowEnd });
requests += 1;
for (const c of batch) byOpenTime.set(c.t, c);
const last = batch.length ? batch[batch.length - 1].t : cursor;
cursor = Math.max(last + step, cursor + step);
}
const candles = [...byOpenTime.values()].sort((a, b) => a.t - b.t);
const gaps = [];
for (let i = 1; i < candles.length; i += 1) {
const delta = candles[i].t - candles[i - 1].t;
if (delta !== step) gaps.push({ after: candles[i - 1].t, before: candles[i].t, delta });
}
return { candles, gaps, requests };
}
collect({
coin: 'BTC',
interval: '1m',
startTime: Date.parse('2026-09-01T00:00:00Z'),
endTime: Date.parse('2026-09-08T00:00:00Z'),
windowMs: 6 * 3600000
}).then((r) => {
console.log('candles', r.candles.length, 'requests', r.requests, 'gaps', r.gaps.length);
if (r.candles.length) console.log('first', r.candles[0].t, 'last', r.candles[r.candles.length - 1].t);
for (const g of r.gaps.slice(0, 20)) console.log('gap', new Date(g.after).toISOString(), '->', new Date(g.before).toISOString());
}).catch((e) => { console.error(e); process.exit(1); });一张针对你自己端点填写的结 果表
服务商行为各不相同,因此描述你的端点唯一诚实的方式就是测量它。针对你自己的端点运行上面的采集器,并记录下面的值。不要将这里的任何数字当作对某个特定服务商的主张;该表是你自己观察结果的模板。
记录每次运行的拉取窗口,以便数字可复现。如果你改变窗口大小,发出的请求数一列会变化,而这正是重点:它直接展示了成本权衡。
- 返回的第一根 K 线(t 的 ISO 时间戳)。
- 返回的最后一根 K 线(t 的 ISO 时间戳)。
- 每次请求的 K 线数(各窗口的最小值、中位数、最大值)。
- 检测到的缺口(数量及开盘时间)。
- 整个范围发出的请求数。
- 实际耗时以及观察到的任何限流响应。
故障模式与排错
某个窗口返回空数组通常意味着该窗口内没有成交,而不是请求失败。通过检查该窗口是否落在低流动性时段,以及重新请求一个你已知有成交的相邻窗口来确认。如果相邻窗口返回了 K 线,而空窗口确实是安静的,就将其记录为缺口,而不是无限重试。
窗口请求过快时出现限流错误是最常见的运维故障。Info API 会返回一个错误对象,而 JSON-RPC 2.0 规范 是此类响应所遵循的错误对象信封形状的权威参考。退避并重试,而不是猛击端点;Hyperliquid API 限流:Info 与 Exchange 页面区分了这两个接口,这很重要,因为它们的预算方式不同。
币种名称大小写或命名不匹配会返回一个空结果,看起来与无成交窗口完全相同。币种标识符在实践中区分大小写,因此请规范化你的输入,并在断定窗口为空之前用已知良好的请求进行验证。看起来滞后的最后一根 K 线通常是 interval 中途拉取的 K 线:它的收盘、最高、最低和成交量仍在变动。解决办法是上面的对账步骤,而不是重试。
- 空数组:在假设失败之前先检查是否为无成交窗口。
- 限流错误:退避,然后重试;不要盲目并行化。
- 命名不匹配:规范化币种大小写,并用已知良好的请求验证。
- 滞后的最后一根 K 线:它是在 interval 中途拉取的;让 WebSocket 接管它。
历史深度、请求成本与来源权衡
历史深度是一个你应该通过经验发现而不是假设的文档化约束。candleSnapshot 能提供多久以前的数据,其实际上限是通过试验发现的,正如 ccxt GitHub 上关于 fetch_ohlcv 限制的 issue 所说明的那样,而且它可能与其他接口可用的深度不同。在承诺多年回填计划之前,先用单个窗口向后探测。
请求成本与窗口大小成反比。许多小窗口让你对重试和可恢复性有更精细的控制,但会使请求数量成倍增加,这会与限流以及你的服务商计费方式相互作用。大窗口减少请求数量,但使单次失败的重试代价更高。正确的窗口大小是对你的 interval 仍能返回完整数据的最大窗口,而这正是上面的结 果表所测量的。
在归档中记录每根 K 线的拉取窗口。在 interval 中途结束的窗口中拉取的 K 线在拉取时可能是部分的,如果没有窗口元数据,你以后就无法判断某个低成交量数字是真实的,还是你提问时机的产物。来源信息存储成本低,重建成本高。关于使用哪个数据接口的更广泛决策,请参阅 OnFinality Learn 中心 和 API 服务 概览。
生产级 OHLCV 归档的后续步骤
从一次性脚本转向计划回填加实时尾部。按计划运行窗口化采集器以扩展历史数据,并持续运行 WebSocket 对账,使边界 K 线保持最新。持久化游标和最后一根已收盘 K 线的 t,这样重启时无需重新拉取整个范围即可恢复。
添加一个缺口修复环节,只重新请求记录缺口周围的窗口,并将缺口报告保留为一等工件,而不是一行日志。如果你的策略在 K 线之外还消费资金费率或预言机价格,Hyperliquid 预言机价格与构建者拍卖 页面涵盖了那些 Info API 接口。最后,在扩大请求量之前查看 RPC 定价,并对照 Hyperliquid RPC 端点(RPC Assistant) 确认你的端点选择。
- 计划回填;持续运行 WebSocket 尾部。
- 持久化游标和最后一根已收盘的 t,以实现可恢复的重启。
- 只重新请求受影响的窗口来修复缺口。
- 为每根 K 线存储拉取窗口以保留来源信息。