Latest fix

This commit is contained in:
Eden Kirin
2025-12-25 20:02:47 +01:00
parent f077893c39
commit af9bf84d0a
4 changed files with 989 additions and 27 deletions

1
.gitignore vendored
View File

@ -32,6 +32,5 @@ Thumbs.db
*.csv *.csv
# Static build artifacts # Static build artifacts
static/
dist/ dist/
build/ build/

View File

@ -1,2 +1,5 @@
build:
@docker compose up --build -d
run: run:
@docker compose up @docker compose up

View File

@ -67,42 +67,68 @@ func fetchExchangeRate() (float64, error) {
func fetchStockPrice(ticker string) (float64, error) { func fetchStockPrice(ticker string) (float64, error) {
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
// Try different ticker formats for European stocks
tickersToTry := []string{ticker}
// If ticker doesn't have exchange suffix, try adding common ones
if !strings.Contains(ticker, ".") {
tickersToTry = append(tickersToTry,
ticker+".L", // London Stock Exchange
ticker+".AS", // Amsterdam
ticker+".PA", // Paris
ticker+".DE", // XETRA (Germany)
ticker+".MI", // Milan
)
}
var lastErr error
for _, tryTicker := range tickersToTry {
// Try Yahoo Finance API // Try Yahoo Finance API
url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", ticker) url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", tryTicker)
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return 0, err lastErr = err
continue
} }
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to fetch from Yahoo: %w", err) lastErr = fmt.Errorf("failed to fetch from Yahoo: %w", err)
continue
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to read response: %w", err) lastErr = fmt.Errorf("failed to read response: %w", err)
continue
} }
var chartData YahooChartResponse var chartData YahooChartResponse
if err := json.Unmarshal(body, &chartData); err != nil { if err := json.Unmarshal(body, &chartData); err != nil {
return 0, fmt.Errorf("failed to decode Yahoo response: %w", err) lastErr = fmt.Errorf("failed to decode Yahoo response: %w", err)
continue
} }
if chartData.Chart.Error != nil { if chartData.Chart.Error != nil {
return 0, fmt.Errorf("Yahoo API error: %s", chartData.Chart.Error.Description) lastErr = fmt.Errorf("Yahoo API error: %s", chartData.Chart.Error.Description)
continue
} }
if len(chartData.Chart.Result) > 0 { if len(chartData.Chart.Result) > 0 {
price := chartData.Chart.Result[0].Meta.RegularMarketPrice price := chartData.Chart.Result[0].Meta.RegularMarketPrice
if price > 0 { if price > 0 {
log.Printf("Successfully fetched price for %s (tried as %s): $%.2f", ticker, tryTicker, price)
return price, nil return price, nil
} }
} }
}
if lastErr != nil {
return 0, fmt.Errorf("price not found after trying all formats: %w", lastErr)
}
return 0, fmt.Errorf("price not found in response") return 0, fmt.Errorf("price not found in response")
} }

934
static/index.html Normal file
View File

@ -0,0 +1,934 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stock Tax Analyzer</title>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.production.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
// Lucide icons as inline SVG components
const Upload = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
);
const Loader2 = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="animate-spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
// Format number with thousand separators
const formatCurrency = (value) => {
if (value === null || value === undefined || isNaN(value))
return "0.00";
return value.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
};
function StockTaxAnalyzer() {
const [results, setResults] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const knownSplits = {
NVDA: [
{ date: "2024-06-10", ratio: 10 },
{ date: "2021-07-20", ratio: 4 },
{ date: "2007-09-11", ratio: 1.5 },
{ date: "2006-04-07", ratio: 2 },
{ date: "2001-09-17", ratio: 2 },
{ date: "2000-06-27", ratio: 2 },
],
TSLA: [
{ date: "2022-08-25", ratio: 3 },
{ date: "2020-08-31", ratio: 5 },
],
AAPL: [
{ date: "2020-08-31", ratio: 4 },
{ date: "2014-06-09", ratio: 7 },
{ date: "2005-02-28", ratio: 2 },
{ date: "2000-06-21", ratio: 2 },
{ date: "1987-06-16", ratio: 2 },
],
GOOGL: [{ date: "2022-07-18", ratio: 20 }],
AMZN: [
{ date: "2022-06-06", ratio: 20 },
{ date: "1999-09-02", ratio: 2 },
{ date: "1999-01-05", ratio: 3 },
{ date: "1998-06-02", ratio: 2 },
],
MSFT: [],
NFLX: [],
AMD: [],
COIN: [],
VWCE: [], // Vanguard FTSE All-World UCITS ETF - no splits
};
const fetchStockPrices = async (tickers) => {
try {
const response = await fetch("/api/stock-prices", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ tickers }),
});
if (!response.ok) {
throw new Error("Failed to fetch stock prices");
}
const data = await response.json();
return {
prices: data.prices,
exchangeRate: data.exchangeRate,
};
} catch (err) {
console.error("Error fetching prices:", err);
return {
prices: {},
exchangeRate: 0.92,
error: err.message,
};
}
};
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
setLoading(true);
setError(null);
try {
const text = await file.text();
const lines = text.split("\n");
const headers = lines[0]
.split(",")
.map((h) => h.trim().replace(/"/g, ""));
const data = lines
.slice(1)
.map((line) => {
const values = line
.split(",")
.map((v) => v.trim().replace(/"/g, ""));
const row = {};
headers.forEach((header, i) => {
row[header] = values[i];
});
return row;
})
.filter((row) => row.Date);
data.sort(
(a, b) => new Date(a.Date) - new Date(b.Date),
);
const today = new Date();
const twoYearsAgo = new Date(today);
twoYearsAgo.setFullYear(today.getFullYear() - 2);
const allBuys = data.filter(
(row) => row.Type === "BUY - MARKET" && row.Ticker,
);
const oldBuys = allBuys.filter(
(row) => new Date(row.Date) < twoYearsAgo,
);
const processHoldings = (buys) => {
const holdings = {};
buys.forEach((buy) => {
const ticker = buy.Ticker;
if (!holdings[ticker]) {
holdings[ticker] = {
quantity: 0,
totalCost: 0,
transactions: [],
dates: [],
appliedSplits: [],
};
}
let quantity = parseFloat(buy.Quantity) || 0;
const buyDate = new Date(buy.Date);
const priceStr = buy["Price per share"] || "";
let price = parseFloat(
priceStr.replace(/[$,]/g, ""),
);
let splitMultiplier = 1;
if (knownSplits[ticker]) {
const relevantSplits = knownSplits[
ticker
].filter(
(split) =>
new Date(split.date) > buyDate,
);
relevantSplits.forEach((split) => {
splitMultiplier *= split.ratio;
const splitExists = holdings[
ticker
].appliedSplits.some(
(s) => s.date === split.date,
);
if (!splitExists) {
holdings[ticker].appliedSplits.push(
split,
);
}
});
}
const adjustedQuantity =
quantity * splitMultiplier;
holdings[ticker].quantity += adjustedQuantity;
if (!isNaN(price)) {
holdings[ticker].totalCost +=
price * quantity;
}
holdings[ticker].transactions.push(buy);
holdings[ticker].dates.push(buyDate);
});
return holdings;
};
const allHoldings = processHoldings(allBuys);
const oldHoldings = processHoldings(oldBuys);
const allTickers = [
...new Set([
...Object.keys(allHoldings),
...Object.keys(oldHoldings),
]),
];
const { prices, exchangeRate } =
await fetchStockPrices(allTickers);
const formattedHoldings = allTickers
.map((ticker) => {
const allInfo = allHoldings[ticker] || {
quantity: 0,
totalCost: 0,
transactions: [],
dates: [],
appliedSplits: [],
};
const oldInfo = oldHoldings[ticker] || {
quantity: 0,
totalCost: 0,
transactions: [],
dates: [],
appliedSplits: [],
};
const currentPrice = prices[ticker];
const totalQuantity = allInfo.quantity;
const totalCostEur =
allInfo.totalCost * exchangeRate;
const totalCurrentValueEur =
currentPrice?.success
? totalQuantity * currentPrice.eur
: null;
const totalGainLossEur = totalCurrentValueEur
? totalCurrentValueEur - totalCostEur
: null;
const totalGainLossPercent =
totalGainLossEur && totalCostEur
? (totalGainLossEur / totalCostEur) *
100
: null;
const oldQuantity = oldInfo.quantity;
const oldCostEur =
oldInfo.totalCost * exchangeRate;
const oldCurrentValueEur = currentPrice?.success
? oldQuantity * currentPrice.eur
: null;
const oldGainLossEur = oldCurrentValueEur
? oldCurrentValueEur - oldCostEur
: null;
const oldGainLossPercent =
oldGainLossEur && oldCostEur
? (oldGainLossEur / oldCostEur) * 100
: null;
return {
ticker,
totalQuantity,
totalCostEur,
totalCurrentValueEur,
totalGainLossEur,
totalGainLossPercent,
totalTransactions:
allInfo.transactions.length,
oldQuantity,
oldCostEur,
oldCurrentValueEur,
oldGainLossEur,
oldGainLossPercent,
oldTransactions:
oldInfo.transactions.length,
oldestDate:
oldInfo.dates.length > 0
? new Date(
Math.min(...oldInfo.dates),
)
: null,
newestOldDate:
oldInfo.dates.length > 0
? new Date(
Math.max(...oldInfo.dates),
)
: null,
currentPriceEur: currentPrice?.eur,
avgPriceEur:
totalQuantity > 0
? (allInfo.totalCost /
totalQuantity) *
exchangeRate
: 0,
oldAvgPriceEur:
oldQuantity > 0
? (oldInfo.totalCost /
oldQuantity) *
exchangeRate
: 0,
priceAvailable:
currentPrice?.success || false,
splits: oldInfo.appliedSplits.sort(
(a, b) =>
new Date(a.date) - new Date(b.date),
),
hasSplitData:
knownSplits[ticker] !== undefined,
};
})
.sort((a, b) => a.ticker.localeCompare(b.ticker));
const totalAllInvestedEur = formattedHoldings.reduce(
(sum, h) => sum + h.totalCostEur,
0,
);
const totalAllCurrentValueEur =
formattedHoldings.reduce(
(sum, h) => sum + (h.totalCurrentValueEur || 0),
0,
);
const totalAllGainLossEur =
totalAllCurrentValueEur - totalAllInvestedEur;
const totalAllGainLossPercent =
(totalAllGainLossEur / totalAllInvestedEur) * 100;
const totalOldInvestedEur = formattedHoldings.reduce(
(sum, h) => sum + h.oldCostEur,
0,
);
const totalOldCurrentValueEur =
formattedHoldings.reduce(
(sum, h) => sum + (h.oldCurrentValueEur || 0),
0,
);
const totalOldGainLossEur =
totalOldCurrentValueEur - totalOldInvestedEur;
const totalOldGainLossPercent =
totalOldInvestedEur > 0
? (totalOldGainLossEur / totalOldInvestedEur) *
100
: 0;
setResults({
holdings: formattedHoldings,
totalAllInvestedEur,
totalAllCurrentValueEur,
totalAllGainLossEur,
totalAllGainLossPercent,
totalOldInvestedEur,
totalOldCurrentValueEur,
totalOldGainLossEur,
totalOldGainLossPercent,
exchangeRate,
cutoffDate: twoYearsAgo,
});
setLoading(false);
} catch (err) {
setError("Error parsing file: " + err.message);
setResults(null);
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
<div className="max-w-7xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8">
<h1 className="text-3xl font-bold text-gray-800 mb-2">
Stock Tax Analyzer
</h1>
<p className="text-gray-600 mb-2">
Compare total holdings vs. stocks bought
more than 2 years ago
</p>
<p className="text-sm text-indigo-600 mb-6">
Real-time prices in EUR Split
adjustments Tax planning
</p>
<div className="mb-8">
<label className="flex items-center justify-center w-full h-32 px-4 transition bg-white border-2 border-gray-300 border-dashed rounded-lg appearance-none cursor-pointer hover:border-indigo-400 focus:outline-none">
<span className="flex items-center space-x-2">
{loading ? (
<>
<Loader2 />
<span className="font-medium text-indigo-600">
Loading prices...
</span>
</>
) : (
<>
<Upload />
<span className="font-medium text-gray-600">
Click to upload CSV file
</span>
</>
)}
</span>
<input
type="file"
accept=".csv"
onChange={handleFileUpload}
className="hidden"
disabled={loading}
/>
</label>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
)}
{results && (
<div>
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg p-6 mb-6">
<h2 className="text-2xl font-bold text-gray-800 mb-4">
Portfolio Summary
</h2>
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-700 mb-3">
All Holdings (Total
Portfolio)
</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg p-4 shadow">
<div className="text-sm text-gray-600 mb-1">
Stocks
</div>
<div className="text-2xl font-bold text-indigo-600">
{
results.holdings
.length
}
</div>
</div>
<div className="bg-white rounded-lg p-4 shadow">
<div className="text-sm text-gray-600 mb-1">
Total Invested
</div>
<div className="text-2xl font-bold text-blue-600">
{formatCurrency(
results.totalAllInvestedEur,
)}
</div>
</div>
<div className="bg-white rounded-lg p-4 shadow">
<div className="text-sm text-gray-600 mb-1">
Current Value
</div>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(
results.totalAllCurrentValueEur,
)}
</div>
</div>
<div className="bg-white rounded-lg p-4 shadow">
<div className="text-sm text-gray-600 mb-1">
Total Gain/Loss
</div>
<div
className={`text-2xl font-bold ${results.totalAllGainLossEur >= 0 ? "text-green-600" : "text-red-600"}`}
>
{results.totalAllGainLossEur >=
0
? "+"
: ""}
{formatCurrency(
results.totalAllGainLossEur,
)}
</div>
<div
className={`text-sm font-semibold ${results.totalAllGainLossPercent >= 0 ? "text-green-600" : "text-red-600"}`}
>
{results.totalAllGainLossPercent >=
0
? "+"
: ""}
{(
results.totalAllGainLossPercent ||
0
).toFixed(2)}
%
</div>
</div>
</div>
</div>
<div className="border-t-2 border-indigo-200 pt-6">
<h3 className="text-lg font-semibold text-amber-700 mb-1">
🏆 Tax-Eligible Holdings
(Bought Before{" "}
{results.cutoffDate.toLocaleDateString()}
)
</h3>
<p className="text-xs text-amber-600 mb-3">
Stocks held for more than 2
years
</p>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
<div className="text-sm text-amber-700 mb-1">
Stocks
</div>
<div className="text-2xl font-bold text-amber-700">
{
results.holdings.filter(
(h) =>
h.oldQuantity >
0,
).length
}
</div>
</div>
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
<div className="text-sm text-amber-700 mb-1">
Total Invested
</div>
<div className="text-2xl font-bold text-blue-700">
{formatCurrency(
results.totalOldInvestedEur,
)}
</div>
</div>
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
<div className="text-sm text-amber-700 mb-1">
Current Value
</div>
<div className="text-2xl font-bold text-green-700">
{formatCurrency(
results.totalOldCurrentValueEur,
)}
</div>
</div>
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
<div className="text-sm text-amber-700 mb-1">
Total Gain/Loss
</div>
<div
className={`text-2xl font-bold ${results.totalOldGainLossEur >= 0 ? "text-green-700" : "text-red-700"}`}
>
{results.totalOldGainLossEur >=
0
? "+"
: ""}
{formatCurrency(
results.totalOldGainLossEur,
)}
</div>
<div
className={`text-sm font-semibold ${results.totalOldGainLossPercent >= 0 ? "text-green-700" : "text-red-700"}`}
>
{results.totalOldGainLossPercent >=
0
? "+"
: ""}
{(
results.totalOldGainLossPercent ||
0
).toFixed(2)}
%
</div>
</div>
</div>
</div>
<div className="mt-4 text-xs text-gray-600">
Exchange rate: 1 USD =
{results.exchangeRate.toFixed(
4,
)}
</div>
</div>
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Detailed Stock Breakdown
</h2>
<div className="space-y-4">
{results.holdings.map((holding) => (
<div
key={holding.ticker}
className="bg-white rounded-lg p-6 border-2 border-gray-200 shadow-sm"
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-2xl font-bold text-indigo-700">
{holding.ticker}
</h3>
{holding.splits
.length > 0 && (
<div className="text-xs text-green-600 font-semibold mt-1">
Split-adjusted
(
{holding.splits.reduce(
(
acc,
s,
) =>
acc *
s.ratio,
1,
)}
x total)
</div>
)}
</div>
<div className="text-right">
{holding.priceAvailable && (
<div>
<div className="text-xs text-gray-600">
Current
Price
</div>
<div className="text-xl font-bold text-gray-900">
{formatCurrency(
holding.currentPriceEur,
)}
</div>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
<h4 className="font-bold text-blue-900 mb-3 flex items-center gap-2">
📊 Total
Holdings
<span className="text-xs font-normal text-blue-700">
(
{
holding.totalTransactions
}{" "}
purchases)
</span>
</h4>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-blue-700 mb-1">
Shares
</div>
<div className="font-bold text-gray-900">
{(
holding.totalQuantity ||
0
).toFixed(
4,
)}
</div>
</div>
<div>
<div className="text-blue-700 mb-1">
Avg Cost
</div>
<div className="font-semibold text-gray-800">
{formatCurrency(
holding.avgPriceEur,
)}
</div>
</div>
<div>
<div className="text-blue-700 mb-1">
Cost
Basis
</div>
<div className="font-bold text-blue-800">
{formatCurrency(
holding.totalCostEur,
)}
</div>
</div>
<div>
<div className="text-blue-700 mb-1">
Current
Value
</div>
<div className="font-bold text-green-700">
{holding.priceAvailable
? `${formatCurrency(holding.totalCurrentValueEur)}`
: "N/A"}
</div>
</div>
<div className="col-span-2">
<div className="text-blue-700 mb-1">
Gain/Loss
</div>
<div
className={`font-bold text-lg ${holding.totalGainLossEur >= 0 ? "text-green-600" : "text-red-600"}`}
>
{holding.priceAvailable ? (
<>
{holding.totalGainLossEur >=
0
? "+"
: ""}
{formatCurrency(
holding.totalGainLossEur,
)}
<span className="text-sm ml-2">
(
{holding.totalGainLossPercent >=
0
? "+"
: ""}
{(
holding.totalGainLossPercent ||
0
).toFixed(
1,
)}
%)
</span>
</>
) : (
"N/A"
)}
</div>
</div>
</div>
</div>
<div
className={`rounded-lg p-4 border-2 ${holding.oldQuantity > 0 ? "bg-amber-50 border-amber-300" : "bg-gray-50 border-gray-200"}`}
>
<h4
className={`font-bold mb-3 flex items-center gap-2 ${holding.oldQuantity > 0 ? "text-amber-900" : "text-gray-500"}`}
>
{holding.oldQuantity >
0
? "🏆 Tax-Eligible (>2 years)"
: "⏳ No Old Holdings"}
{holding.oldQuantity >
0 && (
<span className="text-xs font-normal text-amber-700">
(
{
holding.oldTransactions
}{" "}
purchases
{" "}
{(
(holding.oldQuantity /
holding.totalQuantity) *
100
).toFixed(
1,
)}
% of
total)
</span>
)}
</h4>
{holding.oldQuantity >
0 ? (
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-amber-700 mb-1">
Shares
</div>
<div className="font-bold text-gray-900">
{(
holding.oldQuantity ||
0
).toFixed(
4,
)}
</div>
</div>
<div>
<div className="text-amber-700 mb-1">
Avg
Cost
</div>
<div className="font-semibold text-gray-800">
{formatCurrency(
holding.oldAvgPriceEur,
)}
</div>
</div>
<div>
<div className="text-amber-700 mb-1">
Cost
Basis
</div>
<div className="font-bold text-blue-800">
{formatCurrency(
holding.oldCostEur,
)}
</div>
</div>
<div>
<div className="text-amber-700 mb-1">
Current
Value
</div>
<div className="font-bold text-green-700">
{holding.priceAvailable
? `${formatCurrency(holding.oldCurrentValueEur)}`
: "N/A"}
</div>
</div>
<div className="col-span-2">
<div className="text-amber-700 mb-1">
Gain/Loss
</div>
<div
className={`font-bold text-lg ${holding.oldGainLossEur >= 0 ? "text-green-600" : "text-red-600"}`}
>
{holding.priceAvailable ? (
<>
{holding.oldGainLossEur >=
0
? "+"
: ""}
{formatCurrency(
holding.oldGainLossEur,
)}
<span className="text-sm ml-2">
(
{holding.oldGainLossPercent >=
0
? "+"
: ""}
{(
holding.oldGainLossPercent ||
0
).toFixed(
1,
)}
%)
</span>
</>
) : (
"N/A"
)}
</div>
</div>
{holding.oldestDate && (
<div className="col-span-2 text-xs text-amber-700 mt-1">
Purchased:{" "}
{holding.oldestDate.toLocaleDateString()}{" "}
-{" "}
{holding.newestOldDate.toLocaleDateString()}
</div>
)}
</div>
) : (
<div className="text-sm text-gray-500 italic">
All shares
were bought
within the
last 2 years
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
}
ReactDOM.render(
<StockTaxAnalyzer />,
document.getElementById("root"),
);
</script>
</body>
</html>