vjeai's picture
Deploy: all fixes β€” yfinance candles, ml_signal 2y history, no handoff schemas, sequential report phase
3be03dd
Raw
History Blame Contribute Delete
6.5 kB
import { useEffect, useRef } from 'react'
import anime from 'animejs'
// Per-source display config
const SOURCE = {
ml: {
icon: 'πŸ€–',
title: 'AI Model',
desc: 'Machine learning trained on 5 years of price history',
plain: {
BUY: 'Our AI sees patterns similar to when prices rose in the past',
'STRONG BUY': 'Our AI is strongly predicting upward movement',
SELL: 'Our AI sees patterns similar to when prices dropped in the past',
'STRONG SELL':'Our AI is strongly predicting downward movement',
HOLD: 'Our AI is not confident enough to call a direction yet',
},
},
technical: {
icon: 'πŸ“Š',
title: 'Chart Analysis',
desc: 'Price trends, momentum, moving averages & volume',
plain: {
BUY: 'Price is trending up with positive momentum',
'STRONG BUY': 'Strong uptrend β€” multiple chart indicators agree',
SELL: 'Price is trending down β€” momentum looks negative',
'STRONG SELL':'Strong downtrend β€” charts are flashing warning signs',
HOLD: 'Chart signals are mixed β€” no clear trend right now',
},
},
fundamental: {
icon: '🏒',
title: 'Company Health',
desc: 'P/E ratio, revenue growth, profit margins & debt',
plain: {
undervalued: 'The company looks like a potential bargain vs similar companies',
overvalued: 'The stock may be expensive compared to what the company actually earns',
'fair value': 'The company is priced fairly for its size and earnings',
unknown: 'Not enough financial data to form a clear view',
},
},
sentiment: {
icon: 'πŸ“°',
title: 'News & Mood',
desc: 'Recent headlines and investor sentiment from the web',
plain: {
BULLISH: 'Recent news and investor mood are mostly positive',
BEARISH: 'Recent news and investor mood are mostly negative',
NEUTRAL: 'News is balanced β€” no strong positive or negative lean',
},
},
}
// Signal β†’ colour
function signalColor(score) {
if (score > 0.2) return '#00e87a'
if (score < -0.2) return '#ff4466'
return '#ffcc44'
}
// Normalise signal text for display
function formatSignal(signal) {
return (signal || 'HOLD').toString().toUpperCase()
}
// ── Individual card ───────────────────────────────────────────────────────────
function SignalCard({ sourceKey, sourceData, index }) {
const barRef = useRef(null)
const cfg = SOURCE[sourceKey] || SOURCE.technical
const score = sourceData.score ?? 0
const signal = sourceData.signal ?? 'HOLD'
const weight = Math.round((sourceData.weight ?? 0) * 100)
const color = signalColor(score)
const barPct = Math.round((score + 1) * 50) // -1..+1 β†’ 0..100%
// Pick plain-English text
const plainMap = cfg.plain
const signalKey = signal.toString()
const plainText =
plainMap[signalKey] ||
plainMap[signalKey.toUpperCase()] ||
Object.values(plainMap)[1] ||
''
useEffect(() => {
anime({
targets: barRef.current,
width: ['0%', `${barPct}%`],
delay: 700 + index * 120,
duration: 1000,
easing: 'easeOutExpo',
})
}, [])
return (
<div
className="signal-card card"
style={{ '--signal-color': color }}
>
{/* Header: icon + title + badge */}
<div className="signal-card-header">
<span className="signal-icon">{cfg.icon}</span>
<div className="signal-header-text">
<h4 className="signal-title">{cfg.title}</h4>
<p className="signal-desc">{cfg.desc}</p>
</div>
<div
className="signal-badge"
style={{
color,
borderColor: color + '44',
background: color + '11',
}}
>
{formatSignal(signal)}
</div>
</div>
{/* Progress bar */}
<div className="signal-bar-row">
<div className="signal-bar-track">
<div
ref={barRef}
className="signal-bar-fill"
style={{ width: '0%', background: color }}
/>
<div className="signal-bar-center" />
</div>
<span className="signal-weight">Weight {weight}%</span>
</div>
{/* Plain English */}
<p className="signal-plain">{plainText}</p>
{/* ML extras */}
{sourceKey === 'ml' && (
<div className="signal-meta">
{sourceData.cv_accuracy != null && (
<span>
Model accuracy: {(sourceData.cv_accuracy * 100).toFixed(0)}%
</span>
)}
{sourceData.margin != null && (
<span>
Decision margin: {(sourceData.margin * 100).toFixed(0)}%
</span>
)}
{sourceData.strength && (
<span>Signal strength: {sourceData.strength}</span>
)}
</div>
)}
{/* Technical extras */}
{sourceKey === 'technical' && sourceData.votes && (
<div className="signal-meta">
<span>
Bullish votes: {sourceData.votes[0]} / Bearish: {sourceData.votes[1]}
</span>
</div>
)}
</div>
)
}
// ── Container ─────────────────────────────────────────────────────────────────
export default function SignalCards({ breakdown }) {
const gridRef = useRef(null)
useEffect(() => {
if (!gridRef.current) return
anime({
targets: gridRef.current.querySelectorAll('.signal-card'),
translateY: [40, 0],
opacity: [0, 1],
delay: anime.stagger(110, { start: 350 }),
easing: 'easeOutExpo',
duration: 700,
})
}, [breakdown])
if (!breakdown) return null
const order = ['ml', 'technical', 'fundamental', 'sentiment']
const entries = order
.filter((k) => breakdown[k])
.map((k) => [k, breakdown[k]])
return (
<div className="signal-cards-section">
<h2 className="section-title">πŸ”¬ What's driving the signal?</h2>
<div ref={gridRef} className="signal-cards-grid">
{entries.map(([key, val], i) => (
<SignalCard
key={key}
sourceKey={key}
sourceData={val}
index={i}
/>
))}
</div>
</div>
)
}