priceUpdateService.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. const { logger } = require('../utils/logger');
  2. const { executeQuery } = require('../config/database');
  3. const { broadcastPriceUpdate, broadcastMultiplePriceUpdates } = require('./websocketService');
  4. const { addTick } = require('./candlestickService');
  5. // In-memory cache for current prices
  6. const priceCache = new Map();
  7. const symbolSubscriptions = new Map();
  8. // Simulated price data for development
  9. const generateMockPriceData = (symbol) => {
  10. const basePrice = priceCache.get(symbol)?.close || 1.0;
  11. const change = (Math.random() - 0.5) * 0.02; // ±1% change
  12. const newPrice = basePrice * (1 + change);
  13. return {
  14. symbol,
  15. open: basePrice,
  16. high: Math.max(basePrice, newPrice) * (1 + Math.random() * 0.001),
  17. low: Math.min(basePrice, newPrice) * (1 - Math.random() * 0.001),
  18. close: newPrice,
  19. volume: Math.floor(Math.random() * 1000000) + 100000,
  20. timestamp: new Date(),
  21. timeframe: 'H1'
  22. };
  23. };
  24. class PriceUpdateService {
  25. constructor() {
  26. this.isRunning = false;
  27. this.updateInterval = null;
  28. this.supportedSymbols = [];
  29. this.updateCallbacks = [];
  30. this.initialize();
  31. }
  32. initialize() {
  33. // Load supported symbols from environment
  34. this.supportedSymbols = (process.env.SUPPORTED_SYMBOLS || 'EURUSD,GBPUSD,USDJPY')
  35. .split(',')
  36. .map(symbol => symbol.trim());
  37. logger.info(`Price update service initialized with symbols: ${this.supportedSymbols.join(', ')}`);
  38. // Initialize price cache with mock data
  39. this.initializePriceCache();
  40. }
  41. initializePriceCache() {
  42. this.supportedSymbols.forEach(symbol => {
  43. priceCache.set(symbol, generateMockPriceData(symbol));
  44. });
  45. logger.info('Price cache initialized');
  46. }
  47. start() {
  48. if (this.isRunning) {
  49. logger.warn('Price update service is already running');
  50. return;
  51. }
  52. const interval = parseInt(process.env.PRICE_UPDATE_INTERVAL) || 1000;
  53. this.updateInterval = setInterval(() => {
  54. this.updatePrices();
  55. }, interval);
  56. this.isRunning = true;
  57. logger.info(`Price update service started with ${interval}ms interval`);
  58. // Start cleanup interval
  59. setInterval(() => {
  60. this.cleanup();
  61. }, 60000); // Cleanup every minute
  62. }
  63. stop() {
  64. if (!this.isRunning) {
  65. return;
  66. }
  67. if (this.updateInterval) {
  68. clearInterval(this.updateInterval);
  69. this.updateInterval = null;
  70. }
  71. this.isRunning = false;
  72. logger.info('Price update service stopped');
  73. }
  74. async updatePrices() {
  75. try {
  76. const updates = [];
  77. for (const symbol of this.supportedSymbols) {
  78. const priceData = await this.generatePriceUpdate(symbol);
  79. updates.push(priceData);
  80. // Update cache
  81. priceCache.set(symbol, priceData);
  82. // Store in database (if needed)
  83. await this.storePriceData(priceData);
  84. }
  85. // Broadcast updates to WebSocket clients
  86. if (updates.length > 0) {
  87. broadcastMultiplePriceUpdates(updates);
  88. }
  89. // Call registered update callbacks
  90. this.updateCallbacks.forEach(callback => {
  91. try {
  92. callback(updates);
  93. } catch (error) {
  94. logger.error('Error in price update callback:', error);
  95. }
  96. });
  97. } catch (error) {
  98. logger.error('Error updating prices:', error);
  99. }
  100. }
  101. async generatePriceUpdate(symbol) {
  102. // In a real implementation, this would fetch from external APIs
  103. // For now, we'll generate mock data
  104. const currentPrice = priceCache.get(symbol);
  105. const change = (Math.random() - 0.5) * 0.02; // ±1% change
  106. const newPrice = currentPrice ? currentPrice.close * (1 + change) : 1.0;
  107. const priceData = {
  108. symbol,
  109. open: currentPrice ? currentPrice.close : newPrice,
  110. high: Math.max(currentPrice ? currentPrice.close : newPrice, newPrice) * (1 + Math.random() * 0.001),
  111. low: Math.min(currentPrice ? currentPrice.close : newPrice, newPrice) * (1 - Math.random() * 0.001),
  112. close: newPrice,
  113. volume: Math.floor(Math.random() * 1000000) + 100000,
  114. timestamp: new Date(),
  115. timeframe: 'H1'
  116. };
  117. // Add tick data for candlestick aggregation
  118. // Generate multiple ticks within this price update period
  119. const tickCount = Math.floor(Math.random() * 5) + 3; // 3-7 ticks per update
  120. for (let i = 0; i < tickCount; i++) {
  121. const tickPrice = newPrice + (Math.random() - 0.5) * 0.001; // Small variations
  122. const tickTimestamp = new Date(priceData.timestamp.getTime() - (tickCount - i) * 100); // Spread over the interval
  123. addTick(symbol, tickPrice, tickTimestamp);
  124. }
  125. return priceData;
  126. }
  127. async storePriceData(priceData) {
  128. try {
  129. const dbType = process.env.DB_TYPE || 'sqlite';
  130. if (dbType === 'sqlite') {
  131. await this.storePriceDataSQLite(priceData);
  132. } else if (dbType === 'mysql') {
  133. await this.storePriceDataMySQL(priceData);
  134. } else if (dbType === 'postgres') {
  135. await this.storePriceDataPostgreSQL(priceData);
  136. }
  137. } catch (error) {
  138. logger.error('Error storing price data:', error);
  139. }
  140. }
  141. async storePriceDataSQLite(priceData) {
  142. const query = `
  143. INSERT OR IGNORE INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
  144. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  145. `;
  146. await executeQuery(query, [
  147. priceData.symbol,
  148. priceData.timeframe,
  149. priceData.timestamp,
  150. priceData.open,
  151. priceData.high,
  152. priceData.low,
  153. priceData.close,
  154. priceData.volume
  155. ]);
  156. }
  157. async storePriceDataMySQL(priceData) {
  158. const query = `
  159. INSERT IGNORE INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
  160. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  161. `;
  162. await executeQuery(query, [
  163. priceData.symbol,
  164. priceData.timeframe,
  165. priceData.timestamp,
  166. priceData.open,
  167. priceData.high,
  168. priceData.low,
  169. priceData.close,
  170. priceData.volume
  171. ]);
  172. }
  173. async storePriceDataPostgreSQL(priceData) {
  174. const query = `
  175. INSERT INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
  176. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  177. ON CONFLICT DO NOTHING
  178. `;
  179. await executeQuery(query, [
  180. priceData.symbol,
  181. priceData.timeframe,
  182. priceData.timestamp,
  183. priceData.open,
  184. priceData.high,
  185. priceData.low,
  186. priceData.close,
  187. priceData.volume
  188. ]);
  189. }
  190. // Get current price for a symbol
  191. getCurrentPrice(symbol) {
  192. return priceCache.get(symbol);
  193. }
  194. // Get current prices for multiple symbols
  195. getCurrentPrices(symbols = null) {
  196. const targetSymbols = symbols || this.supportedSymbols;
  197. const prices = {};
  198. targetSymbols.forEach(symbol => {
  199. prices[symbol] = priceCache.get(symbol);
  200. });
  201. return prices;
  202. }
  203. // Get historical data for a symbol
  204. async getHistoricalData(symbol, timeframe = 'H1', limit = 1000) {
  205. try {
  206. const dbType = process.env.DB_TYPE || 'sqlite';
  207. let query;
  208. if (dbType === 'sqlite') {
  209. query = `
  210. SELECT * FROM price_data
  211. WHERE symbol = ? AND timeframe = ?
  212. ORDER BY timestamp DESC
  213. LIMIT ?
  214. `;
  215. } else {
  216. query = `
  217. SELECT * FROM price_data
  218. WHERE symbol = ? AND timeframe = ?
  219. ORDER BY timestamp DESC
  220. LIMIT ?
  221. `;
  222. }
  223. const params = [symbol, timeframe, limit];
  224. const data = await executeQuery(query, params);
  225. return data.reverse(); // Return in chronological order
  226. } catch (error) {
  227. logger.error(`Error fetching historical data for ${symbol}:`, error);
  228. return [];
  229. }
  230. }
  231. // Register callback for price updates
  232. onPriceUpdate(callback) {
  233. this.updateCallbacks.push(callback);
  234. }
  235. // Remove price update callback
  236. removePriceUpdateCallback(callback) {
  237. const index = this.updateCallbacks.indexOf(callback);
  238. if (index > -1) {
  239. this.updateCallbacks.splice(index, 1);
  240. }
  241. }
  242. // Cleanup old data
  243. async cleanup() {
  244. try {
  245. const cutoffDate = new Date();
  246. cutoffDate.setDate(cutoffDate.getDate() - 30); // Keep 30 days of data
  247. const dbType = process.env.DB_TYPE || 'sqlite';
  248. let query;
  249. if (dbType === 'sqlite') {
  250. query = 'DELETE FROM price_data WHERE timestamp < ?';
  251. } else {
  252. query = 'DELETE FROM price_data WHERE timestamp < ?';
  253. }
  254. const params = [cutoffDate];
  255. await executeQuery(query, params);
  256. logger.info('Price data cleanup completed');
  257. } catch (error) {
  258. logger.error('Error during price data cleanup:', error);
  259. }
  260. }
  261. // Get service statistics
  262. getStats() {
  263. return {
  264. isRunning: this.isRunning,
  265. supportedSymbols: this.supportedSymbols,
  266. cachedSymbols: Array.from(priceCache.keys()),
  267. updateCallbacks: this.updateCallbacks.length,
  268. cacheSize: priceCache.size,
  269. uptime: process.uptime()
  270. };
  271. }
  272. }
  273. // Create singleton instance
  274. const priceUpdateService = new PriceUpdateService();
  275. // Export functions for external use
  276. const startPriceUpdates = (io) => {
  277. priceUpdateService.start();
  278. };
  279. const stopPriceUpdates = () => {
  280. priceUpdateService.stop();
  281. };
  282. const getCurrentPrice = (symbol) => {
  283. return priceUpdateService.getCurrentPrice(symbol);
  284. };
  285. const getCurrentPrices = (symbols) => {
  286. return priceUpdateService.getCurrentPrices(symbols);
  287. };
  288. const getHistoricalData = (symbol, timeframe, limit) => {
  289. return priceUpdateService.getHistoricalData(symbol, timeframe, limit);
  290. };
  291. const onPriceUpdate = (callback) => {
  292. priceUpdateService.onPriceUpdate(callback);
  293. };
  294. const getPriceUpdateStats = () => {
  295. return priceUpdateService.getStats();
  296. };
  297. module.exports = {
  298. startPriceUpdates,
  299. stopPriceUpdates,
  300. getCurrentPrice,
  301. getCurrentPrices,
  302. getHistoricalData,
  303. onPriceUpdate,
  304. getPriceUpdateStats,
  305. priceUpdateService
  306. };