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:
2026-09-01 13:21:49 +08:00
parent 5a723d514e
commit bb3f8bd28d
57 changed files with 5244 additions and 397 deletions
+150
View File
@@ -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',
};
/** 价格 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);
}