Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Crypto Portfolio Tracker</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| .profit { | |
| background-color: rgba(74, 222, 128, 0.2); | |
| color: #16a34a; | |
| } | |
| .loss { | |
| background-color: rgba(248, 113, 113, 0.2); | |
| color: #dc2626; | |
| } | |
| .table-container { | |
| overflow-x: auto; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| input[type="number"]::-webkit-inner-spin-button, | |
| input[type="number"]::-webkit-outer-spin-button { | |
| -webkit-appearance: none; | |
| margin: 0; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-800">Crypto Portfolio Tracker</h1> | |
| <p class="text-gray-600">Track your cryptocurrency investments with real-time data</p> | |
| </header> | |
| <div class="bg-white rounded-lg shadow-md p-6 mb-8"> | |
| <div class="flex flex-col md:flex-row gap-4 mb-6"> | |
| <div class="flex-1"> | |
| <label for="coinSearch" class="block text-sm font-medium text-gray-700 mb-1">Search Cryptocurrency</label> | |
| <div class="relative"> | |
| <input type="text" id="coinSearch" placeholder="Search by name or symbol..." | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| <div id="coinDropdown" class="absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-md shadow-lg hidden max-h-60 overflow-y-auto"> | |
| <!-- Coin list will be populated here --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex-1"> | |
| <label for="purchaseDate" class="block text-sm font-medium text-gray-700 mb-1">Purchase Date</label> | |
| <input type="date" id="purchaseDate" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| </div> | |
| <div class="flex-1"> | |
| <label for="quantity" class="block text-sm font-medium text-gray-700 mb-1">Quantity</label> | |
| <input type="number" id="quantity" placeholder="0.00" step="0.00000001" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| </div> | |
| <div class="flex-1"> | |
| <label for="avgCost" class="block text-sm font-medium text-gray-700 mb-1">Avg. Cost (Optional)</label> | |
| <input type="number" id="avgCost" placeholder="0.00" step="0.01" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| </div> | |
| <div class="flex items-end"> | |
| <button id="addCoin" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2"> | |
| <i data-feather="plus"></i> Add Coin | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-800">Your Portfolio</h2> | |
| <div class="flex items-center gap-2"> | |
| <span id="apiCounter" class="text-sm text-gray-500">API calls today: 0</span> | |
| <button id="loadFromCache" class="px-3 py-1 text-sm bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors flex items-center gap-1"> | |
| <i data-feather="refresh-cw" class="w-4 h-4"></i> Load Saved | |
| </button> | |
| </div> | |
| </div> | |
| <div class="table-container"> | |
| <table id="portfolioTable" class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ticker</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Qty</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current Cost</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Cost</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Value</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Diff ($)</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Diff (%)</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Days Since</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th> | |
| </tr> | |
| </thead> | |
| <tbody id="portfolioBody" class="bg-white divide-y divide-gray-200"> | |
| <!-- Portfolio items will be populated here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Portfolio Summary</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="bg-blue-50 p-4 rounded-lg"> | |
| <h3 class="text-sm font-medium text-blue-800">Total Investment</h3> | |
| <p id="totalInvestment" class="text-2xl font-bold text-blue-600">$0.00</p> | |
| </div> | |
| <div class="bg-green-50 p-4 rounded-lg"> | |
| <h3 class="text-sm font-medium text-green-800">Current Value</h3> | |
| <p id="currentValue" class="text-2xl font-bold text-green-600">$0.00</p> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="text-sm font-medium text-gray-800">Profit/Loss</h3> | |
| <p id="profitLoss" class="text-2xl font-bold">$0.00</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize feather icons | |
| feather.replace(); | |
| AOS.init(); | |
| // API configuration | |
| const API_KEY = '48776ed0-aacd-4181-bd9f-e1003791b921'; | |
| const API_BASE_URL = 'https://api.livecoinwatch.com'; | |
| let apiCallCount = 0; | |
| let lastApiCallDate = new Date().toDateString(); | |
| // Sample cryptocurrency data (would be fetched from API in production) | |
| const sampleCoins = [ | |
| { code: 'BTC', name: 'Bitcoin' }, | |
| { code: 'ETH', name: 'Ethereum' }, | |
| { code: 'BNB', name: 'Binance Coin' }, | |
| { code: 'SOL', name: 'Solana' }, | |
| { code: 'XRP', name: 'XRP' }, | |
| { code: 'ADA', name: 'Cardano' }, | |
| { code: 'DOGE', name: 'Dogecoin' }, | |
| { code: 'DOT', name: 'Polkadot' }, | |
| { code: 'SHIB', name: 'Shiba Inu' }, | |
| { code: 'MATIC', name: 'Polygon' }, | |
| { code: 'AVAX', name: 'Avalanche' }, | |
| { code: 'LTC', name: 'Litecoin' }, | |
| { code: 'UNI', name: 'Uniswap' }, | |
| { code: 'LINK', name: 'Chainlink' }, | |
| { code: 'ATOM', name: 'Cosmos' } | |
| ]; | |
| // DOM elements | |
| const coinSearch = document.getElementById('coinSearch'); | |
| const coinDropdown = document.getElementById('coinDropdown'); | |
| const purchaseDate = document.getElementById('purchaseDate'); | |
| const quantity = document.getElementById('quantity'); | |
| const avgCost = document.getElementById('avgCost'); | |
| const addCoin = document.getElementById('addCoin'); | |
| const loadFromCache = document.getElementById('loadFromCache'); | |
| const portfolioBody = document.getElementById('portfolioBody'); | |
| const apiCounter = document.getElementById('apiCounter'); | |
| const totalInvestment = document.getElementById('totalInvestment'); | |
| const currentValue = document.getElementById('currentValue'); | |
| const profitLoss = document.getElementById('profitLoss'); | |
| // Set default date to today | |
| const today = new Date().toISOString().split('T')[0]; | |
| purchaseDate.value = today; | |
| // Initialize from localStorage | |
| loadPortfolioFromStorage(); | |
| updateSummary(); | |
| // Event listeners | |
| coinSearch.addEventListener('input', handleCoinSearch); | |
| coinSearch.addEventListener('focus', () => { | |
| if (coinSearch.value.length > 0) { | |
| coinDropdown.classList.remove('hidden'); | |
| } | |
| }); | |
| coinSearch.addEventListener('blur', () => { | |
| setTimeout(() => coinDropdown.classList.add('hidden'), 200); | |
| }); | |
| addCoin.addEventListener('click', addCoinToPortfolio); | |
| loadFromCache.addEventListener('click', loadPortfolioFromStorage); | |
| // Functions | |
| function handleCoinSearch() { | |
| const searchTerm = coinSearch.value.toLowerCase(); | |
| if (searchTerm.length === 0) { | |
| coinDropdown.classList.add('hidden'); | |
| return; | |
| } | |
| const filteredCoins = sampleCoins.filter(coin => | |
| coin.name.toLowerCase().includes(searchTerm) || | |
| coin.code.toLowerCase().includes(searchTerm) | |
| ); | |
| renderCoinDropdown(filteredCoins); | |
| } | |
| function renderCoinDropdown(coins) { | |
| coinDropdown.innerHTML = ''; | |
| if (coins.length === 0) { | |
| coinDropdown.innerHTML = '<div class="px-4 py-2 text-sm text-gray-500">No coins found</div>'; | |
| coinDropdown.classList.remove('hidden'); | |
| return; | |
| } | |
| coins.forEach(coin => { | |
| const coinItem = document.createElement('div'); | |
| coinItem.className = 'px-4 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center'; | |
| coinItem.innerHTML = ` | |
| <span>${coin.name} (${coin.code})</span> | |
| <button class="text-blue-500 hover:text-blue-700" data-code="${coin.code}" data-name="${coin.name}"> | |
| <i data-feather="plus" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| coinItem.querySelector('button').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| coinSearch.value = `${coin.name} (${coin.code})`; | |
| coinDropdown.classList.add('hidden'); | |
| }); | |
| coinDropdown.appendChild(coinItem); | |
| }); | |
| coinDropdown.classList.remove('hidden'); | |
| feather.replace(); | |
| } | |
| async function addCoinToPortfolio() { | |
| const coinMatch = coinSearch.value.match(/\((.*?)\)/); | |
| if (!coinMatch || !coinMatch[1]) { | |
| alert('Please select a valid cryptocurrency from the dropdown'); | |
| return; | |
| } | |
| const coinCode = coinMatch[1]; | |
| const coinName = coinSearch.value.split('(')[0].trim(); | |
| const qty = parseFloat(quantity.value); | |
| const date = purchaseDate.value; | |
| const avgCostValue = avgCost.value ? parseFloat(avgCost.value) : null; | |
| if (!qty || qty <= 0) { | |
| alert('Please enter a valid quantity'); | |
| return; | |
| } | |
| // Get current price | |
| try { | |
| const priceData = await getCoinPrice(coinCode); | |
| const price = priceData.rate; | |
| // For historical date, get historical price | |
| let historicalPrice = price; | |
| let daysSince = 0; | |
| if (date !== today) { | |
| const historicalData = await getHistoricalPrice(coinCode, date); | |
| historicalPrice = historicalData.rate; | |
| const purchaseDateObj = new Date(date); | |
| const todayObj = new Date(); | |
| daysSince = Math.floor((todayObj - purchaseDateObj) / (1000 * 60 * 60 * 24)); | |
| } | |
| // Create portfolio item | |
| const currentCost = qty * price; | |
| const avgValue = avgCostValue ? qty * avgCostValue : currentCost; | |
| const diffDollar = currentCost - avgValue; | |
| const diffPercent = ((currentCost - avgValue) / avgValue) * 100; | |
| const status = diffDollar >= 0 ? 'Profit' : 'Loss'; | |
| const portfolioItem = { | |
| id: Date.now(), | |
| coinCode, | |
| coinName, | |
| date, | |
| price, | |
| historicalPrice, | |
| qty, | |
| currentCost, | |
| avgCost: avgCostValue, | |
| avgValue, | |
| diffDollar, | |
| diffPercent, | |
| status, | |
| daysSince | |
| }; | |
| addPortfolioRow(portfolioItem); | |
| savePortfolioToStorage(); | |
| updateSummary(); | |
| // Reset form | |
| coinSearch.value = ''; | |
| quantity.value = ''; | |
| avgCost.value = ''; | |
| purchaseDate.value = today; | |
| } catch (error) { | |
| console.error('Error adding coin:', error); | |
| alert('Failed to fetch coin data. Please try again.'); | |
| } | |
| } | |
| function addPortfolioRow(item) { | |
| const row = document.createElement('tr'); | |
| row.className = 'hover:bg-gray-50'; | |
| row.dataset.id = item.id; | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="text-sm font-medium text-gray-900">${item.coinName}</div> | |
| <div class="ml-2 text-sm text-gray-500">${item.coinCode}</div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatDate(item.date)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <span class="text-sm text-gray-900">$${item.price.toFixed(2)}</span> | |
| <button class="ml-2 text-blue-500 hover:text-blue-700 refresh-price" data-coin="${item.coinCode}"> | |
| <i data-feather="refresh-cw" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <input type="number" value="${item.qty}" step="0.00000001" class="qty-input w-24 px-2 py-1 border border-gray-300 rounded-md text-sm text-gray-900"> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">$${item.currentCost.toFixed(2)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <input type="number" value="${item.avgCost || ''}" step="0.01" class="avg-cost-input w-24 px-2 py-1 border border-gray-300 rounded-md text-sm text-gray-900"> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">$${item.avgValue.toFixed(2)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm ${item.diffDollar >= 0 ? 'text-green-600' : 'text-red-600'}"> | |
| $${item.diffDollar.toFixed(2)} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm ${item.diffPercent >= 0 ? 'text-green-600' : 'text-red-600'}"> | |
| ${item.diffPercent.toFixed(2)}% | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 py-1 text-xs rounded-full ${item.status === 'Profit' ? 'profit' : 'loss'}"> | |
| ${item.status} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.daysSince}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
| <button class="text-red-500 hover:text-red-700 delete-item"> | |
| <i data-feather="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </td> | |
| `; | |
| portfolioBody.appendChild(row); | |
| feather.replace(); | |
| // Add event listeners for the new row | |
| row.querySelector('.refresh-price').addEventListener('click', async (e) => { | |
| const coinCode = e.target.closest('button').dataset.coin; | |
| try { | |
| const priceData = await getCoinPrice(coinCode); | |
| const priceCell = e.target.closest('td'); | |
| priceCell.querySelector('span').textContent = `$${priceData.rate.toFixed(2)}`; | |
| // Update the entire row data | |
| updateRowData(row, priceData.rate); | |
| savePortfolioToStorage(); | |
| updateSummary(); | |
| } catch (error) { | |
| console.error('Error refreshing price:', error); | |
| alert('Failed to refresh price. Please try again.'); | |
| } | |
| }); | |
| row.querySelector('.qty-input').addEventListener('change', (e) => { | |
| const newQty = parseFloat(e.target.value); | |
| if (!isNaN(newQty) && newQty > 0) { | |
| updateRowData(row, null, newQty); | |
| savePortfolioToStorage(); | |
| updateSummary(); | |
| } | |
| }); | |
| row.querySelector('.avg-cost-input').addEventListener('change', (e) => { | |
| const newAvgCost = e.target.value ? parseFloat(e.target.value) : null; | |
| updateRowData(row, null, null, newAvgCost); | |
| savePortfolioToStorage(); | |
| updateSummary(); | |
| }); | |
| row.querySelector('.delete-item').addEventListener('click', () => { | |
| row.remove(); | |
| savePortfolioToStorage(); | |
| updateSummary(); | |
| }); | |
| } | |
| function updateRowData(row, newPrice, newQty, newAvgCost) { | |
| const cells = row.querySelectorAll('td'); | |
| const currentPrice = newPrice !== null ? newPrice : parseFloat(cells[2].querySelector('span').textContent.replace('$', '')); | |
| const qty = newQty !== null ? newQty : parseFloat(cells[3].querySelector('input').value); | |
| const avgCost = newAvgCost !== null ? newAvgCost : (cells[5].querySelector('input').value ? parseFloat(cells[5].querySelector('input').value) : null); | |
| const currentCost = qty * currentPrice; | |
| const avgValue = avgCost ? qty * avgCost : currentCost; | |
| const diffDollar = currentCost - avgValue; | |
| const diffPercent = ((currentCost - avgValue) / avgValue) * 100; | |
| const status = diffDollar >= 0 ? 'Profit' : 'Loss'; | |
| // Update cells | |
| if (newPrice !== null) { | |
| cells[2].querySelector('span').textContent = `$${currentPrice.toFixed(2)}`; | |
| } | |
| if (newQty !== null) { | |
| cells[3].querySelector('input').value = qty; | |
| } | |
| if (newAvgCost !== null) { | |
| cells[5].querySelector('input').value = avgCost !== null ? avgCost : ''; | |
| } | |
| cells[4].textContent = `$${currentCost.toFixed(2)}`; | |
| cells[6].textContent = `$${avgValue.toFixed(2)}`; | |
| cells[7].textContent = `$${diffDollar.toFixed(2)}`; | |
| cells[7].className = `px-6 py-4 whitespace-nowrap text-sm ${diffDollar >= 0 ? 'text-green-600' : 'text-red-600'}`; | |
| cells[8].textContent = `${diffPercent.toFixed(2)}%`; | |
| cells[8].className = `px-6 py-4 whitespace-nowrap text-sm ${diffPercent >= 0 ? 'text-green-600' : 'text-red-600'}`; | |
| cells[9].innerHTML = `<span class="px-2 py-1 text-xs rounded-full ${status === 'Profit' ? 'profit' : 'loss'}">${status}</span>`; | |
| } | |
| async function getCoinPrice(coinCode) { | |
| // Check if we've already called the API today | |
| const currentDate = new Date().toDateString(); | |
| if (currentDate !== lastApiCallDate) { | |
| apiCallCount = 0; | |
| lastApiCallDate = currentDate; | |
| } | |
| // Check localStorage for cached price | |
| const cacheKey = `price_${coinCode}_${new Date().toDateString()}`; | |
| const cachedPrice = localStorage.getItem(cacheKey); | |
| if (cachedPrice) { | |
| return JSON.parse(cachedPrice); | |
| } | |
| // Call API | |
| const response = await fetch(`${API_BASE_URL}/coins/single`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': API_KEY | |
| }, | |
| body: JSON.stringify({ | |
| code: coinCode, | |
| currency: 'USD', | |
| meta: false | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch coin price'); | |
| } | |
| const data = await response.json(); | |
| // Update API call counter | |
| apiCallCount++; | |
| updateApiCounter(); | |
| // Cache the result | |
| localStorage.setItem(cacheKey, JSON.stringify({ | |
| rate: data.rate, | |
| timestamp: Date.now() | |
| })); | |
| return { rate: data.rate }; | |
| } | |
| async function getHistoricalPrice(coinCode, date) { | |
| // Check localStorage for cached historical price | |
| const cacheKey = `historical_${coinCode}_${date}`; | |
| const cachedPrice = localStorage.getItem(cacheKey); | |
| if (cachedPrice) { | |
| return JSON.parse(cachedPrice); | |
| } | |
| // Convert date to timestamp (end of day) | |
| const dateObj = new Date(date); | |
| dateObj.setHours(23, 59, 59, 999); | |
| const timestamp = Math.floor(dateObj.getTime() / 1000); | |
| // Call API | |
| const response = await fetch(`${API_BASE_URL}/coins/single/history`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': API_KEY | |
| }, | |
| body: JSON.stringify({ | |
| code: coinCode, | |
| currency: 'USD', | |
| start: timestamp, | |
| end: timestamp, | |
| meta: false | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch historical price'); | |
| } | |
| const data = await response.json(); | |
| // Update API call counter | |
| apiCallCount++; | |
| updateApiCounter(); | |
| // Cache the result | |
| if (data.history && data.history.length > 0) { | |
| localStorage.setItem(cacheKey, JSON.stringify({ | |
| rate: data.history[0].rate, | |
| timestamp: Date.now() | |
| })); | |
| return { rate: data.history[0].rate }; | |
| } else { | |
| throw new Error('No historical data available'); | |
| } | |
| } | |
| function updateApiCounter() { | |
| apiCounter.textContent = `API calls today: ${apiCallCount}`; | |
| } | |
| function formatDate(dateString) { | |
| const options = { year: 'numeric', month: 'short', day: 'numeric' }; | |
| return new Date(dateString).toLocaleDateString(undefined, options); | |
| } | |
| function savePortfolioToStorage() { | |
| const portfolioItems = []; | |
| document.querySelectorAll('#portfolioBody tr').forEach(row => { | |
| const cells = row.querySelectorAll('td'); | |
| portfolioItems.push({ | |
| id: row.dataset.id, | |
| coinCode: cells[0].querySelector('div:last-child').textContent.trim(), | |
| coinName: cells[0].querySelector('div:first-child').textContent.trim(), | |
| date: cells[1].textContent.trim(), | |
| price: parseFloat(cells[2].querySelector('span').textContent.replace('$', '')), | |
| qty: parseFloat(cells[3].querySelector('input').value), | |
| avgCost: cells[5].querySelector('input').value ? parseFloat(cells[5].querySelector('input').value) : null, | |
| daysSince: parseInt(cells[10].textContent.trim()) || 0 | |
| }); | |
| }); | |
| localStorage.setItem('cryptoPortfolio', JSON.stringify(portfolioItems)); | |
| } | |
| async function loadPortfolioFromStorage() { | |
| const savedPortfolio = localStorage.getItem('cryptoPortfolio'); | |
| if (!savedPortfolio) return; | |
| portfolioBody.innerHTML = ''; | |
| const portfolioItems = JSON.parse(savedPortfolio); | |
| for (const item of portfolioItems) { | |
| try { | |
| // Get current price | |
| const priceData = await getCoinPrice(item.coinCode); | |
| const price = priceData.rate; | |
| // For historical date, get historical price | |
| let historicalPrice = price; | |
| let daysSince = item.daysSince; | |
| if (item.date !== today) { | |
| try { | |
| const historicalData = await getHistoricalPrice(item.coinCode, item.date); | |
| historicalPrice = historicalData.rate; | |
| } catch (error) { | |
| console.error('Error fetching historical price:', error); | |
| // Use current price if historical fails | |
| historicalPrice = price; | |
| } | |
| } | |
| // Calculate values | |
| const currentCost = item.qty * price; | |
| const avgValue = item.avgCost ? item.qty * item.avgCost : currentCost; | |
| const diffDollar = currentCost - avgValue; | |
| const diffPercent = ((currentCost - avgValue) / avgValue) * 100; | |
| const status = diffDollar >= 0 ? 'Profit' : 'Loss'; | |
| const portfolioItem = { | |
| id: item.id, | |
| coinCode: item.coinCode, | |
| coinName: item.coinName, | |
| date: item.date, | |
| price, | |
| historicalPrice, | |
| qty: item.qty, | |
| currentCost, | |
| avgCost: item.avgCost, | |
| avgValue, | |
| diffDollar, | |
| diffPercent, | |
| status, | |
| daysSince | |
| }; | |
| addPortfolioRow(portfolioItem); | |
| } catch (error) { | |
| console.error('Error loading portfolio item:', error); | |
| } | |
| } | |
| updateSummary(); | |
| } | |
| function updateSummary() { | |
| let totalInv = 0; | |
| let totalCurrent = 0; | |
| document.querySelectorAll('#portfolioBody tr').forEach(row => { | |
| const cells = row.querySelectorAll('td'); | |
| const avgValue = parseFloat(cells[6].textContent.replace('$', '')); | |
| const currentValue = parseFloat(cells[4].textContent.replace('$', '')); | |
| totalInv += avgValue; | |
| totalCurrent += currentValue; | |
| }); | |
| const profitLossValue = totalCurrent - totalInv; | |
| totalInvestment.textContent = `$${totalInv.toFixed(2)}`; | |
| currentValue.textContent = `$${totalCurrent.toFixed(2)}`; | |
| profitLoss.textContent = `$${profitLossValue.toFixed(2)}`; | |
| // Style profit/loss | |
| profitLoss.className = `text-2xl font-bold ${profitLossValue >= 0 ? 'text-green-600' : 'text-red-600'}`; | |
| } | |
| </script> | |
| </body> | |
| </html> | |