@@ -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">▲</span></th>' ;
5974+ var arrow = '▲' ;
5975+ var arrowStyle = 'opacity:0.3' ;
5976+ if ( ps . sortCol === h . key ) {
5977+ arrow = ps . sortAsc ? '▲' : '▼' ;
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' : '' ) + '>«</button>' ;
6004+ html += '<button onclick="onPagNav(\'' + tableKey + '\',' + ( ps . page - 1 ) + ')"' + ( ps . page <= 1 ? ' disabled' : '' ) + '>‹ 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 ›</button>' ;
6016+ html += '<button onclick="onPagNav(\'' + tableKey + '\',' + totalPages + ')"' + ( ps . page >= totalPages ? ' disabled' : '' ) + '>»</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+
59246084function 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
59536097function 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
59836110function 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 = '▲' ;
6034- if ( arrow ) arrow . style . opacity = '0.3' ;
6035- } ) ;
6036- var arrow = th . querySelector ( '.sort-arrow' ) ;
6037- if ( arrow ) {
6038- arrow . innerHTML = asc === 'asc' ? '▲' : '▼' ;
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