迭代22+23: 策略tab静态化 + 计算字段(数字/判定型)+ 网格超市信号字段
- R-027 静态化:策略恒为内置两项(不可增删改名,保留显隐/排序);设置页移除「策略分组」; 字段配置入口迁到策略 tab 内「列设置」旁(FieldConfigDialog + schema-update 单策略写入) - R-026 计算字段:自写公式引擎(中文变量、四则/括号/round·abs·min·max、缺值短路→—)+ 变量目录(行情/合约/持仓/自定义字段)+ strategy-positions 逐行现算(只读内存缓存,不落库、列只读) - R-028 判定型:比较运算 + and/or + inferResultKind + 结果类型一致性校验 + 判定列 ✓/— 渲染; 网格超市落地 可下空单=涨停价>基准值+网格大小、可下多单=跌停价<基准值-网格大小 - 变量选择改标签平铺(老师反馈);公式手册 docs/99-其他材料/计算字段公式说明.md - 约束同步:产品约束-002/003/004/009/013/014、技术约束-014/015/022/023/024、UI约束-002/003/005/008 - 回归:新增 test-r026/test-r027,更新 r011/r013/r017,17 个脚本全绿;typecheck/build 通过
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* R-027 回归测试:策略 tab 静态化 + 字段配置存储归一化(迭代 22 阶段一)
|
||||
* 覆盖:
|
||||
* 1. 策略常量:getStrategies 恒为内置两项(存量自建策略被忽略)
|
||||
* 2. 旧数据兼容:strategies[].configSchema → 读取回填;strategyFields 优先
|
||||
* 3. 字段归一化:类型白名单 / decimals 夹取 / formula 字段不带 def
|
||||
* 4. 单策略写入:updateStrategyFields 只改目标策略;未知 strategyId 抛 strategy-not-found
|
||||
* 5. tabs 归一化:丢弃自建策略条目、保留 visible/order、缺失补齐、旧布尔对象兼容
|
||||
* 6. updateTabs 只接受内置条目
|
||||
* 7. 端点:strategies/schema-update 校验 + 落库;已退役端点调用被拒
|
||||
* 纯内存 mock(技术约束-011:不触真实数据)
|
||||
*/
|
||||
import {
|
||||
BUILTIN_STRATEGIES, getStrategies, getStrategyFields, updateStrategyFields, normalizeFields,
|
||||
getTabs, updateTabs, normalizeTabs, strategySchema,
|
||||
} from '../src/settings.js';
|
||||
import { STRATEGY_METHODS, handleStrategy } from '../src/api/strategies.js';
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { pass++; console.log(' ✅ ' + msg); } else { fail++; console.log(' ❌ ' + msg); }
|
||||
}
|
||||
function section(t) { console.log('\n[' + t + ']'); }
|
||||
function makeScope(initial) {
|
||||
const store = structuredClone(initial ?? {});
|
||||
return {
|
||||
get: () => structuredClone(store),
|
||||
update: async (patch) => { Object.assign(store, structuredClone(patch)); },
|
||||
_store: store,
|
||||
};
|
||||
}
|
||||
async function throwsAsync(fn, code, msg) {
|
||||
try { await fn(); assert(false, msg + '(未抛错)'); }
|
||||
catch (e) { assert(e?.code === code, msg + '(code=' + e?.code + ')'); }
|
||||
}
|
||||
|
||||
// ---------- 1. 策略常量 ----------
|
||||
section('1. 策略恒为内置两项');
|
||||
const legacyScope = makeScope({
|
||||
strategies: [
|
||||
{ id: 'grid-supermarket', name: '网格超市', configSchema: [{ key: 'g', label: '网格大小', type: 'number', def: 1, unit: '元' }] },
|
||||
{ id: 'manual-t', name: '手动做T', configSchema: [{ key: 't', label: 'T仓成本价', type: 'number', unit: '元' }] },
|
||||
{ id: 'long-term', name: '长线持有', configSchema: [{ key: 'x', label: '目标价', type: 'number' }] },
|
||||
],
|
||||
});
|
||||
const list = getStrategies(legacyScope);
|
||||
assert(list.length === 2, '策略表恒 2 项(自建策略被忽略)');
|
||||
assert(list.map((s) => s.id).join(',') === BUILTIN_STRATEGIES.map((s) => s.id).join(','), '顺序与内置常量一致');
|
||||
assert(!list.some((s) => s.id === 'long-term'), '自建策略不出现在读取结果');
|
||||
|
||||
// ---------- 2. 旧数据兼容 ----------
|
||||
section('2. 字段定义读取(旧数据回填 / 新键优先)');
|
||||
assert(getStrategyFields(legacyScope, 'grid-supermarket').length === 1, '旧 strategies[].configSchema 回填');
|
||||
assert(getStrategyFields(legacyScope, 'grid-supermarket')[0].label === '网格大小', '回填内容正确');
|
||||
const newScope = makeScope({
|
||||
strategies: [{ id: 'grid-supermarket', name: '网格超市', configSchema: [{ key: 'old', label: '旧的', type: 'text' }] }],
|
||||
strategyFields: { 'grid-supermarket': [{ key: 'new', label: '新的', type: 'number', def: 1 }] },
|
||||
});
|
||||
assert(getStrategyFields(newScope, 'grid-supermarket')[0].label === '新的', 'strategyFields 优先于旧键');
|
||||
assert(getStrategyFields(newScope, 'manual-t').length === 0, '另一策略无定义 → []');
|
||||
|
||||
// ---------- 3. 字段归一化 ----------
|
||||
section('3. normalizeFields');
|
||||
const norm = normalizeFields([
|
||||
{ key: 'a', label: 'A', type: '不合法', def: 1 },
|
||||
{ key: 'b', label: 'B', type: 'formula', formula: ' 现价 × 份额 ', decimals: 9, def: 123 },
|
||||
{ key: 'c', label: 'C', type: 'number', def: 5, unit: '元' },
|
||||
]);
|
||||
assert(norm[0].type === 'text', '非法类型回退 text');
|
||||
assert(norm[1].formula === '现价 × 份额', '公式去首尾空白');
|
||||
assert(norm[1].decimals === 4, 'decimals 夹取上限 4');
|
||||
assert(!('def' in norm[1]), 'formula 字段不带 def');
|
||||
assert(norm[2].def === 5 && norm[2].unit === '元', '普通字段保留 def/unit');
|
||||
|
||||
// ---------- 4. 单策略写入 ----------
|
||||
section('4. updateStrategyFields');
|
||||
const wScope = makeScope({ strategyFields: { 'manual-t': [{ key: 't', label: 'T仓成本价', type: 'number' }] } });
|
||||
const updated = await updateStrategyFields(wScope, 'grid-supermarket', [{ key: 'g', label: '网格大小', type: 'number', def: 1 }]);
|
||||
assert(updated.configSchema.length === 1, '返回更新后的策略对象');
|
||||
assert(wScope._store.strategyFields['grid-supermarket'].length === 1, '写入 strategyFields[目标策略]');
|
||||
assert(wScope._store.strategyFields['manual-t'].length === 1, '另一策略定义不受影响');
|
||||
assert(!wScope._store.strategies, '不写 strategies[] 键(真相源单一)');
|
||||
await throwsAsync(() => updateStrategyFields(wScope, 'long-term', []), 'strategy-not-found', '未知策略 id 被拒');
|
||||
|
||||
// ---------- 5. tabs 归一化 ----------
|
||||
section('5. normalizeTabs(静态化)');
|
||||
const tScope = makeScope({
|
||||
tabs: [
|
||||
{ id: 'tab-strategy-long-term', kind: 'strategy', refId: 'long-term', name: '长线持有', visible: true, order: 0 },
|
||||
{ id: 'tab-strategy-manual-t', kind: 'strategy', refId: 'manual-t', name: '手动做T', visible: false, order: 1 },
|
||||
{ id: 'tab-strategy-grid-supermarket', kind: 'strategy', refId: 'grid-supermarket', name: '网格超市', visible: true, order: 2 },
|
||||
{ id: 'tab-trade-records', kind: 'builtin', refKey: 'tradeRecords', name: '交易记录', visible: true, order: 3 },
|
||||
],
|
||||
});
|
||||
const tabs = normalizeTabs(tScope);
|
||||
assert(tabs.length === 5, 'tabs 恒 5 条(3 内置 + 2 策略)');
|
||||
assert(!tabs.some((t) => t.refId === 'long-term'), '自建策略 tab 条目被丢弃');
|
||||
const mt = tabs.find((t) => t.refId === 'manual-t');
|
||||
assert(mt && mt.visible === false, '保留存量 visible 偏好(手动做T 隐藏)');
|
||||
assert(tabs.findIndex((t) => t.refId === 'manual-t') < tabs.findIndex((t) => t.refId === 'grid-supermarket'),
|
||||
'保留存量 order 相对次序(手动做T 在网格超市前)');
|
||||
assert(tabs.some((t) => t.refKey === 'allPositions'), '缺失的内置条目被补齐');
|
||||
const legacyBool = normalizeTabs(makeScope({ tabs: { allPositions: false, tradeRecords: true, watchlist: false } }));
|
||||
assert(legacyBool.find((t) => t.refKey === 'allPositions').visible === false, '旧布尔对象格式兼容');
|
||||
assert(legacyBool.some((t) => t.kind === 'strategy'), '旧格式下策略行补齐');
|
||||
assert(getTabs(tScope).length === 5, 'getTabs 走归一化');
|
||||
|
||||
// ---------- 6. updateTabs 过滤 ----------
|
||||
section('6. updateTabs 只接受内置条目');
|
||||
const uScope = makeScope({});
|
||||
await updateTabs(uScope, [
|
||||
{ id: 'tab-strategy-long-term', kind: 'strategy', refId: 'long-term', visible: true, order: 0 },
|
||||
{ id: 'tab-all-positions', kind: 'builtin', refKey: 'allPositions', visible: true, order: 1 },
|
||||
]);
|
||||
assert(uScope._store.tabs.length === 1 && uScope._store.tabs[0].id === 'tab-all-positions', '未知条目被过滤');
|
||||
|
||||
// ---------- 7. 端点 ----------
|
||||
section('7. 端点:schema-update 与退役端点');
|
||||
assert(STRATEGY_METHODS.has('strategies/schema-update'), 'STRATEGY_METHODS 含 schema-update');
|
||||
assert(!STRATEGY_METHODS.has('strategies/add') && !STRATEGY_METHODS.has('strategies/remove') && !STRATEGY_METHODS.has('strategies/update'),
|
||||
'退役端点已从方法表移除');
|
||||
const runtime = { manager: {}, settings: makeScope({}), storage: {}, marketHub: null };
|
||||
await throwsAsync(() => handleStrategy('strategies/add', { name: 'x' }, runtime), 'not-found', 'strategies/add 被拒');
|
||||
await throwsAsync(() => handleStrategy('strategies/update', { strategies: [] }, runtime), 'not-found', 'strategies/update 被拒');
|
||||
await throwsAsync(() => handleStrategy('strategies/remove', { strategyId: 'manual-t' }, runtime), 'not-found', 'strategies/remove 被拒');
|
||||
const apiScope = makeScope({});
|
||||
const apiRuntime = { manager: {}, settings: apiScope, storage: {}, marketHub: null };
|
||||
const saved = await handleStrategy('strategies/schema-update', {
|
||||
strategyId: 'grid-supermarket',
|
||||
configSchema: [{ key: 'grid_size', label: '网格大小', type: 'number', def: 1, unit: '元' }],
|
||||
}, apiRuntime);
|
||||
assert(saved.configSchema.length === 1 && saved.configSchema[0].label === '网格大小', 'schema-update 写入成功');
|
||||
assert(apiScope._store.strategyFields['grid-supermarket'].length === 1, '落库到 strategyFields');
|
||||
await throwsAsync(() => handleStrategy('strategies/schema-update', { strategyId: 'nope', configSchema: [] }, apiRuntime),
|
||||
'strategy-not-found', '未知策略写入被拒');
|
||||
assert(strategySchema != null, 'strategySchema 导出可用');
|
||||
|
||||
console.log('\n结果: ' + pass + ' 通过 / ' + fail + ' 失败');
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user