From 991cb6768c7bfb9dfeb4efffb226eeca372744cb Mon Sep 17 00:00:00 2001 From: intfoo Date: Fri, 16 Oct 2026 14:06:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(strategy):=20=E7=AD=96=E7=95=A5=E5=8F=82?= =?UTF-8?q?=E6=95=B0=20UI=20=E5=85=83=E6=95=B0=E6=8D=AE=EF=BC=88=E7=BA=A7?= =?UTF-8?q?=E8=81=94=E6=98=BE=E9=9A=90=20/=20=E5=88=86=E7=BB=84=20/=20?= =?UTF-8?q?=E9=A2=84=E8=AE=BE=E6=89=B9=E9=87=8F=E5=BA=94=E7=94=A8=EF=BC=89?= =?UTF-8?q?=20(#447)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(strategy): 策略参数 UI 元数据(级联显隐 / 分组 / 预设批量应用) META["params"] 的 dict 项本就由后端 _normalize_param_item 原样透传额外 key,三个可选 UI 元数据字段零后端改动即可生效,未声明的参数行为不变: - visible_if: 级联显隐,支持 {param,in} 单条件与 all/any 递归组合; 隐藏仅是显示层过滤,参数值仍按当前值随请求提交(策略语义不受影响) - group: 相同组名的相邻参数在参数面板归并为一节(带节标题) - option_applies: select 选中某选项时批量应用一组参数值(如标的池预设) 实现: - 新增 lib/strategyParams.ts(递归条件求值 + 分组归并,含 vitest 单测) - 回测页参数面板:分节渲染 + 级联联动 + 命名组内横排布局 - 策略设置弹窗:同样分节 + 级联 + 预设应用 - paramSweep:级联隐藏参数不参与扫描(按持久化默认值判定显隐) - strategy-guide 双文档补元数据说明 * fix(strategy): 精简指南移除 UI 元数据注释行 — 修复 5000 字符上限测试 * fix(frontend): 参数扫描面板短标签加组名前缀 — paramDisplayLabel 分组内短标签(「止盈」「入池」)脱离分组上下文后不可区分: 扫描面板是 平铺列表, 同名参数挤在一起无法辨认, 级联隐藏提示行同样不可读。 新增 paramDisplayLabel (组名 · 标签), 用于扫描行与隐藏提示; 参数面板本身有组标题不受影响。 --- .../app/strategy/prompts/strategy-guide.md | 8 ++ .../screener/StrategySettingsDialog.tsx | 30 ++++- frontend/src/lib/api.ts | 15 +++ frontend/src/lib/strategyParams.test.ts | 105 ++++++++++++++++++ frontend/src/lib/strategyParams.ts | 68 ++++++++++++ .../src/pages/backtest/StrategyBacktest.tsx | 46 ++++++-- .../src/pages/backtest/StrategyOptimizer.tsx | 2 +- .../pages/backtest/StrategyWalkForward.tsx | 2 +- .../pages/backtest/components/paramSweep.tsx | 27 ++++- 9 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 frontend/src/lib/strategyParams.test.ts create mode 100644 frontend/src/lib/strategyParams.ts diff --git a/backend/app/strategy/prompts/strategy-guide.md b/backend/app/strategy/prompts/strategy-guide.md index 74d377d7..acbcc7d9 100644 --- a/backend/app/strategy/prompts/strategy-guide.md +++ b/backend/app/strategy/prompts/strategy-guide.md @@ -37,6 +37,14 @@ META = { # 策略参数 (只把用户可能调节的阈值放这里,公式常数不必参数化) # type 支持: float / int / bool / select(带 options) / date(格式 "YYYY-MM-DD") / string(自由文本) # float/int 可带 min/max/step;select 带 options: [{label, value}];date/string 的 default 是字符串 + # + # 可选 UI 元数据 (只影响参数面板展示, 不影响策略语义; 后端原样透传): + # "group": "组名" — 相同组名的相邻参数归并为一节显示 + # "visible_if": {...} — 级联显隐: {"param": "pid", "in": [值...]} 单条件, + # 或 {"all": [...]} / {"any": [...]} 递归组合; + # in 值类型应与目标参数一致 (bool 用 True/False 而非字符串); + # 隐藏只是不渲染, 参数值仍按当前值随请求提交 + # "option_applies": {选项: {pid: 值}} — select 选中某选项时批量应用一组参数值 (如预设) "params": [ ], diff --git a/frontend/src/components/screener/StrategySettingsDialog.tsx b/frontend/src/components/screener/StrategySettingsDialog.tsx index 23e19114..52a55bbc 100644 --- a/frontend/src/components/screener/StrategySettingsDialog.tsx +++ b/frontend/src/components/screener/StrategySettingsDialog.tsx @@ -5,6 +5,8 @@ import { X, Settings2, RotateCcw, Save, Filter, Star, TrendingUp, Sparkles, Down import { api, type StrategyDetail, type StrategyParamDef, type CompositeChildInfo, type ScoringDirection, type CustomSignalCondition } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { toPercentages, normalizeWeights } from '@/lib/weights' +import { groupParams, visibleParams } from '@/lib/strategyParams' +import { toast } from '@/components/Toast' import { BUILTIN_COLUMNS } from '@/lib/watchlist-columns' import { color } from '@/lib/colors' import { SignalPicker } from './SignalPicker' @@ -167,10 +169,12 @@ function RiskPctField({ label, value, onChange, min, max, step = 0.5, unit = '%' // 策略参数字段 // memo: 单参数修改只重渲本行 (父组件需传稳定回调, 否则 memo 失效) -const ParamField = memo(function ParamField({ def, value, onChange }: { +const ParamField = memo(function ParamField({ def, value, onChange, onApplyValues }: { def: StrategyParamDef value: any onChange: (pid: string, v: any) => void + /** select 带 option_applies 时, 选中某选项需批量应用一组参数值 (如标的池预设) */ + onApplyValues?: (values: Record) => void }) { if (def.type === 'bool') { const checked = value === true || value === 'true' || value === 'True' @@ -199,7 +203,15 @@ const ParamField = memo(function ParamField({ def, value, onChange }: { {def.label} onChange(param.id, e.target.value)} className={INPUT_CLS}> + @@ -2935,14 +2950,25 @@ export function StrategyBacktest({ loadCandidate, onLoadConsumed }: { actions={detail.params.length > 0 ? {detail.params.length} 项 : undefined} > {detail.params.length > 0 ? ( -
- {detail.params.map(param => ( - +
+ {groupParams(visibleParams(detail.params, strategyParams)).map((g, gi) => ( +
+ {g.name && ( +
{g.name}
+ )} + {/* 命名组横排(如「入池开关 + 止盈 + 锁定」同组一行);无名组回退网格 */} +
+ {g.items.map(param => ( + setStrategyParams(prev => ({ ...prev, ...values }))} + /> + ))} +
+
))}
) : ( diff --git a/frontend/src/pages/backtest/StrategyOptimizer.tsx b/frontend/src/pages/backtest/StrategyOptimizer.tsx index d89dc1da..8212fdcd 100644 --- a/frontend/src/pages/backtest/StrategyOptimizer.tsx +++ b/frontend/src/pages/backtest/StrategyOptimizer.tsx @@ -105,7 +105,7 @@ export function StrategyOptimizer() {
- + {task?.isPending ? ( diff --git a/frontend/src/pages/backtest/StrategyWalkForward.tsx b/frontend/src/pages/backtest/StrategyWalkForward.tsx index e6dde95c..a54b5c35 100644 --- a/frontend/src/pages/backtest/StrategyWalkForward.tsx +++ b/frontend/src/pages/backtest/StrategyWalkForward.tsx @@ -176,7 +176,7 @@ export function StrategyWalkForward() { - +
每折跑 {sweep.combos || 0} 组优化 × N 折,耗时较长
diff --git a/frontend/src/pages/backtest/components/paramSweep.tsx b/frontend/src/pages/backtest/components/paramSweep.tsx index f2c939d9..c2e3a736 100644 --- a/frontend/src/pages/backtest/components/paramSweep.tsx +++ b/frontend/src/pages/backtest/components/paramSweep.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react' import type { StrategyDetail, StrategyParamDef } from '@/lib/api' +import { paramDisplayLabel, visibleParams } from '@/lib/strategyParams' /** 参数扫描配置的共享逻辑与 UI — 优化器与 walk-forward 复用。 */ @@ -69,7 +70,11 @@ export function useParamSweep(strategies: StrategyDetail[], onStrategyChange?: ( const [sweeps, setSweeps] = useState>({}) const selected = strategies.find(s => s.id === strategyId) - const params = selected?.params ?? [] + // 级联隐藏参数不参与扫描: 扫描面板未扫描参数固定为默认值, 按默认值判定显隐 + const params = useMemo( + () => visibleParams(selected?.params ?? [], selected?.params_defaults ?? {}), + [selected], + ) const selectStrategy = (id: string) => { setStrategyId(id) @@ -112,7 +117,14 @@ export function useParamSweep(strategies: StrategyDetail[], onStrategyChange?: ( return grid } - return { strategyId, selected, selectStrategy, params, sweeps, updateSweep, combos, gridError, buildGrid } + // 因级联规则 (visible_if) 在默认配置下隐藏、不参与扫描的参数, 供 UI 提示 + // (paramDisplayLabel: 平铺列表无组上下文, 短标签需带组名才可区分) + const hiddenParamLabels = useMemo( + () => (selected?.params ?? []).filter(p => !params.includes(p)).map(p => paramDisplayLabel(p)), + [selected, params], + ) + + return { strategyId, selected, selectStrategy, params, sweeps, updateSweep, combos, gridError, buildGrid, hiddenParamLabels } } /** 策略选择器。 */ @@ -130,15 +142,22 @@ export function StrategySelect({ strategies, value, onChange }: { } /** 可扫参数列表 (勾选 + min/max/step)。 */ -export function SweepParamList({ params, sweeps, updateSweep }: { +export function SweepParamList({ params, sweeps, updateSweep, hiddenLabels = [] }: { params: StrategyParamDef[] sweeps: Record updateSweep: (pid: string, patch: Partial) => void + /** 因级联规则 (visible_if) 在默认配置下隐藏的参数标签, 仅作提示 */ + hiddenLabels?: string[] }) { if (!params.length) return null return (
扫描参数 (勾选后设范围)
+ {hiddenLabels.length > 0 && ( +
+ {hiddenLabels.length} 个参数因级联规则在默认配置下隐藏,不参与扫描:{hiddenLabels.join('、')} +
+ )}
{params.map(p => { const s = sweeps[p.id] ?? defaultSweep(p) @@ -159,7 +178,7 @@ export function SweepParamList({ params, sweeps, updateSweep }: {
{s.enabled && numeric && (