Add DCA Calculator feature to dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
<span style={styles[actualHighlight]}>{fmtPL(actualPL)}</span>
|
||||
|
|
||||
DCA P&L:
|
||||
<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
|
||||
|
||||
Reference in New Issue
Block a user