/** * 服务端 API:行情域(2026-08-31 拆分;2026-09-02 R-015/迭代 13 改造) * * 端点: * market-snapshot → 按 code 查盘口内存快照(含涨停/跌停;miss 读穿透) * qmt-health → QMT 连接健康检查(服务端缓存 + refresh 即时探测,2026-09-01) * sync-status → 数据同步指示灯状态(持仓/盘口/QMT 三域,吸收原临时诊断 market-stats) * sync-now → 手动触发某域立即同步(指示灯点击) */ import { resolveActiveBaseUrl } from './common.js'; /** 行情端点方法表 */ export const MARKET_METHODS = new Set([ 'market-snapshot', 'qmt-health', 'sync-status', 'sync-now', ]); /** 盘口快照对外投影:UI 消费字段 + updatedAt(价龄);涨停/跌停来自 QuoteHub 合约缓存 */ function projectQuote(hub, code, snap) { const inst = hub.instruments.get(code) ?? {}; return { code, lastPrice: snap?.lastPrice ?? null, lastClose: snap?.lastClose ?? null, open: snap?.open ?? null, high: snap?.high ?? null, low: snap?.low ?? null, volume: snap?.volume ?? null, amount: snap?.amount ?? null, upStopPrice: inst.upStopPrice ?? null, downStopPrice: inst.downStopPrice ?? null, updatedAt: snap?.updatedAt ?? 0, }; } /** * 处理行情端点 * @param {string} method * @param {object} args * @param {object} runtime { settings, dataSource, marketHub, marketFeed(=QuoteSync), qmtHealthMonitor, positionSync } */ export async function handleMarket(method, args, { settings, dataSource, marketHub, marketFeed: marketSync, qmtHealthMonitor, positionSync, mcpManager }) { switch (method) { case 'market-snapshot': { const codes = Array.isArray(args.codes) ? args.codes : []; if (!marketHub) return {}; const quotes = await marketHub.getQuotes(codes, { settings, dataSource }); const out = {}; for (const [code, snap] of Object.entries(quotes)) { out[code] = projectQuote(marketHub, code, snap); } return out; } case 'qmt-health': { // QMT 连接健康检查(服务端缓存 + 即时探测,2026-09-01) if (!qmtHealthMonitor) { return { healthy: null, baseUrl: resolveActiveBaseUrl(settings, dataSource), fromCache: false }; } if (args.refresh) { return await qmtHealthMonitor.probe(); } return qmtHealthMonitor.getStatus(); } case 'sync-status': { // 数据同步指示灯(R-015 产品约束-012):绿=新鲜 / 黄=失败中陈旧 / 灰=从未;阈值 = 3×周期 const state = (status) => { if (!status || !status.syncedAt) return 'never'; return Date.now() - status.syncedAt <= status.periodMs * 3 ? 'fresh' : 'stale'; }; return { serverTime: Date.now(), position: positionSync ? { syncedAt: positionSync.getSyncedAt(), periodMs: 10000, state: state({ syncedAt: positionSync.getSyncedAt(), periodMs: 10000 }), snapshotSize: positionSync.getSnapshot().length, failCount: positionSync.stats.failCount, lastError: positionSync.stats.lastError, } : null, quote: marketSync ? { ...marketSync.getStatus(), state: state(marketSync.getStatus()) } : null, qmt: qmtHealthMonitor ? qmtHealthMonitor.getStatus() : null, mcp: mcpManager ? mcpManager.getStatus() : null, // R-020/迭代18:QMT MCP 状态(会话头部指示灯) }; } case 'sync-now': { // 指示灯点击:立即触发对应域同步(不等下个周期) const domain = args.domain; if (domain === 'position' && positionSync) { const r = await positionSync.syncNow(); return { ok: r !== null, domain, result: r }; } if (domain === 'quote' && marketSync) { const r = await marketSync.syncNow(); return { ok: r !== null, domain, result: r }; } throw Object.assign(new Error('unknown sync domain: ' + domain), { code: 'bad-request' }); } default: throw Object.assign(new Error('unknown market method: ' + method), { code: 'not-found' }); } }