skayn-trading-sdk
Version:
Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture
181 lines (153 loc) • 5.34 kB
JavaScript
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;