Files
one_divine_lot/src/component/TradeSync.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

89 lines
3.2 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.
/**
* 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();
}
}