Improve portfolio chart with historical price-based data points

Chart now plots weekly data points from first purchase to today using
candle/history price data, giving an accurate view of portfolio value
over time rather than just at purchase dates.

Backend seeds up to 365 days of daily close prices from CoinGecko as
synthetic OHLC candles, refreshing stale entries older than 31 days.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 19:30:48 +02:00
parent ce227e47d6
commit 5cf3726f59
3 changed files with 112 additions and 24 deletions
@@ -19,40 +19,84 @@ const styles = {
saveBtn: { marginTop: '0.75rem', background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '6px', padding: '0.4rem 1rem', cursor: 'pointer', fontSize: '0.85rem' },
};
export default function PortfolioChart({ purchases, stats }) {
function toDateKey(date) {
return date.toISOString().split('T')[0];
}
function priceOn(date, priceMap, currentPrice, isToday, sortedPurchases) {
if (isToday) return currentPrice || 0;
// Try candle history (walk back up to 7 days)
for (let i = 0; i <= 7; i++) {
const d = new Date(date);
d.setDate(d.getDate() - i);
const p = priceMap[toDateKey(d)];
if (p) return p;
}
// Fall back to most recent purchase price up to this date
let fallback = null;
for (const p of sortedPurchases) {
const pd = new Date(p.created_at);
pd.setHours(0, 0, 0, 0);
if (pd <= date) fallback = p.price_eur;
}
return fallback;
}
export default function PortfolioChart({ purchases, stats, btcHistory }) {
const chartRef = useRef(null);
if (!purchases || purchases.length === 0) return null;
// Build cumulative data points sorted by date
const sorted = [...purchases].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const today = new Date();
today.setHours(0, 0, 0, 0);
// Build price lookup from candle history
const priceMap = {};
(btcHistory || []).forEach(({ date, close }) => { priceMap[date] = close; });
const firstDate = new Date(sorted[0].created_at);
firstDate.setHours(0, 0, 0, 0);
// Generate biweekly dates from first purchase to today
const dates = [];
const cursor = new Date(firstDate);
while (cursor <= today) {
dates.push(new Date(cursor));
cursor.setDate(cursor.getDate() + 7);
}
if (toDateKey(dates[dates.length - 1]) !== toDateKey(today)) {
dates.push(new Date(today));
}
let cumInvested = 0;
let cumBtc = 0;
const labels = [];
const portfolioValues = [];
const investedValues = [];
sorted.forEach((p, i) => {
cumInvested += p.amount_eur;
cumBtc += p.amount_eur / p.price_eur;
const currentVal = cumBtc * (stats?.current_price || p.price_eur);
labels.push(new Date(p.created_at).toLocaleDateString());
portfolioValues.push(parseFloat(currentVal.toFixed(2)));
dates.forEach(date => {
const isToday = toDateKey(date) === toDateKey(today);
const price = priceOn(date, priceMap, stats?.current_price, isToday, sorted);
if (price === null) return; // no price data available, skip
// Cumulative BTC and invested up to this date
let cumBtc = 0;
let cumInvested = 0;
sorted.forEach(p => {
const pDate = new Date(p.created_at);
pDate.setHours(0, 0, 0, 0);
if (pDate <= date) {
cumBtc += p.amount_eur / p.price_eur;
cumInvested += p.amount_eur;
}
});
if (cumBtc === 0) return; // no purchases yet at this date
labels.push(date.toLocaleDateString('en-GB'));
portfolioValues.push(parseFloat((cumBtc * price).toFixed(2)));
investedValues.push(parseFloat(cumInvested.toFixed(2)));
});
const todayLabel = new Date().toLocaleDateString();
if (labels.length === 0 || labels[labels.length - 1] !== todayLabel) {
const currentVal = cumBtc * (stats?.current_price || 0);
labels.push(todayLabel);
portfolioValues.push(parseFloat(currentVal.toFixed(2)));
investedValues.push(parseFloat(cumInvested.toFixed(2)));
}
const currentPrice = stats?.current_price || 0;
const breakEvenLine = labels.map(() => stats?.average_price || 0);
const data = {
labels,
datasets: [
@@ -135,7 +135,7 @@ export default function Dashboard() {
>{label}</button>
))}
</div>
{(chartView === 'both' || chartView === 'portfolio') && <PortfolioChart purchases={purchases} stats={stats} />}
{(chartView === 'both' || chartView === 'portfolio') && <PortfolioChart purchases={purchases} stats={stats} btcHistory={candles?.candles ?? []} />}
{(chartView === 'both' || chartView === 'history') && (
<BTCCandlestickChart
candles={activeCandles?.candles ?? null}