迭代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:
+6
-6
@@ -12,12 +12,12 @@
|
||||
*
|
||||
* 端点(POST /odl/api/<method>,body = { args: {...} }):
|
||||
* positions → 全量持仓
|
||||
* strategies → 策略列表(含 visible)
|
||||
* strategies/update → 更新策略列表(整表)
|
||||
* strategies/add → 新增策略 { name }
|
||||
* strategies/remove → 删除策略 { strategyId },联动清份额
|
||||
* strategies/move → 排序 { strategyId, dir }
|
||||
* strategy-positions → 某策略持仓 { strategyId }
|
||||
* strategies → 策略列表(R-027:恒为内置两项 + 各自 configSchema)
|
||||
* strategies/schema-update → 写某策略字段定义 { strategyId, configSchema }(R-027 唯一写路径)
|
||||
* formula/variables → 公式变量目录 { strategyId? }(R-026)
|
||||
* formula/trial → 公式试算 { strategyId, formula }(R-026)
|
||||
* strategy-positions → 某策略持仓 { strategyId }(含计算字段 computed)
|
||||
* (R-027 退役:strategies/update、strategies/add、strategies/remove、strategies/move)
|
||||
* strategy-holdings/history → 某策略已清仓历史持仓 { strategyId, range }(R-016)
|
||||
* unallocated → 未分配持仓
|
||||
* summary → 分仓摘要
|
||||
|
||||
+94
-18
@@ -2,18 +2,74 @@
|
||||
* 服务端 API:策略 / 份额 / tabs 域(从 api.js 拆分,2026-08-31)
|
||||
*
|
||||
* 端点:
|
||||
* strategies / strategies/update / strategies/add / strategies/remove
|
||||
* strategies(读;R-027:恒为内置两项)
|
||||
* strategies/schema-update(R-027:写某策略字段定义,唯一写路径)
|
||||
* strategy-positions / unallocated / summary
|
||||
* strategy-holdings/history(R-016 迭代 14:已清仓历史持仓,范围筛选)
|
||||
* add-shares / move-all-shares / remove-shares
|
||||
* tabs / tabs/update
|
||||
* formula/variables(R-026:公式变量目录)/ formula/trial(R-026:试算)
|
||||
* holdings/values-update / strategy-columns / strategy-columns/update
|
||||
*
|
||||
* R-027 退役:strategies/update、strategies/add、strategies/remove(策略静态化,不可增删改名)
|
||||
*/
|
||||
|
||||
import {
|
||||
getStrategies, updateStrategies, addStrategy, removeStrategy,
|
||||
getStrategies, getStrategyFields, updateStrategyFields, assertBuiltinStrategy,
|
||||
getTabs, updateTabs,
|
||||
getStrategyColumns, updateStrategyColumns,
|
||||
} from '../settings.js';
|
||||
import { validateFormula, compileFormula, evaluateFormula, inferResultKind } from '../formula/evaluator.js';
|
||||
import { allowedVarNames, buildContext, catalogFor, BUILTIN_VAR_NAMES } from '../formula/variables.js';
|
||||
import { computeRows } from '../formula/FormulaService.js';
|
||||
|
||||
/** 字段类型白名单(R-026 含计算类型) */
|
||||
const FIELD_TYPE_WHITELIST = new Set(['text', 'number', 'boolean', 'enum', 'formula']);
|
||||
|
||||
/** 字段校验失败(前端 Toast + 公式框描红) */
|
||||
function fieldError(message) {
|
||||
return Object.assign(new Error(message), { code: 'field-validation' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段定义校验(R-027/R-026):结构 + 唯一性 + 名称冲突 + 公式(语法/变量/仅非公式字段)
|
||||
* @param {Array} fieldDefs 客户端提交的 configSchema
|
||||
*/
|
||||
function validateConfigSchema(fieldDefs) {
|
||||
const fields = Array.isArray(fieldDefs) ? fieldDefs : [];
|
||||
const keys = new Set();
|
||||
const labels = new Set();
|
||||
fields.forEach((f, i) => {
|
||||
const label = String(f?.label ?? '').trim();
|
||||
const key = String(f?.key ?? '').trim();
|
||||
if (!label) throw fieldError(`第 ${i + 1} 个字段:展示名不能为空`);
|
||||
if (!key) throw fieldError(`字段『${label}』:标识(key)不能为空`);
|
||||
if (!FIELD_TYPE_WHITELIST.has(f?.type)) throw fieldError(`字段『${label}』:类型无效`);
|
||||
if (keys.has(key)) throw fieldError(`字段标识重复: ${key}`);
|
||||
if (labels.has(label)) throw fieldError(`字段展示名重复: ${label}`);
|
||||
if (BUILTIN_VAR_NAMES.has(label)) throw fieldError(`变量名『${label}』与内置变量冲突,请修改字段展示名`);
|
||||
if (f.type === 'enum' && !(Array.isArray(f.enum) && f.enum.some((x) => String(x).trim()))) {
|
||||
throw fieldError(`字段『${label}』:枚举类型需填写选项`);
|
||||
}
|
||||
keys.add(key);
|
||||
labels.add(label);
|
||||
});
|
||||
// 公式校验:变量 = 内置名 ∪ 本策略非 formula 字段展示名(零循环依赖)
|
||||
const vars = allowedVarNames(fields);
|
||||
for (const f of fields) {
|
||||
if (f?.type !== 'formula') continue;
|
||||
const res = validateFormula(f.formula, vars);
|
||||
if (!res.ok) throw fieldError(`字段『${f.label}』公式错误:${res.error.message}`);
|
||||
// R-028:结果类型声明必须与公式形态一致(判定 = 顶层比较/and/or;数字 = 其余)
|
||||
const declared = f.resultKind === 'boolean' ? 'boolean' : 'number';
|
||||
const inferred = inferResultKind(compileFormula(f.formula).ast);
|
||||
if (declared !== inferred) {
|
||||
throw fieldError(declared === 'boolean'
|
||||
? `字段『${f.label}』:结果类型选了「判定」,但公式没有比较运算(如 涨停价 > 基准值 + 1)`
|
||||
: `字段『${f.label}』:公式结果是判定,请把「结果类型」改为「判定」`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** R-016 范围档 → 回溯毫秒(自然日近似,展示用途足够;技术约束-019)。
|
||||
* range='today' 不走回溯毫秒——二轮补充(2026-09-07 老师定):本地自然日 00:00 起清仓的持仓
|
||||
@@ -36,9 +92,9 @@ function localDayStartMs() {
|
||||
/** 策略/份额/tabs 端点方法表 */
|
||||
export const STRATEGY_METHODS = new Set([
|
||||
'strategies',
|
||||
'strategies/update',
|
||||
'strategies/add',
|
||||
'strategies/remove',
|
||||
'strategies/schema-update', // R-027:写某策略字段定义(唯一写路径)
|
||||
'formula/variables', // R-026:公式变量目录
|
||||
'formula/trial', // R-026:公式试算(取一行真实持仓)
|
||||
'strategy-positions',
|
||||
'unallocated',
|
||||
'summary',
|
||||
@@ -59,27 +115,47 @@ export const STRATEGY_METHODS = new Set([
|
||||
* @param {object} args
|
||||
* @param {object} runtime { manager, settings, storage }
|
||||
*/
|
||||
export async function handleStrategy(method, args, { manager, settings, storage }) {
|
||||
export async function handleStrategy(method, args, { manager, settings, storage, marketHub }) {
|
||||
switch (method) {
|
||||
case 'strategies':
|
||||
return getStrategies(settings);
|
||||
case 'strategies/update':
|
||||
await updateStrategies(settings, args.strategies);
|
||||
return getStrategies(settings);
|
||||
case 'strategies/add':
|
||||
return await addStrategy(settings, args.name);
|
||||
case 'strategies/remove':
|
||||
// 删除策略 + 联动清空该策略下所有份额(份额回未分配)
|
||||
await removeStrategy(settings, args.strategyId);
|
||||
await manager.clearStrategyShares(args.strategyId);
|
||||
return getStrategies(settings);
|
||||
case 'strategies/schema-update':
|
||||
// R-027:单策略字段定义写入(结构/唯一性/公式校验 → strategyFields)
|
||||
assertBuiltinStrategy(args.strategyId);
|
||||
validateConfigSchema(args.configSchema);
|
||||
return await updateStrategyFields(settings, args.strategyId, args.configSchema);
|
||||
case 'formula/variables':
|
||||
// R-026:公式变量目录(内置分组 + 本策略非 formula 字段)
|
||||
if (args.strategyId != null) assertBuiltinStrategy(args.strategyId);
|
||||
return catalogFor(getStrategyFields(settings, args.strategyId));
|
||||
case 'formula/trial': {
|
||||
// R-026:试算(用该策略一行真实持仓数据求值;无持仓返回 reason)
|
||||
assertBuiltinStrategy(args.strategyId);
|
||||
const fieldDefs = getStrategyFields(settings, args.strategyId);
|
||||
const check = validateFormula(args.formula, allowedVarNames(fieldDefs));
|
||||
if (!check.ok) throw fieldError(check.error.message);
|
||||
const rows = await manager.getStrategyPositions(args.strategyId);
|
||||
const row = rows.find((r) => (r.shares ?? 0) > 0) ?? rows[0] ?? null;
|
||||
if (!row) return { code: null, name: null, value: null, reason: 'no-holding' };
|
||||
const ctx = buildContext({
|
||||
row,
|
||||
quote: marketHub?.quotes?.get(row.code) ?? null,
|
||||
instrument: marketHub?.instruments?.get(row.code) ?? null,
|
||||
fieldDefs,
|
||||
});
|
||||
const value = evaluateFormula(args.formula, ctx);
|
||||
return { code: row.code, name: row.name ?? null, value, reason: value === null ? 'no-data' : null };
|
||||
}
|
||||
case 'tabs':
|
||||
return getTabs(settings);
|
||||
case 'tabs/update':
|
||||
await updateTabs(settings, args.tabs);
|
||||
return getTabs(settings);
|
||||
case 'strategy-positions':
|
||||
return await manager.getStrategyPositions(args.strategyId);
|
||||
case 'strategy-positions': {
|
||||
// R-026:含计算字段的策略,逐行现算(只读内存缓存;无 formula 字段零开销)
|
||||
const rows = await manager.getStrategyPositions(args.strategyId);
|
||||
return computeRows({ rows, fieldDefs: getStrategyFields(settings, args.strategyId), hub: marketHub });
|
||||
}
|
||||
case 'unallocated':
|
||||
return await manager.getUnallocatedPositions();
|
||||
case 'summary':
|
||||
|
||||
Reference in New Issue
Block a user