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

@@ -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()