Cache last known BTC price and show stale warning in UI
When the CoinGecko API fails, fall back to the last successful price instead of 0.0, and surface a warning indicator on the price card. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ def get_stats(
|
|||||||
net_btc = total_btc_bought - total_btc_sold
|
net_btc = total_btc_bought - total_btc_sold
|
||||||
net_invested = total_invested - proceeds_eur
|
net_invested = total_invested - proceeds_eur
|
||||||
average_price = net_invested / net_btc if net_btc > 0 else 0.0
|
average_price = net_invested / net_btc if net_btc > 0 else 0.0
|
||||||
current_price = get_btc_price_eur()
|
current_price, price_is_cached = get_btc_price_eur()
|
||||||
portfolio_value = net_btc * current_price
|
portfolio_value = net_btc * current_price
|
||||||
profit_loss = portfolio_value - net_invested
|
profit_loss = portfolio_value - net_invested
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ def get_stats(
|
|||||||
"total_btc": round(net_btc, 8),
|
"total_btc": round(net_btc, 8),
|
||||||
"average_price": round(average_price, 2),
|
"average_price": round(average_price, 2),
|
||||||
"current_price": round(current_price, 2),
|
"current_price": round(current_price, 2),
|
||||||
|
"price_is_cached": price_is_cached,
|
||||||
"portfolio_value": round(portfolio_value, 2),
|
"portfolio_value": round(portfolio_value, 2),
|
||||||
"profit_loss": round(profit_loss, 2),
|
"profit_loss": round(profit_loss, 2),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ def aggregate_to_daily(raw: list) -> dict:
|
|||||||
return by_date
|
return by_date
|
||||||
|
|
||||||
|
|
||||||
def get_btc_price_eur() -> float:
|
_last_known_price: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def get_btc_price_eur() -> tuple[float, bool]:
|
||||||
|
"""Returns (price, is_cached). is_cached=True when using a stale fallback."""
|
||||||
|
global _last_known_price
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
"https://api.coingecko.com/api/v3/simple/price",
|
"https://api.coingecko.com/api/v3/simple/price",
|
||||||
@@ -62,7 +67,9 @@ def get_btc_price_eur() -> float:
|
|||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return float(resp.json()["bitcoin"]["eur"])
|
price = float(resp.json()["bitcoin"]["eur"])
|
||||||
|
_last_known_price = price
|
||||||
|
return price, False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to fetch BTC price: {e}")
|
logger.error(f"Failed to fetch BTC price: {e}")
|
||||||
return 0.0
|
return _last_known_price, True
|
||||||
|
|||||||
@@ -28,11 +28,19 @@ const styles = {
|
|||||||
tabActive: { padding: '0.45rem 1.1rem', borderRadius: '8px', border: '1px solid #f7931a', background: 'rgba(247,147,26,0.1)', color: '#f7931a', cursor: 'pointer', fontSize: '0.9rem', fontWeight: 700 },
|
tabActive: { padding: '0.45rem 1.1rem', borderRadius: '8px', border: '1px solid #f7931a', background: 'rgba(247,147,26,0.1)', color: '#f7931a', cursor: 'pointer', fontSize: '0.9rem', fontWeight: 700 },
|
||||||
};
|
};
|
||||||
|
|
||||||
function StatCard({ label, value, highlight }) {
|
function StatCard({ label, value, highlight, warning }) {
|
||||||
return (
|
return (
|
||||||
<div style={styles.statCard}>
|
<div style={styles.statCard}>
|
||||||
<div style={styles.statLabel}>{label}</div>
|
<div style={{ ...styles.statLabel, display: 'flex', alignItems: 'center', gap: '0.3rem' }}>
|
||||||
|
{label}
|
||||||
|
{warning && (
|
||||||
|
<span title={warning} style={{ color: '#f7931a', cursor: 'default', fontSize: '0.85rem' }}>⚠</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div style={{ ...styles.statValue, ...(highlight ? styles[highlight] : {}) }}>{value}</div>
|
<div style={{ ...styles.statValue, ...(highlight ? styles[highlight] : {}) }}>{value}</div>
|
||||||
|
{warning && (
|
||||||
|
<div style={{ color: '#888', fontSize: '0.7rem', marginTop: '0.25rem' }}>{warning}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,7 +129,11 @@ export default function Dashboard() {
|
|||||||
<StatCard label="Total Invested" value={`€${stats.total_invested.toLocaleString()}`} />
|
<StatCard label="Total Invested" value={`€${stats.total_invested.toLocaleString()}`} />
|
||||||
<StatCard label="Avg Buy Price" value={`€${stats.average_price.toLocaleString()}`} />
|
<StatCard label="Avg Buy Price" value={`€${stats.average_price.toLocaleString()}`} />
|
||||||
<StatCard label="Total BTC" value={`₿${stats.total_btc}`} highlight="neutral" />
|
<StatCard label="Total BTC" value={`₿${stats.total_btc}`} highlight="neutral" />
|
||||||
<StatCard label="Current BTC Price" value={`€${stats.current_price.toLocaleString()}`} />
|
<StatCard
|
||||||
|
label="Current BTC Price"
|
||||||
|
value={`€${stats.current_price.toLocaleString()}`}
|
||||||
|
warning={stats.price_is_cached ? 'Price may be outdated — live fetch failed' : undefined}
|
||||||
|
/>
|
||||||
<StatCard label="Portfolio Value" value={`€${stats.portfolio_value.toLocaleString()}`} />
|
<StatCard label="Portfolio Value" value={`€${stats.portfolio_value.toLocaleString()}`} />
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Profit / Loss"
|
label="Profit / Loss"
|
||||||
|
|||||||
Reference in New Issue
Block a user