休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

成交量字段到处都有,口径却不一样:跨市场对比的四个必要条件

同一个 volume 字段,在股票、加密货币与行情快照里回答的是四个不同的问题。本文拆解计价资产、统计对象、时间桶与价格口径四个必要条件,并给出一个可直接运行的成交量口径校验器,让跨市场比较先自证可比性再计算。

跨市场成交量对比是数据团队最容易做错、也最难自查的一类分析。TIQEX 各市场的响应里都有 volume 字段:美股 K 线有、加密货币 K 线有、行情快照也有。字段名一致,口径并不一致。把两个市场的 volume 直接相除或排序,结论既无法复现,也无法在下一版数据里重现。本文给出跨市场比较前必须确认的四个必要条件,并附一个可直接运行的成交量口径校验器。

一、同名不同义的 volume:先把口径摊开

把站点文档中与成交量有关的字段并排列出,差异立刻显现。关键不在于字段叫什么,而在于每个字段各自回答了哪一个问题。

接口字段文档口径
/api/v1/market/equity/klinebars[].volumeOHLCV 中的 V,bar 区间内成交量;单位未在响应中声明
/api/v1/market/crypto/klinebars[].volume基础资产(base)数量,bar 区间内
/api/v1/market/crypto/klinebars[].quoteVolume计价资产(quote)金额,bar 区间内
/api/v1/market/crypto/klinebars[].takerBuyVolume仅主动买方向的基础资产数量
/api/v1/market/crypto/klinebars[].trades成交笔数
/api/v1/market/quotevolume快照成交量(时点值),统计区间未在响应中声明

这张表已经暴露了三种典型误用。第一,把 quoteVolume 与 volume 混算,量级相差的就是标的价格本身。第二,把快照的 volume 与 bar 的 volume 相减求增量,两个量的时间语义不同,差值可能为负。第三,把 takerBuyVolume 当作成交量,结果只统计主动买方向,系统性低估。

字段语义属于「是什么」,术语表里已经讲清;本文不重复,只补上「为什么不能直接比」。字段本身的含义可先读 OHLCV 字段说明。

二、必要条件一:计价资产必须一致

成交量的第一个自由度是「计量单位是哪个资产」。加密货币 K 线把这件事显式拆成两个字段:volume 以基础资产计(例如 BTCUSDT 上以 BTC 计),quoteVolume 以计价资产计(以 USDT 计)。两者相除得到的是该 bar 的均价量级,而非可以当作成交价使用的数值——这是量级校验,不是定价依据。

股票 K 线只提供 volume,没有 quoteVolume。如果需要成交额,只能用同一根 bar 自带的 vwap 重建:amount ≈ vwap × volume。这种做法本身合理,但重建值必须在口径声明里显式标注,否则下游会把它当成原始字段,在 vwap 缺失或异常时静默出错。

跨市场比较时,把加密货币的 volume 与美股的 volume 放在同一列排序,是最常见的错误:前者是币,后者是股,二者没有共同单位。可比较的替代量是成交额,前提是两边都换算到同一计价货币。加密货币侧的覆盖范围见 Crypto 市场页,字段细节见 加密货币 K 线接口文档。

三、必要条件二:统计对象必须一致

第二个自由度是「统计的是什么」。volume 是数量,trades 是笔数,takerBuyVolume 是方向切片。三者之间没有换算关系:笔数多不代表数量大,主动买占比高不代表总量高。

把 takerBuyVolume 当总量使用是最隐蔽的一种。它与 volume 的差额恰是主动卖方向的数量,因此该错误会稳定地低估,且在震荡行情里低估得更少、在单边行情里低估得更多——偏差随行情变化,因而难以用固定系数修正。正确做法是把它当作派生比率:takerBuyVolume / volume,用于刻画主动买占比,而不是量级。

笔数同样不宜跨市场横向比较。撮合粒度、最小变动价位与拆单规则不同,同样的经济成交量在不同市场会产生数量级不同的笔数。笔数适合做同一市场内的时序监控,不适合做跨市场排序。

四、必要条件三与四:时间口径与价格口径要成对声明

第三个自由度是时间桶。K 线 bar 是区间量,带 openTime/closeTime;行情快照的 volume 是时点量。区间量可以相加、可以按桶对齐,时点量既不能与区间量相加,也不能简单相减。更麻烦的是桶的取值跨市场不统一:股票 K 线的 interval 取 1T/5T/30T/1H/2H/4H/1D/1W/1M(T 表示分钟),加密货币 K 线沿用 1m/1h 这类写法,且 session 字段只出现在股票 bar 上。桶不同,同一个「成交量」就不是同一个量。

第四个自由度是价格口径,它经常被误认为与成交量无关。K 线响应的 data.adjustmentMode 决定了价格序列是否复权;成交量与价格口径必须成对声明,否则「量价关系」类分析在拆股与除权日附近会失真。一个可操作的判据是:只要价格序列经过了调整,任何跨越该调整日的量价比值都需要重新确认口径,而不是沿用调整前的结论。

站点定价页对这一点给出了产品侧的依据:数据口径、延迟档位与历史深度因套餐与权益而异,以控制台与文档为准,且不保证与单一交易所盘口完全一致。也就是说,「口径由使用方自己声明并记录」是这套接口的既定前提,参见 定价与套餐说明。

五、把口径写进代码:可运行的校验器

口径声明只有落到代码里才有效。下面的校验器把每个「市场 + 字段」组合登记为一个规格,然后在比较前逐项比对单位、统计对象与时间桶。三个属性只要有一项不一致就拒绝比较,并给出具体差异。代码不依赖任何第三方库,可直接运行。

Python
from dataclasses import dataclass

@dataclass(frozen=True)
class VolumeSpec:
    market: str
    field: str
    unit: str    # BASE | QUOTE | UNKNOWN
    kind: str    # BAR_RANGE | SNAPSHOT | TRADE_COUNT | TAKER_BUY
    bucket: str  # 时间桶,如 "1h" / "1d" / "snapshot"

SPECS = {
    ("crypto", "volume"):         VolumeSpec("crypto", "volume", "BASE", "BAR_RANGE", "1h"),
    ("crypto", "quoteVolume"):    VolumeSpec("crypto", "quoteVolume", "QUOTE", "BAR_RANGE", "1h"),
    ("crypto", "takerBuyVolume"): VolumeSpec("crypto", "takerBuyVolume", "BASE", "TAKER_BUY", "1h"),
    ("equity", "volume"):         VolumeSpec("equity", "volume", "BASE", "BAR_RANGE", "1d"),
    ("quote", "volume"):          VolumeSpec("quote", "volume", "UNKNOWN", "SNAPSHOT", "snapshot"),
}

class NotComparable(Exception):
    pass

def assert_comparable(left, right):
    for attr in ("unit", "kind", "bucket"):
        a, b = getattr(left, attr), getattr(right, attr)
        if a != b:
            raise NotComparable(
                "%s.%s vs %s.%s: %s mismatch (%s != %s)"
                % (left.market, left.field, right.market, right.field, attr, a, b)
            )
    return True

if __name__ == "__main__":
    pairs = [
        (SPECS[("crypto", "volume")], SPECS[("crypto", "volume")]),
        (SPECS[("crypto", "volume")], SPECS[("crypto", "quoteVolume")]),
        (SPECS[("crypto", "volume")], SPECS[("equity", "volume")]),
        (SPECS[("quote", "volume")], SPECS[("equity", "volume")]),
        (SPECS[("crypto", "volume")], SPECS[("crypto", "takerBuyVolume")]),
    ]
    for a, b in pairs:
        try:
            assert_comparable(a, b)
            print("PASS   %s.%s vs %s.%s" % (a.market, a.field, b.market, b.field))
        except NotComparable as e:
            print("REJECT " + str(e))

运行结果是 1 次通过、4 次拒绝:同市场同字段通过;volume 与 quoteVolume 因单位不同被拒;加密货币与股票的 volume 因时间桶不同被拒;快照量与 bar 量因统计对象不同被拒;总量与主动买量因方向切片不同被拒。

第二个片段演示如何从两个市场取数并归一到同一口径。为便于离线运行,用 if __name__ == "__main__" 在模块内替换传输函数,脚本无需网络即可跑通,也不消耗配额。注意响应信封必须与 HTTP 状态码同检:succ 为假时即使状态码是 2xx 也应当视为失败。

Python
import json
import urllib.request

API = "https://api.tiqex.io/api/v1/market"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"

def http_get(path, api_key):
    req = urllib.request.Request(API + path)
    req.add_header("X-API-Key", api_key)
    req.add_header("User-Agent", UA)
    with urllib.request.urlopen(req, timeout=10) as r:
        return r.status, json.loads(r.read().decode())

def fetch_series(path, api_key, field):
    status, body = http_get(path, api_key)
    if status != 200 or not body.get("succ"):
        raise RuntimeError("HTTP %s code=%s msg=%s" % (status, body.get("code"), body.get("msg")))
    bars = body["data"]["bars"]
    return [(b.get("openTimeMs") or b.get("openTime"), b.get(field)) for b in bars
            if b.get(field) is not None]

if __name__ == "__main__":
    def http_get(path, api_key):  # 离线桩:真实运行请删除本段
        return 200, {"succ": True, "code": "OK", "msg": "ok", "data": {"bars": [
            {"openTimeMs": 1758931200000, "volume": 1240.5, "quoteVolume": 74120000.0},
            {"openTimeMs": 1758934800000, "volume": 980.2, "quoteVolume": 58550000.0},
        ]}}

    series = fetch_series("/crypto/kline?symbol=BTCUSDT&interval=1h", "YOUR_API_KEY", "quoteVolume")
    total = sum(v for _, v in series)
    print("bars=%d  quoteVolume 合计=%.2f" % (len(series), total))

把这两段接起来用:先在采集层用 assert_comparable() 卡住跨口径比较,再在展示层统一换算到目标口径。口径声明表应当与代码一起进版本库,这样任何一次口径变更都有据可查。

六、常见问题

quoteVolume / volume 能当作该 bar 的成交均价吗?

可以用于量级校验,不能当作定价依据。这个比值反映的是 bar 内的加权量级,与 bar 的 open/high/low/close 以及 vwap 都不等价;当存在大额单笔成交或数据缺失时,偏离会更明显。

跨市场成交量排序有意义吗?

只有在单位、统计对象、时间桶、价格口径四项全部对齐后才有意义。否则应改为比较成交额,或比较同一市场内的时序变化,而不是跨市场绝对值排序。

用 WebSocket tick 的 volume 累加,能得到 K 线成交量吗?

不推荐。tick 的 volume 是推送帧里的快照字段,与 REST bar 的区间量不同源;两者对账需要先明确各自的统计区间。历史与对账建议以 REST 的 bars[] 为准,实时部分只用于驱动更新。

做口径校验会消耗多少配额?

REST 的 quote 与 kline 调用计入日配额与每分钟限流;实时行情走 WebSocket,握手计入配额而推送不按条计费。校验脚本应设置较小的 limit、复用本地缓存,并避免在高频循环里反复拉取历史。

跨市场比较的正确顺序是:先声明口径,再校验可比性,最后才做计算。多市场统一接口的接入方式可参考 多市场接入指南;免费版即可跑通本文的校验器,先在控制台确认可用市场与延迟档位,再决定是否为实时推送升级。