feat(交易记录): SQLite 本地存储 + 手动归属(迭代 07,R-009)

- trade_orders + trade_fills 两表(委托/成交,holding_id/strategy_id 手动归属)
- TradeSync 定时同步(启动预热 + 60s UPSERT,不覆盖归属列)
- 手动设置归属(candidates 候选 + set-attribution 端点 + 前端归属下拉)
- 历史查询 trades/history + 策略过滤
- QMT code 归一化(补后缀)+ tradeDate 兜底 insertDate
This commit is contained in:
2026-09-02 01:20:50 +08:00
parent dd98b4d45b
commit b3526c7693
9 changed files with 933 additions and 89 deletions
+2 -2
View File
@@ -59,8 +59,8 @@ const HANDLERS = [
* @param {import('@deepseek-ai/cordis').Context} ctx
* @param {object} runtime { manager, settings, dataSource, marketCache }
*/
export function registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor }) {
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor };
export function registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync }) {
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync };
ctx.effect(() => ctx.webServer.register({
kind: 'prefix',
+64 -3
View File
@@ -5,16 +5,24 @@
* orders → 当日委托(支持 code/status 过滤)
* trades → 当日成交
* trading-dates → 交易日历(日期导航)
* trades/history → 本地 SQLite 历史查询(R-009:时间段/code/策略/方向过滤)
* trades/by-holding → 按 holding_id 查委托汇总(R-010:策略持仓行展开)
*
* 数据流:前端轮询 → /odl/api/orders|trades → dataSource.getOrders/getTrades
* (服务端直连 QMT Bridge REST,技术约束-003
*/
import { getStrategies } from '../settings.js';
/** 交易记录端点方法表 */
export const TRADE_METHODS = new Set([
'orders',
'trades',
'trading-dates',
'trades/history', // R-009:本地 SQLite 历史查询(策略过滤)
'orders/attribution-candidates', // R-009:委托归属候选列表(该 code 当前持仓策略)
'orders/set-attribution', // R-009:手动设置委托归属
'trades/by-holding', // R-010:按 holding_id 查委托汇总(策略持仓行展开)
]);
/**
@@ -23,15 +31,31 @@ export const TRADE_METHODS = new Set([
* @param {object} args
* @param {object} runtime { dataSource }
*/
export async function handleTrade(method, args, { dataSource }) {
export async function handleTrade(method, args, { dataSource, storage, settings }) {
switch (method) {
case 'orders':
return await dataSource.getOrders({
case 'orders': {
const orders = await dataSource.getOrders({
code: args.code,
status: args.status,
start: args.start,
end: args.end,
});
// R-009:今日实时委托附加【持久化归属】(本地库中用户已设置的 strategy_id/holding_id
// 未设置为 null(前端显示「未关联」,用户可手动设置);候选由 orders/attribution-candidates 提供
if (storage && Array.isArray(orders) && orders.length > 0) {
const ids = orders.map((o) => o.orderId).filter(Boolean);
const ph = ids.map(() => '?').join(',');
const rows = storage.sqlite.db.prepare(
'SELECT order_id, strategy_id, holding_id FROM trade_orders WHERE order_id IN (' + ph + ')'
).all(...ids);
const attr = new Map(rows.map((r) => [r.order_id, { strategyId: r.strategy_id ?? null, holdingId: r.holding_id ?? null }]));
return orders.map((o) => {
const a = attr.get(o.orderId) ?? { strategyId: null, holdingId: null };
return { ...o, strategyId: a.strategyId, holdingId: a.holdingId };
});
}
return orders;
}
case 'trades':
return await dataSource.getTrades({
start: args.start,
@@ -42,6 +66,43 @@ export async function handleTrade(method, args, { dataSource }) {
start: args.start,
end: args.end,
});
case 'orders/attribution-candidates': {
// R-009:委托归属候选(该 code 当前持仓策略,Q3 全手动选)
if (!storage) throw Object.assign(new Error('本地存储不可用'), { code: 'storage-unavailable' });
const list = await storage.getTradeAttributionCandidates(args.code);
// 附策略名(settings 的策略 name,便于前端展示)
const nameMap = new Map((getStrategies(settings) || []).map((s) => [s.id, s.name]));
return list.map((c) => ({ ...c, strategyName: nameMap.get(c.strategyId) ?? c.strategyId }));
}
case 'orders/set-attribution': {
// R-009:手动设置委托归属(Q4 可随时改,以最终为准)
if (!storage) throw Object.assign(new Error('本地存储不可用'), { code: 'storage-unavailable' });
const ok = await storage.setTradeOrderAttribution(args.orderId, args.strategyId ?? null, args.holdingId ?? null);
return { ok };
}
case 'trades/by-holding': {
// R-010:按 holding_id 查委托汇总(策略持仓行展开)
if (!storage) throw Object.assign(new Error('本地存储不可用'), { code: 'storage-unavailable' });
return await storage.getTradeOrdersByHolding(args.holdingId);
}
case 'trades/history': {
// R-009:本地 SQLite 历史查询(时间段/code/策略/方向过滤)
const opts = {
start: args.start,
end: args.end,
code: args.code,
strategyId: args.strategyId,
direction: args.direction,
};
if (!storage) {
throw Object.assign(new Error('本地存储不可用'), { code: 'storage-unavailable' });
}
const [orders, fills] = await Promise.all([
storage.getTradeOrderHistory(opts),
storage.getTradeFillHistory(opts),
]);
return { orders, fills };
}
default:
throw Object.assign(new Error('unknown trade method: ' + method), { code: 'not-found' });
}