Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FinWise — Investment Tracker</title> | |
| <link rel="stylesheet" href="shared.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> | |
| <style> | |
| .tracker-header-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .holding-row td:first-child { font-weight: 700; } | |
| .gain-cell { font-family: var(--font-mono); font-weight: 700; } | |
| .ticker-cell { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .ticker-logo { | |
| width: 34px; height: 34px; | |
| border-radius: 8px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 9px; | |
| font-weight: 800; | |
| font-family: var(--font-mono); | |
| color: var(--bg); | |
| flex-shrink: 0; | |
| } | |
| .ticker-name-sub { font-size: 11px; color: var(--text2); font-weight: 400; } | |
| .mini-chart { width: 80px; height: 34px; } | |
| .pnl-bar-mini { height: 3px; border-radius: 2px; margin-top: 4px; } | |
| .edit-input { | |
| background: transparent; | |
| border: none; | |
| border-bottom: 1px dashed var(--border2); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| width: 80px; | |
| padding: 2px 4px; | |
| text-align: right; | |
| outline: none; | |
| } | |
| .edit-input:focus { border-bottom-color: var(--cyan); } | |
| .add-holding-form { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 12px; | |
| padding: 16px; | |
| background: var(--bg3); | |
| border-radius: var(--r-sm); | |
| border: 1px dashed var(--border2); | |
| margin-top: 16px; | |
| } | |
| .summary-strip { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px,1fr)); | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .ss-item { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--r-sm); | |
| padding: 14px 16px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .ss-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text3); font-weight: 700; } | |
| .ss-val { font-family: var(--font-head); font-size: 20px; font-weight: 800; } | |
| .period-tabs { | |
| display: flex; | |
| gap: 4px; | |
| background: var(--bg3); | |
| border-radius: 8px; | |
| padding: 3px; | |
| } | |
| .period-tab { | |
| padding: 6px 14px; | |
| border-radius: 6px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text2); | |
| font-size: 12px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| font-family: var(--font-body); | |
| transition: all var(--transition); | |
| } | |
| .period-tab.active { background: var(--card); color: var(--cyan); } | |
| .sort-header { cursor: pointer; user-select: none; white-space: nowrap; } | |
| .sort-header:hover { color: var(--cyan); } | |
| .sort-arrow { margin-left: 4px; opacity: 0.5; font-size: 10px; } | |
| .delete-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text3); | |
| cursor: pointer; | |
| font-size: 16px; | |
| padding: 4px; | |
| border-radius: 4px; | |
| transition: all var(--transition); | |
| } | |
| .delete-btn:hover { color: var(--rose); background: rgba(244,63,94,0.1); } | |
| .watch-tag { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 10px; | |
| font-weight: 700; | |
| padding: 2px 8px; | |
| border-radius: 100px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-shell"> | |
| <nav class="sidebar"> | |
| <div class="sidebar-logo"> | |
| <div class="logo-mark"> | |
| <div class="logo-icon">📈</div> | |
| <div><div class="logo-text">FinWise</div><div class="logo-sub">Smart Investing</div></div> | |
| </div> | |
| </div> | |
| <div class="nav-section"> | |
| <div class="nav-label">Main</div> | |
| <a href="index.html" class="nav-item"><span class="nav-icon">🏠</span> Dashboard</a> | |
| <a href="portfolio.html" class="nav-item"><span class="nav-icon">📊</span> Portfolio Builder</a> | |
| <a href="risk.html" class="nav-item"><span class="nav-icon">🎯</span> Risk Analyzer</a> | |
| <a href="tracker.html" class="nav-item"><span class="nav-icon">📈</span> Tracker</a> | |
| <div class="nav-label">Tools</div> | |
| <a href="calculators.html" class="nav-item"><span class="nav-icon">🧮</span> Calculators</a> | |
| <a href="insights.html" class="nav-item"><span class="nav-icon">💡</span> Insights</a> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <div class="market-ticker">Live Market</div> | |
| <div id="sidebar-tickers"></div> | |
| </div> | |
| </nav> | |
| <main class="main-content"> | |
| <div class="page-header fade-in"> | |
| <div class="page-title">Investment <span>Tracker</span></div> | |
| <div class="page-subtitle">Track your holdings, gains, and portfolio performance</div> | |
| </div> | |
| <!-- Summary Strip --> | |
| <div class="summary-strip fade-in"> | |
| <div class="ss-item"> | |
| <div class="ss-label">Total Value</div> | |
| <div class="ss-val" id="total-val" style="color:var(--cyan)">—</div> | |
| </div> | |
| <div class="ss-item"> | |
| <div class="ss-label">Total Cost</div> | |
| <div class="ss-val" id="total-cost">—</div> | |
| </div> | |
| <div class="ss-item"> | |
| <div class="ss-label">Total Gain</div> | |
| <div class="ss-val" id="total-gain">—</div> | |
| </div> | |
| <div class="ss-item"> | |
| <div class="ss-label">Gain %</div> | |
| <div class="ss-val" id="total-gain-pct">—</div> | |
| </div> | |
| <div class="ss-item"> | |
| <div class="ss-label">Best Performer</div> | |
| <div class="ss-val" id="best-performer" style="color:var(--emerald)">—</div> | |
| </div> | |
| <div class="ss-item"> | |
| <div class="ss-label">Holdings</div> | |
| <div class="ss-val" id="holdings-count" style="color:var(--violet)">—</div> | |
| </div> | |
| </div> | |
| <!-- Performance Chart --> | |
| <div class="card fade-in fade-in-1" style="margin-bottom:20px"> | |
| <div class="flex justify-between items-center" style="margin-bottom:16px"> | |
| <div class="card-title" style="margin-bottom:0">📈 Performance History</div> | |
| <div class="period-tabs"> | |
| <button class="period-tab" onclick="setPeriod(7)">1W</button> | |
| <button class="period-tab" onclick="setPeriod(30)">1M</button> | |
| <button class="period-tab active" onclick="setPeriod(90)">3M</button> | |
| <button class="period-tab" onclick="setPeriod(180)">6M</button> | |
| <button class="period-tab" onclick="setPeriod(365)">1Y</button> | |
| </div> | |
| </div> | |
| <div style="height:220px;position:relative"> | |
| <canvas id="perfChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Holdings Table --> | |
| <div class="card fade-in fade-in-2"> | |
| <div class="tracker-header-bar"> | |
| <div class="section-title" style="margin-bottom:0">📋 Holdings</div> | |
| <div class="flex gap-8"> | |
| <input type="text" id="search-input" placeholder="🔍 Search ticker..." style="width:160px;padding:8px 12px;font-size:13px"> | |
| <button class="btn btn-primary btn-sm" onclick="toggleAddForm()">+ Add Holding</button> | |
| </div> | |
| </div> | |
| <div class="table-wrap"> | |
| <table id="holdings-table"> | |
| <thead> | |
| <tr> | |
| <th>Asset</th> | |
| <th class="sort-header" onclick="sortBy('currentPrice')">Price <span class="sort-arrow">↕</span></th> | |
| <th class="sort-header" onclick="sortBy('shares')">Shares <span class="sort-arrow">↕</span></th> | |
| <th class="sort-header" onclick="sortBy('totalValue')">Value <span class="sort-arrow">↕</span></th> | |
| <th>Avg Cost</th> | |
| <th class="sort-header" onclick="sortBy('gain')">Gain/Loss <span class="sort-arrow">↕</span></th> | |
| <th class="sort-header" onclick="sortBy('gainPct')">Return <span class="sort-arrow">↕</span></th> | |
| <th>Alloc</th> | |
| <th></th> | |
| </tr> | |
| </thead> | |
| <tbody id="holdings-body"></tbody> | |
| </table> | |
| </div> | |
| <!-- Add Holding Form --> | |
| <div class="add-holding-form hidden" id="add-form"> | |
| <div class="field-group" style="margin-bottom:0"> | |
| <label class="field-label">Ticker</label> | |
| <select id="new-ticker" style="padding:9px 12px"> | |
| <option value="">Select…</option> | |
| <option value="VOO">VOO — Vanguard S&P 500</option> | |
| <option value="QQQ">QQQ — Nasdaq 100</option> | |
| <option value="NVDA">NVDA — NVIDIA</option> | |
| <option value="AAPL">AAPL — Apple</option> | |
| <option value="AMZN">AMZN — Amazon</option> | |
| <option value="TSLA">TSLA — Tesla</option> | |
| <option value="BND">BND — Bond ETF</option> | |
| <option value="GLD">GLD — Gold Trust</option> | |
| <option value="WMT">WMT — Walmart</option> | |
| <option value="MCD">MCD — McDonald's</option> | |
| <option value="VTI">VTI — Total Market</option> | |
| </select> | |
| </div> | |
| <div class="field-group" style="margin-bottom:0"> | |
| <label class="field-label">Shares</label> | |
| <input type="number" id="new-shares" placeholder="e.g. 5" min="0.001" step="0.001" style="padding:9px 12px"> | |
| </div> | |
| <div class="field-group" style="margin-bottom:0"> | |
| <label class="field-label">Avg Buy Price</label> | |
| <input type="number" id="new-cost" placeholder="e.g. 450.00" min="0" step="0.01" style="padding:9px 12px"> | |
| </div> | |
| <div style="display:flex;gap:8px;align-items:flex-end"> | |
| <button class="btn btn-emerald btn-full" onclick="addHolding()">Add</button> | |
| <button class="btn btn-ghost" onclick="toggleAddForm()">✕</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Allocation Chart + Distribution --> | |
| <div class="grid-2 fade-in fade-in-3" style="margin-top:20px"> | |
| <div class="card"> | |
| <div class="card-title">🥧 Portfolio Allocation</div> | |
| <div style="height:200px;position:relative"> | |
| <canvas id="allocChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">🏆 Top Performers</div> | |
| <div id="top-performers"></div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <nav class="bottom-nav"> | |
| <div class="bottom-nav-inner"> | |
| <a href="index.html" class="bottom-nav-item"><span class="bnav-icon">🏠</span>Home</a> | |
| <a href="portfolio.html" class="bottom-nav-item"><span class="bnav-icon">📊</span>Portfolio</a> | |
| <a href="risk.html" class="bottom-nav-item"><span class="bnav-icon">🎯</span>Risk</a> | |
| <a href="tracker.html" class="bottom-nav-item"><span class="bnav-icon">📈</span>Track</a> | |
| <a href="calculators.html" class="bottom-nav-item"><span class="bnav-icon">🧮</span>Calc</a> | |
| <a href="insights.html" class="bottom-nav-item"><span class="bnav-icon">💡</span>Insights</a> | |
| </div> | |
| </nav> | |
| <script src="shared.js"></script> | |
| <script> | |
| let holdings = []; | |
| let sortField = 'totalValue'; | |
| let sortAsc = false; | |
| let perfChart = null, allocChart = null; | |
| let currentPeriod = 90; | |
| const AVG_COSTS = { VOO:420, QQQ:390, NVDA:640, AAPL:170, BND:74, GLD:190, AMZN:165, VTI:210, TSLA:210, WMT:63, MCD:270 }; | |
| function initHoldings() { | |
| const portfolio = getPortfolio(); | |
| holdings = portfolio.assets.map(a => ({ | |
| ticker: a.ticker, | |
| name: a.name, | |
| color: a.color, | |
| type: a.type, | |
| shares: a.shares, | |
| currentPrice: MARKET_PRICES[a.ticker]?.price || a.price, | |
| avgCost: AVG_COSTS[a.ticker] || a.price * 0.9, | |
| })); | |
| computeAndRender(); | |
| } | |
| function computeAndRender() { | |
| const totalVal = holdings.reduce((s,h) => s + h.shares * h.currentPrice, 0); | |
| holdings.forEach(h => { | |
| h.totalValue = h.shares * h.currentPrice; | |
| h.totalCost = h.shares * h.avgCost; | |
| h.gain = h.totalValue - h.totalCost; | |
| h.gainPct = h.totalCost > 0 ? (h.gain / h.totalCost) * 100 : 0; | |
| h.alloc = totalVal > 0 ? (h.totalValue / totalVal) * 100 : 0; | |
| }); | |
| updateSummary(totalVal); | |
| renderTable(); | |
| renderPerfChart(currentPeriod); | |
| renderAllocChart(); | |
| renderTopPerformers(); | |
| } | |
| function updateSummary(totalVal) { | |
| const totalCost = holdings.reduce((s,h) => s + h.totalCost, 0); | |
| const totalGain = totalVal - totalCost; | |
| const gainPct = totalCost > 0 ? (totalGain/totalCost)*100 : 0; | |
| const best = holdings.length > 0 ? holdings.reduce((m,h) => h.gainPct > m.gainPct ? h : m, holdings[0]) : null; | |
| document.getElementById('total-val').textContent = fmt$(totalVal); | |
| document.getElementById('total-cost').textContent = fmt$(totalCost); | |
| const gainEl = document.getElementById('total-gain'); | |
| gainEl.textContent = (totalGain>=0?'+':'') + fmt$(totalGain); | |
| gainEl.style.color = totalGain >= 0 ? 'var(--emerald)' : 'var(--rose)'; | |
| const gainPctEl = document.getElementById('total-gain-pct'); | |
| gainPctEl.textContent = fmtPct(gainPct); | |
| gainPctEl.style.color = gainPct >= 0 ? 'var(--emerald)' : 'var(--rose)'; | |
| document.getElementById('best-performer').textContent = best ? best.ticker : '—'; | |
| document.getElementById('holdings-count').textContent = holdings.length; | |
| } | |
| function renderTable() { | |
| const query = document.getElementById('search-input').value.toLowerCase(); | |
| let rows = [...holdings].filter(h => h.ticker.toLowerCase().includes(query) || h.name.toLowerCase().includes(query)); | |
| rows.sort((a,b) => sortAsc ? a[sortField]-b[sortField] : b[sortField]-a[sortField]); | |
| const body = document.getElementById('holdings-body'); | |
| body.innerHTML = rows.map((h,i) => { | |
| const mktData = MARKET_PRICES[h.ticker] || {}; | |
| const dayChg = mktData.changePct || 0; | |
| const badgeClass = h.type==='ETF'?'badge-cyan':h.type==='Bond'?'badge-violet':h.type==='Commodity'?'badge-amber':'badge-emerald'; | |
| return ` | |
| <tr class="holding-row" id="row-${h.ticker}"> | |
| <td> | |
| <div class="ticker-cell"> | |
| <div class="ticker-logo" style="background:${h.color}">${h.ticker}</div> | |
| <div> | |
| <div style="font-weight:700">${h.ticker} <span class="badge ${badgeClass}" style="font-size:9px">${h.type}</span></div> | |
| <div class="ticker-name-sub">${h.name}</div> | |
| <div style="font-size:11px;margin-top:2px;color:${dayChg>=0?'var(--emerald)':'var(--rose)'}">${dayChg>=0?'▲':'▼'} ${Math.abs(dayChg).toFixed(2)}% today</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="mono">${fmt$(h.currentPrice)}</td> | |
| <td class="mono"> | |
| <input class="edit-input" type="number" value="${h.shares.toFixed(3)}" step="0.001" min="0" | |
| onchange="updateShares('${h.ticker}', this.value)"> | |
| </td> | |
| <td class="mono bold">${fmt$(h.totalValue)}</td> | |
| <td class="mono"> | |
| <input class="edit-input" type="number" value="${h.avgCost.toFixed(2)}" step="0.01" min="0" | |
| onchange="updateAvgCost('${h.ticker}', this.value)"> | |
| </td> | |
| <td class="gain-cell ${h.gain>=0?'up':'down'}">${h.gain>=0?'+':''}${fmt$(h.gain)}</td> | |
| <td class="gain-cell ${h.gainPct>=0?'up':'down'}">${fmtPct(h.gainPct)}</td> | |
| <td style="min-width:90px"> | |
| <div style="font-size:12px;font-family:var(--font-mono)">${h.alloc.toFixed(1)}%</div> | |
| <div class="pnl-bar-mini" style="width:${Math.min(h.alloc,100)}%;background:${h.color}"></div> | |
| </td> | |
| <td><button class="delete-btn" onclick="removeHolding('${h.ticker}')">🗑</button></td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| } | |
| function updateShares(ticker, val) { | |
| const h = holdings.find(h=>h.ticker===ticker); | |
| if (h) { h.shares = parseFloat(val)||0; computeAndRender(); } | |
| } | |
| function updateAvgCost(ticker, val) { | |
| const h = holdings.find(h=>h.ticker===ticker); | |
| if (h) { h.avgCost = parseFloat(val)||0; computeAndRender(); } | |
| } | |
| function removeHolding(ticker) { | |
| holdings = holdings.filter(h=>h.ticker!==ticker); | |
| computeAndRender(); | |
| showToast(`${ticker} removed from tracker`); | |
| } | |
| function sortBy(field) { | |
| if (sortField === field) sortAsc = !sortAsc; | |
| else { sortField = field; sortAsc = false; } | |
| renderTable(); | |
| } | |
| function setPeriod(days) { | |
| currentPeriod = days; | |
| document.querySelectorAll('.period-tab').forEach(b => b.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| renderPerfChart(days); | |
| } | |
| function renderPerfChart(days) { | |
| const totalCost = holdings.reduce((s,h)=>s+h.totalCost,0) || 10000; | |
| const history = generateHistory(days, totalCost); | |
| const ctx = document.getElementById('perfChart').getContext('2d'); | |
| if (perfChart) perfChart.destroy(); | |
| const labels = history.filter((_,i)=> days<=30 ? i%2===0 : days<=90 ? i%6===0 : i%14===0).map(d=>d.date); | |
| const values = history.filter((_,i)=> days<=30 ? i%2===0 : days<=90 ? i%6===0 : i%14===0).map(d=>d.value); | |
| const isUp = values[values.length-1] >= values[0]; | |
| const grad = ctx.createLinearGradient(0,0,0,220); | |
| grad.addColorStop(0, isUp ? 'rgba(16,185,129,0.25)' : 'rgba(244,63,94,0.25)'); | |
| grad.addColorStop(1, 'rgba(0,0,0,0)'); | |
| perfChart = new Chart(ctx, { | |
| type:'line', | |
| data: { labels, datasets:[{ | |
| data: values, | |
| borderColor: isUp ? '#10b981' : '#f43f5e', | |
| backgroundColor: grad, | |
| borderWidth: 2.5, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 0, | |
| pointHoverRadius: 5, | |
| }]}, | |
| options: { | |
| responsive:true, maintainAspectRatio:false, | |
| plugins:{ legend:{display:false}, tooltip:{ callbacks:{ label: ctx => ' '+fmt$(ctx.raw) } } }, | |
| scales:{ | |
| x:{ grid:{display:false}, ticks:{font:{size:11}} }, | |
| y:{ grid:{color:'rgba(34,211,238,0.06)'}, ticks:{ callback: v => '$'+(v/1000).toFixed(0)+'K', font:{size:11} } } | |
| }, | |
| interaction:{ intersect:false, mode:'index' } | |
| } | |
| }); | |
| } | |
| function renderAllocChart() { | |
| const ctx = document.getElementById('allocChart').getContext('2d'); | |
| if (allocChart) allocChart.destroy(); | |
| allocChart = new Chart(ctx, { | |
| type:'doughnut', | |
| data:{ | |
| labels: holdings.map(h=>h.ticker), | |
| datasets:[{ | |
| data: holdings.map(h=>h.alloc), | |
| backgroundColor: holdings.map(h=>h.color), | |
| borderColor:'rgba(5,13,26,0.8)', | |
| borderWidth:3, | |
| hoverOffset:6 | |
| }] | |
| }, | |
| options:{ | |
| responsive:true, maintainAspectRatio:false, | |
| cutout:'68%', | |
| plugins:{ legend:{ position:'right', labels:{ color:'#8faac8', font:{size:11}, boxWidth:12 } } } | |
| } | |
| }); | |
| } | |
| function renderTopPerformers() { | |
| const sorted = [...holdings].sort((a,b)=>b.gainPct-a.gainPct); | |
| document.getElementById('top-performers').innerHTML = sorted.slice(0,5).map((h,i) => ` | |
| <div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid rgba(34,211,238,0.06)"> | |
| <div style="font-size:16px">${i===0?'🥇':i===1?'🥈':i===2?'🥉':'📊'}</div> | |
| <div class="ticker-logo" style="background:${h.color};width:30px;height:30px;font-size:8px">${h.ticker}</div> | |
| <div style="flex:1"> | |
| <div style="font-weight:700;font-size:14px">${h.ticker}</div> | |
| <div style="font-size:11px;color:var(--text2)">${fmt$(h.totalValue)}</div> | |
| </div> | |
| <div style="font-family:var(--font-mono);font-weight:700;color:${h.gainPct>=0?'var(--emerald)':'var(--rose)'}"> | |
| ${fmtPct(h.gainPct)} | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function toggleAddForm() { | |
| document.getElementById('add-form').classList.toggle('hidden'); | |
| } | |
| function addHolding() { | |
| const ticker = document.getElementById('new-ticker').value; | |
| const shares = parseFloat(document.getElementById('new-shares').value); | |
| const cost = parseFloat(document.getElementById('new-cost').value); | |
| if (!ticker || !shares || !cost) { showToast('Please fill all fields', 'error'); return; } | |
| if (holdings.find(h=>h.ticker===ticker)) { showToast('Already in portfolio', 'error'); return; } | |
| const mkt = MARKET_PRICES[ticker] || { price: cost }; | |
| const assetDef = { VOO:'Vanguard S&P 500 ETF', QQQ:'Invesco Nasdaq 100', NVDA:'NVIDIA Corp', AAPL:'Apple Inc.', | |
| AMZN:'Amazon.com Inc.', TSLA:'Tesla Inc.', BND:'Vanguard Bond ETF', GLD:'SPDR Gold Trust', | |
| WMT:'Walmart Inc.', MCD:"McDonald's Corp", VTI:'Vanguard Total Market' }; | |
| const typeMap = { VOO:'ETF', VTI:'ETF', QQQ:'ETF', BND:'Bond', GLD:'Commodity', SLV:'Commodity' }; | |
| holdings.push({ | |
| ticker, | |
| name: assetDef[ticker] || ticker, | |
| color: ASSET_COLORS[holdings.length % ASSET_COLORS.length], | |
| type: typeMap[ticker] || 'Stock', | |
| shares, | |
| currentPrice: mkt.price, | |
| avgCost: cost, | |
| }); | |
| document.getElementById('new-ticker').value = ''; | |
| document.getElementById('new-shares').value = ''; | |
| document.getElementById('new-cost').value = ''; | |
| toggleAddForm(); | |
| computeAndRender(); | |
| showToast(`✅ ${ticker} added to tracker`); | |
| } | |
| document.getElementById('search-input').addEventListener('input', renderTable); | |
| document.addEventListener('DOMContentLoaded', () => { | |
| applyChartDefaults(); | |
| initHoldings(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |