update pay ui

This commit is contained in:
2025-12-10 13:23:49 +08:00
parent 1adbeda168
commit d6d4bb8a12
3 changed files with 213 additions and 25 deletions

98
app.py
View File

@@ -12832,8 +12832,61 @@ def get_concept_stocks(concept_id):
})
stock_codes.append(code)
# 2. 从 ClickHouse 获取最新涨跌幅
change_map = {}
if not stock_codes:
return jsonify({
'success': True,
'data': {
'concept_id': concept_id,
'concept_name': concept_name,
'stocks': stocks_info
}
})
# 2. 获取最新交易日和前一交易日
today = datetime.now().date()
trading_day = None
prev_trading_day = None
with engine.connect() as conn:
# 获取最新交易日
result = conn.execute(text("""
SELECT EXCHANGE_DATE FROM trading_days
WHERE EXCHANGE_DATE <= :today
ORDER BY EXCHANGE_DATE DESC LIMIT 1
"""), {"today": today}).fetchone()
if result:
trading_day = result[0].date() if hasattr(result[0], 'date') else result[0]
# 获取前一交易日
if trading_day:
result = conn.execute(text("""
SELECT EXCHANGE_DATE FROM trading_days
WHERE EXCHANGE_DATE < :date
ORDER BY EXCHANGE_DATE DESC LIMIT 1
"""), {"date": trading_day}).fetchone()
if result:
prev_trading_day = result[0].date() if hasattr(result[0], 'date') else result[0]
# 3. 从 MySQL ea_trade 获取前一交易日收盘价F007N
prev_close_map = {}
if prev_trading_day and stock_codes:
with engine.connect() as conn:
placeholders = ','.join([f':code{i}' for i in range(len(stock_codes))])
params = {f'code{i}': code for i, code in enumerate(stock_codes)}
params['trade_date'] = prev_trading_day
result = conn.execute(text(f"""
SELECT SECCODE, F007N
FROM ea_trade
WHERE SECCODE IN ({placeholders})
AND TRADEDATE = :trade_date
AND F007N > 0
"""), params).fetchall()
prev_close_map = {row[0]: float(row[1]) for row in result if row[1]}
# 4. 从 ClickHouse 获取最新价格
current_price_map = {}
if stock_codes:
try:
ch_client = Client(
@@ -12859,35 +12912,48 @@ def get_concept_stocks(concept_id):
ch_codes_str = "','".join(ch_codes)
# 查询最新分钟数据
# 查询当天最新价格
query = f"""
SELECT code, close, pre_close
SELECT code, close
FROM stock_minute
WHERE code IN ('{ch_codes_str}')
AND timestamp >= today()
AND toDate(timestamp) = today()
ORDER BY timestamp DESC
LIMIT 1 BY code
"""
result = ch_client.execute(query)
for row in result:
ch_code, close_price, pre_close = row
if ch_code in code_mapping and pre_close and pre_close > 0:
ch_code, close_price = row
if ch_code in code_mapping and close_price:
original_code = code_mapping[ch_code]
change_pct = (float(close_price) - float(pre_close)) / float(pre_close) * 100
change_map[original_code] = round(change_pct, 2)
current_price_map[original_code] = float(close_price)
except Exception as ch_err:
app.logger.warning(f"ClickHouse 获取涨跌幅失败: {ch_err}")
app.logger.warning(f"ClickHouse 获取价格失败: {ch_err}")
# 3. 合并数据
# 5. 计算涨跌幅并合并数据
result_stocks = []
for stock in stocks_info:
stock['change_pct'] = change_map.get(stock['code'])
result_stocks.append(stock)
code = stock['code']
prev_close = prev_close_map.get(code)
current_price = current_price_map.get(code)
change_pct = None
if prev_close and current_price and prev_close > 0:
change_pct = round((current_price - prev_close) / prev_close * 100, 2)
result_stocks.append({
'code': code,
'name': stock['name'],
'reason': stock['reason'],
'change_pct': change_pct,
'price': current_price,
'prev_close': prev_close
})
# 按涨跌幅排序(涨停优先)
result_stocks.sort(key=lambda x: x.get('change_pct') or -999, reverse=True)
result_stocks.sort(key=lambda x: x.get('change_pct') if x.get('change_pct') is not None else -999, reverse=True)
return jsonify({
'success': True,
@@ -12895,12 +12961,14 @@ def get_concept_stocks(concept_id):
'concept_id': concept_id,
'concept_name': concept_name,
'stock_count': len(result_stocks),
'trading_day': str(trading_day) if trading_day else None,
'stocks': result_stocks
}
})
except Exception as e:
app.logger.error(f"获取概念股票失败: {e}")
import traceback
app.logger.error(f"获取概念股票失败: {traceback.format_exc()}")
return jsonify({
'success': False,
'error': str(e)

View File

@@ -106,8 +106,48 @@ const handleSZSERealtimeMessage = (
switch (category) {
case 'stock': {
const stockData = data as SZSEStockData;
const { prices: bidPrices, volumes: bidVolumes } = extractOrderBook(stockData.bids);
const { prices: askPrices, volumes: askVolumes } = extractOrderBook(stockData.asks);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawData = data as any; // 用于检查替代字段名
// 调试日志:检查深交所返回的盘口原始数据(临时使用 warn 级别方便调试)
if (!stockData.bids || stockData.bids.length === 0) {
logger.warn('FlexScreen', `SZSE股票数据无盘口 ${fullCode}`, {
hasBids: !!stockData.bids,
hasAsks: !!stockData.asks,
bidsLength: stockData.bids?.length || 0,
asksLength: stockData.asks?.length || 0,
// 检查替代字段名
hasBidPrices: !!rawData.bid_prices,
hasAskPrices: !!rawData.ask_prices,
dataKeys: Object.keys(stockData), // 查看服务端实际返回了哪些字段
});
}
// 优先使用 bids/asks 对象数组格式,如果不存在则尝试 bid_prices/ask_prices 分离数组格式
let bidPrices: number[] = [];
let bidVolumes: number[] = [];
let askPrices: number[] = [];
let askVolumes: number[] = [];
if (stockData.bids && stockData.bids.length > 0) {
const extracted = extractOrderBook(stockData.bids);
bidPrices = extracted.prices;
bidVolumes = extracted.volumes;
} else if (rawData.bid_prices && Array.isArray(rawData.bid_prices)) {
// 替代格式bid_prices 和 bid_volumes 分离
bidPrices = rawData.bid_prices;
bidVolumes = rawData.bid_volumes || [];
}
if (stockData.asks && stockData.asks.length > 0) {
const extracted = extractOrderBook(stockData.asks);
askPrices = extracted.prices;
askVolumes = extracted.volumes;
} else if (rawData.ask_prices && Array.isArray(rawData.ask_prices)) {
// 替代格式ask_prices 和 ask_volumes 分离
askPrices = rawData.ask_prices;
askVolumes = rawData.ask_volumes || [];
}
updated[fullCode] = {
code: fullCode,
@@ -270,8 +310,43 @@ const handleSZSESnapshotMessage = (
if (subscribedCodes.has(rawCode) || subscribedCodes.has(fullCode)) {
hasUpdate = true;
const { prices: bidPrices, volumes: bidVolumes } = extractOrderBook(s.bids);
const { prices: askPrices, volumes: askVolumes } = extractOrderBook(s.asks);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawData = s as any; // 用于检查替代字段名
// 调试日志:检查快照消息中的盘口数据(无盘口时警告)
if (!s.bids || s.bids.length === 0) {
logger.warn('FlexScreen', `SZSE快照股票数据无盘口 ${fullCode}`, {
hasBids: !!s.bids,
hasAsks: !!s.asks,
hasBidPrices: !!rawData.bid_prices,
hasAskPrices: !!rawData.ask_prices,
dataKeys: Object.keys(s),
});
}
// 优先使用 bids/asks 对象数组格式,如果不存在则尝试 bid_prices/ask_prices 分离数组格式
let bidPrices: number[] = [];
let bidVolumes: number[] = [];
let askPrices: number[] = [];
let askVolumes: number[] = [];
if (s.bids && s.bids.length > 0) {
const extracted = extractOrderBook(s.bids);
bidPrices = extracted.prices;
bidVolumes = extracted.volumes;
} else if (rawData.bid_prices && Array.isArray(rawData.bid_prices)) {
bidPrices = rawData.bid_prices;
bidVolumes = rawData.bid_volumes || [];
}
if (s.asks && s.asks.length > 0) {
const extracted = extractOrderBook(s.asks);
askPrices = extracted.prices;
askVolumes = extracted.volumes;
} else if (rawData.ask_prices && Array.isArray(rawData.ask_prices)) {
askPrices = rawData.ask_prices;
askVolumes = rawData.ask_volumes || [];
}
updated[fullCode] = {
code: fullCode,

View File

@@ -75,20 +75,65 @@ export const normalizeCode = (code: string): string => {
return code.split('.')[0];
};
/**
* 盘口数据可能的格式(根据不同的 WebSocket 服务端实现)
*/
type OrderBookInput =
| OrderBookLevel[] // 格式1: [{price, volume}, ...]
| Array<[number, number]> // 格式2: [[price, volume], ...]
| { prices: number[]; volumes: number[] } // 格式3: {prices: [...], volumes: [...]}
| undefined;
/**
* 从深交所 bids/asks 数组提取价格和量数组
* @param orderBook - 盘口数组 [{price, volume}, ...]
* 支持多种可能的数据格式
* @param orderBook - 盘口数据,支持多种格式
* @returns { prices, volumes }
*/
export const extractOrderBook = (
orderBook: OrderBookLevel[] | undefined
orderBook: OrderBookInput
): { prices: number[]; volumes: number[] } => {
if (!orderBook || !Array.isArray(orderBook)) {
if (!orderBook) {
return { prices: [], volumes: [] };
}
const prices = orderBook.map(item => item.price || 0);
const volumes = orderBook.map(item => item.volume || 0);
return { prices, volumes };
// 格式3: 已经是 {prices, volumes} 结构
if (!Array.isArray(orderBook) && 'prices' in orderBook && 'volumes' in orderBook) {
return {
prices: orderBook.prices || [],
volumes: orderBook.volumes || [],
};
}
// 必须是数组才能继续
if (!Array.isArray(orderBook) || orderBook.length === 0) {
return { prices: [], volumes: [] };
}
const firstItem = orderBook[0];
// 格式2: [[price, volume], ...]
if (Array.isArray(firstItem)) {
const prices = orderBook.map((item: unknown) => {
const arr = item as [number, number];
return arr[0] || 0;
});
const volumes = orderBook.map((item: unknown) => {
const arr = item as [number, number];
return arr[1] || 0;
});
return { prices, volumes };
}
// 格式1: [{price, volume}, ...] (标准格式)
if (typeof firstItem === 'object' && firstItem !== null) {
const typedBook = orderBook as OrderBookLevel[];
const prices = typedBook.map(item => item.price || 0);
const volumes = typedBook.map(item => item.volume || 0);
return { prices, volumes };
}
return { prices: [], volumes: [] };
};
/**