feat: new cockpit

This commit is contained in:
OpenSquared
2026-07-14 11:21:43 +02:00
parent 9778c74ae3
commit ad07c8d886
32 changed files with 871 additions and 551 deletions

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GeoOptions Intelligence</title>
<title>OpenFin Intelligence</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<style>

View File

@@ -1,11 +1,11 @@
{
"name": "geooptions-cockpit",
"name": "openfin-cockpit",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "geooptions-cockpit",
"name": "openfin-cockpit",
"version": "1.0.0",
"dependencies": {
"@tanstack/react-query": "^5.59.0",

View File

@@ -1,5 +1,5 @@
{
"name": "geooptions-cockpit",
"name": "openfin-cockpit",
"private": true,
"version": "1.0.0",
"type": "module",

View File

@@ -0,0 +1,52 @@
import { Link } from 'react-router-dom'
import { ArrowUpRight } from 'lucide-react'
import clsx from 'clsx'
export default function TradeRankList({ trades, mode, title, linkTo }: {
trades: any[]
mode: 'top' | 'worst'
title: string
linkTo: string
}) {
const sorted = [...trades]
.sort((a, b) => mode === 'top'
? (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999)
: (a.pnl_pct ?? 999) - (b.pnl_pct ?? 999))
.slice(0, 5)
return (
<Link to={linkTo} className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">{title}</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{sorted.length > 0 ? (
<div className="space-y-1.5 mt-1">
{sorted.map((t: any, i: number) => {
const pnl: number | null = t.pnl_pct ?? null
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
const entryDate = t.entry_date ? t.entry_date.slice(0, 10) : null
return (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className="text-[9px] text-slate-700 font-mono w-3 shrink-0">{i + 1}</span>
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold shrink-0">{t.underlying ?? '—'}</span>
<span className="text-slate-500 truncate flex-1 text-[9px]">{t.strategy ?? ''}</span>
{entryDate && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{entryDate}</span>}
{pnl !== null ? (
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span>
) : (
<span className="text-slate-700 shrink-0 text-[9px]"></span>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">No trades logged</div>
)}
</Link>
)
}

View File

@@ -59,7 +59,7 @@ export default function Sidebar() {
<div className="flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-400" />
<div>
<div className="text-sm font-bold text-white tracking-wide">GeoOptions</div>
<div className="text-sm font-bold text-white tracking-wide">OpenFin</div>
<div className="text-xs text-slate-500">Intelligence v2.0</div>
</div>
</div>
@@ -115,7 +115,7 @@ export default function Sidebar() {
{/* Footer */}
<div className="p-3 border-t border-slate-700/40 text-xs text-slate-600">
<div>© 2026 GeoOptions</div>
<div>© 2026 OpenFin</div>
<div className="text-slate-700 mt-0.5">Local · IBKR ready</div>
</div>
</aside>

View File

@@ -0,0 +1,5 @@
export const ASSET_CLASS_COLORS: Record<string, string> = {
energy: '#f97316', metals: '#eab308', agriculture: '#84cc16',
equities: '#22c55e', indices: '#3b82f6', forex: '#8b5cf6', rates: '#06b6d4',
unknown: '#64748b',
}

View File

@@ -96,6 +96,68 @@ export const useCalendar = () =>
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 } = {}) =>
useQuery<{ events: FFEvent[] }>({
queryKey: ['eco-calendar', params],
queryFn: () => api.get('/eco/calendar', { params: { period: 'recent', limit: 30, ...params } }).then(r => r.data),
staleTime: 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({

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig } from '../hooks/useApi'
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert, DatabaseBackup } from 'lucide-react'
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, validateTicker } from '../hooks/useApi'
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert, DatabaseBackup, Radar } from 'lucide-react'
import clsx from 'clsx'
const API = ''
@@ -330,6 +330,85 @@ function RiskProfilesCard() {
)
}
function WatchlistCard() {
const { data: items, isLoading } = useInstrumentsWatchlist()
const { mutateAsync: addTicker, isPending: adding } = useAddWatchlistInstrument()
const { mutate: removeTicker } = useRemoveWatchlistInstrument()
const [input, setInput] = useState('')
const [error, setError] = useState('')
const handleAdd = async () => {
const sym = input.trim().toUpperCase()
if (!sym) return
setError('')
const check = await validateTicker(sym)
if (!check.valid) {
setError(check.reason ?? 'Ticker not found')
return
}
try {
await addTicker(sym)
setInput('')
} catch (e: unknown) {
const msg = (e as any)?.response?.data?.detail ?? (e as { message?: string })?.message ?? 'Could not add ticker'
setError(msg)
}
}
const list: any[] = items ?? []
return (
<div className="card bg-dark-700/20 mb-4">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-sm font-semibold text-slate-300 flex items-center gap-1.5">
<Radar className="w-4 h-4 text-blue-400" /> Instruments Watchlist
</div>
<div className="text-[10px] text-slate-600 mt-0.5">
Instruments surveillés pour le radar du Dashboard et les highlights Options Lab liste indépendante des autres watchlists (Markets, Options Lab, InstrumentModels).
</div>
</div>
</div>
<div className="flex items-center gap-2 mb-3">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleAdd()}
placeholder="Ticker (ex. AAPL, EURUSD=X, ^GSPC)"
className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-64 focus:outline-none focus:border-blue-500/50"
/>
<button onClick={handleAdd} disabled={adding || !input.trim()}
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-40 transition-colors">
<Plus className="w-3.5 h-3.5" /> Add
</button>
</div>
{error && <div className="text-[10px] text-red-400 mb-3">{error}</div>}
{isLoading ? (
<div className="text-xs text-slate-600">Loading</div>
) : list.length === 0 ? (
<div className="text-xs text-slate-600">No instrument watched yet.</div>
) : (
<div className="space-y-1.5">
{list.map(item => (
<div key={item.ticker} className="flex items-center justify-between bg-dark-800/60 rounded px-3 py-1.5">
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs font-mono font-bold text-white">{item.ticker}</span>
<span className="text-[10px] text-slate-500 truncate">{item.name}</span>
<span className="text-[9px] text-slate-700 bg-dark-700 px-1.5 py-0.5 rounded capitalize">{item.asset_class}</span>
</div>
<button onClick={() => removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
</div>
)
}
export default function Config() {
const { data: sources, isLoading } = useSources()
const { data: config } = useConfig()
@@ -354,7 +433,7 @@ export default function Config() {
const [calendarRefreshH, setCalendarRefreshH] = useState(6)
const [showKeys, setShowKeys] = useState(false)
const [savedMsg, setSavedMsg] = useState('')
const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'options' | 'profiles' | 'data'>('ia')
const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'options' | 'profiles' | 'watchlist' | 'data'>('ia')
// IV gate local state
const [ivGateEnabled, setIvGateEnabled] = useState(true)
@@ -525,6 +604,7 @@ export default function Config() {
{ key: 'sources', label: 'Sources', icon: Globe },
{ key: 'options', label: 'Options — Settings', icon: TrendingUp },
{ key: 'profiles', label: 'Risk Profiles', icon: SlidersHorizontal },
{ key: 'watchlist', label: 'Watchlist', icon: Radar },
{ key: 'data', label: 'Data Management', icon: DatabaseBackup },
] as const
@@ -1542,6 +1622,9 @@ export default function Config() {
{/* ── Profils de risque ── */}
{configTab === 'profiles' && <RiskProfilesCard />}
{/* ── Watchlist ── */}
{configTab === 'watchlist' && <WatchlistCard />}
{/* ── Data Management ── */}
{configTab === 'data' && <DataManagementCard />}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import {
RefreshCw, Edit3, X, Trash2, ChevronDown, ChevronUp,
TrendingUp, TrendingDown, Minus, LineChart, Table2, Network, Activity,
Plus, Zap, Settings2, Check,
Plus, Zap, Settings2, Check, Eye, EyeOff,
} from 'lucide-react'
import clsx from 'clsx'
import axios from 'axios'
@@ -119,6 +119,8 @@ interface VirtualEventForm {
actual_value?: string | null
forecast_value?: string | null
event_name?: string
routed_node?: string
disabled?: boolean
}
interface EventDetail {
@@ -149,6 +151,7 @@ interface CalendarEvent {
forecast_value: string | null
previous_value: string | null
category: string
routed_node: string
is_future: boolean
estimated_pips: number
has_surprise: boolean
@@ -966,6 +969,7 @@ function newVirtualEvent(): VirtualEventForm {
rise_days: 1,
plateau_days: 0,
decay_type: 'exp',
disabled: false,
}
}
@@ -1004,6 +1008,7 @@ function TimelineView({ instrument }: { instrument: string }) {
[virtuals])
const activeVirtuals = useMemo(() => virtuals.filter(ve => {
if (ve.disabled) return false
if (filterImpact.length > 0 && ve.impact && !filterImpact.includes(ve.impact)) return false
if (filterCurrency.length > 0 && ve.currency && !filterCurrency.includes(ve.currency)) return false
if (filterSurprise && !ve.has_surprise) return false
@@ -1494,6 +1499,8 @@ function TimelineView({ instrument }: { instrument: string }) {
actual_value: ev.actual_value,
forecast_value: ev.forecast_value,
event_name: ev.event_name,
routed_node: ev.routed_node,
disabled: false,
}))
if (imported.length === 0) {
setCalMsg('Aucun event FF calendrier (high/medium) pour cette période')
@@ -1643,6 +1650,14 @@ function TimelineView({ instrument }: { instrument: string }) {
<div key={ve.id}
className={clsx('flex items-center gap-1.5 rounded px-2 py-1 text-xs transition-all',
isActive ? 'bg-dark-800/60' : 'bg-dark-800/20 opacity-40')}>
{/* Disable toggle */}
<button
onClick={() => setVirtuals(v => v.map((x, i) => i === idx ? {...x, disabled: !x.disabled} : x))}
className={clsx('flex-shrink-0 transition-colors',
ve.disabled ? 'text-slate-600 hover:text-slate-400' : 'text-violet-400 hover:text-violet-300')}
title={ve.disabled ? 'Réactiver' : 'Désactiver'}>
{ve.disabled ? <EyeOff className="w-3 h-3"/> : <Eye className="w-3 h-3"/>}
</button>
{/* Impact dot */}
<span className={clsx('w-1.5 h-1.5 rounded-full flex-shrink-0', impDot)}/>
{/* Currency */}
@@ -1651,6 +1666,16 @@ function TimelineView({ instrument }: { instrument: string }) {
)}
{/* Date */}
<span className="text-slate-500 font-mono w-10 flex-shrink-0">{dispDate}</span>
{/* Node routing badge */}
{ve.routed_node && (
<span className={clsx('flex-shrink-0 px-1 rounded border font-mono',
ve.routed_node === 'direct'
? 'text-slate-500 border-slate-700/40 text-[9px]'
: 'text-violet-400 border-violet-700/40 bg-violet-900/20 text-[9px]'
)} title={`Route → nœud : ${ve.routed_node}`}>
{ve.routed_node === 'direct' ? '⇢' : ve.routed_node.slice(0, 10)}
</span>
)}
{/* Event name */}
<span className="flex-1 text-slate-300 truncate" title={ve.event_name || ve.label}>
{(ve.event_name || ve.label).replace(/^\[.*?\]\s*/, '')}

View File

@@ -3,6 +3,7 @@ import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw,
import { TradeIdeasTab, CATEGORIES } from '../components/TradeIdeas'
import clsx from 'clsx'
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, useSkippedTrades, useDeleteTrade, api } from '../hooks/useApi'
import { ASSET_CLASS_COLORS } from '../constants/assetColors'
import { useQueryClient } from '@tanstack/react-query'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale'
@@ -1453,12 +1454,6 @@ function CyclesSection() {
// ── Page principale ────────────────────────────────────────────────────────────
const ASSET_CLASS_COLORS: Record<string, string> = {
energy: '#f97316', metals: '#eab308', agriculture: '#84cc16',
equities: '#22c55e', indices: '#3b82f6', forex: '#8b5cf6', rates: '#06b6d4',
unknown: '#64748b',
}
function SimPortfolioRiskPanel() {
const { data, isLoading, refetch } = useSimPortfolioRisk()
const risk = data as any

View File

@@ -114,7 +114,7 @@ export default function MacroRegime() {
useEffect(() => {
try {
const raw = localStorage.getItem('geo-options:macro-narration')
const raw = localStorage.getItem('openfin:macro-narration')
if (raw) setSavedNarration(JSON.parse(raw))
} catch {}
}, [])
@@ -151,7 +151,7 @@ export default function MacroRegime() {
const text = res.data.narration ?? res.data.text ?? JSON.stringify(res.data)
const entry = { text, at: new Date().toISOString() }
setSavedNarration(entry)
localStorage.setItem('geo-options:macro-narration', JSON.stringify(entry))
localStorage.setItem('openfin:macro-narration', JSON.stringify(entry))
} catch {
const entry = { text: 'Error during generation — please check your OpenAI key.', at: new Date().toISOString() }
setSavedNarration(entry)

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'
import { useRiskDashboard, usePatternCorrelations, usePnlTimeline, useRiskExposure, useSimPortfolioRisk } from '../hooks/useApi'
import { ASSET_CLASS_COLORS } from '../constants/assetColors'
import clsx from 'clsx'
import { ShieldAlert, TrendingUp, GitBranch, AlertTriangle, CheckCircle, Activity, Brain, RefreshCw, PieChart } from 'lucide-react'
@@ -131,11 +132,6 @@ function RecommendationCard({ rec }: { rec: any }) {
}
// ── Main page ────────────────────────────────────────────────────────────────
const ASSET_CLASS_COLORS: Record<string, string> = {
energy: '#f97316', metals: '#eab308', agriculture: '#84cc16',
equities: '#22c55e', indices: '#3b82f6', forex: '#8b5cf6', rates: '#06b6d4',
unknown: '#64748b',
}
function SimRiskPanel() {
const { data, isLoading, refetch } = useSimPortfolioRisk()