// src/services/marketService.js /** * 完整的市场行情数据服务层 * 对应Flask后端的所有市场API接口 */ import axios from '@utils/axiosConfig'; import { logger } from '../utils/logger'; /** * 统一的 API 请求函数 * axios 拦截器已自动处理日志记录 */ const apiRequest = async (url, options = {}) => { const { method = 'GET', body, ...rest } = options; const config = { method, url, ...rest, }; // 如果有 body,根据方法设置 data 或 params if (body) { if (method === 'GET') { config.params = typeof body === 'string' ? JSON.parse(body) : body; } else { config.data = typeof body === 'string' ? JSON.parse(body) : body; } } const response = await axios(config); return response.data; }; export const marketService = { /** * 获取股票交易数据(日K线) * @param {string} seccode - 股票代码 * @param {number} days - 获取天数 * @param {string} end_date - 截止日期 */ getTradeData: async (seccode, days = 60, end_date = null) => { let url = `/api/market/trade/${seccode}?days=${days}`; if (end_date) { url += `&end_date=${end_date}`; } return await apiRequest(url); }, /** * 批量获取多只股票的交易数据(日K线) * @param {string[]} codes - 股票代码数组(6位代码) * @param {number} days - 获取天数,默认1 * @param {string} end_date - 截止日期 * @returns {Promise<{success: boolean, data: Object}>} */ getBatchTradeData: async (codes, days = 1, end_date = null) => { const body = { codes, days }; if (end_date) { body.end_date = end_date; } return await apiRequest('/api/market/trade/batch', { method: 'POST', body: JSON.stringify(body) }); }, /** * 获取融资融券数据 * @param {string} seccode - 股票代码 * @param {number} days - 获取天数 */ getFundingData: async (seccode, days = 30) => { return await apiRequest(`/api/market/funding/${seccode}?days=${days}`); }, /** * 获取大宗交易数据 * @param {string} seccode - 股票代码 * @param {number} days - 获取天数 */ getBigDealData: async (seccode, days = 30) => { return await apiRequest(`/api/market/bigdeal/${seccode}?days=${days}`); }, /** * 获取龙虎榜数据 * @param {string} seccode - 股票代码 * @param {number} days - 获取天数 */ getUnusualData: async (seccode, days = 30) => { return await apiRequest(`/api/market/unusual/${seccode}?days=${days}`); }, /** * 获取股权质押数据 * @param {string} seccode - 股票代码 */ getPledgeData: async (seccode) => { return await apiRequest(`/api/market/pledge/${seccode}`); }, /** * 获取市场汇总数据 * @param {string} seccode - 股票代码 */ getMarketSummary: async (seccode) => { return await apiRequest(`/api/market/summary/${seccode}`); }, /** * 批量获取多只股票数据 * @param {array} seccodes - 股票代码数组 */ getBatchMarketData: async (seccodes) => { const promises = seccodes.map(code => marketService.getMarketSummary(code)); const results = await Promise.allSettled(promises); return results.map((result, index) => { if (result.status === 'fulfilled' && result.value.success) { return result.value.data; } return { stock_code: seccodes[index], error: true, message: result.reason?.message || 'Failed to fetch data' }; }); }, }; // 数据格式化工具函数(与financialService保持一致的风格) export const marketFormatUtils = { /** * 格式化大数字(亿/万) * @param {number} num - 数字 * @param {number} decimal - 小数位数 */ formatLargeNumber: (num, decimal = 2) => { if (num === null || num === undefined) return '-'; const absNum = Math.abs(num); let result = ''; if (absNum >= 100000000) { result = (num / 100000000).toFixed(decimal) + '亿'; } else if (absNum >= 10000) { result = (num / 10000).toFixed(decimal) + '万'; } else if (absNum >= 1) { result = num.toFixed(decimal); } else { result = num.toFixed(4); } return result; }, /** * 格式化百分比 * @param {number} num - 数字 * @param {number} decimal - 小数位数 */ formatPercent: (num, decimal = 2) => { if (num === null || num === undefined) return '-'; return num.toFixed(decimal) + '%'; }, /** * 格式化日期 * @param {string} dateStr - 日期字符串 */ formatDate: (dateStr) => { if (!dateStr) return '-'; const date = new Date(dateStr); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; }, /** * 格式化成交量 * @param {number} volume - 成交量 */ formatVolume: (volume) => { if (!volume) return '-'; if (volume >= 100000000) { return (volume / 100000000).toFixed(2) + '亿股'; } else if (volume >= 10000) { return (volume / 10000).toFixed(2) + '万股'; } return volume.toFixed(0) + '股'; }, /** * 格式化成交额 * @param {number} amount - 成交额 */ formatAmount: (amount) => { if (!amount) return '-'; return '¥' + marketFormatUtils.formatLargeNumber(amount); }, /** * 获取涨跌颜色(中国市场:红涨绿跌) * @param {number} value - 涨跌值 */ getPriceColor: (value) => { if (value === null || value === undefined) return 'gray.500'; if (value > 0) return 'red.500'; if (value < 0) return 'green.500'; return 'gray.500'; }, /** * 格式化涨跌幅 * @param {number} change - 涨跌幅 */ formatChange: (change) => { if (change === null || change === undefined) return '-'; const formatted = change.toFixed(2) + '%'; return change >= 0 ? '+' + formatted : formatted; }, /** * 格式化换手率 * @param {number} turnover - 换手率 */ formatTurnover: (turnover) => { if (!turnover) return '-'; return turnover.toFixed(2) + '%'; }, /** * 获取融资融券状态 * @param {object} funding - 融资融券数据 */ getFundingStatus: (funding) => { if (!funding) return { status: 'unknown', label: '未知' }; const netFinancing = funding.financing.buy - funding.financing.repay; const netSecurities = funding.securities.sell - funding.securities.repay; if (netFinancing > 0 && netSecurities <= 0) { return { status: 'bullish', label: '看多', color: 'red.500' }; } else if (netFinancing <= 0 && netSecurities > 0) { return { status: 'bearish', label: '看空', color: 'green.500' }; } else if (netFinancing > 0 && netSecurities > 0) { return { status: 'divergent', label: '分歧', color: 'yellow.500' }; } else { return { status: 'neutral', label: '中性', color: 'gray.500' }; } }, /** * 计算技术指标 * @param {array} data - K线数据 */ calculateTechnicalIndicators: (data) => { // 计算MA均线 const calculateMA = (period) => { return data.map((item, index) => { if (index < period - 1) return null; const sum = data.slice(index - period + 1, index + 1) .reduce((acc, cur) => acc + cur.close, 0); return sum / period; }); }; // 计算RSI const calculateRSI = (period = 14) => { const changes = data.map((item, index) => { if (index === 0) return null; return item.close - data[index - 1].close; }); let avgGain = 0; let avgLoss = 0; for (let i = 1; i <= period; i++) { if (changes[i] > 0) avgGain += changes[i]; else avgLoss += Math.abs(changes[i]); } avgGain /= period; avgLoss /= period; const rsi = [null]; for (let i = 1; i < period; i++) { rsi.push(null); } for (let i = period; i < data.length; i++) { const change = changes[i]; if (change > 0) { avgGain = (avgGain * (period - 1) + change) / period; avgLoss = (avgLoss * (period - 1)) / period; } else { avgGain = (avgGain * (period - 1)) / period; avgLoss = (avgLoss * (period - 1) + Math.abs(change)) / period; } const rs = avgGain / avgLoss; rsi.push(100 - (100 / (1 + rs))); } return rsi; }; return { ma5: calculateMA(5), ma10: calculateMA(10), ma20: calculateMA(20), ma60: calculateMA(60), rsi: calculateRSI(14) }; }, /** * 获取大宗交易评级 * @param {number} amount - 交易金额 * @param {number} avgAmount - 平均交易金额 */ getBigDealRating: (amount, avgAmount) => { if (!amount || !avgAmount) return { level: 'normal', label: '普通' }; const ratio = amount / avgAmount; if (ratio >= 3) return { level: 'huge', label: '超大', color: 'red.600' }; if (ratio >= 2) return { level: 'large', label: '大额', color: 'orange.500' }; if (ratio >= 1.5) return { level: 'medium', label: '中等', color: 'yellow.500' }; return { level: 'normal', label: '普通', color: 'gray.500' }; }, }; // 图表数据处理工具 export const marketChartUtils = { /** * 准备K线图数据 * @param {array} data - 原始交易数据 */ prepareKLineData: (data) => { return data.map(item => ({ date: item.date.substring(5, 10), // MM-DD格式 fullDate: item.date, open: item.open, high: item.high, low: item.low, close: item.close, volume: item.volume, amount: item.amount, change: item.change_percent, k: [item.open, item.close, item.low, item.high], // K线数据格式 color: item.change_percent >= 0 ? '#ef4444' : '#10b981' // 红涨绿跌 })); }, /** * 准备融资融券趋势图数据 * @param {array} data - 原始融资融券数据 */ prepareFundingTrendData: (data) => { return data.map(item => ({ date: item.date.substring(5, 10), fullDate: item.date, financing: item.financing.balance / 100000000, // 转换为亿 securities: item.securities.balance_amount / 100000000, total: item.total_balance / 100000000, netBuy: (item.financing.buy - item.financing.repay) / 10000, // 转换为万 netSell: (item.securities.sell - item.securities.repay) / 10000 })); }, /** * 准备龙虎榜资金流向图数据 * @param {array} data - 龙虎榜数据 */ prepareUnusualFlowData: (data) => { const flowData = {}; data.grouped_data.forEach(dayData => { flowData[dayData.date] = { date: dayData.date, totalBuy: dayData.total_buy / 10000, // 转换为万 totalSell: dayData.total_sell / 10000, netFlow: dayData.net_amount / 10000, buyersCount: dayData.buyers.length, sellersCount: dayData.sellers.length }; }); return Object.values(flowData).sort((a, b) => new Date(a.date) - new Date(b.date) ); }, /** * 准备股权质押趋势图数据 * @param {array} data - 原始质押数据 */ preparePledgeTrendData: (data) => { return data.map(item => ({ date: item.end_date.substring(0, 10), pledgeRatio: item.pledge_ratio, pledgeCount: item.pledge_count, totalPledge: item.total_pledge / 10000, // 转换为万股 unrestricted: item.unrestricted_pledge / 10000, restricted: item.restricted_pledge / 10000 })).reverse(); // 按时间正序排列 }, /** * 获取图表配色方案 * @param {string} theme - 主题名称 */ getChartTheme: (theme = 'default') => { const themes = { default: { upColor: '#ef4444', // 红色上涨 downColor: '#10b981', // 绿色下跌 volumeUpColor: 'rgba(239, 68, 68, 0.7)', volumeDownColor: 'rgba(16, 185, 129, 0.7)', ma5Color: '#3b82f6', ma10Color: '#8b5cf6', ma20Color: '#f59e0b', ma60Color: '#ec4899', gridColor: '#e5e7eb' }, dark: { upColor: '#ef4444', downColor: '#10b981', volumeUpColor: 'rgba(239, 68, 68, 0.5)', volumeDownColor: 'rgba(16, 185, 129, 0.5)', ma5Color: '#60a5fa', ma10Color: '#a78bfa', ma20Color: '#fbbf24', ma60Color: '#f472b6', gridColor: '#374151' } }; return themes[theme] || themes.default; }, }; // 数据缓存管理 export const marketCacheManager = { cache: new Map(), /** * 生成缓存键 * @param {string} stockCode - 股票代码 * @param {string} dataType - 数据类型 * @param {object} params - 查询参数 */ generateKey: (stockCode, dataType, params = {}) => { return `market_${stockCode}_${dataType}_${JSON.stringify(params)}`; }, /** * 获取缓存数据 * @param {string} key - 缓存键 * @param {number} maxAge - 最大缓存时间(毫秒) */ get: function(key, maxAge = 5 * 60 * 1000) { const cached = this.cache.get(key); if (cached) { const { data, timestamp } = cached; if (Date.now() - timestamp < maxAge) { logger.debug('marketCacheManager', '缓存命中', { key }); return data; } this.cache.delete(key); } return null; }, /** * 设置缓存数据 * @param {string} key - 缓存键 * @param {any} data - 要缓存的数据 */ set: function(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); logger.debug('marketCacheManager', '数据已缓存', { key }); }, /** * 清除所有缓存 */ clear: function() { const cacheSize = this.cache.size; this.cache.clear(); logger.debug('marketCacheManager', '清除所有缓存', { clearedCount: cacheSize }); }, /** * 清除特定股票的缓存 * @param {string} stockCode - 股票代码 */ clearStock: function(stockCode) { let clearedCount = 0; for (const key of this.cache.keys()) { if (key.includes(`market_${stockCode}_`)) { this.cache.delete(key); clearedCount++; } } logger.debug('marketCacheManager', '清除股票缓存', { stockCode, clearedCount }); } }; export default marketService;