Feature request: declare the unit of OHLCV volume in features (base / quote / contracts)
Problem
The unit of the volume in fetchOHLCV candles differs between exchanges, and between market types on the same exchange. The Manual allows this: "usually in terms of the base currency, the exchanges docstring may list whether quote or base units are used". But this is not exposed anywhere a program can read it, and test.ohlcv.ts only checks the structure, so a caller cannot find out what candle[5] means.
The differences are big. Multiplying the volume by the close (the natural reading of the base-currency convention) is off by the contract size (10⁻⁴…10²×) or by the price (~10⁵× for BTC). Some results are obviously wrong, but others look plausible, and those are the dangerous ones.
Examples
Yesterday's 1d candle of each venue's BTC market (ccxt 4.5.84), shown as volume × close, compared with the venue's own 24h ticker quoteVolume:
| exchange | market | volume × close | actual unit | correct notional | ticker quoteVolume |
|---|---|---|---|---|---|
| binance | BTC/USDT:USDT | $5.6B | base | $5.6B | $12.6B |
| bybit | BTC/USDT:USDT | $2.1B | base | $2.1B | $5.0B |
| gate | BTC/USDT:USDT | $23.3T | contracts (×0.0001) | $2.3B | $5.7B |
| mexc | BTC/USDT:USDT | $22.3T | contracts | $2.2B | $3.0B |
| xt | BTC/USDT:USDT | $6.7T | contracts (field a) |
$670M | $1.5B |
| xt | BTC/USD:BTC | $690M | base (field v) |
$690M | – |
| toobit | BTC/USDT:USDT | $4.2T | contracts | $4.2B | $7.8B |
| deepcoin | BTC/USDT:USDT | $2.25T | contracts (×0.001) | $2.25B | (current UTC day only) |
| hashkey | BTC/USDT:USDT | $515M | contracts (×0.001) | $515k | $735k |
| phemex | BTC/USD:BTC | $4.9T | contracts (1 USD each) | $58M | – |
| hibachi | BTC/USDT:USDT | $891B | quote (volumeNotional) |
$10.6M | $9.1M |
| coinex | BTC/USDT (spot) | $40.7B | quote (#30667, fixed in #30668) | $480k | $988k |
The rows are not all bugs. Contracts are CCXT's unit for amounts on derivatives everywhere else (trades, orders, positions), so gate or mexc returning contracts is arguably consistent. hibachi has no base volume in its kline response at all. xt shows that the parser already knows the unit per market type (it switches between the a and v fields on market['inverse']), but that knowledge is not exposed to callers.
The hashkey row is the important one. $515M/day for a BTC perpetual passes any sanity check, but it is 1000× the real volume. Without knowing the unit, a consumer cannot tell.
Proposal
Declare the unit next to the existing OHLCV capabilities in features, per market type:
'features': {
'spot': { 'fetchOHLCV': { 'limit': 1000, 'volume': 'base' } },
'swap': {
'linear': { 'fetchOHLCV': { 'limit': 1000, 'volume': 'contracts' } },
'inverse': { 'fetchOHLCV': { 'limit': 1000, 'volume': 'contracts' } },
},
...
}- values:
'base' | 'quote' | 'contracts'(contracts×contractSizeis base for linear and quote for inverse, as elsewhere in CCXT). It staysundefinedwhere nobody has checked. - It is additive: no behaviour change for anyone who already compensates.
- It gives the live tests something to assert. With the declared unit, the candle notional and the ticker notional must agree within an order of magnitude. That catches unit errors (≥10⁴×) without being flaky. A few venues need a skip because their ticker is itself off, e.g. deepcoin's "24h" ticker covers only the current UTC day.
Normalising every venue to base volume would also solve it, but it breaks everyone who compensates today. Metadata first would be a safe step towards that.
Current downstream workaround
We keep our own map (exchange × {spot, linear, inverse} → unit, default base) and convert every candle:
if unit == "quote":
quote = volume
elif unit == "contracts" and market["inverse"]:
quote = volume * contract_size
else:
quote = volume * (contract_size if unit == "contracts" else 1) * close
base = quote / closeThe map comes from a probe that we re-run after every CCXT upgrade. For each venue it reads yesterday's BTC candle for spot, linear and inverse markets and computes the USD notional under each hypothesis (base, quote, contracts). It keeps the one that falls within $10k–$50B/day. When two hypotheses are plausible, the venue's 24h ticker has to match within 3×, and anything else is left undecided and checked by hand against the raw payload.
Pitfalls we hit:
- CoinEx swapped its
volume/valuefields after the parser was written (#30667). - CoinEx swaps have
active: undefined. - Deepcoin's ticker is not a rolling 24h.
- Phemex rejects small
limitvalues on OHLCV.
It works, but every consumer has to rediscover it, and it silently breaks whenever a parser or an exchange changes a field. I'm happy to contribute the initial volume values for the venues above.
Source: ccxt/ccxt