Spaces:
Sleeping
Sleeping
File size: 4,998 Bytes
3be03dd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import { useState, useEffect, useRef } from 'react'
import anime from 'animejs'
const REC_COLOR = {
'STRONG BUY': 'var(--buy)',
'BUY': 'var(--buy)',
'HOLD': 'var(--hold)',
'SELL': 'var(--sell)',
'STRONG SELL':'var(--sell)',
}
const REC_BG = {
'STRONG BUY': 'rgba(0,232,122,0.12)',
'BUY': 'rgba(0,232,122,0.08)',
'HOLD': 'rgba(255,204,68,0.08)',
'SELL': 'rgba(255,68,102,0.08)',
'STRONG SELL':'rgba(255,68,102,0.12)',
}
function ScoreBar({ score }) {
const pct = Math.round(((score + 1) / 2) * 100)
const color = score > 0.1 ? 'var(--buy)' : score < -0.1 ? 'var(--sell)' : 'var(--hold)'
return (
<div style={{ display:'flex', alignItems:'center', gap:'0.5rem', flex:1 }}>
<div style={{ flex:1, height:4, background:'rgba(255,255,255,0.06)', borderRadius:2, overflow:'hidden' }}>
<div style={{ width:`${pct}%`, height:'100%', background:color, borderRadius:2, transition:'width 0.8s ease' }} />
</div>
<span style={{ fontSize:'0.75rem', color, minWidth:40, textAlign:'right', fontWeight:600 }}>
{score > 0 ? '+' : ''}{score.toFixed(2)}
</span>
</div>
)
}
export default function PortfolioView({ onAnalyse }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const listRef = useRef(null)
useEffect(() => {
fetch('/portfolio')
.then(r => { if (!r.ok) throw new Error('Failed to load portfolio'); return r.json() })
.then(d => { setData(d); setLoading(false) })
.catch(e => { setError(e.message); setLoading(false) })
}, [])
useEffect(() => {
if (!data) return
anime({
targets: '.holding-row',
translateY: [20, 0],
opacity: [0, 1],
delay: anime.stagger(60),
easing: 'easeOutExpo',
duration: 500,
})
}, [data])
if (loading) return (
<div style={{ textAlign:'center', padding:'3rem', color:'var(--text-muted)' }}>
<div style={{ fontSize:'1.5rem', marginBottom:'0.75rem' }}>📊</div>
Loading your portfolio…
</div>
)
if (error) return (
<div style={{ textAlign:'center', padding:'3rem', color:'var(--sell)' }}>
⚠️ {error} — is the backend running?
</div>
)
const { holdings, summary } = data
return (
<div className="portfolio-view">
{/* Summary bar */}
<div className="portfolio-summary">
<div className="port-stat">
<span className="port-stat-val">{summary.total}</span>
<span className="port-stat-label">Holdings</span>
</div>
<div className="port-stat buy">
<span className="port-stat-val">{summary.buy}</span>
<span className="port-stat-label">Buy signals</span>
</div>
<div className="port-stat hold">
<span className="port-stat-val">{summary.hold}</span>
<span className="port-stat-label">Hold signals</span>
</div>
<div className="port-stat sell">
<span className="port-stat-val">{summary.sell}</span>
<span className="port-stat-label">Sell signals</span>
</div>
</div>
{/* Holdings table */}
<div className="card" ref={listRef}>
<div className="holdings-header">
<span>Ticker</span>
<span>Sector</span>
<span>Weight</span>
<span>Signal</span>
<span>Score</span>
<span>Conf.</span>
<span></span>
</div>
{holdings.map((h) => (
<div key={h.ticker} className="holding-row" style={{ opacity: 0 }}>
<div className="holding-ticker">
<span className="hticker">{h.ticker}</span>
<span className="hname">{h.name?.split(' ').slice(0,3).join(' ')}</span>
</div>
<span className="holding-sector">{h.sector}</span>
<span className="holding-weight">{h.pct_of_portfolio}%</span>
<span
className="holding-rec"
style={{
color: REC_COLOR[h.recommendation] || 'var(--text-muted)',
background: REC_BG[h.recommendation] || 'transparent',
}}
>
{h.recommendation}
</span>
<div className="holding-score">
<ScoreBar score={h.final_score} />
</div>
<span className="holding-conf" style={{ color: h.confidence >= 60 ? 'var(--text)' : 'var(--text-muted)' }}>
{h.confidence}%
</span>
<button
className="holding-analyse"
onClick={() => onAnalyse(h.ticker)}
title={`Analyse ${h.ticker}`}
>
→
</button>
</div>
))}
</div>
<p className="portfolio-note">
Signals are quick estimates — click → on any row for a full deep analysis.
</p>
</div>
)
}
|