Files
OpenFin/frontend/src/hooks/useApi.ts
OpenSquared f09c5b8ee7 feat: Phase 2 — Pattern Reliability, Contre-thèses & Calibration probabiliste
Sprint 2.1 — Pattern Reliability Score
- database.py: get_pattern_reliability() — win_rate × log(n+1) sur trades matures (≥35% horizon)
- database.py: get_all_pattern_reliability_map() pour injection rapide dans les prompts
- ai_analyzer.py: inject reliability_map dans suggest_patterns (patterns fiables mis en avant)
- auto_cycle.py: charge reliability_map avant suggestion et le passe au suggéreur
- routers/analytics.py: GET /api/analytics/reliability
- PatternEditor.tsx: ReliabilityBadge sur chaque card + usePatternReliability hook
- useApi.ts: usePatternReliability, useCalibration hooks

Sprint 2.2 — Contre-thèses & Invalidation Triggers
- database.py: migration ALTER TABLE — counter_thesis, invalidation_trigger, invalidation_probability
- database.py: save_custom_pattern() persiste les 3 nouveaux champs
- ai_analyzer.py: counter_thesis + invalidation_trigger + invalidation_probability dans le JSON schema
- auto_cycle.py: détection automatique des triggers d'invalidation contre les news (keyword match)
- routers/analytics.py: GET /api/analytics/invalidation-alerts
- PatternEditor.tsx: affichage contre-thèse dans les cards + champs dans le formulaire
- PatternEditor.tsx: affichage dans AiSuggestModal (suggestions IA)
- routers/patterns.py: PatternRequest inclut les 3 nouveaux champs

Sprint 2.3 — Calibration probabiliste & Demi-vie KB
- database.py: migration — predicted_probability sur pattern_score_history
- database.py: save_pattern_scores() stocke probability du pattern à chaque scoring run
- database.py: get_calibration_data() — Brier score + buckets de calibration par décile
- database.py: expires_at + confidence_decay_days sur knowledge_base
- database.py: decay_kb_confidence() — decay automatique + archivage à 0
- auto_cycle.py: decay_kb_confidence() appelé au début de chaque cycle (non-bloquant)
- routers/analytics.py: GET /api/analytics/calibration + POST /api/analytics/kb/decay
- frontend/src/pages/Analytics.tsx: nouvelle page — tableau fiabilité + calibration Brier
- App.tsx + Sidebar.tsx: route /analytics + entrée menu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 16:50:53 +02:00

632 lines
22 KiB
TypeScript

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<Record<string, Quote[]>>({
queryKey: ['quotes'],
queryFn: () => api.get('/market/quotes').then(r => r.data),
refetchInterval: 60_000,
})
export const useHistory = (symbol: string, period = '1y', interval = '1d') =>
useQuery<HistoricalCandle[]>({
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<GeoNews[]>({
queryKey: ['geo-news'],
queryFn: () => api.get('/geo/news').then(r => r.data),
refetchInterval: 3_600_000,
})
export const useGeoRiskScore = () =>
useQuery<GeoRiskScore>({
queryKey: ['geo-risk-score'],
queryFn: () => api.get('/geo/risk-score').then(r => r.data),
refetchInterval: 3_600_000,
})
export const usePatternMatches = () =>
useQuery<PatternMatch[]>({
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<TradeIdea[]>({
queryKey: ['trade-ideas'],
queryFn: () => api.get('/geo/trade-ideas').then(r => r.data),
})
export const useCalendar = () =>
useQuery<EconomicEvent[]>({
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<BacktestResult, Error, Record<string, unknown>>({
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<string, unknown>) =>
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<string, unknown>) =>
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<string, unknown>) =>
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<string, string>) =>
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<string, unknown>) =>
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,
})