Exchange schemas define data source interfaces for fetching historical market data and formatting prices/quantities according to exchange-specific precision rules. They provide the abstraction layer between the framework's signal processing logic and external data sources (APIs, databases, CSV files, etc.).
This page covers the structure and registration of exchange schemas. For information about the runtime exchange client implementation, see ClientExchange. For utility functions that use registered exchanges, see Exchange Functions.
An exchange schema registered via addExchange() must implement the IExchangeSchema interface, which defines three core responsibilities:
getCandles()formatPrice()formatQuantity()The framework uses these methods internally for backtesting (historical data iteration) and live trading (VWAP calculation, TP/SL monitoring). The exchange schema does not contain business logic—it is a pure data access interface. Runtime logic is handled by ClientExchange.
Required Fields:
ExchangeName = string)Optional Fields:
The registration process follows this sequence:
addExchange(exchangeSchema) - Provides schema configurationToolRegistry pattern to store schema by exchangeNameloggerService.info()Once registered, the schema becomes available for use in backtest and live execution modes. The framework uses ExchangeConnectionService to lazily instantiate ClientExchange instances from the stored schema.
The getCandles() method is the primary data source interface for the framework. It must return an array of OHLCV candles sorted chronologically.
Signature:
getCandles: (
symbol: string,
interval: CandleInterval,
since: Date,
limit: number
) => Promise<ICandleData[]>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol (e.g., "BTCUSDT") |
interval |
CandleInterval |
Candle timeframe ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h") |
since |
Date |
Start date for candle fetching |
limit |
number |
Maximum number of candles to return |
Return Value:
Array of ICandleData objects with fields:
timestamp - Unix milliseconds when candle openedopen - Opening pricehigh - Highest price during periodlow - Lowest price during periodclose - Closing pricevolume - Trading volumeUsage Context:
executionContext.whenstrategy.backtest()The formatPrice() method formats raw price values to exchange-specific precision rules. This ensures displayed prices match exchange conventions.
Signature:
formatPrice: (
symbol: string,
price: number
) => Promise<string>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol (e.g., "BTCUSDT") |
price |
number |
Raw price value to format |
Return Value:
Formatted price string respecting exchange precision (e.g., "50123.45" for BTC with 2 decimal places).
Usage Context:
The formatQuantity() method formats raw quantity values to exchange-specific precision rules. This ensures displayed quantities match exchange conventions.
Signature:
formatQuantity: (
symbol: string,
quantity: number
) => Promise<string>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol (e.g., "BTCUSDT") |
quantity |
number |
Raw quantity value to format |
Return Value:
Formatted quantity string respecting exchange precision (e.g., "0.00123456" for BTC with 8 decimal places).
Usage Context:
The callbacks field allows registering lifecycle event handlers. Currently, only onCandleData is supported.
Called immediately after getCandles() completes successfully. Useful for logging, debugging, or caching candle data.
Signature:
onCandleData: (
symbol: string,
interval: CandleInterval,
since: Date,
limit: number,
data: ICandleData[]
) => void
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol |
interval |
CandleInterval |
Requested candle interval |
since |
Date |
Requested start date |
limit |
number |
Requested limit |
data |
ICandleData[] |
Fetched candle data array |
Example:
addExchange({
exchangeName: "binance",
getCandles: async (symbol, interval, since, limit) => { /* ... */ },
formatPrice: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
callbacks: {
onCandleData: (symbol, interval, since, limit, data) => {
console.log(`Fetched ${data.length} candles for ${symbol} ${interval}`);
}
}
});
Flow:
addExchange(schema), stored in ExchangeSchemaServiceExchangeConnectionService.get(exchangeName) creates memoized ClientExchange instanceIExchangeParams with schema, logger, and execution contextClientExchange delegates to schema methods, adds VWAP calculation logicKey Distinction:
IExchangeSchema - User-defined data access interface (no business logic)IExchange - Runtime interface implemented by ClientExchange (adds VWAP, forward/backward candle fetching)The ClientExchange class wraps the schema methods and adds framework-specific functionality like VWAP calculation and context-aware candle fetching. It uses executionContext.when to determine the current timestamp for fetching candles backward (past data) or forward (future data for backtesting).
The exchange schema methods do not receive explicit context parameters. Instead, context flows implicitly through ExecutionContextService:
Context Fields Used:
executionContext.when - Used to calculate since parameter for getCandles()executionContext.symbol - Passed directly to schema methodsexecutionContext.backtest - Determines whether to fetch backward (backtest) or forward (live)The schema implementation does not need to be aware of this context—it simply receives parameters and returns data. The framework handles all context-aware logic in ClientExchange.
import ccxt from 'ccxt';
const exchange = new ccxt.binance();
addExchange({
exchangeName: "binance-ccxt",
getCandles: async (symbol, interval, since, limit) => {
const ohlcv = await exchange.fetchOHLCV(
symbol,
interval,
since.getTime(),
limit
);
return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
timestamp,
open,
high,
low,
close,
volume
}));
},
formatPrice: async (symbol, price) => {
const market = exchange.market(symbol);
return exchange.priceToPrecision(symbol, price);
},
formatQuantity: async (symbol, quantity) => {
const market = exchange.market(symbol);
return exchange.amountToPrecision(symbol, quantity);
}
});
import { Database } from './db';
const db = new Database('./candles.db');
addExchange({
exchangeName: "sqlite-candles",
getCandles: async (symbol, interval, since, limit) => {
return await db.query(`
SELECT timestamp, open, high, low, close, volume
FROM candles
WHERE symbol = ? AND interval = ? AND timestamp >= ?
ORDER BY timestamp ASC
LIMIT ?
`, [symbol, interval, since.getTime(), limit]);
},
formatPrice: async (symbol, price) => {
// Get precision from metadata table
const { pricePrecision } = await db.getSymbolInfo(symbol);
return price.toFixed(pricePrecision);
},
formatQuantity: async (symbol, quantity) => {
const { quantityPrecision } = await db.getSymbolInfo(symbol);
return quantity.toFixed(quantityPrecision);
}
});
import { readCSV } from './csv-reader';
addExchange({
exchangeName: "csv-historical",
getCandles: async (symbol, interval, since, limit) => {
const allCandles = await readCSV(`./data/${symbol}_${interval}.csv`);
const filtered = allCandles.filter(c => c.timestamp >= since.getTime());
return filtered.slice(0, limit);
},
formatPrice: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8)
});
The ExchangeValidationService enforces these validation rules during registration:
Validation errors are thrown synchronously from addExchange() to provide immediate feedback.
Use listExchanges() to retrieve all registered exchange schemas:
import { listExchanges } from 'backtest-kit';
const exchanges = await listExchanges();
console.log(exchanges.map(e => e.exchangeName));
// ["binance", "coinbase", "sqlite-candles"]
This is useful for:
1. Cache candle data when possible - Avoid redundant API calls by implementing caching in getCandles():
const candleCache = new Map();
getCandles: async (symbol, interval, since, limit) => {
const key = `${symbol}:${interval}:${since.getTime()}:${limit}`;
if (!candleCache.has(key)) {
candleCache.set(key, await fetchFromAPI(symbol, interval, since, limit));
}
return candleCache.get(key);
}
2. Handle rate limiting - Implement exponential backoff for API-based exchanges to avoid rate limit errors.
3. Validate candle data - Ensure returned candles are sorted chronologically and contain valid OHLCV values.
4. Use callbacks for observability - Implement onCandleData to log data fetching for debugging:
callbacks: {
onCandleData: (symbol, interval, since, limit, data) => {
logger.debug(`[Exchange] Fetched ${data.length}/${limit} candles for ${symbol}`);
}
}
5. Keep schemas stateless - Exchange schemas should not maintain state. Store state in external services (databases, caches) and keep the schema as a pure interface.