109 lines
3.5 KiB
HTML
109 lines
3.5 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block title %}Portfolio — IBKR Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1>Portfolio</h1>
|
|
|
|
<div class="portfolio-summary" id="pnlCards">
|
|
<div class="summary-card">
|
|
<div class="card-label">Net Liquidation</div>
|
|
<div class="card-value" id="netLiq">—</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="card-label">Total Cash</div>
|
|
<div class="card-value" id="totalCash">—</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="card-label">Unrealized P&L</div>
|
|
<div class="card-value" id="unrealizedPnl">—</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="card-label">Realized P&L</div>
|
|
<div class="card-value" id="realizedPnl">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-container" style="margin-top: 24px;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Symbol</th>
|
|
<th>Position</th>
|
|
<th>Avg Cost</th>
|
|
<th>Chart</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="positionsBody">
|
|
<tr><td colspan="4" class="empty-state">Loading positions...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function fmt(val) {
|
|
if (val === undefined || val === null) return '—';
|
|
return '$' + parseFloat(val).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function colorClass(val) {
|
|
const n = parseFloat(val);
|
|
if (isNaN(n)) return '';
|
|
return n >= 0 ? 'positive' : 'negative';
|
|
}
|
|
|
|
async function loadPnl() {
|
|
try {
|
|
const res = await fetch('/portfolio/pnl');
|
|
const d = await res.json();
|
|
document.getElementById('netLiq').textContent = fmt(d.NetLiquidation);
|
|
document.getElementById('totalCash').textContent = fmt(d.TotalCashValue);
|
|
|
|
const upEl = document.getElementById('unrealizedPnl');
|
|
upEl.textContent = fmt(d.UnrealizedPnL);
|
|
upEl.className = 'card-value ' + colorClass(d.UnrealizedPnL);
|
|
|
|
const rpEl = document.getElementById('realizedPnl');
|
|
rpEl.textContent = fmt(d.RealizedPnL);
|
|
rpEl.className = 'card-value ' + colorClass(d.RealizedPnL);
|
|
} catch (e) {
|
|
console.error('PnL fetch error:', e);
|
|
}
|
|
}
|
|
|
|
async function loadPositions() {
|
|
try {
|
|
const res = await fetch('/portfolio/data');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('positionsBody');
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No open positions.</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = data.map(p => `
|
|
<tr>
|
|
<td><a class="symbol-link" href="/?symbol=${p.symbol}">${p.symbol}</a></td>
|
|
<td>${p.position}</td>
|
|
<td class="price-cell">${fmt(p.avg_cost)}</td>
|
|
<td><a class="symbol-link" href="/?symbol=${p.symbol}">View Chart</a></td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Positions fetch error:', e);
|
|
}
|
|
}
|
|
|
|
function refresh() {
|
|
loadPnl();
|
|
loadPositions();
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
refresh();
|
|
setInterval(refresh, 30000);
|
|
});
|
|
</script>
|
|
{% endblock %}
|