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
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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',
|
||||
};
|
||||
|
||||
/** 价格 reducer:code → 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);
|
||||
}
|
||||
Reference in New Issue
Block a user