Files
OpenFin/frontend/src/hooks/useApi.ts
2026-07-19 10:55:56 +02:00

1834 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 type TickerValidation = { valid: boolean; symbol: string; name?: string; price?: number; change_pct?: number; reason?: string }
export const validateTicker = (symbol: string): Promise<TickerValidation> =>
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<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),
})
// Real Forex Factorybacked 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<WatchlistItem[]>({
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<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 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<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 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<string, CycleStepParam> }
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<string, Record<string, unknown>> }) =>
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<MatchResult> =>
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<DeskConfig[]>({
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<DeskConfig> }) =>
api.put(`/specialist-desks/configs/${asset_class}`, body).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['desk-configs'] }),
})
}
export const useAllSpecialistReports = () =>
useQuery<SpecialistReport[]>({
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<SpecialistReport> & { 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<SpecialistReport> & { 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<CotEntry[]>({
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<ForwardCurveEntry[]>({
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<TextSentimentResult>,
})
}
// ── 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<ChainSlice>({
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<PriceCombo>('/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<StrategyCandidate[]>('/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<SavedScenario[]>({
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<SavedStrategyRecord[]>({
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<SaxoStatus>({
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<string, number> }),
})
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<SaxoSnapshotRow[]>({
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<SaxoSymbolSummary[]>({
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<SaxoValidation[]>({
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<SaxoCatalogEntry[]>({
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<SaxoCatalogSummary[]>({
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<SaxoQuote>(`/saxo/quote/${encodeURIComponent(symbol)}`, { params: assetType ? { asset_type: assetType } : {} }).then(r => r.data),
})