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>
  )
}