Jimin Huang commited on
Commit
6fc3606
·
1 Parent(s): 6df9459

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +20 -278
src/views/LiveView.vue CHANGED
@@ -1,6 +1,5 @@
1
  <template>
2
  <div class="live">
3
- <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
@@ -13,39 +12,22 @@
13
  </div>
14
  </header>
15
 
16
- <!-- Chart -->
17
  <section class="panel panel--chart">
18
- <CompareChartE
19
- v-if="winnersForChart.length"
20
- :selected="winnersForChart"
21
- :visible="true"
22
- :mode="mode"
23
- />
24
- <div v-else class="empty">
25
- No data for <strong>{{ asset }}</strong> yet. Check Supabase runs or try another asset.
26
- </div>
27
  </section>
28
 
29
- <!-- F1 Scoreboard Cards -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
  <div class="cards-grid-f1">
32
- <article
33
- v-for="c in cards"
34
- :key="c.key"
35
- class="card-f1"
36
- :class="{ winner: c.isWinner, bh: c.kind==='bh', neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }"
37
- :style="{ '--bar': (c.barPct ?? 0) + '%'}"
38
- >
39
- <!-- Rank / BH badge -->
40
  <div v-if="c.rank" class="rank">P{{ c.rank }}</div>
41
  <div v-else-if="c.kind==='bh'" class="rank bh-badge">B&H</div>
42
- <span v-if="c.isWinner" class="crown" aria-label="Top performer">👑</span>
43
 
44
- <!-- Header: logo + names -->
45
  <header class="head">
46
  <div class="logo">
47
  <img v-if="c.logo" :src="c.logo" alt="" />
48
- <div v-else class="logo__fallback" aria-hidden="true"></div>
49
  </div>
50
  <div class="names">
51
  <div class="agent">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
@@ -53,23 +35,18 @@
53
  </div>
54
  </header>
55
 
56
- <!-- Net value row -->
57
  <div class="net">
58
  <div class="net__label">Net value</div>
59
  <div class="net__value">{{ fmtUSD(c.balance) }}</div>
60
  </div>
61
 
62
- <!-- Performance bar (vs B&H) -->
63
  <div class="bar" :class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }">
64
  <span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
65
  </div>
66
 
67
- <!-- Bottom row: chips + EOD -->
68
  <div class="bottom">
69
  <div class="chips">
70
- <span class="chip" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">
71
- {{ signedMoney(profitOf(c)) }}
72
- </span>
73
  <span class="chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
74
  <template v-if="c.kind==='agent'">
75
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
@@ -78,7 +55,9 @@
78
  <template v-else>—</template>
79
  </span>
80
  </div>
81
- <div class="eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
 
 
82
  </div>
83
  </article>
84
  </div>
@@ -95,18 +74,13 @@ import { ref, computed, onMounted, watch, shallowRef } from 'vue'
95
  import AssetTabs from '../components/AssetTabs.vue'
96
  import CompareChartE from '../components/CompareChartE.vue'
97
  import { dataService } from '../lib/dataService'
98
-
99
- /* --- same helpers as chart --- */
100
  import { getAllDecisions } from '../lib/dataCache'
101
  import { readAllRawDecisions } from '../lib/idb'
102
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
103
  import { STRATEGIES } from '../lib/strategies'
104
  import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
105
 
106
- /* ---------- config ---------- */
107
- const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
108
- const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
109
-
110
  const ASSET_ICONS = {
111
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
112
  ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
@@ -114,263 +88,31 @@ const ASSET_ICONS = {
114
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
115
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
116
  }
117
- const AGENT_LOGOS = {}
118
- const ASSET_CUTOFF = { BTC: '2025-08-01' }
119
-
120
- /* ---------- state ---------- */
121
  const mode = ref('usd')
122
  const asset = ref('BTC')
123
  const rowsRef = ref([])
124
  let allDecisions = []
125
  const cards = shallowRef([])
126
 
127
- /* ---------- bootstrap ---------- */
128
  onMounted(async () => {
129
  try {
130
- if (!dataService.loaded && !dataService.loading) {
131
- await dataService.load(false)
132
- }
133
- } catch (e) { console.error('LiveView: dataService.load failed', e) }
134
- rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
135
- if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
136
-
137
- allDecisions = getAllDecisions() || []
138
- if (!allDecisions.length) {
139
- try {
140
- const cached = await readAllRawDecisions()
141
- if (cached?.length) allDecisions = cached
142
- } catch {}
143
- }
144
  })
145
 
146
- /* ---------- helpers ---------- */
147
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
148
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
149
- const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
150
- const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
151
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
152
-
153
- /* rows for selected asset (exclude vanilla/vinilla) */
154
- const filteredRows = computed(() =>
155
- (rowsRef.value || []).filter(r => {
156
- if (r.asset !== asset.value) return false
157
- const name = (r?.agent_name || '').toLowerCase()
158
- return !EXCLUDED_AGENT_NAMES.has(name)
159
- })
160
- )
161
-
162
- /* winners: best model per agent (by leaderboard balance) */
163
- const winners = computed(() => {
164
- const byAgent = new Map()
165
- for (const r of filteredRows.value) {
166
- const k = r.agent_name
167
- const cur = byAgent.get(k)
168
- if (!cur || score(r) > score(cur)) byAgent.set(k, r)
169
- }
170
- return [...byAgent.values()]
171
- })
172
-
173
- /* chart selections */
174
- const winnersForChart = computed(() =>
175
- winners.value.map(w => ({
176
- agent_name: w.agent_name,
177
- asset: w.asset,
178
- model: w.model,
179
- strategy: w.strategy,
180
- decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
181
- }))
182
- )
183
-
184
- /* stable key to avoid identity churn */
185
- const winnersKey = computed(() => {
186
- const sels = winnersForChart.value || []
187
- return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
188
- })
189
-
190
- /* ---------- PERF (chart parity) ---------- */
191
- async function buildSeq(sel) {
192
- const { agent_name: agentName, asset: assetCode, model } = sel
193
- const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
194
- let seq = ids.length
195
- ? allDecisions.filter(r => ids.includes(r.id))
196
- : allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
197
-
198
- seq.sort((a,b) => (a.date > b.date ? 1 : -1))
199
-
200
- const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
201
- let filtered = seq
202
- if (!isCrypto) filtered = await filterRowsToNyseTradingDays(seq)
203
-
204
- const cutoff = ASSET_CUTOFF[assetCode]
205
- if (cutoff) {
206
- const t0 = new Date(cutoff + 'T00:00:00Z')
207
- filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
208
- }
209
- return filtered
210
- }
211
-
212
- async function computeEquities(sel) {
213
- const seq = await buildSeq(sel)
214
- if (!seq.length) return null
215
-
216
- const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
217
-
218
- const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
219
- const bhY = computeBuyHoldEquity(seq, 100000) || []
220
- const lastIdx = Math.min(stratY.length, bhY.length) - 1
221
- if (lastIdx < 0) return null
222
-
223
- return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
224
- }
225
-
226
- /* build cards whenever winners/asset change */
227
- let computing = false
228
- watch(
229
- () => [asset.value, winnersKey.value],
230
- async () => {
231
- if (computing) return
232
- if (!winnersForChart.value.length) { cards.value = []; return }
233
- computing = true
234
- try {
235
- const perfs = (await Promise.all(
236
- winnersForChart.value.map(async sel => ({ sel, perf: await computeEquities(sel) }))
237
- )).filter(x => x.perf)
238
-
239
- if (!perfs.length) { cards.value = []; return }
240
-
241
- // Buy & Hold card from the asset’s aligned BH series
242
- const first = perfs[0]
243
- const assetCode = first.sel.asset
244
- const bhCard = {
245
- key: `bh|${assetCode}`,
246
- kind: 'bh',
247
- title: 'Buy & Hold',
248
- subtitle: assetCode,
249
- balance: first.perf.bhLast,
250
- date: first.perf.date,
251
- logo: ASSET_ICONS[assetCode] || null,
252
- profitUsd: (first.perf.bhLast ?? 0) - 100000,
253
- gapUsd: 0,
254
- gapPct: 0,
255
- isWinner: false,
256
- rank: null,
257
- barPct: 0
258
- }
259
-
260
- // Agent cards (gap vs BH)
261
- const agentCards = perfs.map(({ sel, perf }) => {
262
- const gapUsd = perf.stratLast - perf.bhLast
263
- const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
264
- const profitUsd = (perf.stratLast ?? 0) - 100000
265
- return {
266
- key: `agent|${sel.agent_name}|${sel.model}`,
267
- kind: 'agent',
268
- title: sel.agent_name,
269
- subtitle: sel.model,
270
- balance: perf.stratLast,
271
- date: perf.date,
272
- logo: AGENT_LOGOS[sel.agent_name] || null,
273
- gapUsd, gapPct,
274
- profitUsd,
275
- isWinner: false,
276
- rank: null,
277
- barPct: 0
278
- }
279
- })
280
-
281
- // Rank by balance (agents only)
282
- agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
283
- agentCards.forEach((c, i) => { c.rank = i + 1 })
284
-
285
- // Winner flag
286
- if (agentCards.length) agentCards[0].isWinner = true
287
-
288
- // Perf bar width scaled to max |gapUsd|
289
- const maxAbsGap = Math.max(1, ...agentCards.map(c => Math.abs(c.gapUsd ?? 0)))
290
- agentCards.forEach(c => { c.barPct = Math.max(3, Math.round((Math.abs(c.gapUsd ?? 0) / maxAbsGap) * 100)) })
291
-
292
- // Build final list: BH first, then agents in rank order
293
- cards.value = [bhCard, ...agentCards].slice(0,5)
294
- } catch (e) {
295
- console.error('LiveView: compute cards failed', e)
296
- cards.value = []
297
- } finally { computing = false }
298
- },
299
- { immediate: true }
300
- )
301
  </script>
302
 
303
  <style scoped>
304
- .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; background: #ffffff; }
305
-
306
- /* toolbar */
307
- .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
308
- .mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
309
- .mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
310
-
311
- /* panels */
312
- .panel { background: #ffffff; border: 1px solid #E7ECF3; border-radius: 14px; }
313
- .panel--chart { padding: 10px 10px 2px; }
314
- .panel--cards { padding: 12px; }
315
-
316
- /* empty */
317
- .empty { padding: 14px; border: 1px dashed #D7DDE7; border-radius: 12px; color: #6B7280; font-size: .92rem; background: #ffffff; }
318
-
319
- /* GRID */
320
  .cards-grid-f1 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0,1fr)); }
321
- @media (max-width: 1400px) { .cards-grid-f1 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
322
- @media (max-width: 1100px) { .cards-grid-f1 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
323
- @media (max-width: 900px) { .cards-grid-f1 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
324
- @media (max-width: 640px) { .cards-grid-f1 { grid-template-columns: 1fr; } }
325
-
326
- /* F1 Card */
327
- .card-f1 {
328
- position: relative;
329
- display: grid;
330
- grid-template-rows: auto auto auto; /* head, net, bottom */
331
- gap: 10px;
332
- padding: 16px 16px 18px;
333
- min-height: 210px;
334
- border-radius: 14px;
335
- background: linear-gradient(145deg,#ffffff,#fafbfd 55%,#ffffff 100%);
336
- border: 1px solid #E7ECF3;
337
- box-shadow: 0 1px 2px rgba(16,24,40,.03), 0 4px 12px rgba(16,24,40,.04);
338
- color: #0f172a;
339
- transition: transform .18s ease, box-shadow .2s ease, border-color .2s ease;
340
- }
341
- .card-f1:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(16,24,40,.08); border-color: #D9E2EF; }
342
- .card-f1.winner { border-color: #16a34a; box-shadow: 0 0 0 2px rgba(22,163,74,.18), 0 10px 26px rgba(16,24,40,.10); }
343
- .card-f1.bh { border-style: dashed; opacity: 1; }
344
-
345
- /* Rank / Crown */
346
- .rank { position: absolute; top: 10px; left: 12px; font-weight: 900; font-size: 18px; color: rgba(15,23,42,.30); letter-spacing: .04em; }
347
- .rank.bh-badge { font-size: 12px; background: rgba(15,23,42,.06); padding: 2px 6px; border-radius: 6px; color: #4b5563; }
348
- .crown { position: absolute; top: 12px; right: 12px; font-size: 18px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.12)); }
349
-
350
- /* Head */
351
- .head { display: grid; grid-template-columns: 40px minmax(0,1fr); align-items: center; gap: 10px; }
352
- .logo { width: 40px; height: 40px; border-radius: 10px; background: #f3f4f6; display: grid; place-items: center; overflow: hidden; border: 1px solid #E7ECF3; }
353
- .logo img { width: 100%; height: 100%; object-fit: contain; }
354
- .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #e5e7eb; }
355
- .names { min-width: 0; }
356
- .agent { font-size: 16px; font-weight: 900; letter-spacing: .02em; text-transform: uppercase; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
357
- .model { font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
358
-
359
- /* Net row */
360
- .net { display: grid; grid-template-columns: 1fr auto; align-items: end; }
361
- .net__label { font-size: 12px; color: #6b7280; }
362
- .net__value { font-size: clamp(22px, 2.2vw, 30px); font-weight: 900; letter-spacing: -.01em; color: #0f172a; }
363
-
364
- /* Bar vs B&H */
365
- .bar { height: 6px; border-radius: 999px; background: #F2F4F8; overflow: hidden; border: 1px solid #E7ECF3; }
366
- .bar span { display: block; height: 100%; background: linear-gradient(90deg,#16a34a,#22c55e); width: var(--bar, 40%); transition: width .5s ease; }
367
- .bar.neg span { background: linear-gradient(90deg,#ef4444,#dc2626); }
368
-
369
- /* Bottom */
370
- .bottom { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 8px; }
371
- .chips { display: inline-flex; gap: 8px; }
372
- .chip { font-size: 12px; font-weight: 800; padding: 4px 8px; border-radius: 999px; background: #F6F8FB; color: #0f172a; border: 1px solid #E7ECF3; }
373
- .chip.pos { color: #0e7a3a; background: #E9F7EF; border-color: #d7f0e0; }
374
- .chip.neg { color: #B91C1C; background: #FBEAEA; border-color: #F3DADA; }
375
- .eod { font-size: 12px; color: #6b7280; }
376
  </style>
 
1
  <template>
2
  <div class="live">
 
3
  <header class="toolbar">
4
  <div class="toolbar__left">
5
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
 
12
  </div>
13
  </header>
14
 
 
15
  <section class="panel panel--chart">
16
+ <CompareChartE v-if="winnersForChart.length" :selected="winnersForChart" :visible="true" :mode="mode" />
17
+ <div v-else class="empty">No data for <strong>{{ asset }}</strong> yet.</div>
 
 
 
 
 
 
 
18
  </section>
19
 
 
20
  <section class="panel panel--cards" v-if="cards.length">
21
  <div class="cards-grid-f1">
22
+ <article v-for="c in cards" :key="c.key" class="card-f1" :class="{ winner: c.isWinner, bh: c.kind==='bh' }" :style="{ '--bar': (c.barPct ?? 0) + '%' }">
 
 
 
 
 
 
 
23
  <div v-if="c.rank" class="rank">P{{ c.rank }}</div>
24
  <div v-else-if="c.kind==='bh'" class="rank bh-badge">B&H</div>
25
+ <span v-if="c.isWinner" class="crown">👑</span>
26
 
 
27
  <header class="head">
28
  <div class="logo">
29
  <img v-if="c.logo" :src="c.logo" alt="" />
30
+ <div v-else class="logo__fallback"></div>
31
  </div>
32
  <div class="names">
33
  <div class="agent">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
 
35
  </div>
36
  </header>
37
 
 
38
  <div class="net">
39
  <div class="net__label">Net value</div>
40
  <div class="net__value">{{ fmtUSD(c.balance) }}</div>
41
  </div>
42
 
 
43
  <div class="bar" :class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }">
44
  <span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
45
  </div>
46
 
 
47
  <div class="bottom">
48
  <div class="chips">
49
+ <span class="chip" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">{{ signedMoney(profitOf(c)) }}</span>
 
 
50
  <span class="chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
51
  <template v-if="c.kind==='agent'">
52
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
 
55
  <template v-else>—</template>
56
  </span>
57
  </div>
58
+ <div class="footer-row">
59
+ <div class="eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
60
+ </div>
61
  </div>
62
  </article>
63
  </div>
 
74
  import AssetTabs from '../components/AssetTabs.vue'
75
  import CompareChartE from '../components/CompareChartE.vue'
76
  import { dataService } from '../lib/dataService'
 
 
77
  import { getAllDecisions } from '../lib/dataCache'
78
  import { readAllRawDecisions } from '../lib/idb'
79
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
80
  import { STRATEGIES } from '../lib/strategies'
81
  import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
82
 
83
+ const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA']
 
 
 
84
  const ASSET_ICONS = {
85
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
86
  ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
 
88
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
89
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
90
  }
 
 
 
 
91
  const mode = ref('usd')
92
  const asset = ref('BTC')
93
  const rowsRef = ref([])
94
  let allDecisions = []
95
  const cards = shallowRef([])
96
 
 
97
  onMounted(async () => {
98
  try {
99
+ if (!dataService.loaded && !dataService.loading) await dataService.load(false)
100
+ } catch (e) { console.error(e) }
101
+ rowsRef.value = dataService.tableRows || []
 
 
 
 
 
 
 
 
 
 
 
102
  })
103
 
 
104
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
105
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
106
+ const signedPct = (p) => `${p >= 0 ? '+' : '−'}${Math.abs(p * 100).toFixed(2)}%`
 
107
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  </script>
109
 
110
  <style scoped>
111
+ .live { background: #ffffff; max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
112
+ .toolbar { background: #ffffff; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  .cards-grid-f1 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0,1fr)); }
114
+ .card-f1 { background: #fff; border: 1px solid #E7ECF3; border-radius: 14px; box-shadow: 0 1px 2px rgba(16,24,40,.05); padding: 16px; display: flex; flex-direction: column; justify-content: space-between; }
115
+ .net__value { font-size: clamp(18px, 2vw, 26px); font-weight: 900; color: #0f172a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
116
+ .bottom { display: flex; flex-direction: column; gap: 6px; }
117
+ .footer-row { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #6b7280; margin-top: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </style>