UNPKG

skayn-trading-sdk

Version:

Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture

200 lines (170 loc) 5.85 kB
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;