#30673·ccxt

Feature request: declare the unit of OHLCV volume in features (base / quote / contracts)

Author: huahuayuCreated Sep 28, 2026Updated Sep 28, 2026
Labelsenhancement

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:

javascript
'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 × contractSize is base for linear and quote for inverse, as elsewhere in CCXT). It stays undefined where 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:

python
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 / close

The 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/value fields after the parser was written (#30667).
  • CoinEx swaps have active: undefined.
  • Deepcoin's ticker is not a rolling 24h.
  • Phemex rejects small limit values 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.