Support custom column selection in OHLC queries and extend CCXT with configurable exchange-specific fields
- Add `columns` parameter to `get_ohlc_async` and pass through to Iceberg queries - Replace hardcoded Binance field extraction with declarative `EXCHANGE_OHLCV_EXTENSIONS` config - Add `applyScale` helper for field-specific transformations (ms_to_ns, price, size, int) - Support `complementOf` spec for derived fields (e.g., sell_vol from total - buy_vol) - Apply extensions dynamically in `convertToOHLC` and gap-filling logic - Remove redundant column filtering in DataAPI (now handled upstream)
This commit is contained in:
@@ -32,6 +32,32 @@ function extractRetryAfterMs(exchange, error) {
|
||||
return 30_000;
|
||||
}
|
||||
|
||||
// Per-exchange descriptor of extended OHLCV fields beyond the standard 6
|
||||
// (timestamp, open, high, low, close, volume).
|
||||
//
|
||||
// 'index' — extract candle[index]; skipped if candle is too short.
|
||||
// 'complementOf' — compute as Math.round((totalVolume - candle[index]) * sizeMult).
|
||||
// Scale types: 'ms_to_ns' | 'price' | 'size' | 'int'
|
||||
const EXCHANGE_OHLCV_EXTENSIONS = {
|
||||
binance: {
|
||||
close_time: { index: 6, scale: 'ms_to_ns' },
|
||||
quote_volume: { index: 7, scale: 'price' },
|
||||
num_trades: { index: 8, scale: 'int' },
|
||||
buy_vol: { index: 9, scale: 'size' },
|
||||
sell_vol: { complementOf: 9, scale: 'size' },
|
||||
},
|
||||
// Add future exchanges here
|
||||
};
|
||||
|
||||
function applyScale(raw, scale, priceMult, sizeMult) {
|
||||
switch (scale) {
|
||||
case 'ms_to_ns': return String(Number(raw) * 1_000_000);
|
||||
case 'price': return String(Math.round(parseFloat(raw) * priceMult));
|
||||
case 'size': return String(Math.round(parseFloat(raw) * sizeMult));
|
||||
case 'int': return String(Number(raw));
|
||||
}
|
||||
}
|
||||
|
||||
export class CCXTFetcher {
|
||||
constructor(config, logger, metadataGenerator = null) {
|
||||
this.config = config;
|
||||
@@ -281,7 +307,7 @@ export class CCXTFetcher {
|
||||
|
||||
for (let ts = firstRealTs; ts <= lastRealTs; ts += periodMs) {
|
||||
if (fetchedByTs.has(ts)) {
|
||||
const bar = this.convertToOHLC(fetchedByTs.get(ts), ticker, periodSeconds, metadata);
|
||||
const bar = this.convertToOHLC(fetchedByTs.get(ts), ticker, periodSeconds, metadata, exchangeName);
|
||||
prevClose = bar.close;
|
||||
allCandles.push(bar);
|
||||
} else if (prevClose !== null) {
|
||||
@@ -298,11 +324,9 @@ export class CCXTFetcher {
|
||||
open_time: (ts * 1_000_000).toString(),
|
||||
close_time: ((ts + periodSeconds * 1000) * 1_000_000).toString()
|
||||
};
|
||||
if (isBinance) {
|
||||
gapBar.buy_vol = '0';
|
||||
gapBar.sell_vol = '0';
|
||||
gapBar.num_trades = '0';
|
||||
gapBar.quote_volume = '0';
|
||||
const gapExtensions = EXCHANGE_OHLCV_EXTENSIONS[exchangeName] || {};
|
||||
for (const [fieldName] of Object.entries(gapExtensions)) {
|
||||
if (fieldName !== 'close_time') gapBar[fieldName] = '0';
|
||||
}
|
||||
allCandles.push(gapBar);
|
||||
}
|
||||
@@ -368,7 +392,7 @@ export class CCXTFetcher {
|
||||
*
|
||||
* Prices/volumes use integer representation scaled by market metadata precision.
|
||||
*/
|
||||
convertToOHLC(candle, ticker, periodSeconds, metadata) {
|
||||
convertToOHLC(candle, ticker, periodSeconds, metadata, exchangeName = null) {
|
||||
const timestamp = Number(candle[0]);
|
||||
const open = parseFloat(candle[1]);
|
||||
const high = parseFloat(candle[2]);
|
||||
@@ -388,22 +412,21 @@ export class CCXTFetcher {
|
||||
close: Math.round(close * priceMult).toString(),
|
||||
volume: Math.round(volume * sizeMult).toString(),
|
||||
open_time: (timestamp * 1_000_000).toString(),
|
||||
close_time: ((timestamp + periodSeconds * 1000) * 1_000_000).toString(),
|
||||
};
|
||||
|
||||
if (candle.length >= 10) {
|
||||
// Binance extended klines format
|
||||
const closeTimeMs = Number(candle[6]);
|
||||
const quoteVolRaw = parseFloat(candle[7]);
|
||||
const numTrades = Number(candle[8]);
|
||||
const takerBuyBase = parseFloat(candle[9]);
|
||||
|
||||
result.close_time = (closeTimeMs * 1_000_000).toString();
|
||||
result.quote_volume = Math.round(quoteVolRaw * priceMult).toString();
|
||||
result.num_trades = numTrades.toString();
|
||||
result.buy_vol = Math.round(takerBuyBase * sizeMult).toString();
|
||||
result.sell_vol = Math.round((volume - takerBuyBase) * sizeMult).toString();
|
||||
} else {
|
||||
result.close_time = ((timestamp + periodSeconds * 1000) * 1_000_000).toString();
|
||||
const extensions = EXCHANGE_OHLCV_EXTENSIONS[exchangeName] || {};
|
||||
for (const [fieldName, spec] of Object.entries(extensions)) {
|
||||
if ('complementOf' in spec) {
|
||||
if (candle.length > spec.complementOf) {
|
||||
const base = parseFloat(candle[spec.complementOf]);
|
||||
result[fieldName] = String(Math.round((volume - base) * sizeMult));
|
||||
}
|
||||
} else if ('index' in spec) {
|
||||
if (candle.length > spec.index) {
|
||||
result[fieldName] = applyScale(candle[spec.index], spec.scale, priceMult, sizeMult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user