feat: new cockpit
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user