from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from typing import Optional, List, Dict, Any from services.database import ( save_custom_pattern, get_custom_patterns, delete_custom_pattern, toggle_pattern_active ) from services.geo_analyzer import PATTERN_TAXONOMY router = APIRouter(prefix="/api/patterns", tags=["patterns"]) class PatternRequest(BaseModel): id: Optional[str] = None name: str description: str triggers: List[str] keywords: List[str] historical_instances: Optional[List[Dict[str, Any]]] = [] suggested_trades: Optional[List[Dict[str, Any]]] = [] asset_class: str expected_move_pct: float probability: float horizon_days: int ai_quality_score: Optional[int] = None ai_evaluation: Optional[Dict[str, Any]] = None source: Optional[str] = "custom" counter_thesis: Optional[str] = None invalidation_trigger: Optional[str] = None invalidation_probability: Optional[float] = None @router.get("/all") def list_all(): """Return all active patterns from DB (builtin + custom).""" return get_custom_patterns() @router.get("/builtin") def list_builtin(): return [p for p in get_custom_patterns() if p.get("source") == "builtin"] @router.get("/custom") def list_custom(): return [p for p in get_custom_patterns() if p.get("source") != "builtin"] @router.post("/custom") def create_pattern(req: PatternRequest): data = req.model_dump() data["source"] = "custom" pat_id = save_custom_pattern(data) return {"id": pat_id, "status": "saved"} @router.put("/custom/{pat_id}") def update_pattern(pat_id: str, req: PatternRequest): data = req.model_dump() data["id"] = pat_id save_custom_pattern(data) return {"id": pat_id, "status": "updated"} @router.delete("/custom/{pat_id}") def delete_pattern(pat_id: str): delete_custom_pattern(pat_id) return {"status": "deleted"} @router.put("/toggle/{pat_id}") def toggle_pattern(pat_id: str): """Enable or disable a pattern (works for builtin and custom).""" new_state = toggle_pattern_active(pat_id) return {"id": pat_id, "is_active": new_state} @router.get("/taxonomy") def get_taxonomy(): """Return the taxonomy tree + all patterns (frontend builds node→pattern mapping).""" patterns = get_custom_patterns() return {"tree": PATTERN_TAXONOMY, "patterns": patterns} @router.get("/by-instrument") def get_by_instrument(ticker: str = Query(..., description="Ticker / underlying to search")): """Return all patterns that have a suggested_trade matching the given ticker.""" ticker_lower = ticker.strip().lower() result = [] for p in get_custom_patterns(): trades = p.get("suggested_trades") or [] matching = [ t for t in trades if ticker_lower in (t.get("underlying") or "").lower() ] if matching: result.append({**p, "matching_trades": matching}) return result