feat: new cockpit

This commit is contained in:
OpenSquared
2026-07-14 12:17:15 +02:00
parent ad07c8d886
commit 09b9efeda7
2 changed files with 56 additions and 28 deletions

View File

@@ -102,10 +102,10 @@ export interface FFEvent {
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 } = {}) =>
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: 30, ...params } }).then(r => r.data),
queryFn: () => api.get('/eco/calendar', { params: { period: 'recent', limit: 50, impacts: 'high,medium', ...params } }).then(r => r.data),
staleTime: 5 * 60_000,
})

View File

@@ -164,7 +164,7 @@ export default function Dashboard() {
const topNews = useMemo(() =>
[...(geoNews ?? [])]
.sort((a, b) => b.impact_score - a.impact_score)
.slice(0, 2)
.slice(0, 5)
, [geoNews])
// Watchlist radar: change_pct normalized to a 0-100 scale (50 = flat)
@@ -246,30 +246,40 @@ export default function Dashboard() {
<div className="animate-pulse h-16 bg-dark-600 rounded"></div>
) : riskScore && gauge ? (
<>
<div className={clsx('text-5xl font-bold', gauge.color)}>{riskScore.score}</div>
<div className={clsx('text-sm font-semibold mt-1', gauge.color)}>{gauge.label}</div>
<div className="flex items-start justify-between gap-3">
<div>
<div className={clsx('text-5xl font-bold', gauge.color)}>{riskScore.score}</div>
<div className={clsx('text-sm font-semibold mt-1', gauge.color)}>{gauge.label}</div>
</div>
<div className="text-right space-y-0.5 mt-1 shrink-0">
{riskScore.top_risks?.map(([cat, val]) => (
<div key={cat} className="flex items-center justify-end gap-1.5 text-[9px]">
<span className="text-slate-500 capitalize truncate max-w-[86px]">{(cat as string).replace('_', ' ')}</span>
<span className="text-slate-400 font-mono w-7 text-right">{Math.round((val as number) * 100)}%</span>
</div>
))}
</div>
</div>
<div className="mt-3 bg-dark-700 rounded-full h-2">
<div className={clsx('h-2 rounded-full', gauge.bg)} style={{ width: `${riskScore.score}%` }} />
</div>
<div className="mt-2 space-y-1">
{riskScore.top_risks?.map(([cat, val]) => (
<div key={cat} className="flex justify-between text-xs">
<span className="text-slate-500 capitalize">{(cat as string).replace('_', ' ')}</span>
<span className="text-slate-300">{Math.round((val as number) * 100)}%</span>
</div>
))}
</div>
{topNews.length > 0 && (
<div className="mt-2.5 pt-2 border-t border-slate-700/30 space-y-1">
<div className="mt-2.5 pt-2 border-t border-slate-700/30 space-y-1.5">
<div className="flex items-center gap-1 text-[9px] text-slate-600 mb-0.5">
<Newspaper className="w-2.5 h-2.5" /> Top news
</div>
{topNews.map((n, i) => (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className="text-slate-300 truncate flex-1 line-clamp-1">{n.title}</span>
<span className="text-slate-500 font-mono shrink-0">{Math.round(n.impact_score * 100)}</span>
</div>
))}
{topNews.map((n, i) => {
const impact = Math.round(n.impact_score * 100)
const impactColor = impact >= 75 ? 'text-red-400' : impact >= 50 ? 'text-orange-400' : impact >= 25 ? 'text-yellow-400' : 'text-emerald-400'
const dotColor = impact >= 75 ? 'bg-red-500' : impact >= 50 ? 'bg-orange-500' : impact >= 25 ? 'bg-yellow-500' : 'bg-emerald-500'
return (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', dotColor)} />
<span className="text-slate-300 truncate flex-1 line-clamp-1">{n.title}</span>
<span className={clsx('font-mono font-bold shrink-0', impactColor)}>{impact}</span>
</div>
)
})}
</div>
)}
</>
@@ -294,7 +304,7 @@ export default function Dashboard() {
<Radar dataKey="value" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
</RadarChart>
</ResponsiveContainer>
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30 space-y-1 max-h-16 overflow-y-auto">
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30 space-y-1 max-h-40 overflow-y-auto">
{((watchlistQuotesData as any)?.items ?? []).map((it: any) => (
<div key={it.ticker} className="flex items-center justify-between text-[10px]">
<span className="text-slate-400 font-mono">{it.ticker}</span>
@@ -794,7 +804,7 @@ export default function Dashboard() {
.map((w: any) => snapshots[w.ticker])
.filter(Boolean)
.sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50))
.slice(0, 5)
.slice(0, 6)
return (
<Link to="/options" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
@@ -811,13 +821,31 @@ export default function Dashboard() {
{highlights.map((h: any) => {
const rank = h.iv_rank
const skewPct = h.skew?.skew_pct
const ivCur = h.iv_current_pct
const ivPctl = h.iv_percentile
const flow: string | null = h.options_flow?.flow_bias ?? null
const pcRatio = h.options_flow?.pc_oi_ratio
return (
<div key={h.ticker} className="flex items-center gap-1.5 text-[10px]">
<span>{rank != null && rank > 80 ? '🔴' : rank != null && rank < 20 ? '🟢' : '⚪'}</span>
<span className="font-mono text-slate-200 font-bold shrink-0">{h.ticker}</span>
<span className="text-slate-500 flex-1">IVR {rank != null ? rank.toFixed(0) : '—'}</span>
{skewPct != null && Math.abs(skewPct) > 3 && (
<span className="text-slate-600 text-[9px]">skew {skewPct >= 0 ? '+' : ''}{skewPct.toFixed(1)}</span>
<div key={h.ticker} className="pb-1 border-b border-slate-700/20 last:border-0">
<div className="flex items-center gap-1.5 text-[10px]">
<span>{rank != null && rank > 80 ? '🔴' : rank != null && rank < 20 ? '🟢' : '⚪'}</span>
<span className="font-mono text-slate-200 font-bold shrink-0">{h.ticker}</span>
<span className="text-slate-500">IVR {rank != null ? rank.toFixed(0) : '—'}</span>
{ivCur != null && <span className="text-slate-600">· IV {ivCur.toFixed(1)}%</span>}
{skewPct != null && Math.abs(skewPct) > 3 && (
<span className={clsx('ml-auto text-[9px]', skewPct > 0 ? 'text-orange-400' : 'text-blue-400')}>
skew {skewPct >= 0 ? '+' : ''}{skewPct.toFixed(1)}
</span>
)}
</div>
{(ivPctl != null || flow != null || pcRatio != null) && (
<div className="text-[8px] text-slate-700 mt-0.5 ml-5 truncate">
{ivPctl != null && <span>Pctl {ivPctl.toFixed(0)}</span>}
{ivPctl != null && pcRatio != null && ' · '}
{pcRatio != null && <span>P/C {pcRatio.toFixed(2)}</span>}
{(ivPctl != null || pcRatio != null) && flow && ' · '}
{flow && <span className="italic">{flow}</span>}
</div>
)}
</div>
)