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
+88
View File
@@ -0,0 +1,88 @@
/**
* TradeSync —— 交易记录本地同步(R-009 / 迭代 072026-09-01
*
* 职责:把 QMT Bridge 当日交易数据(委托 + 成交)定时同步落 SQLitetrade_orders + trade_fills)。
* - 启动预热:插件启动即同步一次(今日数据入库);
* - 定时同步:60s 轮询拉当日 orders+trades → UPSERT(幂等,状态覆盖更新);
* - 失败容忍:单次同步失败只记日志,不中断定时循环。
*
* 数据来源:QmtBridgeRestDataSource.getOrders() / getTrades()(仅当日,QMT 无历史接口)。
* 存储:DataStore.upsertTradeOrders / upsertTradeFills(委托 SqliteStoreUPSERT 幂等)。
*/
const SYNC_INTERVAL_MS = 60 * 1000; // 60s
export class TradeSync {
/**
* @param {object} opts
* @param {object} opts.runtime { dataSource, storage } —— dataSource 拉当日数据,storage 落库
* @param {object} [opts.logger]
*/
constructor({ runtime, logger } = {}) {
this.runtime = runtime;
this.logger = logger;
this.timer = null;
this.mounted = false;
this.syncing = false;
}
/** 启动:立即同步一次(预热)+ 定时同步 */
start() {
if (this.mounted) return;
this.mounted = true;
this.syncNow().catch((e) => {
this.logger?.warn?.('[one-divine-lot] TradeSync 启动预热失败: ' + (e?.message ?? e));
});
this.timer = setInterval(() => {
this.syncNow().catch((e) => {
this.logger?.debug?.('[one-divine-lot] TradeSync 定时同步失败: ' + (e?.message ?? e));
});
}, SYNC_INTERVAL_MS);
this.logger?.info?.('[one-divine-lot] TradeSync 启动(60s 定时同步交易记录)');
}
/** 停止(插件释放时) */
stop() {
this.mounted = false;
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
/**
* 同步一次:拉当日 orders+trades → UPSERT 落库(幂等)
* @returns {Promise<{orders: number, fills: number} | null>} 写入条数;数据源不可用返回 null
*/
async syncNow() {
if (!this.mounted || this.syncing) return null;
const { dataSource, storage } = this.runtime;
if (!dataSource || !storage) return null;
this.syncing = true;
try {
const [orders, fills] = await Promise.all([
dataSource.getOrders({}).catch((e) => {
this.logger?.debug?.('[one-divine-lot] TradeSync 拉委托失败: ' + (e?.message ?? e));
return null;
}),
dataSource.getTrades({}).catch((e) => {
this.logger?.debug?.('[one-divine-lot] TradeSync 拉成交失败: ' + (e?.message ?? e));
return null;
}),
]);
const nOrders = Array.isArray(orders) ? await storage.upsertTradeOrders(orders) : 0;
const nFills = Array.isArray(fills) ? await storage.upsertTradeFills(fills) : 0;
if (nOrders > 0 || nFills > 0) {
this.logger?.debug?.('[one-divine-lot] TradeSync 同步完成: 委托 ' + nOrders + ' / 成交 ' + nFills);
}
return { orders: nOrders, fills: nFills };
} finally {
this.syncing = false;
}
}
/** 手动触发同步(api 层可调用;返回最新同步结果) */
async sync() {
return this.syncNow();
}
}