迭代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:
2026-09-10 14:05:06 +08:00
parent 3dfa39b8bf
commit ec02a791ee
40 changed files with 3369 additions and 659 deletions
+63 -42
View File
@@ -1,11 +1,13 @@
/**
* R-011 API 层集成测试:handleStrategy tabs 域(联动链路
* R-011 API 层集成测试:handleStrategy tabs 域(R-027 策略静态化更新
* 独立内存 mock settings scope + mock manager,验证:
* 1. tabs 端点:返回归一化后的统一数组
* 1. tabs 端点:返回归一化统一数组(恒 5 条:3 内置 + 2 策略)
* 2. tabs/update:整表更新(顺序 + 显隐)
* 3. strategies/add联动追加 tab 条目(末尾
* 4. strategies/remove联动删除 tab 条目 + 清份额(manager
* 5. strategies/update重命名(tabs 行 name join 跟随
* 3. strategies/add已退役(方法表不含 + 调用抛 not-found
* 4. strategies/remove已退役(方法表不含 + 调用抛 not-found
* 5. strategies/update已退役(方法表不含 + 调用抛 not-found
* 6. strategies/schema-update:单策略字段定义写入(读回一致)
* 7. 端点表不含 strategies/move(已废弃)
* 纯内存,不触真实数据(技术约束-011)
*/
import { handleStrategy, STRATEGY_METHODS } from '../src/api/strategies.js';
@@ -25,60 +27,79 @@ function makeScope(initial) {
};
}
/** 断言某端点已退役:STRATEGY_METHODS 不含 + 调用抛 code=not-found */
async function assertRetired(method, args, runtime) {
assert(!STRATEGY_METHODS.has(method), "STRATEGY_METHODS 不含 " + method);
try {
await handleStrategy(method, args, runtime);
assert(false, method + " 应抛 not-found(未抛错)");
} catch (e) {
assert(e.code === "not-found", method + " 抛 code=not-found");
}
}
const scope = makeScope({
strategies: [{ id: "grid-supermarket", name: "网格超市" }],
strategies: [
{ id: "grid-supermarket", name: "网格超市" },
{ id: "manual-t", name: "手动做T" },
],
tabs: [
{ id: "tab-all", kind: "builtin", refKey: "allPositions", name: "全部持仓", visible: true, order: 0 },
{ id: "tab-g", kind: "strategy", refId: "grid-supermarket", name: "网格超市", visible: true, order: 1 },
{ id: "tab-all-positions", kind: "builtin", refKey: "allPositions", name: "全部持仓", visible: true, order: 0 },
{ id: "tab-trade-records", kind: "builtin", refKey: "tradeRecords", name: "交易记录", visible: true, order: 1 },
{ id: "tab-watchlist", kind: "builtin", refKey: "watchlist", name: "关注列表", visible: true, order: 2 },
{ id: "tab-strategy-grid-supermarket", kind: "strategy", refId: "grid-supermarket", name: "网格超市", visible: true, order: 3 },
{ id: "tab-strategy-manual-t", kind: "strategy", refId: "manual-t", name: "手动做T", visible: true, order: 4 },
],
});
const cleared = [];
const manager = {
async clearStrategyShares(id) { cleared.push(id); },
};
const runtime = { manager, settings: scope };
const runtime = { manager: {}, settings: scope };
// 1. tabs 端点
console.log("\n[1] tabs 端点");
const strat1 = await handleStrategy("strategies", {}, runtime);
assert(Array.isArray(strat1) && strat1.length === 2, "strategies 端点恒 2 项(内置)");
const tabs1 = await handleStrategy("tabs", {}, runtime);
assert(Array.isArray(tabs1) && tabs1.length === 2, "tabs 返回统一数组");
assert(tabs1[0].kind === "builtin" && tabs1[1].kind === "strategy", "内置 + 策略混排");
assert(Array.isArray(tabs1) && tabs1.length === 5, "tabs 恒 5 条(3 内置 + 2 策略)");
assert(tabs1.filter((t) => t.kind === "builtin").length === 3 && tabs1.filter((t) => t.kind === "strategy").length === 2, "内置 3 + 策略 2 混排");
assert(tabs1.some((t) => t.refId === "grid-supermarket") && tabs1.some((t) => t.refId === "manual-t"), "策略 tab 恒在(不可增删)");
// 2. tabs/update 整表更新
console.log("\n[2] tabs/update 整表更新");
const afterUpdate = await handleStrategy("tabs/update", { tabs: [
{ id: "tab-g", kind: "strategy", refId: "grid-supermarket", name: "网格超市", visible: false, order: 0 },
{ id: "tab-all", kind: "builtin", refKey: "allPositions", name: "全部持仓", visible: true, order: 1 },
{ id: "tab-strategy-manual-t", kind: "strategy", refId: "manual-t", name: "手动做T", visible: false, order: 0 },
{ id: "tab-all-positions", kind: "builtin", refKey: "allPositions", name: "全部持仓", visible: true, order: 1 },
{ id: "tab-watchlist", kind: "builtin", refKey: "watchlist", name: "关注列表", visible: true, order: 2 },
{ id: "tab-strategy-grid-supermarket", kind: "strategy", refId: "grid-supermarket", name: "网格超市", visible: true, order: 3 },
{ id: "tab-trade-records", kind: "builtin", refKey: "tradeRecords", name: "交易记录", visible: true, order: 4 },
] }, runtime);
assert(afterUpdate[0].id === "tab-g" && afterUpdate[0].visible === false, "策略 tab 排到前面且隐藏");
assert(afterUpdate[1].id === "tab-all", "内置 tab 排");
assert(afterUpdate[0].id === "tab-strategy-manual-t" && afterUpdate[0].visible === false, "手动做T 排最前且隐藏(顺序+显隐生效)");
assert(afterUpdate[1].id === "tab-all-positions", "内置 tab 排第二位");
assert(afterUpdate.length === 5, "整表更新后恒 5 条");
// 3. strategies/add 联动追加 tab
console.log("\n[3] strategies/add 联动追加 tab");
const added = await handleStrategy("strategies/add", { name: "长线持有" }, runtime);
const tabsAfterAdd = await handleStrategy("tabs", {}, runtime);
assert(tabsAfterAdd.length === 3, "add 后 tabs 联动新增一条");
assert(tabsAfterAdd[2].kind === "strategy" && tabsAfterAdd[2].refId === added.id, "新策略 tab 追加末尾");
assert(tabsAfterAdd[2].name === "长线持有", "新策略 tab name 正确");
// 3. strategies/add 已退役
console.log("\n[3] strategies/add 退役");
await assertRetired("strategies/add", { name: "长线持有" }, runtime);
// 4. strategies/remove 联动删除 tab + 清份额
console.log("\n[4] strategies/remove 联动删除 tab");
await handleStrategy("strategies/remove", { strategyId: "grid-supermarket" }, runtime);
const tabsAfterRm = await handleStrategy("tabs", {}, runtime);
assert(!tabsAfterRm.some((t) => t.refId === "grid-supermarket"), "删除策略后对应 tab 条目消失");
assert(tabsAfterRm.some((t) => t.refId === added.id), "其余策略 tab 保留");
assert(cleared.includes("grid-supermarket"), "manager.clearStrategyShares 被调用(份额回未分配)");
// 4. strategies/remove 已退役
console.log("\n[4] strategies/remove 退役");
await assertRetired("strategies/remove", { strategyId: "grid-supermarket" }, runtime);
// 5. strategies/update 重命名(tabs 行 join 跟随)
console.log("\n[5] strategies/update 重命名跟随");
await handleStrategy("strategies/update", { strategies: [
{ id: added.id, name: "长线持有v2" },
] }, runtime);
const tabsAfterRename = await handleStrategy("tabs", {}, runtime);
assert(tabsAfterRename.find((t) => t.refId === added.id).name === "长线持有v2", "重命名后 tabs 行 name join 跟随(Q4");
// 5. strategies/update 已退役
console.log("\n[5] strategies/update 退役");
await assertRetired("strategies/update", { strategies: [] }, runtime);
// 6. 端点表不含 strategies/move
console.log("\n[6] strategies/move 已废弃");
// 6. strategies/schema-update 单策略写入(R-027 唯一写路径)
console.log("\n[6] strategies/schema-update");
const saved = await handleStrategy("strategies/schema-update", {
strategyId: "grid-supermarket",
configSchema: [{ key: "gridGap", label: "网格间距", type: "text", def: "3%" }],
}, runtime);
assert(saved.configSchema[0].label === "网格间距", "schema-update 返回更新后的策略对象");
const stratAfter = await handleStrategy("strategies", {}, runtime);
assert(stratAfter.find((s) => s.id === "grid-supermarket").configSchema[0].label === "网格间距", "读回一致(写入 strategyFields");
assert(stratAfter.find((s) => s.id === "manual-t").configSchema.length === 0, "只影响目标策略(manual-t 无定义)");
// 7. 端点表不含 strategies/move
console.log("\n[7] strategies/move 已废弃");
assert(!STRATEGY_METHODS.has("strategies/move"), "STRATEGY_METHODS 不含 strategies/move");
console.log("\n结果: " + pass + " 通过 / " + fail + " 失败");