1834 lines
64 KiB
TypeScript
1834 lines
64 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 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 Factory–backed calendar (used by CalendarPage.tsx and InstrumentModels)
|
||
export interface FFEvent {
|
||
event_date: string; event_time: string | null; currency: string
|
||
impact: 'high' | 'medium' | 'low'; event_name: string
|
||
actual_value: number | null; forecast_value: number | null; previous_value: number | null
|
||
}
|
||
export const useEcoCalendar = (params: { period?: string; limit?: number; impacts?: string } = {}) =>
|
||
useQuery<{ events: FFEvent[] }>({
|
||
queryKey: ['eco-calendar', params],
|
||
queryFn: () => api.get('/eco/calendar', { params: { period: 'recent', limit: 50, impacts: 'high,medium', ...params } }).then(r => r.data),
|
||
staleTime: 5 * 60_000,
|
||
refetchInterval: 5 * 60_000,
|
||
})
|
||
|
||
// ── Instruments Watchlist (Dashboard "radar" card) ────────────────────────────
|
||
export interface WatchlistItem { ticker: string; name: string; asset_class: string; sort_order: number; added_at: string }
|
||
export interface WatchlistQuoteItem extends WatchlistItem { price: number | null; change_pct: number | null }
|
||
|
||
export const useInstrumentsWatchlist = () =>
|
||
useQuery<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),
|
||
})
|