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 type TickerValidation = { valid: boolean; symbol: string; name?: string; price?: number; change_pct?: number; reason?: string } export const validateTicker = (symbol: string): Promise => api.get('/market/validate', { params: { symbol } }).then(r => r.data) export const useMarketCustomTickers = () => useQuery<{ ticker: string; name: string; added_at: string }[]>({ queryKey: ['market-custom-tickers'], queryFn: () => api.get('/market/custom-tickers').then(r => r.data), }) export const useAddMarketTicker = () => { const qc = useQueryClient() return useMutation({ mutationFn: (ticker: string) => api.post(`/market/custom-tickers/${encodeURIComponent(ticker)}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['market-custom-tickers'] }) qc.invalidateQueries({ queryKey: ['quotes'] }) }, }) } export const useRemoveMarketTicker = () => { const qc = useQueryClient() return useMutation({ mutationFn: (ticker: string) => api.delete(`/market/custom-tickers/${encodeURIComponent(ticker)}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['market-custom-tickers'] }) qc.invalidateQueries({ queryKey: ['quotes'] }) }, }) } 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), }) // Real Forex Factory–backed calendar (used by CalendarPage.tsx and InstrumentModels) export interface FFEvent { event_date: string; event_time: string | null; currency: string impact: 'high' | 'medium' | 'low'; event_name: string actual_value: number | null; forecast_value: number | null; previous_value: number | null } export const useEcoCalendar = (params: { period?: string; limit?: number; impacts?: string } = {}) => useQuery<{ events: FFEvent[] }>({ queryKey: ['eco-calendar', params], queryFn: () => api.get('/eco/calendar', { params: { period: 'recent', limit: 50, impacts: 'high,medium', ...params } }).then(r => r.data), staleTime: 5 * 60_000, refetchInterval: 5 * 60_000, }) // ── Instruments Watchlist (Dashboard "radar" card) ──────────────────────────── export interface WatchlistItem { ticker: string; name: string; asset_class: string; sort_order: number; added_at: string } export interface WatchlistQuoteItem extends WatchlistItem { price: number | null; change_pct: number | null } export const useInstrumentsWatchlist = () => useQuery({ queryKey: ['instruments-watchlist'], queryFn: () => api.get('/watchlist/').then(r => r.data), staleTime: 60_000, }) export const useInstrumentsWatchlistQuotes = () => useQuery<{ items: WatchlistQuoteItem[] }>({ queryKey: ['instruments-watchlist-quotes'], queryFn: () => api.get('/watchlist/quotes').then(r => r.data), refetchInterval: 60_000, staleTime: 30_000, }) export const useAddWatchlistInstrument = () => { const qc = useQueryClient() return useMutation({ mutationFn: (ticker: string) => api.post(`/watchlist/${encodeURIComponent(ticker)}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['instruments-watchlist'] }) qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] }) }, }) } export const useRemoveWatchlistInstrument = () => { const qc = useQueryClient() return useMutation({ mutationFn: (ticker: string) => api.delete(`/watchlist/${encodeURIComponent(ticker)}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['instruments-watchlist'] }) qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] }) }, }) } // Latest cycle report — shares the 'cycle-report-latest' queryKey with RapportIA.tsx export const useLatestCycleReport = () => useQuery({ queryKey: ['cycle-report-latest'], queryFn: () => api.get('/reports/cycle/latest').then(r => r.data), staleTime: 60_000, }) // ── 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 usePatternTaxonomy = () => useQuery({ queryKey: ['pattern-taxonomy'], queryFn: () => api.get('/patterns/taxonomy').then(r => r.data), staleTime: 5 * 60_000, }) export const usePatternsByInstrument = (ticker: string) => useQuery({ queryKey: ['patterns-by-instrument', ticker], queryFn: () => api.get('/patterns/by-instrument', { params: { ticker } }).then(r => r.data), enabled: !!ticker, staleTime: 2 * 60_000, }) 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 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({ mutationFn: (id: string) => api.put(`/patterns/toggle/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }), }) } export interface PatternCalibration { pattern_id: string pattern_name: string asset_class: string ai_estimate: number | null observed_avg_win_pct: number | null calibrated_expected_move: number | null calibration_weight: number calibration_weight_pct: number n_mature_trades: number bayes_win_rate: number source: 'pure_ai' | 'early' | 'mixed' | 'data_driven' } export const usePatternCalibration = () => useQuery({ queryKey: ['pattern-calibration'], queryFn: () => api.get('/patterns/calibration').then(r => r.data as PatternCalibration[]), staleTime: 60_000, }) export interface SimilarityResult { recommendation: 'merge_as_instance' | 'counter_scenario' | 'new_pattern' match_id: string | null match_name: string | null confidence: number reasoning: string suggested_regime_tag: string } export const useFindSimilarPattern = () => useMutation({ mutationFn: (pattern_id: string) => api.post('/patterns/find-similar', { pattern_id }).then(r => r.data as SimilarityResult), }) export const useMergePatterns = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { keep_id: string; discard_id: string }) => api.post('/patterns/merge', body).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['all-patterns'] }) qc.invalidateQueries({ queryKey: ['last-scores'] }) }, }) } // ── 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 interface CycleStepParam { type: 'bool' | 'int' | 'float'; label?: string; default: unknown; min?: number; max?: number } export interface CycleStepDef { id: string; label: string; description: string; group: string; params: Record } export const useCycleStepCatalog = () => useQuery({ queryKey: ['cycle-step-catalog'], queryFn: () => api.get('/cycle/step-catalog').then(r => r.data.steps as CycleStepDef[]), staleTime: Infinity, }) 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; trade_budget_eur?: number; preferred_horizon_min?: number; preferred_horizon_max?: number; journal_retention_days?: number; maturity_threshold_pct?: number; weekend_cycle_enabled?: boolean; weekend_cycle_times?: string; cycle_step_config?: Record> }) => 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'], ['journal-portfolio-risk'], ] 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, refetchIntervalInBackground: false, }) export const useClosedTrades = (days = 180) => useQuery({ queryKey: ['journal-closed', days], queryFn: () => api.get(`/journal/closed-trades?days=${days}`).then(r => r.data), staleTime: 30_000, }) export const useExitDefaults = () => useQuery({ queryKey: ['journal-exit-defaults'], queryFn: () => api.get('/journal/exit-defaults').then(r => r.data), staleTime: 60_000, }) export const useCloseTrade = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, body }: { id: number; body: { close_price: number; pnl_realized?: number; close_reason: string; close_note: string }}) => api.patch(`/journal/trades/${id}/close`, body).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['journal-mtm'] }) qc.invalidateQueries({ queryKey: ['journal-closed'] }) qc.invalidateQueries({ queryKey: ['journal-summary'] }) }, }) } export const useDeleteTrade = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: number) => api.delete(`/journal/trades/${id}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['journal-mtm'] }) qc.invalidateQueries({ queryKey: ['journal-closed'] }) qc.invalidateQueries({ queryKey: ['journal-summary'] }) }, }) } export const useUpdateExitParams = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, body }: { id: number; body: { target_pct?: number; stop_loss_pct?: number; signal_threshold?: number }}) => api.patch(`/journal/trades/${id}/exit-params`, body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['journal-mtm'] }), }) } export const useSaveExitDefaults = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { target_pct?: number; stop_loss_pct?: number; signal_reversal_mode?: string; signal_reversal_threshold?: number }) => api.put('/journal/exit-defaults', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['journal-exit-defaults'] }), }) } export const useOptionsGate = () => useQuery({ queryKey: ['options-gate-config'], queryFn: () => api.get('/config/options-gate').then(r => r.data), staleTime: 60_000, }) export const useSaveOptionsGate = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { iv_gate_enabled?: boolean iv_gate_ivr_high?: number iv_gate_ivr_extreme?: number iv_gate_skew_threshold?: number }) => api.put('/config/options-gate', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['options-gate-config'] }), }) } export const useTechIndicatorsConfig = () => useQuery({ queryKey: ['tech-indicators-config'], queryFn: () => api.get('/config/tech-indicators').then(r => r.data), staleTime: 60_000, }) export const useSaveTechIndicatorsConfig = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { tech_indicators_enabled?: boolean tech_indicators_list?: string tech_indicators_auto_calibrate?: boolean }) => api.put('/config/tech-indicators', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['tech-indicators-config'] }), }) } export const useSimPortfolioRisk = () => useQuery({ queryKey: ['journal-portfolio-risk'], queryFn: () => api.get('/journal/portfolio-risk').then(r => r.data), staleTime: 30_000, }) export const useTradeCheck = () => useMutation({ mutationFn: (body: { underlying: string; strategy: string; asset_class: string }) => api.post('/journal/trade-check', body).then(r => r.data), }) export const useSkippedTrades = (days = 30) => useQuery({ queryKey: ['journal-skipped', days], queryFn: () => api.get(`/journal/skipped-trades?days=${days}`).then(r => r.data), staleTime: 60_000, }) // ── 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 4 — Bayesian + Clustering + Embeddings ─────────────────── export const useBayesianPosteriors = () => useQuery({ queryKey: ['bayesian-posteriors'], queryFn: () => api.get('/analytics/bayesian').then(r => r.data), staleTime: 5 * 60_000, }) export const useRegimeClusters = (days = 90) => useQuery({ queryKey: ['regime-clusters', days], queryFn: () => api.get('/analytics/regime-clusters', { params: { days } }).then(r => r.data), staleTime: 5 * 60_000, }) export const useRegimeTransitions = (days = 180) => useQuery({ queryKey: ['regime-transitions', days], queryFn: () => api.get('/analytics/regime-transitions', { params: { days } }).then(r => r.data), staleTime: 10 * 60_000, }) export const usePatternEmbeddings = () => useQuery({ queryKey: ['pattern-embeddings'], queryFn: () => api.get('/analytics/embeddings').then(r => r.data), staleTime: 10 * 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, }) // ── Risk Engine (Phase 3) ───────────────────────────────────────────────────── export const useRiskExposure = () => useQuery({ queryKey: ['risk-exposure'], queryFn: () => api.get('/risk/exposure').then(r => r.data), staleTime: 2 * 60_000, }) export const usePnlTimeline = (days = 90) => useQuery({ queryKey: ['pnl-timeline', days], queryFn: () => api.get('/risk/timeline', { params: { days } }).then(r => r.data), staleTime: 5 * 60_000, }) export const useRiskClusters = () => useQuery({ queryKey: ['risk-clusters'], queryFn: () => api.get('/risk/clusters').then(r => r.data), staleTime: 2 * 60_000, }) export const usePatternCorrelations = () => useQuery({ queryKey: ['pattern-correlations'], queryFn: () => api.get('/risk/correlations').then(r => r.data), staleTime: 10 * 60_000, }) export const useKellySizing = (patternId: string, capital = 10000) => useQuery({ queryKey: ['kelly', patternId, capital], queryFn: () => api.get(`/risk/kelly/${encodeURIComponent(patternId)}`, { params: { capital } }).then(r => r.data), enabled: !!patternId, staleTime: 5 * 60_000, }) export const useRiskDashboard = () => useQuery({ queryKey: ['risk-dashboard'], queryFn: () => api.get('/risk/dashboard').then(r => r.data), staleTime: 2 * 60_000, refetchInterval: 5 * 60_000, }) // ── System Logs ─────────────────────────────────────────────────────────────── export interface LogFilters { level?: string source?: string cycle_id?: string ticker?: string date_from?: string date_to?: string limit?: number } export const useSystemLogs = (filters: LogFilters = {}) => useQuery({ queryKey: ['system-logs', filters], queryFn: () => api.get('/logs', { params: filters }).then(r => r.data), staleTime: 30_000, refetchInterval: 30_000, }) export const useLogSources = () => useQuery({ queryKey: ['log-sources'], queryFn: () => api.get('/logs/sources').then(r => r.data), staleTime: 60_000, }) // ── Wavelets — latest per-ticker signal from the watchlist scan (auto-cycle) ── export const useWaveletWatchlistSignals = () => useQuery({ queryKey: ['wavelet-watchlist-signals'], queryFn: () => api.get('/wavelet/watchlist-signals').then(r => r.data as { signals: any[] }), staleTime: 5 * 60_000, }) export const useLogCycles = () => useQuery({ queryKey: ['log-cycles'], queryFn: () => api.get('/logs/cycles').then(r => r.data), staleTime: 60_000, }) export const useClearLogs = () => useMutation({ mutationFn: (days: number) => api.delete('/logs/clear', { params: { older_than_days: days } }).then(r => r.data), }) export const useCycleContextSnapshots = (limit = 30) => useQuery({ queryKey: ['cycle-context-snapshots'], queryFn: () => api.get('/cycle/contexts', { params: { limit } }).then(r => r.data), staleTime: 30_000, }) export const useCycleContextSnapshot = (runId: string | null) => useQuery({ queryKey: ['cycle-context-snapshot', runId], queryFn: () => api.get(`/cycle/contexts/${runId}`).then(r => r.data), enabled: !!runId, staleTime: 300_000, }) export const useReplayCycle = () => useMutation({ mutationFn: ({ runId, notes }: { runId: string; notes?: string }) => api.post(`/cycle/contexts/${runId}/replay`, { override_notes: notes ?? null }).then(r => r.data), }) export const useAiCallLogs = (runId: string | null) => useQuery({ queryKey: ['ai-call-logs', runId], queryFn: () => api.get(`/cycle/ai-calls/${runId}`).then(r => r.data), enabled: !!runId, staleTime: 60_000, }) // ── IV Watchlist Management ─────────────────────────────────────────────────── export const useWatchlistTickers = () => useQuery({ queryKey: ['watchlist-tickers'], queryFn: () => api.get('/options-vol/watchlist-tickers').then(r => r.data), staleTime: 60_000, }) export const useAddWatchlistTicker = () => useMutation({ mutationFn: (ticker: string) => api.post(`/options-vol/watchlist-tickers/${encodeURIComponent(ticker)}`).then(r => r.data), }) export const useRemoveWatchlistTicker = () => useMutation({ mutationFn: (ticker: string) => api.delete(`/options-vol/watchlist-tickers/${encodeURIComponent(ticker)}`).then(r => r.data), }) // ── Institutional Reports ───────────────────────────────────────────────────── export const useInstitutionalReports = (params: { report_type?: string category?: string importance?: number days?: number }) => useQuery({ queryKey: ['institutional-reports', params], queryFn: () => api.get('/institutional/reports', { params }).then(r => r.data), refetchInterval: 3_600_000, }) export const useInstitutionalStats = () => useQuery({ queryKey: ['institutional-stats'], queryFn: () => api.get('/institutional/stats').then(r => r.data), staleTime: 300_000, }) export const useRefreshInstitutionalReports = () => useMutation({ mutationFn: (report_type?: string) => api.post('/institutional/refresh', null, { params: report_type ? { report_type } : {} }).then(r => r.data), }) // ── Pattern Lab ─────────────────────────────────────────────────────────────── export const usePatternLabRuns = () => useQuery({ queryKey: ['pattern-lab-runs'], queryFn: () => api.get('/pattern-lab/runs').then(r => r.data), staleTime: 30_000, }) export const useRunPatternLab = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { preset_id?: string; theme: string; analysis_date: string; horizon_days: number; assets: string[]; theme_hint: string; }) => api.post('/pattern-lab/run', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['pattern-lab-runs'] }), }) } export const useEvaluatePatternLab = () => { const qc = useQueryClient() return useMutation({ mutationFn: (run_id: string) => api.post(`/pattern-lab/evaluate/${run_id}`, {}).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['pattern-lab-runs'] }), }) } export const useSaveLabPattern = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { run_id: string; pattern_index: number; name?: string; category?: string; signal_direction?: string; asset_class?: string; action?: 'new' | 'instance' | 'counter'; target_id?: string; regime_tag?: string; event_name?: string; }) => api.post('/pattern-lab/save-pattern', body).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['all-patterns'] }) qc.invalidateQueries({ queryKey: ['pattern-lab-runs'] }) }, }) } export type MatchResult = { recommendation: 'merge_as_instance' | 'counter_scenario' | 'new_pattern' match_id?: string match_name?: string confidence: number reasoning: string suggested_regime_tag?: string } export const useFindMatchingPattern = () => useMutation({ mutationFn: (body: { run_id: string; pattern_index: number }): Promise => api.post('/pattern-lab/find-matching', body).then(r => r.data), }) export type DiscoveredEvent = { label: string; date: string; category: string; horizon_days: number assets: string[]; hint: string; confidence: number } export const useDiscoverEvents = () => useMutation({ mutationFn: (body: { query: string; n?: number }): Promise<{ events: DiscoveredEvent[]; source: string; query: string }> => api.post('/pattern-lab/discover', body).then(r => r.data), }) export const useDeleteLabRun = () => { const qc = useQueryClient() return useMutation({ mutationFn: (run_id: string) => api.delete(`/pattern-lab/runs/${run_id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['pattern-lab-runs'] }), }) } export const useInstrumentScan = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { ticker: string; instrument_name: string; start_date: string; end_date: string; horizon_days: number; }) => api.post('/pattern-lab/instrument-scan', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['pattern-lab-runs'] }), }) } export const useEvaluateInstrumentScan = () => { const qc = useQueryClient() return useMutation({ mutationFn: (run_id: string) => api.post(`/pattern-lab/evaluate-instrument/${run_id}`, {}).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['pattern-lab-runs'] }), }) } // ── Specialist Desks ─────────────────────────────────────────────────────────── export type MacroSensitivity = { regime: string; effect: string } export type PriceDeltaThresholds = { significant_day: number; extreme_day: number significant_week: number; extreme_week: number } export type SpecialistReport = { id: string; name: string; source: string; cadence: string next_date: string | null; last_date: string | null importance: number; notes: string; url: string desks?: string[] created_at: string; updated_at: string } export type DeskConfig = { asset_class: string; display_name: string; icon: string fundamentals: string; macro_sensitivity: MacroSensitivity[] price_delta_thresholds: PriceDeltaThresholds; notes: string reports: SpecialistReport[]; updated_at: string } export const useAllDeskConfigs = () => useQuery({ queryKey: ['desk-configs'], queryFn: () => api.get('/specialist-desks/configs').then(r => r.data), staleTime: 60_000, }) export const useUpdateDeskConfig = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ asset_class, body }: { asset_class: string; body: Partial }) => api.put(`/specialist-desks/configs/${asset_class}`, body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['desk-configs'] }), }) } export const useAllSpecialistReports = () => useQuery({ queryKey: ['specialist-reports'], queryFn: () => api.get('/specialist-desks/reports').then(r => r.data), staleTime: 60_000, }) export const useCreateSpecialistReport = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: Partial & { desks?: string[] }) => api.post('/specialist-desks/reports', body).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['specialist-reports'] }) qc.invalidateQueries({ queryKey: ['desk-configs'] }) }, }) } export const useUpdateSpecialistReport = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, body }: { id: string; body: Partial & { desks?: string[] } }) => api.put(`/specialist-desks/reports/${id}`, body).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['specialist-reports'] }) qc.invalidateQueries({ queryKey: ['desk-configs'] }) }, }) } export const useDeleteSpecialistReport = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.delete(`/specialist-desks/reports/${id}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['specialist-reports'] }) qc.invalidateQueries({ queryKey: ['desk-configs'] }) }, }) } export const useLinkReportDesk = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ report_id, asset_class }: { report_id: string; asset_class: string }) => api.post(`/specialist-desks/reports/${report_id}/link/${asset_class}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['specialist-reports'] }) qc.invalidateQueries({ queryKey: ['desk-configs'] }) }, }) } export const useUnlinkReportDesk = () => { const qc = useQueryClient() return useMutation({ mutationFn: ({ report_id, asset_class }: { report_id: string; asset_class: string }) => api.delete(`/specialist-desks/reports/${report_id}/link/${asset_class}`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['specialist-reports'] }) qc.invalidateQueries({ queryKey: ['desk-configs'] }) }, }) } // ── COT / Forward Curves / Surprise Index / Hawk-Dove — new types ───────────── export interface CotEntry { id: number market_name: string commodity: string asset_class: string report_date: string mm_long: number mm_short: number open_interest: number net_position: number net_pct_oi: number change_net: number fetched_at: string } export interface ForwardCurveEntry { id: number asset: string asset_class: string front_price: number | null far_price: number | null slope_pct: number | null structure: 'contango' | 'backwardation' | 'flat' | 'unknown' months_spread: number fetched_at: string } export interface TextSentimentResult { score: number label: string summary: string key_phrases: string[] confidence: 'high' | 'medium' | 'low' } export interface ReportResultUpdate { actual_value?: number | null consensus_estimate?: number | null text_sentiment_score?: number | null text_sentiment_label?: string | null text_sentiment_summary?: string | null } // ── COT Data ────────────────────────────────────────────────────────────────── export function useCotData() { return useQuery({ queryKey: ['specialist-desks', 'cot'], queryFn: () => api.get('/specialist-desks/cot').then(r => r.data), staleTime: 1000 * 60 * 60, // 1h }) } export function useFetchCot() { const qc = useQueryClient() return useMutation({ mutationFn: () => api.post('/specialist-desks/cot/fetch').then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['specialist-desks', 'cot'] }), }) } // ── Forward Curves ──────────────────────────────────────────────────────────── export function useForwardCurves() { return useQuery({ queryKey: ['specialist-desks', 'forward-curves'], queryFn: () => api.get('/specialist-desks/forward-curves').then(r => r.data), staleTime: 1000 * 60 * 60, }) } export function useFetchForwardCurves() { const qc = useQueryClient() return useMutation({ mutationFn: () => api.post('/specialist-desks/forward-curves/fetch').then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['specialist-desks', 'forward-curves'] }), }) } // ── Surprise Index ──────────────────────────────────────────────────────────── export function useUpdateReportResult() { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, body }: { id: string; body: ReportResultUpdate }) => api.put(`/specialist-desks/reports/${id}/result`, body).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['specialist-reports'] }) qc.invalidateQueries({ queryKey: ['desk-configs'] }) }, }) } // ── Hawk/Dove Text Scorer ───────────────────────────────────────────────────── export function useScoreText() { return useMutation({ mutationFn: (body: { text: string; report_name: string; desk: string }) => api.post('/specialist-desks/score-text', body).then(r => r.data) as Promise, }) } // ── AI Chat widget — free-form, read-only, context-aware conversation ──────── export interface ChatMessage { role: 'user' | 'assistant'; content: string; created_at?: string; trade_proposal?: TradeProposalRef | null } export const useChatContextBlocks = () => useQuery({ queryKey: ['ai-chat-blocks'], queryFn: () => api.get('/ai-chat/blocks').then(r => r.data.blocks as string[]), staleTime: Infinity, }) export const useChatHistory = (sessionId: string) => useQuery({ queryKey: ['ai-chat-history', sessionId], queryFn: () => api.get('/ai-chat/history', { params: { session_id: sessionId } }).then(r => r.data.messages as ChatMessage[]), enabled: !!sessionId, staleTime: Infinity, }) export interface TradeProposalRef { id: string; title?: string; underlying?: string; strategy?: string } export const useSendChatMessage = () => useMutation({ mutationFn: (body: { session_id: string; message: string; enabled_blocks?: string[]; refresh_context?: boolean }) => api.post('/ai-chat/', body).then(r => r.data as { reply: string; blocks_included: string[]; trade_proposal: TradeProposalRef | null }), }) export const useClearChatSession = () => useMutation({ mutationFn: (sessionId: string) => api.post('/ai-chat/clear', { session_id: sessionId }).then(r => r.data), }) // ── AI Chat widget — trade proposals (pending confirmation) ────────────────── export interface AiTradeProposal { id: string session_id: string created_at: string status: 'pending' | 'confirmed' | 'rejected' title: string underlying: string strategy: string asset_class: string expiry_days: number capital_invested: number legs: Array<{ strike: number; option_type: string; quantity: number; position: string }> geo_trigger: string rationale: string portfolio_id?: string | null } export const useAiTradeProposals = (status: string = 'pending') => useQuery({ queryKey: ['ai-trade-proposals', status], queryFn: () => api.get('/ai-chat/trade-proposals', { params: { status } }).then(r => r.data.proposals as AiTradeProposal[]), refetchInterval: 30000, }) export const useConfirmAiTradeProposal = () => { const qc = useQueryClient() return useMutation({ mutationFn: (proposalId: string) => api.post(`/ai-chat/trade-proposals/${proposalId}/confirm`).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['ai-trade-proposals'] }) qc.invalidateQueries({ queryKey: ['portfolio'] }) qc.invalidateQueries({ queryKey: ['portfolio-summary'] }) }, }) } export const useRejectAiTradeProposal = () => { const qc = useQueryClient() return useMutation({ mutationFn: (proposalId: string) => api.post(`/ai-chat/trade-proposals/${proposalId}/reject`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['ai-trade-proposals'] }), }) } // ── Strategy Builder ──────────────────────────────────────────────────────── export type ChainRow = { strike: number; bid: number; ask: number; mid: number; last: number; iv: number; open_interest: number; volume: number } export type ChainExpiry = { expiry_date: string; days_to_expiry: number; calls: ChainRow[]; puts: ChainRow[] } export type ChainSlice = { symbol: string; proxy: string; spot: number; expiries: ChainExpiry[] } export type ManualGridCell = { days_to_expiry: number; strike_pct: number; iv: number | null } export type StrategyScenario = { symbol: string horizon_days: number spot_shock_pct: number iv_level_shift: number skew_tilt: number term_shift: number manual_grid?: ManualGridCell[] rate?: number n_expiries?: number contract_size?: number } export type StrategyLeg = { expiry_date: string days_to_expiry: number strike: number option_type: 'call' | 'put' position: 'long' | 'short' quantity: number } export type Greeks = { delta: number; gamma: number; theta: number; vega: number } export type PayoffPoint = { underlying: number; pnl: number } export type PriceCombo = { entry_cost: number entry_cost_mid: number scenario_value: number scenario_value_mid: number net_pnl: number broker_spread_cost: number max_gain: number | null max_loss: number | null bounded_risk: boolean greeks_now: Greeks greeks_scenario: Greeks net_delta_now: number net_delta_scenario: number at_expiry: PayoffPoint[] at_scenario: PayoffPoint[] spot: number scenario_spot: number proxy: string } export type StrategyCandidate = { template_name: string legs: StrategyLeg[] score: number objective: string } & PriceCombo export const useOptionChainSlice = (symbol: string, horizonDays: number, nExpiries = 3, enabled = true) => useQuery({ queryKey: ['strategy-builder-chain', symbol, horizonDays, nExpiries], queryFn: () => api.get('/strategy-builder/chain', { params: { symbol, horizon_days: horizonDays, n_expiries: nExpiries } }).then(r => r.data), enabled: enabled && !!symbol, staleTime: 30_000, retry: 1, }) export const usePriceStrategy = () => useMutation({ mutationFn: (body: { scenario: StrategyScenario; legs: StrategyLeg[] }) => api.post('/strategy-builder/price', body).then(r => r.data), }) export type OptimizeConstraints = { max_legs: number delta_threshold: number | null max_loss_cap?: number | null objective: 'net_pnl' | 'return_on_risk' | 'prob_weighted' top_n?: number } export const useOptimizeStrategy = () => useMutation({ mutationFn: (body: { scenario: StrategyScenario; constraints: OptimizeConstraints }) => api.post('/strategy-builder/optimize', body).then(r => r.data), }) export type SavedScenario = { id: string; symbol: string; label: string; horizon_days: number spot_shock_pct: number; iv_level_shift: number; skew_tilt: number; term_shift: number manual_grid: ManualGridCell[]; created_at: string } export type SavedStrategyRecord = { id: string; scenario_id: string | null; symbol: string; template_name: string; objective: string legs: StrategyLeg[]; entry_cost: number | null; max_gain: number | null; max_loss: number | null net_pnl_scenario: number | null; net_delta: number | null; notes: string; created_at: string } export const useScenarios = (symbol?: string) => useQuery({ queryKey: ['strategy-scenarios', symbol], queryFn: () => api.get('/strategy-builder/scenarios', { params: symbol ? { symbol } : {} }).then(r => r.data), }) export const useSaveScenario = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: StrategyScenario & { label?: string }) => api.post('/strategy-builder/scenarios', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['strategy-scenarios'] }), }) } export const useDeleteScenario = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.delete(`/strategy-builder/scenarios/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['strategy-scenarios'] }), }) } export const useSavedStrategies = (symbol?: string) => useQuery({ queryKey: ['saved-strategies', symbol], queryFn: () => api.get('/strategy-builder/saved', { params: symbol ? { symbol } : {} }).then(r => r.data), }) export const useSaveStrategyRecord = () => { const qc = useQueryClient() return useMutation({ mutationFn: (body: { scenario_id?: string | null; symbol: string; template_name?: string; objective?: string legs: StrategyLeg[]; entry_cost?: number | null; max_gain?: number | null; max_loss?: number | null net_pnl_scenario?: number | null; net_delta?: number | null; notes?: string }) => api.post('/strategy-builder/saved', body).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-strategies'] }), }) } export const useDeleteSavedStrategy = () => { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.delete(`/strategy-builder/saved/${id}`).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-strategies'] }), }) } // ── Saxo connection ─────────────────────────────────────────────────────────── export type SaxoStatus = { connected: boolean; configured: boolean; environment: string expires_at?: string; refresh_expires_at?: string } export const useSaxoStatus = () => useQuery({ queryKey: ['saxo-status'], queryFn: () => api.get('/saxo/status').then(r => r.data), refetchInterval: 60_000, }) export const useDisconnectSaxo = () => { const qc = useQueryClient() return useMutation({ mutationFn: () => api.post('/saxo/disconnect').then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['saxo-status'] }), }) } export const useSaxoWatchlist = () => useQuery<{ symbols: string[] }>({ queryKey: ['saxo-watchlist'], queryFn: () => api.get('/saxo/watchlist').then(r => r.data), }) export const useUpdateSaxoWatchlist = () => { const qc = useQueryClient() return useMutation({ mutationFn: (symbols: string[]) => api.put('/saxo/watchlist', { symbols }).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['saxo-watchlist'] }), }) } export const useExpandSaxoWatchlist = () => { const qc = useQueryClient() return useMutation({ mutationFn: (keyword: string) => api.post('/saxo/watchlist/expand', { keyword }).then(r => r.data as { symbols: string[]; added: string[] }), onSuccess: () => qc.invalidateQueries({ queryKey: ['saxo-watchlist'] }), }) } export const useSnapshotSaxoNow = () => useMutation({ mutationFn: (symbol: string) => api.post(`/saxo/snapshot-now/${encodeURIComponent(symbol)}`).then(r => r.data), }) export const useSnapshotAllSaxoNow = () => useMutation({ mutationFn: () => api.post('/saxo/snapshot-now').then(r => r.data as { results: Record }), }) export const useSaxoSettings = () => useQuery<{ snapshot_minutes: number }>({ queryKey: ['saxo-settings'], queryFn: () => api.get('/saxo/settings').then(r => r.data), }) export const useUpdateSaxoSettings = () => { const qc = useQueryClient() return useMutation({ mutationFn: (snapshotMinutes: number) => api.put('/saxo/settings', { snapshot_minutes: snapshotMinutes }).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['saxo-settings'] }), }) } export type SaxoSnapshotRow = { id: string; symbol: string; snapshot_date: string; spot: number | null expiry_date: string; strike: number; option_type: 'call' | 'put' bid: number | null; ask: number | null; mid: number | null; volatility_pct: number | null delta: number | null; gamma: number | null; theta: number | null; vega: number | null is_synthetic?: number; created_at: string } export const useDedupeSaxoHistory = () => { const qc = useQueryClient() return useMutation({ mutationFn: () => api.post('/saxo/history/dedupe').then(r => r.data as { rows_deleted: number }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['saxo-history'] }) qc.invalidateQueries({ queryKey: ['saxo-symbols'] }) }, }) } export const useSaxoHistory = (symbol?: string, asOf?: string) => useQuery({ queryKey: ['saxo-history', symbol, asOf], queryFn: () => api.get('/saxo/history', { params: { ...(symbol ? { symbol } : {}), ...(asOf ? { as_of: asOf } : {}) }, }).then(r => r.data), }) export const useDeleteSaxoHistory = () => { const qc = useQueryClient() return useMutation({ mutationFn: (symbol: string) => api.delete(`/saxo/history/${encodeURIComponent(symbol)}`).then(r => r.data as { symbol: string; rows_deleted: number }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['saxo-history'] }) qc.invalidateQueries({ queryKey: ['saxo-symbols'] }) }, }) } export type SaxoSymbolSummary = { symbol: string; rows_count: number; first_date: string; last_date: string } export const useSaxoSymbols = () => useQuery({ queryKey: ['saxo-symbols'], queryFn: () => api.get('/saxo/symbols').then(r => r.data), }) export type SaxoValidation = { symbol: string; valid: boolean; uic: number | null; error: string | null matched_symbol?: string; description?: string; asset_type?: string } export const useValidateSaxoWatchlist = () => useQuery({ queryKey: ['saxo-validate'], queryFn: () => api.get('/saxo/validate').then(r => r.data), enabled: false, }) export type SaxoCatalogEntry = { uic: number; symbol: string; asset_type: string; description: string; updated_at: string } export type SaxoCatalogSummary = { asset_type: string; count: number; last_refreshed: string } export const useSaxoCatalog = (assetType?: string, q?: string, opts?: { enabled?: boolean; limit?: number }) => useQuery({ queryKey: ['saxo-catalog', assetType, q, opts?.limit], queryFn: () => api.get('/saxo/catalog', { params: { ...(assetType ? { asset_type: assetType } : {}), ...(q ? { q } : {}), ...(opts?.limit ? { limit: opts.limit } : {}) }, }).then(r => r.data), enabled: opts?.enabled ?? !!(assetType || q), }) export const useSaxoCatalogSummary = () => useQuery({ queryKey: ['saxo-catalog-summary'], queryFn: () => api.get('/saxo/catalog/summary').then(r => r.data), }) export const useRefreshSaxoCatalog = () => { const qc = useQueryClient() return useMutation({ mutationFn: (assetTypes?: string[]) => api.post('/saxo/catalog/refresh', { asset_types: assetTypes ?? null }).then(r => r.data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['saxo-catalog'] }) qc.invalidateQueries({ queryKey: ['saxo-catalog-summary'] }) }, }) } export type SaxoQuote = { symbol: string; description: string | null; asset_type: string; uic: number bid: number | null; ask: number | null; last_updated: string | null; price_source: string | null } export const useTestSaxoQuote = () => useMutation({ mutationFn: ({ symbol, assetType }: { symbol: string; assetType?: string }) => api.get(`/saxo/quote/${encodeURIComponent(symbol)}`, { params: assetType ? { asset_type: assetType } : {} }).then(r => r.data), })