迭代13: 盘口内存快照(QuoteSync/QuoteHub)+ 同步指示灯 + 行情列
- QuoteSync/QuoteHub 替换 MarketFeed/MarketDataHub(方案A 删除重写): 5s REST 同步 watch 集合 + 涨停/跌停经 /data/instrument 按交易日内存缓存 + 读穿透走适配层 getTicks(修正旧 hub 绕适配层的层级破洞) - WS 数据通路移除(从无生效结论;ingest 留 source 标签回归口子) - market_quotes_cache 表退役 DROP(幂等);价格单一入口 = QuoteHub - 数据同步指示灯组: QMT连接|持仓数据|行情数据(绿/黄/灰,点击即同步/即时探测) + sync-status 端点(吸收 market-stats)+ sync-now;修复 runtime 漏传 positionSync 致持灯恒灰 - 策略持仓表行情列: 涨停价/跌停价/今开/最高(COLUMN_META defaultVisible=false,纯价格) - 回归 test-quote-sync.mjs 27 项(含 DROP 幂等真实 SQLite 验证) - 文档链: R-015 + PLAN-014 + 迭代三件套 + 技术约束-010/012变更、018新增 + 产品约束-012 需求: R-015(老师五拍板: 替换/WS移除/纯内存DROP/watch现状/指示灯)
This commit is contained in:
+68
-19
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* 服务端 API:行情域(2026-08-31 拆分;2026-09-01 精简)
|
||||
* 服务端 API:行情域(2026-08-31 拆分;2026-09-02 R-015/迭代 13 改造)
|
||||
*
|
||||
* 端点:
|
||||
* market-snapshot → 按 code 查询行情缓存(miss 自动补拉在 MarketDataHub 内部,此处只做编排)
|
||||
* qmt-health → QMT 连接健康检查(读服务端缓存;refresh=true 触发即时探测)
|
||||
*
|
||||
* 2026-09-01:QMT health 改为「服务端定时探测(5 分钟)+ 缓存」机制(QmtHealthMonitor),
|
||||
* 前端打开读缓存秒回,点击/悬停传 refresh=true 触发即时探测。
|
||||
* market-snapshot → 按 code 查盘口内存快照(含涨停/跌停;miss 读穿透)
|
||||
* qmt-health → QMT 连接健康检查(服务端缓存 + refresh 即时探测,2026-09-01)
|
||||
* sync-status → 数据同步指示灯状态(持仓/盘口/QMT 三域,吸收原临时诊断 market-stats)
|
||||
* sync-now → 手动触发某域立即同步(指示灯点击)
|
||||
*/
|
||||
|
||||
import { resolveActiveBaseUrl } from './common.js';
|
||||
@@ -15,21 +14,45 @@ import { resolveActiveBaseUrl } from './common.js';
|
||||
export const MARKET_METHODS = new Set([
|
||||
'market-snapshot',
|
||||
'qmt-health',
|
||||
'market-stats', // 临时诊断:WS/REST 通道统计
|
||||
'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, qmtHealthMonitor }
|
||||
* @param {object} runtime { settings, dataSource, marketHub, marketFeed(=QuoteSync), qmtHealthMonitor, positionSync }
|
||||
*/
|
||||
export async function handleMarket(method, args, { ctx, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor }) {
|
||||
export async function handleMarket(method, args, { settings, dataSource, marketHub, marketFeed: marketSync, qmtHealthMonitor, positionSync }) {
|
||||
switch (method) {
|
||||
case 'market-snapshot': {
|
||||
const codes = Array.isArray(args.codes) ? args.codes : [];
|
||||
if (!marketHub) return {};
|
||||
return await marketHub.getByCodes(codes, { settings, dataSource });
|
||||
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)
|
||||
@@ -37,20 +60,46 @@ export async function handleMarket(method, args, { ctx, settings, dataSource, ma
|
||||
return { healthy: null, baseUrl: resolveActiveBaseUrl(settings, dataSource), fromCache: false };
|
||||
}
|
||||
if (args.refresh) {
|
||||
// 前端点击/悬停触发即时探测(刷新缓存)
|
||||
return await qmtHealthMonitor.probe();
|
||||
}
|
||||
// 默认读缓存(秒回)
|
||||
return qmtHealthMonitor.getStatus();
|
||||
}
|
||||
case 'market-stats': {
|
||||
// 临时诊断:WS 推送 vs REST 拉取 计数(确认 WS 是否生效)
|
||||
return {
|
||||
stats: marketHub?.stats ?? null,
|
||||
cacheSize: marketHub?.size ?? 0,
|
||||
watchSize: marketHub?.watchCodes?.size ?? 0,
|
||||
serverTime: Date.now(),
|
||||
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,
|
||||
};
|
||||
}
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user