Add DCA Calculator feature to dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:17:46 +02:00
parent 59f833d7fd
commit fd21aa7f4e
4 changed files with 253 additions and 1 deletions
@@ -0,0 +1,140 @@
import React, { useState, useEffect, useRef } from 'react';
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
const styles = {
card: { background: '#1a1a1a', padding: '1rem', borderRadius: '12px', border: '1px solid #333', marginBottom: '1.5rem' },
header: { color: '#888', fontSize: '0.8rem', marginBottom: '0.75rem' },
row: { display: 'flex', gap: '1rem', alignItems: 'flex-start', flexWrap: 'wrap' },
inputWrap: { display: 'flex', flexDirection: 'column', gap: '0.3rem' },
label: { color: '#888', fontSize: '0.8rem' },
input: { background: '#111', border: '1px solid #444', borderRadius: '8px', color: '#fff', padding: '0.55rem 0.75rem', fontSize: '1rem', width: '160px', outline: 'none' },
results: { display: 'flex', flexDirection: 'column', gap: '0.75rem', flex: 1 },
statsRow: { display: 'flex', gap: '1rem', flexWrap: 'wrap' },
statCard: { background: '#111', padding: '0.75rem 1rem', borderRadius: '10px', border: '1px solid #2a2a2a', minWidth: '130px' },
statLabel: { color: '#888', fontSize: '0.75rem', marginBottom: '0.2rem' },
statValue: { fontSize: '1.1rem', fontWeight: 700 },
positive: { color: '#6bff8e' },
negative: { color: '#ff6b6b' },
neutral: { color: '#f7931a' },
comparison: { color: '#888', fontSize: '0.8rem' },
placeholder: { color: '#555', fontSize: '0.9rem', padding: '0.5rem 0' },
};
function MiniStatCard({ label, value, highlight }) {
return (
<div style={styles.statCard}>
<div style={styles.statLabel}>{label}</div>
<div style={{ ...styles.statValue, ...(highlight ? styles[highlight] : {}) }}>{value}</div>
</div>
);
}
export default function DCACalculator({ purchases, stats }) {
const defaultStart = () => {
if (purchases && purchases.length > 0) {
const sorted = [...purchases].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
return sorted[0].created_at.slice(0, 10);
}
const d = new Date();
d.setFullYear(d.getFullYear() - 1);
return d.toISOString().slice(0, 10);
};
const [monthlyAmount, setMonthlyAmount] = useState('');
const [startDate, setStartDate] = useState(defaultStart);
const [result, setResult] = useState(null);
const debounceRef = useRef(null);
// Update default start date when purchases load
useEffect(() => {
if (purchases && purchases.length > 0 && !monthlyAmount) {
setStartDate(defaultStart());
}
}, [purchases]); // eslint-disable-line
useEffect(() => {
const amount = parseFloat(monthlyAmount);
if (!amount || amount <= 0 || !startDate) {
setResult(null);
return;
}
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
try {
const res = await fetch(
`${API}/dca?monthly_amount=${amount}&start_date=${startDate}`,
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
);
if (res.ok) setResult(await res.json());
} catch {
// silently fail
}
}, 500);
return () => clearTimeout(debounceRef.current);
}, [monthlyAmount, startDate]);
const plHighlight = result
? result.dca_profit_loss >= 0 ? 'positive' : 'negative'
: null;
const actualPL = stats ? stats.profit_loss : null;
const actualHighlight = actualPL != null ? (actualPL >= 0 ? 'positive' : 'negative') : null;
const fmt = (n) => `${n.toLocaleString()}`;
const fmtPL = (n) => `${n >= 0 ? '+' : ''}${fmt(n)}`;
return (
<div style={styles.card}>
<div style={styles.header}>DCA Calculator What if you had invested monthly?</div>
<div style={styles.row}>
<div style={styles.inputWrap}>
<label style={styles.label}>Monthly amount ()</label>
<input
type="number"
min="1"
placeholder="e.g. 200"
value={monthlyAmount}
onChange={e => setMonthlyAmount(e.target.value)}
style={styles.input}
/>
</div>
<div style={styles.inputWrap}>
<label style={styles.label}>Starting from</label>
<input
type="date"
value={startDate}
onChange={e => setStartDate(e.target.value)}
style={styles.input}
/>
</div>
{result ? (
<div style={styles.results}>
<div style={styles.statsRow}>
<MiniStatCard label="DCA Invested" value={fmt(result.dca_total_invested)} />
<MiniStatCard label="DCA BTC" value={`${result.dca_total_btc}`} highlight="neutral" />
<MiniStatCard label="DCA Value" value={fmt(result.dca_current_value)} />
<MiniStatCard label="DCA P&L" value={fmtPL(result.dca_profit_loss)} highlight={plHighlight} />
</div>
{actualPL != null && (
<div style={styles.comparison}>
Actual P&L:&nbsp;
<span style={styles[actualHighlight]}>{fmtPL(actualPL)}</span>
&nbsp;&nbsp;|&nbsp;&nbsp;
DCA P&L:&nbsp;
<span style={styles[plHighlight]}>{fmtPL(result.dca_profit_loss)}</span>
</div>
)}
</div>
) : (
<div style={{ ...styles.placeholder, alignSelf: 'center' }}>
Enter a monthly amount to simulate DCA
</div>
)}
</div>
</div>
);
}
@@ -6,6 +6,7 @@ import AddSell from '../components/AddSell';
import SellList from '../components/SellList';
import PortfolioChart from '../components/PortfolioChart';
import BTCCandlestickChart from '../components/BTCCandlestickChart';
import DCACalculator from '../components/DCACalculator';
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
@@ -193,6 +194,8 @@ export default function Dashboard() {
</div>
</div>
<DCACalculator purchases={purchases} stats={stats} />
<div style={styles.tabs}>
{[['both', 'Both'], ['portfolio', 'Portfolio'], ['history', 'BTC Candles']].map(([key, label]) => (
<button