diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad0a8a66..81e2276e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
always survives in a `title` tooltip; applied across Overview / Strategies /
Orders / PnL / Logs and the legacy single-engine dashboard (ui-ux-overhaul
leaf 02).
+- **Overview: positions by strategy + summary strip.** Positions now group by
+ strategy by default (dropping the redundant Strategy column from both header
+ and rows in that view), the group-by and KPI-level choices persist across
+ reloads (`localStorage`), and a summary strip above the KPI card shows
+ running/total strategies, open orders, total realised PnL (server-exact, no
+ client-side math) and the next scheduled tick (ui-ux-overhaul leaf 03).
### Fixed
diff --git a/doc/dev/plans/ui-ux-overhaul/00-plan.md b/doc/dev/plans/ui-ux-overhaul/00-plan.md
index 8ce88a14..ec5137ed 100644
--- a/doc/dev/plans/ui-ux-overhaul/00-plan.md
+++ b/doc/dev/plans/ui-ux-overhaul/00-plan.md
@@ -55,7 +55,7 @@ ever parsed back into a computation.
- [x] 01 api-read-model — feat/ui-api-read-model — medium
- [x] 02 formatters — feat/ui-formatters — medium
-- [ ] 03 overview-groups — feat/ui-overview-groups — low
+- [x] 03 overview-groups — feat/ui-overview-groups — low
- [ ] 04 cadence — feat/ui-cadence — medium
- [ ] 05 tables-polish — feat/ui-tables-polish — medium
diff --git a/doc/dev/plans/ui-ux-overhaul/03-overview-groups.md b/doc/dev/plans/ui-ux-overhaul/03-overview-groups.md
index 33b11f04..40fe42a2 100644
--- a/doc/dev/plans/ui-ux-overhaul/03-overview-groups.md
+++ b/doc/dev/plans/ui-ux-overhaul/03-overview-groups.md
@@ -1,12 +1,12 @@
---
plan: ui-ux-overhaul/03-overview-groups
kind: leaf
-status: planned
+status: done
complexity: low
depends: [02]
parallel: false
branch: feat/ui-overview-groups
-pr: ""
+pr: "https://github.com/ArthurBernard/Trading_Bot/pull/161"
---
# Leaf 03 — Overview: positions by strategy + summary strip
diff --git a/trading_bot/interfaces/ui/templates/overview.html b/trading_bot/interfaces/ui/templates/overview.html
index a53cb818..58770ab4 100644
--- a/trading_bot/interfaces/ui/templates/overview.html
+++ b/trading_bot/interfaces/ui/templates/overview.html
@@ -3,6 +3,18 @@
{% block content %}
Overview
+
+
+
+ —/— strategies running
+ — open orders
+ total PnL —
+ next check —
+
+
+
@@ -34,21 +46,25 @@
Performance
-
+
Positions
-
+
+
-
+
-
Strategy
Exchange
Instrument
Net qty
@@ -58,7 +74,7 @@
Positions
-
Loading…
+
Loading…
@@ -97,9 +113,30 @@
Open orders
// and format.js's tbFmt.*Cell() helpers (rounded, unit-labelled, exact value
// on hover — quote/base derived client-side from each row's instrument).
- // Current selections (mirrored on the toggle buttons).
- let KPI_LEVEL = 'strategy';
- let POS_GROUP = ''; // '' = flat, else 'crypto' | 'exchange'
+ // Current selections (mirrored on the toggle buttons), persisted across
+ // reloads in localStorage — restored on load, tolerating an absent or
+ // no-longer-valid stored value (falls back to the default below).
+ const POS_GROUP_KEY = 'tb.overview.posGroup';
+ const KPI_LEVEL_KEY = 'tb.overview.kpiLevel';
+ const POS_GROUPS = ['', 'strategy', 'crypto', 'exchange'];
+ const KPI_LEVELS = ['strategy', 'exchange', 'total'];
+
+ function loadPref(key, allowed, fallback) {
+ try {
+ const v = localStorage.getItem(key);
+ return allowed.indexOf(v) !== -1 ? v : fallback;
+ } catch (e) {
+ return fallback; // storage disabled (e.g. private browsing) — use the default
+ }
+ }
+ function savePref(key, value) {
+ try { localStorage.setItem(key, value); } catch (e) { /* ignore — in-memory state still works */ }
+ }
+
+ // 'strategy' by default — "how is each strategy doing?" is the page's
+ // headline question, so positions open grouped by strategy.
+ let KPI_LEVEL = loadPref(KPI_LEVEL_KEY, KPI_LEVELS, 'strategy');
+ let POS_GROUP = loadPref(POS_GROUP_KEY, POS_GROUPS, 'strategy'); // '' = flat
// A signed-money cell class: green for a gain, red for a loss.
function pnlClass(v) {
@@ -110,6 +147,69 @@
Open orders
return '';
}
+ // --- Summary strip ------------------------------------------------------ //
+ // Running/total strategies, open orders (reusing loadOrders()'s already-
+ // fetched payload — see below), total realised PnL, and the next tick.
+
+ async function loadSummaryStrategies() {
+ const totalEl = document.getElementById('sum-total');
+ const runningEl = document.getElementById('sum-running');
+ try {
+ const list = await api('GET', '/api/strategies');
+ totalEl.textContent = list.length;
+ runningEl.textContent = list.filter(function (s) { return s.running; }).length;
+ } catch (e) {
+ totalEl.textContent = '—';
+ runningEl.textContent = '—';
+ }
+ }
+
+ // Total realised PnL from /api/kpi?level=total — always exactly one row (a
+ // zero total even with nothing running), rendered via tbFmt.moneyCell with
+ // its quote (null quote -> no suffix + "mixed quote currencies" tooltip).
+ // No client-side money arithmetic — the server's exact Decimal total is the
+ // only source.
+ async function loadSummaryPnl() {
+ const pnlEl = document.getElementById('sum-pnl');
+ try {
+ const rows = await api('GET', '/api/kpi?level=total');
+ const total = rows[0];
+ if (total) {
+ pnlEl.className = pnlClass(total.realised_pnl);
+ pnlEl.innerHTML = tbFmt.moneyCell(total.realised_pnl, { quote: total.quote });
+ } else {
+ pnlEl.className = '';
+ pnlEl.textContent = '—';
+ }
+ } catch (e) {
+ pnlEl.className = '';
+ pnlEl.textContent = '—';
+ }
+ }
+
+ // A compact "in Xs/Xm/Xh" label from /api/health's next_tick_ts (epoch ms),
+ // recomputed at each refresh — a static render only; the live 1-second
+ // countdown is leaf 04's job.
+ function nextTickLabel(nextTickTs) {
+ if (nextTickTs == null) return '—';
+ const deltaS = Math.round((nextTickTs - Date.now()) / 1000);
+ if (deltaS <= 0) return 'due now';
+ if (deltaS < 60) return 'in ' + deltaS + 's';
+ const deltaM = Math.round(deltaS / 60);
+ if (deltaM < 60) return 'in ' + deltaM + 'm';
+ return 'in ' + Math.round(deltaM / 60) + 'h';
+ }
+
+ async function loadSummaryNextTick() {
+ const el = document.getElementById('sum-next-tick');
+ try {
+ const h = await api('GET', '/api/health');
+ el.textContent = nextTickLabel(h.next_tick_ts);
+ } catch (e) {
+ el.textContent = '—';
+ }
+ }
+
// --- KPI strip --------------------------------------------------------- //
async function loadKpi() {
const head = document.getElementById('kpi-key-head');
@@ -144,10 +244,24 @@
Open orders
}
// --- Positions --------------------------------------------------------- //
- function positionRowHtml(r) {
+ // The Strategy column is redundant when grouping by strategy (the group
+ // header already names it) — dropped from both the header and the row cells,
+ // with the rebuilt per grouping mode so colspans stay consistent.
+ function positionsTheadHtml(includeStrategy) {
+ return '
' +
+ (includeStrategy ? '
Strategy
' : '') +
+ '
Exchange
' +
+ '
Instrument
' +
+ '
Net qty
' +
+ '
Avg entry
' +
+ '
Realised PnL
' +
+ '
Fees
' +
+ '
';
+ }
+ function positionRowHtml(r, includeStrategy) {
const unit = tbFmt.splitInstrument(r.instrument); // {base, quote}
return '
';
+ document.getElementById('sum-open-orders').textContent = '—';
return;
}
+ // Feeds the summary strip's open-orders count — no separate fetch for it.
+ document.getElementById('sum-open-orders').textContent = rows.length;
const body = document.getElementById('orders-body');
if (!rows.length) {
body.innerHTML = '
No open orders.
';
@@ -228,31 +348,43 @@
Open orders
}
function refreshAll() {
+ loadSummaryStrategies();
+ loadSummaryPnl();
+ loadSummaryNextTick();
loadKpi();
loadPositions();
loadOrders();
}
+ // Reflect KPI_LEVEL / POS_GROUP (restored from localStorage, or the default)
+ // onto the toggle buttons' is-active state.
+ function syncToggleButtons() {
+ document.querySelectorAll('.kpi-level').forEach(function (b) {
+ b.classList.toggle('is-active', b.dataset.level === KPI_LEVEL);
+ });
+ document.querySelectorAll('.pos-group').forEach(function (b) {
+ b.classList.toggle('is-active', b.dataset.group === POS_GROUP);
+ });
+ }
+
document.addEventListener('DOMContentLoaded', function () {
+ syncToggleButtons(); // apply the restored (or default) preferences
+
// KPI level toggle.
document.querySelectorAll('.kpi-level').forEach(function (btn) {
btn.addEventListener('click', function () {
- document.querySelectorAll('.kpi-level').forEach(function (b) {
- b.classList.remove('is-active');
- });
- btn.classList.add('is-active');
KPI_LEVEL = btn.dataset.level;
+ savePref(KPI_LEVEL_KEY, KPI_LEVEL);
+ syncToggleButtons();
loadKpi();
});
});
// Positions group-by control.
document.querySelectorAll('.pos-group').forEach(function (btn) {
btn.addEventListener('click', function () {
- document.querySelectorAll('.pos-group').forEach(function (b) {
- b.classList.remove('is-active');
- });
- btn.classList.add('is-active');
POS_GROUP = btn.dataset.group;
+ savePref(POS_GROUP_KEY, POS_GROUP);
+ syncToggleButtons();
loadPositions();
});
});
diff --git a/trading_bot/tests/interfaces/test_dashboard.py b/trading_bot/tests/interfaces/test_dashboard.py
index 8cb35536..f5fbc096 100644
--- a/trading_bot/tests/interfaces/test_dashboard.py
+++ b/trading_bot/tests/interfaces/test_dashboard.py
@@ -865,7 +865,20 @@ def test_overview_page_has_kpi_strip_and_tables() -> None:
assert 'id="positions-table"' in html
assert "pos-group" in html # the group-by control
assert 'data-group="crypto"' in html and 'data-group="exchange"' in html
+ # The "By strategy" grouping is present and the default (ui-ux leaf 03).
+ assert 'data-group="strategy"' in html
+ assert 'class="btn pos-group is-active" data-group="strategy"' in html
assert 'id="orders-table"' in html
+ # The summary strip (running/total strategies, open orders, total PnL, next
+ # tick) sits above the KPI card.
+ assert 'id="summary-strip"' in html
+ assert 'id="sum-running"' in html and 'id="sum-total"' in html
+ assert 'id="sum-open-orders"' in html
+ assert 'id="sum-pnl"' in html
+ assert 'id="sum-next-tick"' in html
+ # View preferences persist across reloads.
+ assert "tb.overview.posGroup" in html
+ assert "tb.overview.kpiLevel" in html
# It wires the merged SSE stream + polling fallback.
assert "/api/events" in html
assert "/api/positions" in html