Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
/**
* Hybrid Trading System (HTS) Page
* Complete implementation with real-time data, WebSocket, and full functionality
*/
import HTSEngine from './hts-engine.js';
import { TradingIcons } from './icons.js';
import { escapeHtml, safeFormatNumber, safeFormatCurrency } from '../../shared/js/utils/sanitizer.js';
class HTSPage {
constructor() {
this.engine = new HTSEngine();
this.symbol = 'BTCUSDT';
this.timeframe = '1h';
this.chart = null;
this.candlestickSeries = null;
this.rsiSeries = null;
this.macdSeries = null;
this.volumeSeries = null;
this.ohlcvData = [];
this.analysisResult = null;
this.autoAnalysisInterval = null;
this.dataUpdateInterval = null;
}
async init() {
try {
console.log('[HTS] Initializing Hybrid Trading System...');
this.bindEvents();
await this.initChart();
await this.loadInitialData();
await this.runAnalysis();
this.startDataUpdates();
this.startAutoAnalysis();
console.log('[HTS] Ready');
} catch (error) {
console.error('[HTS] Init error:', error);
this.showError('Failed to initialize HTS. Please refresh the page.');
}
}
/**
* Bind event listeners
*/
bindEvents() {
// Tab switching
document.querySelectorAll('.trading-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const view = e.currentTarget.dataset.view;
this.switchView(view);
});
});
// Symbol change
document.getElementById('hts-symbol')?.addEventListener('change', (e) => {
this.symbol = e.target.value;
this.loadInitialData();
});
// Timeframe change
document.getElementById('hts-timeframe')?.addEventListener('change', (e) => {
this.timeframe = e.target.value;
this.loadInitialData();
});
// Auto-analysis toggle
document.getElementById('hts-auto-trade')?.addEventListener('change', (e) => {
if (e.target.checked) {
this.startAutoAnalysis();
} else {
this.stopAutoAnalysis();
}
});
// Manual analyze button
document.getElementById('hts-analyze-btn')?.addEventListener('click', () => {
this.runAnalysis();
});
// Indicator toggles
document.getElementById('show-rsi')?.addEventListener('change', () => this.updateChart());
document.getElementById('show-macd')?.addEventListener('change', () => this.updateChart());
document.getElementById('show-volume')?.addEventListener('change', () => this.updateChart());
}
/**
* Switch between standard and HTS views
*/
switchView(view) {
document.querySelectorAll('.trading-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelector(`[data-view="${view}"]`)?.classList.add('active');
const standardView = document.getElementById('standard-trading-view');
const htsView = document.getElementById('hts-trading-view');
if (view === 'hts') {
standardView.style.display = 'none';
htsView.style.display = 'block';
if (!this.chart) {
this.init();
}
} else {
standardView.style.display = 'block';
htsView.style.display = 'none';
}
}
/**
* Initialize TradingView Lightweight Chart
*/
async initChart() {
const container = document.getElementById('hts-chart-container');
if (!container) {
console.warn('[HTS] Chart container not found');
return;
}
// Wait for LightweightCharts library to load (max 5 seconds)
let retries = 0;
const maxRetries = 10;
while (typeof LightweightCharts === 'undefined' && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 500));
retries++;
}
if (typeof LightweightCharts === 'undefined') {
console.error('[HTS] TradingView Lightweight Charts library not loaded after timeout');
this.showError('Charting library not available. Please refresh the page.');
return;
}
try {
this.chart = LightweightCharts.createChart(container, {
width: container.clientWidth,
height: 500,
layout: {
background: { color: '#1a1a1a' },
textColor: '#d1d5db',
},
grid: {
vertLines: { color: '#2a2a2a' },
horzLines: { color: '#2a2a2a' },
},
timeScale: {
timeVisible: true,
secondsVisible: false,
},
});
if (!this.chart) {
throw new Error('Failed to create chart instance');
}
// Try multiple methods to create candlestick series (compatibility with different library versions)
const seriesOptions = {
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: false,
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
};
// Method 1: Try addCandlestickSeries (older API)
if (typeof this.chart.addCandlestickSeries === 'function') {
this.candlestickSeries = this.chart.addCandlestickSeries(seriesOptions);
}
// Method 2: Try addSeries with CandlestickSeries type (newer API)
else if (typeof this.chart.addSeries === 'function' && LightweightCharts.SeriesType && LightweightCharts.SeriesType.Candlestick) {
this.candlestickSeries = this.chart.addSeries(LightweightCharts.SeriesType.Candlestick, seriesOptions);
}
// Method 3: Try addSeries with string type
else if (typeof this.chart.addSeries === 'function') {
try {
this.candlestickSeries = this.chart.addSeries('Candlestick', seriesOptions);
} catch (e) {
console.warn('[HTS] Failed to create series with string type:', e);
}
}
if (!this.candlestickSeries) {
console.error('[HTS] Available chart methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(this.chart)));
throw new Error('Failed to create candlestick series - no compatible method found');
}
if (typeof this.chart.addHistogramSeries === 'function') {
this.volumeSeries = this.chart.addHistogramSeries({
color: '#26a69a',
priceFormat: {
type: 'volume',
},
priceScaleId: 'volume',
scaleMargins: {
top: 0.8,
bottom: 0,
},
});
}
if (typeof this.chart.addLineSeries === 'function') {
this.rsiSeries = this.chart.addLineSeries({
color: '#ff9800',
lineWidth: 2,
priceScaleId: 'rsi',
scaleMargins: {
top: 0.7,
bottom: 0,
},
});
this.macdSeries = this.chart.addLineSeries({
color: '#2196f3',
lineWidth: 2,
priceScaleId: 'macd',
scaleMargins: {
top: 0.5,
bottom: 0.3,
},
});
}
// Handle resize
window.addEventListener('resize', () => {
if (this.chart && container) {
this.chart.applyOptions({ width: container.clientWidth });
}
});
console.log('[HTS] Chart initialized successfully');
} catch (error) {
console.error('[HTS] Chart initialization error:', error);
this.showError(`Failed to initialize chart: ${error.message}`);
this.chart = null;
this.candlestickSeries = null;
this.volumeSeries = null;
this.rsiSeries = null;
this.macdSeries = null;
}
}
/**
* Start periodic data updates from API
*/
startDataUpdates() {
this.stopDataUpdates();
// Update data every 30 seconds
this.dataUpdateInterval = setInterval(async () => {
try {
await this.loadInitialData();
if (document.getElementById('hts-auto-trade')?.checked) {
await this.runAnalysis();
}
} catch (error) {
console.warn('[HTS] Data update error:', error);
}
}, 30000);
}
/**
* Stop data updates
*/
stopDataUpdates() {
if (this.dataUpdateInterval) {
clearInterval(this.dataUpdateInterval);
this.dataUpdateInterval = null;
}
}
/**
* Load initial OHLCV data from API
*/
async loadInitialData() {
try {
this.updateConnectionStatus('Loading data...', 'info');
const symbol = this.symbol.replace('USDT', '');
// Get base API URL - use relative URLs for HuggingFace compatibility
const baseUrl = window.location.origin;
const apiUrl = `${baseUrl}/api/market?symbol=${symbol}&limit=100`;
// Try multiple API endpoints with retry logic
let data = null;
let response = null;
let retries = 0;
const maxRetries = 2;
// Try /api/market endpoint first
while (retries <= maxRetries) {
try {
if (retries > 0) {
const delay = Math.min(1000 * Math.pow(2, retries - 1), 5000);
await new Promise(resolve => setTimeout(resolve, delay));
}
response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(10000)
});
if (response.ok) {
break;
}
if (retries < maxRetries && response.status >= 500) {
retries++;
continue;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
if (retries < maxRetries && (error.name === 'AbortError' || error.message.includes('timeout') || error.message.includes('network'))) {
retries++;
continue;
}
throw error;
}
}
if (!response || !response.ok) {
throw new Error('Failed to fetch data after retries');
}
data = await response.json();
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format');
}
if (data && data.success && Array.isArray(data.items) && data.items.length > 0) {
const item = data.items.find(i => i && i.symbol === symbol) || data.items[0];
if (item && typeof item === 'object') {
const price = parseFloat(item.price);
if (!isNaN(price) && price > 0) {
// Generate OHLCV from price data
this.ohlcvData = this.generateOHLCVFromPrice(price, 100);
this.updateChart();
this.updateConnectionStatus('Data loaded', 'success');
return;
}
}
}
} catch (e) {
console.warn('[HTS] Primary API failed, trying fallback:', e);
// Log the error for debugging
if (e.message && e.message.includes('ERR_CONNECTION_REFUSED')) {
console.warn('[HTS] Connection refused - ensure backend is running or use correct API URL');
}
}
// Fallback: Generate synthetic OHLCV data
this.generateFallbackData();
this.updateConnectionStatus('Using synthetic data', 'warning');
}
/**
* Generate OHLCV data from single price point
*/
generateOHLCVFromPrice(basePrice, count) {
const data = [];
const now = Math.floor(Date.now() / 1000);
const interval = 3600; // 1 hour intervals
for (let i = count; i >= 0; i--) {
const priceVariation = (Math.random() - 0.5) * basePrice * 0.02; // ±1% variation
const open = basePrice + priceVariation;
const close = open + (Math.random() - 0.5) * basePrice * 0.01;
const high = Math.max(open, close) + Math.random() * basePrice * 0.005;
const low = Math.min(open, close) - Math.random() * basePrice * 0.005;
data.push({
time: now - (i * interval),
open: Math.max(0, open),
high: Math.max(open, high, close),
low: Math.min(open, low, close),
close: Math.max(0, close),
volume: Math.random() * 1000000
});
}
return data;
}
/**
* Generate fallback OHLCV data for testing
*/
generateFallbackData() {
const basePrice = 50000;
const data = [];
const now = Math.floor(Date.now() / 1000);
for (let i = 100; i >= 0; i--) {
const priceChange = (Math.random() - 0.5) * 1000;
const open = basePrice + priceChange;
const close = open + (Math.random() - 0.5) * 500;
const high = Math.max(open, close) + Math.random() * 200;
const low = Math.min(open, close) - Math.random() * 200;
data.push({
time: now - (i * 3600), // 1 hour intervals
open: open,
high: high,
low: low,
close: close,
volume: Math.random() * 1000000
});
}
this.ohlcvData = data;
this.updateChart();
}
/**
* Update chart with current data
*/
updateChart() {
if (!this.chart || !this.candlestickSeries || this.ohlcvData.length === 0) {
if (!this.chart) {
console.warn('[HTS] Chart not initialized, skipping update');
}
return;
}
try {
// Update candlestick data
const candlestickData = this.ohlcvData.map(d => ({
time: d.time,
open: d.open,
high: d.high,
low: d.low,
close: d.close
}));
if (typeof this.candlestickSeries.setData === 'function') {
this.candlestickSeries.setData(candlestickData);
}
// Update volume
if (this.volumeSeries && document.getElementById('show-volume')?.checked) {
if (typeof this.volumeSeries.setData === 'function') {
const volumeData = this.ohlcvData.map(d => ({
time: d.time,
value: d.volume,
color: d.close >= d.open ? '#26a69a80' : '#ef535080'
}));
this.volumeSeries.setData(volumeData);
}
}
// Calculate and update RSI
if (this.rsiSeries && document.getElementById('show-rsi')?.checked) {
if (typeof this.rsiSeries.setData === 'function') {
const rsiValues = this.calculateRSIForChart();
if (rsiValues.length > 0) {
this.rsiSeries.setData(rsiValues);
}
}
}
// Calculate and update MACD
if (this.macdSeries && document.getElementById('show-macd')?.checked) {
if (typeof this.macdSeries.setData === 'function') {
const macdValues = this.calculateMACDForChart();
if (macdValues.length > 0) {
this.macdSeries.setData(macdValues);
}
}
}
// Fit content to view
if (typeof this.chart.timeScale === 'function') {
const timeScale = this.chart.timeScale();
if (timeScale && typeof timeScale.fitContent === 'function') {
timeScale.fitContent();
}
}
} catch (error) {
console.error('[HTS] Chart update error:', error);
}
}
/**
* Calculate RSI for chart display
*/
calculateRSIForChart() {
if (this.ohlcvData.length < 15) return [];
const closes = this.ohlcvData.map(d => d.close);
const rsiValues = [];
for (let i = 14; i < closes.length; i++) {
const rsi = this.engine.calculateRSI(closes.slice(0, i + 1), 14);
if (rsi !== null) {
rsiValues.push({
time: this.ohlcvData[i].time,
value: rsi
});
}
}
return rsiValues;
}
/**
* Calculate MACD for chart display
*/
calculateMACDForChart() {
if (this.ohlcvData.length < 26) return [];
const closes = this.ohlcvData.map(d => d.close);
const macdValues = [];
for (let i = 26; i < closes.length; i++) {
const macd = this.engine.calculateMACD(closes.slice(0, i + 1));
if (macd && macd.macd !== null) {
macdValues.push({
time: this.ohlcvData[i].time,
value: macd.macd
});
}
}
return macdValues;
}
/**
* Run HTS analysis
*/
async runAnalysis() {
try {
if (this.ohlcvData.length < 30) {
this.showError('Insufficient data for analysis. Please wait...');
return;
}
const symbol = this.symbol.replace('USDT', '');
this.analysisResult = await this.engine.analyze(this.ohlcvData, symbol);
this.renderAnalysisResult();
this.renderComponents();
this.renderSMCLevels();
this.renderPatterns();
} catch (error) {
console.error('[HTS] Analysis error:', error);
this.showError('Analysis failed: ' + error.message);
}
}
/**
* Render analysis result
*/
renderAnalysisResult() {
if (!this.analysisResult) return;
const container = document.getElementById('hts-signal-content');
if (!container) return;
if (!this.analysisResult || typeof this.analysisResult !== 'object') {
container.innerHTML = '<div class="error-message">Invalid analysis result</div>';
return;
}
const { finalScore, finalSignal, confidence, currentPrice, stopLoss, takeProfitLevels, riskReward, marketRegime } = this.analysisResult;
const signal = String(finalSignal || 'hold').toLowerCase();
const signalColor = signal === 'buy' ? '#22c55e' : signal === 'sell' ? '#ef4444' : '#eab308';
const signalIcon = signal === 'buy' ? TradingIcons.buy : signal === 'sell' ? TradingIcons.sell : TradingIcons.hold;
const validScore = typeof finalScore === 'number' && !isNaN(finalScore) ? finalScore : 0;
const validConfidence = typeof confidence === 'number' && !isNaN(confidence) ? Math.max(0, Math.min(100, confidence)) : 0;
const validPrice = typeof currentPrice === 'number' && !isNaN(currentPrice) && currentPrice > 0 ? currentPrice : 0;
const validStopLoss = typeof stopLoss === 'number' && !isNaN(stopLoss) && stopLoss > 0 ? stopLoss : 0;
const validTakeProfits = Array.isArray(takeProfitLevels) ? takeProfitLevels.filter(tp => tp && typeof tp === 'object' && typeof tp.level === 'number' && !isNaN(tp.level)) : [];
const validRiskReward = typeof riskReward === 'number' && !isNaN(riskReward) ? riskReward : 0;
const regimeColors = {
'trending': '#3b82f6',
'ranging': '#8b5cf6',
'volatile': '#f59e0b',
'volatile-trending': '#ef4444',
'neutral': '#6b7280'
};
const regimeLabels = {
'trending': 'Trending Market',
'ranging': 'Ranging Market',
'volatile': 'Volatile Market',
'volatile-trending': 'Volatile Trending',
'neutral': 'Neutral Market'
};
container.innerHTML = `
<div class="signal-main">
${marketRegime ? `
<div class="market-regime-badge" style="background: ${regimeColors[marketRegime.regime || 'neutral']}20; border-color: ${regimeColors[marketRegime.regime || 'neutral']}40;">
<span class="regime-label">Market Regime:</span>
<span class="regime-value">${regimeLabels[marketRegime.regime || 'neutral']}</span>
<span class="regime-stats">
Volatility: ${(marketRegime.volatility || 0).toFixed(2)}% |
Trend: ${(marketRegime.trendStrength || 0).toFixed(0)}%
</span>
</div>
` : ''}
<div class="signal-score">
<div class="score-value" style="color: ${signalColor}">${escapeHtml(safeFormatNumber(validScore, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}</div>
<div class="score-label">Final Score</div>
</div>
<div class="signal-details">
<div class="detail-item">
<span class="detail-label">Signal:</span>
<span class="detail-value signal-${escapeHtml(signal)}" style="color: ${signalColor}">
${signalIcon} ${escapeHtml(signal.toUpperCase())}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Confidence:</span>
<span class="detail-value">${escapeHtml(safeFormatNumber(validConfidence, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}%</span>
</div>
<div class="detail-item">
<span class="detail-label">Current Price:</span>
<span class="detail-value">${validPrice > 0 ? safeFormatCurrency(validPrice) : '—'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Stop Loss:</span>
<span class="detail-value text-danger">${validStopLoss > 0 ? safeFormatCurrency(validStopLoss) : '—'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Risk/Reward:</span>
<span class="detail-value">1:${escapeHtml(safeFormatNumber(validRiskReward, { minimumFractionDigits: 2, maximumFractionDigits: 2 }))}</span>
</div>
</div>
<div class="take-profit-levels">
<h4>Take Profit Levels</h4>
${validTakeProfits.length > 0 ? validTakeProfits.map(tp => {
const tpType = escapeHtml(String(tp.type || 'TP'));
const tpLevel = safeFormatCurrency(tp.level);
const tpRR = typeof tp.riskReward === 'number' && !isNaN(tp.riskReward)
? escapeHtml(safeFormatNumber(tp.riskReward, { minimumFractionDigits: 2, maximumFractionDigits: 2 }))
: '—';
return `
<div class="tp-level">
<span class="tp-label">${tpType}:</span>
<span class="tp-value">${tpLevel}</span>
<span class="tp-rr">R:R ${tpRR}</span>
</div>
`;
}).join('') : '<div class="no-tp-levels">No take profit levels available</div>'}
</div>
</div>
`;
// Update signal badge
const badge = document.getElementById('hts-signal-badge');
if (badge) {
badge.textContent = finalSignal.toUpperCase();
badge.className = `signal-badge signal-${finalSignal}`;
}
}
/**
* Render component scores
*/
renderComponents() {
if (!this.analysisResult || !this.analysisResult.components) return;
const container = document.getElementById('hts-components-grid');
if (!container) return;
const components = this.analysisResult.components;
if (!components || typeof components !== 'object') {
container.innerHTML = '<div class="no-components">No component data available</div>';
return;
}
container.innerHTML = Object.entries(components)
.filter(([key, comp]) => comp && typeof comp === 'object')
.map(([key, comp]) => {
const validScore = typeof comp.score === 'number' && !isNaN(comp.score)
? Math.max(0, Math.min(100, comp.score))
: 50;
const validWeight = typeof comp.weight === 'number' && !isNaN(comp.weight)
? Math.max(0, Math.min(1, comp.weight))
: 0;
const validBaseWeight = (comp.baseWeight && typeof comp.baseWeight === 'number' && !isNaN(comp.baseWeight))
? Math.max(0, Math.min(1, comp.baseWeight))
: validWeight;
const validConfidence = typeof comp.confidence === 'number' && !isNaN(comp.confidence)
? Math.max(0, Math.min(100, comp.confidence))
: 0;
const scoreColor = validScore > 60 ? '#22c55e' : validScore < 40 ? '#ef4444' : '#eab308';
const weightPercent = (validWeight * 100).toFixed(1);
const baseWeightPercent = (validBaseWeight * 100).toFixed(1);
const weightChange = validBaseWeight ? validWeight - validBaseWeight : 0;
const weightChangePercent = (weightChange * 100).toFixed(1);
const weightChangeColor = weightChange > 0.001 ? '#22c55e' : weightChange < -0.001 ? '#ef4444' : '#6b7280';
const signal = escapeHtml(String(comp.signal || 'hold').toUpperCase());
const signalClass = escapeHtml(String(comp.signal || 'hold'));
const keyDisplay = escapeHtml(String(key).toUpperCase());
const detailsHtml = (key === 'rsiMacd' && comp.details && typeof comp.details === 'object') ? `
<div class="component-details">
<div>RSI: ${escapeHtml(String(comp.details.rsi || '—'))}</div>
<div>MACD: ${escapeHtml(String(comp.details.macd || '—'))}</div>
<div>Histogram: ${escapeHtml(String(comp.details.histogram || '—'))}</div>
</div>
` : '';
return `
<div class="component-card">
<div class="component-header">
<h4>${keyDisplay}</h4>
<div class="weight-info">
<span class="component-weight">${escapeHtml(weightPercent)}%</span>
${Math.abs(weightChange) > 0.001 ? `
<span class="weight-change" style="color: ${weightChangeColor}">
${weightChange > 0 ? '↑' : '↓'} ${escapeHtml(String(Math.abs(weightChangePercent)))}%
</span>
` : ''}
</div>
</div>
<div class="weight-bar-container">
<div class="weight-bar-base" style="width: ${escapeHtml(baseWeightPercent)}%"></div>
<div class="weight-bar-current" style="width: ${escapeHtml(weightPercent)}%; background: ${weightChangeColor}"></div>
</div>
<div class="component-score" style="color: ${scoreColor}">
${escapeHtml(safeFormatNumber(validScore, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}
</div>
<div class="component-signal signal-${signalClass}">
${signal}
</div>
<div class="component-confidence">
Confidence: ${escapeHtml(safeFormatNumber(validConfidence, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}%
</div>
${detailsHtml}
</div>
`;
}).filter(html => html.length > 0).join('') || '<div class="no-components">No component data available</div>';
}
/**
* Render SMC levels
*/
renderSMCLevels() {
if (!this.analysisResult || !this.analysisResult.smcLevels) return;
const container = document.getElementById('hts-smc-content');
if (!container) return;
const smcLevels = this.analysisResult.smcLevels;
if (!smcLevels || typeof smcLevels !== 'object') {
container.innerHTML = '<div class="no-smc">No SMC levels available</div>';
return;
}
const orderBlocks = Array.isArray(smcLevels.orderBlocks) ? smcLevels.orderBlocks : [];
const liquidityZones = Array.isArray(smcLevels.liquidityZones) ? smcLevels.liquidityZones : [];
const breakerBlocks = Array.isArray(smcLevels.breakerBlocks) ? smcLevels.breakerBlocks : [];
container.innerHTML = `
<div class="smc-section">
<h4>Order Blocks: ${escapeHtml(String(orderBlocks.length))}</h4>
<div class="smc-items">
${orderBlocks.slice(-3)
.filter(block => block && typeof block === 'object' &&
typeof block.high === 'number' && !isNaN(block.high) &&
typeof block.low === 'number' && !isNaN(block.low))
.map(block => {
const volume = typeof block.volume === 'number' && !isNaN(block.volume)
? (block.volume / 1000000).toFixed(2)
: '0.00';
return `
<div class="smc-item">
<span>High: ${safeFormatCurrency(block.high)}</span>
<span>Low: ${safeFormatCurrency(block.low)}</span>
<span>Volume: ${escapeHtml(volume)}M</span>
</div>
`;
}).join('') || '<div class="no-items">No order blocks</div>'}
</div>
</div>
<div class="smc-section">
<h4>Liquidity Zones: ${escapeHtml(String(liquidityZones.length))}</h4>
<div class="smc-items">
${liquidityZones
.filter(zone => zone && typeof zone === 'object' &&
typeof zone.level === 'number' && !isNaN(zone.level))
.map(zone => {
const zoneType = escapeHtml(String(zone.type || 'unknown').toUpperCase());
const zoneTypeClass = escapeHtml(String(zone.type || 'unknown'));
const zoneStrength = escapeHtml(String(zone.strength || 'Medium'));
return `
<div class="smc-item smc-${zoneTypeClass}">
<span>${zoneType}: ${safeFormatCurrency(zone.level)}</span>
<span>Strength: ${zoneStrength}</span>
</div>
`;
}).join('') || '<div class="no-items">No liquidity zones</div>'}
</div>
</div>
<div class="smc-section">
<h4>Breaker Blocks: ${escapeHtml(String(breakerBlocks.length))}</h4>
<div class="smc-items">
${breakerBlocks
.filter(block => block && typeof block === 'object' &&
typeof block.level === 'number' && !isNaN(block.level))
.map(block => {
const blockType = escapeHtml(String(block.type || 'unknown').toUpperCase());
const blockTypeClass = escapeHtml(String(block.type || 'unknown'));
return `
<div class="smc-item smc-${blockTypeClass}">
<span>${blockType}</span>
<span>Level: ${safeFormatCurrency(block.level)}</span>
</div>
`;
}).join('') || '<div class="no-items">No breaker blocks</div>'}
</div>
</div>
`;
}
/**
* Render detected patterns
*/
renderPatterns() {
if (!this.analysisResult || !this.analysisResult.patterns) return;
const container = document.getElementById('hts-patterns-content');
if (!container) return;
const patterns = Array.isArray(this.analysisResult.patterns) ? this.analysisResult.patterns : [];
if (patterns.length === 0) {
container.innerHTML = '<p class="no-patterns">No patterns detected</p>';
return;
}
container.innerHTML = `
<div class="patterns-grid">
${patterns
.filter(pattern => pattern && typeof pattern === 'object')
.map(pattern => {
const patternName = escapeHtml(String(pattern.name || 'Unknown Pattern'));
const patternType = escapeHtml(String(pattern.type || 'neutral').toUpperCase());
const patternTypeClass = escapeHtml(String(pattern.type || 'neutral'));
const patternConfidence = typeof pattern.confidence === 'number' && !isNaN(pattern.confidence)
? escapeHtml(safeFormatNumber(pattern.confidence, { minimumFractionDigits: 0, maximumFractionDigits: 0 }))
: '0';
return `
<div class="pattern-card pattern-${patternTypeClass}">
<div class="pattern-name">${patternName}</div>
<div class="pattern-type">${patternType}</div>
<div class="pattern-confidence">Confidence: ${patternConfidence}%</div>
</div>
`;
}).filter(html => html.length > 0).join('') || '<p class="no-patterns">No valid patterns detected</p>'}
</div>
`;
}
/**
* Update connection status
*/
updateConnectionStatus(status, type) {
const statusEl = document.getElementById('hts-connection-status');
if (statusEl) {
statusEl.textContent = status;
statusEl.className = `status-indicator status-${type}`;
}
}
/**
* Show error message
*/
showError(message) {
const container = document.getElementById('hts-signal-content');
if (container) {
container.innerHTML = `
<div class="error-message">
${TradingIcons.risk}
<p>${message}</p>
</div>
`;
}
}
/**
* Start auto-analysis
*/
startAutoAnalysis() {
this.stopAutoAnalysis();
this.autoAnalysisInterval = setInterval(async () => {
if (this.ohlcvData.length >= 30) {
await this.runAnalysis();
}
}, 60000); // Every minute
}
/**
* Stop auto-analysis
*/
stopAutoAnalysis() {
if (this.autoAnalysisInterval) {
clearInterval(this.autoAnalysisInterval);
this.autoAnalysisInterval = null;
}
}
}
// Initialize HTS Page when DOM is ready
let htsPageInstance = null;
document.addEventListener('DOMContentLoaded', () => {
// Only initialize if we're on the trading assistant page
if (document.getElementById('hts-trading-view')) {
htsPageInstance = new HTSPage();
window.htsPage = htsPageInstance;
}
});
// Export for module use
export default HTSPage;