51c70c48fb
- R-016 Q1-Q7: 两个策略 tab title 旁「历史持仓」开关 + 清仓范围筛选(近一周默认/1月/3月/半年/1年) - SqliteStore.getHoldingsHistory(strategy_id + closed_at 非空 + sinceMs,closed_at DESC,只读) - DataStore 门面透传 + strategy-holdings/history 端点(name 兜底 + 参数校验) - 前端: 开关/范围下拉/历史行灰显+「已清仓」徽标/展开复用 R-010(rowKey 唯一化) - R-016 二轮补充(Q8-Q9 老师拍板): 今日已清仓默认层 - range=today = 本地自然日 00:00 起(特判零点,不落回溯毫秒档) - 当天已清仓的持仓默认恒显示、不受「历史持仓」开关控制(开关只控今天之前) - rowsToRender 分层合成 + holdingId 去重;标题计数不含已清仓行;零写路径变更 - 回归: test-r016-history 24/24 + 存量回归全绿
102 lines
4.3 KiB
JavaScript
102 lines
4.3 KiB
JavaScript
/**
|
||
* 服务端 HTTP API 入口(2026-08-31 从 api.js 按领域拆分)
|
||
*
|
||
* 结构:
|
||
* src/api/
|
||
* ├── index.js # 路由入口:合并各领域 + 公共处理(405/404/500)
|
||
* ├── common.js # 公共工具(writeJson/readJsonBody/resolveActiveBaseUrl)
|
||
* ├── positions.js # 持仓域(positions)
|
||
* ├── strategies.js # 策略/份额/tabs 域
|
||
* ├── qmt-connections.js # QMT 连接配置域(含热切换编排)
|
||
* └── market.js # 行情域(market-snapshot)
|
||
*
|
||
* 端点(POST /odl/api/<method>,body = { args: {...} }):
|
||
* positions → 全量持仓
|
||
* strategies → 策略列表(含 visible)
|
||
* strategies/update → 更新策略列表(整表)
|
||
* strategies/add → 新增策略 { name }
|
||
* strategies/remove → 删除策略 { strategyId },联动清份额
|
||
* strategies/move → 排序 { strategyId, dir }
|
||
* strategy-positions → 某策略持仓 { strategyId }
|
||
* strategy-holdings/history → 某策略已清仓历史持仓 { strategyId, range }(R-016)
|
||
* unallocated → 未分配持仓
|
||
* summary → 分仓摘要
|
||
* add-shares → 添加份额 { code, strategyId, shares }
|
||
* move-all-shares → 全部移入 { code, strategyId }
|
||
* remove-shares → 移出份额 { code, strategyId, shares }
|
||
* tabs / tabs/update → 通用 tab 显隐配置
|
||
* qmt-connections → QMT 连接配置(R-004)
|
||
* qmt-connections/* → 增删改/激活/默认/测试/active-host/订阅代理
|
||
* market-snapshot → 按 code 查询行情缓存(R-005)
|
||
*/
|
||
|
||
import { API_PREFIX, writeJson, readJsonBody } from './common.js';
|
||
import { POSITION_METHODS, handlePosition } from './positions.js';
|
||
import { STRATEGY_METHODS, handleStrategy } from './strategies.js';
|
||
import { QMT_METHODS, handleQmt } from './qmt-connections.js';
|
||
import { MARKET_METHODS, handleMarket } from './market.js';
|
||
import { TRADE_METHODS, handleTrade } from './trades.js';
|
||
|
||
/** 全部端点方法表(合并各领域) */
|
||
const METHODS = new Set([
|
||
...POSITION_METHODS,
|
||
...STRATEGY_METHODS,
|
||
...QMT_METHODS,
|
||
...MARKET_METHODS,
|
||
...TRADE_METHODS,
|
||
]);
|
||
|
||
/** 领域分发:method → handler */
|
||
const HANDLERS = [
|
||
{ set: POSITION_METHODS, handle: handlePosition },
|
||
{ set: STRATEGY_METHODS, handle: handleStrategy },
|
||
{ set: QMT_METHODS, handle: handleQmt },
|
||
{ set: MARKET_METHODS, handle: handleMarket },
|
||
{ set: TRADE_METHODS, handle: handleTrade },
|
||
];
|
||
|
||
/**
|
||
* 注册 HTTP API
|
||
* @param {import('@deepseek-ai/cordis').Context} ctx
|
||
* @param {object} runtime { manager, settings, dataSource, marketCache }
|
||
*/
|
||
export function registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync }) {
|
||
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync };
|
||
|
||
ctx.effect(() => ctx.webServer.register({
|
||
kind: 'prefix',
|
||
path: '/odl/api',
|
||
handler: async (req, res) => {
|
||
if (req.method !== 'POST') {
|
||
writeJson(res, 405, { ok: false, error: { code: 'method-error', message: 'method not allowed' } });
|
||
return;
|
||
}
|
||
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname;
|
||
const method = pathname.startsWith(API_PREFIX) ? pathname.slice(API_PREFIX.length) : void 0;
|
||
if (!method || !METHODS.has(method)) {
|
||
writeJson(res, 404, { ok: false, error: { code: 'not-found', message: 'unknown method: ' + method } });
|
||
return;
|
||
}
|
||
try {
|
||
const body = await readJsonBody(req);
|
||
const args = body?.args ?? {};
|
||
// 按领域分发
|
||
let value;
|
||
for (const { set, handle } of HANDLERS) {
|
||
if (set.has(method)) {
|
||
value = await handle(method, args, runtime);
|
||
break;
|
||
}
|
||
}
|
||
writeJson(res, 200, { ok: true, value });
|
||
} catch (error) {
|
||
writeJson(res, 500, {
|
||
ok: false,
|
||
error: { code: error?.code ?? 'internal', message: error?.message ?? String(error), details: {} },
|
||
});
|
||
}
|
||
},
|
||
}), 'one-divine-lot.api');
|
||
|
||
ctx.logger?.info('[one-divine-lot] HTTP API 已注册 (/odl/api/*)');
|
||
} |