WM01 / tracker.html
AndyKandy26's picture
Upload 9 files
00e4c29 verified
<!DOCTYPE html>
<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>