diff --git a/backend/routers/patterns.py b/backend/routers/patterns.py index 7915cf8..0c32a86 100644 --- a/backend/routers/patterns.py +++ b/backend/routers/patterns.py @@ -61,6 +61,34 @@ def update_pattern(pat_id: str, req: PatternRequest): return {"id": pat_id, "status": "updated"} +class PatternPatchRequest(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + category: Optional[str] = None + signal_direction: Optional[str] = None + regime_tag: Optional[str] = None + + +@router.patch("/custom/{pat_id}") +def patch_pattern(pat_id: str, req: PatternPatchRequest): + """Partial update — only provided fields are changed.""" + from services.database import get_conn + conn = get_conn() + if not conn.execute("SELECT id FROM custom_patterns WHERE id=?", (pat_id,)).fetchone(): + conn.close() + raise HTTPException(404, "Pattern not found") + updates = {k: v for k, v in req.model_dump().items() if v is not None} + if updates: + set_clause = ", ".join(f"{k}=?" for k in updates) + conn.execute( + f"UPDATE custom_patterns SET {set_clause}, updated_at=datetime('now') WHERE id=?", + list(updates.values()) + [pat_id], + ) + conn.commit() + conn.close() + return {"id": pat_id, "status": "updated", "fields": list(updates.keys())} + + @router.delete("/purge-all") def purge_all_patterns(): """Delete ALL custom/backtested patterns from the pattern library.""" diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index cdd2360..29bc71b 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -311,6 +311,21 @@ export const useDeletePattern = () => { }) } +export const usePatchPattern = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: (body: { + id: string + name?: string + description?: string + category?: string + signal_direction?: string + regime_tag?: string + }) => api.patch(`/patterns/custom/${body.id}`, body).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }), + }) +} + export const useTogglePattern = () => { const qc = useQueryClient() return useMutation({ diff --git a/frontend/src/pages/PatternExplorer.tsx b/frontend/src/pages/PatternExplorer.tsx index 5d4ab35..b1eb5c4 100644 --- a/frontend/src/pages/PatternExplorer.tsx +++ b/frontend/src/pages/PatternExplorer.tsx @@ -4,13 +4,14 @@ import clsx from 'clsx' import { TreePine, Search, ChevronRight, ChevronDown, TrendingUp, TrendingDown, Zap, BookOpen, ExternalLink, RefreshCw, - Layers, GitMerge, Tag, Trash2, + Layers, GitMerge, Tag, Trash2, Pencil, Check, X, } from 'lucide-react' import { useAllPatterns, usePatternsByInstrument, useLastScores, useDeletePattern, + usePatchPattern, validateTicker, } from '../hooks/useApi' import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments' @@ -140,59 +141,99 @@ function PatternCard({ pattern: Pattern highlightTrades?: string[] }) { - const [expanded, setExpanded] = useState(false) - const [confirmDel, setConfirmDel] = useState(false) - const { mutate: deletePattern, isPending: deleting } = useDeletePattern() + const [expanded, setExpanded] = useState(false) + const [confirmDel, setConfirmDel] = useState(false) + const [editing, setEditing] = useState(false) + const [editName, setEditName] = useState(pattern.name) + const [editDesc, setEditDesc] = useState(pattern.description) + const [editCat, setEditCat] = useState(pattern.category ?? '') + const [editDir, setEditDir] = useState(pattern.signal_direction ?? '') + const [editRegime, setEditRegime] = useState(pattern.regime_tag ?? '') + + const { mutate: deletePattern, isPending: deleting } = useDeletePattern() + const { mutate: patchPattern, isPending: saving } = usePatchPattern() const trades = pattern.suggested_trades ?? [] const instances = pattern.historical_instances ?? [] const assetLabel = ASSET_CLASS_LABELS[pattern.asset_class] ?? pattern.asset_class - const isDeletable = pattern.source !== 'builtin' + const isEditable = pattern.source !== 'builtin' const moveSign = pattern.expected_move_pct >= 0 ? '+' : '' + const startEdit = () => { + setEditName(pattern.name) + setEditDesc(pattern.description) + setEditCat(pattern.category ?? '') + setEditDir(pattern.signal_direction ?? '') + setEditRegime(pattern.regime_tag ?? '') + setEditing(true) + } + + const saveEdit = () => { + patchPattern({ + id: pattern.id, + name: editName.trim() || pattern.name, + description: editDesc.trim() || pattern.description, + category: editCat || undefined, + signal_direction: editDir || undefined, + regime_tag: editRegime.trim() || undefined, + }, { onSuccess: () => setEditing(false) }) + } + return ( -