Feature request: declare the unit of OHLCV volume in features (base / quote / contracts) · Issue #30673 · ccxt/ccxt · GitHub
Skip to content

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

Open

Description

@huahuayu

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 × 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:

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:

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.

Activity

  1. carlotestor commented on Sep 28, 2026

    @carlotestor
    Collaborator

    Thanks for the detailed write-up and the per-venue measurements — noted as a feature request.

    Declaring the unit as an optional volume key under features.<type>.fetchOHLCV looks like a low-risk, additive first step: it reuses the existing per-market-type features tree (spot / swap.linear / swap.inverse / future.*), leaves the parsers untouched, and undefined keeps meaning "unverified". The features shape is validated in ts/src/test/Exchange/test.features.ts, so a PR would also add 'volume' to the fetchOHLCV entry there (and ideally the candle-vs-ticker order-of-magnitude check you describe in test.ohlcv.ts, gated on the declared unit).

    The hashkey row (contracts × 0.001 on BTC/USDT:USDT) is worth a separate look regardless, since that is the case where the number looks plausible.

    Since this touches the unified features schema, I'll leave the go/no-go on the key name and value set (base / quote / contracts) to the maintainers before anyone fills in per-exchange values.

  2. mvanhorn commented on Oct 6, 2026

    @mvanhorn
    Contributor

    fetchOHLCV blocks in features listed limits and related flags and had no volume field. The features builder left that key absent, so featureValue could not report a unit and callers following the manual treated the volume as base currency.

    fetchOHLCV.volume is now 'base', 'quote', 'contracts', or undefined, and BaseExchange sets undefined when an exchange does not declare it. The declarations are binance and bybit linear 'base'; gate, mexc, hashkey, and deepcoin linear 'contracts'; hibachi linear 'quote'; phemex inverse 'contracts'; and xt linear 'contracts' with xt inverse 'base'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions