Skip to content

Commit 704f356

Browse files
committed
fix: ATH tracker uses spot 24h high/low, market dominance uses CoinGecko global, lower divergence threshold
1 parent f9afd9c commit 704f356

3 files changed

Lines changed: 93 additions & 75 deletions

File tree

frontend/js/panels/ath-tracker.js

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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
22
import { BasePanel } from '../components/base-panel.js';
33

44
const { 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 (&lt;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 (&gt;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 => {

frontend/js/panels/market-dominance.js

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,35 @@ export class MarketDominancePanel extends BasePanel {
1515
}
1616

1717
async fetchData() {
18-
const res = await window.mefaiApi.products.symbols();
19-
if (!res || res?.error) return { _fetchError: true };
20-
return res;
18+
const [symbolsRes, globalRes] = await Promise.all([
19+
window.mefaiApi.products.symbols(),
20+
window.mefaiApi.coingecko.global(),
21+
]);
22+
if (!symbolsRes || symbolsRes?.error) return { _fetchError: true };
23+
return { symbols: symbolsRes, global: globalRes };
2124
}
2225

2326
renderContent(data) {
2427
if (data?._fetchError) return '<div class="panel-loading">Unable to load market data</div>';
2528

26-
const list = data?.data || data;
29+
const list = data?.symbols?.data || data?.data || data;
2730
if (!Array.isArray(list) || !list.length) return '<div class="panel-loading">No market data</div>';
2831

32+
// Get CoinGecko dominance data
33+
const cgGlobal = data?.global?.data || {};
34+
const cgDominance = cgGlobal.market_cap_percentage || {};
35+
const cgTotalMcap = cgGlobal.total_market_cap?.usd || 0;
36+
2937
// Parse coins with market cap
3038
let coins = [];
3139
let totalMcap = 0;
3240
for (const item of list) {
33-
const name = item.name || item.symbol || '';
34-
const symbol = item.symbol || '';
35-
const mcap = parseFloat(item.marketCap || item.circulatingMarketCap || 0);
36-
const price = parseFloat(item.price || item.lastPrice || 0);
37-
const change24h = parseFloat(item.priceChangePercent24h || item.priceChange || 0);
38-
const dominance = parseFloat(item.dominance || 0);
41+
const name = item.name || '';
42+
const symbol = item.symbol || item.name || '';
43+
const mcap = parseFloat(item.marketCap || 0);
44+
const price = parseFloat(item.price || 0);
45+
const change24h = parseFloat(item.dayChange || 0);
46+
const dominance = cgDominance[name.toLowerCase()] || 0;
3947
if (mcap > 0) {
4048
coins.push({ name, symbol, mcap, price, change24h, dominance });
4149
totalMcap += mcap;
@@ -58,18 +66,30 @@ export class MarketDominancePanel extends BasePanel {
5866
h += '.md-total{font-size:10px;color:var(--text-muted);text-align:right;padding:0 0 6px}';
5967
h += '</style>';
6068

61-
h += `<div class="md-total">Total Market Cap: ${formatCurrency(totalMcap)}</div>`;
69+
h += `<div class="md-total">Total Market Cap: ${formatCurrency(cgTotalMcap || totalMcap)}</div>`;
6270

63-
// Dominance bar
64-
const colors = ['#f0b90b', '#627eea', '#f3ba2f', '#26a17b', '#e84142'];
71+
// Dominance bar — use CoinGecko global percentages
72+
const domColors = { btc: '#f0b90b', eth: '#627eea', usdt: '#26a17b', bnb: '#f3ba2f', xrp: '#23292f', sol: '#9945ff' };
73+
const domEntries = Object.entries(cgDominance).filter(([, v]) => v > 1).slice(0, 6);
6574
h += '<div class="md-bar">';
66-
for (let i = 0; i < top5.length; i++) {
67-
const pct = totalMcap > 0 ? (top5[i].mcap / totalMcap * 100) : 0;
68-
if (pct < 1) continue;
69-
h += `<div class="md-seg" style="width:${pct}%;background:${colors[i % colors.length]}" title="${top5[i].symbol}: ${pct.toFixed(1)}%">${top5[i].symbol} ${pct.toFixed(1)}%</div>`;
75+
if (domEntries.length) {
76+
let used = 0;
77+
for (const [key, pct] of domEntries) {
78+
const color = domColors[key] || '#555';
79+
h += `<div class="md-seg" style="width:${pct}%;background:${color}" title="${key.toUpperCase()}: ${pct.toFixed(1)}%">${key.toUpperCase()} ${pct.toFixed(1)}%</div>`;
80+
used += pct;
81+
}
82+
const otherPct = 100 - used;
83+
if (otherPct > 1) h += `<div class="md-seg" style="width:${otherPct}%;background:#555">Other ${otherPct.toFixed(1)}%</div>`;
84+
} else {
85+
// Fallback: compute from Binance market caps
86+
for (let i = 0; i < top5.length; i++) {
87+
const pct = totalMcap > 0 ? (top5[i].mcap / totalMcap * 100) : 0;
88+
if (pct < 1) continue;
89+
const fallbackColors = ['#f0b90b', '#627eea', '#f3ba2f', '#26a17b', '#e84142'];
90+
h += `<div class="md-seg" style="width:${pct}%;background:${fallbackColors[i % 5]}" title="${top5[i].name}: ${pct.toFixed(1)}%">${top5[i].name} ${pct.toFixed(1)}%</div>`;
91+
}
7092
}
71-
const otherPct = 100 - top5.reduce((s, c) => s + (totalMcap > 0 ? c.mcap / totalMcap * 100 : 0), 0);
72-
if (otherPct > 1) h += `<div class="md-seg" style="width:${otherPct}%;background:#555">Other ${otherPct.toFixed(1)}%</div>`;
7393
h += '</div>';
7494

7595
// Table

frontend/js/panels/trader-divergence.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export class TraderDivergencePanel extends BasePanel {
7373

7474
// Summary
7575
const maxDiv = rows[0];
76-
const bullish = rows.filter(r => r.divergence > 5).length;
77-
const bearish = rows.filter(r => r.divergence < -5).length;
76+
const bullish = rows.filter(r => r.divergence > 2).length;
77+
const bearish = rows.filter(r => r.divergence < -2).length;
7878
h += '<div class="td-cards">';
7979
h += `<div class="td-card"><div class="td-card-label">Top Bias Bullish</div><div class="td-card-value val-up">${bullish} coins</div></div>`;
8080
h += `<div class="td-card"><div class="td-card-label">Top Bias Bearish</div><div class="td-card-value val-down">${bearish} coins</div></div>`;

0 commit comments

Comments
 (0)