跨市场成交量对比是数据团队最容易做错、也最难自查的一类分析。TIQEX 各市场的响应里都有 volume 字段:美股 K 线有、加密货币 K 线有、行情快照也有。字段名一致,口径并不一致。把两个市场的 volume 直接相除或排序,结论既无法复现,也无法在下一版数据里重现。本文给出跨市场比较前必须确认的四个必要条件,并附一个可直接运行的成交量口径校验器。
一、同名不同义的 volume:先把口径摊开
把站点文档中与成交量有关的字段并排列出,差异立刻显现。关键不在于字段叫什么,而在于每个字段各自回答了哪一个问题。
| 接口 | 字段 | 文档口径 |
|---|---|---|
/api/v1/market/equity/kline | bars[].volume | OHLCV 中的 V,bar 区间内成交量;单位未在响应中声明 |
/api/v1/market/crypto/kline | bars[].volume | 基础资产(base)数量,bar 区间内 |
/api/v1/market/crypto/kline | bars[].quoteVolume | 计价资产(quote)金额,bar 区间内 |
/api/v1/market/crypto/kline | bars[].takerBuyVolume | 仅主动买方向的基础资产数量 |
/api/v1/market/crypto/kline | bars[].trades | 成交笔数 |
/api/v1/market/quote | volume | 快照成交量(时点值),统计区间未在响应中声明 |
这张表已经暴露了三种典型误用。第一,把 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 决定了价格序列是否复权;成交量与价格口径必须成对声明,否则「量价关系」类分析在拆股与除权日附近会失真。一个可操作的判据是:只要价格序列经过了调整,任何跨越该调整日的量价比值都需要重新确认口径,而不是沿用调整前的结论。
站点定价页对这一点给出了产品侧的依据:数据口径、延迟档位与历史深度因套餐与权益而异,以控制台与文档为准,且不保证与单一交易所盘口完全一致。也就是说,「口径由使用方自己声明并记录」是这套接口的既定前提,参见 定价与套餐说明。
五、把口径写进代码:可运行的校验器
口径声明只有落到代码里才有效。下面的校验器把每个「市场 + 字段」组合登记为一个规格,然后在比较前逐项比对单位、统计对象与时间桶。三个属性只要有一项不一致就拒绝比较,并给出具体差异。代码不依赖任何第三方库,可直接运行。
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 也应当视为失败。
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、复用本地缓存,并避免在高频循环里反复拉取历史。
跨市场比较的正确顺序是:先声明口径,再校验可比性,最后才做计算。多市场统一接口的接入方式可参考 多市场接入指南;免费版即可跑通本文的校验器,先在控制台确认可用市场与延迟档位,再决定是否为实时推送升级。