Files
one_divine_lot/src/client/market/MarketDataProvider.jsx
T
kyugao bb3f8bd28d feat: 迭代03-05 交易记录接入、行情实时、QMT连接配置、data store 标准化
- R-004 QMT连接配置:多配置管理 + 会话头部快捷切换(03-QMT连接配置)
- R-005 WS盘中价格实时更新:服务端中转+缓存+前端轮询(04-WS盘中价格实时更新)
- R-006 策略数据 JSON data store 标准化(store.json/market.json/schema.json)
- R-007 交易记录接入:当日订单+成交单表合并(委托主行+展开成交明细)、时间段查询(今日/本周/本月+手动起止)、费用计算(佣金费率万m_dCommission+最低5元、印花税卖出万5、过户费双向万0.1)
- 架构治理:api 按领域拆分、component 目录、QmtHealthMonitor
2026-09-01 13:21:49 +08:00

151 lines
4.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MarketDataProvider —— 行情数据提供者(R-005 服务端中转架构,2026-08-31
*
* 架构(服务端中转,老师确认):
* - DSH 服务端做「一次 WS 订阅 + QMT 桥整合」,维护一份实时行情缓存(MarketDataHub + 持久化);
* - 前端**统一轮询** DSH 接口(/odl/api/market-snapshot,按 code 查询),不做前端直连;
* - 轮询间隔 5s(老师确认)。
*
* 关注列表(2026-08-31 去重优化):不再自拉 positions,
* 改由表格组件在渲染时通过 registerCodes(codes) 上报,Provider 只轮询行情。
*
* 2026-09-01 修复:registerCodes 上报新 code 后**立即触发一次行情拉取**(不等 5s 定时器),
* 解决「打开持仓 tab 价格要等很久才出现」的时序问题。
*/
import { createContext, useContext, useEffect, useReducer, useRef, useState, useCallback } from 'react';
import { useRpc } from '../views/connection.jsx';
const MarketContext = createContext(null);
/** 连接状态(轮询模式) */
export const MARKET_STATUS = {
IDLE: 'idle',
CONNECTING: 'connecting',
LIVE: 'live',
RECONNECTING: 'reconnecting',
DISCONNECTED: 'disconnected',
};
/** 价格 reducercode → snapshot */
function pricesReducer(state, action) {
if (action.type === 'update') {
const next = {};
for (const [code, snap] of Object.entries(action.data || {})) {
if (!snap) continue;
next[code] = { ...state[code], ...snap };
}
return { ...state, ...next };
}
return state;
}
const POLL_INTERVAL_MS = 5000; // 轮询间隔(老师确认 5s
export function MarketDataProvider({ children }) {
const call = useRpc();
/** 统一 setStatus:同步 ref(供轮询闭包读取最新值) */
const updateStatus = useCallback((s) => {
statusRef.current = s;
setStatus(s);
}, []);
const [prices, dispatch] = useReducer(pricesReducer, {});
const [status, setStatus] = useState(MARKET_STATUS.IDLE);
const statusRef = useRef(MARKET_STATUS.IDLE);
const mountedRef = useRef(true);
const watchCodesRef = useRef(new Set());
const pollMarketRef = useRef(null); // 供 registerCodes 触发立即拉取
/** 轮询行情缓存(按关注 code 查询,服务端中转) */
const pollMarket = useCallback(async () => {
const codes = [...watchCodesRef.current];
if (codes.length === 0) return;
try {
const res = await call('one-divine-lot/market-snapshot', {
args: { codes },
});
if (res?.ok && res.value) {
const n = Object.keys(res.value).length;
if (n > 0) {
dispatch({ type: 'update', data: res.value });
updateStatus(MARKET_STATUS.LIVE);
} else {
updateStatus(MARKET_STATUS.CONNECTING); // 缓存未填充
}
} else {
console.warn('[odl-mkt] market-snapshot 失败:', JSON.stringify(res).slice(0, 200));
updateStatus(MARKET_STATUS.RECONNECTING);
}
} catch (e) {
console.warn('[odl-mkt] market-snapshot 异常:', e?.message);
updateStatus(MARKET_STATUS.RECONNECTING);
}
}, [call]);
/** 表格上报关注代码;上报新 code 后立即拉一次行情(不等定时器) */
const registerCodes = useCallback((codes) => {
const list = Array.isArray(codes) ? codes : [];
const next = new Set(watchCodesRef.current);
let changed = false;
for (const c of list) {
if (c && !next.has(c)) { next.add(c); changed = true; }
}
if (changed) {
watchCodesRef.current = next;
// 立即触发一次行情拉取(首屏快速显示价格)
pollMarketRef.current?.();
}
}, []);
// 同步 pollMarket 到 ref(供 registerCodes 调用)
useEffect(() => {
pollMarketRef.current = pollMarket;
}, [pollMarket]);
/** 主循环:定时轮询行情(关注 code 由表格上报) */
useEffect(() => {
mountedRef.current = true;
let cancelled = false;
const poll = async () => {
if (cancelled || !mountedRef.current) return;
if (watchCodesRef.current.size === 0) {
// 关注代码尚未上报(表格未加载)——保持 IDLE
if (statusRef.current !== MARKET_STATUS.IDLE) updateStatus(MARKET_STATUS.IDLE);
return;
}
await pollMarket();
};
poll();
const timer = setInterval(poll, POLL_INTERVAL_MS);
return () => {
cancelled = true;
mountedRef.current = false;
clearInterval(timer);
};
}, [pollMarket]);
const value = {
prices,
status,
getPrice: useCallback((code) => prices[code] ?? null, [prices]),
registerCodes,
wsInfo: null,
};
return (
<MarketContext.Provider value={value}>
{children}
</MarketContext.Provider>
);
}
/** 消费价格 Map */
export function useMarket() {
return useContext(MarketContext);
}