update pay ui
This commit is contained in:
@@ -79,14 +79,100 @@ const handleSSEMessage = (
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理深交所实时消息 (realtime)
|
||||
* 从深交所数据中提取盘口价格和量
|
||||
* 新 API 格式:直接使用 bid_prices/bid_volumes/ask_prices/ask_volumes 数组
|
||||
* 旧 API 格式:使用 bids/asks 对象数组
|
||||
*/
|
||||
const extractSZSEOrderBook = (
|
||||
stockData: SZSEStockData
|
||||
): { bidPrices: number[]; bidVolumes: number[]; askPrices: number[]; askVolumes: number[] } => {
|
||||
// 新 API 格式:直接是数组
|
||||
if (stockData.bid_prices && Array.isArray(stockData.bid_prices)) {
|
||||
return {
|
||||
bidPrices: stockData.bid_prices,
|
||||
bidVolumes: stockData.bid_volumes || [],
|
||||
askPrices: stockData.ask_prices || [],
|
||||
askVolumes: stockData.ask_volumes || [],
|
||||
};
|
||||
}
|
||||
// 旧 API 格式:对象数组
|
||||
const { prices: bidPrices, volumes: bidVolumes } = extractOrderBook(stockData.bids);
|
||||
const { prices: askPrices, volumes: askVolumes } = extractOrderBook(stockData.asks);
|
||||
return { bidPrices, bidVolumes, askPrices, askVolumes };
|
||||
};
|
||||
|
||||
/**
|
||||
* 从深交所数据中提取价格字段(兼容新旧 API)
|
||||
*/
|
||||
const extractSZSEPrices = (stockData: SZSEStockData) => {
|
||||
return {
|
||||
prevClose: stockData.prev_close_px ?? stockData.prev_close ?? 0,
|
||||
volume: stockData.total_volume_trade ?? stockData.volume ?? 0,
|
||||
amount: stockData.total_value_trade ?? stockData.amount ?? 0,
|
||||
upperLimit: stockData.upper_limit_px ?? stockData.upper_limit,
|
||||
lowerLimit: stockData.lower_limit_px ?? stockData.lower_limit,
|
||||
tradingPhase: stockData.trading_phase_code ?? stockData.trading_phase,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理深交所股票行情消息(新 API 格式:type='stock')
|
||||
*/
|
||||
const handleSZSEStockMessage = (
|
||||
data: SZSEStockData,
|
||||
timestamp: string,
|
||||
subscribedCodes: Set<string>,
|
||||
prevQuotes: QuotesMap
|
||||
): QuotesMap | null => {
|
||||
const rawCode = data.security_id;
|
||||
const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`;
|
||||
|
||||
if (!subscribedCodes.has(rawCode) && !subscribedCodes.has(fullCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(data);
|
||||
const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(data);
|
||||
|
||||
const updated: QuotesMap = { ...prevQuotes };
|
||||
updated[fullCode] = {
|
||||
code: fullCode,
|
||||
name: prevQuotes[fullCode]?.name || '',
|
||||
price: data.last_px,
|
||||
prevClose,
|
||||
open: data.open_px,
|
||||
high: data.high_px,
|
||||
low: data.low_px,
|
||||
volume,
|
||||
amount,
|
||||
numTrades: data.num_trades,
|
||||
upperLimit,
|
||||
lowerLimit,
|
||||
change: data.last_px - prevClose,
|
||||
changePct: calcChangePct(data.last_px, prevClose),
|
||||
bidPrices,
|
||||
bidVolumes,
|
||||
askPrices,
|
||||
askVolumes,
|
||||
tradingPhase,
|
||||
updateTime: data.update_time || timestamp,
|
||||
exchange: 'SZSE',
|
||||
} as QuoteData;
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理深交所实时消息 (兼容新旧 API)
|
||||
* 新 API: type='stock'/'bond'/'fund', data 直接是行情对象
|
||||
* 旧 API: type='realtime', category='stock'/'bond'/etc
|
||||
*/
|
||||
const handleSZSERealtimeMessage = (
|
||||
msg: SZSERealtimeMessage,
|
||||
subscribedCodes: Set<string>,
|
||||
prevQuotes: QuotesMap
|
||||
): QuotesMap | null => {
|
||||
const { category, data, timestamp } = msg;
|
||||
const { type, category, data, timestamp } = msg;
|
||||
const rawCode = data.security_id;
|
||||
const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`;
|
||||
|
||||
@@ -96,33 +182,36 @@ const handleSZSERealtimeMessage = (
|
||||
|
||||
const updated: QuotesMap = { ...prevQuotes };
|
||||
|
||||
switch (category) {
|
||||
// 确定实际的类别:新 API 直接用 type,旧 API 用 category
|
||||
const actualCategory = (type === 'realtime' ? category : type) as string;
|
||||
|
||||
switch (actualCategory) {
|
||||
case 'stock': {
|
||||
const stockData = data as SZSEStockData;
|
||||
const { prices: bidPrices, volumes: bidVolumes } = extractOrderBook(stockData.bids);
|
||||
const { prices: askPrices, volumes: askVolumes } = extractOrderBook(stockData.asks);
|
||||
const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(stockData);
|
||||
const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(stockData);
|
||||
|
||||
updated[fullCode] = {
|
||||
code: fullCode,
|
||||
name: prevQuotes[fullCode]?.name || '',
|
||||
price: stockData.last_px,
|
||||
prevClose: stockData.prev_close,
|
||||
prevClose,
|
||||
open: stockData.open_px,
|
||||
high: stockData.high_px,
|
||||
low: stockData.low_px,
|
||||
volume: stockData.volume,
|
||||
amount: stockData.amount,
|
||||
volume,
|
||||
amount,
|
||||
numTrades: stockData.num_trades,
|
||||
upperLimit: stockData.upper_limit,
|
||||
lowerLimit: stockData.lower_limit,
|
||||
change: stockData.last_px - stockData.prev_close,
|
||||
changePct: calcChangePct(stockData.last_px, stockData.prev_close),
|
||||
upperLimit,
|
||||
lowerLimit,
|
||||
change: stockData.last_px - prevClose,
|
||||
changePct: calcChangePct(stockData.last_px, prevClose),
|
||||
bidPrices,
|
||||
bidVolumes,
|
||||
askPrices,
|
||||
askVolumes,
|
||||
tradingPhase: stockData.trading_phase,
|
||||
updateTime: timestamp,
|
||||
tradingPhase,
|
||||
updateTime: stockData.update_time || timestamp,
|
||||
exchange: 'SZSE',
|
||||
} as QuoteData;
|
||||
break;
|
||||
@@ -155,13 +244,15 @@ const handleSZSERealtimeMessage = (
|
||||
break;
|
||||
}
|
||||
|
||||
case 'bond': {
|
||||
case 'bond':
|
||||
case 'fund': {
|
||||
const bondData = data as SZSEBondData;
|
||||
const prevClose = (bondData as SZSEStockData).prev_close_px ?? bondData.prev_close ?? 0;
|
||||
updated[fullCode] = {
|
||||
code: fullCode,
|
||||
name: prevQuotes[fullCode]?.name || '',
|
||||
price: bondData.last_px,
|
||||
prevClose: bondData.prev_close,
|
||||
prevClose,
|
||||
open: bondData.open_px,
|
||||
high: bondData.high_px,
|
||||
low: bondData.low_px,
|
||||
@@ -169,8 +260,8 @@ const handleSZSERealtimeMessage = (
|
||||
amount: bondData.amount,
|
||||
numTrades: bondData.num_trades,
|
||||
weightedAvgPx: bondData.weighted_avg_px,
|
||||
change: bondData.last_px - bondData.prev_close,
|
||||
changePct: calcChangePct(bondData.last_px, bondData.prev_close),
|
||||
change: bondData.last_px - prevClose,
|
||||
changePct: calcChangePct(bondData.last_px, prevClose),
|
||||
bidPrices: [],
|
||||
bidVolumes: [],
|
||||
askPrices: [],
|
||||
@@ -280,45 +371,90 @@ const handleSZSERealtimeMessage = (
|
||||
|
||||
/**
|
||||
* 处理深交所快照消息 (snapshot)
|
||||
* 订阅后首次返回的批量数据
|
||||
* 新 API: data 是单个股票对象 { security_id, last_px, ... }
|
||||
* 旧 API: data 是批量数据 { stocks: [...], indexes: [...], bonds: [...] }
|
||||
*/
|
||||
const handleSZSESnapshotMessage = (
|
||||
msg: SZSESnapshotMessage,
|
||||
subscribedCodes: Set<string>,
|
||||
prevQuotes: QuotesMap
|
||||
): QuotesMap | null => {
|
||||
const { stocks = [], indexes = [], bonds = [] } = msg.data || {};
|
||||
const updated: QuotesMap = { ...prevQuotes };
|
||||
let hasUpdate = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const data = msg.data as any;
|
||||
|
||||
// 新 API 格式:data 直接是单个股票对象
|
||||
if (data && data.security_id) {
|
||||
const stockData = data as SZSEStockData;
|
||||
const rawCode = stockData.security_id;
|
||||
const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`;
|
||||
|
||||
if (subscribedCodes.has(rawCode) || subscribedCodes.has(fullCode)) {
|
||||
const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(stockData);
|
||||
const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(stockData);
|
||||
|
||||
updated[fullCode] = {
|
||||
code: fullCode,
|
||||
name: '',
|
||||
price: stockData.last_px,
|
||||
prevClose,
|
||||
open: stockData.open_px,
|
||||
high: stockData.high_px,
|
||||
low: stockData.low_px,
|
||||
volume,
|
||||
amount,
|
||||
numTrades: stockData.num_trades,
|
||||
upperLimit,
|
||||
lowerLimit,
|
||||
change: stockData.last_px - prevClose,
|
||||
changePct: calcChangePct(stockData.last_px, prevClose),
|
||||
bidPrices,
|
||||
bidVolumes,
|
||||
askPrices,
|
||||
askVolumes,
|
||||
tradingPhase,
|
||||
updateTime: stockData.update_time || msg.timestamp,
|
||||
exchange: 'SZSE',
|
||||
} as QuoteData;
|
||||
return updated;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 旧 API 格式:批量数据
|
||||
const { stocks = [], indexes = [], bonds = [] } = data || {};
|
||||
|
||||
stocks.forEach((s: SZSEStockData) => {
|
||||
const rawCode = s.security_id;
|
||||
const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`;
|
||||
|
||||
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);
|
||||
const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(s);
|
||||
const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(s);
|
||||
|
||||
updated[fullCode] = {
|
||||
code: fullCode,
|
||||
name: '',
|
||||
price: s.last_px,
|
||||
prevClose: s.prev_close,
|
||||
prevClose,
|
||||
open: s.open_px,
|
||||
high: s.high_px,
|
||||
low: s.low_px,
|
||||
volume: s.volume,
|
||||
amount: s.amount,
|
||||
volume,
|
||||
amount,
|
||||
numTrades: s.num_trades,
|
||||
upperLimit: s.upper_limit,
|
||||
lowerLimit: s.lower_limit,
|
||||
change: s.last_px - s.prev_close,
|
||||
changePct: calcChangePct(s.last_px, s.prev_close),
|
||||
upperLimit,
|
||||
lowerLimit,
|
||||
change: s.last_px - prevClose,
|
||||
changePct: calcChangePct(s.last_px, prevClose),
|
||||
bidPrices,
|
||||
bidVolumes,
|
||||
askPrices,
|
||||
askVolumes,
|
||||
tradingPhase,
|
||||
exchange: 'SZSE',
|
||||
} as QuoteData;
|
||||
}
|
||||
@@ -358,18 +494,19 @@ const handleSZSESnapshotMessage = (
|
||||
|
||||
if (subscribedCodes.has(rawCode) || subscribedCodes.has(fullCode)) {
|
||||
hasUpdate = true;
|
||||
const prevClose = (b as SZSEStockData).prev_close_px ?? b.prev_close ?? 0;
|
||||
updated[fullCode] = {
|
||||
code: fullCode,
|
||||
name: '',
|
||||
price: b.last_px,
|
||||
prevClose: b.prev_close,
|
||||
prevClose,
|
||||
open: b.open_px,
|
||||
high: b.high_px,
|
||||
low: b.low_px,
|
||||
volume: b.volume,
|
||||
amount: b.amount,
|
||||
change: b.last_px - b.prev_close,
|
||||
changePct: calcChangePct(b.last_px, b.prev_close),
|
||||
change: b.last_px - prevClose,
|
||||
changePct: calcChangePct(b.last_px, prevClose),
|
||||
bidPrices: [],
|
||||
bidVolumes: [],
|
||||
askPrices: [],
|
||||
@@ -419,27 +556,25 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
heartbeatRefs.current[exchange] = setInterval(() => {
|
||||
const ws = wsRefs.current[exchange];
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
// 上交所使用 action: 'ping',深交所使用 type: 'ping'
|
||||
const pingMsg = exchange === 'SSE'
|
||||
? { action: 'ping' }
|
||||
: { type: 'ping' };
|
||||
ws.send(JSON.stringify(pingMsg));
|
||||
// 上交所和深交所(新 API)都使用 action: 'ping'
|
||||
ws.send(JSON.stringify({ action: 'ping' }));
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}, [stopHeartbeat]);
|
||||
|
||||
/**
|
||||
* 发送深交所订阅请求
|
||||
* 格式:{ type: 'subscribe', securities: ['000001', '000002'] }
|
||||
* 发送深交所订阅请求(新 API 格式)
|
||||
* 格式:{ action: 'subscribe', channels: ['stock'], codes: ['000001', '000002'] }
|
||||
*/
|
||||
const sendSZSESubscribe = useCallback((baseCodes: string[]) => {
|
||||
const ws = wsRefs.current.SZSE;
|
||||
if (ws && ws.readyState === WebSocket.OPEN && baseCodes.length > 0) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
securities: baseCodes,
|
||||
action: 'subscribe',
|
||||
channels: ['stock'], // 订阅股票频道
|
||||
codes: baseCodes,
|
||||
}));
|
||||
logger.info('FlexScreen', `SZSE 发送订阅请求`, { securities: baseCodes });
|
||||
logger.info('FlexScreen', `SZSE 发送订阅请求`, { codes: baseCodes });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -473,7 +608,7 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
setQuotes(prev => ({ ...prev, ...result }));
|
||||
}
|
||||
} else {
|
||||
// 深交所消息处理
|
||||
// 深交所消息处理(支持新旧两种 API 格式)
|
||||
switch (msg.type) {
|
||||
case 'welcome':
|
||||
// 收到欢迎消息,深交所 WebSocket 就绪
|
||||
@@ -494,8 +629,10 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
break;
|
||||
|
||||
case 'subscribed':
|
||||
// 订阅成功确认
|
||||
// 订阅成功确认(兼容新旧格式)
|
||||
logger.info('FlexScreen', 'SZSE 订阅成功', {
|
||||
channels: anyMsg.channels,
|
||||
codes: anyMsg.codes,
|
||||
securities: anyMsg.securities,
|
||||
categories: anyMsg.categories,
|
||||
});
|
||||
@@ -519,7 +656,21 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
break;
|
||||
|
||||
case 'realtime':
|
||||
// 实时行情推送
|
||||
// 旧 API:实时行情推送 (type='realtime' + category)
|
||||
setQuotes(prev => {
|
||||
const result = handleSZSERealtimeMessage(
|
||||
msg as SZSERealtimeMessage,
|
||||
subscribedCodes.current.SZSE,
|
||||
prev
|
||||
);
|
||||
return result || prev;
|
||||
});
|
||||
break;
|
||||
|
||||
// 新 API:直接使用 type='stock'/'bond'/'fund' 作为消息类型
|
||||
case 'stock':
|
||||
case 'bond':
|
||||
case 'fund':
|
||||
setQuotes(prev => {
|
||||
const result = handleSZSERealtimeMessage(
|
||||
msg as SZSERealtimeMessage,
|
||||
@@ -700,12 +851,12 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
|
||||
subscribedCodes.current[exchange].delete(fullCode);
|
||||
|
||||
// 发送取消订阅请求(深交所)
|
||||
// 发送取消订阅请求(深交所新 API 格式)
|
||||
const ws = wsRefs.current[exchange];
|
||||
if (exchange === 'SZSE' && ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'unsubscribe',
|
||||
securities: [baseCode],
|
||||
action: 'unsubscribe',
|
||||
codes: [baseCode],
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -139,36 +139,44 @@ export interface SSEMessage {
|
||||
}
|
||||
|
||||
// ==================== 深交所 WebSocket 消息类型 ====================
|
||||
// API 文档: SZSE_WEBSOCKET_API.md
|
||||
// 与上交所 API 保持一致的设计
|
||||
|
||||
/** 深交所数据类别 */
|
||||
export type SZSECategory =
|
||||
| 'stock' // 300111 股票快照
|
||||
| 'bond' // 300211 债券快照
|
||||
| 'afterhours_block' // 300611 盘后定价大宗交易
|
||||
| 'afterhours_trading' // 303711 盘后定价交易
|
||||
| 'hk_stock' // 306311 港股快照
|
||||
| 'index' // 309011 指数快照
|
||||
| 'volume_stats' // 309111 成交量统计
|
||||
| 'fund_nav'; // 309211 基金净值
|
||||
/** 深交所数据类别(对应 channels) */
|
||||
export type SZSECategory = 'stock' | 'bond' | 'fund';
|
||||
|
||||
/** 深交所股票行情数据 */
|
||||
/** 深交所股票行情数据(新 API 格式) */
|
||||
export interface SZSEStockData {
|
||||
security_id: string;
|
||||
md_stream_id?: string; // MDStreamID: 010
|
||||
orig_time?: number;
|
||||
channel_no?: number;
|
||||
trading_phase?: string;
|
||||
last_px: number;
|
||||
trading_phase_code?: string; // 新字段名
|
||||
trading_phase?: string; // 兼容旧字段名
|
||||
prev_close_px: number; // 新字段名
|
||||
prev_close?: number; // 兼容旧字段名
|
||||
open_px: number;
|
||||
high_px: number;
|
||||
low_px: number;
|
||||
prev_close: number;
|
||||
volume: number;
|
||||
amount: number;
|
||||
last_px: number;
|
||||
upper_limit_px?: number; // 新字段名
|
||||
upper_limit?: number; // 兼容旧字段名
|
||||
lower_limit_px?: number; // 新字段名
|
||||
lower_limit?: number; // 兼容旧字段名
|
||||
num_trades?: number;
|
||||
upper_limit?: number;
|
||||
lower_limit?: number;
|
||||
total_volume_trade?: number; // 新字段名 (成交量)
|
||||
total_value_trade?: number; // 新字段名 (成交额)
|
||||
volume?: number; // 兼容旧字段名
|
||||
amount?: number; // 兼容旧字段名
|
||||
// 新 API 格式:直接是数组
|
||||
bid_prices?: number[];
|
||||
bid_volumes?: number[];
|
||||
ask_prices?: number[];
|
||||
ask_volumes?: number[];
|
||||
// 兼容旧格式
|
||||
bids?: OrderBookLevel[];
|
||||
asks?: OrderBookLevel[];
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
/** 深交所指数行情数据 */
|
||||
@@ -245,10 +253,10 @@ export interface SZSEAfterhoursData {
|
||||
num_trades?: number;
|
||||
}
|
||||
|
||||
/** 深交所实时消息 */
|
||||
/** 深交所实时消息(新 API 格式:type 直接是 'stock' | 'bond' | 'fund') */
|
||||
export interface SZSERealtimeMessage {
|
||||
type: 'realtime';
|
||||
category: SZSECategory;
|
||||
type: 'stock' | 'bond' | 'fund' | 'realtime'; // 新 API 直接用 type='stock' 等
|
||||
category?: SZSECategory; // 旧 API 使用 category
|
||||
msg_type?: number;
|
||||
timestamp: string;
|
||||
data: SZSEStockData | SZSEIndexData | SZSEBondData | SZSEHKStockData | SZSEAfterhoursData;
|
||||
@@ -257,8 +265,9 @@ export interface SZSERealtimeMessage {
|
||||
/** 深交所快照消息 */
|
||||
export interface SZSESnapshotMessage {
|
||||
type: 'snapshot';
|
||||
timestamp: string;
|
||||
data: {
|
||||
timestamp?: string;
|
||||
data: SZSEStockData | {
|
||||
// 兼容旧格式的批量快照
|
||||
stocks?: SZSEStockData[];
|
||||
indexes?: SZSEIndexData[];
|
||||
bonds?: SZSEBondData[];
|
||||
@@ -277,8 +286,10 @@ export interface SZSEWelcomeMessage {
|
||||
/** 深交所订阅确认消息 */
|
||||
export interface SZSESubscribedMessage {
|
||||
type: 'subscribed';
|
||||
securities?: string[];
|
||||
categories?: string[];
|
||||
channels?: string[]; // 新 API 格式
|
||||
codes?: string[]; // 新 API 格式
|
||||
securities?: string[]; // 兼容旧格式
|
||||
categories?: string[]; // 兼容旧格式
|
||||
all?: boolean;
|
||||
incremental_only?: boolean;
|
||||
message?: string;
|
||||
@@ -287,8 +298,10 @@ export interface SZSESubscribedMessage {
|
||||
/** 深交所取消订阅确认消息 */
|
||||
export interface SZSEUnsubscribedMessage {
|
||||
type: 'unsubscribed';
|
||||
securities?: string[];
|
||||
categories?: string[];
|
||||
channels?: string[]; // 新 API 格式
|
||||
codes?: string[]; // 新 API 格式
|
||||
securities?: string[]; // 兼容旧格式
|
||||
categories?: string[]; // 兼容旧格式
|
||||
remaining_securities?: string[];
|
||||
remaining_categories?: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user