update pay ui
This commit is contained in:
98
app.py
98
app.py
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [] };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user