迭代16: 交易关联驱动持仓份额动态调整(R-018)
This commit is contained in:
+2
-2
@@ -60,8 +60,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, storage, tradeSync, positionSync }) {
|
||||
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync };
|
||||
export function registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution }) {
|
||||
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution };
|
||||
|
||||
ctx.effect(() => ctx.webServer.register({
|
||||
kind: 'prefix',
|
||||
|
||||
+24
-3
@@ -2,24 +2,45 @@
|
||||
* 服务端 API:持仓域(从 api.js 拆分,2026-08-31)
|
||||
*
|
||||
* 端点:
|
||||
* positions → 全量持仓
|
||||
* positions → 全量持仓
|
||||
* positions/orphan-hints → R-018 漏关联软提示:账本活动行 ∩ QMT 快照缺失的 code(只读,不动账本)
|
||||
*/
|
||||
|
||||
/** 持仓端点方法表 */
|
||||
export const POSITION_METHODS = new Set([
|
||||
'positions',
|
||||
'positions/orphan-hints',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 处理持仓端点
|
||||
* @param {string} method
|
||||
* @param {object} args
|
||||
* @param {object} runtime { manager }
|
||||
* @param {object} runtime { manager, storage, positionSync }
|
||||
*/
|
||||
export async function handlePosition(method, args, { manager }) {
|
||||
export async function handlePosition(method, args, { manager, storage, positionSync }) {
|
||||
switch (method) {
|
||||
case 'positions':
|
||||
return await manager.getAllPositions();
|
||||
case 'positions/orphan-hints': {
|
||||
// R-018 T3:漏关联软提示。账本有活动行(closed/void 双空)但 QMT 快照无此 code → 提示去关联/移出。
|
||||
// 只读不写(同步机制与账本分界:软提示不自动改账本)。
|
||||
const snapshot = positionSync && Array.isArray(positionSync.getSnapshot())
|
||||
? positionSync.getSnapshot()
|
||||
: null;
|
||||
if (!storage) return [];
|
||||
const holdings = await storage.getCurrentHoldings(); // 全策略当前活动持仓
|
||||
if (!snapshot) return []; // 快照不可用(从未同步/未注入)→ 不提示(避免误报)
|
||||
const snapshotCodes = new Set(snapshot.map((p) => p.code).filter(Boolean));
|
||||
return holdings
|
||||
.filter((h) => !snapshotCodes.has(h.code))
|
||||
.map((h) => ({
|
||||
holdingId: h.holdingId,
|
||||
strategyId: h.strategyId,
|
||||
code: h.code,
|
||||
shares: h.shares,
|
||||
}));
|
||||
}
|
||||
default:
|
||||
throw Object.assign(new Error('unknown position method: ' + method), { code: 'not-found' });
|
||||
}
|
||||
|
||||
@@ -118,12 +118,15 @@ export async function handleStrategy(method, args, { manager, settings, storage
|
||||
}
|
||||
const rows = await storage.getHoldingsHistory(args.strategyId, { sinceMs });
|
||||
// name 兜底:该 holding 最近一笔关联委托的 name(前端免二次请求)
|
||||
const ids = rows.map((r) => r.holdingId);
|
||||
// R-018:真相源 = 段表 —— 历史行关联委托经段表 join(同一单多段也能兜到 name)
|
||||
const ids = rows.map((r) => r.holdingId).filter((x) => x != null);
|
||||
const nameMap = new Map();
|
||||
if (ids.length > 0) {
|
||||
const ph = ids.map(() => '?').join(',');
|
||||
const orows = storage.sqlite.db.prepare(
|
||||
"SELECT holding_id, name FROM trade_orders WHERE holding_id IN (" + ph + ") AND name != '' ORDER BY insert_ts DESC"
|
||||
'SELECT a.holding_id, t.name FROM trade_order_attributions a ' +
|
||||
'JOIN trade_orders t ON t.order_id = a.order_id ' +
|
||||
'WHERE a.holding_id IN (' + ph + ") AND t.name != '' ORDER BY t.insert_ts DESC"
|
||||
).all(...ids);
|
||||
for (const o of orows) if (!nameMap.has(o.holding_id)) nameMap.set(o.holding_id, o.name);
|
||||
}
|
||||
|
||||
+78
-34
@@ -1,12 +1,17 @@
|
||||
/**
|
||||
* 服务端 API:交易记录域(R-007,2026-09-01)
|
||||
* 服务端 API:交易记录域(R-007,2026-09-01;R-018 迭代 16 归属改造)
|
||||
*
|
||||
* 端点:
|
||||
* orders → 当日委托(支持 code/status 过滤)
|
||||
* trades → 当日成交
|
||||
* trading-dates → 交易日历(日期导航)
|
||||
* trades/history → 本地 SQLite 历史查询(R-009:时间段/code/策略/方向过滤)
|
||||
* trades/by-holding → 按 holding_id 查委托汇总(R-010:策略持仓行展开)
|
||||
* orders → 当日委托(code/status 过滤;R-018 附加 segments 归属段)
|
||||
* trades → 当日成交
|
||||
* trading-dates → 交易日历(日期导航)
|
||||
* trades/history → 本地 SQLite 历史查询(时间段/code/策略/方向过滤;策略过滤经段表)
|
||||
* trades/by-holding → 按 holding_id 查委托汇总(R-010,段表 join)
|
||||
* orders/attribution-targets → R-018 策略级归属目标(候选 = 全部策略 + 该 code 活动份额)
|
||||
* orders/attribution-set → R-018 全量替换归属段(segments 数组,原子)
|
||||
* orders/attribution-segments → R-018 读某 order 的归属段
|
||||
* (orders/attribution-candidates / orders/set-attribution 已退役:R-017 7 天候选退役,
|
||||
* 归属候选改策略级 targets;归属写改 attribution-set)
|
||||
*
|
||||
* 数据流:前端轮询 → /odl/api/orders|trades → dataSource.getOrders/getTrades
|
||||
* (服务端直连 QMT Bridge REST,技术约束-003)
|
||||
@@ -20,18 +25,19 @@ export const TRADE_METHODS = new Set([
|
||||
'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 查委托汇总(策略持仓行展开)
|
||||
'trades/by-holding', // R-010:按 holding_id 查委托汇总(策略持仓行展开)
|
||||
'orders/attribution-targets', // R-018:策略级归属候选(含 activityShares)
|
||||
'orders/attribution-set', // R-018:归属段全量替换(账本写操作)
|
||||
'orders/attribution-segments', // R-018:读某 order 归属段
|
||||
]);
|
||||
|
||||
/**
|
||||
* 处理交易记录端点
|
||||
* @param {string} method
|
||||
* @param {object} args
|
||||
* @param {object} runtime { dataSource }
|
||||
* @param {object} runtime { dataSource, storage, attribution, settings }
|
||||
*/
|
||||
export async function handleTrade(method, args, { dataSource, storage, settings }) {
|
||||
export async function handleTrade(method, args, { dataSource, storage, attribution, settings }) {
|
||||
switch (method) {
|
||||
case 'orders': {
|
||||
const orders = await dataSource.getOrders({
|
||||
@@ -40,18 +46,25 @@ export async function handleTrade(method, args, { dataSource, storage, settings
|
||||
start: args.start,
|
||||
end: args.end,
|
||||
});
|
||||
// R-009:今日实时委托附加【持久化归属】(本地库中用户已设置的 strategy_id/holding_id)
|
||||
// 未设置为 null(前端显示「未关联」,用户可手动设置);候选由 orders/attribution-candidates 提供
|
||||
// R-018:今日实时委托附加【持久化归属段】(段表真相源;order 带 segments 数组 + 兼容首段 strategyId/holdingId)
|
||||
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 }]));
|
||||
const idList = orders.map((o) => o.orderId).filter(Boolean);
|
||||
const segMap = storage.getOrderSegmentsByOrderIds(idList); // Map<orderId, segment[]>
|
||||
const strategyName = (sid) => {
|
||||
if (!sid) return '';
|
||||
const found = (getStrategies(settings) || []).find((s) => s.id === sid);
|
||||
return found ? found.name : sid;
|
||||
};
|
||||
return orders.map((o) => {
|
||||
const a = attr.get(o.orderId) ?? { strategyId: null, holdingId: null };
|
||||
return { ...o, strategyId: a.strategyId, holdingId: a.holdingId };
|
||||
const segs = segMap.get(o.orderId) ?? [];
|
||||
const withName = segs.map((s) => ({ ...s, strategyName: strategyName(s.strategyId) }));
|
||||
const first = withName[0];
|
||||
return {
|
||||
...o,
|
||||
segments: withName,
|
||||
strategyId: first ? first.strategyId : null, // 兼容首段(旧 UI 字段)
|
||||
holdingId: first ? first.holdingId : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
return orders;
|
||||
@@ -66,19 +79,37 @@ export async function handleTrade(method, args, { dataSource, storage, settings
|
||||
start: args.start,
|
||||
end: args.end,
|
||||
});
|
||||
case 'orders/attribution-candidates': {
|
||||
// R-009:委托归属候选(该 code 当前持仓策略,Q3 全手动选)
|
||||
case 'orders/attribution-targets': {
|
||||
// R-018(Q1):候选 = 策略级下拉。全部策略 + 该 code 每策略当前活动份额 activityShares(0=无仓)。
|
||||
// 不预筛(有无持仓/历史持仓由指向后服务端判定);holdingId 不再作下拉键。
|
||||
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 }));
|
||||
const code = args.code;
|
||||
const strategies = getStrategies(settings) || [];
|
||||
// 该 code 当前活动持仓(closed/void 双空)per strategy(getCurrentHoldings 无过滤=全策略)
|
||||
const allHoldings = await storage.getCurrentHoldings();
|
||||
const holdings = code ? allHoldings.filter((h) => h.code === code) : [];
|
||||
const shareByStrategy = new Map(holdings.map((h) => [h.strategyId, h.shares]));
|
||||
return strategies.map((s) => ({
|
||||
strategyId: s.id,
|
||||
strategyName: s.name,
|
||||
activityShares: Number(shareByStrategy.get(s.id) ?? 0),
|
||||
hasActiveHolding: (shareByStrategy.get(s.id) ?? 0) > 0,
|
||||
}));
|
||||
}
|
||||
case 'orders/set-attribution': {
|
||||
// R-009:手动设置委托归属(Q4 可随时改,以最终为准)
|
||||
case 'orders/attribution-set': {
|
||||
// R-018:全量替换归属段(account写操作)。segments: [{strategyId, volume}]
|
||||
if (!attribution || !storage) throw Object.assign(new Error('归属服务不可用'), { code: 'storage-unavailable' });
|
||||
const res = attribution.setSegments({
|
||||
orderId: args.orderId,
|
||||
segments: Array.isArray(args.segments) ? args.segments : [],
|
||||
});
|
||||
return { ok: true, segments: res.segments };
|
||||
}
|
||||
case 'orders/attribution-segments': {
|
||||
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 };
|
||||
const segs = storage.getOrderSegments(args.orderId);
|
||||
const nameMap = new Map((getStrategies(settings) || []).map((s) => [s.id, s.name]));
|
||||
return segs.map((s) => ({ ...s, strategyName: nameMap.get(s.strategyId) ?? s.strategyId }));
|
||||
}
|
||||
case 'trades/by-holding': {
|
||||
// R-010:按 holding_id 查委托汇总(策略持仓行展开)
|
||||
@@ -86,7 +117,7 @@ export async function handleTrade(method, args, { dataSource, storage, settings
|
||||
return await storage.getTradeOrdersByHolding(args.holdingId);
|
||||
}
|
||||
case 'trades/history': {
|
||||
// R-009:本地 SQLite 历史查询(时间段/code/策略/方向过滤)
|
||||
// R-009:本地 SQLite 历史查询(时间段/code/策略/方向过滤;R-018 orders 附加 segments)
|
||||
const opts = {
|
||||
start: args.start,
|
||||
end: args.end,
|
||||
@@ -101,9 +132,22 @@ export async function handleTrade(method, args, { dataSource, storage, settings
|
||||
storage.getTradeOrderHistory(opts),
|
||||
storage.getTradeFillHistory(opts),
|
||||
]);
|
||||
return { orders, fills };
|
||||
const idList = (orders ?? []).map((o) => o.orderId).filter(Boolean);
|
||||
const segMap = storage.getOrderSegmentsByOrderIds(idList);
|
||||
const strategyName = (sid) => {
|
||||
if (!sid) return '';
|
||||
const found = (getStrategies(settings) || []).find((s) => s.id === sid);
|
||||
return found ? found.name : sid;
|
||||
};
|
||||
const withSegs = (orders ?? []).map((o) => {
|
||||
const segs = segMap.get(o.orderId) ?? [];
|
||||
const withName = segs.map((s) => ({ ...s, strategyName: strategyName(s.strategyId) }));
|
||||
const first = withName[0];
|
||||
return { ...o, segments: withName, strategyId: first ? first.strategyId : null, holdingId: first ? first.holdingId : null };
|
||||
});
|
||||
return { orders: withSegs, fills };
|
||||
}
|
||||
default:
|
||||
throw Object.assign(new Error('unknown trade method: ' + method), { code: 'not-found' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user