UNPKG

skayn-trading-sdk

Version:

Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture

181 lines (153 loc) 5.34 kB
const IDataProvider = require('../../interfaces/IDataProvider'); const logger = require('../../utils/logger'); const fs = require('fs'); const path = require('path'); /** * Offline Historical Data Provider * Uses bundled historical data - no internet required * Perfect for demos, testing, and areas with restricted API access */ class OfflineDataProvider extends IDataProvider { constructor() { super(); this.dataPath = path.join(__dirname, '../../data/historical'); this.loadedData = new Map(); } /** * Get historical data from bundled files * @param {string} startDate - ISO date string * @param {string} endDate - ISO date string * @param {string} interval - Interval (1h, 4h, 1d) * @param {string} symbol - Trading pair * @returns {Promise<Array>} OHLCV data */ async getData(startDate, endDate, interval = '1d', symbol = 'BTCUSDT') { try { logger.info('📊 Loading historical data (offline)', { symbol, interval, period: `${startDate} to ${endDate}` }); // Load the appropriate dataset const dataset = await this.loadDataset(symbol, interval); if (!dataset || dataset.length === 0) { throw new Error(`No offline data available for ${symbol} ${interval}`); } // Filter data by date range const startTime = new Date(startDate).getTime(); const endTime = new Date(endDate).getTime(); const filteredData = dataset.filter(bar => { const barTime = new Date(bar.timestamp).getTime(); return barTime >= startTime && barTime <= endTime; }); logger.info('✅ Offline historical data loaded', { totalBars: filteredData.length, period: `${filteredData[0]?.timestamp} to ${filteredData[filteredData.length - 1]?.timestamp}` }); return filteredData; } catch (error) { logger.error('❌ Failed to load offline data', { error: error.message }); throw error; } } /** * Load dataset from bundled files */ async loadDataset(symbol, interval) { const cacheKey = `${symbol}_${interval}`; // Return cached data if available if (this.loadedData.has(cacheKey)) { return this.loadedData.get(cacheKey); } // Try to load from file const filename = `${symbol}_${interval}.json`; const filepath = path.join(this.dataPath, filename); try { if (fs.existsSync(filepath)) { const rawData = fs.readFileSync(filepath, 'utf8'); const data = JSON.parse(rawData); this.loadedData.set(cacheKey, data); return data; } } catch (error) { logger.warn('Failed to load dataset file', { filepath, error: error.message }); } // Fallback to sample data generator return this.generateSampleData(startDate, endDate, interval); } /** * Generate realistic sample data for testing */ generateSampleData(startDate, endDate, interval) { logger.info('🎲 Generating sample data (no historical data found)'); const start = new Date(startDate); const end = new Date(endDate); const data = []; // Calculate interval in milliseconds const intervalMs = this.getIntervalMs(interval); let currentTime = start.getTime(); let currentPrice = 45000; // Starting BTC price while (currentTime <= end.getTime()) { // Generate realistic price movement const change = (Math.random() - 0.5) * 0.04; // ±2% max change const volatility = 0.001 + Math.random() * 0.003; // 0.1-0.4% volatility const open = currentPrice; const close = open * (1 + change); const high = Math.max(open, close) * (1 + Math.random() * volatility); const low = Math.min(open, close) * (1 - Math.random() * volatility); const volume = 100000 + Math.random() * 500000; data.push({ timestamp: new Date(currentTime).toISOString(), open: Math.round(open * 100) / 100, high: Math.round(high * 100) / 100, low: Math.round(low * 100) / 100, close: Math.round(close * 100) / 100, volume: Math.round(volume) }); currentPrice = close; currentTime += intervalMs; } return data; } /** * Convert interval string to milliseconds */ getIntervalMs(interval) { const intervals = { '1m': 60 * 1000, '5m': 5 * 60 * 1000, '15m': 15 * 60 * 1000, '1h': 60 * 60 * 1000, '4h': 4 * 60 * 60 * 1000, '1d': 24 * 60 * 60 * 1000 }; return intervals[interval] || intervals['1d']; } /** * Check if offline data is available */ hasOfflineData(symbol, interval) { const filename = `${symbol}_${interval}.json`; const filepath = path.join(this.dataPath, filename); return fs.existsSync(filepath); } /** * Get available datasets */ getAvailableDatasets() { try { if (!fs.existsSync(this.dataPath)) { return []; } return fs.readdirSync(this.dataPath) .filter(file => file.endsWith('.json')) .map(file => { const [symbol, interval] = file.replace('.json', '').split('_'); return { symbol, interval, file }; }); } catch (error) { return []; } } } module.exports = OfflineDataProvider;