From 82cc6cc237e4440e822b110274fbe68658393f3a Mon Sep 17 00:00:00 2001 From: Prashant Date: Mon, 8 Dec 2025 19:17:05 +0000 Subject: [PATCH 1/2] feat: Add investment performance chart with benchmark comparison --- public/static/app.js | 479 ++++++++++++++++++++++++++++++-------- scripts/seed-final.sql | 106 +++++++++ src/index.tsx | 1 + src/routes/investments.ts | 186 ++++++++++++++- 4 files changed, 679 insertions(+), 93 deletions(-) create mode 100644 scripts/seed-final.sql diff --git a/public/static/app.js b/public/static/app.js index 9034840..e6f77bd 100644 --- a/public/static/app.js +++ b/public/static/app.js @@ -33,6 +33,9 @@ const state = { investBasketMinAmount: 0 // Minimum investment for the basket being invested in }; +// Global chart instance +let performanceChartInstance = null; + // API Helper const api = { async request(endpoint, options = {}) { @@ -1948,7 +1951,7 @@ async function editBasket(basketId) { } } -async function viewInvestment(investmentId) { +{/*async function viewInvestment(investmentId) { const res = await api.get(`/investments/${investmentId}`); if (res?.success) { state.selectedInvestment = res.data; @@ -1960,8 +1963,36 @@ async function viewInvestment(investmentId) { await loadInvestmentTransactions(investmentId); renderApp(); } +}*/} + +async function viewInvestment(investmentId) { + console.log('[DEBUG] Viewing investment:', investmentId); + + // Find investment in state + const investment = state.investments.find(inv => inv.id === investmentId); + if (!investment) { + showNotification('Investment not found', 'error'); + return; + } + + // Load full investment details including holdings + const response = await api.get(`/investments/${investmentId}`); + if (response?.success) { + state.selectedInvestment = response.data; + } else { + state.selectedInvestment = investment; + } + + state.currentView = 'investment-detail'; + renderApp(); + + // Auto-load performance chart (1Y by default) + setTimeout(() => { + loadPerformanceChart(investmentId, '1Y'); + }, 100); } + async function investInBasket(basketId) { // Fetch basket details to get minimum investment const res = await api.get(`/baskets/${basketId}`); @@ -2787,122 +2818,207 @@ async function refreshBasketPrices(basketId) { } } +/** + * Render investment holdings table + */ +function renderInvestmentHoldings(investment) { + if (!investment.holdings || investment.holdings.length === 0) { + return ` +
+ +

No holdings data available

+
+ `; + } + + return ` + + + + + + + + + + + + + ${investment.holdings.map(holding => { + const currentValue = holding.quantity * (holding.current_price || holding.average_price); + const investedValue = holding.quantity * holding.average_price; + const pnl = currentValue - investedValue; + const pnlPct = investedValue > 0 ? ((pnl / investedValue) * 100).toFixed(2) : 0; + + return ` + + + + + + + + + `; + }).join('')} + +
SymbolQuantityAvg PriceLTPCurrent ValueP&L
+
+
+
${holding.trading_symbol}
+
${holding.exchange}
+
+
+
+ ${holding.quantity} + + ${formatCurrency(holding.average_price)} + + ${formatCurrency(holding.current_price || holding.average_price)} + + ${formatCurrency(currentValue)} + +
+ ${pnl >= 0 ? '+' : ''}${formatCurrency(pnl)} +
+ ${pnl >= 0 ? '+' : ''}${pnlPct}% +
+
+
+ `; +} + + function renderInvestmentDetail() { - const inv = state.selectedInvestment; - if (!inv) return '

Loading...

'; - - const pnl = (inv.current_value || inv.invested_amount) - inv.invested_amount; - const pnlPct = inv.invested_amount > 0 ? ((pnl / inv.invested_amount) * 100).toFixed(2) : 0; - + const investment = state.selectedInvestment; + if (!investment) { + setView('investments'); + return ''; + } + + const pnl = (investment.current_value || investment.invested_amount) - investment.invested_amount; + const pnlPct = investment.invested_amount > 0 + ? ((pnl / investment.invested_amount) * 100).toFixed(2) + : 0; + return `
-
-
- -
-

${inv.basket_name}

-

Invested on ${new Date(inv.invested_at).toLocaleDateString()}

-
-
-
- - -
+
+ +

${investment.basket_name}

+ + ${investment.status} +
-
-
-

Invested

-

${formatCurrency(inv.invested_amount)}

+ +
+
+

Invested Amount

+

${formatCurrency(investment.invested_amount)}

+

${new Date(investment.invested_at).toLocaleDateString('en-IN')}

-
+ +

Current Value

-

${formatCurrency(inv.current_value)}

+

${formatCurrency(investment.current_value || investment.invested_amount)}

-
-

P&L

-

+ +

+

Total P&L

+

${pnl >= 0 ? '+' : ''}${formatCurrency(pnl)}

-
-
-

Returns

-

+

${pnl >= 0 ? '+' : ''}${pnlPct}%

-
-
-

Holdings

- - - - - - - - - - - - - - ${(inv.holdings || []).map(h => { - const value = h.quantity * (h.current_price || h.average_price); - const hPnl = value - (h.quantity * h.average_price); - return ` - - - - - - - - - - `; - }).join('')} - -
SymbolQtyAvg PriceLTPValueP&LWeight
${h.trading_symbol}${h.quantity}${formatCurrency(h.average_price)}${formatCurrency(h.current_price)}${formatCurrency(value)} - - ${hPnl >= 0 ? '+' : ''}${formatCurrency(hPnl)} - - -
- ${h.target_weight?.toFixed(1)}% - - → ${h.actual_weight?.toFixed(1)}% - -
-
+
+

Holdings

+

${investment.holdings?.length || 0}

+

stocks

+
- +
-
-

Transaction History

+
+

Performance

+ +
- - - - + + + + +
-
- ${renderTransactionHistory()} + + +
+ +
+ + + + + +
+ + +
+
+

Holdings

+
+
+ ${renderInvestmentHoldings(investment)} +
+
+ + +
+ + +
`; } + function renderTransactionHistory() { const transactions = state.investmentTransactions || []; @@ -2993,5 +3109,186 @@ async function loadInvestmentTransactions(investmentId) { } } + +/** + * Load performance chart data and render + */ +async function loadPerformanceChart(investmentId, period = '1Y') { + const chartContainer = document.getElementById('performanceChart'); + const loadingEl = document.getElementById('chartLoading'); + const errorEl = document.getElementById('chartError'); + + if (!chartContainer) return; + + // Show loading + if (loadingEl) loadingEl.classList.remove('hidden'); + if (errorEl) errorEl.classList.add('hidden'); + + // Update period button active state + document.querySelectorAll('.period-btn').forEach(btn => { + btn.classList.remove('bg-indigo-600', 'text-white'); + btn.classList.add('border', 'border-gray-300', 'text-gray-600'); + }); + const activeBtn = document.getElementById(`period-${period}`); + if (activeBtn) { + activeBtn.classList.add('bg-indigo-600', 'text-white'); + activeBtn.classList.remove('border', 'border-gray-300', 'text-gray-600'); + } + + try { + const response = await api.get(`/investments/${investmentId}/performance?period=${period}`); + + if (loadingEl) loadingEl.classList.add('hidden'); + + if (!response?.success || !response.data) { + throw new Error(response?.error?.message || 'Failed to load performance data'); + } + + renderPerformanceChart(response.data); + + } catch (error) { + console.error('Error loading performance chart:', error); + if (loadingEl) loadingEl.classList.add('hidden'); + if (errorEl) { + errorEl.classList.remove('hidden'); + const errorMsg = document.getElementById('chartErrorMessage'); + if (errorMsg) { + errorMsg.textContent = error.message || 'Failed to load performance data'; + } + } + } +} + +/** + * Render Chart.js performance chart + */ +function renderPerformanceChart(data) { + const chartCanvas = document.getElementById('performanceChart'); + if (!chartCanvas) return; + + // Destroy existing chart instance + if (performanceChartInstance) { + performanceChartInstance.destroy(); + } + + const ctx = chartCanvas.getContext('2d'); + + performanceChartInstance = new Chart(ctx, { + type: 'line', + data: { + labels: data.dates.map(date => { + const d = new Date(date); + return d.toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }); + }), + datasets: [ + { + label: 'Portfolio', + data: data.values, + borderColor: '#4F46E5', // Indigo 600 + backgroundColor: 'rgba(79, 70, 229, 0.1)', + borderWidth: 2, + pointRadius: 0, + pointHoverRadius: 4, + tension: 0.3, + fill: true + }, + { + label: data.benchmark_name, + data: data.benchmark_values, + borderColor: '#6B7280', // Gray 500 + backgroundColor: 'transparent', + borderWidth: 2, + borderDash: [5, 5], + pointRadius: 0, + pointHoverRadius: 4, + tension: 0.3, + fill: false + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false + }, + plugins: { + legend: { + display: true, + position: 'top', + align: 'end', + labels: { + usePointStyle: true, + pointStyle: 'circle', + padding: 15, + font: { + size: 12, + family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' + } + } + }, + tooltip: { + backgroundColor: 'rgba(255, 255, 255, 0.95)', + titleColor: '#111827', + bodyColor: '#374151', + borderColor: '#E5E7EB', + borderWidth: 1, + padding: 12, + displayColors: true, + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + label += context.parsed.y.toFixed(2); + return label; + } + } + } + }, + scales: { + x: { + grid: { + display: false + }, + ticks: { + font: { + size: 11 + }, + maxRotation: 0, + autoSkipPadding: 20 + } + }, + y: { + beginAtZero: false, + grid: { + color: '#F3F4F6' + }, + ticks: { + font: { + size: 11 + }, + callback: function(value) { + return value.toFixed(0); + } + }, + title: { + display: true, + text: 'Normalized to 100', + font: { + size: 12, + weight: '500' + }, + color: '#6B7280' + } + } + } + } + }); +} + + // Initialize initApp(); diff --git a/scripts/seed-final.sql b/scripts/seed-final.sql new file mode 100644 index 0000000..6d5b55f --- /dev/null +++ b/scripts/seed-final.sql @@ -0,0 +1,106 @@ +-- ============================================ +-- CORRECT SEED SCRIPT FOR YOUR SCHEMA +-- ============================================ + +-- Step 1: Create account (using zerodha_user_id) +INSERT OR IGNORE INTO accounts ( + id, zerodha_user_id, name, email, broker_type, + is_primary, is_active, created_at, updated_at +) VALUES ( + 1, 'AB1234', 'Prashant Trading', 'focusedprshnt@gmail.com', 'zerodha', + 1, 1, datetime('now'), datetime('now') +); + +-- Step 2: Create broker account +INSERT OR IGNORE INTO broker_accounts ( + user_id, broker_type, account_name, broker_user_id, + is_connected, is_active, connection_status, created_at, updated_at +) VALUES ( + 1, 'zerodha', 'My Trading Account', 'AB1234', + 1, 1, 'connected', datetime('now'), datetime('now') +); + +-- Step 3: Create baskets +INSERT OR IGNORE INTO baskets ( + user_id, account_id, name, description, theme, category, + is_active, is_public, is_template, min_investment, risk_level, + benchmark_symbol, created_at, updated_at +) VALUES +(1, 1, 'Tech Leaders', 'Top technology stocks from India', 'Technology', 'custom', 1, 0, 0, 25000, 'moderate', 'NIFTY IT', datetime('now', '-180 days'), datetime('now')), +(1, 1, 'Banking Giants', 'Leading banking and financial services', 'Banking', 'custom', 1, 0, 0, 30000, 'low', 'NIFTY BANK', datetime('now', '-240 days'), datetime('now')), +(1, 1, 'Pharma Power', 'Pharmaceutical and healthcare stocks', 'Healthcare', 'custom', 1, 0, 0, 20000, 'high', 'NIFTY PHARMA', datetime('now', '-120 days'), datetime('now')); + +-- Step 4: Create basket stocks +INSERT OR IGNORE INTO basket_stocks (basket_id, trading_symbol, exchange, weight_percentage, company_name, sector, created_at) VALUES +(1, 'TCS', 'NSE', 25.0, 'Tata Consultancy Services', 'IT', datetime('now')), +(1, 'INFY', 'NSE', 25.0, 'Infosys Limited', 'IT', datetime('now')), +(1, 'WIPRO', 'NSE', 25.0, 'Wipro Limited', 'IT', datetime('now')), +(1, 'HCLTECH', 'NSE', 25.0, 'HCL Technologies', 'IT', datetime('now')), +(2, 'HDFCBANK', 'NSE', 30.0, 'HDFC Bank', 'Banking', datetime('now')), +(2, 'ICICIBANK', 'NSE', 30.0, 'ICICI Bank', 'Banking', datetime('now')), +(2, 'AXISBANK', 'NSE', 20.0, 'Axis Bank', 'Banking', datetime('now')), +(2, 'KOTAKBANK', 'NSE', 20.0, 'Kotak Mahindra Bank', 'Banking', datetime('now')), +(3, 'SUNPHARMA', 'NSE', 30.0, 'Sun Pharma', 'Pharma', datetime('now')), +(3, 'DRREDDY', 'NSE', 30.0, 'Dr Reddys Labs', 'Pharma', datetime('now')), +(3, 'CIPLA', 'NSE', 20.0, 'Cipla', 'Pharma', datetime('now')), +(3, 'DIVISLAB', 'NSE', 20.0, 'Divis Laboratories', 'Pharma', datetime('now')); + +-- Step 5: Create investments +INSERT OR IGNORE INTO investments ( + user_id, account_id, basket_id, broker_account_id, + invested_amount, current_value, units, invested_at, + last_synced_at, status +) VALUES +(1, 1, 1, 1, 50000, 55000, 1, date('now', '-180 days'), datetime('now'), 'ACTIVE'), +(1, 1, 2, 1, 75000, 78500, 1, date('now', '-240 days'), datetime('now'), 'ACTIVE'), +(1, 1, 3, 1, 40000, 44000, 1, date('now', '-120 days'), datetime('now'), 'ACTIVE'); + +-- Step 6: Create investment holdings +INSERT OR IGNORE INTO investment_holdings (investment_id, trading_symbol, exchange, quantity, average_price, current_price, target_weight, actual_weight, pnl, pnl_percentage, last_updated) VALUES +(1, 'TCS', 'NSE', 3, 4166.67, 4350.00, 25.0, 25.0, 550, 4.4, datetime('now')), +(1, 'INFY', 'NSE', 8, 1562.50, 1625.00, 25.0, 25.0, 500, 4.0, datetime('now')), +(1, 'WIPRO', 'NSE', 25, 500.00, 530.00, 25.0, 25.0, 750, 6.0, datetime('now')), +(1, 'HCLTECH', 'NSE', 9, 1388.89, 1450.00, 25.0, 25.0, 550, 4.4, datetime('now')), +(2, 'HDFCBANK', 'NSE', 14, 1607.14, 1650.00, 30.0, 30.0, 600, 2.7, datetime('now')), +(2, 'ICICIBANK', 'NSE', 22, 1022.73, 1050.00, 30.0, 30.0, 600, 2.7, datetime('now')), +(2, 'AXISBANK', 'NSE', 15, 1000.00, 1020.00, 20.0, 20.0, 300, 2.0, datetime('now')), +(2, 'KOTAKBANK', 'NSE', 9, 1666.67, 1700.00, 20.0, 20.0, 300, 2.0, datetime('now')), +(3, 'SUNPHARMA', 'NSE', 8, 1500.00, 1575.00, 30.0, 30.0, 600, 5.0, datetime('now')), +(3, 'DRREDDY', 'NSE', 2, 6000.00, 6300.00, 30.0, 30.0, 600, 5.0, datetime('now')), +(3, 'CIPLA', 'NSE', 7, 1142.86, 1200.00, 20.0, 20.0, 400, 5.0, datetime('now')), +(3, 'DIVISLAB', 'NSE', 2, 4000.00, 4200.00, 20.0, 20.0, 400, 5.0, datetime('now')); + +-- Step 7: Populate investment_history +DELETE FROM investment_history; +INSERT INTO investment_history (investment_id, recorded_date, invested_amount, current_value, day_change, day_change_percentage, total_pnl, total_pnl_percentage, created_at) +SELECT i.id, date(i.invested_at, '+' || n.day || ' days'), i.invested_amount, + CAST(i.invested_amount * (1.0 + (n.day * 0.0003) + ((ABS(RANDOM()) % 200 - 100) / 10000.0)) AS REAL), + 0, 0, 0, 0, datetime('now') +FROM investments i +CROSS JOIN (WITH RECURSIVE days(day) AS (SELECT 0 UNION ALL SELECT day + 1 FROM days WHERE day < 365) SELECT day FROM days) n +WHERE date(i.invested_at, '+' || n.day || ' days') <= date('now'); + +UPDATE investment_history SET + total_pnl = current_value - invested_amount, + total_pnl_percentage = ((current_value - invested_amount) / invested_amount) * 100; + +-- Step 8: Add benchmark historical data +INSERT OR IGNORE INTO benchmark_data (symbol, recorded_date, close_price, created_at) +SELECT 'NIFTY 50', date('now', '-' || n.day || ' days'), + CAST(24500 - (n.day * 10) + ((ABS(RANDOM()) % 500 - 250)) AS REAL), datetime('now') +FROM (WITH RECURSIVE days(day) AS (SELECT 1 UNION ALL SELECT day + 1 FROM days WHERE day < 365) SELECT day FROM days) n; + +INSERT OR IGNORE INTO benchmark_data (symbol, recorded_date, close_price, created_at) +SELECT 'NIFTY IT', date('now', '-' || n.day || ' days'), + CAST(38000 - (n.day * 15) + ((ABS(RANDOM()) % 400 - 200)) AS REAL), datetime('now') +FROM (WITH RECURSIVE days(day) AS (SELECT 1 UNION ALL SELECT day + 1 FROM days WHERE day < 365) SELECT day FROM days) n; + +INSERT OR IGNORE INTO benchmark_data (symbol, recorded_date, close_price, created_at) +SELECT 'NIFTY BANK', date('now', '-' || n.day || ' days'), + CAST(52000 - (n.day * 20) + ((ABS(RANDOM()) % 600 - 300)) AS REAL), datetime('now') +FROM (WITH RECURSIVE days(day) AS (SELECT 1 UNION ALL SELECT day + 1 FROM days WHERE day < 365) SELECT day FROM days) n; + +INSERT OR IGNORE INTO benchmark_data (symbol, recorded_date, close_price, created_at) +SELECT 'NIFTY PHARMA', date('now', '-' || n.day || ' days'), + CAST(21000 - (n.day * 8) + ((ABS(RANDOM()) % 300 - 150)) AS REAL), datetime('now') +FROM (WITH RECURSIVE days(day) AS (SELECT 1 UNION ALL SELECT day + 1 FROM days WHERE day < 365) SELECT day FROM days) n; diff --git a/src/index.tsx b/src/index.tsx index ff43f0b..64b88c3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -139,6 +139,7 @@ app.get('/', async (c) => { .card-hover { transition: transform 0.2s, box-shadow 0.2s; } .card-hover:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.15); } + diff --git a/src/routes/investments.ts b/src/routes/investments.ts index be73ae7..321b70b 100644 --- a/src/routes/investments.ts +++ b/src/routes/investments.ts @@ -16,9 +16,10 @@ import type { BuyBasketRequest, SellBasketRequest, RebalancePreview, - KiteOrder + KiteOrder, + PerformanceData, InvestmentHistory, BenchmarkData } from '../types'; -import { successResponse, errorResponse, decrypt, calculatePercentageChange } from '../lib/utils'; +import { successResponse, errorResponse, decrypt, calculatePercentageChange, normalizeToBase100, getISTDateString } from '../lib/utils'; import { KiteClient } from '../lib/kite'; import { AngelOneClient, AngelOneOrder } from '../lib/angelone'; @@ -1635,4 +1636,185 @@ investments.get('/:id/rebalance-history', async (c) => { } }); +/** + * GET /api/investments/:id/performance + * Get historical performance data for investment with benchmark comparison + * + * Query Parameters: + * - period: '1M' | '3M' | '6M' | '1Y' | 'ALL' (default: '1Y') + * - benchmark: Benchmark symbol (default: from basket.benchmark_symbol) + */ +investments.get('/:id/performance', async (c) => { + try { + const session = c.get('session'); + if (!session) { + return c.json(errorResponse('UNAUTHORIZED', 'Authentication required'), 401); + } + + const investmentId = parseInt(c.req.param('id')); + const period = c.req.query('period') || '1Y'; + const customBenchmark = c.req.query('benchmark'); + + // Validate investment exists and belongs to user + const investment = await c.env.DB.prepare(` + SELECT + i.id, + i.user_id, + i.basket_id, + i.invested_at, + b.benchmark_symbol, + b.name as basket_name + FROM investments i + JOIN baskets b ON b.id = i.basket_id + WHERE i.id = ? AND i.user_id = ? + `).bind(investmentId, session.user_id).first<{ + id: number; + user_id: number; + basket_id: number; + invested_at: string; + benchmark_symbol: string; + basket_name: string; + }>(); + + if (!investment) { + return c.json(errorResponse('NOT_FOUND', 'Investment not found'), 404); + } + + // Calculate date range based on period + const endDate = new Date(); + const startDate = new Date(investment.invested_at); + + switch (period) { + case '1M': + startDate.setMonth(endDate.getMonth() - 1); + break; + case '3M': + startDate.setMonth(endDate.getMonth() - 3); + break; + case '6M': + startDate.setMonth(endDate.getMonth() - 6); + break; + case '1Y': + startDate.setFullYear(endDate.getFullYear() - 1); + break; + case 'ALL': + // Use original investment date + startDate.setTime(new Date(investment.invested_at).getTime()); + break; + default: + startDate.setFullYear(endDate.getFullYear() - 1); + } + + // Ensure startDate is not before investment date + const investmentStartDate = new Date(investment.invested_at); + if (startDate < investmentStartDate) { + startDate.setTime(investmentStartDate.getTime()); + } + + const startDateStr = getISTDateString(startDate); + const endDateStr = getISTDateString(endDate); + + // Fetch investment history + const investmentHistory = await c.env.DB.prepare(` + SELECT + recorded_date, + current_value, + total_pnl, + total_pnl_percentage + FROM investment_history + WHERE investment_id = ? + AND recorded_date >= ? + AND recorded_date <= ? + ORDER BY recorded_date ASC + `).bind(investmentId, startDateStr, endDateStr).all(); + + if (!investmentHistory.results || investmentHistory.results.length === 0) { + return c.json(errorResponse('NO_DATA', 'No historical data available for this investment. Data is recorded daily.'), 404); + } + + // Determine benchmark symbol + const benchmarkSymbol = customBenchmark || investment.benchmark_symbol || 'NIFTY 50'; + +// Fetch benchmark data +const benchmarkHistory = await c.env.DB.prepare(` + SELECT + recorded_date, + close_price + FROM benchmark_data + WHERE symbol = ? + AND recorded_date >= ? + AND recorded_date <= ? + ORDER BY recorded_date ASC +`).bind(benchmarkSymbol, startDateStr, endDateStr).all(); + +console.log('[DEBUG] Benchmark history count:', benchmarkHistory.results?.length); +console.log('[DEBUG] First benchmark record:', benchmarkHistory.results?.[0]); +console.log('[DEBUG] Last benchmark record:', benchmarkHistory.results?.[benchmarkHistory.results?.length - 1]); + +// Create maps for quick lookup +const investmentMap = new Map( + investmentHistory.results.map(row => [row.recorded_date, row.current_value]) +); +const benchmarkMap = new Map( + benchmarkHistory.results?.map(row => [row.recorded_date, row.close_price]) || [] +); + +console.log('[DEBUG] Investment map size:', investmentMap.size); +console.log('[DEBUG] Benchmark map size:', benchmarkMap.size); + +// Prepare dates array - use investment dates as primary +const dates = investmentHistory.results.map(row => row.recorded_date); + +// Fill data arrays (forward-fill missing dates) +const investmentValues: number[] = []; +const benchmarkValues: number[] = []; +let lastInvestmentValue = investmentHistory.results[0].current_value; +let lastBenchmarkValue = benchmarkHistory.results?.[0]?.close_price || 21000; // Use realistic default + +dates.forEach((date, index) => { + // Investment value + if (investmentMap.has(date)) { + lastInvestmentValue = investmentMap.get(date)!; + } + investmentValues.push(lastInvestmentValue); + + // Benchmark value + if (benchmarkMap.has(date)) { + lastBenchmarkValue = benchmarkMap.get(date)!; + } + benchmarkValues.push(lastBenchmarkValue); + + // Debug first few and last few + if (index < 3 || index > dates.length - 3) { + console.log(`[DEBUG] Date ${date}: investment=${lastInvestmentValue}, benchmark=${lastBenchmarkValue}`); + } +}); + +console.log('[DEBUG] Investment values sample:', investmentValues.slice(0, 5)); +console.log('[DEBUG] Benchmark values sample:', benchmarkValues.slice(0, 5)); + + + // Normalize both series to base 100 + const normalizedInvestment = normalizeToBase100(investmentValues); + const normalizedBenchmark = normalizeToBase100(benchmarkValues); + + // Prepare response + const performanceData: PerformanceData = { + dates, + values: normalizedInvestment, + benchmark_values: normalizedBenchmark, + benchmark_name: benchmarkSymbol + }; + + return c.json(successResponse(performanceData)); + + } catch (error) { + console.error('Error fetching performance data:', error); + return c.json( + errorResponse('INTERNAL_ERROR', 'Failed to fetch performance data'), + 500 + ); + } +}); + export default investments; From afd375470fff47293bac9277cee88e0e48b0a221 Mon Sep 17 00:00:00 2001 From: Prashant Date: Mon, 8 Dec 2025 20:44:48 +0000 Subject: [PATCH 2/2] fix: Remove debug console.log and update seed script safety --- public/static/app.js | 14 ++++---- scripts/seed-final.sql | 24 +++++++++---- src/routes/investments.ts | 71 +++++++++++++++++++++------------------ 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/public/static/app.js b/public/static/app.js index e6f77bd..e6264b0 100644 --- a/public/static/app.js +++ b/public/static/app.js @@ -2979,14 +2979,14 @@ function renderInvestmentDetail() {
-
- -
- - - +