1- // ATH Distance Tracker — How far each coin is from its all-time high/low
1+ // ATH Tracker — Distance from 24h high/low + market cap recovery analysis
22import { BasePanel } from '../components/base-panel.js' ;
33
44const { formatPrice, formatCurrency, formatPercent, escapeHtml } = window . mefaiUtils ;
@@ -9,39 +9,53 @@ export class AthTrackerPanel extends BasePanel {
99
1010 constructor ( ) {
1111 super ( ) ;
12- this . _refreshRate = 120000 ;
13- this . _sortKey = 'athDist ' ;
12+ this . _refreshRate = 30000 ;
13+ this . _sortKey = 'highDist ' ;
1414 this . _sortDir = 'asc' ;
1515 this . _search = '' ;
16- this . _view = 'ath' ; // ath | atl
1716 }
1817
1918 async fetchData ( ) {
20- const res = await window . mefaiApi . products . symbols ( ) ;
21- if ( ! res || res ?. error ) return { _fetchError : true } ;
22- return res ;
19+ // Use spot tickers (24h high/low) + products symbols (market cap, issue price)
20+ const [ tickers , symbols ] = await Promise . all ( [
21+ window . mefaiApi . spot . tickers ( ) ,
22+ window . mefaiApi . products . symbols ( ) ,
23+ ] ) ;
24+ if ( ! tickers || tickers ?. error ) return { _fetchError : true } ;
25+ return { tickers, symbols } ;
2326 }
2427
2528 renderContent ( data ) {
26- if ( data ?. _fetchError ) return '<div class="panel-loading">Unable to load ATH data</div>' ;
29+ if ( data ?. _fetchError ) return '<div class="panel-loading">Unable to load tracker data</div>' ;
2730
28- const list = data ?. data || data ;
29- if ( ! Array . isArray ( list ) || ! list . length ) return '<div class="panel-loading">No ATH data available</div>' ;
31+ const tickerArr = Array . isArray ( data . tickers ) ? data . tickers : [ ] ;
32+ const symList = data . symbols ?. data || [ ] ;
33+
34+ // Build issue price map
35+ const issueMap = { } ;
36+ for ( const s of symList ) {
37+ if ( s . issuePrice && s . name ) issueMap [ s . name ] = parseFloat ( s . issuePrice ) ;
38+ }
3039
3140 let rows = [ ] ;
32- for ( const item of list ) {
33- const symbol = item . symbol || item . name || '' ;
34- if ( ! symbol ) continue ;
35- const price = parseFloat ( item . price || item . lastPrice || 0 ) ;
36- const ath = parseFloat ( item . allTimeHigh || item . athPrice || 0 ) ;
37- const atl = parseFloat ( item . allTimeLow || item . atlPrice || 0 ) ;
38- const mcap = parseFloat ( item . marketCap || item . circulatingMarketCap || 0 ) ;
39- if ( ! price || ! ath ) continue ;
40-
41- const athDist = ( ( price - ath ) / ath ) * 100 ; // negative = below ATH
42- const atlDist = atl > 0 ? ( ( price - atl ) / atl ) * 100 : 0 ; // positive = above ATL
43-
44- rows . push ( { symbol, price, ath, atl, athDist, atlDist, mcap } ) ;
41+ for ( const t of tickerArr ) {
42+ const sym = t . symbol || '' ;
43+ if ( ! sym . endsWith ( 'USDT' ) ) continue ;
44+ const short = sym . replace ( 'USDT' , '' ) ;
45+ const price = parseFloat ( t . lastPrice || 0 ) ;
46+ const high = parseFloat ( t . highPrice || 0 ) ;
47+ const low = parseFloat ( t . lowPrice || 0 ) ;
48+ const volume = parseFloat ( t . quoteVolume || 0 ) ;
49+ const change = parseFloat ( t . priceChangePercent || 0 ) ;
50+ if ( ! price || ! high ) continue ;
51+
52+ const highDist = ( ( price - high ) / high ) * 100 ;
53+ const lowDist = low > 0 ? ( ( price - low ) / low ) * 100 : 0 ;
54+ const range = high > 0 && low > 0 ? ( ( high - low ) / low ) * 100 : 0 ;
55+ const issuePrice = issueMap [ short ] || 0 ;
56+ const issueGain = issuePrice > 0 ? ( ( price - issuePrice ) / issuePrice ) * 100 : null ;
57+
58+ rows . push ( { symbol : short , price, high, low, highDist, lowDist, range, change, volume, issueGain } ) ;
4559 }
4660
4761 // Search
@@ -51,17 +65,16 @@ export class AthTrackerPanel extends BasePanel {
5165 }
5266
5367 // Sort
54- const sortKey = this . _view === 'atl' ? 'atlDist' : this . _sortKey ;
5568 const dir = this . _sortDir === 'asc' ? 1 : - 1 ;
5669 rows . sort ( ( a , b ) => {
57- if ( sortKey === 'symbol' ) return a . symbol . localeCompare ( b . symbol ) * dir ;
58- return ( ( a [ sortKey ] || 0 ) - ( b [ sortKey ] || 0 ) ) * dir ;
70+ if ( this . _sortKey === 'symbol' ) return a . symbol . localeCompare ( b . symbol ) * dir ;
71+ return ( ( a [ this . _sortKey ] || 0 ) - ( b [ this . _sortKey ] || 0 ) ) * dir ;
5972 } ) ;
6073
6174 // Stats
62- const nearATH = rows . filter ( r => r . athDist > - 10 ) . length ;
63- const crashed = rows . filter ( r => r . athDist < - 90 ) . length ;
64- const avgDist = rows . length ? rows . reduce ( ( s , r ) => s + r . athDist , 0 ) / rows . length : 0 ;
75+ const nearHigh = rows . filter ( r => r . highDist > - 1 ) . length ;
76+ const nearLow = rows . filter ( r => r . lowDist < 1 ) . length ;
77+ const avgRange = rows . length ? rows . reduce ( ( s , r ) => s + r . range , 0 ) / rows . length : 0 ;
6578
6679 let h = '<style scoped>' ;
6780 h += '.at-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;padding:0 0 8px}' ;
@@ -74,40 +87,29 @@ export class AthTrackerPanel extends BasePanel {
7487 h += '</style>' ;
7588
7689 h += '<div class="at-stats">' ;
77- h += `<div class="at-stat"><div class="at-stat-label">Near ATH (<10%) </div><div class="at-stat-value val-up">${ nearATH } </div></div>` ;
78- h += `<div class="at-stat"><div class="at-stat-label">Avg Distance </div><div class="at-stat-value val-down ">${ avgDist . toFixed ( 1 ) } %</div></div>` ;
79- h += `<div class="at-stat"><div class="at-stat-label">Crashed (>90%) </div><div class="at-stat-value val-down">${ crashed } </div></div>` ;
90+ h += `<div class="at-stat"><div class="at-stat-label">Near 24h High </div><div class="at-stat-value val-up">${ nearHigh } </div></div>` ;
91+ h += `<div class="at-stat"><div class="at-stat-label">Avg 24h Range </div><div class="at-stat-value">${ avgRange . toFixed ( 1 ) } %</div></div>` ;
92+ h += `<div class="at-stat"><div class="at-stat-label">Near 24h Low </div><div class="at-stat-value val-down">${ nearLow } </div></div>` ;
8093 h += '</div>' ;
8194
8295 h += '<div class="at-bar">' ;
8396 h += `<input type="text" class="at-search form-input" placeholder="Filter..." value="${ escapeHtml ( this . _search ) } " style="width:100px">` ;
84- h += '<select class="at-view form-select">' ;
85- h += `<option value="ath"${ this . _view === 'ath' ? ' selected' : '' } >From ATH</option>` ;
86- h += `<option value="atl"${ this . _view === 'atl' ? ' selected' : '' } >From ATL</option>` ;
87- h += '</select>' ;
8897 h += `<span style="font-size:10px;color:var(--text-muted)">${ rows . length } coins</span>` ;
8998 h += '</div>' ;
9099
91100 const top30 = rows . slice ( 0 , 30 ) ;
92101 const { renderTable } = window . mefaiTable ;
93-
94- const cols = this . _view === 'atl' ? [
95- { key : 'symbol' , label : 'Symbol' , width : '60px' } ,
102+ const cols = [
103+ { key : 'symbol' , label : 'Symbol' , width : '55px' } ,
96104 { key : 'price' , label : 'Price' , align : 'right' , render : v => '$' + formatPrice ( v ) } ,
97- { key : 'atl' , label : 'ATL' , align : 'right' , render : v => v > 0 ? '$' + formatPrice ( v ) : '—' } ,
98- { key : 'atlDist' , label : 'From ATL' , align : 'right' , render : v => {
99- return `<span class="val-up">+${ v . toFixed ( 1 ) } %</span>` ;
100- } } ,
101- ] : [
102- { key : 'symbol' , label : 'Symbol' , width : '60px' } ,
103- { key : 'price' , label : 'Price' , align : 'right' , render : v => '$' + formatPrice ( v ) } ,
104- { key : 'ath' , label : 'ATH' , align : 'right' , render : v => '$' + formatPrice ( v ) } ,
105- { key : 'athDist' , label : 'From ATH' , align : 'right' , render : v => {
105+ { key : 'highDist' , label : 'From High' , align : 'right' , render : v => {
106106 const pct = Math . abs ( v ) ;
107107 const fillPct = Math . min ( 100 , 100 - pct ) ;
108- const color = pct < 20 ? '#0ecb81' : pct < 50 ? '#f0b90b' : '#f6465d' ;
109- return `<div class="at-dist-bar"><div class="at-dist-fill" style="width:${ fillPct } %;background:${ color } "></div></div> <span class="val-down">${ v . toFixed ( 1 ) } %</span>` ;
108+ const color = pct < 1 ? '#0ecb81' : pct < 3 ? '#f0b90b' : '#f6465d' ;
109+ return `<div class="at-dist-bar"><div class="at-dist-fill" style="width:${ fillPct } %;background:${ color } "></div></div> <span class="${ v > - 1 ? ' val-up' : 'val- down' } ">${ v . toFixed ( 2 ) } %</span>` ;
110110 } } ,
111+ { key : 'range' , label : '24h Range' , align : 'right' , render : v => v . toFixed ( 1 ) + '%' } ,
112+ { key : 'change' , label : '24h%' , align : 'right' , render : v => formatPercent ( v ) } ,
111113 ] ;
112114 h += renderTable ( cols , top30 , { sortKey : this . _sortKey , sortDir : this . _sortDir } ) ;
113115 return h ;
@@ -120,10 +122,6 @@ export class AthTrackerPanel extends BasePanel {
120122 this . _search = e . target . value ;
121123 this . _renderBody ( ) ;
122124 } ) ;
123- body . querySelector ( '.at-view' ) ?. addEventListener ( 'change' , e => {
124- this . _view = e . target . value ;
125- this . _renderBody ( ) ;
126- } ) ;
127125 const { bindTableEvents } = window . mefaiTable ;
128126 bindTableEvents ( body , [ ] , [ ] , {
129127 onSort : key => {
0 commit comments