跨市場成交量對比是數據團隊最容易做錯、也最難自查的一類分析。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、複用本地緩存,並避免在高頻循環裡反覆拉取歷史。
跨市場比較的正確順序是:先聲明口徑,再校驗可比性,最後才做計算。多市場統一接口的接入方式可參考 多市場接入指南;免費版即可跑通本文的校驗器,先在控制台確認可用市場與延遲檔位,再決定是否為實時推送升級。