skayn-trading-sdk
Version:
Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture
200 lines (170 loc) • 5.85 kB
JavaScript
const IDataProvider = require('../../interfaces/IDataProvider');
const logger = require('../../utils/logger');
/**
* Binance Historical Data Provider
* Fast, reliable, no API key required
* Rate limit: 1200 requests/minute
*/
class BinanceDataProvider extends IDataProvider {
constructor() {
super();
this.baseUrl = 'https://api.binance.com';
this.cache = new Map();
this.requestCount = 0;
this.resetTime = Date.now() + 60000; // Reset every minute
}
/**
* Fetch historical data from Binance
* @param {string} startDate - ISO date string
* @param {string} endDate - ISO date string
* @param {string} interval - Binance interval format
* @param {string} symbol - Trading pair
* @returns {Promise<Array>} OHLCV data
*/
async getData(startDate, endDate, interval = '1d', symbol = 'BTCUSDT') {
const cacheKey = this.getCacheKey(startDate, endDate, interval, symbol);
// Check cache first
if (this.cache.has(cacheKey)) {
logger.info('📊 Using cached historical data', { symbol, interval, cacheKey });
return this.cache.get(cacheKey);
}
// Check rate limits
await this.checkRateLimit();
try {
const data = await this.fetchFromBinance(startDate, endDate, interval, symbol);
// Validate data
const validation = this.validateData(data, interval);
if (!validation.valid) {
logger.warn('⚠️ Data validation issues', { issues: validation.issues });
}
// Cache the data
this.cache.set(cacheKey, data);
logger.info('✅ Historical data fetched from Binance', {
symbol,
interval,
bars: data.length,
startDate: new Date(data[0]?.timestamp).toISOString(),
endDate: new Date(data[data.length - 1]?.timestamp).toISOString()
});
return data;
} catch (error) {
logger.error('❌ Failed to fetch from Binance', {
error: error.message,
symbol,
interval,
startDate,
endDate
});
throw error;
}
}
/**
* Fetch data from Binance API with chunking for large ranges
* @param {string} startDate
* @param {string} endDate
* @param {string} interval
* @param {string} symbol
* @returns {Promise<Array>} Raw OHLCV data
*/
async fetchFromBinance(startDate, endDate, interval, symbol) {
const startTime = new Date(startDate).getTime();
const endTime = new Date(endDate).getTime();
// Binance returns max 1000 candles per request
const maxCandles = 1000;
const intervalMs = this.getIntervalMs(interval);
const maxTimeRange = maxCandles * intervalMs;
let allData = [];
let currentStart = startTime;
while (currentStart < endTime) {
const currentEnd = Math.min(currentStart + maxTimeRange, endTime);
const url = `${this.baseUrl}/api/v3/klines?` +
`symbol=${symbol}&` +
`interval=${interval}&` +
`startTime=${currentStart}&` +
`endTime=${currentEnd}&` +
`limit=${maxCandles}`;
logger.debug('🔍 Fetching Binance data chunk', {
url,
startTime: new Date(currentStart).toISOString(),
endTime: new Date(currentEnd).toISOString()
});
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Binance API error: ${response.status} ${response.statusText}`);
}
const rawData = await response.json();
const chunk = this.transformBinanceData(rawData);
allData = allData.concat(chunk);
currentStart = currentEnd + intervalMs; // Move to next chunk
// Increment request counter
this.requestCount++;
// Small delay between requests to be respectful
if (currentStart < endTime) {
await this.sleep(100);
}
}
// Sort by timestamp to ensure chronological order
return allData.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* Transform Binance raw kline data to standard OHLCV format
* @param {Array} rawData - Raw Binance kline data
* @returns {Array} Standardized OHLCV data
*/
transformBinanceData(rawData) {
return rawData.map(candle => ({
timestamp: parseInt(candle[0]), // Open time
open: parseFloat(candle[1]),
high: parseFloat(candle[2]),
low: parseFloat(candle[3]),
close: parseFloat(candle[4]),
volume: parseFloat(candle[5]),
closeTime: parseInt(candle[6]),
quoteVolume: parseFloat(candle[7]),
trades: parseInt(candle[8]),
takerBuyBaseVolume: parseFloat(candle[9]),
takerBuyQuoteVolume: parseFloat(candle[10])
}));
}
/**
* Check and enforce rate limits
*/
async checkRateLimit() {
const now = Date.now();
// Reset counter every minute
if (now > this.resetTime) {
this.requestCount = 0;
this.resetTime = now + 60000;
}
// Conservative limit: 1000 requests per minute (vs Binance's 1200)
if (this.requestCount >= 1000) {
const waitTime = this.resetTime - now;
logger.warn('⏳ Rate limit reached, waiting', { waitTime: `${waitTime}ms` });
await this.sleep(waitTime);
this.requestCount = 0;
this.resetTime = Date.now() + 60000;
}
}
/**
* Sleep utility
* @param {number} ms - Milliseconds to sleep
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Get supported intervals
* @returns {Array} Supported interval strings
*/
getSupportedIntervals() {
return ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'];
}
/**
* Clear cache
*/
clearCache() {
this.cache.clear();
logger.info('🗑️ Binance data cache cleared');
}
}
module.exports = BinanceDataProvider;