DockerSpace / frontend /src /components /MetalPredictionCard.tsx
DennisChan0909's picture
Backup current stock predictor strategies
ee37d63
Raw
History Blame Contribute Delete
7.93 kB
import React from 'react'
import { TrendingUp, TrendingDown, Minus, AlertTriangle, Gem } from 'lucide-react'
import { getPredictionReliabilitySummary } from '../utils/predictionReliability'
export interface MetalPredictionData {
symbol: string
name: string
type: string
signal: 'BUY' | 'SELL' | 'HOLD'
signal_probability: number
predicted_price: number
predicted_change_pct: number
sell_target: number
stop_loss: number
confidence: 'HIGH' | 'MEDIUM' | 'LOW'
current_price: number
training_accuracy: number
prediction_horizon_days?: number
oil_news_signal?: {
summary?: string
sentiment_score?: number
event_score?: number
confidence?: number
} | null
trend_conflict_guard?: {
applied?: boolean
reason?: string | null
original_signal?: string
final_signal?: string
snapshot?: {
bearish_count?: number
bullish_count?: number
return_5d?: number | null
return_20d?: number | null
rsi?: number | null
}
} | null
oil_news_probability_adjustment?: number
oil_news_change_adjustment_pct?: number
disclaimer: string
error?: string
}
interface Props {
prediction: MetalPredictionData
theme: 'gold' | 'silver' | 'oil'
}
const themeConfig = {
gold: { border: 'border-yellow-500', bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
silver: { border: 'border-slate-400', bg: 'bg-slate-400/10', text: 'text-slate-300' },
oil: { border: 'border-cyan-500', bg: 'bg-cyan-500/10', text: 'text-cyan-300' },
}
const signalConfig = {
BUY: { label: 'BUY', zh: '買入', bg: 'bg-green-600', Icon: TrendingUp },
SELL: { label: 'SELL', zh: '賣出', bg: 'bg-red-600', Icon: TrendingDown },
HOLD: { label: 'HOLD', zh: '觀望', bg: 'bg-yellow-600', Icon: Minus },
}
export const MetalPredictionCard: React.FC<Props> = ({ prediction: p, theme }) => {
const t = themeConfig[theme]
const s = signalConfig[p.signal] || signalConfig.HOLD
const up = p.predicted_change_pct >= 0
const prob = Math.round(p.signal_probability * 100)
const reliability = getPredictionReliabilitySummary({
confidence: p.confidence,
trainingAccuracy: p.training_accuracy,
})
if (p.error) {
return (
<div className={`rounded-xl border-2 ${t.border} ${t.bg} p-4`}>
<div className="flex items-center gap-2 mb-2">
<Gem size={18} className={t.text} />
<h3 className="text-base font-bold text-white">{p.name}</h3>
</div>
<p className="text-red-400 text-sm">{p.error}</p>
</div>
)
}
return (
<div className={`rounded-xl border-2 ${t.border} ${t.bg} p-4 space-y-4`}>
{/* Header */}
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<Gem size={18} className={t.text} />
<div>
<h3 className="text-base font-bold text-white">{p.name}</h3>
<p className="text-xs text-slate-400">ML 預測 · 未來 {p.prediction_horizon_days ?? 5} 交易日</p>
</div>
</div>
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg ${s.bg}`}>
<s.Icon size={16} className="text-white" />
<span className="text-white font-black text-lg">{s.label}</span>
<span className="text-white/80 text-xs">({s.zh})</span>
</div>
</div>
{/* Price grid */}
<div className="grid grid-cols-2 gap-3">
{[
{ label: '現貨價', val: `$${p.current_price.toFixed(2)}`, cls: 'text-white' },
{ label: '預測價(5日)', val: `$${p.predicted_price.toFixed(2)}`, cls: up ? 'text-green-400' : 'text-red-400',
sub: `${up ? '+' : ''}${p.predicted_change_pct.toFixed(2)}%` },
{ label: '目標價', val: `$${p.sell_target.toFixed(2)}`, cls: 'text-green-300' },
{ label: '停損', val: `$${p.stop_loss.toFixed(2)}`, cls: 'text-red-300' },
].map(({ label, val, cls, sub }) => (
<div key={label} className="bg-slate-900/60 rounded-lg p-3 border border-slate-700">
<p className="text-xs text-slate-400">{label}</p>
<p className={`text-base font-bold ${cls}`}>{val}</p>
{sub && <p className={`text-xs font-semibold mt-0.5 ${up ? 'text-green-400' : 'text-red-400'}`}>{sub}</p>}
</div>
))}
</div>
{/* Probability bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-slate-400">買入機率</span>
<span className={`font-bold ${t.text}`}>{prob}%</span>
</div>
<div className="h-2.5 bg-slate-700 rounded-full overflow-hidden">
<div className={`h-full rounded-full transition-all duration-700 ${
prob >= 60 ? 'bg-green-500' : prob <= 35 ? 'bg-red-500' : 'bg-yellow-500'
}`} style={{ width: `${prob}%` }} />
</div>
</div>
{/* Reliability */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 text-sm">
<div className="bg-slate-800 rounded-lg px-3 py-2">
<div className="text-[11px] text-slate-400">整體可靠度</div>
<div className={`font-bold ${reliability.levelColor}`}>{reliability.levelText}</div>
</div>
<div className="bg-slate-800 rounded-lg px-3 py-2">
<div className="text-[11px] text-slate-400">預測信心度</div>
<div className={`font-bold ${reliability.confidenceColor}`}>{reliability.confidenceText}</div>
</div>
<div className="bg-slate-800 rounded-lg px-3 py-2">
<div className="text-[11px] text-slate-400">模型近期驗證準確率</div>
<div className={`font-semibold ${reliability.accuracyColor}`}>{reliability.accuracyText}</div>
</div>
</div>
{reliability.warning && (
<div className="flex items-start gap-2 bg-red-500/10 rounded-lg p-3 border border-red-700/70">
<AlertTriangle size={13} className="text-red-300 mt-0.5 flex-shrink-0" />
<p className="text-xs text-red-200 leading-relaxed">{reliability.warning}</p>
</div>
)}
{p.trend_conflict_guard?.applied && (
<div className="flex items-start gap-2 bg-amber-500/10 rounded-lg p-3 border border-amber-700/70">
<AlertTriangle size={13} className="text-amber-300 mt-0.5 flex-shrink-0" />
<p className="text-xs text-amber-100 leading-relaxed">
強趨勢衝突 guard 已將 {p.trend_conflict_guard.original_signal} 調整為 {p.trend_conflict_guard.final_signal}。
空方條件 {p.trend_conflict_guard.snapshot?.bearish_count ?? 0} 項,5日報酬 {((p.trend_conflict_guard.snapshot?.return_5d ?? 0) * 100).toFixed(2)}%。
</p>
</div>
)}
{theme === 'oil' && p.oil_news_signal && (
<div className="bg-slate-900/70 rounded-lg p-3 border border-cyan-700/60">
<div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs font-semibold text-cyan-300">新聞 / 政治事件 overlay</span>
<span className="text-xs text-slate-400">
{((p.oil_news_probability_adjustment ?? 0) * 100).toFixed(2)}pp
</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed line-clamp-4 whitespace-pre-line">
{p.oil_news_signal.summary || '尚無新聞訊號'}
</p>
</div>
)}
{/* Disclaimer */}
<div className="flex items-start gap-2 bg-slate-900/70 rounded-lg p-3 border border-slate-700">
<AlertTriangle size={13} className="text-yellow-400 mt-0.5 flex-shrink-0" />
<p className="text-xs text-slate-400">
<span className="text-yellow-400 font-semibold">免責聲明:</span>
{p.disclaimer}
</p>
</div>
</div>
)
}