mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
feat(ext-data): 扩展表字段接入信号与因子(数值因子/评分 + string 归属筛选)
数值字段(int/float) → 信号+因子双通道: - ext_factors: 帧组装时 join 扩展列并注册 kind=base 因子(分组「扩展数据」), 时序模式按 (symbol,交易日) 精确对齐无未来函数, 快照模式仅当日单日帧 注入(历史帧跳过防未来函数) - registry.all_factors 惰性同步(配置目录签名幂等, 以注册表为权威增删); custom_signals.allowed_fields 自动并入 → 信号下拉/因子库/AI提示词/检验 同一份清单; factor 补算入口按需注入 - 失效链: 上传/拉取/配置变更自动清扩展帧缓存+策略缓存, API层补 repo.clear_cache; 写入后下一次计算立即生效 - 列名保留中文(预设表字段名), 非ASCII数值字段只进信号不注册因子 (DSL标识符ASCII-only) string 字段(概念/行业归属) → 仅信号条件通道: - 运算符 包含(contains,字面量匹配非正则)/等于/不等于, 右值为字符串字面量, 可与数值条件混合(强势板块归属 AND 热度阈值) - 前端信号编辑器按字段类型切换运算符与右值输入; /options 暴露 stringFields; AI 提示词含字符串字段清单与 contains 用法 - string 不注册为因子(数值口径), 空值不误报 测试: test_ext_factors 18个(PIT对齐/跨日不泄露/快照门控/写入失效/ contains字面量/中文列名端到端等); 存量因子计数测试补 data/ 运行时隔离 夹具(黄金断言不依赖本机扩展表); 受影响回归148个全过; pnpm build 通过; ruff 对齐 main 基线
This commit is contained in:
@@ -15,6 +15,9 @@ interface Props {
|
||||
onSaved?: (signal: CustomSignal) => void
|
||||
}
|
||||
|
||||
// 字符串运算符的中文标签 (数值运算符直接显示符号)
|
||||
const OP_LABELS: Record<string, string> = { contains: '包含' }
|
||||
|
||||
const emptySignal = (kind: CustomSignal['kind'] = 'exit'): CustomSignal => ({
|
||||
id: '', name: '', kind, enabled: true,
|
||||
conditions: [{ left: 'close', op: '>', right: 'field:ma20', leftDays: 0, rightDays: 0 }],
|
||||
@@ -40,6 +43,8 @@ export function CustomSignalDialog({ open, signal, defaultKind = 'exit', onClose
|
||||
const groups = options.data?.groups
|
||||
const maxDays = options.data?.maxDays ?? 60
|
||||
const operators = options.data?.operators ?? ['>', '>=', '<', '<=', '==', '!=']
|
||||
const stringFields = options.data?.stringFields ?? []
|
||||
const stringOperators = options.data?.stringOperators ?? ['contains', '==', '!=']
|
||||
const editing = !!signal
|
||||
|
||||
useEffect(() => {
|
||||
@@ -99,6 +104,20 @@ export function CustomSignalDialog({ open, signal, defaultKind = 'exit', onClose
|
||||
|
||||
const updateCond = (idx: number, patch: Partial<CustomSignalCondition>) =>
|
||||
setDraft(d => ({ ...d, conditions: d.conditions.map((c, i) => i === idx ? { ...c, ...patch } : c) }))
|
||||
// 切换左字段时若跨越 数值↔字符串 类型, 运算符/右值语义不再成立, 一并复位
|
||||
const changeLeft = (idx: number, left: string) =>
|
||||
setDraft(d => ({
|
||||
...d,
|
||||
conditions: d.conditions.map((c, i) => {
|
||||
if (i !== idx) return c
|
||||
const wasStr = stringFields.includes(c.left)
|
||||
const isStr = stringFields.includes(left)
|
||||
if (wasStr === isStr) return { ...c, left }
|
||||
return isStr
|
||||
? { ...c, left, op: 'contains', right: '', rightDays: 0 }
|
||||
: { ...c, left, op: '>', right: '0', rightDays: 0 }
|
||||
}),
|
||||
}))
|
||||
const addCond = () => setDraft(d => ({ ...d, conditions: [...d.conditions, { left: 'close', op: '>', right: '0', leftDays: 0, rightDays: 0 }] }))
|
||||
const removeCond = (idx: number) => setDraft(d => ({ ...d, conditions: d.conditions.filter((_, i) => i !== idx) }))
|
||||
|
||||
@@ -174,23 +193,32 @@ export function CustomSignalDialog({ open, signal, defaultKind = 'exit', onClose
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 rounded-card border border-border/70 bg-base/50 p-3">
|
||||
{draft.conditions.map((c, i) => (
|
||||
{draft.conditions.map((c, i) => {
|
||||
const isStr = stringFields.includes(c.left)
|
||||
const condOps = isStr ? stringOperators : operators
|
||||
return (
|
||||
<div key={i} className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted/60 w-5 text-right shrink-0">{i === 0 ? '当' : '且'}</span>
|
||||
|
||||
{/* 左操作数: 前N日 + 字段(弹出选择) */}
|
||||
<DaysInput value={c.leftDays ?? 0} max={maxDays} onChange={v => updateCond(i, { leftDays: v })} />
|
||||
<FieldPicker value={c.left} fields={fields} groups={groups} onChange={v => updateCond(i, { left: v })} />
|
||||
<FieldPicker value={c.left} fields={fields} groups={groups} onChange={v => changeLeft(i, v)} />
|
||||
|
||||
{/* 运算符 */}
|
||||
{/* 运算符: 字符串字段为 包含/等于/不等于 */}
|
||||
<select value={c.op} onChange={e => updateCond(i, { op: e.target.value })} className="w-11 h-7 px-0.5 rounded bg-base border border-border text-[11px] font-mono text-foreground text-center focus:outline-none focus:border-accent/50">
|
||||
{operators.map(op => <option key={op} value={op}>{op}</option>)}
|
||||
{condOps.map(op => <option key={op} value={op}>{OP_LABELS[op] ?? op}</option>)}
|
||||
</select>
|
||||
|
||||
{/* 右操作数: 前N日(仅字段) + 字段/常量(弹出选择) */}
|
||||
<RightValueInput cond={c} fields={fields} groups={groups} maxDays={maxDays}
|
||||
onChangeRight={v => updateCond(i, { right: v })}
|
||||
onChangeDays={v => updateCond(i, { rightDays: v })} />
|
||||
{/* 右操作数: 字符串字段为文本字面量; 其余为 前N日(仅字段) + 字段/常量(弹出选择) */}
|
||||
{isStr ? (
|
||||
<input type="text" value={c.right} onChange={e => updateCond(i, { right: e.target.value })}
|
||||
placeholder="概念/行业名, 如 AI" maxLength={64}
|
||||
className="flex-1 min-w-0 h-7 px-1.5 rounded bg-base border border-border text-[11px] text-foreground focus:outline-none focus:border-accent/50" />
|
||||
) : (
|
||||
<RightValueInput cond={c} fields={fields} groups={groups} maxDays={maxDays}
|
||||
onChangeRight={v => updateCond(i, { right: v })}
|
||||
onChangeDays={v => updateCond(i, { rightDays: v })} />
|
||||
)}
|
||||
|
||||
{draft.conditions.length > 1 && (
|
||||
<button onClick={() => removeCond(i)} className="p-1 rounded text-muted hover:text-danger hover:bg-danger/10 cursor-pointer">
|
||||
@@ -198,7 +226,8 @@ export function CustomSignalDialog({ open, signal, defaultKind = 'exit', onClose
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{aiOpen && (
|
||||
<div className="rounded-card border border-amber-400/30 bg-amber-400/5 p-3 space-y-2">
|
||||
|
||||
@@ -794,6 +794,9 @@ export interface CustomSignalOptions {
|
||||
groups?: CustomSignalFieldGroup[]
|
||||
maxDays?: number
|
||||
operators: string[]
|
||||
/** string 扩展字段 (概念/行业归属): 这些字段用 stringOperators + 字符串右值 */
|
||||
stringFields?: string[]
|
||||
stringOperators?: string[]
|
||||
kinds: { key: string; label: string }[]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user