获取代币的 DEX 日线(开高低收与 VWAP)与精确分数

用 getDexPrices 端点按代币与起止日期查询每日 VWAP 与开高低收:返回开高低收价格字段与分子分母精确分数,单次跨度不得超过 90 天,超出返回 409 span_exceeded,并给出分段回填写法。

什么是 DEX 日线数据集

BlockVectra 的 DEX 数据集涵盖链上去中心化交易所(DEX)的成交记录与日聚合价格。其中,DEX 日线接口(getDexPrices)提供指定代币在指定日期范围内的每日成交量加权平均价(VWAP)、价格指标(字段名 first_price、last_price、min_price、max_price)以及成交量指标。

各链开放该数据集的情况有所不同,提供该数据集的链以支持的链页面为准。

该端点返回指定时间窗口内的数据列表,不进行分页,响应体中永不包含 next_cursor。如果业务需要逐笔具体的成交流水而非按天聚合的日线,请参阅 GET /{chain}/dex/swaps 端点(详见 Data API 参考)。

请求参数与限制

DEX 日线请求地址为 GET https://api.blockvectra.com/v1/data/{chain}/dex/prices。所有请求必须在 x-api-key 请求头中传入 API key 进行鉴权。

端点支持以下查询参数:

参数名位置类型是否必填说明
chainpath字符串是目标链标识,例如 robinhood_mainnet
tokenquery字符串是20 字节基础代币地址(base token address),0x 前缀可选,大小写均可
quotequery字符串否可选的 20 字节计价代币地址,用于限定为单个基础/计价代币交易对
fromquery日期字符串是UTC 起始日期(包含),格式为 YYYY-MM-DD
toquery日期字符串是UTC 截止日期(包含),格式为 YYYY-MM-DD,且 to - from 必须 <= 90 天

约束与错误码

接口在参数不符合要求时返回标准错误结构 {"error":{"code","message"}}:

  • HTTP 400 (bad_request):缺少必需参数(token、from 或 to)、token 或 quote 代币地址格式无效、日期不是合法日历日期,或 from 晚于 to。
  • HTTP 409 (span_exceeded):请求跨度 to - from 超过 90 天。
  • HTTP 404(error.code: not_found,error.data.reason: unknown_chain):{chain} 未知或未在 GET /chains 中公开;先检查链,再校验查询参数。
  • HTTP 422 (no_coverage):请求的链未提供 dex_prices 数据集能力。
  • HTTP 503 (unavailable):服务暂时不可用,可依照响应的 Retry-After 头重试。

请求示例

下方示例查询特定基础代币在 2026 年 9 月份的日线数据:

curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/dex/prices?token=0x1Cdad396DB64BDa184d5182A97Dd9B3C62100b7D&from=2026-09-01&to=2026-09-30" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

返回字段详解

data 数组中的每项表示该代币在某一 UTC 日期的交易汇总:

代币与计价信息

  • day(字符串):UTC 日期,格式为 YYYY-MM-DD。
  • token(字符串):20 字节基础代币地址(0x 前缀的小写十六进制)。
  • token_symbol(字符串或 null):基础代币符号。
  • token_name(字符串或 null):基础代币名称。
  • quote_token(字符串):计价资产地址。全零地址(0x0000000000000000000000000000000000000000)代表原生 ETH 作为计价资产。
  • quote_symbol(字符串或 null):计价资产符号,当 quote_token 为全零地址时固定为 "ETH"。
  • quote_name(字符串或 null):计价资产名称,当 quote_token 为全零地址时固定为 "Ether"。
  • base_decimals(整数或 null):基础代币小数位数(0–255)。
  • quote_decimals(整数或 null):计价资产小数位数,当 quote_token 为全零地址时固定为 18。

交易量与成交统计

  • swap_count(整数):该行对应的成交笔数。
  • base_volume_raw(字符串):基础代币原始成交量,以最小单位表示的非负十进制整数字符串(UInt256String)。
  • quote_volume_raw(字符串):计价资产原始成交量,以最小单位表示的非负十进制整数字符串(UInt256String)。
  • base_volume(字符串或 null):经 base_decimals 换算后的基础代币人类可读成交量(DecimalString);若 base_decimals 未知则为 null。
  • quote_volume(字符串或 null):经计价代币小数位换算后的计价成交量(DecimalString),或 null。

价格指标与 VWAP

  • vwap(字符串或 null):成交量加权平均价(以小数定点数字符串表示),或 null。
  • first_price(字符串或 null):首个价格指标(以定点数字符串表示),或 null。
  • last_price(字符串或 null):末个价格指标(以定点数字符串表示),或 null。
  • min_price(字符串或 null):最低价格指标(以定点数字符串表示),或 null。
  • max_price(字符串或 null):最高价格指标(以定点数字符串表示),或 null。

精确分数表示

  • first_price_numerator / first_price_denominator(字符串):该价格对应成交的原始整数金额(UInt256String):分子为 quote_amount_raw(计价代币最小单位),分母为 base_amount_raw(标的代币最小单位)。它们不是按精度缩放后的 first_price 的精确分数。
  • last_price_numerator / last_price_denominator(字符串):该价格对应成交的原始整数金额(UInt256String):分子为 quote_amount_raw(计价代币最小单位),分母为 base_amount_raw(标的代币最小单位)。它们不是按精度缩放后的 last_price 的精确分数。
  • min_price_numerator / min_price_denominator(字符串):该价格对应成交的原始整数金额(UInt256String):分子为 quote_amount_raw(计价代币最小单位),分母为 base_amount_raw(标的代币最小单位)。它们不是按精度缩放后的 min_price 的精确分数。
  • max_price_numerator / max_price_denominator(字符串):该价格对应成交的原始整数金额(UInt256String):分子为 quote_amount_raw(计价代币最小单位),分母为 base_amount_raw(标的代币最小单位)。它们不是按精度缩放后的 max_price 的精确分数。
  • refreshed_at(字符串):该行的刷新时间(ISO-8601 UTC 时间戳)。

原始比值 分子 / 分母 的单位是「计价代币最小单位 / 标的代币最小单位」。可读价格(每个标的代币对应的计价代币数量)= 原始比值 × 10^(base_decimals − quote_decimals)。base_decimals 或 quote_decimals 为 null 时可读价格未知,不要假定为 18;0 是合法的精度值。分母为 0 时不要做除法,按可读价格为 null 处理。同一交易对(精度相同)的涨跌与相对变化可直接用原始比值计算。

元数据字段 (meta)

  • chain:链标识名称。
  • chain_slug:全大写规范链标识。
  • chain_external_id:CAIP-2 标准外部链 ID。
  • as_of_block:该链最新已完整写入的区块高度(本数据集仅上报,不参与请求校验)。
  • coverage:覆盖度状态(本端点固定为 "full")。
  • refreshed_at:元数据时间戳。可能为 null:null 表示这份数据的更新时间未知,应按过期处理(区块类接口始终有值)。

为什么价格使用分子与分母表示

直接使用标准 JSON 数字(IEEE-754 双精度浮点数)存在精度局限:

  1. 浮点截断与舍入偏差:双精度浮点数仅有 53 位有效精度,在储备比值除法运算中会产生舍入误差并在后续计算中级联放大。
  2. 传输安全:以十进制整数字符串(UInt256String)表示分子与分母,确保数值在 HTTP 传输与 JSON 解析过程中不丢失精度。

通过返回精确的整数分子与分母,可以在不经过浮点数转换的情况下进行精确计算。

在 TypeScript(BigInt)中不经过浮点数处理

TypeScript 中可用原生 BigInt 做不经过浮点数的比率比较与精度缩放。isPriceUp 比较同一交易对的原始比值;rawRatioToHumanString 按精度缩放,精度未知或分母为 0 时返回 null。其输出按 displayDigits 位截断(不四舍五入):这是展示截断,与代币自身精度无关,也不表示存在有限的精确小数展开。

interface DexDailyPrice {
  base_decimals: number | null;
  quote_decimals: number | null;
  // Raw amounts: numerator is quote_amount_raw, denominator is base_amount_raw.
  first_price_numerator: string;
  first_price_denominator: string;
  last_price_numerator: string;
  last_price_denominator: string;
}

// 1. Raw ratios of one pair share the same decimals, so they compare directly.
// a / b > c / d  is equivalent to  a * d > c * b  (denominators must be non-zero)
export function isPriceUp(row: DexDailyPrice): boolean | null {
  const openNum = BigInt(row.first_price_numerator);
  const openDen = BigInt(row.first_price_denominator);
  const closeNum = BigInt(row.last_price_numerator);
  const closeDen = BigInt(row.last_price_denominator);
  if (openDen === 0n || closeDen === 0n) return null;

  return closeNum * openDen > openNum * closeDen;
}

// 2. Raw ratio -> human-readable price (quote tokens per base token):
//    human = numerator / denominator * 10^(baseDecimals - quoteDecimals)
// Returns null when a decimals value is null (0 is valid) or the denominator is 0.
// The result is truncated to displayDigits digits for display only.
export function rawRatioToHumanString(
  numeratorStr: string,
  denominatorStr: string,
  baseDecimals: number | null,
  quoteDecimals: number | null,
  displayDigits = 18
): string | null {
  if (baseDecimals === null || quoteDecimals === null) return null;
  let num = BigInt(numeratorStr);
  let den = BigInt(denominatorStr);
  if (den === 0n) return null;
  const shift = baseDecimals - quoteDecimals;
  if (shift >= 0) {
    num *= 10n ** BigInt(shift);
  } else {
    den *= 10n ** BigInt(-shift);
  }
  const unit = 10n ** BigInt(displayDigits);
  const scaled = (num * unit) / den;
  const intPart = scaled / unit;
  if (displayDigits === 0) return intPart.toString();
  const fracPart = (scaled % unit).toString().padStart(displayDigits, "0");

  return `${intPart}.${fracPart}`;
}

在 Python 中不经过浮点数处理

Python 标准库的 fractions.Fraction 用于精确有理数运算。human_price 精确地按精度缩放,精度未知或分母为 0 时返回 None;format_fraction 仅用于展示,用整数运算按「四舍六入五成双」舍入。

from fractions import Fraction


def raw_ratio(numerator, denominator):
    """Raw ratio: quote atomic units per base atomic unit. None when the denominator is 0."""
    den = int(denominator)
    if den == 0:
        return None
    return Fraction(int(numerator), den)


def human_price(numerator, denominator, base_decimals, quote_decimals):
    """Quote tokens per base token = raw ratio * 10^(base_decimals - quote_decimals).

    Returns None when a decimals value is None (0 is valid) or the denominator is 0.
    """
    if base_decimals is None or quote_decimals is None:
        return None
    raw = raw_ratio(numerator, denominator)
    if raw is None:
        return None
    return raw * Fraction(10) ** (base_decimals - quote_decimals)


def percentage_change(row):
    """Same pair, same decimals: raw ratios compare directly. Exact Fraction, no float."""
    open_raw = raw_ratio(row["first_price_numerator"], row["first_price_denominator"])
    close_raw = raw_ratio(row["last_price_numerator"], row["last_price_denominator"])
    if open_raw is None or close_raw is None or open_raw == 0:
        return None
    return (close_raw - open_raw) / open_raw * 100


def format_fraction(value, digits=18):
    """Display only: round half to even at `digits` places with exact integer arithmetic."""
    quotient, remainder = divmod(abs(value.numerator) * 10**digits, value.denominator)
    twice = remainder * 2
    if twice > value.denominator or (twice == value.denominator and quotient % 2 == 1):
        quotient += 1
    whole, fraction = divmod(quotient, 10**digits)
    text = str(whole) if digits == 0 else f"{whole}.{fraction:0{digits}d}"
    return f"-{text}" if value < 0 and quotient != 0 else text

回填一年历史日线的分段请求写法

在 90 天单次跨度限制下回填一整年(365 天)历史日线时,需将目标时间段按不超过 90 天的窗口切分成多个子区间,依次发起请求并合并结果:

interface DateSpan {
  from: string;
  to: string;
}

/**
 * 将任意较长日期区间按最大天数(上限 90 天)切分为连续子区间
 */
export function splitDateRange(startDateStr: string, endDateStr: string, maxDays = 90): DateSpan[] {
  const spans: DateSpan[] = [];
  let currentStart = new Date(startDateStr);
  const end = new Date(endDateStr);

  while (currentStart <= end) {
    const chunkEnd = new Date(currentStart);
    chunkEnd.setUTCDate(chunkEnd.getUTCDate() + (maxDays - 1));
    const effectiveEnd = chunkEnd < end ? chunkEnd : end;

    spans.push({
      from: currentStart.toISOString().slice(0, 10),
      to: effectiveEnd.toISOString().slice(0, 10),
    });

    const nextStart = new Date(effectiveEnd);
    nextStart.setUTCDate(nextStart.getUTCDate() + 1);
    currentStart = nextStart;
  }

  return spans;
}

/**
 * 分段回填特定代币的历史日线
 */
export async function backfillTokenDailyPrices(
  chain: string,
  token: string,
  startDate: string,
  endDate: string,
  apiKey: string
) {
  const chunks = splitDateRange(startDate, endDate, 90);
  const allDailyPrices = [];

  for (const chunk of chunks) {
    const url = new URL(`https://api.blockvectra.com/v1/data/${chain}/dex/prices`);
    url.searchParams.set("token", token);
    url.searchParams.set("from", chunk.from);
    url.searchParams.set("to", chunk.to);

    const res = await fetch(url, {
      headers: { "x-api-key": apiKey },
    });

    if (!res.ok) {
      throw new Error(`Failed to fetch span ${chunk.from}..${chunk.to}: HTTP ${res.status}`);
    }

    const json = await res.json();
    allDailyPrices.push(...json.data);
  }

  return allDailyPrices;
}

用量与 CU 消耗测算

调用 Data API 的每个方法均按计算单元(CU)计量。单次调用 data.dex_prices 消耗的 CU 以及批量回填任务的测算如下所示:

方法计费权重data.dex_prices: 15 CU / call
  • 回填 200 个代币一年日线:单次请求最大跨度为 90 天,一年 365 天需分 5 段请求,单个代币需 5 次调用,总计 1,000 次调用。 总计消耗 15,000 CU(约占单用量周期免费额度的 <0.1%),按标价换算约 <$0.01。
  • 日常维护(每天刷新 200 个代币的最新日线):每天调用 200 次(每天消耗 3,000 CU),每个 30 天周期约调用 6,000 次,消耗 90,000 CU(约占免费额度的 0.3%),按标价约每月 <$0.01。

当你的回填数据量较大或并发请求较高时,可以在控制台账单页进行链上充值以获得更高的速率上限与充裕的计算单元。关于当前的计量单元与定价细则,请参阅定价页。

下一步

最后更新:

本页目录