Skip to content

Commit c886226

Browse files
committed
feat: move trading data to separate tab with pagination
- Add Performance/Trading tab bar on individual strategy pages - Move trades, orders, positions tables and timeline scatter to Trading tab - Add pagination with page size selector (10/25/50/100), prev/next, page numbers - Sort state and page position preserved across re-renders - Scatter chart renders on-demand when Trading tab is opened (Chart.js sizing)
1 parent ebafedd commit c886226

3 files changed

Lines changed: 218 additions & 109 deletions

File tree

investing_algorithm_framework/app/reporting/templates/dashboard.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ body { font-family:'Inter',-apple-system,sans-serif; background:var(--bg); color
174174
.tab-panel { display:none; }
175175
.tab-panel.active { display:block; }
176176

177+
/* pagination */
178+
.pagination { display:flex; align-items:center; justify-content:space-between; padding:0.6rem 0.25rem; font-size:0.78rem; color:var(--text-dim); }
179+
.pagination-btns { display:flex; gap:0.35rem; align-items:center; }
180+
.pagination-btns button { font-family:inherit; font-size:0.75rem; font-weight:500; padding:0.3rem 0.7rem; border-radius:6px; border:1px solid var(--border); background:var(--surface2); color:var(--text-secondary); cursor:pointer; transition:all 0.15s; }
181+
.pagination-btns button:hover:not(:disabled) { border-color:var(--accent); color:var(--accent); }
182+
.pagination-btns button:disabled { opacity:0.35; cursor:default; }
183+
.pagination-btns button.active { background:var(--accent); color:#000; border-color:var(--accent); }
184+
.page-size-select { font-family:inherit; font-size:0.75rem; padding:0.25rem 0.5rem; border-radius:6px; border:1px solid var(--border); background:var(--surface2); color:var(--text-secondary); cursor:pointer; }
185+
177186
/* run selector pills */
178187
.run-selector { display:flex; gap:0.4rem; flex-wrap:wrap; margin-bottom:1.25rem; padding:0.25rem 0; }
179188
.run-pill { font-family:'Inter',sans-serif; font-size:0.72rem; font-weight:500; padding:0.3rem 0.75rem; border-radius:16px; border:1px solid var(--border); background:var(--surface2); color:var(--text-secondary); cursor:pointer; transition:all 0.15s; }

investing_algorithm_framework/app/reporting/templates/dashboard.js

Lines changed: 193 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -5886,23 +5886,101 @@ function getStratRunData(stratIdx) {
58865886
var strat = STRATEGIES[stratIdx];
58875887
var runId = stratSelectedRun[stratIdx] || 'summary';
58885888
if (runId === 'summary') {
5889-
// Use first run
58905889
var bestRid = strat.runIds[0];
58915890
return RUN_DATA[bestRid];
58925891
}
58935892
return RUN_DATA[runId];
58945893
}
58955894

5896-
function sortableTable(headers, rows, tableId) {
5897-
// headers: [{key, label, align?}]
5898-
// rows: [{key: value, ...}]
5899-
var html = '<div class="table-wrap"><table class="comp-table" id="' + tableId + '">';
5895+
// ===== STRATEGY TAB SWITCHING =====
5896+
function switchStratTab(sid, tabId) {
5897+
var page = document.getElementById('page-' + sid);
5898+
if (!page) return;
5899+
// Toggle panels
5900+
var panels = page.querySelectorAll('.tab-panel');
5901+
panels.forEach(function(p) { p.classList.remove('active'); p.style.display = 'none'; });
5902+
var target = document.getElementById(sid + '-' + tabId);
5903+
if (target) { target.classList.add('active'); target.style.display = 'block'; }
5904+
// Toggle tab highlights
5905+
var tabs = page.querySelectorAll('.strat-nav-tabs .tab');
5906+
tabs.forEach(function(t) { t.classList.remove('active'); });
5907+
var tabNames = ['performance', 'trading'];
5908+
var idx = tabNames.indexOf(tabId);
5909+
if (idx >= 0 && tabs[idx]) tabs[idx].classList.add('active');
5910+
// Re-draw charts if switching to trading tab (scatter needs canvas)
5911+
if (tabId === 'trading') {
5912+
var stratIdx = parseInt(sid.replace('strat-', ''));
5913+
buildTimelineScatter(stratIdx);
5914+
}
5915+
if (tabId === 'performance') {
5916+
requestAnimationFrame(function() { drawPageCharts(sid); });
5917+
}
5918+
}
5919+
5920+
// ===== PAGINATION STATE =====
5921+
var paginationState = {};
5922+
var PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
5923+
var DEFAULT_PAGE_SIZE = 25;
5924+
5925+
function getPagState(tableKey) {
5926+
if (!paginationState[tableKey]) {
5927+
paginationState[tableKey] = { page: 1, pageSize: DEFAULT_PAGE_SIZE, sortCol: null, sortAsc: true };
5928+
}
5929+
return paginationState[tableKey];
5930+
}
5931+
5932+
function paginatedTable(headers, allRows, tableKey, containerEl, title) {
5933+
var ps = getPagState(tableKey);
5934+
5935+
// Sort rows
5936+
var rows = allRows.slice();
5937+
if (ps.sortCol !== null) {
5938+
var hdr = headers.find(function(h) { return h.key === ps.sortCol; });
5939+
rows.sort(function(a, b) {
5940+
var va = a[ps.sortCol], vb = b[ps.sortCol];
5941+
if (va == null) va = '';
5942+
if (vb == null) vb = '';
5943+
var na = parseFloat(va), nb = parseFloat(vb);
5944+
if (!isNaN(na) && !isNaN(nb)) {
5945+
return ps.sortAsc ? na - nb : nb - na;
5946+
}
5947+
return ps.sortAsc ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
5948+
});
5949+
}
5950+
5951+
var totalRows = rows.length;
5952+
var totalPages = Math.max(1, Math.ceil(totalRows / ps.pageSize));
5953+
if (ps.page > totalPages) ps.page = totalPages;
5954+
var start = (ps.page - 1) * ps.pageSize;
5955+
var pageRows = rows.slice(start, start + ps.pageSize);
5956+
5957+
var html = '<div class="chart-card">';
5958+
html += '<div class="chart-title">' + title + ' (' + totalRows + ')</div>';
5959+
5960+
// Page size selector + info row
5961+
html += '<div class="pagination">';
5962+
html += '<div><span>Show </span><select class="page-size-select" onchange="onPageSizeChange(\'' + tableKey + '\',this.value)">';
5963+
PAGE_SIZE_OPTIONS.forEach(function(sz) {
5964+
html += '<option value="' + sz + '"' + (sz === ps.pageSize ? ' selected' : '') + '>' + sz + '</option>';
5965+
});
5966+
html += '</select><span> per page</span></div>';
5967+
html += '<div>' + (start + 1) + '\u2013' + Math.min(start + ps.pageSize, totalRows) + ' of ' + totalRows + '</div>';
5968+
html += '</div>';
5969+
5970+
// Table
5971+
html += '<div class="table-wrap"><table class="comp-table" id="' + tableKey + '">';
59005972
html += '<thead><tr>';
59015973
headers.forEach(function(h) {
5902-
html += '<th data-col="' + h.key + '" style="cursor:pointer;' + (h.align === 'right' ? 'text-align:right' : '') + '">' + h.label + ' <span class="sort-arrow">&#9650;</span></th>';
5974+
var arrow = '&#9650;';
5975+
var arrowStyle = 'opacity:0.3';
5976+
if (ps.sortCol === h.key) {
5977+
arrow = ps.sortAsc ? '&#9650;' : '&#9660;';
5978+
arrowStyle = 'opacity:1';
5979+
}
5980+
html += '<th data-col="' + h.key + '" style="cursor:pointer;' + (h.align === 'right' ? 'text-align:right' : '') + '" onclick="onPagSort(\'' + tableKey + '\',\'' + h.key + '\')">' + h.label + ' <span class="sort-arrow" style="' + arrowStyle + '">' + arrow + '</span></th>';
59035981
});
59045982
html += '</tr></thead><tbody>';
5905-
rows.forEach(function(r) {
5983+
pageRows.forEach(function(r) {
59065984
html += '<tr>';
59075985
headers.forEach(function(h) {
59085986
var v = r[h.key];
@@ -5918,128 +5996,128 @@ function sortableTable(headers, rows, tableId) {
59185996
html += '</tr>';
59195997
});
59205998
html += '</tbody></table></div>';
5921-
return html;
5999+
6000+
// Pagination buttons
6001+
if (totalPages > 1) {
6002+
html += '<div class="pagination"><div></div><div class="pagination-btns">';
6003+
html += '<button onclick="onPagNav(\'' + tableKey + '\',' + 1 + ')"' + (ps.page <= 1 ? ' disabled' : '') + '>&laquo;</button>';
6004+
html += '<button onclick="onPagNav(\'' + tableKey + '\',' + (ps.page - 1) + ')"' + (ps.page <= 1 ? ' disabled' : '') + '>&lsaquo; Prev</button>';
6005+
6006+
// Page number buttons (show up to 5 around current)
6007+
var lo = Math.max(1, ps.page - 2);
6008+
var hi = Math.min(totalPages, ps.page + 2);
6009+
if (lo > 1) html += '<button disabled>...</button>';
6010+
for (var p = lo; p <= hi; p++) {
6011+
html += '<button onclick="onPagNav(\'' + tableKey + '\',' + p + ')"' + (p === ps.page ? ' class="active"' : '') + '>' + p + '</button>';
6012+
}
6013+
if (hi < totalPages) html += '<button disabled>...</button>';
6014+
6015+
html += '<button onclick="onPagNav(\'' + tableKey + '\',' + (ps.page + 1) + ')"' + (ps.page >= totalPages ? ' disabled' : '') + '>Next &rsaquo;</button>';
6016+
html += '<button onclick="onPagNav(\'' + tableKey + '\',' + totalPages + ')"' + (ps.page >= totalPages ? ' disabled' : '') + '>&raquo;</button>';
6017+
html += '</div></div>';
6018+
}
6019+
6020+
html += '</div>';
6021+
containerEl.innerHTML = html;
59226022
}
59236023

6024+
// Global re-render registry: maps tableKey to a re-render function
6025+
var pagTableRenderers = {};
6026+
6027+
function onPagNav(tableKey, page) {
6028+
var ps = getPagState(tableKey);
6029+
ps.page = page;
6030+
if (pagTableRenderers[tableKey]) pagTableRenderers[tableKey]();
6031+
}
6032+
6033+
function onPagSort(tableKey, col) {
6034+
var ps = getPagState(tableKey);
6035+
if (ps.sortCol === col) {
6036+
ps.sortAsc = !ps.sortAsc;
6037+
} else {
6038+
ps.sortCol = col;
6039+
ps.sortAsc = true;
6040+
}
6041+
ps.page = 1;
6042+
if (pagTableRenderers[tableKey]) pagTableRenderers[tableKey]();
6043+
}
6044+
6045+
function onPageSizeChange(tableKey, val) {
6046+
var ps = getPagState(tableKey);
6047+
ps.pageSize = parseInt(val);
6048+
ps.page = 1;
6049+
if (pagTableRenderers[tableKey]) pagTableRenderers[tableKey]();
6050+
}
6051+
6052+
// ===== TRADES/ORDERS/POSITIONS TABLE BUILDERS =====
6053+
var tradesHeaders = [
6054+
{key: 'id', label: '#'},
6055+
{key: 'sym', label: 'Symbol'},
6056+
{key: 'opened', label: 'Opened'},
6057+
{key: 'closed', label: 'Closed'},
6058+
{key: 'open_price', label: 'Open Price', align: 'right'},
6059+
{key: 'close_price', label: 'Close Price', align: 'right'},
6060+
{key: 'cost', label: 'Cost', align: 'right'},
6061+
{key: 'net_gain', label: 'Net Gain', align: 'right', colorFn: function(r) { return r.net_gain >= 0 ? 'var(--green)' : 'var(--red)'; }},
6062+
{key: 'pct', label: 'Return %', align: 'right', colorFn: function(r) { return r.pct >= 0 ? 'var(--green)' : 'var(--red)'; }},
6063+
];
6064+
6065+
var ordersHeaders = [
6066+
{key: 'sym', label: 'Symbol'},
6067+
{key: 'side', label: 'Side', colorFn: function(r) { return r.side === 'BUY' ? 'var(--green)' : 'var(--red)'; }},
6068+
{key: 'type', label: 'Type'},
6069+
{key: 'status', label: 'Status'},
6070+
{key: 'price', label: 'Price', align: 'right'},
6071+
{key: 'amount', label: 'Amount', align: 'right'},
6072+
{key: 'filled', label: 'Filled', align: 'right'},
6073+
{key: 'cost', label: 'Cost', align: 'right'},
6074+
{key: 'created', label: 'Created'},
6075+
{key: 'updated', label: 'Updated'},
6076+
];
6077+
6078+
var positionsHeaders = [
6079+
{key: 'sym', label: 'Symbol'},
6080+
{key: 'amount', label: 'Amount', align: 'right'},
6081+
{key: 'cost', label: 'Cost', align: 'right'},
6082+
];
6083+
59246084
function buildTradesTable(stratIdx) {
59256085
var sid = 'strat-' + stratIdx;
59266086
var el = document.getElementById(sid + '-trades-table');
59276087
if (!el) return;
59286088
var rd = getStratRunData(stratIdx);
5929-
if (!rd || !rd.TRADES || rd.TRADES.length === 0) {
5930-
el.innerHTML = '';
5931-
return;
5932-
}
5933-
var trades = rd.TRADES;
5934-
var headers = [
5935-
{key: 'id', label: '#'},
5936-
{key: 'sym', label: 'Symbol'},
5937-
{key: 'opened', label: 'Opened'},
5938-
{key: 'closed', label: 'Closed'},
5939-
{key: 'open_price', label: 'Open Price', align: 'right'},
5940-
{key: 'close_price', label: 'Close Price', align: 'right'},
5941-
{key: 'cost', label: 'Cost', align: 'right'},
5942-
{key: 'net_gain', label: 'Net Gain', align: 'right', colorFn: function(r) { return r.net_gain >= 0 ? 'var(--green)' : 'var(--red)'; }},
5943-
{key: 'pct', label: 'Return %', align: 'right', colorFn: function(r) { return r.pct >= 0 ? 'var(--green)' : 'var(--red)'; }},
5944-
];
5945-
var html = '<div class="chart-card">';
5946-
html += '<div class="chart-title">Trades (' + trades.length + ')</div>';
5947-
html += sortableTable(headers, trades, sid + '-trades-tbl');
5948-
html += '</div>';
5949-
el.innerHTML = html;
5950-
makeTableSortable(sid + '-trades-tbl');
6089+
if (!rd || !rd.TRADES || rd.TRADES.length === 0) { el.innerHTML = ''; return; }
6090+
var tableKey = sid + '-trades-tbl';
6091+
pagTableRenderers[tableKey] = function() {
6092+
paginatedTable(tradesHeaders, rd.TRADES, tableKey, el, 'Trades');
6093+
};
6094+
pagTableRenderers[tableKey]();
59516095
}
59526096

59536097
function buildOrdersTable(stratIdx) {
59546098
var sid = 'strat-' + stratIdx;
59556099
var el = document.getElementById(sid + '-orders-table');
59566100
if (!el) return;
59576101
var rd = getStratRunData(stratIdx);
5958-
if (!rd || !rd.ORDERS || rd.ORDERS.length === 0) {
5959-
el.innerHTML = '';
5960-
return;
5961-
}
5962-
var orders = rd.ORDERS;
5963-
var headers = [
5964-
{key: 'sym', label: 'Symbol'},
5965-
{key: 'side', label: 'Side', colorFn: function(r) { return r.side === 'BUY' ? 'var(--green)' : 'var(--red)'; }},
5966-
{key: 'type', label: 'Type'},
5967-
{key: 'status', label: 'Status'},
5968-
{key: 'price', label: 'Price', align: 'right'},
5969-
{key: 'amount', label: 'Amount', align: 'right'},
5970-
{key: 'filled', label: 'Filled', align: 'right'},
5971-
{key: 'cost', label: 'Cost', align: 'right'},
5972-
{key: 'created', label: 'Created'},
5973-
{key: 'updated', label: 'Updated'},
5974-
];
5975-
var html = '<div class="chart-card">';
5976-
html += '<div class="chart-title">Orders (' + orders.length + ')</div>';
5977-
html += sortableTable(headers, orders, sid + '-orders-tbl');
5978-
html += '</div>';
5979-
el.innerHTML = html;
5980-
makeTableSortable(sid + '-orders-tbl');
6102+
if (!rd || !rd.ORDERS || rd.ORDERS.length === 0) { el.innerHTML = ''; return; }
6103+
var tableKey = sid + '-orders-tbl';
6104+
pagTableRenderers[tableKey] = function() {
6105+
paginatedTable(ordersHeaders, rd.ORDERS, tableKey, el, 'Orders');
6106+
};
6107+
pagTableRenderers[tableKey]();
59816108
}
59826109

59836110
function buildPositionsTable(stratIdx) {
59846111
var sid = 'strat-' + stratIdx;
59856112
var el = document.getElementById(sid + '-positions-table');
59866113
if (!el) return;
59876114
var rd = getStratRunData(stratIdx);
5988-
if (!rd || !rd.POSITIONS || rd.POSITIONS.length === 0) {
5989-
el.innerHTML = '';
5990-
return;
5991-
}
5992-
var positions = rd.POSITIONS;
5993-
var headers = [
5994-
{key: 'sym', label: 'Symbol'},
5995-
{key: 'amount', label: 'Amount', align: 'right'},
5996-
{key: 'cost', label: 'Cost', align: 'right'},
5997-
];
5998-
var html = '<div class="chart-card">';
5999-
html += '<div class="chart-title">Positions (' + positions.length + ')</div>';
6000-
html += sortableTable(headers, positions, sid + '-positions-tbl');
6001-
html += '</div>';
6002-
el.innerHTML = html;
6003-
makeTableSortable(sid + '-positions-tbl');
6004-
}
6005-
6006-
function makeTableSortable(tableId) {
6007-
var table = document.getElementById(tableId);
6008-
if (!table) return;
6009-
var ths = table.querySelectorAll('th[data-col]');
6010-
var sortState = {};
6011-
ths.forEach(function(th) {
6012-
th.addEventListener('click', function() {
6013-
var col = th.getAttribute('data-col');
6014-
var asc = sortState[col] === 'asc' ? 'desc' : 'asc';
6015-
sortState = {};
6016-
sortState[col] = asc;
6017-
var tbody = table.querySelector('tbody');
6018-
var rows = Array.from(tbody.querySelectorAll('tr'));
6019-
var colIdx = Array.from(th.parentNode.children).indexOf(th);
6020-
rows.sort(function(a, b) {
6021-
var va = a.children[colIdx].textContent.trim();
6022-
var vb = b.children[colIdx].textContent.trim();
6023-
var na = parseFloat(va), nb = parseFloat(vb);
6024-
if (!isNaN(na) && !isNaN(nb)) {
6025-
return asc === 'asc' ? na - nb : nb - na;
6026-
}
6027-
return asc === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
6028-
});
6029-
rows.forEach(function(r) { tbody.appendChild(r); });
6030-
// Update arrows
6031-
ths.forEach(function(h) {
6032-
var arrow = h.querySelector('.sort-arrow');
6033-
if (arrow) arrow.innerHTML = '&#9650;';
6034-
if (arrow) arrow.style.opacity = '0.3';
6035-
});
6036-
var arrow = th.querySelector('.sort-arrow');
6037-
if (arrow) {
6038-
arrow.innerHTML = asc === 'asc' ? '&#9650;' : '&#9660;';
6039-
arrow.style.opacity = '1';
6040-
}
6041-
});
6042-
});
6115+
if (!rd || !rd.POSITIONS || rd.POSITIONS.length === 0) { el.innerHTML = ''; return; }
6116+
var tableKey = sid + '-positions-tbl';
6117+
pagTableRenderers[tableKey] = function() {
6118+
paginatedTable(positionsHeaders, rd.POSITIONS, tableKey, el, 'Positions');
6119+
};
6120+
pagTableRenderers[tableKey]();
60436121
}
60446122

60456123
// Scatter chart for trade/order timeline
@@ -6196,7 +6274,13 @@ function updateStratTables(stratIdx) {
61966274
buildTradesTable(stratIdx);
61976275
buildOrdersTable(stratIdx);
61986276
buildPositionsTable(stratIdx);
6199-
buildTimelineScatter(stratIdx);
6277+
// Scatter chart is built on-demand when Trading tab is shown
6278+
// (Chart.js needs visible canvas for proper sizing)
6279+
var sid = 'strat-' + stratIdx;
6280+
var tradingPanel = document.getElementById(sid + '-trading');
6281+
if (tradingPanel && tradingPanel.classList.contains('active')) {
6282+
buildTimelineScatter(stratIdx);
6283+
}
62006284
}
62016285

62026286
// ===== INIT =====

0 commit comments

Comments
 (0)