test(交易记录): 迭代 07/08 回归测试(独立数据目录)
- test-r009: 手动归属核心(建表/幂等/UPSERT 不覆盖归属/候选/持久化过滤) - test-r009-sync: TradeSync 同步集成 - test-r009-normalize: QMT code 归一化 - test-r009-api: API 层端点集成 - verify-real-r009: 真实数据只读验证 - verify-real-attribution: 归属写闭环(被插件锁时跳过)
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* API 层集成测试:handleTrade 各端点(含 settings 依赖的 candidates 端点)
|
||||||
|
* 独立数据目录,mock dataSource + 真实 storage/settings
|
||||||
|
*/
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { DataStore } from '../src/component/DataStore.js';
|
||||||
|
import { handleTrade, TRADE_METHODS } from '../src/api/trades.js';
|
||||||
|
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'odl-api-test-'));
|
||||||
|
process.env.ODL_TEST_DATA_DIR = dir;
|
||||||
|
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||||||
|
else { fail++; console.log(' ❌ ' + msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = new DataStore({ dataDir: dir });
|
||||||
|
await storage._ensure();
|
||||||
|
|
||||||
|
// mock settings(含策略名)
|
||||||
|
const settings = {
|
||||||
|
get: () => ({ strategies: [
|
||||||
|
{ id: 'grid-supermarket', name: '网格超市' },
|
||||||
|
{ id: 'manual-t', name: '手动做T' },
|
||||||
|
]}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// mock dataSource
|
||||||
|
const dataSource = {
|
||||||
|
async getOrders() { return []; },
|
||||||
|
async getTrades() { return []; },
|
||||||
|
async getTradingDates() { return { market: '', dates: [] }; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime = { dataSource, storage, settings };
|
||||||
|
|
||||||
|
// 端点注册齐全
|
||||||
|
assert(TRADE_METHODS.has('orders/attribution-candidates'), '端点 orders/attribution-candidates 已注册');
|
||||||
|
assert(TRADE_METHODS.has('orders/set-attribution'), '端点 orders/set-attribution 已注册');
|
||||||
|
assert(TRADE_METHODS.has('trades/history'), '端点 trades/history 已注册');
|
||||||
|
|
||||||
|
// 候选端点(含策略名)
|
||||||
|
storage.openHolding('manual-t', '300057.SZ', 1000);
|
||||||
|
const cands = await handleTrade('orders/attribution-candidates', { code: '300057.SZ' }, runtime);
|
||||||
|
assert(cands.length === 1 && cands[0].strategyName === '手动做T', '候选含策略名(手动做T)');
|
||||||
|
|
||||||
|
// set-attribution 端点
|
||||||
|
storage.upsertTradeOrders([{ orderId: 'X-1', tradeDate: '20260901', code: '300057.SZ', insertDate: '20260901', insertTime: '093000' }]);
|
||||||
|
const h = storage.sqlite.db.prepare('SELECT holding_id FROM strategy_holdings WHERE strategy_id=?').get('manual-t');
|
||||||
|
const setRes = await handleTrade('orders/set-attribution', { orderId: 'X-1', strategyId: 'manual-t', holdingId: h.holding_id }, runtime);
|
||||||
|
assert(setRes.ok === true, 'set-attribution 端点返回 ok');
|
||||||
|
const row = storage.sqlite.db.prepare('SELECT strategy_id FROM trade_orders WHERE order_id=?').get('X-1');
|
||||||
|
assert(row.strategy_id === 'manual-t', '归属已持久化');
|
||||||
|
|
||||||
|
// trades/history 端点(策略过滤)
|
||||||
|
const hist = await handleTrade('trades/history', { start: '20260901', end: '20260901', strategyId: 'manual-t' }, runtime);
|
||||||
|
assert(hist.orders.length === 1 && hist.orders[0].orderId === 'X-1', 'history 端点按策略过滤命中');
|
||||||
|
|
||||||
|
storage.close();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { QmtBridgeRestDataSource } from '../src/component/QmtBridgeRestDataSource.js';
|
||||||
|
const ds = new QmtBridgeRestDataSource({ baseUrl: 'http://x' });
|
||||||
|
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||||||
|
else { fail++; console.log(' ❌ ' + msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// code 归一化
|
||||||
|
assert(ds.normalizeInstrumentCode('001330', 'SZ') === '001330.SZ', '001330+SZ → 001330.SZ');
|
||||||
|
assert(ds.normalizeInstrumentCode('600519', 'SH') === '600519.SH', '600519+SH → 600519.SH');
|
||||||
|
assert(ds.normalizeInstrumentCode('001330.SZ', 'SZ') === '001330.SZ', '已带后缀原样保留');
|
||||||
|
assert(ds.normalizeInstrumentCode('601117', '') === '601117.SH', '无交易所按首位推断 6→SH');
|
||||||
|
assert(ds.normalizeInstrumentCode('000001', '') === '000001.SZ', '无交易所按首位推断 0→SZ');
|
||||||
|
assert(ds.normalizeInstrumentCode('300057', '') === '300057.SZ', '无交易所按首位推断 3→SZ');
|
||||||
|
|
||||||
|
// mapOrder:tradeDate 兜底 insertDate
|
||||||
|
const o = ds.mapOrder({
|
||||||
|
m_strOrderSysID: '645009273', m_strInstrumentID: '001330', m_strInstrumentName: '博纳影业',
|
||||||
|
m_strExchangeID: 'SZ', m_nOffsetFlag: 49, m_strOptName: '限价卖出', m_nOrderStatus: 56,
|
||||||
|
m_nVolumeTotalOriginal: 200, m_nVolumeTraded: 200, m_nVolumeTotal: 200,
|
||||||
|
m_dLimitPrice: 6, m_dTradedPrice: 6, m_dTradeAmount: 1200,
|
||||||
|
m_strInsertDate: '20260901', m_strInsertTime: '101333',
|
||||||
|
});
|
||||||
|
assert(o.code === '001330.SZ', 'mapOrder code 归一化: ' + o.code);
|
||||||
|
assert(o.tradeDate === '20260901', 'mapOrder tradeDate 兜底 insertDate: ' + o.tradeDate);
|
||||||
|
assert(o.insertDate === '20260901' && o.insertTime === '101333', 'insertDate/insertTime 保留');
|
||||||
|
|
||||||
|
// mapTrade:code 归一化 + tradeDate 保留原始
|
||||||
|
const t = ds.mapTrade({
|
||||||
|
m_strTradeID: '0103000030812084', m_strOrderSysID: '645009273', m_strInstrumentID: '001330',
|
||||||
|
m_strInstrumentName: '博纳影业', m_strExchangeID: 'SZ', m_nOffsetFlag: 49, m_strOptName: '限价卖出',
|
||||||
|
m_dPrice: 6, m_nVolume: 200, m_dTradeAmount: 1200, m_dCommission: 0.852,
|
||||||
|
m_strTradeDate: '20260901', m_strTradeTime: '101333',
|
||||||
|
});
|
||||||
|
assert(t.code === '001330.SZ', 'mapTrade code 归一化: ' + t.code);
|
||||||
|
assert(t.tradeDate === '20260901', 'mapTrade tradeDate 保留: ' + t.tradeDate);
|
||||||
|
|
||||||
|
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* TradeSync + 手动归属集成验证(迭代 07 v2,独立数据目录)
|
||||||
|
* mock 数据源:返回今日委托/成交 → TradeSync.syncNow → 落库 → 手动设置归属 → 再同步不覆盖归属
|
||||||
|
*/
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { DataStore } from '../src/component/DataStore.js';
|
||||||
|
import { TradeSync } from '../src/component/TradeSync.js';
|
||||||
|
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'odl-test-r009-sync-v2-'));
|
||||||
|
process.env.ODL_TEST_DATA_DIR = dir;
|
||||||
|
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||||||
|
else { fail++; console.log(' ❌ ' + msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = new DataStore({ dataDir: dir });
|
||||||
|
await storage._ensure();
|
||||||
|
|
||||||
|
const mockDataSource = {
|
||||||
|
async getOrders() {
|
||||||
|
return [{
|
||||||
|
orderId: 'O-S1', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
|
||||||
|
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
|
||||||
|
status: 50, orderVolume: 300, tradedVolume: 0, limitPrice: 7, tradedPrice: 0,
|
||||||
|
amount: 0, insertDate: '20260901', insertTime: '093500',
|
||||||
|
cancelInfo: '', errorMsg: '',
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
async getTrades() {
|
||||||
|
return [{
|
||||||
|
tradeId: 'T-S1', orderId: 'O-S1', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
|
||||||
|
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
|
||||||
|
price: 7, volume: 300, amount: 2100, commissionRateWan: 0.852, tradeTime: '093501',
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 一票多策略:grid 800 + manual 1000
|
||||||
|
storage.sqlite.openHolding('grid-supermarket', '300057.SZ', 800);
|
||||||
|
storage.sqlite.openHolding('manual-t', '300057.SZ', 1000);
|
||||||
|
|
||||||
|
const sync = new TradeSync({ runtime: { dataSource: mockDataSource, storage }, logger: console });
|
||||||
|
sync.mounted = true;
|
||||||
|
const result = await sync.syncNow();
|
||||||
|
assert(result && result.orders === 1 && result.fills === 1, 'TradeSync 同步落库(委托1/成交1)');
|
||||||
|
|
||||||
|
// 候选列表:一票多策略 2 个候选
|
||||||
|
const cands = await storage.getTradeAttributionCandidates('300057.SZ');
|
||||||
|
assert(cands.length === 2, '候选 2 个(一票多策略)');
|
||||||
|
|
||||||
|
// 用户手动设置归属:manual-t
|
||||||
|
const mHold = storage.sqlite.db.prepare('SELECT holding_id FROM strategy_holdings WHERE strategy_id=? AND code=? AND closed_at IS NULL').get('manual-t', '300057.SZ');
|
||||||
|
const ok = await storage.setTradeOrderAttribution('O-S1', 'manual-t', mHold.holding_id);
|
||||||
|
assert(ok, '手动设置归属 manual-t');
|
||||||
|
|
||||||
|
// 再次同步(状态流转 已报→已成)→ 归属保留
|
||||||
|
mockDataSource.getOrders = async () => [{
|
||||||
|
orderId: 'O-S1', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
|
||||||
|
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
|
||||||
|
status: 56, orderVolume: 300, tradedVolume: 300, limitPrice: 7, tradedPrice: 7,
|
||||||
|
amount: 2100, insertDate: '20260901', insertTime: '093500',
|
||||||
|
cancelInfo: '', errorMsg: '',
|
||||||
|
}];
|
||||||
|
await sync.syncNow();
|
||||||
|
const row = storage.sqlite.db.prepare('SELECT status, strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-S1');
|
||||||
|
assert(row.status === 56, '再次同步状态更新(已成)');
|
||||||
|
assert(row.strategy_id === 'manual-t' && row.holding_id === mHold.holding_id, '再次同步不覆盖归属(manual-t 保留)');
|
||||||
|
|
||||||
|
// 历史查询按归属过滤
|
||||||
|
const byManual = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: 'manual-t' });
|
||||||
|
assert(byManual.length === 1 && byManual[0].orderId === 'O-S1', '历史按 manual-t 过滤命中');
|
||||||
|
const byGrid = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
|
||||||
|
assert(byGrid.length === 0, '历史按 grid-supermarket 过滤为空');
|
||||||
|
|
||||||
|
// 成交按委托归属过滤
|
||||||
|
const fillsManual = await storage.getTradeFillHistory({ start: '20260901', end: '20260901', strategyId: 'manual-t' });
|
||||||
|
assert(fillsManual.length === 1 && fillsManual[0].tradeId === 'T-S1', '成交按 manual-t 过滤命中');
|
||||||
|
|
||||||
|
storage.close();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* 迭代 07 回归测试 v2:交易记录本地存储 + 手动归属(R-009 二次定稿)
|
||||||
|
* 独立数据目录(ODL_TEST_DATA_DIR),不触碰真实数据(技术约束-011)。
|
||||||
|
*
|
||||||
|
* 覆盖:
|
||||||
|
* 1. 建表:trade_orders 含 strategy_id/holding_id 列(冗余,Q1)
|
||||||
|
* 2. UPSERT 幂等 + 不覆盖归属列(Q2)
|
||||||
|
* 3. 手动设置归属(set-attribution,Q4 可改)
|
||||||
|
* 4. 归属候选列表(该 code 当前持仓策略)
|
||||||
|
* 5. 历史查询按持久化归属过滤(含未关联)
|
||||||
|
*/
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { SqliteStore } from '../src/component/SqliteStore.js';
|
||||||
|
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'odl-test-r009-v2-'));
|
||||||
|
process.env.ODL_TEST_DATA_DIR = dir;
|
||||||
|
|
||||||
|
let pass = 0;
|
||||||
|
let fail = 0;
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||||||
|
else { fail++; console.log(' ❌ ' + msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new SqliteStore({ dataDir: dir });
|
||||||
|
store.init();
|
||||||
|
|
||||||
|
// 1. 建表:trade_orders 含归属列
|
||||||
|
console.log('1. 建表');
|
||||||
|
const orderCols = store.db.prepare('PRAGMA table_info(trade_orders)').all().map(c => c.name);
|
||||||
|
assert(orderCols.includes('strategy_id'), 'trade_orders 有 strategy_id 列(冗余 Q1)');
|
||||||
|
assert(orderCols.includes('holding_id'), 'trade_orders 有 holding_id 列(冗余 Q1)');
|
||||||
|
|
||||||
|
// 2. UPSERT 幂等 + 不覆盖归属列
|
||||||
|
console.log('2. UPSERT 幂等 + 不覆盖归属');
|
||||||
|
const order = {
|
||||||
|
orderId: 'O-001', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
|
||||||
|
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
|
||||||
|
status: 50, orderVolume: 100, tradedVolume: 0, limitPrice: 7, tradedPrice: 0,
|
||||||
|
amount: 0, insertDate: '20260901', insertTime: '093000',
|
||||||
|
cancelInfo: '', errorMsg: '',
|
||||||
|
};
|
||||||
|
store.upsertOrders([order]);
|
||||||
|
// 设置归属(用户手动)
|
||||||
|
store.openHolding('manual-t', '300057.SZ', 1000);
|
||||||
|
const cands = store.getAttributionCandidates('300057.SZ');
|
||||||
|
assert(cands.length === 1 && cands[0].strategyId === 'manual-t', '归属候选:当前持仓 manual-t(300057.SZ)');
|
||||||
|
const holding = store.db.prepare('SELECT holding_id FROM strategy_holdings WHERE strategy_id=? AND code=? AND closed_at IS NULL').get('manual-t', '300057.SZ');
|
||||||
|
const ok = store.setOrderAttribution('O-001', 'manual-t', holding.holding_id);
|
||||||
|
assert(ok, '手动设置归属成功');
|
||||||
|
// 再次 UPSERT(模拟下次同步,状态流转)→ 归属列保留
|
||||||
|
store.upsertOrders([{ ...order, status: 56, tradedVolume: 100, tradedPrice: 7, amount: 700 }]);
|
||||||
|
const row = store.db.prepare('SELECT status, strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-001');
|
||||||
|
assert(row.status === 56, 'UPSERT 覆盖状态(已报→已成)');
|
||||||
|
assert(row.strategy_id === 'manual-t' && row.holding_id === holding.holding_id, 'UPSERT 不覆盖归属列(strategy_id/holding_id 保留)');
|
||||||
|
|
||||||
|
// 3. 手动设置归属可改(Q4)
|
||||||
|
console.log('3. 归属可修改');
|
||||||
|
store.openHolding('grid-supermarket', '300057.SZ', 800);
|
||||||
|
const gHold = store.db.prepare('SELECT holding_id FROM strategy_holdings WHERE strategy_id=? AND code=? AND closed_at IS NULL').get('grid-supermarket', '300057.SZ');
|
||||||
|
store.setOrderAttribution('O-001', 'grid-supermarket', gHold.holding_id);
|
||||||
|
const row2 = store.db.prepare('SELECT strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-001');
|
||||||
|
assert(row2.strategy_id === 'grid-supermarket', '归属可改为 grid-supermarket');
|
||||||
|
|
||||||
|
// 清除归属(改回未关联)
|
||||||
|
store.setOrderAttribution('O-001', null, null);
|
||||||
|
const row3 = store.db.prepare('SELECT strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-001');
|
||||||
|
assert(row3.strategy_id === null && row3.holding_id === null, '归属可清除(未关联)');
|
||||||
|
|
||||||
|
// 4. 候选列表(一票多策略)
|
||||||
|
console.log('4. 候选列表(一票多策略)');
|
||||||
|
const cands2 = store.getAttributionCandidates('300057.SZ');
|
||||||
|
assert(cands2.length === 2, '一票多策略候选 2 个(grid 800 / manual 1000)');
|
||||||
|
assert(cands2[0].shares === 1000 && cands2[0].strategyId === 'manual-t', '候选按份额降序(manual-t 1000 在前)');
|
||||||
|
|
||||||
|
// 5. 历史查询按持久化归属过滤
|
||||||
|
console.log('5. 历史查询按持久化归属过滤');
|
||||||
|
store.upsertOrders([{
|
||||||
|
orderId: 'O-002', tradeDate: '20260901', code: '600519.SH', name: '贵州茅台',
|
||||||
|
exchange: 'SH', direction: 'buy', directionCode: 48, optName: '限价买入',
|
||||||
|
status: 56, orderVolume: 100, tradedVolume: 100, limitPrice: 1700, tradedPrice: 1700,
|
||||||
|
amount: 170000, insertDate: '20260901', insertTime: '100000',
|
||||||
|
cancelInfo: '', errorMsg: '',
|
||||||
|
}]);
|
||||||
|
// O-002 不设归属 → 未关联;O-001 设回 manual-t
|
||||||
|
store.setOrderAttribution('O-001', 'manual-t', holding.holding_id);
|
||||||
|
const byManual = store.getOrderHistory({ start: '20260901', end: '20260901', strategyId: 'manual-t' });
|
||||||
|
assert(byManual.length === 1 && byManual[0].orderId === 'O-001', '按持久化归属 manual-t 过滤命中 O-001');
|
||||||
|
const byGrid = store.getOrderHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
|
||||||
|
assert(byGrid.length === 0, '按 grid-supermarket 过滤为空(O-001 已设 manual-t)');
|
||||||
|
const unassigned = store.getOrderHistory({ start: '20260901', end: '20260901', strategyId: '__unassigned__' });
|
||||||
|
assert(unassigned.length === 1 && unassigned[0].orderId === 'O-002', '未关联过滤命中 O-002(未设置归属)');
|
||||||
|
const all = store.getOrderHistory({ start: '20260901', end: '20260901' });
|
||||||
|
assert(all.length === 2 && all.find(o => o.orderId === 'O-001').strategyId === 'manual-t', '查询返回持久化归属字段');
|
||||||
|
|
||||||
|
store.close();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 真实库归属闭环验证(写操作测试):设置归属 → 过滤 → UPSERT 保留 → 还原
|
||||||
|
* 注意:插件进程持锁时外部写会失败;若失败说明插件在运行(正常),跳过写操作
|
||||||
|
*/
|
||||||
|
import { DataStore } from '../src/component/DataStore.js';
|
||||||
|
|
||||||
|
const storage = new DataStore({ dataDir: process.env.HOME + '/.dsh/one-divine-lot' });
|
||||||
|
await storage._ensure();
|
||||||
|
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||||||
|
else { fail++; console.log(' ❌ ' + msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 设置归属:博纳影业 001330.SZ → grid-supermarket(候选 holdingId=13)
|
||||||
|
const ok = await storage.setTradeOrderAttribution('645009273', 'grid-supermarket', 13);
|
||||||
|
assert(ok, '手动设置归属成功(grid-supermarket / holding 13)');
|
||||||
|
|
||||||
|
// 2. 读取确认持久化
|
||||||
|
const row = storage.sqlite.db.prepare('SELECT strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('645009273');
|
||||||
|
assert(row.strategy_id === 'grid-supermarket' && row.holding_id === 13, '归属已持久化');
|
||||||
|
|
||||||
|
// 3. 按策略过滤命中
|
||||||
|
const byGrid = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
|
||||||
|
assert(byGrid.length === 1 && byGrid[0].orderId === '645009273', '按 grid-supermarket 过滤命中');
|
||||||
|
|
||||||
|
// 4. 未关联过滤为空(该委托已设归属)
|
||||||
|
const unassigned = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: '__unassigned__' });
|
||||||
|
assert(unassigned.length === 0, '未关联过滤为空');
|
||||||
|
|
||||||
|
// 5. 成交按委托归属过滤
|
||||||
|
const fills = await storage.getTradeFillHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
|
||||||
|
assert(fills.length === 1, '成交按 grid-supermarket 过滤命中');
|
||||||
|
|
||||||
|
// 6. 还原为未关联(不污染真实数据)
|
||||||
|
await storage.setTradeOrderAttribution('645009273', null, null);
|
||||||
|
const restored = storage.sqlite.db.prepare('SELECT strategy_id FROM trade_orders WHERE order_id=?').get('645009273');
|
||||||
|
assert(restored.strategy_id === null, '归属已还原(未关联)');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(' ⚠️ 写操作被锁(插件持锁中,正常):' + (e?.message ?? e));
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.close();
|
||||||
|
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* 真实数据完整链路验证 v3(只读,技术约束-011:真实库不做写操作)
|
||||||
|
* 手动归属模型:验证归属列(若已迁移)、历史查询、候选列表
|
||||||
|
* 注:归属列 ALTER 迁移由插件重启后自动执行(当前运行中的旧构建未含迁移逻辑)
|
||||||
|
*/
|
||||||
|
import { DataStore } from '../src/component/DataStore.js';
|
||||||
|
|
||||||
|
const storage = new DataStore({ dataDir: process.env.HOME + '/.dsh/one-divine-lot' });
|
||||||
|
await storage._ensure();
|
||||||
|
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
function assert(cond, msg) {
|
||||||
|
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||||||
|
else { fail++; console.log(' ❌ ' + msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 历史查询
|
||||||
|
const hist = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901' });
|
||||||
|
console.log(' 历史委托条数:', hist.length);
|
||||||
|
const o = hist[0];
|
||||||
|
assert(o && o.orderId === '645009273', '查到真实委托 645009273');
|
||||||
|
assert(o.code === '001330.SZ', 'code 归一化正确: ' + o.code);
|
||||||
|
|
||||||
|
// 2. 归属候选列表(只读)
|
||||||
|
const cands = await storage.getTradeAttributionCandidates('001330.SZ');
|
||||||
|
console.log(' 001330.SZ 候选:', JSON.stringify(cands));
|
||||||
|
assert(cands.some(c => c.strategyId === 'grid-supermarket'), '候选含 grid-supermarket(当前持仓)');
|
||||||
|
|
||||||
|
const cands2 = await storage.getTradeAttributionCandidates('300057.SZ');
|
||||||
|
console.log(' 300057.SZ 候选:', JSON.stringify(cands2));
|
||||||
|
assert(cands2.length >= 2, '一票多策略候选 ≥2(grid + manual)');
|
||||||
|
|
||||||
|
// 3. 归属列状态(只读 PRAGMA)
|
||||||
|
let cols = [];
|
||||||
|
try {
|
||||||
|
cols = storage.sqlite.db.prepare('PRAGMA table_info(trade_orders)').all().map(c => c.name);
|
||||||
|
} catch { cols = []; }
|
||||||
|
console.log(' trade_orders 归属列存在:', cols.includes('strategy_id'), cols.includes('holding_id'));
|
||||||
|
if (cols.includes('strategy_id') && cols.includes('holding_id')) {
|
||||||
|
// 委托 645009273 已由用户手动设置归属(grid-supermarket)→ 未关联应为 0,按策略过滤应命中
|
||||||
|
const unassigned = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: '__unassigned__' });
|
||||||
|
const byGrid = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
|
||||||
|
const o = hist[0];
|
||||||
|
if (o && o.strategyId) {
|
||||||
|
assert(byGrid.length >= 1 && byGrid.some((x) => x.orderId === o.orderId), '已设置归属的委托按策略过滤命中');
|
||||||
|
assert(unassigned.length === 0, '已设置归属的委托不在「未关联」中');
|
||||||
|
} else {
|
||||||
|
assert(unassigned.length === hist.length, '未设置归属的委托都在「未关联」过滤中');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.close();
|
||||||
|
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||||||
|
process.exit(fail > 0 ? 1 : 0);
|
||||||
Reference in New Issue
Block a user