import { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Plus, Trash2, Zap, Settings2, Lock } from 'lucide-react' import { api, type CustomSignal } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { BUILTIN_SIGNAL_DEFINITIONS, type SignalKind } from '@/lib/signals' import { CustomSignalDialog } from '@/components/signals/CustomSignalDialog' type SignalSection = 'builtin' | 'custom' const KIND_LABEL: Record = { entry: '买入', exit: '卖出', both: '买卖通用' } const KIND_CLASS: Record = { entry: 'bg-accent/10 text-accent', exit: 'bg-warning/10 text-warning', both: 'bg-muted/10 text-muted', } export function SettingsCustomSignalsPanel() { const qc = useQueryClient() const list = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList }) const options = useQuery({ queryKey: QK.customSignalsOptions, queryFn: api.customSignalsOptions }) const [activeSection, setActiveSection] = useState('builtin') const [showForm, setShowForm] = useState(false) const [editing, setEditing] = useState(null) const [confirmingDeleteId, setConfirmingDeleteId] = useState(null) const resetDeleteTimer = useRef | null>(null) const fields = options.data?.fields ?? [] const signals = list.data?.signals ?? [] const enabledCustomSignals = signals.filter(sig => sig.enabled).length const tabs = [ { key: 'builtin' as const, label: '内置信号', count: BUILTIN_SIGNAL_DEFINITIONS.length, hint: '系统提供,只读' }, { key: 'custom' as const, label: '自定义信号', count: signals.length, hint: `${enabledCustomSignals} 个已启用` }, ] useEffect(() => () => { if (resetDeleteTimer.current) clearTimeout(resetDeleteTimer.current) }, []) const clearDeleteConfirm = () => { if (resetDeleteTimer.current) clearTimeout(resetDeleteTimer.current) resetDeleteTimer.current = null setConfirmingDeleteId(null) } const openNew = () => { setEditing(null) clearDeleteConfirm() setActiveSection('custom') setShowForm(true) } const openEdit = (sig: CustomSignal) => { setEditing(sig) clearDeleteConfirm() setActiveSection('custom') setShowForm(true) } const closeForm = () => { setShowForm(false) setEditing(null) } const del = useMutation({ mutationFn: api.customSignalDelete, onSuccess: () => { clearDeleteConfirm() qc.invalidateQueries({ queryKey: QK.customSignals }) }, }) const toggleEnabled = (sig: CustomSignal) => { api.customSignalSave({ ...sig, enabled: !sig.enabled }).then(() => qc.invalidateQueries({ queryKey: QK.customSignals })) } const handleDeleteClick = (sig: CustomSignal) => { if (confirmingDeleteId === sig.id) { clearDeleteConfirm() del.mutate(sig.id) return } setConfirmingDeleteId(sig.id) if (resetDeleteTimer.current) clearTimeout(resetDeleteTimer.current) resetDeleteTimer.current = setTimeout(() => setConfirmingDeleteId(null), 3000) } return (
信号库

统一查看策略、回测与监控可用信号

内置信号由系统预计算,作为只读信号库展示;自定义信号可用「字段 + 运算符 + 值」组合条件创建,保存后可在策略、回测与监控中选择使用。

{tabs.map(tab => { const active = activeSection === tab.key return ( ) })}
{activeSection === 'builtin' && (

内置信号

只读

这些信号由系统在 enriched 数据中预计算,策略选择器会直接展示。

ID 前缀:signal_
{BUILTIN_SIGNAL_DEFINITIONS.map(sig => (

{sig.name}

{KIND_LABEL[sig.kind]}

{sig.id}

{sig.category}

{sig.description}

))}
)} {activeSection === 'custom' && (

自定义信号

可配置

这些信号由你定义,可启用/停用,并在策略、回测与监控中作为 csg_* 信号使用。

{signals.map(sig => (

{sig.name}

{KIND_LABEL[sig.kind]} {!sig.enabled && 已停用}

csg_{sig.id}

{confirmingDeleteId === sig.id ? ( ) : ( )}
{sig.conditions.map((c, i) => (
{i === 0 ? '当' : '且'} {fieldLabel(c.left, fields)} {c.op} {rightDisplay(c.right, fields)}
))}
))} {signals.length === 0 && (
暂无自定义信号,点击右上角「新建自定义信号」。
)}
)}
) } function StatCard({ label, value, hint }: { label: string; value: number; hint: string }) { return (
{label}
{value}
{hint}
) } function fieldLabel(key: string, fields: { key: string; label: string }[]): string { return fields.find(f => f.key === key)?.label ?? key } function rightDisplay(right: string, fields: { key: string; label: string }[]): string { if (right.startsWith('field:')) return fieldLabel(right.slice(6), fields) return right }