diff --git a/app.py b/app.py index bbfd72ac..0bbd23f3 100755 --- a/app.py +++ b/app.py @@ -13590,6 +13590,118 @@ def get_trade_data(seccode): }), 500 +@app.route('/api/market/trade/batch', methods=['POST']) +def get_batch_trade_data(): + """批量获取多只股票的交易数据(日K线) + 请求体:{ + codes: string[], // 股票代码列表(6位代码) + days: number // 获取天数,默认1 + } + 返回:{ success: true, data: { [seccode]: { data: [], stats: {} } } } + """ + try: + data = request.json + codes = data.get('codes', []) + days = data.get('days', 1) + end_date = data.get('end_date', datetime.now().strftime('%Y-%m-%d')) + + if not codes: + return jsonify({'success': False, 'error': '请提供股票代码列表'}), 400 + + if len(codes) > 100: + return jsonify({'success': False, 'error': '单次最多查询100只股票'}), 400 + + # 构建批量查询 + placeholders = ','.join([f':code{i}' for i in range(len(codes))]) + params = {f'code{i}': code for i, code in enumerate(codes)} + params['end_date'] = end_date + params['days'] = days + + query = text(f""" + SELECT SECCODE, + TRADEDATE, + SECNAME, + F002N as pre_close, + F003N as open, + F004N as volume, + F005N as high, + F006N as low, + F007N as close, + F008N as trades_count, + F009N as change_amount, + F010N as change_percent, + F011N as amount, + F012N as turnover_rate, + F013N as amplitude + FROM ea_trade + WHERE SECCODE IN ({placeholders}) + AND TRADEDATE <= :end_date + ORDER BY SECCODE, TRADEDATE DESC + """) + + with engine.connect() as conn: + result = conn.execute(query, params) + rows = result.fetchall() + + # 按股票代码分组,每只股票只取最近N天 + stock_data = {} + stock_counts = {} + + for row in rows: + seccode = row.SECCODE + if seccode not in stock_data: + stock_data[seccode] = [] + stock_counts[seccode] = 0 + + # 只取指定天数的数据 + if stock_counts[seccode] < days: + stock_data[seccode].append({ + 'date': format_date(row.TRADEDATE), + 'stock_name': row.SECNAME, + 'open': format_decimal(row.open), + 'high': format_decimal(row.high), + 'low': format_decimal(row.low), + 'close': format_decimal(row.close), + 'pre_close': format_decimal(row.pre_close), + 'volume': format_decimal(row.volume), + 'amount': format_decimal(row.amount), + 'change_amount': format_decimal(row.change_amount), + 'change_percent': format_decimal(row.change_percent), + 'turnover_rate': format_decimal(row.turnover_rate), + 'amplitude': format_decimal(row.amplitude), + 'trades_count': format_decimal(row.trades_count), + }) + stock_counts[seccode] += 1 + + # 倒序每只股票的数据(让最早的日期在前) + results = {} + for seccode, data_list in stock_data.items(): + data_list.reverse() + results[seccode] = { + 'data': data_list, + 'stats': { + 'latest_price': data_list[-1]['close'] if data_list else None, + 'change_percent': data_list[-1]['change_percent'] if data_list else None, + } if data_list else {} + } + + # 为没有数据的股票返回空结果 + for code in codes: + if code not in results: + results[code] = {'data': [], 'stats': {}} + + return jsonify({ + 'success': True, + 'data': results + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @app.route('/api/market/funding/', methods=['GET']) def get_funding_data(seccode): """获取融资融券数据""" diff --git a/src/components/ConceptStocksModal/index.tsx b/src/components/ConceptStocksModal/index.tsx index 671e172a..7825367c 100644 --- a/src/components/ConceptStocksModal/index.tsx +++ b/src/components/ConceptStocksModal/index.tsx @@ -89,7 +89,7 @@ const ConceptStocksModal: React.FC = ({ const modalSize = useBreakpointValue({ base: 'xl', md: '4xl' }, { fallback: 'md' }); const tableMaxH = useBreakpointValue({ base: '45vh', md: '60vh' }, { fallback: 'md' }); - // 批量获取股票行情数据 + // 批量获取股票行情数据(使用批量接口,减少网络请求) const fetchStockMarketData = useCallback(async (stocks: StockInfo[]) => { if (!stocks || stocks.length === 0) return; @@ -97,30 +97,34 @@ const ConceptStocksModal: React.FC = ({ const newMarketData: Record = {}; try { - const batchSize = 5; - for (let i = 0; i < stocks.length; i += batchSize) { - const batch = stocks.slice(i, i + batchSize); - const promises = batch.map(async (stock) => { - const stockCode = getStockCode(stock); - if (!stockCode) return null; - const seccode = stockCode.substring(0, 6); - try { - const response = await marketService.getTradeData(seccode, 1); - if (response.success && response.data?.length > 0) { - const latestData = response.data[response.data.length - 1]; - return { stock_code: stockCode, ...latestData }; - } - } catch (error) { - logger.warn('ConceptStocksModal', '获取股票行情失败', { stockCode: seccode }); - } - return null; - }); + // 提取所有6位股票代码 + const stockCodeMap: Record = {}; // seccode -> fullCode 映射 + const seccodes: string[] = []; - const results = await Promise.all(promises); - results.forEach((result) => { - if (result) newMarketData[result.stock_code] = result; + stocks.forEach((stock) => { + const stockCode = getStockCode(stock); + if (stockCode) { + const seccode = stockCode.substring(0, 6); + stockCodeMap[seccode] = stockCode; + seccodes.push(seccode); + } + }); + + if (seccodes.length === 0) return; + + // 使用批量接口一次性获取所有数据 + const response = await marketService.getBatchTradeData(seccodes, 1); + + if (response.success && response.data) { + Object.entries(response.data).forEach(([seccode, stockData]: [string, any]) => { + const fullCode = stockCodeMap[seccode]; + if (fullCode && stockData.data?.length > 0) { + const latestData = stockData.data[stockData.data.length - 1]; + newMarketData[fullCode] = { stock_code: fullCode, ...latestData }; + } }); } + setStockMarketData(newMarketData); } catch (error) { logger.error('ConceptStocksModal', 'fetchStockMarketData', error); diff --git a/src/services/marketService.js b/src/services/marketService.js index d8c45636..a39062fd 100644 --- a/src/services/marketService.js +++ b/src/services/marketService.js @@ -65,6 +65,24 @@ export const marketService = { 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 - 股票代码 diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js index 414f2474..1b765fa9 100644 --- a/src/views/Concept/index.js +++ b/src/views/Concept/index.js @@ -644,43 +644,37 @@ const ConceptCenter = () => { window.open(htmlPath, '_blank'); }; - // 获取股票行情数据 + // 获取股票行情数据(使用批量接口,减少网络请求) const fetchStockMarketData = async (stocks) => { if (!stocks || stocks.length === 0) return; - + setLoadingStockData(true); const newMarketData = {}; - + try { - // 批量获取股票数据,每次处理5个股票以避免并发过多 - const batchSize = 5; - for (let i = 0; i < stocks.length; i += batchSize) { - const batch = stocks.slice(i, i + batchSize); - const promises = batch.map(async (stock) => { - if (!stock.stock_code) return null; - - // 提取6位股票代码(去掉交易所后缀) + // 提取所有6位股票代码 + const stockCodeMap = {}; // seccode -> fullCode 映射 + const seccodes = []; + + stocks.forEach((stock) => { + if (stock.stock_code) { const seccode = stock.stock_code.substring(0, 6); - - try { - const response = await marketService.getTradeData(seccode, 1); - if (response.success && response.data && response.data.length > 0) { - const latestData = response.data[response.data.length - 1]; - return { - stock_code: stock.stock_code, - ...latestData - }; - } - } catch (error) { - logger.warn('ConceptCenter', `获取股票行情数据失败`, { stockCode: seccode, error: error.message }); - } - return null; - }); - - const batchResults = await Promise.all(promises); - batchResults.forEach(result => { - if (result) { - newMarketData[result.stock_code] = result; + stockCodeMap[seccode] = stock.stock_code; + seccodes.push(seccode); + } + }); + + if (seccodes.length === 0) return; + + // 使用批量接口一次性获取所有数据 + const response = await marketService.getBatchTradeData(seccodes, 1); + + if (response.success && response.data) { + Object.entries(response.data).forEach(([seccode, stockData]) => { + const fullCode = stockCodeMap[seccode]; + if (fullCode && stockData.data?.length > 0) { + const latestData = stockData.data[stockData.data.length - 1]; + newMarketData[fullCode] = { stock_code: fullCode, ...latestData }; } }); }