Files
one_divine_lot/scripts/test-r027-static-strategies.mjs
kyugao ec02a791ee 迭代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 通过
2026-09-10 14:05:06 +08:00

140 lines
8.5 KiB
JavaScript
Raw Permalink 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.
/**
* 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);