Symbol & data refactoring for Nautilus

This commit is contained in:
2026-04-01 00:59:13 -04:00
parent cd28e18e52
commit 93bc8a3a4f
55 changed files with 537 additions and 600 deletions

View File

@@ -25,7 +25,7 @@ import type {
TradingViewBar,
} from '../types/ohlc.js';
import {
secondsToMicros,
secondsToNanos,
backendToTradingView,
DEFAULT_SUPPORTED_RESOLUTIONS,
} from '../types/ohlc.js';
@@ -79,19 +79,19 @@ export class OHLCService {
countback,
}, 'Fetching OHLC data');
// Convert times to microseconds, then align to period boundaries using
// Convert times to nanoseconds, then align to period boundaries using
// [ceil(start), ceil(end)) semantics:
// - start: ceil to next period boundary — excludes any in-progress candle whose
// official timestamp is before from_time.
// - end: ceil to next period boundary, used as EXCLUSIVE upper bound — includes
// the last candle whose timestamp < to_time, excludes one sitting exactly on
// to_time (which would be the next candle, not yet started).
const periodMicros = BigInt(period_seconds) * 1_000_000n;
const raw_start = secondsToMicros(from_time);
const raw_end = secondsToMicros(to_time);
const periodNanos = BigInt(period_seconds) * 1_000_000_000n;
const raw_start = secondsToNanos(from_time);
const raw_end = secondsToNanos(to_time);
// bigint ceiling: ceil(a/b)*b = ((a + b - 1) / b) * b
const start_time = ((raw_start + periodMicros - 1n) / periodMicros) * periodMicros;
const end_time = ((raw_end + periodMicros - 1n) / periodMicros) * periodMicros; // exclusive
const start_time = ((raw_start + periodNanos - 1n) / periodNanos) * periodNanos;
const end_time = ((raw_end + periodNanos - 1n) / periodNanos) * periodNanos; // exclusive
// Step 1: Check Iceberg for existing data
let data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);
@@ -220,11 +220,11 @@ export class OHLCService {
// For now, return default symbol if query matches
if (query.toLowerCase().includes('btc') || query.toLowerCase().includes('binance')) {
return [{
symbol: 'BINANCE:BTC/USDT',
full_name: 'BINANCE:BTC/USDT',
symbol: 'BTC/USDT',
full_name: 'BTC/USDT (BINANCE)',
description: 'Bitcoin / Tether USD',
exchange: 'BINANCE',
ticker: 'BINANCE:BTC/USDT',
ticker: 'BTC/USDT.BINANCE',
type: 'crypto',
}];
}
@@ -241,12 +241,12 @@ export class OHLCService {
this.logger.debug({ symbol }, 'Resolving symbol');
// TODO: Implement central symbol registry
// For now, return default symbol info for BINANCE:BTC/USDT
if (symbol === 'BINANCE:BTC/USDT' || symbol === 'BTC/USDT') {
// For now, return default symbol info for BTC/USDT.BINANCE
if (symbol === 'BTC/USDT.BINANCE' || symbol === 'BTC/USDT') {
return {
symbol: 'BINANCE:BTC/USDT',
name: 'BINANCE:BTC/USDT',
ticker: 'BINANCE:BTC/USDT',
symbol: 'BTC/USDT',
name: 'BTC/USDT',
ticker: 'BTC/USDT.BINANCE',
description: 'Bitcoin / Tether USD',
type: 'crypto',
session: '24x7',

View File

@@ -23,7 +23,7 @@ export interface SymbolIndexServiceConfig {
export class SymbolIndexService {
private icebergClient: IcebergClient;
private logger: FastifyBaseLogger;
private symbols: Map<string, SymbolMetadata> = new Map(); // key: "EXCHANGE:MARKET_ID"
private symbols: Map<string, SymbolMetadata> = new Map(); // key: "MARKET_ID.EXCHANGE" (Nautilus format)
private initialized: boolean = false;
constructor(config: SymbolIndexServiceConfig) {
@@ -52,7 +52,7 @@ export class SymbolIndexService {
const uniqueKeys = new Set<string>();
for (const symbol of symbols) {
const key = `${symbol.exchange_id}:${symbol.market_id}`;
const key = `${symbol.market_id}.${symbol.exchange_id}`;
uniqueKeys.add(key);
this.symbols.set(key, symbol);
}
@@ -86,7 +86,7 @@ export class SymbolIndexService {
* Update or add a symbol to the index
*/
updateSymbol(symbol: SymbolMetadata): void {
const key = `${symbol.exchange_id}:${symbol.market_id}`;
const key = `${symbol.market_id}.${symbol.exchange_id}`;
this.symbols.set(key, symbol);
this.logger.debug({ key }, 'Updated symbol in index');
}
@@ -149,11 +149,11 @@ export class SymbolIndexService {
return null;
}
// ticker format: "EXCHANGE:MARKET_ID" or just "MARKET_ID"
// ticker format: "MARKET_ID.EXCHANGE" (Nautilus) or just "MARKET_ID"
let key = ticker;
// If no exchange prefix, search for first match
if (!ticker.includes(':')) {
// If no dot separator, search for first match by market_id
if (!ticker.includes('.')) {
for (const [k, metadata] of this.symbols) {
if (metadata.market_id === ticker) {
key = k;
@@ -176,7 +176,7 @@ export class SymbolIndexService {
*/
private metadataToSearchResult(metadata: SymbolMetadata): SearchResult {
const symbol = metadata.market_id; // Clean format: "BTC/USDT"
const ticker = `${metadata.exchange_id}:${metadata.market_id}`; // "BINANCE:BTC/USDT"
const ticker = `${metadata.market_id}.${metadata.exchange_id}`; // "BTC/USDT.BINANCE"
const fullName = `${metadata.market_id} (${metadata.exchange_id})`;
return {
@@ -194,15 +194,12 @@ export class SymbolIndexService {
*/
private metadataToSymbolInfo(metadata: SymbolMetadata): SymbolInfo {
const symbol = metadata.market_id;
const ticker = `${metadata.exchange_id}:${metadata.market_id}`;
const ticker = `${metadata.market_id}.${metadata.exchange_id}`; // "BTC/USDT.BINANCE"
// Convert supported_period_seconds to resolution strings
const supportedResolutions = this.periodSecondsToResolutions(metadata.supported_period_seconds || []);
// Calculate pricescale from tick_denom
// tick_denom is 10^n where n is the number of decimal places
// pricescale is the same value
const pricescale = metadata.tick_denom ? Number(metadata.tick_denom) : 100;
// pricescale = 10^price_precision (e.g., price_precision=2 → pricescale=100)
const pricescale = metadata.price_precision != null ? Math.pow(10, metadata.price_precision) : 100;
return {
symbol,
@@ -222,9 +219,12 @@ export class SymbolIndexService {
base_currency: metadata.base_asset,
quote_currency: metadata.quote_asset,
data_status: 'streaming',
tick_denominator: metadata.tick_denom ? Number(metadata.tick_denom) : undefined,
base_denominator: metadata.base_denom ? Number(metadata.base_denom) : undefined,
quote_denominator: metadata.quote_denom ? Number(metadata.quote_denom) : undefined,
price_precision: metadata.price_precision,
size_precision: metadata.size_precision,
tick_size: metadata.tick_size,
lot_size: metadata.lot_size,
maker_fee: metadata.maker_fee,
taker_fee: metadata.taker_fee,
};
}