Files

109 lines
4.1 KiB
JavaScript
Raw Permalink 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.
/**
* 服务端 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/迭代18QMT 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' });
}
}