Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FinWise — Portfolio Builder</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> | |
| .builder-steps { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 28px; | |
| background: var(--bg3); | |
| border-radius: var(--r-sm); | |
| padding: 4px; | |
| } | |
| .step-btn { | |
| flex: 1; | |
| padding: 10px 8px; | |
| border-radius: 6px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text2); | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| text-align: center; | |
| white-space: nowrap; | |
| } | |
| .step-btn.active { | |
| background: var(--card); | |
| color: var(--cyan); | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3); | |
| } | |
| .step-num { | |
| display: inline-flex; | |
| width: 20px; height: 20px; | |
| background: var(--bg3); | |
| border-radius: 50%; | |
| align-items: center; justify-content: center; | |
| font-size: 11px; | |
| margin-right: 6px; | |
| } | |
| .step-btn.active .step-num { background: var(--cyan); color: var(--bg); } | |
| .step-panel { display: none; } | |
| .step-panel.active { display: block; } | |
| .goal-cards { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | |
| gap: 12px; | |
| margin-bottom: 24px; | |
| } | |
| .goal-card { | |
| background: var(--bg3); | |
| border: 2px solid var(--border); | |
| border-radius: var(--r); | |
| padding: 20px 16px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| } | |
| .goal-card:hover { border-color: var(--border2); transform: translateY(-2px); } | |
| .goal-card.selected { border-color: var(--cyan); background: rgba(34,211,238,0.08); box-shadow: var(--glow-c); } | |
| .goal-icon { font-size: 32px; margin-bottom: 8px; } | |
| .goal-title { font-size: 13px; font-weight: 700; margin-bottom: 4px; } | |
| .goal-desc { font-size: 11px; color: var(--text2); } | |
| .risk-slider-wrap { | |
| padding: 20px; | |
| background: var(--bg3); | |
| border-radius: var(--r); | |
| margin-bottom: 20px; | |
| } | |
| .risk-labels { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 12px; | |
| color: var(--text2); | |
| margin-top: 10px; | |
| } | |
| .risk-profile-display { | |
| text-align: center; | |
| padding: 16px; | |
| background: rgba(34,211,238,0.06); | |
| border-radius: var(--r-sm); | |
| border: 1px solid var(--border2); | |
| margin-top: 16px; | |
| } | |
| .rp-icon { font-size: 36px; margin-bottom: 6px; } | |
| .rp-label { font-family: var(--font-head); font-size: 20px; font-weight: 700; } | |
| .rp-desc { font-size: 13px; color: var(--text2); margin-top: 4px; } | |
| .asset-card { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| padding: 16px; | |
| background: var(--bg3); | |
| border-radius: var(--r-sm); | |
| margin-bottom: 10px; | |
| border: 1px solid var(--border); | |
| transition: all var(--transition); | |
| } | |
| .asset-card:hover { border-color: var(--border2); } | |
| .asset-logo { | |
| width: 42px; height: 42px; | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-weight: 800; | |
| font-size: 11px; | |
| font-family: var(--font-mono); | |
| flex-shrink: 0; | |
| color: var(--bg); | |
| } | |
| .asset-info { flex: 1; min-width: 0; } | |
| .asset-ticker { font-weight: 700; font-size: 15px; } | |
| .asset-name { font-size: 12px; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .asset-type { margin-top: 3px; } | |
| .asset-sliders { flex: 2; } | |
| .asset-pct-val { | |
| font-family: var(--font-mono); | |
| font-size: 15px; | |
| font-weight: 700; | |
| color: var(--cyan); | |
| min-width: 46px; | |
| text-align: right; | |
| } | |
| .asset-dollar { font-size: 11px; color: var(--text2); text-align: right; margin-top: 2px; } | |
| .asset-remove { | |
| background: rgba(244,63,94,0.1); | |
| border: none; | |
| color: var(--rose); | |
| border-radius: 6px; | |
| width: 28px; height: 28px; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background var(--transition); | |
| flex-shrink: 0; | |
| } | |
| .asset-remove:hover { background: rgba(244,63,94,0.25); } | |
| .add-asset-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); | |
| gap: 8px; | |
| margin-top: 16px; | |
| } | |
| .add-asset-btn { | |
| padding: 10px 8px; | |
| background: var(--bg3); | |
| border: 1px dashed var(--border2); | |
| border-radius: var(--r-sm); | |
| color: var(--text2); | |
| font-size: 12px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| text-align: center; | |
| transition: all var(--transition); | |
| font-family: var(--font-body); | |
| } | |
| .add-asset-btn:hover { border-color: var(--cyan); color: var(--cyan); background: rgba(34,211,238,0.05); } | |
| .add-asset-btn.in-portfolio { border-style: solid; border-color: var(--emerald); color: var(--emerald); background: rgba(16,185,129,0.06); } | |
| .portfolio-summary-side { | |
| position: sticky; | |
| top: 20px; | |
| } | |
| .pie-wrap { height: 220px; position: relative; } | |
| .portfolio-total { | |
| text-align: center; | |
| padding: 16px; | |
| background: var(--bg3); | |
| border-radius: var(--r-sm); | |
| margin: 16px 0; | |
| } | |
| .pt-label { font-size: 12px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.06em; } | |
| .pt-value { font-family: var(--font-head); font-size: 28px; font-weight: 800; color: var(--cyan); } | |
| .balance-warning { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px 14px; | |
| border-radius: var(--r-sm); | |
| font-size: 13px; | |
| margin-bottom: 16px; | |
| } | |
| .balance-warning.ok { background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); color: var(--emerald); } | |
| .balance-warning.warn { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); color: var(--amber); } | |
| .balance-warning.err { background: rgba(244,63,94,0.1); border: 1px solid rgba(244,63,94,0.3); color: var(--rose); } | |
| .rebal-btn { | |
| width: 100%; | |
| padding: 10px; | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| border-radius: var(--r-sm); | |
| color: var(--text2); | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| font-family: var(--font-body); | |
| margin-bottom: 8px; | |
| } | |
| .rebal-btn:hover { border-color: var(--cyan); color: var(--cyan); background: rgba(34,211,238,0.05); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-shell"> | |
| <!-- Sidebar --> | |
| <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">Portfolio <span>Builder</span></div> | |
| <div class="page-subtitle">Build a smart, diversified portfolio in 3 easy steps</div> | |
| </div> | |
| <!-- Step Nav --> | |
| <div class="builder-steps fade-in"> | |
| <button class="step-btn active" data-step="1"><span class="step-num">1</span>Your Goals</button> | |
| <button class="step-btn" data-step="2"><span class="step-num">2</span>Risk Profile</button> | |
| <button class="step-btn" data-step="3"><span class="step-num">3</span>Build & Tune</button> | |
| </div> | |
| <div class="grid-60-40"> | |
| <!-- Left: Steps --> | |
| <div> | |
| <!-- Step 1: Goals --> | |
| <div class="step-panel active" id="step-1"> | |
| <div class="card fade-in"> | |
| <div class="section-title">🎯 What are you investing for?</div> | |
| <div class="text-muted text-sm" style="margin-bottom:20px">Select one or more goals (you can always change later)</div> | |
| <div class="goal-cards" id="goal-cards"> | |
| <div class="goal-card" data-goal="Retirement"><div class="goal-icon">🏖️</div><div class="goal-title">Retirement</div><div class="goal-desc">Long-term wealth for a comfortable retirement</div></div> | |
| <div class="goal-card" data-goal="Wealth Building"><div class="goal-icon">💰</div><div class="goal-title">Wealth Building</div><div class="goal-desc">Grow your money over time</div></div> | |
| <div class="goal-card" data-goal="Down Payment"><div class="goal-icon">🏠</div><div class="goal-title">Home Purchase</div><div class="goal-desc">Save for a down payment</div></div> | |
| <div class="goal-card" data-goal="Education"><div class="goal-icon">📚</div><div class="goal-title">Education</div><div class="goal-desc">Fund for college or courses</div></div> | |
| <div class="goal-card" data-goal="Emergency Fund"><div class="goal-icon">🛡️</div><div class="goal-title">Emergency Fund</div><div class="goal-desc">Safety net for unexpected expenses</div></div> | |
| <div class="goal-card" data-goal="Passive Income"><div class="goal-icon">💸</div><div class="goal-title">Passive Income</div><div class="goal-desc">Earn dividends & interest</div></div> | |
| </div> | |
| <div class="slider-wrap"> | |
| <div class="slider-header"> | |
| <span class="slider-label">💵 How much to invest?</span> | |
| <span class="slider-val" id="invest-display">$5,000</span> | |
| </div> | |
| <input type="range" id="invest-slider" min="500" max="100000" value="5000" step="500"> | |
| <div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text3);margin-top:4px"> | |
| <span>$500</span><span>$100K</span> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary" onclick="goStep(2)">Next: Set Risk Profile →</button> | |
| </div> | |
| </div> | |
| <!-- Step 2: Risk --> | |
| <div class="step-panel" id="step-2"> | |
| <div class="card fade-in"> | |
| <div class="section-title">🎯 Your Risk Tolerance</div> | |
| <div class="text-muted text-sm" style="margin-bottom:16px">Drag the slider to find your comfort level</div> | |
| <div class="risk-slider-wrap"> | |
| <div class="slider-header"> | |
| <span class="slider-label">Risk Level</span> | |
| <span class="slider-val" id="risk-val-display">Moderate</span> | |
| </div> | |
| <input type="range" id="risk-slider" min="1" max="5" value="3" step="1"> | |
| <div class="risk-labels"> | |
| <span>🐢 Very Conservative</span> | |
| <span>🦁 Very Aggressive</span> | |
| </div> | |
| </div> | |
| <div class="risk-profile-display" id="risk-profile-box"> | |
| <div class="rp-icon">⚖️</div> | |
| <div class="rp-label">Moderate Investor</div> | |
| <div class="rp-desc">Balanced mix of growth and stability. You're OK with some market swings for better long-term returns.</div> | |
| </div> | |
| <div class="flex gap-12" style="margin-top:20px"> | |
| <button class="btn btn-ghost" onclick="goStep(1)">← Back</button> | |
| <button class="btn btn-primary" onclick="goStep(3);buildPortfolio()">Generate Portfolio →</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 3: Build --> | |
| <div class="step-panel" id="step-3"> | |
| <div class="card fade-in"> | |
| <div class="flex justify-between items-center" style="margin-bottom:16px"> | |
| <div class="section-title" style="margin-bottom:0">🏗️ Your Portfolio</div> | |
| <div class="flex gap-8"> | |
| <button class="rebal-btn" style="width:auto;padding:8px 14px" onclick="autoRebalance()">⚖️ Auto-rebalance</button> | |
| </div> | |
| </div> | |
| <div id="asset-balance-warning" class="balance-warning ok" style="margin-bottom:14px"> | |
| ✅ Portfolio is balanced at 100% | |
| </div> | |
| <div id="asset-list"></div> | |
| <div class="divider"></div> | |
| <div class="section-title" style="font-size:14px">➕ Add Assets</div> | |
| <div class="add-asset-grid" id="add-asset-grid"></div> | |
| <div class="flex gap-12" style="margin-top:20px"> | |
| <button class="btn btn-ghost" onclick="goStep(2)">← Back</button> | |
| <button class="btn btn-emerald btn-full" onclick="savePortfolio_()">💾 Save Portfolio</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right: Live Preview --> | |
| <div class="portfolio-summary-side"> | |
| <div class="card fade-in-1"> | |
| <div class="card-title">🔴 Live Preview</div> | |
| <div class="pie-wrap"> | |
| <canvas id="previewPieChart"></canvas> | |
| </div> | |
| <div class="portfolio-total"> | |
| <div class="pt-label">Total Value</div> | |
| <div class="pt-value" id="preview-total">$5,000</div> | |
| </div> | |
| <div id="preview-legend" style="margin-top:8px"></div> | |
| <div class="divider"></div> | |
| <div class="card-title">📊 Portfolio Metrics</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px"> | |
| <div style="background:var(--bg3);border-radius:var(--r-sm);padding:12px;text-align:center"> | |
| <div style="font-size:11px;color:var(--text2);margin-bottom:4px">Risk Score</div> | |
| <div style="font-family:var(--font-head);font-size:20px;font-weight:800;color:var(--amber)" id="preview-risk">—</div> | |
| </div> | |
| <div style="background:var(--bg3);border-radius:var(--r-sm);padding:12px;text-align:center"> | |
| <div style="font-size:11px;color:var(--text2);margin-bottom:4px">Diversification</div> | |
| <div style="font-family:var(--font-head);font-size:20px;font-weight:800;color:var(--emerald)" id="preview-div">—</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Mobile Bottom Nav --> | |
| <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> | |
| // ── State ───────────────────────────────────────────────────────── | |
| let portfolio = getPortfolio(); | |
| let investAmount = portfolio.totalInvested || 5000; | |
| let riskLevel = 3; // 1-5 | |
| let selectedGoals = portfolio.goals || ['Wealth Building']; | |
| let previewChart = null; | |
| const RISK_PROFILES = { | |
| 1: { icon:'🐢', label:'Very Conservative', desc:'Capital preservation is your #1 priority. Heavy bonds & cash equivalents.', template:[30,0,5,0,50,10,5] }, | |
| 2: { icon:'🛡️', label:'Conservative', desc:'Modest growth with low risk. Bonds-heavy with some equity exposure.', template:[20,10,10,5,40,10,5] }, | |
| 3: { icon:'⚖️', label:'Moderate', desc:'Balanced growth & stability. OK with some swings for better returns.', template:[30,20,15,12,13,7,3] }, | |
| 4: { icon:'🚀', label:'Aggressive', desc:'Growth-focused. Higher potential returns, higher volatility accepted.', template:[35,30,20,12,0,3,0] }, | |
| 5: { icon:'🦁', label:'Very Aggressive', desc:'Maximum growth. Concentrated tech & equities. High-risk, high-reward.', template:[20,35,30,15,0,0,0] }, | |
| }; | |
| const ALL_ASSETS = [ | |
| { ticker:'VOO', name:'Vanguard S&P 500 ETF', price:478.22, color:'#22d3ee', type:'ETF' }, | |
| { ticker:'VTI', name:'Vanguard Total Market', price:240.30, color:'#0ea5e9', type:'ETF' }, | |
| { ticker:'QQQ', name:'Invesco Nasdaq 100', price:456.80, color:'#8b5cf6', type:'ETF' }, | |
| { ticker:'NVDA', name:'NVIDIA Corp', price:875.40, color:'#10b981', type:'Stock' }, | |
| { ticker:'AAPL', name:'Apple Inc.', price:188.60, color:'#f59e0b', type:'Stock' }, | |
| { ticker:'AMZN', name:'Amazon.com Inc.', price:188.90, color:'#0891b2', type:'Stock' }, | |
| { ticker:'TSLA', name:'Tesla Inc.', price:182.30, color:'#f43f5e', type:'Stock' }, | |
| { ticker:'WMT', name:'Walmart Inc.', price: 67.80, color:'#34d399', type:'Stock' }, | |
| { ticker:'MCD', name:"McDonald's Corp", price:281.50, color:'#fb923c', type:'Stock' }, | |
| { ticker:'BND', name:'Vanguard Bond ETF', price: 73.40, color:'#6366f1', type:'Bond' }, | |
| { ticker:'GLD', name:'SPDR Gold Trust', price:218.10, color:'#fbbf24', type:'Commodity' }, | |
| { ticker:'SLV', name:'iShares Silver Trust', price: 28.60, color:'#94a3b8', type:'Commodity' }, | |
| ]; | |
| // ── Step Navigation ─────────────────────────────────────────────── | |
| function goStep(n) { | |
| document.querySelectorAll('.step-panel').forEach(p => p.classList.remove('active')); | |
| document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById('step-' + n).classList.add('active'); | |
| document.querySelector(`.step-btn[data-step="${n}"]`).classList.add('active'); | |
| } | |
| document.querySelectorAll('.step-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const s = parseInt(btn.dataset.step); | |
| if (s === 3) buildPortfolio(); | |
| goStep(s); | |
| }); | |
| }); | |
| // ── Goal Cards ──────────────────────────────────────────────────── | |
| document.querySelectorAll('.goal-card').forEach(card => { | |
| card.addEventListener('click', () => { | |
| const goal = card.dataset.goal; | |
| const idx = selectedGoals.indexOf(goal); | |
| if (idx >= 0) selectedGoals.splice(idx, 1); | |
| else selectedGoals.push(goal); | |
| card.classList.toggle('selected'); | |
| }); | |
| }); | |
| // Pre-select saved goals | |
| selectedGoals.forEach(g => { | |
| const c = document.querySelector(`.goal-card[data-goal="${g}"]`); | |
| if (c) c.classList.add('selected'); | |
| }); | |
| // ── Investment Slider ───────────────────────────────────────────── | |
| const investSlider = document.getElementById('invest-slider'); | |
| investSlider.value = investAmount; | |
| document.getElementById('invest-display').textContent = '$' + investAmount.toLocaleString(); | |
| investSlider.addEventListener('input', () => { | |
| investAmount = parseInt(investSlider.value); | |
| document.getElementById('invest-display').textContent = '$' + investAmount.toLocaleString(); | |
| document.getElementById('preview-total').textContent = '$' + investAmount.toLocaleString(); | |
| updatePortfolioAmounts(); | |
| }); | |
| // ── Risk Slider ─────────────────────────────────────────────────── | |
| const riskSlider = document.getElementById('risk-slider'); | |
| riskSlider.addEventListener('input', () => { | |
| riskLevel = parseInt(riskSlider.value); | |
| updateRiskDisplay(); | |
| }); | |
| function updateRiskDisplay() { | |
| const profile = RISK_PROFILES[riskLevel]; | |
| const box = document.getElementById('risk-profile-box'); | |
| box.querySelector('.rp-icon').textContent = profile.icon; | |
| box.querySelector('.rp-label').textContent = profile.label; | |
| box.querySelector('.rp-desc').textContent = profile.desc; | |
| document.getElementById('risk-val-display').textContent = profile.label; | |
| } | |
| // ── Build Portfolio from template ───────────────────────────────── | |
| function buildPortfolio() { | |
| const template = RISK_PROFILES[riskLevel].template; | |
| const baseAssets = [ | |
| ALL_ASSETS.find(a=>a.ticker==='VOO'), | |
| ALL_ASSETS.find(a=>a.ticker==='QQQ'), | |
| ALL_ASSETS.find(a=>a.ticker==='NVDA'), | |
| ALL_ASSETS.find(a=>a.ticker==='AAPL'), | |
| ALL_ASSETS.find(a=>a.ticker==='BND'), | |
| ALL_ASSETS.find(a=>a.ticker==='GLD'), | |
| ALL_ASSETS.find(a=>a.ticker==='AMZN'), | |
| ]; | |
| portfolio.assets = baseAssets.map((a, i) => ({ | |
| ...a, | |
| pct: template[i], | |
| shares: parseFloat(((investAmount * template[i] / 100) / a.price).toFixed(3)) | |
| })).filter(a => a.pct > 0); | |
| portfolio.totalInvested = investAmount; | |
| portfolio.goals = selectedGoals; | |
| portfolio.riskProfile = RISK_PROFILES[riskLevel].label; | |
| renderAssetList(); | |
| renderAddGrid(); | |
| renderPreviewChart(); | |
| } | |
| // ── Render Asset List ───────────────────────────────────────────── | |
| function renderAssetList() { | |
| const container = document.getElementById('asset-list'); | |
| container.innerHTML = ''; | |
| portfolio.assets.forEach((asset, idx) => { | |
| const dollarVal = (investAmount * asset.pct / 100).toFixed(0); | |
| const div = document.createElement('div'); | |
| div.className = 'asset-card'; | |
| div.innerHTML = ` | |
| <div class="asset-logo" style="background:${asset.color}">${asset.ticker}</div> | |
| <div class="asset-info"> | |
| <div class="asset-ticker">${asset.ticker}</div> | |
| <div class="asset-name">${asset.name}</div> | |
| <span class="badge badge-${asset.type==='ETF'?'cyan':asset.type==='Bond'?'violet':asset.type==='Commodity'?'amber':'emerald'}">${asset.type}</span> | |
| </div> | |
| <div class="asset-sliders"> | |
| <input type="range" min="0" max="80" value="${asset.pct}" step="1" | |
| oninput="updateAssetPct(${idx}, this.value)" | |
| style="width:100%;margin-bottom:4px"> | |
| </div> | |
| <div> | |
| <div class="asset-pct-val" id="pct-${idx}">${asset.pct}%</div> | |
| <div class="asset-dollar" id="dollar-${idx}">$${parseInt(dollarVal).toLocaleString()}</div> | |
| </div> | |
| <button class="asset-remove" onclick="removeAsset(${idx})">✕</button> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| updateBalanceWarning(); | |
| updateMetrics(); | |
| } | |
| function updateAssetPct(idx, val) { | |
| portfolio.assets[idx].pct = parseInt(val); | |
| const dollarVal = (investAmount * parseInt(val) / 100).toFixed(0); | |
| document.getElementById('pct-' + idx).textContent = val + '%'; | |
| document.getElementById('dollar-' + idx).textContent = '$' + parseInt(dollarVal).toLocaleString(); | |
| updateBalanceWarning(); | |
| renderPreviewChart(); | |
| updateMetrics(); | |
| } | |
| function updatePortfolioAmounts() { | |
| portfolio.assets.forEach((a, i) => { | |
| const dEl = document.getElementById('dollar-' + i); | |
| if (dEl) dEl.textContent = '$' + parseInt(investAmount * a.pct / 100).toLocaleString(); | |
| }); | |
| renderPreviewChart(); | |
| } | |
| function updateBalanceWarning() { | |
| const total = portfolio.assets.reduce((s, a) => s + a.pct, 0); | |
| const warn = document.getElementById('asset-balance-warning'); | |
| if (total === 100) { | |
| warn.className = 'balance-warning ok'; | |
| warn.innerHTML = '✅ Portfolio is balanced at 100%'; | |
| } else if (total < 100) { | |
| warn.className = 'balance-warning warn'; | |
| warn.innerHTML = `⚠️ Under-allocated: ${total}% used, ${100-total}% remaining`; | |
| } else { | |
| warn.className = 'balance-warning err'; | |
| warn.innerHTML = `❌ Over-allocated: ${total}% total (exceeds 100% by ${total-100}%)`; | |
| } | |
| } | |
| function updateMetrics() { | |
| document.getElementById('preview-risk').textContent = calcRiskScore(portfolio) + '/100'; | |
| document.getElementById('preview-div').textContent = calcDiversification(portfolio) + '/100'; | |
| } | |
| function removeAsset(idx) { | |
| portfolio.assets.splice(idx, 1); | |
| renderAssetList(); | |
| renderAddGrid(); | |
| renderPreviewChart(); | |
| } | |
| function autoRebalance() { | |
| const total = portfolio.assets.reduce((s, a) => s + a.pct, 0); | |
| const perAsset = Math.floor(100 / portfolio.assets.length); | |
| let rem = 100 - perAsset * portfolio.assets.length; | |
| portfolio.assets.forEach((a, i) => { a.pct = perAsset + (i === 0 ? rem : 0); }); | |
| renderAssetList(); | |
| renderPreviewChart(); | |
| showToast('Portfolio auto-rebalanced to equal weights!'); | |
| } | |
| // ── Add Asset Grid ──────────────────────────────────────────────── | |
| function renderAddGrid() { | |
| const grid = document.getElementById('add-asset-grid'); | |
| grid.innerHTML = ALL_ASSETS.map(a => { | |
| const inPortfolio = portfolio.assets.some(pa => pa.ticker === a.ticker); | |
| return `<button class="add-asset-btn ${inPortfolio?'in-portfolio':''}" | |
| onclick="toggleAsset('${a.ticker}')" title="${a.name}"> | |
| ${inPortfolio ? '✓ ' : '+'} ${a.ticker} | |
| </button>`; | |
| }).join(''); | |
| } | |
| function toggleAsset(ticker) { | |
| const exists = portfolio.assets.findIndex(a => a.ticker === ticker); | |
| if (exists >= 0) { | |
| removeAsset(exists); | |
| } else { | |
| if (portfolio.assets.length >= 10) { showToast('Max 10 assets per portfolio', 'error'); return; } | |
| const asset = ALL_ASSETS.find(a => a.ticker === ticker); | |
| portfolio.assets.push({ ...asset, pct: 5, shares: 0 }); | |
| renderAssetList(); | |
| renderPreviewChart(); | |
| renderAddGrid(); | |
| } | |
| } | |
| // ── Preview Pie Chart ───────────────────────────────────────────── | |
| function renderPreviewChart() { | |
| const ctx = document.getElementById('previewPieChart').getContext('2d'); | |
| if (previewChart) previewChart.destroy(); | |
| const validAssets = portfolio.assets.filter(a => a.pct > 0); | |
| previewChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: validAssets.map(a => a.ticker), | |
| datasets: [{ | |
| data: validAssets.map(a => a.pct), | |
| backgroundColor: validAssets.map(a => a.color), | |
| borderColor: 'rgba(5,13,26,0.8)', | |
| borderWidth: 3, | |
| hoverOffset: 8 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| cutout: '68%', | |
| plugins: { legend: { display: false } } | |
| } | |
| }); | |
| const leg = document.getElementById('preview-legend'); | |
| leg.innerHTML = validAssets.map(a => ` | |
| <div class="legend-item"> | |
| <div class="legend-dot" style="background:${a.color}"></div> | |
| <span class="legend-name">${a.ticker}</span> | |
| <span class="legend-pct">${a.pct}%</span> | |
| </div> | |
| `).join(''); | |
| document.getElementById('preview-total').textContent = '$' + investAmount.toLocaleString(); | |
| } | |
| // ── Save Portfolio ──────────────────────────────────────────────── | |
| function savePortfolio_() { | |
| const total = portfolio.assets.reduce((s,a) => s + a.pct, 0); | |
| if (total !== 100) { showToast(`Total must be 100% (currently ${total}%)`, 'error'); return; } | |
| portfolio.assets.forEach(a => { | |
| a.shares = parseFloat(((investAmount * a.pct / 100) / a.price).toFixed(3)); | |
| }); | |
| savePortfolio(portfolio); | |
| showToast('✅ Portfolio saved successfully!'); | |
| setTimeout(() => window.location.href = 'index.html', 1500); | |
| } | |
| // ── Init ────────────────────────────────────────────────────────── | |
| document.addEventListener('DOMContentLoaded', () => { | |
| applyChartDefaults(); | |
| renderAddGrid(); | |
| renderAssetList(); | |
| renderPreviewChart(); | |
| updateMetrics(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |