Files
one_divine_lot/src/api/trades.js
T
kyugao b3526c7693 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
2026-09-02 01:20:50 +08:00

109 lines
4.6 KiB
JavaScript
Raw 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:交易记录域(R-0072026-09-01
*
* 端点:
* 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 查委托汇总(策略持仓行展开)
]);
/**
* 处理交易记录端点
* @param {string} method
* @param {object} args
* @param {object} runtime { dataSource }
*/
export async function handleTrade(method, args, { dataSource, storage, settings }) {
switch (method) {
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,
end: args.end,
});
case 'trading-dates':
return await dataSource.getTradingDates({
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' });
}
}