/** * QMT Bridge REST 数据源适配器 * * 设计约束: * - 技术约束-001:统一数据源抽象(业务层不感知具体数据源) * - 技术约束-003:直连 QMT Bridge RESTful 接口(不经过 MCP) * * 服务地址:http://192.168.3.43:8610(可通过 config 覆盖) */ const DEFAULT_BASE_URL = 'http://192.168.3.43:8610'; const REQUEST_TIMEOUT_MS = 15000; export class QmtBridgeRestDataSource { constructor({ baseUrl = DEFAULT_BASE_URL, timeoutMs = REQUEST_TIMEOUT_MS } = {}) { this.name = 'qmt-bridge-rest'; this.baseUrl = baseUrl.replace(/\/$/, ''); this.timeoutMs = timeoutMs; } /** * 运行期更新基础地址(R-004 激活热切换,技术约束-008)。 * baseUrl 为按请求读取的实例字段,改字段即对后续请求生效,无需重建实例或重启。 */ setBaseUrl(url) { this.baseUrl = String(url ?? '').trim().replace(/\/+$/, ''); } /** 基础 HTTP 请求(fetch + 超时) */ async request(path, { method = 'GET', body } = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); if (!res.ok) { throw new Error(`QMT Bridge REST ${method} ${path} 失败: HTTP ${res.status}`); } return await res.json(); } catch (err) { if (err.name === 'AbortError') { throw new Error(`QMT Bridge REST 请求超时: ${path} (${this.timeoutMs}ms)`); } throw err; } finally { clearTimeout(timer); } } /** 可用性检查 */ async isAvailable() { try { const data = await this.request('/health'); return data?.status === 'ok'; } catch { return false; } } /** 资金摘要(/trade/asset) */ async getAsset() { const res = await this.request('/trade/asset'); const a = Array.isArray(res?.data) ? res.data[0] : res?.data; if (!a) throw new Error('QMT 资金数据为空'); return { accountId: a.m_strAccountID ?? '', status: a.m_strStatus ?? '', tradingDate: a.m_strTradingDate ?? '', totalAsset: a.m_dAssetBalance ?? 0, available: a.m_dAvailable ?? 0, stockValue: a.m_dStockValue ?? 0, balance: a.m_dBalance ?? 0, positionProfit: a.m_dPositionProfit ?? 0, }; } /** 全量持仓(/trade/positions) */ async getPositions() { const res = await this.request('/trade/positions'); const data = res?.data; const list = Array.isArray(data) ? data : data?.positions; if (!Array.isArray(list)) return []; return list.map((p) => this.mapPosition(p)); } /** * 合约基本信息(GET /data/instrument,技术约束-001 统一数据源抽象) * 语义化字段优先,回退 QMT 原始字段(实际返回比 OpenAPI 规范更全,含股本/状态等) * @param {string} code 完整证券代码(含后缀,如 600519.SH) * @returns {Promise} */ async getInstrument(code) { if (!code) throw new Error('QMT instrument 查询缺少 code'); const res = await this.request('/data/instrument?code=' + encodeURIComponent(code)); const d = res?.data; if (!d || !d.InstrumentID) throw new Error('QMT 合约信息为空: ' + code); return { code, instrumentId: d.InstrumentID ?? '', name: d.InstrumentName ?? '', exchange: d.ExchangeID ?? '', openDate: d.OpenDate ?? 0, preClose: +(d.PreClose ?? d.preClose ?? 0), upStopPrice: +(d.UpStopPrice ?? d.upStopPrice ?? 0), downStopPrice: +(d.DownStopPrice ?? d.downStopPrice ?? 0), floatVolume: +(d.FloatVolume ?? d.FloatVolumn ?? 0), totalVolume: +(d.TotalVolume ?? d.TotalVolumn ?? 0), priceTick: +(d.PriceTick ?? 0), volumeMultiple: +(d.VolumeMultiple ?? 1), instrumentStatus: +(d.InstrumentStatus ?? 0), tradingDay: +(d.TradingDay ?? 0), }; } /** * 当日委托(GET /trade/orders,R-007) * 过滤:code(600519 或 600519.SH)、status(active=排除已撤54/废单57,或数字状态码 48-57) * 语义化映射(方向判定:m_nOffsetFlag 为主 48买/49卖 + m_strOptName 交叉验证,R-007 Q3 闭环) * @param {object} [opts] { code, status } * @returns {Promise} 语义化 order 列表 */ async getOrders({ code, status, start, end } = {}) { const params = new URLSearchParams(); if (code) params.set('code', code); if (status) params.set('status', status); const qs = params.toString(); const res = await this.request('/trade/orders' + (qs ? '?' + qs : '')); const list = Array.isArray(res?.data) ? res.data : []; return list.map((o) => this.mapOrder(o)); } /** * 当日成交(GET /trade/trades,R-007) * 无过滤参数,返回全部当日成交 * @returns {Promise} 语义化 trade 列表 */ async getTrades({ start, end } = {}) { // 本期:QMT Bridge 无历史参数(老师将完善);预留 start/end 透传通道 void start; void end; const res = await this.request('/trade/trades'); const list = Array.isArray(res?.data) ? res.data : []; return list.map((t) => this.mapTrade(t)); } /** * 交易日历(GET /data/calendar/trading_dates,R-007 日期导航) * @param {object} [opts] { start, end } YYYYMMDD * @returns {Promise<{ market: string, dates: string[] }>} */ async getTradingDates({ start, end } = {}) { const params = new URLSearchParams(); if (start) params.set('start', start); if (end) params.set('end', end); const qs = params.toString(); const res = await this.request('/data/calendar/trading_dates' + (qs ? '?' + qs : '')); return { market: res?.market ?? '', dates: Array.isArray(res?.dates) ? res.dates : [], }; } /** * 归一化证券代码:QMT 委托/成交返回无后缀(如 001330),持仓体系带后缀(001330.SZ)。 * 规则(与持仓 mapPosition 一致):6 位数字 + 交易所后缀(SH/SZ/BJ);已有后缀原样保留。 */ normalizeInstrumentCode(raw, exchange) { let code = String(raw ?? '').trim().toUpperCase(); if (!code) return ''; if (/^\d{6}\.(SH|SZ|BJ)$/.test(code)) return code; // 已带后缀 if (/^\d{6}$/.test(code)) { const ex = String(exchange ?? '').toUpperCase(); if (ex === 'SH' || ex === 'SZ' || ex === 'BJ') return code + '.' + ex; // 无交易所信息时按规则推断:6/9 开头沪市,0/3 开头深市 const head = code[0]; if (head === '6' || head === '9') return code + '.SH'; return code + '.SZ'; } return code; } /** 委托字段映射:m_ 原始字段 → 语义字段(R-007 2.1/2.2;R-009 迭代 07 补 tradeDate + code 归一化) */ mapOrder(o) { const direction = o.m_nOffsetFlag === 48 ? 'buy' : o.m_nOffsetFlag === 49 ? 'sell' : ''; const code = this.normalizeInstrumentCode(o.m_strInstrumentID, o.m_strExchangeID); return { orderId: o.m_strOrderSysID ?? '', code, name: o.m_strInstrumentName ?? '', exchange: o.m_strExchangeID ?? '', direction, // 'buy' | 'sell' | '' directionCode: o.m_nOffsetFlag ?? null, // 原始 48/49 optName: o.m_strOptName ?? '', // 中文操作名(限价卖出) status: o.m_nOrderStatus ?? null, // 48-57 orderVolume: o.m_nVolumeTotalOriginal ?? 0, tradedVolume: o.m_nVolumeTraded ?? 0, totalVolume: o.m_nVolumeTotal ?? 0, limitPrice: +(o.m_dLimitPrice ?? 0), tradedPrice: +(o.m_dTradedPrice ?? 0), amount: +(o.m_dTradeAmount ?? 0), tradeDate: o.m_strTradeDate ?? o.m_strInsertDate ?? '', // 交易日:委托无独立字段,用 insertDate 兜底 insertDate: o.m_strInsertDate ?? '', insertTime: o.m_strInsertTime ?? '', cancelInfo: o.m_strCancelInfo ?? '', errorMsg: o.m_strErrorMsg ?? '', }; } /** * 计算一笔委托的总费用(2026-09-01 老师定规则 + 修正) * 核心规则:同一笔委托分多笔成交,按总成交金额合并算一次佣金(不逐笔重复收费); * 多笔独立委托(分开下单)才各自算佣金、各自触发 5 元最低。 * - 佣金:Σ成交额 × (佣金费率万分之m_dCommission),不足 5 元按 5 元(账号没免五,一次) * - 印花税:卖出单边,Σ成交额 × 万分之5(2023-08-28 起标准) * - 过户费:双向,Σ成交额 × 万分之0.1(2022-04-29 起标准,无最低限额) * @param {Array} trades 该委托的成交列表(每笔含 amount/direction) * @param {number} commissionRateWan 佣金费率(万分之,来自 m_dCommission) * @returns {{ totalAmount: number, commission: number, stampTax: number, transferFee: number, total: number }} */ calcOrderFees(trades, commissionRateWan) { const list = Array.isArray(trades) ? trades : []; // 合并总成交额(同一委托分笔成交合并计费) const totalAmount = list.reduce((s, t) => s + Math.max(0, +(t.amount ?? 0)), 0); const direction = list[0]?.direction ?? ''; // 佣金:合并总额 × 费率,不足 5 元按 5 元(一次) let commission = totalAmount * (commissionRateWan / 10000); if (commission < 5 && commission > 0) commission = 5; // 印花税:卖出单边,万分之5 const stampTax = direction === 'sell' ? totalAmount * 0.0005 : 0; // 过户费:双向,万分之0.1 const transferFee = totalAmount * 0.00001; return { totalAmount: +totalAmount.toFixed(2), commission: +commission.toFixed(4), stampTax: +stampTax.toFixed(4), transferFee: +transferFee.toFixed(4), total: +(commission + stampTax + transferFee).toFixed(4), }; } /** 成交字段映射:m_ 原始字段 → 语义字段(R-007 2.1/2.2) * m_dCommission 语义(2026-09-01 老师定):佣金费率(万分之),如 0.852 = 万分之0.852 */ mapTrade(t) { const direction = t.m_nOffsetFlag === 48 ? 'buy' : t.m_nOffsetFlag === 49 ? 'sell' : ''; return { tradeId: t.m_strTradeID ?? '', orderId: t.m_strOrderSysID ?? '', // 关联委托 code: this.normalizeInstrumentCode(t.m_strInstrumentID, t.m_strExchangeID), name: t.m_strInstrumentName ?? '', exchange: t.m_strExchangeID ?? '', direction, // 'buy' | 'sell' | '' directionCode: t.m_nOffsetFlag ?? null, optName: t.m_strOptName ?? '', price: +(t.m_dPrice ?? 0), volume: t.m_nVolume ?? 0, amount: +(t.m_dTradeAmount ?? 0), commissionRateWan: +(t.m_dCommission ?? t.m_dComssion ?? 0), // 佣金费率(万分之) tradeDate: t.m_strTradeDate ?? '', tradeTime: t.m_strTradeTime ?? '', }; } /** 持仓字段映射:语义字段优先,回退 m_ 原始字段 */ mapPosition(p) { return { code: p.stock_code ?? p.m_strInstrumentID ?? '', name: p.stock_name ?? p.m_strInstrumentName ?? '', exchange: p.m_strExchangeName ?? '', volume: p.volume ?? p.m_nVolume ?? 0, available: p.available ?? p.m_nCanUseVolume ?? 0, frozenVolume: p.frozen_volume ?? p.m_nFrozenVolume ?? 0, avgPrice: +(p.avg_price ?? p.m_dAvgOpenPrice ?? 0), price: +(p.price ?? p.m_dLastPrice ?? 0), marketValue: +(p.market_value ?? p.m_dInstrumentValue ?? 0), profit: +(p.profit ?? p.m_dFloatProfit ?? 0), profitPct: +(p.profit_pct ?? (p.m_dProfitRate != null ? p.m_dProfitRate * 100 : 0)), }; } }