Merge pull request #446 from intfoo/pr/strategy-param-string-type

feat(strategy): 支持 string 类型策略参数
This commit is contained in:
wshy
2026-10-04 00:24:53 +08:00
committed by GitHub
8 changed files with 49 additions and 7 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ _SYSTEM_PREFIX = """你是A股量化策略设计专家。根据用户描述的
4. polars 策略只 import polars 和 datetime;matrix_native 策略只允许 import numpy 以及 from app.backtest.matrix import 所需矩阵协议和算子
要求:
1. 用户可能调整的策略阈值通过 META["params"] 暴露,每项使用 id/label/type/default/min/max/step;公式常数、固定窗口边界、布尔开关不必强行参数化
1. 用户可能调整的策略阈值通过 META["params"] 暴露,每项使用 id/label/type/default/min/max/step(type 支持 float/int/bool/select/date/string);公式常数、固定窗口边界、布尔开关不必强行参数化
2. 遵循指南中的文件结构,但优先贴合用户规则,不要为了套模板歪曲策略含义
3. ENTRY_SIGNALS/EXIT_SIGNALS 根据策略逻辑自行选择匹配的信号列,不要照搬示例
4. scoring 权重根据策略核心逻辑定制,总和 = 1.0;键只能使用指南中的真实数值字段或受控虚拟评分字段 ma20_bias,不得创造条件名称作为评分列
@@ -35,7 +35,7 @@ META = {
"exclude_st": True,
"exclude_new_days": 30,
},
"params": [], # type: float/int/bool/select/date;float/int 带 min/max/step
"params": [], # type: float/int/bool/select/date/string;float/int 带 min/max/step
"scoring": {},
"order_by": "score",
"descending": True,
@@ -35,8 +35,8 @@ META = {
},
# 策略参数 (只把用户可能调节的阈值放这里,公式常数不必参数化)
# type 支持: float / int / bool / select(带 options) / date(格式 "YYYY-MM-DD")
# float/int 可带 min/max/step;select 带 options: [{label, value}];date 的 default 是字符串
# type 支持: float / int / bool / select(带 options) / date(格式 "YYYY-MM-DD") / string(自由文本)
# float/int 可带 min/max/step;select 带 options: [{label, value}];date/string 的 default 是字符串
"params": [
],
@@ -663,7 +663,9 @@ export function StrategyBuilderDialog({ open, onClose, onSavedId, mode = 'create
) : (
<>
<span className="text-[11px] font-mono text-foreground">{p.default}</span>
<span className="text-[10px] text-muted">{p.min} ~ {p.max}{p.type === 'float' || p.type === 'int' ? ' · 步长 ' + p.step : ''}</span>
{(p.type === 'float' || p.type === 'int') && (
<span className="text-[10px] text-muted">{p.min} ~ {p.max} · 步长 {p.step}</span>
)}
</>
)}
</div>
@@ -155,6 +155,20 @@ function ParamField({ def, value, onChange }: {
</div>
)
}
if (def.type === 'string') {
return (
<div className="flex items-center gap-2">
<span className="text-[11px] text-secondary w-16 shrink-0 text-right">{def.label}</span>
<input
type="text"
value={value ?? def.default ?? ''}
onChange={e => onChange(e.target.value)}
className="flex-1 min-w-0 px-1.5 py-0.5 rounded bg-base border border-border text-[11px] font-mono text-foreground focus:outline-none focus:border-accent/50"
/>
</div>
)
}
return (
<div className="flex items-center gap-2">
+1 -1
View File
@@ -759,7 +759,7 @@ export interface AuctionBenchmarkPayload {
export interface StrategyParamDef {
id: string
label: string
type: 'float' | 'int' | 'select' | 'bool'
type: 'float' | 'int' | 'select' | 'bool' | 'string'
default: number | string | boolean
min?: number
max?: number
@@ -747,6 +747,19 @@ function StrategyParamInput({ param, value, onChange }: {
</label>
)
}
if (param.type === 'string') {
return (
<label className="block">
<span className="mb-1 block text-[11px] text-secondary">{param.label}</span>
<input
type="text"
value={value ?? param.default ?? ''}
onChange={e => onChange(e.target.value)}
className={INPUT_CLS}
/>
</label>
)
}
return (
<label className="block">
<span className="mb-1 block text-[11px] text-secondary">{param.label}</span>
@@ -106,7 +106,8 @@ export function useParamSweep(strategies: StrategyDetail[], onStrategyChange?: (
if (!s?.enabled) continue
if (p.type === 'bool') grid[p.id] = [true, false]
else if (p.type === 'select') grid[p.id] = p.options ?? []
else grid[p.id] = { min: Number(s.min), max: Number(s.max), step: Number(s.step) }
else if (p.type === 'float' || p.type === 'int') grid[p.id] = { min: Number(s.min), max: Number(s.max), step: Number(s.step) }
// string 等类型不支持范围展开, 跳过不可扫描参数
}
return grid
}
@@ -142,6 +143,18 @@ export function SweepParamList({ params, sweeps, updateSweep }: {
{params.map(p => {
const s = sweeps[p.id] ?? defaultSweep(p)
const numeric = p.type === 'float' || p.type === 'int'
const sweepable = numeric || p.type === 'bool' || p.type === 'select'
if (!sweepable) {
return (
<div key={p.id} className="rounded-input border border-border/60 p-2">
<div className="flex items-center gap-2 text-xs">
<span className="font-medium text-muted">{p.label}</span>
<span className="text-muted">({p.type})</span>
<span className="text-[11px] text-muted">文本参数不可扫描</span>
</div>
</div>
)
}
return (
<div key={p.id} className="rounded-input border border-border/60 p-2">
<label className="flex items-center gap-2 text-xs">