import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useEffect, useRef } from 'react' import axios from 'axios' import type { Quote, GeoNews, GeoRiskScore, PatternMatch, TradeIdea, EconomicEvent, HistoricalCandle, BacktestResult } from '../types' export const api = axios.create({ baseURL: '/api' }) // ── Market ──────────────────────────────────────────────────────────────────── export const useAllQuotes = () => useQuery>({ queryKey: ['quotes'], queryFn: () => api.get('/market/quotes').then(r => r.data), refetchInterval: 60_000, }) export const useHistory = (symbol: string, period = '1y', interval = '1d') => useQuery({ queryKey: ['history', symbol, period, interval], queryFn: () => api.get(`/market/history/${encodeURIComponent(symbol)}`, { params: { period, interval } }).then(r => r.data), enabled: !!symbol, }) // ── Geo ─────────────────────────────────────────────────────────────────────── export const useGeoNews = () => useQuery({ queryKey: ['geo-news'], queryFn: () => api.get('/geo/news').then(r => r.data), refetchInterval: 3_600_000, }) export const useGeoRiskScore = () => useQuery({ queryKey: ['geo-risk-score'], queryFn: () => api.get('/geo/risk-score').then(r => r.data), refetchInterval: 3_600_000, }) export const usePatternMatches = () => useQuery({ queryKey: ['pattern-matches'], queryFn: () => api.get('/geo/pattern-matches').then(r => r.data), }) export const usePatternRelevance = (days: number) => useQuery({ queryKey: ['pattern-relevance', days], queryFn: () => api.get('/geo/pattern-relevance', { params: { days } }).then(r => r.data), staleTime: 5 * 60_000, }) export const useTradeIdeas = () => useQuery({ queryKey: ['trade-ideas'], queryFn: () => api.get('/geo/trade-ideas').then(r => r.data), }) export const useCalendar = () => useQuery({ queryKey: ['calendar'], queryFn: () => api.get('/geo/calendar').then(r => r.data), }) // ── Options ─────────────────────────────────────────────────────────────────── export const useIvSurface = (symbol: string) => useQuery({ queryKey: ['iv-surface', symbol], queryFn: () => api.get('/options/iv-surface', { params: { symbol } }).then(r => r.data), enabled: !!symbol, }) export const usePnlCurve = (params: { symbol: string; strike: number; expiry_days: number option_type: string; quantity: number; premium_paid: number }) => useQuery({ queryKey: ['pnl-curve', params], queryFn: () => api.get('/options/pnl-curve', { params }).then(r => r.data), enabled: !!params.symbol && !!params.strike && !!params.premium_paid, }) // ── Backtest ────────────────────────────────────────────────────────────────── export const useBacktest = () => useMutation>({ mutationFn: (data) => api.post('/backtest/run', data).then(r => r.data), }) // ── AI ──────────────────────────────────────────────────────────────────────── export const useAiStatus = () => useQuery({ queryKey: ['ai-status'], queryFn: () => api.get('/ai/status').then(r => r.data), refetchInterval: 30_000, }) export const useAiTopIdeas = () => useQuery({ queryKey: ['ai-top-ideas'], queryFn: () => api.get('/ai/top-ideas').then(r => r.data), enabled: false, retry: false, }) export const useAnalyzeSpeech = () => useMutation({ mutationFn: (data: { text: string; speaker?: string }) => api.post('/ai/analyze-speech', data).then(r => r.data), }) export const useEvaluatePattern = () => useMutation({ mutationFn: (pattern: Record) => api.post('/ai/evaluate-pattern', { pattern }).then(r => r.data), }) export const useSuggestPattern = () => useMutation({ mutationFn: (context: string) => api.post('/ai/suggest-pattern', { context }).then(r => r.data), }) export const useAiTopIdeasRefetch = () => useMutation({ mutationFn: () => api.get('/ai/top-ideas').then(r => r.data), }) // ── Portfolio ───────────────────────────────────────────────────────────────── export const usePortfolioPositions = (status = 'open') => useQuery({ queryKey: ['portfolio', status], queryFn: () => api.get('/portfolio/positions', { params: { status } }).then(r => r.data), refetchInterval: status === 'open' ? 60_000 : false, }) export const usePortfolioSummary = () => useQuery({ queryKey: ['portfolio-summary'], queryFn: () => api.get('/portfolio/summary').then(r => r.data), refetchInterval: 60_000, }) export const usePnlHistory = () => useQuery({ queryKey: ['pnl-history'], queryFn: () => api.get('/portfolio/pnl-history').then(r => r.data), }) export const useAddPosition = () => { const qc = useQueryClient() return useMutation({ mutationFn: (data: Record) => api.post('/portfolio/add', data).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['portfolio'] }) qc.invalidateQueries({ queryKey: ['portfolio-summary'] }) }, }) } export const useClosePosition = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, close_value }: { id: string; close_value: number }) => api.post(`/portfolio/close/${id}`, { close_value }).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['portfolio'] }) qc.invalidateQueries({ queryKey: ['portfolio-summary'] }) qc.invalidateQueries({ queryKey: ['pnl-history'] }) }, }) } // ── Config ──────────────────────────────────────────────────────────────────── export const useConfig = () => useQuery({ queryKey: ['config'], queryFn: () => api.get('/config/').then(r => r.data), }) export const useSources = () => useQuery({ queryKey: ['sources'], queryFn: () => api.get('/config/sources').then(r => r.data), }) export const useUpdateSources = () => { const qc = useQueryClient() return useMutation({ mutationFn: (sources: Record) => api.put('/config/sources', { sources }).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['sources'] }), }) } export const useUpdateApiKeys = () => { const qc = useQueryClient() return useMutation({ mutationFn: (keys: Record) => api.put('/config/api-keys', keys).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['config'] }) qc.invalidateQueries({ queryKey: ['ai-status'] }) }, }) } // ── AI Pattern Scoring ──────────────────────────────────────────────────────── export const useScorePatterns = () => { const qc = useQueryClient() return useMutation({ mutationFn: (params: { top_n?: number; category_filter?: string; template?: string }) => api.post('/ai/score-patterns', params).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['last-scores'] }), }) } export const useLastScores = () => useQuery({ queryKey: ['last-scores'], queryFn: () => api.get('/ai/last-scores').then(r => r.data), staleTime: Infinity, }) export const useSuggestNewPatterns = () => useMutation({ mutationFn: () => api.post('/ai/suggest-new-patterns').then(r => r.data), }) export const useAnalysisConfig = () => useQuery({ queryKey: ['analysis-config'], queryFn: () => api.get('/config/analysis').then(r => r.data), }) export const useSaveAnalysisConfig = () => { const qc = useQueryClient() return useMutation({ mutationFn: (cfg: { top_n?: number; category_filter?: string; template?: string }) => api.put('/config/analysis', cfg).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['analysis-config'] }), }) } // ── Patterns ────────────────────────────────────────────────────────────────── export const useAllPatterns = () => useQuery({ queryKey: ['all-patterns'], queryFn: () => api.get('/patterns/all').then(r => r.data), }) export const usePatternSimilarity = () => useQuery({ queryKey: ['pattern-similarity'], queryFn: () => api.get('/ai/pattern-similarity').then(r => r.data), staleTime: 5 * 60_000, }) export const useMacroRegime = () => { const qc = useQueryClient() const query = useQuery({ queryKey: ['macro-regime'], queryFn: () => api.get('/market/macro-regime').then(r => r.data), staleTime: 10 * 60 * 1000, refetchInterval: 15 * 60 * 1000, }) const forceRefetch = async () => { const fresh = await api.get('/market/macro-regime?force=true').then(r => r.data) qc.setQueryData(['macro-regime'], fresh) return fresh } return { ...query, forceRefetch } } export const useSavePattern = () => { const qc = useQueryClient() return useMutation({ mutationFn: (pattern: Record) => api.post('/patterns/custom', pattern).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }), }) } export const useDeletePattern = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.delete(`/patterns/custom/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }), }) } export const useTogglePattern = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.put(`/patterns/toggle/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }), }) } // ── Auto-Cycle ─────────────────────────────────────────────────────────────── export const useCycleStatus = () => useQuery({ queryKey: ['cycle-status'], queryFn: () => api.get('/cycle/status').then(r => r.data), staleTime: 5_000, // Poll every 5s while a cycle is running, every 30s otherwise refetchInterval: (query) => ((query.state.data as any)?.running ? 5_000 : 30_000), }) export const useCycleHistory = (limit = 20) => useQuery({ queryKey: ['cycle-history', limit], queryFn: () => api.get(`/cycle/history?limit=${limit}`).then(r => r.data), staleTime: 60_000, }) export const useUpdateCycleConfig = () => { const qc = useQueryClient() return useMutation({ mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number }) => api.post('/cycle/config', cfg).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['cycle-status'] }), }) } /** Keys to refresh once a cycle finishes — covers cockpit, macro, journal */ export const CYCLE_REFRESH_KEYS = [ ['macro-regime'], ['last-scores'], ['all-patterns'], ['cycle-history'], ['cycle-status'], ['journal-summary'], ['journal-mtm'], ['journal-geo'], ['journal-macro'], ['geo-risk-score'], ['pattern-matches'], ] export const useTriggerCycle = () => { const qc = useQueryClient() return useMutation({ mutationFn: () => api.post('/cycle/trigger').then(r => r.data), onSuccess: () => { // Immediate status refresh so the spinner shows qc.invalidateQueries({ queryKey: ['cycle-status'] }) }, }) } /** * Mount this hook once at a high level (e.g. CyclesSection or App). * It watches cycle-status and, when a running cycle finishes, refreshes * all cockpit + journal queries so the UI reflects the new scores/regime. */ export const useCycleWatcher = () => { const qc = useQueryClient() const { data: statusData } = useCycleStatus() const wasRunning = useRef(false) useEffect(() => { const running = (statusData as any)?.running ?? false if (running) { wasRunning.current = true } else if (wasRunning.current) { // Transition: was running → now done → refresh everything wasRunning.current = false CYCLE_REFRESH_KEYS.forEach(key => qc.invalidateQueries({ queryKey: key })) } }, [(statusData as any)?.running]) } // ── Journal de Bord ────────────────────────────────────────────────────────── export const useJournalSummary = () => useQuery({ queryKey: ['journal-summary'], queryFn: () => api.get('/journal/summary').then(r => r.data), staleTime: 2 * 60_000, }) export const useMacroHistory = (days = 15) => useQuery({ queryKey: ['journal-macro', days], queryFn: () => api.get(`/journal/macro-history?days=${days}`).then(r => r.data), staleTime: 5 * 60_000, }) export const useGeoHistory = (days = 30) => useQuery({ queryKey: ['journal-geo', days], queryFn: () => api.get(`/journal/geo-history?days=${days}`).then(r => r.data), staleTime: 5 * 60_000, }) export const useTradeMtm = (days = 30) => useQuery({ queryKey: ['journal-mtm', days], queryFn: () => api.get(`/journal/trade-mtm?days=${days}`).then(r => r.data), staleTime: 0, refetchInterval: 5 * 60_000, // re-fetch live prices every 5 minutes refetchIntervalInBackground: false, }) // ── Risk Profiles ───────────────────────────────────────────────────────────── export const useRiskProfiles = () => useQuery({ queryKey: ['risk-profiles'], queryFn: () => api.get('/profiles').then(r => r.data), staleTime: 30_000, }) export const useUpsertProfile = () => { const qc = useQueryClient() return useMutation({ mutationFn: (profile: { id?: number; name: string; min_score: number; min_gain_pct: number; color?: string; enabled?: boolean; sort_order?: number }) => { if (profile.id) { return api.put(`/profiles/${profile.id}`, profile).then(r => r.data) } return api.post('/profiles', profile).then(r => r.data) }, onSuccess: () => qc.invalidateQueries({ queryKey: ['risk-profiles'] }), }) } export const useDeleteProfile = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: number) => api.delete(`/profiles/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['risk-profiles'] }), }) } export const usePreviewTradeScore = (score: number, gainPct: number, enabled = true) => useQuery({ queryKey: ['profile-preview', score, gainPct], queryFn: () => api.get(`/profiles/preview?score=${score}&gain_pct=${gainPct}`).then(r => r.data), enabled, staleTime: 0, }) // ── Reasoning Traces / Post-mortem ──────────────────────────────────────────── export const useTradePostmortem = (tradeId: number | null) => useQuery({ queryKey: ['postmortem', tradeId], queryFn: () => api.get(`/reasoning/postmortem/${tradeId}`).then(r => r.data), enabled: tradeId !== null, staleTime: 60_000, }) export const useAnalyzePostmortem = () => useMutation({ mutationFn: (tradeId: number) => api.post(`/reasoning/postmortem/${tradeId}/analyze`).then(r => r.data), }) export const usePortfolioReportData = (days: number) => useQuery({ queryKey: ['portfolio-report-data', days], queryFn: () => api.get(`/reasoning/portfolio-report?days=${days}`).then(r => r.data), staleTime: 120_000, }) export const useGeneratePortfolioReport = () => useMutation({ mutationFn: (days: number) => api.post(`/reasoning/portfolio-report/generate?days=${days}`).then(r => r.data), }) export const useAiReportsList = () => useQuery({ queryKey: ['ai-reports-list'], queryFn: () => api.get('/reasoning/reports').then(r => r.data), staleTime: 30_000, }) export const useAiReport = (reportId: number | null) => useQuery({ queryKey: ['ai-report', reportId], queryFn: () => api.get(`/reasoning/reports/${reportId}`).then(r => r.data), enabled: reportId !== null, staleTime: Infinity, }) // ── Super Contexte / Knowledge Base ────────────────────────────────────────── export const useKnowledgeState = () => useQuery({ queryKey: ['knowledge-state'], queryFn: () => api.get('/knowledge/state').then(r => r.data), staleTime: 5 * 60_000, }) export const useKnowledgeHistory = () => useQuery({ queryKey: ['knowledge-history'], queryFn: () => api.get('/knowledge/history').then(r => r.data), staleTime: 5 * 60_000, }) export const useKnowledgeStateVersion = (stateId: number | null) => useQuery({ queryKey: ['knowledge-state-version', stateId], queryFn: () => api.get(`/knowledge/history/${stateId}`).then(r => r.data), enabled: stateId !== null, staleTime: Infinity, }) export const useKnowledgeEntries = () => useQuery({ queryKey: ['knowledge-entries'], queryFn: () => api.get('/knowledge/entries').then(r => r.data), staleTime: 5 * 60_000, }) export const useSynthesizeKnowledge = () => { const qc = useQueryClient() return useMutation({ mutationFn: (force: boolean = false) => api.post(`/knowledge/synthesize${force ? '?force=true' : ''}`).then(r => r.data), onSuccess: (_data) => { if (!_data?.skipped) { qc.invalidateQueries({ queryKey: ['knowledge-state'] }) qc.invalidateQueries({ queryKey: ['knowledge-history'] }) qc.invalidateQueries({ queryKey: ['knowledge-entries'] }) } }, }) } export const usePatchKbEntryStatus = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, status }: { id: number; status: string }) => api.patch(`/knowledge/entries/${id}/status`, { status }).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['knowledge-entries'] }), }) } export const useDeleteAiReport = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: number) => api.delete(`/reasoning/reports/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['ai-reports-list'] }), }) } export const useDeleteReasoningState = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: number) => api.delete(`/knowledge/history/${id}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['knowledge-history'] }) qc.invalidateQueries({ queryKey: ['knowledge-state'] }) }, }) } export const useDeleteKbEntry = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: number) => api.delete(`/knowledge/entries/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['knowledge-entries'] }), }) } // ── Options Volatility ──────────────────────────────────────────────────────── export const useIvSnapshot = (ticker: string) => useQuery({ queryKey: ['iv-snapshot', ticker], queryFn: () => api.get(`/options-vol/snapshot/${encodeURIComponent(ticker)}`).then(r => r.data), enabled: !!ticker, staleTime: 60 * 60_000, // 1h — IV doesn't change that fast }) export const useIvWatchlist = () => useQuery({ queryKey: ['iv-watchlist'], queryFn: () => api.get('/options-vol/watchlist').then(r => r.data), staleTime: 60 * 60_000, }) export const useIvBatch = (tickers: string) => useQuery({ queryKey: ['iv-batch', tickers], queryFn: () => api.get('/options-vol/batch', { params: { tickers } }).then(r => r.data), enabled: !!tickers, staleTime: 60 * 60_000, }) export const useIvHistory = (ticker: string, days = 90) => useQuery({ queryKey: ['iv-history', ticker, days], queryFn: () => api.get(`/options-vol/history/${encodeURIComponent(ticker)}`, { params: { days } }).then(r => r.data), enabled: !!ticker, staleTime: 60 * 60_000, }) export const useIvForTrade = (underlying: string) => useQuery({ queryKey: ['iv-for-trade', underlying], queryFn: () => api.get(`/options-vol/for-trade/${encodeURIComponent(underlying)}`).then(r => r.data), enabled: !!underlying, staleTime: 60 * 60_000, }) // ── Analytics (Phase 2) ─────────────────────────────────────────────────────── export const usePatternReliability = () => useQuery({ queryKey: ['pattern-reliability'], queryFn: () => api.get('/analytics/reliability').then(r => r.data), staleTime: 10 * 60_000, }) export const useCalibration = (days = 365) => useQuery({ queryKey: ['calibration', days], queryFn: () => api.get('/analytics/calibration', { params: { days } }).then(r => r.data), staleTime: 10 * 60_000, })