priceRoutes.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. const express = require('express');
  2. const router = express.Router();
  3. const { catchAsync } = require('../middleware/errorHandler');
  4. const { logger } = require('../utils/logger');
  5. const {
  6. getCurrentPrice,
  7. getCurrentPrices,
  8. getHistoricalData,
  9. getPriceUpdateStats
  10. } = require('../services/priceUpdateService');
  11. const {
  12. getHistoricalCandles,
  13. validateCandles,
  14. generateTestCandles
  15. } = require('../services/candlestickService');
  16. // GET /api/prices/current/:symbol
  17. router.get('/current/:symbol', catchAsync(async (req, res) => {
  18. const { symbol } = req.params;
  19. if (!symbol) {
  20. return res.status(400).json({
  21. status: 'error',
  22. message: 'Symbol parameter is required'
  23. });
  24. }
  25. const priceData = getCurrentPrice(symbol.toUpperCase());
  26. if (!priceData) {
  27. return res.status(404).json({
  28. status: 'error',
  29. message: `Price data not found for symbol: ${symbol}`
  30. });
  31. }
  32. res.json({
  33. status: 'success',
  34. data: {
  35. symbol: priceData.symbol,
  36. price: priceData.close,
  37. change: priceData.close - priceData.open,
  38. changePercent: ((priceData.close - priceData.open) / priceData.open) * 100,
  39. high: priceData.high,
  40. low: priceData.low,
  41. volume: priceData.volume,
  42. timestamp: priceData.timestamp,
  43. timeframe: priceData.timeframe
  44. }
  45. });
  46. }));
  47. // GET /api/prices/current
  48. router.get('/current', catchAsync(async (req, res) => {
  49. const { symbols } = req.query;
  50. let targetSymbols = null;
  51. if (symbols) {
  52. targetSymbols = symbols.split(',').map(s => s.trim().toUpperCase());
  53. }
  54. const pricesData = getCurrentPrices(targetSymbols);
  55. const formattedData = Object.entries(pricesData).map(([symbol, data]) => ({
  56. symbol,
  57. price: data.close,
  58. change: data.close - data.open,
  59. changePercent: ((data.close - data.open) / data.open) * 100,
  60. high: data.high,
  61. low: data.low,
  62. volume: data.volume,
  63. timestamp: data.timestamp,
  64. timeframe: data.timeframe
  65. }));
  66. res.json({
  67. status: 'success',
  68. data: formattedData,
  69. count: formattedData.length
  70. });
  71. }));
  72. // GET /api/prices/historical/:symbol
  73. router.get('/historical/:symbol', catchAsync(async (req, res) => {
  74. const { symbol } = req.params;
  75. const { timeframe = 'H1', limit = 1000 } = req.query;
  76. if (!symbol) {
  77. return res.status(400).json({
  78. status: 'error',
  79. message: 'Symbol parameter is required'
  80. });
  81. }
  82. const historicalData = await getHistoricalData(
  83. symbol.toUpperCase(),
  84. timeframe,
  85. parseInt(limit)
  86. );
  87. if (historicalData.length === 0) {
  88. return res.status(404).json({
  89. status: 'error',
  90. message: `Historical data not found for symbol: ${symbol}`
  91. });
  92. }
  93. res.json({
  94. status: 'success',
  95. data: historicalData,
  96. symbol: symbol.toUpperCase(),
  97. timeframe,
  98. count: historicalData.length,
  99. meta: {
  100. requestedLimit: parseInt(limit),
  101. actualCount: historicalData.length,
  102. startDate: historicalData[0]?.timestamp,
  103. endDate: historicalData[historicalData.length - 1]?.timestamp
  104. }
  105. });
  106. }));
  107. // GET /api/prices/bulk-historical
  108. router.get('/bulk-historical', catchAsync(async (req, res) => {
  109. const { symbols, timeframe = 'H1', limit = 1000 } = req.query;
  110. if (!symbols) {
  111. return res.status(400).json({
  112. status: 'error',
  113. message: 'Symbols parameter is required (comma-separated)'
  114. });
  115. }
  116. const symbolList = symbols.split(',').map(s => s.trim().toUpperCase());
  117. const results = {};
  118. for (const symbol of symbolList) {
  119. try {
  120. const data = await getHistoricalData(symbol, timeframe, parseInt(limit));
  121. results[symbol] = {
  122. status: 'success',
  123. data,
  124. count: data.length
  125. };
  126. } catch (error) {
  127. logger.error(`Error fetching historical data for ${symbol}:`, error);
  128. results[symbol] = {
  129. status: 'error',
  130. message: error.message,
  131. count: 0
  132. };
  133. }
  134. }
  135. res.json({
  136. status: 'success',
  137. data: results,
  138. timeframe,
  139. requestedLimit: parseInt(limit),
  140. symbols: symbolList
  141. });
  142. }));
  143. // GET /api/prices/stats
  144. router.get('/stats', catchAsync(async (req, res) => {
  145. const stats = getPriceUpdateStats();
  146. res.json({
  147. status: 'success',
  148. data: stats
  149. });
  150. }));
  151. // POST /api/prices/subscribe
  152. router.post('/subscribe', catchAsync(async (req, res) => {
  153. const { symbols } = req.body;
  154. if (!Array.isArray(symbols) || symbols.length === 0) {
  155. return res.status(400).json({
  156. status: 'error',
  157. message: 'Symbols array is required and cannot be empty'
  158. });
  159. }
  160. const validSymbols = symbols.map(s => s.toUpperCase());
  161. res.json({
  162. status: 'success',
  163. message: `Subscribed to ${validSymbols.length} symbols`,
  164. data: {
  165. symbols: validSymbols,
  166. timestamp: new Date().toISOString()
  167. }
  168. });
  169. }));
  170. // GET /api/prices/search
  171. router.get('/search', catchAsync(async (req, res) => {
  172. const { q: query, limit = 50 } = req.query;
  173. if (!query) {
  174. return res.status(400).json({
  175. status: 'error',
  176. message: 'Search query parameter (q) is required'
  177. });
  178. }
  179. // This would typically search your database or external API
  180. // For now, we'll return a placeholder response
  181. res.json({
  182. status: 'success',
  183. data: [],
  184. query,
  185. count: 0,
  186. message: 'Search functionality not yet implemented'
  187. });
  188. }));
  189. // GET /api/prices/validate-candles/:symbol
  190. router.get('/validate-candles/:symbol', catchAsync(async (req, res) => {
  191. const { symbol } = req.params;
  192. const { timeframe = 'H1', limit = 100 } = req.query;
  193. if (!symbol) {
  194. return res.status(400).json({
  195. status: 'error',
  196. message: 'Symbol parameter is required'
  197. });
  198. }
  199. try {
  200. // Get candles for validation
  201. const candles = await getHistoricalCandles(symbol.toUpperCase(), timeframe, parseInt(limit));
  202. if (candles.length === 0) {
  203. return res.status(404).json({
  204. status: 'error',
  205. message: `No candle data found for symbol: ${symbol}`
  206. });
  207. }
  208. // Validate the candles
  209. const validationResults = validateCandles(candles);
  210. res.json({
  211. status: 'success',
  212. data: validationResults,
  213. symbol: symbol.toUpperCase(),
  214. timeframe,
  215. count: candles.length,
  216. meta: {
  217. validationPassed: validationResults.valid === validationResults.total,
  218. validCandles: validationResults.valid,
  219. invalidCandles: validationResults.invalid,
  220. successRate: `${((validationResults.valid / validationResults.total) * 100).toFixed(2)}%`
  221. }
  222. });
  223. } catch (error) {
  224. logger.error(`Error validating candles for ${symbol}:`, error);
  225. res.status(500).json({
  226. status: 'error',
  227. message: 'Error validating candles',
  228. error: error.message
  229. });
  230. }
  231. }));
  232. // GET /api/prices/generate-test-candles/:symbol
  233. router.get('/generate-test-candles/:symbol', catchAsync(async (req, res) => {
  234. const { symbol } = req.params;
  235. const { timeframe = 'H1', count = 100 } = req.query;
  236. if (!symbol) {
  237. return res.status(400).json({
  238. status: 'error',
  239. message: 'Symbol parameter is required'
  240. });
  241. }
  242. try {
  243. const testCandles = generateTestCandles(symbol.toUpperCase(), timeframe, parseInt(count));
  244. // Validate the generated test candles
  245. const validationResults = validateCandles(testCandles);
  246. res.json({
  247. status: 'success',
  248. data: testCandles,
  249. validation: validationResults,
  250. symbol: symbol.toUpperCase(),
  251. timeframe,
  252. count: testCandles.length,
  253. meta: {
  254. description: 'Test candles generated with realistic price movements for validation purposes',
  255. validationPassed: validationResults.valid === validationResults.total,
  256. successRate: `${((validationResults.valid / validationResults.total) * 100).toFixed(2)}%`
  257. }
  258. });
  259. } catch (error) {
  260. logger.error(`Error generating test candles for ${symbol}:`, error);
  261. res.status(500).json({
  262. status: 'error',
  263. message: 'Error generating test candles',
  264. error: error.message
  265. });
  266. }
  267. }));
  268. // GET /api/prices/candle-info/:symbol
  269. router.get('/candle-info/:symbol', catchAsync(async (req, res) => {
  270. const { symbol } = req.params;
  271. const { timeframe = 'H1' } = req.query;
  272. if (!symbol) {
  273. return res.status(400).json({
  274. status: 'error',
  275. message: 'Symbol parameter is required'
  276. });
  277. }
  278. try {
  279. const candles = await getHistoricalCandles(symbol.toUpperCase(), timeframe, 1000);
  280. if (candles.length === 0) {
  281. return res.status(404).json({
  282. status: 'error',
  283. message: `No candle data found for symbol: ${symbol}`
  284. });
  285. }
  286. // Calculate statistics
  287. const closes = candles.map(c => parseFloat(c.close));
  288. const highs = candles.map(c => parseFloat(c.high));
  289. const lows = candles.map(c => parseFloat(c.low));
  290. const volumes = candles.map(c => parseInt(c.volume));
  291. const stats = {
  292. count: candles.length,
  293. priceRange: {
  294. min: Math.min(...closes),
  295. max: Math.max(...closes),
  296. avg: closes.reduce((a, b) => a + b, 0) / closes.length
  297. },
  298. highLowRange: {
  299. overallHigh: Math.max(...highs),
  300. overallLow: Math.min(...lows)
  301. },
  302. volume: {
  303. total: volumes.reduce((a, b) => a + b, 0),
  304. avg: volumes.reduce((a, b) => a + b, 0) / volumes.length,
  305. max: Math.max(...volumes),
  306. min: Math.min(...volumes)
  307. },
  308. timeframe,
  309. symbol: symbol.toUpperCase(),
  310. lastUpdated: candles[candles.length - 1]?.timestamp
  311. };
  312. res.json({
  313. status: 'success',
  314. data: stats
  315. });
  316. } catch (error) {
  317. logger.error(`Error getting candle info for ${symbol}:`, error);
  318. res.status(500).json({
  319. status: 'error',
  320. message: 'Error getting candle information',
  321. error: error.message
  322. });
  323. }
  324. }));
  325. module.exports = router;