feat: new cockpit
This commit is contained in:
@@ -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>
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "geooptions-cockpit",
|
||||
"name": "openfin-cockpit",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
|
||||
52
frontend/src/components/TradeRankList.tsx
Normal file
52
frontend/src/components/TradeRankList.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
5
frontend/src/constants/assetColors.ts
Normal file
5
frontend/src/constants/assetColors.ts
Normal 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',
|
||||
}
|
||||
@@ -96,6 +96,68 @@ export const useCalendar = () =>
|
||||
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 } = {}) =>
|
||||
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({
|
||||
|
||||
@@ -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
@@ -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*/, '')}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user