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>
632 lines
22 KiB
TypeScript
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,
|
|
})
|