diff --git a/index.html b/index.html index bb50d88..97c4819 100644 --- a/index.html +++ b/index.html @@ -326,6 +326,44 @@ + +

Individual Performance

diff --git a/script.js b/script.js index f20bfff..970f104 100644 --- a/script.js +++ b/script.js @@ -1098,6 +1098,9 @@ class ETFTradeTracker { // Calculate and display CGT information this.calculateAndDisplayCGT(etfMap, hasCurrentPrices); + // Calculate and display 8+ year CGT information + this.calculateAndDisplayLongTermCGT(etfMap, hasCurrentPrices); + this.renderGainsBreakdown(etfMap); } @@ -1239,6 +1242,162 @@ class ETFTradeTracker { document.getElementById('cgt-summary').style.display = 'block'; } + calculateAndDisplayLongTermCGT(etfMap, hasCurrentPrices) { + const longTermSection = document.getElementById('long-term-cgt-section'); + + if (!hasCurrentPrices) { + longTermSection.style.display = 'none'; + return; + } + + const now = new Date(); + const eightYearsInDays = 8 * 365.25; + const cgtRate = 0.33; // 33% rate for 8+ years + + let eligiblePositions = []; + let totalEligibleValue = 0; + let totalEligibleGains = 0; + let totalCost = 0; + + etfMap.forEach((etf, symbol) => { + const currentPrice = this.currentPrices.get(symbol); + if (!currentPrice) return; + + // Get all buy trades for this ETF that are 8+ years old + const longTermTrades = this.trades + .filter(t => t.etfSymbol === symbol && t.tradeType === 'buy') + .filter(t => { + const tradeDate = new Date(t.dateTime); + const holdingDays = Math.floor((now - tradeDate) / (1000 * 60 * 60 * 24)); + return holdingDays >= eightYearsInDays; + }) + .sort((a, b) => new Date(a.dateTime) - new Date(b.dateTime)); + + if (longTermTrades.length === 0) return; + + let positionShares = 0; + let positionCost = 0; + let oldestTradeDate = null; + + longTermTrades.forEach(trade => { + positionShares += trade.shares; + positionCost += trade.totalValue; + if (!oldestTradeDate || new Date(trade.dateTime) < oldestTradeDate) { + oldestTradeDate = new Date(trade.dateTime); + } + }); + + const currentValue = positionShares * currentPrice; + const gains = currentValue - positionCost; + const holdingDays = Math.floor((now - oldestTradeDate) / (1000 * 60 * 60 * 24)); + const holdingYears = holdingDays / 365.25; + + if (positionShares > 0) { + eligiblePositions.push({ + symbol, + shares: positionShares, + cost: positionCost, + currentValue, + gains, + currentPrice, + avgCost: positionCost / positionShares, + holdingYears, + currency: etf.currency + }); + + totalEligibleValue += currentValue; + totalEligibleGains += gains; + totalCost += positionCost; + } + }); + + if (eligiblePositions.length === 0) { + longTermSection.style.display = 'none'; + return; + } + + // Calculate CGT liability (only on gains) + const taxableGains = Math.max(0, totalEligibleGains); + const cgtLiability = taxableGains * cgtRate; + const afterTaxGains = totalEligibleGains - cgtLiability; + const gainsPercentage = totalCost > 0 ? ((totalEligibleGains / totalCost) * 100) : 0; + + // Update display elements + document.getElementById('long-term-eligible-value').textContent = this.formatCurrency(totalEligibleValue); + document.getElementById('long-term-eligible-count').textContent = `${eligiblePositions.length} position${eligiblePositions.length === 1 ? '' : 's'}`; + + document.getElementById('long-term-total-gains').textContent = this.formatCurrency(totalEligibleGains); + document.getElementById('long-term-gains-percentage').textContent = `${gainsPercentage >= 0 ? '+' : ''}${gainsPercentage.toFixed(1)}% gain`; + + document.getElementById('long-term-cgt-liability').textContent = this.formatCurrency(cgtLiability); + + document.getElementById('long-term-after-tax').textContent = this.formatCurrency(afterTaxGains); + document.getElementById('long-term-effective-rate').textContent = `Effective rate: ${cgtRate * 100}%`; + + // Render breakdown + this.renderLongTermBreakdown(eligiblePositions); + + longTermSection.style.display = 'block'; + } + + renderLongTermBreakdown(eligiblePositions) { + const breakdownList = document.getElementById('long-term-breakdown-list'); + + if (eligiblePositions.length === 0) { + breakdownList.innerHTML = '

No holdings over 8 years

'; + return; + } + + const breakdownHTML = eligiblePositions.map(position => { + const currencySymbol = position.currency === 'EUR' ? '€' : '$'; + const gainsClass = position.gains >= 0 ? 'positive' : 'negative'; + const performanceClass = position.gains >= 0 ? 'positive' : 'negative'; + const gainsPercentage = position.cost > 0 ? ((position.gains / position.cost) * 100) : 0; + const cgtLiability = Math.max(0, position.gains) * 0.33; + const afterTaxGains = position.gains - cgtLiability; + + return ` +
+
+ ${position.symbol} + + ${position.gains >= 0 ? '+' : ''}${currencySymbol}${position.gains.toFixed(2)} + (${position.gains >= 0 ? '+' : ''}${gainsPercentage.toFixed(1)}%) + +
+
+
+ Held + ${position.holdingYears.toFixed(1)} years +
+
+ Shares + ${position.shares.toFixed(3)} +
+
+ Avg Cost + ${currencySymbol}${position.avgCost.toFixed(2)} +
+
+ Current Price + ${currencySymbol}${position.currentPrice.toFixed(2)} +
+
+ CGT (33%) + -${currencySymbol}${cgtLiability.toFixed(2)} +
+
+ After-Tax Gain + ${afterTaxGains >= 0 ? '+' : ''}${currencySymbol}${afterTaxGains.toFixed(2)} +
+
+
+ `; + }).join(''); + + breakdownList.innerHTML = breakdownHTML; + } + // CGT Settings and Calculation Methods async loadCGTSettings() { try { diff --git a/styles.css b/styles.css index 73d23c1..7a87f72 100644 --- a/styles.css +++ b/styles.css @@ -1644,4 +1644,158 @@ body { font-size: 14px; font-weight: 600; color: #333; +} + +/* Long-Term CGT Section (8+ Years) */ +.long-term-cgt-section { + margin-top: 30px; + background: white; + padding: 25px; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + border-left: 4px solid #17a2b8; +} + +.long-term-cgt-section h3 { + margin-bottom: 20px; + color: #333; + display: flex; + align-items: center; + gap: 8px; +} + +.long-term-cgt-section h3::before { + content: '📅'; + font-size: 18px; +} + +.long-term-cgt-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 25px; +} + +.long-term-cgt-card { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + border-left: 4px solid #17a2b8; + transition: all 0.2s; +} + +.long-term-cgt-card.total-eligible { + border-left-color: #6c757d; +} + +.long-term-cgt-card.total-gains-8yr { + border-left-color: #28a745; + background: #f8fff9; +} + +.long-term-cgt-card.cgt-liability-8yr { + border-left-color: #dc3545; + background: #fffafa; +} + +.long-term-cgt-card.after-tax-8yr { + border-left-color: #007bff; + background: #f8f9ff; +} + +.long-term-cgt-card h4 { + margin-bottom: 12px; + color: #666; + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.long-term-value { + font-size: 24px; + font-weight: bold; + margin-bottom: 8px; + color: #28a745; +} + +.long-term-value.negative { + color: #dc3545; +} + +.long-term-detail { + font-size: 12px; + color: #666; +} + +.long-term-breakdown { + border-top: 1px solid #e9ecef; + padding-top: 20px; +} + +.long-term-breakdown h4 { + margin-bottom: 15px; + color: #333; + font-size: 16px; +} + +.long-term-breakdown-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.long-term-breakdown-item { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #17a2b8; +} + +.long-term-breakdown-item.positive { + border-left-color: #28a745; + background: #f8fff9; +} + +.long-term-breakdown-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.long-term-symbol { + font-weight: bold; + font-size: 16px; + color: #333; +} + +.long-term-performance { + font-weight: bold; + color: #28a745; +} + +.long-term-performance.negative { + color: #dc3545; +} + +.long-term-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 10px; +} + +.long-term-stat { + display: flex; + justify-content: space-between; + font-size: 12px; +} + +.long-term-stat span:first-child { + color: #666; +} + +.long-term-stat span:last-child { + font-weight: 600; + color: #333; } \ No newline at end of file