ClientExchange is the core data access class that provides market data to trading strategies through a unified interface. It wraps external exchange data providers (CCXT libraries, custom APIs, or database sources) and handles candle fetching, price formatting, and VWAP calculation. The class implements time-aware data fetching by using ExecutionContextService to determine the temporal boundary for historical vs. future data requests.
This document covers the implementation details of ClientExchange including its constructor parameters, core methods, retry logic, and integration with the execution context system. For exchange schema registration, see Exchange Schemas. For how ClientExchange instances are managed and memoized, see Connection Services.
ClientExchange sits in the Data & Integration Layer and serves as the boundary between the framework's execution logic and external data sources. The class is instantiated by ExchangeConnectionService, wrapped by ExchangeGlobalService for context injection, and invoked by ClientStrategy during signal generation and by backtest logic during fast-forward simulation.
ClientExchange is instantiated with an IExchangeParams object that defines the exchange name, data provider functions, formatting functions, execution context reference, and logger.
| Field | Type | Description |
|---|---|---|
exchangeName |
string |
Unique identifier for the exchange (e.g., "binance", "bybit") |
getCandles |
(symbol, interval, since, limit) => Promise<ICandleData[]> |
Function to fetch candles from data source starting at since |
formatPrice |
(symbol, price) => Promise<number> |
Function to format price to exchange precision |
formatQuantity |
(symbol, quantity) => Promise<number> |
Function to format quantity to exchange precision |
execution |
ExecutionContextService |
Reference to execution context for time-aware fetching |
logger |
LoggerService |
Logger instance for debug/warn/error messages |
callbacks?.onCandleData |
(symbol, interval, since, limit, data) => void |
Optional callback invoked after fetching candles |
const exchange = new ClientExchange({
exchangeName: "binance",
getCandles: async (symbol, interval, since, limit) => {
// CCXT implementation or custom data source
return await ccxt.fetchOHLCV(symbol, interval, since.getTime(), limit);
},
formatPrice: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
execution: executionContextService,
logger: loggerService,
callbacks: {
onCandleData: (symbol, interval, since, limit, data) => {
console.log(`Fetched ${data.length} candles for ${symbol}`);
}
}
});
The getCandles method fetches historical candles backwards from the execution context time (ExecutionContextService.context.when). This method is used by strategies during signal generation to analyze past market data.
async getCandles(
symbol: string,
interval: CandleInterval,
limit: number
): Promise<ICandleData[]>
Implementation Details:
Time Adjustment Calculation src/client/ClientExchange.ts:201-212
INTERVAL_MINUTES mapping src/client/ClientExchange.ts:10-21adjust = step * limit - stepsince = when - (adjust * 60 * 1000)Retry Logic src/client/ClientExchange.ts:214
GET_CANDLES_FN which retries CC_GET_CANDLES_RETRY_COUNT timesCC_GET_CANDLES_RETRY_DELAY_MS between attempts src/client/ClientExchange.ts:114-151Range Filtering src/client/ClientExchange.ts:217-229
[since, when] timestamp rangeCallback Invocation src/client/ClientExchange.ts:231-239
params.callbacks.onCandleData if providedThe getNextCandles method fetches candles forwards from the execution context time. This method is only used in backtest mode to retrieve candles for signal duration simulation. It returns an empty array if the requested time range extends beyond Date.now().
async getNextCandles(
symbol: string,
interval: CandleInterval,
limit: number
): Promise<ICandleData[]>
Implementation Details:
Time Range Validation src/client/ClientExchange.ts:265-275
endTime = since + (limit * interval_minutes * 60 * 1000)endTime > Date.now()Data Fetching src/client/ClientExchange.ts:277
GET_CANDLES_FN retry logic as getCandleswhen timestamp instead of adjusted historical timeUsage in Backtest src/lib/services/logic/private/BacktestLogicPrivateService.ts:127-155
CC_SCHEDULE_AWAIT_MINUTES + minuteEstimatedTime + 1 candlesThe getAveragePrice method computes the Volume Weighted Average Price (VWAP) from the last N 1-minute candles, where N is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT (default: 5).
async getAveragePrice(symbol: string): Promise<number>
Implementation Details:
Typical Price Formula src/client/ClientExchange.ts:340
typicalPrice = (high + low + close) / 3VWAP Formula src/client/ClientExchange.ts:339-352
VWAP = sum(typicalPrice * volume) / sum(volume)Zero Volume Fallback src/client/ClientExchange.ts:346-349
Usage Context
ClientStrategy.backtest() during signal closure simulationThese methods delegate to user-provided formatting functions to ensure prices and quantities conform to exchange-specific precision requirements.
async formatPrice(symbol: string, price: number): Promise<number>
async formatQuantity(symbol: string, quantity: number): Promise<number>
Implementation: Direct delegation to params.formatPrice and params.formatQuantity with logging src/client/ClientExchange.ts:357-371
Typical Use Cases:
formatPrice: Rounds price to exchange's tick size (e.g., 0.01 for BTCUSDT)formatQuantity: Rounds quantity to exchange's lot size (e.g., 0.00001 BTC)ClientExchange implements robust validation and retry mechanisms to handle data quality issues and transient failures from external data sources.
The VALIDATE_NO_INCOMPLETE_CANDLES_FN function detects anomalous candles that result from incomplete API responses, such as prices that are orders of magnitude too low or zero volumes.
Validation Checks: src/client/ClientExchange.ts:31-105
Reference Price Calculation src/client/ClientExchange.ts:39-56
CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIANNumeric Validity src/client/ClientExchange.ts:65-76
Positivity Check src/client/ClientExchange.ts:79-89
Anomaly Detection src/client/ClientExchange.ts:92-104
referencePrice / CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTORThe GET_CANDLES_FN helper implements automatic retry with exponential backoff for transient failures.
Configuration Parameters:
CC_GET_CANDLES_RETRY_COUNT: Number of retry attempts (default: 3) src/config/params.ts:15CC_GET_CANDLES_RETRY_DELAY_MS: Delay between retries in milliseconds (default: 1000) src/config/params.ts:16ClientExchange depends on ExecutionContextService to determine the current temporal position, which controls the boundary between historical and future data requests.
Key Context Dependencies:
Backtest Mode src/client/ClientExchange.ts:265-275
getNextCandles checks if endTime > Date.now()Time Boundaries src/client/ClientExchange.ts:211-212, src/client/ClientExchange.ts:217-223
execution.context.when as referenceSymbol Context src/interfaces/Exchange.interface.ts:18-35
ClientExchange// Inside ClientStrategy.tick()
const exchange = exchangeGlobalService.getExchange(symbol, exchangeName);
const candles = await exchange.getCandles("BTCUSDT", "1h", 24);
// Analyze candles and generate signal
const signal = strategy.getSignal(symbol, candles);
// Inside BacktestLogicPrivateService.run()
if (result.action === "opened") {
const candles = await exchangeGlobalService.getNextCandles(
symbol,
"1m",
signal.minuteEstimatedTime,
when,
true
);
const backtestResult = await strategyGlobalService.backtest(symbol, candles, when, true);
}
// Inside ClientStrategy.backtest()
const vwap = await this.params.exchange.getAveragePrice(symbol);
const pnl = this.calculatePnL(signal, vwap);
import { addExchange } from "backtest-kit";
addExchange({
exchangeName: "custom-database",
getCandles: async (symbol, interval, since, limit) => {
const query = `
SELECT timestamp, open, high, low, close, volume
FROM candles
WHERE symbol = ? AND interval = ? AND timestamp >= ?
ORDER BY timestamp ASC
LIMIT ?
`;
const rows = await db.query(query, [symbol, interval, since.getTime(), limit]);
return rows.map(row => ({
timestamp: row.timestamp,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume
}));
},
formatPrice: async (symbol, price) => {
const precision = await db.getPricePrecision(symbol);
return Number(price.toFixed(precision));
},
formatQuantity: async (symbol, quantity) => {
const precision = await db.getQuantityPrecision(symbol);
return Number(quantity.toFixed(precision));
}
});