109 lines
4.1 KiB
JavaScript
109 lines
4.1 KiB
JavaScript
/**
|
||
* 服务端 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' });
|
||
}
|
||
}
|