Initial commit — GeoOptions Intelligence Cockpit v2.0

Stack: FastAPI + React/TypeScript + SQLite + GPT-4o
Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM,
Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA.
Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-16 20:29:59 +02:00
commit d256b65d30
69 changed files with 18301 additions and 0 deletions

View File

@@ -0,0 +1,545 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'
import axios from 'axios'
import type {
Quote, GeoNews, GeoRiskScore, PatternMatch, TradeIdea,
EconomicEvent, HistoricalCandle, BacktestResult
} from '../types'
export const api = axios.create({ baseURL: '/api' })
// ── Market ────────────────────────────────────────────────────────────────────
export const useAllQuotes = () =>
useQuery<Record<string, Quote[]>>({
queryKey: ['quotes'],
queryFn: () => api.get('/market/quotes').then(r => r.data),
refetchInterval: 60_000,
})
export const useHistory = (symbol: string, period = '1y', interval = '1d') =>
useQuery<HistoricalCandle[]>({
queryKey: ['history', symbol, period, interval],
queryFn: () =>
api.get(`/market/history/${encodeURIComponent(symbol)}`, { params: { period, interval } }).then(r => r.data),
enabled: !!symbol,
})
// ── Geo ───────────────────────────────────────────────────────────────────────
export const useGeoNews = () =>
useQuery<GeoNews[]>({
queryKey: ['geo-news'],
queryFn: () => api.get('/geo/news').then(r => r.data),
refetchInterval: 3_600_000,
})
export const useGeoRiskScore = () =>
useQuery<GeoRiskScore>({
queryKey: ['geo-risk-score'],
queryFn: () => api.get('/geo/risk-score').then(r => r.data),
refetchInterval: 3_600_000,
})
export const usePatternMatches = () =>
useQuery<PatternMatch[]>({
queryKey: ['pattern-matches'],
queryFn: () => api.get('/geo/pattern-matches').then(r => r.data),
})
export const usePatternRelevance = (days: number) =>
useQuery({
queryKey: ['pattern-relevance', days],
queryFn: () => api.get('/geo/pattern-relevance', { params: { days } }).then(r => r.data),
staleTime: 5 * 60_000,
})
export const useTradeIdeas = () =>
useQuery<TradeIdea[]>({
queryKey: ['trade-ideas'],
queryFn: () => api.get('/geo/trade-ideas').then(r => r.data),
})
export const useCalendar = () =>
useQuery<EconomicEvent[]>({
queryKey: ['calendar'],
queryFn: () => api.get('/geo/calendar').then(r => r.data),
})
// ── Options ───────────────────────────────────────────────────────────────────
export const useIvSurface = (symbol: string) =>
useQuery({
queryKey: ['iv-surface', symbol],
queryFn: () => api.get('/options/iv-surface', { params: { symbol } }).then(r => r.data),
enabled: !!symbol,
})
export const usePnlCurve = (params: {
symbol: string; strike: number; expiry_days: number
option_type: string; quantity: number; premium_paid: number
}) =>
useQuery({
queryKey: ['pnl-curve', params],
queryFn: () => api.get('/options/pnl-curve', { params }).then(r => r.data),
enabled: !!params.symbol && !!params.strike && !!params.premium_paid,
})
// ── Backtest ──────────────────────────────────────────────────────────────────
export const useBacktest = () =>
useMutation<BacktestResult, Error, Record<string, unknown>>({
mutationFn: (data) => api.post('/backtest/run', data).then(r => r.data),
})
// ── AI ────────────────────────────────────────────────────────────────────────
export const useAiStatus = () =>
useQuery({
queryKey: ['ai-status'],
queryFn: () => api.get('/ai/status').then(r => r.data),
refetchInterval: 30_000,
})
export const useAiTopIdeas = () =>
useQuery({
queryKey: ['ai-top-ideas'],
queryFn: () => api.get('/ai/top-ideas').then(r => r.data),
enabled: false,
retry: false,
})
export const useAnalyzeSpeech = () =>
useMutation({
mutationFn: (data: { text: string; speaker?: string }) =>
api.post('/ai/analyze-speech', data).then(r => r.data),
})
export const useEvaluatePattern = () =>
useMutation({
mutationFn: (pattern: Record<string, unknown>) =>
api.post('/ai/evaluate-pattern', { pattern }).then(r => r.data),
})
export const useSuggestPattern = () =>
useMutation({
mutationFn: (context: string) =>
api.post('/ai/suggest-pattern', { context }).then(r => r.data),
})
export const useAiTopIdeasRefetch = () =>
useMutation({
mutationFn: () => api.get('/ai/top-ideas').then(r => r.data),
})
// ── Portfolio ─────────────────────────────────────────────────────────────────
export const usePortfolioPositions = (status = 'open') =>
useQuery({
queryKey: ['portfolio', status],
queryFn: () => api.get('/portfolio/positions', { params: { status } }).then(r => r.data),
refetchInterval: status === 'open' ? 60_000 : false,
})
export const usePortfolioSummary = () =>
useQuery({
queryKey: ['portfolio-summary'],
queryFn: () => api.get('/portfolio/summary').then(r => r.data),
refetchInterval: 60_000,
})
export const usePnlHistory = () =>
useQuery({
queryKey: ['pnl-history'],
queryFn: () => api.get('/portfolio/pnl-history').then(r => r.data),
})
export const useAddPosition = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (data: Record<string, unknown>) =>
api.post('/portfolio/add', data).then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['portfolio'] })
qc.invalidateQueries({ queryKey: ['portfolio-summary'] })
},
})
}
export const useClosePosition = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ id, close_value }: { id: string; close_value: number }) =>
api.post(`/portfolio/close/${id}`, { close_value }).then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['portfolio'] })
qc.invalidateQueries({ queryKey: ['portfolio-summary'] })
qc.invalidateQueries({ queryKey: ['pnl-history'] })
},
})
}
// ── Config ────────────────────────────────────────────────────────────────────
export const useConfig = () =>
useQuery({
queryKey: ['config'],
queryFn: () => api.get('/config/').then(r => r.data),
})
export const useSources = () =>
useQuery({
queryKey: ['sources'],
queryFn: () => api.get('/config/sources').then(r => r.data),
})
export const useUpdateSources = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (sources: Record<string, unknown>) =>
api.put('/config/sources', { sources }).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['sources'] }),
})
}
export const useUpdateApiKeys = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (keys: Record<string, string>) =>
api.put('/config/api-keys', keys).then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['config'] })
qc.invalidateQueries({ queryKey: ['ai-status'] })
},
})
}
// ── AI Pattern Scoring ────────────────────────────────────────────────────────
export const useScorePatterns = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (params: { top_n?: number; category_filter?: string; template?: string }) =>
api.post('/ai/score-patterns', params).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['last-scores'] }),
})
}
export const useLastScores = () =>
useQuery({
queryKey: ['last-scores'],
queryFn: () => api.get('/ai/last-scores').then(r => r.data),
staleTime: Infinity,
})
export const useSuggestNewPatterns = () =>
useMutation({
mutationFn: () => api.post('/ai/suggest-new-patterns').then(r => r.data),
})
export const useAnalysisConfig = () =>
useQuery({
queryKey: ['analysis-config'],
queryFn: () => api.get('/config/analysis').then(r => r.data),
})
export const useSaveAnalysisConfig = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (cfg: { top_n?: number; category_filter?: string; template?: string }) =>
api.put('/config/analysis', cfg).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['analysis-config'] }),
})
}
// ── Patterns ──────────────────────────────────────────────────────────────────
export const useAllPatterns = () =>
useQuery({
queryKey: ['all-patterns'],
queryFn: () => api.get('/patterns/all').then(r => r.data),
})
export const usePatternSimilarity = () =>
useQuery({
queryKey: ['pattern-similarity'],
queryFn: () => api.get('/ai/pattern-similarity').then(r => r.data),
staleTime: 5 * 60_000,
})
export const useMacroRegime = () => {
const qc = useQueryClient()
const query = useQuery({
queryKey: ['macro-regime'],
queryFn: () => api.get('/market/macro-regime').then(r => r.data),
staleTime: 10 * 60 * 1000,
refetchInterval: 15 * 60 * 1000,
})
const forceRefetch = async () => {
const fresh = await api.get('/market/macro-regime?force=true').then(r => r.data)
qc.setQueryData(['macro-regime'], fresh)
return fresh
}
return { ...query, forceRefetch }
}
export const useSavePattern = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (pattern: Record<string, unknown>) =>
api.post('/patterns/custom', pattern).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }),
})
}
export const useDeletePattern = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) =>
api.delete(`/patterns/custom/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }),
})
}
export const useTogglePattern = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) =>
api.put(`/patterns/toggle/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }),
})
}
// ── Auto-Cycle ───────────────────────────────────────────────────────────────
export const useCycleStatus = () =>
useQuery({
queryKey: ['cycle-status'],
queryFn: () => api.get('/cycle/status').then(r => r.data),
staleTime: 5_000,
// Poll every 5s while a cycle is running, every 30s otherwise
refetchInterval: (query) => ((query.state.data as any)?.running ? 5_000 : 30_000),
})
export const useCycleHistory = (limit = 20) =>
useQuery({
queryKey: ['cycle-history', limit],
queryFn: () => api.get(`/cycle/history?limit=${limit}`).then(r => r.data),
staleTime: 60_000,
})
export const useUpdateCycleConfig = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number }) =>
api.post('/cycle/config', cfg).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['cycle-status'] }),
})
}
/** Keys to refresh once a cycle finishes — covers cockpit, macro, journal */
export const CYCLE_REFRESH_KEYS = [
['macro-regime'],
['last-scores'],
['all-patterns'],
['cycle-history'],
['cycle-status'],
['journal-summary'],
['journal-mtm'],
['journal-geo'],
['journal-macro'],
['geo-risk-score'],
['pattern-matches'],
]
export const useTriggerCycle = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: () => api.post('/cycle/trigger').then(r => r.data),
onSuccess: () => {
// Immediate status refresh so the spinner shows
qc.invalidateQueries({ queryKey: ['cycle-status'] })
},
})
}
/**
* Mount this hook once at a high level (e.g. CyclesSection or App).
* It watches cycle-status and, when a running cycle finishes, refreshes
* all cockpit + journal queries so the UI reflects the new scores/regime.
*/
export const useCycleWatcher = () => {
const qc = useQueryClient()
const { data: statusData } = useCycleStatus()
const wasRunning = useRef(false)
useEffect(() => {
const running = (statusData as any)?.running ?? false
if (running) {
wasRunning.current = true
} else if (wasRunning.current) {
// Transition: was running → now done → refresh everything
wasRunning.current = false
CYCLE_REFRESH_KEYS.forEach(key => qc.invalidateQueries({ queryKey: key }))
}
}, [(statusData as any)?.running])
}
// ── Journal de Bord ──────────────────────────────────────────────────────────
export const useJournalSummary = () =>
useQuery({
queryKey: ['journal-summary'],
queryFn: () => api.get('/journal/summary').then(r => r.data),
staleTime: 2 * 60_000,
})
export const useMacroHistory = (days = 15) =>
useQuery({
queryKey: ['journal-macro', days],
queryFn: () => api.get(`/journal/macro-history?days=${days}`).then(r => r.data),
staleTime: 5 * 60_000,
})
export const useGeoHistory = (days = 30) =>
useQuery({
queryKey: ['journal-geo', days],
queryFn: () => api.get(`/journal/geo-history?days=${days}`).then(r => r.data),
staleTime: 5 * 60_000,
})
export const useTradeMtm = (days = 30) =>
useQuery({
queryKey: ['journal-mtm', days],
queryFn: () => api.get(`/journal/trade-mtm?days=${days}`).then(r => r.data),
staleTime: 0,
refetchInterval: 5 * 60_000, // re-fetch live prices every 5 minutes
refetchIntervalInBackground: false,
})
// ── Risk Profiles ─────────────────────────────────────────────────────────────
export const useRiskProfiles = () =>
useQuery({
queryKey: ['risk-profiles'],
queryFn: () => api.get('/profiles').then(r => r.data),
staleTime: 30_000,
})
export const useUpsertProfile = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (profile: {
id?: number; name: string; min_score: number; min_gain_pct: number;
color?: string; enabled?: boolean; sort_order?: number
}) => {
if (profile.id) {
return api.put(`/profiles/${profile.id}`, profile).then(r => r.data)
}
return api.post('/profiles', profile).then(r => r.data)
},
onSuccess: () => qc.invalidateQueries({ queryKey: ['risk-profiles'] }),
})
}
export const useDeleteProfile = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: number) => api.delete(`/profiles/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['risk-profiles'] }),
})
}
export const usePreviewTradeScore = (score: number, gainPct: number, enabled = true) =>
useQuery({
queryKey: ['profile-preview', score, gainPct],
queryFn: () => api.get(`/profiles/preview?score=${score}&gain_pct=${gainPct}`).then(r => r.data),
enabled,
staleTime: 0,
})
// ── Reasoning Traces / Post-mortem ────────────────────────────────────────────
export const useTradePostmortem = (tradeId: number | null) =>
useQuery({
queryKey: ['postmortem', tradeId],
queryFn: () => api.get(`/reasoning/postmortem/${tradeId}`).then(r => r.data),
enabled: tradeId !== null,
staleTime: 60_000,
})
export const useAnalyzePostmortem = () =>
useMutation({
mutationFn: (tradeId: number) =>
api.post(`/reasoning/postmortem/${tradeId}/analyze`).then(r => r.data),
})
export const usePortfolioReportData = (days: number) =>
useQuery({
queryKey: ['portfolio-report-data', days],
queryFn: () => api.get(`/reasoning/portfolio-report?days=${days}`).then(r => r.data),
staleTime: 120_000,
})
export const useGeneratePortfolioReport = () =>
useMutation({
mutationFn: (days: number) =>
api.post(`/reasoning/portfolio-report/generate?days=${days}`).then(r => r.data),
})
export const useAiReportsList = () =>
useQuery({
queryKey: ['ai-reports-list'],
queryFn: () => api.get('/reasoning/reports').then(r => r.data),
staleTime: 30_000,
})
export const useAiReport = (reportId: number | null) =>
useQuery({
queryKey: ['ai-report', reportId],
queryFn: () => api.get(`/reasoning/reports/${reportId}`).then(r => r.data),
enabled: reportId !== null,
staleTime: Infinity,
})
// ── Super Contexte / Knowledge Base ──────────────────────────────────────────
export const useKnowledgeState = () =>
useQuery({
queryKey: ['knowledge-state'],
queryFn: () => api.get('/knowledge/state').then(r => r.data),
staleTime: 5 * 60_000,
})
export const useKnowledgeHistory = () =>
useQuery({
queryKey: ['knowledge-history'],
queryFn: () => api.get('/knowledge/history').then(r => r.data),
staleTime: 5 * 60_000,
})
export const useKnowledgeStateVersion = (stateId: number | null) =>
useQuery({
queryKey: ['knowledge-state-version', stateId],
queryFn: () => api.get(`/knowledge/history/${stateId}`).then(r => r.data),
enabled: stateId !== null,
staleTime: Infinity,
})
export const useKnowledgeEntries = () =>
useQuery({
queryKey: ['knowledge-entries'],
queryFn: () => api.get('/knowledge/entries').then(r => r.data),
staleTime: 5 * 60_000,
})
export const useSynthesizeKnowledge = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: () => api.post('/knowledge/synthesize').then(r => r.data),
onSuccess: () => {
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'] }),
})
}