update pay ui
This commit is contained in:
112
app.py
112
app.py
@@ -13590,6 +13590,118 @@ def get_trade_data(seccode):
|
|||||||
}), 500
|
}), 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/<seccode>', methods=['GET'])
|
@app.route('/api/market/funding/<seccode>', methods=['GET'])
|
||||||
def get_funding_data(seccode):
|
def get_funding_data(seccode):
|
||||||
"""获取融资融券数据"""
|
"""获取融资融券数据"""
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
|||||||
const modalSize = useBreakpointValue({ base: 'xl', md: '4xl' }, { fallback: 'md' });
|
const modalSize = useBreakpointValue({ base: 'xl', md: '4xl' }, { fallback: 'md' });
|
||||||
const tableMaxH = useBreakpointValue({ base: '45vh', md: '60vh' }, { fallback: 'md' });
|
const tableMaxH = useBreakpointValue({ base: '45vh', md: '60vh' }, { fallback: 'md' });
|
||||||
|
|
||||||
// 批量获取股票行情数据
|
// 批量获取股票行情数据(使用批量接口,减少网络请求)
|
||||||
const fetchStockMarketData = useCallback(async (stocks: StockInfo[]) => {
|
const fetchStockMarketData = useCallback(async (stocks: StockInfo[]) => {
|
||||||
if (!stocks || stocks.length === 0) return;
|
if (!stocks || stocks.length === 0) return;
|
||||||
|
|
||||||
@@ -97,30 +97,34 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
|||||||
const newMarketData: Record<string, MarketData> = {};
|
const newMarketData: Record<string, MarketData> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const batchSize = 5;
|
// 提取所有6位股票代码
|
||||||
for (let i = 0; i < stocks.length; i += batchSize) {
|
const stockCodeMap: Record<string, string> = {}; // seccode -> fullCode 映射
|
||||||
const batch = stocks.slice(i, i + batchSize);
|
const seccodes: string[] = [];
|
||||||
const promises = batch.map(async (stock) => {
|
|
||||||
|
stocks.forEach((stock) => {
|
||||||
const stockCode = getStockCode(stock);
|
const stockCode = getStockCode(stock);
|
||||||
if (!stockCode) return null;
|
if (stockCode) {
|
||||||
const seccode = stockCode.substring(0, 6);
|
const seccode = stockCode.substring(0, 6);
|
||||||
try {
|
stockCodeMap[seccode] = stockCode;
|
||||||
const response = await marketService.getTradeData(seccode, 1);
|
seccodes.push(seccode);
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
if (seccodes.length === 0) return;
|
||||||
results.forEach((result) => {
|
|
||||||
if (result) newMarketData[result.stock_code] = result;
|
// 使用批量接口一次性获取所有数据
|
||||||
|
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);
|
setStockMarketData(newMarketData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('ConceptStocksModal', 'fetchStockMarketData', error);
|
logger.error('ConceptStocksModal', 'fetchStockMarketData', error);
|
||||||
|
|||||||
@@ -65,6 +65,24 @@ export const marketService = {
|
|||||||
return await apiRequest(url);
|
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 {string} seccode - 股票代码
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ const ConceptCenter = () => {
|
|||||||
window.open(htmlPath, '_blank');
|
window.open(htmlPath, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取股票行情数据
|
// 获取股票行情数据(使用批量接口,减少网络请求)
|
||||||
const fetchStockMarketData = async (stocks) => {
|
const fetchStockMarketData = async (stocks) => {
|
||||||
if (!stocks || stocks.length === 0) return;
|
if (!stocks || stocks.length === 0) return;
|
||||||
|
|
||||||
@@ -652,35 +652,29 @@ const ConceptCenter = () => {
|
|||||||
const newMarketData = {};
|
const newMarketData = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 批量获取股票数据,每次处理5个股票以避免并发过多
|
// 提取所有6位股票代码
|
||||||
const batchSize = 5;
|
const stockCodeMap = {}; // seccode -> fullCode 映射
|
||||||
for (let i = 0; i < stocks.length; i += batchSize) {
|
const seccodes = [];
|
||||||
const batch = stocks.slice(i, i + batchSize);
|
|
||||||
const promises = batch.map(async (stock) => {
|
|
||||||
if (!stock.stock_code) return null;
|
|
||||||
|
|
||||||
// 提取6位股票代码(去掉交易所后缀)
|
stocks.forEach((stock) => {
|
||||||
|
if (stock.stock_code) {
|
||||||
const seccode = stock.stock_code.substring(0, 6);
|
const seccode = stock.stock_code.substring(0, 6);
|
||||||
|
stockCodeMap[seccode] = stock.stock_code;
|
||||||
try {
|
seccodes.push(seccode);
|
||||||
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);
|
if (seccodes.length === 0) return;
|
||||||
batchResults.forEach(result => {
|
|
||||||
if (result) {
|
// 使用批量接口一次性获取所有数据
|
||||||
newMarketData[result.stock_code] = result;
|
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 };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user