|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { Toast } from '../../shared/js/components/toast.js';
|
|
|
import { escapeHtml, safeFormatNumber, safeFormatCurrency } from '../../shared/js/utils/sanitizer.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const API_CONFIG = {
|
|
|
backend: window.location.origin + '/api',
|
|
|
timeout: 8000,
|
|
|
retries: 1,
|
|
|
fallbacks: {
|
|
|
coingecko: 'https://api.coingecko.com/api/v3',
|
|
|
binance: 'https://api.binance.com/api/v3',
|
|
|
cryptocompare: 'https://min-api.cryptocompare.com/data'
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const API_CACHE = {
|
|
|
data: new Map(),
|
|
|
ttl: 60000,
|
|
|
|
|
|
set(key, value) {
|
|
|
this.data.set(key, {
|
|
|
value,
|
|
|
timestamp: Date.now()
|
|
|
});
|
|
|
},
|
|
|
|
|
|
get(key) {
|
|
|
const item = this.data.get(key);
|
|
|
if (!item) return null;
|
|
|
|
|
|
if (Date.now() - item.timestamp > this.ttl) {
|
|
|
this.data.delete(key);
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
return item.value;
|
|
|
},
|
|
|
|
|
|
clear() {
|
|
|
this.data.clear();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const SYMBOL_MAPPING = {
|
|
|
'BTC': { coingecko: 'bitcoin', binance: 'BTCUSDT', cc: 'BTC' },
|
|
|
'ETH': { coingecko: 'ethereum', binance: 'ETHUSDT', cc: 'ETH' },
|
|
|
'BNB': { coingecko: 'binancecoin', binance: 'BNBUSDT', cc: 'BNB' },
|
|
|
'SOL': { coingecko: 'solana', binance: 'SOLUSDT', cc: 'SOL' },
|
|
|
'ADA': { coingecko: 'cardano', binance: 'ADAUSDT', cc: 'ADA' },
|
|
|
'XRP': { coingecko: 'ripple', binance: 'XRPUSDT', cc: 'XRP' },
|
|
|
'DOT': { coingecko: 'polkadot', binance: 'DOTUSDT', cc: 'DOT' },
|
|
|
'DOGE': { coingecko: 'dogecoin', binance: 'DOGEUSDT', cc: 'DOGE' },
|
|
|
'AVAX': { coingecko: 'avalanche-2', binance: 'AVAXUSDT', cc: 'AVAX' },
|
|
|
'MATIC': { coingecko: 'matic-network', binance: 'MATICUSDT', cc: 'MATIC' }
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const TIMEFRAME_MAP = {
|
|
|
'1m': { binance: '1m', cc: 1 },
|
|
|
'5m': { binance: '5m', cc: 5 },
|
|
|
'15m': { binance: '15m', cc: 15 },
|
|
|
'1h': { binance: '1h', cc: 60 },
|
|
|
'4h': { binance: '4h', cc: 240 },
|
|
|
'1d': { binance: '1d', cc: 1440 },
|
|
|
'1w': { binance: '1w', cc: 10080 }
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TechnicalAnalysisProfessional {
|
|
|
constructor() {
|
|
|
this.chart = null;
|
|
|
this.candlestickSeries = null;
|
|
|
this.volumeSeries = null;
|
|
|
this.currentSymbol = 'BTC';
|
|
|
this.currentTimeframe = '4h';
|
|
|
this.currentMode = 'quick';
|
|
|
this.ohlcvData = [];
|
|
|
this.indicators = {
|
|
|
rsi: null,
|
|
|
macd: null,
|
|
|
ema: null,
|
|
|
volume: null
|
|
|
};
|
|
|
this.dataSource = 'none';
|
|
|
this.lastUpdate = null;
|
|
|
this.autoRefreshInterval = null;
|
|
|
this.isLoading = false;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async init() {
|
|
|
try {
|
|
|
console.log('[TechnicalAnalysis] Initializing Professional Edition...');
|
|
|
|
|
|
this.bindEvents();
|
|
|
this.initializeChart();
|
|
|
await this.loadMarketData();
|
|
|
this.setupAutoRefresh();
|
|
|
|
|
|
this.showToast('✅ Technical Analysis Ready', 'success');
|
|
|
console.log('[TechnicalAnalysis] Initialization complete');
|
|
|
} catch (error) {
|
|
|
console.error('[TechnicalAnalysis] Initialization error:', error);
|
|
|
this.showToast('⚠️ Initialization error - using fallback mode', 'warning');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bindEvents() {
|
|
|
|
|
|
const symbolSelect = document.getElementById('symbol-select');
|
|
|
if (symbolSelect) {
|
|
|
symbolSelect.addEventListener('change', (e) => {
|
|
|
this.currentSymbol = e.target.value;
|
|
|
this.loadMarketData();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
const timeframeButtons = document.querySelectorAll('[data-timeframe]');
|
|
|
timeframeButtons.forEach(btn => {
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
timeframeButtons.forEach(b => b.classList.remove('active'));
|
|
|
e.currentTarget.classList.add('active');
|
|
|
this.currentTimeframe = e.currentTarget.dataset.timeframe;
|
|
|
this.loadMarketData();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
const modeTabs = document.querySelectorAll('[data-mode]');
|
|
|
modeTabs.forEach(tab => {
|
|
|
tab.addEventListener('click', (e) => {
|
|
|
modeTabs.forEach(t => t.classList.remove('active'));
|
|
|
e.currentTarget.classList.add('active');
|
|
|
this.currentMode = e.currentTarget.dataset.mode;
|
|
|
this.performAnalysis();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
const analyzeBtn = document.getElementById('analyze-btn');
|
|
|
if (analyzeBtn) {
|
|
|
analyzeBtn.addEventListener('click', () => this.performAnalysis());
|
|
|
}
|
|
|
|
|
|
|
|
|
const refreshBtn = document.getElementById('refresh-data');
|
|
|
if (refreshBtn) {
|
|
|
refreshBtn.addEventListener('click', () => this.loadMarketData(true));
|
|
|
}
|
|
|
|
|
|
|
|
|
const exportBtn = document.getElementById('export-analysis');
|
|
|
if (exportBtn) {
|
|
|
exportBtn.addEventListener('click', () => this.exportAnalysis());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
initializeChart() {
|
|
|
const chartContainer = document.getElementById('tradingview-chart');
|
|
|
if (!chartContainer) {
|
|
|
console.warn('Chart container not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof LightweightCharts === 'undefined') {
|
|
|
console.warn('LightweightCharts not loaded, showing fallback');
|
|
|
this.showChartFallback();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
this.chart = LightweightCharts.createChart(chartContainer, {
|
|
|
width: chartContainer.clientWidth,
|
|
|
height: 500,
|
|
|
layout: {
|
|
|
background: { color: 'transparent' },
|
|
|
textColor: '#d1d5db',
|
|
|
},
|
|
|
grid: {
|
|
|
vertLines: { color: 'rgba(255, 255, 255, 0.05)' },
|
|
|
horzLines: { color: 'rgba(255, 255, 255, 0.05)' },
|
|
|
},
|
|
|
crosshair: {
|
|
|
mode: LightweightCharts.CrosshairMode.Normal,
|
|
|
},
|
|
|
rightPriceScale: {
|
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
|
},
|
|
|
timeScale: {
|
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
|
timeVisible: true,
|
|
|
secondsVisible: false,
|
|
|
},
|
|
|
});
|
|
|
|
|
|
|
|
|
this.candlestickSeries = this.chart.addCandlestickSeries({
|
|
|
upColor: '#22c55e',
|
|
|
downColor: '#ef4444',
|
|
|
borderVisible: false,
|
|
|
wickUpColor: '#22c55e',
|
|
|
wickDownColor: '#ef4444',
|
|
|
});
|
|
|
|
|
|
|
|
|
this.volumeSeries = this.chart.addHistogramSeries({
|
|
|
color: '#26a69a',
|
|
|
priceFormat: {
|
|
|
type: 'volume',
|
|
|
},
|
|
|
priceScaleId: '',
|
|
|
scaleMargins: {
|
|
|
top: 0.8,
|
|
|
bottom: 0,
|
|
|
},
|
|
|
});
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
if (this.chart && chartContainer) {
|
|
|
this.chart.applyOptions({
|
|
|
width: chartContainer.clientWidth
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
console.log('✅ Chart initialized successfully');
|
|
|
} catch (error) {
|
|
|
console.error('❌ Chart initialization error:', error);
|
|
|
this.showChartFallback();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showChartFallback() {
|
|
|
const chartContainer = document.getElementById('tradingview-chart');
|
|
|
if (chartContainer) {
|
|
|
chartContainer.innerHTML = `
|
|
|
<div style="display: flex; align-items: center; justify-content: center; height: 500px; background: rgba(0,0,0,0.2); border-radius: 12px;">
|
|
|
<div style="text-align: center; color: #9ca3af;">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin: 0 auto 1rem;">
|
|
|
<path d="M3 3v18h18"></path>
|
|
|
<path d="M19 9l-5 5-4-4-3 3"></path>
|
|
|
</svg>
|
|
|
<p style="font-size: 1.1rem; margin-bottom: 0.5rem;">Chart Loading...</p>
|
|
|
<p style="font-size: 0.875rem; opacity: 0.7;">Analysis data will still be available</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadMarketData(forceRefresh = false) {
|
|
|
if (this.isLoading) {
|
|
|
console.log('Already loading data, skipping...');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.isLoading = true;
|
|
|
this.showLoadingState(true);
|
|
|
|
|
|
try {
|
|
|
console.log(`[TechnicalAnalysis] Loading data for ${this.currentSymbol} (${this.currentTimeframe})...`);
|
|
|
|
|
|
|
|
|
const cacheKey = `ohlcv_${this.currentSymbol}_${this.currentTimeframe}`;
|
|
|
const cached = API_CACHE.get(cacheKey);
|
|
|
if (cached) {
|
|
|
console.log('✅ Using cached data');
|
|
|
this.ohlcvData = cached;
|
|
|
this.dataSource = 'cache';
|
|
|
this.lastUpdate = new Date();
|
|
|
|
|
|
this.updateChart(cached);
|
|
|
this.updatePriceInfo(cached[cached.length - 1]);
|
|
|
this.calculateIndicators(cached);
|
|
|
this.performAnalysis();
|
|
|
|
|
|
this.showToast(`✅ Data loaded from cache`, 'success');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
let ohlcvData = null;
|
|
|
try {
|
|
|
ohlcvData = await this.fetchFromBackend(this.currentSymbol, this.currentTimeframe);
|
|
|
this.dataSource = 'backend';
|
|
|
console.log('✅ Data loaded from backend');
|
|
|
} catch (backendError) {
|
|
|
console.warn('Backend API failed, trying fallbacks...', backendError.message || backendError);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!ohlcvData || ohlcvData.length === 0) {
|
|
|
try {
|
|
|
ohlcvData = await this.fetchFromBinance(this.currentSymbol, this.currentTimeframe);
|
|
|
this.dataSource = 'binance';
|
|
|
console.log('✅ Data loaded from Binance');
|
|
|
} catch (binanceError) {
|
|
|
console.warn('Binance API failed, trying CryptoCompare...', binanceError);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!ohlcvData || ohlcvData.length === 0) {
|
|
|
try {
|
|
|
ohlcvData = await this.fetchFromCryptoCompare(this.currentSymbol, this.currentTimeframe);
|
|
|
this.dataSource = 'cryptocompare';
|
|
|
console.log('✅ Data loaded from CryptoCompare');
|
|
|
} catch (ccError) {
|
|
|
console.warn('CryptoCompare API failed', ccError);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!ohlcvData || ohlcvData.length === 0) {
|
|
|
console.error('❌ All data sources failed - no real data available');
|
|
|
this.showErrorState('Unable to fetch real market data. Please check your connection and try again.');
|
|
|
this.showToast('❌ Failed to load real data from all sources', 'error');
|
|
|
return;
|
|
|
} else {
|
|
|
|
|
|
API_CACHE.set(cacheKey, ohlcvData);
|
|
|
}
|
|
|
|
|
|
this.ohlcvData = ohlcvData;
|
|
|
this.lastUpdate = new Date();
|
|
|
|
|
|
this.updateChart(ohlcvData);
|
|
|
this.updatePriceInfo(ohlcvData[ohlcvData.length - 1]);
|
|
|
this.calculateIndicators(ohlcvData);
|
|
|
this.performAnalysis();
|
|
|
|
|
|
this.showToast(`✅ Data loaded from ${this.dataSource}`, 'success');
|
|
|
} catch (error) {
|
|
|
console.error('❌ Failed to load market data:', error);
|
|
|
this.showToast('❌ Failed to load data - please try again', 'error');
|
|
|
this.showErrorState(error.message);
|
|
|
} finally {
|
|
|
this.isLoading = false;
|
|
|
this.showLoadingState(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFromBackend(symbol, timeframe) {
|
|
|
|
|
|
try {
|
|
|
const unifiedUrl = `${API_CONFIG.backend}/market/ohlc?symbol=${symbol}&interval=${timeframe}&limit=100`;
|
|
|
const unifiedResponse = await this.fetchWithTimeout(unifiedUrl, API_CONFIG.timeout);
|
|
|
|
|
|
if (unifiedResponse.ok) {
|
|
|
const unifiedData = await unifiedResponse.json();
|
|
|
const items = unifiedData.data || unifiedData.ohlcv || unifiedData.items || (Array.isArray(unifiedData) ? unifiedData : []);
|
|
|
|
|
|
if (Array.isArray(items) && items.length > 0) {
|
|
|
return this.normalizeOHLCV(items);
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('[TechnicalAnalysis] Unified OHLC API failed, trying legacy endpoint:', e.message);
|
|
|
}
|
|
|
|
|
|
|
|
|
const url = `${API_CONFIG.backend}/ohlcv/${symbol}?interval=${timeframe}&limit=100`;
|
|
|
const response = await this.fetchWithTimeout(url, API_CONFIG.timeout);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`Backend API error: ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
const items = data.data || data.ohlcv || data.items || (Array.isArray(data) ? data : []);
|
|
|
|
|
|
if (!Array.isArray(items) || items.length === 0) {
|
|
|
throw new Error('Invalid or empty data from backend');
|
|
|
}
|
|
|
|
|
|
|
|
|
return this.normalizeOHLCV(items);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFromBinance(symbol, timeframe) {
|
|
|
const mapping = SYMBOL_MAPPING[symbol];
|
|
|
if (!mapping) {
|
|
|
throw new Error(`Symbol ${symbol} not supported`);
|
|
|
}
|
|
|
|
|
|
const binanceSymbol = mapping.binance;
|
|
|
const interval = TIMEFRAME_MAP[timeframe]?.binance || '4h';
|
|
|
|
|
|
const url = `${API_CONFIG.fallbacks.binance}/klines?symbol=${binanceSymbol}&interval=${interval}&limit=100`;
|
|
|
|
|
|
const response = await this.fetchWithTimeout(url, API_CONFIG.timeout);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`Binance API error: ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
|
throw new Error('Invalid data from Binance');
|
|
|
}
|
|
|
|
|
|
|
|
|
return data.map(item => ({
|
|
|
time: Math.floor(item[0] / 1000),
|
|
|
open: parseFloat(item[1]),
|
|
|
high: parseFloat(item[2]),
|
|
|
low: parseFloat(item[3]),
|
|
|
close: parseFloat(item[4]),
|
|
|
volume: parseFloat(item[5])
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFromCryptoCompare(symbol, timeframe) {
|
|
|
const mapping = SYMBOL_MAPPING[symbol];
|
|
|
if (!mapping) {
|
|
|
throw new Error(`Symbol ${symbol} not supported`);
|
|
|
}
|
|
|
|
|
|
const ccSymbol = mapping.cc;
|
|
|
const limit = 100;
|
|
|
|
|
|
|
|
|
let endpoint;
|
|
|
if (['1m', '5m', '15m'].includes(timeframe)) {
|
|
|
endpoint = 'histominute';
|
|
|
} else if (['1h', '4h'].includes(timeframe)) {
|
|
|
endpoint = 'histohour';
|
|
|
} else {
|
|
|
endpoint = 'histoday';
|
|
|
}
|
|
|
|
|
|
const url = `${API_CONFIG.fallbacks.cryptocompare}/${endpoint}?fsym=${ccSymbol}&tsym=USD&limit=${limit}`;
|
|
|
|
|
|
const response = await this.fetchWithTimeout(url, API_CONFIG.timeout);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`CryptoCompare API error: ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.Response === 'Error' || !data.Data || !Array.isArray(data.Data)) {
|
|
|
throw new Error('Invalid data from CryptoCompare');
|
|
|
}
|
|
|
|
|
|
|
|
|
return data.Data.map(item => ({
|
|
|
time: item.time,
|
|
|
open: item.open,
|
|
|
high: item.high,
|
|
|
low: item.low,
|
|
|
close: item.close,
|
|
|
volume: item.volumefrom
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchWithTimeout(url, timeout) {
|
|
|
const controller = new AbortController();
|
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
|
|
|
try {
|
|
|
const response = await fetch(url, {
|
|
|
signal: controller.signal,
|
|
|
headers: {
|
|
|
'Accept': 'application/json'
|
|
|
}
|
|
|
});
|
|
|
clearTimeout(timeoutId);
|
|
|
return response;
|
|
|
} catch (error) {
|
|
|
clearTimeout(timeoutId);
|
|
|
if (error.name === 'AbortError') {
|
|
|
throw new Error('Request timeout');
|
|
|
}
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
normalizeOHLCV(items) {
|
|
|
return items.map(item => {
|
|
|
const normalized = {
|
|
|
time: this.parseTime(item.timestamp || item.time || item.t || item.date),
|
|
|
open: parseFloat(item.open || item.o),
|
|
|
high: parseFloat(item.high || item.h),
|
|
|
low: parseFloat(item.low || item.l),
|
|
|
close: parseFloat(item.close || item.c),
|
|
|
volume: parseFloat(item.volume || item.v || 0)
|
|
|
};
|
|
|
|
|
|
|
|
|
if (!normalized.time || isNaN(normalized.time)) {
|
|
|
throw new Error('Invalid timestamp in OHLCV data');
|
|
|
}
|
|
|
if (isNaN(normalized.open) || isNaN(normalized.high) ||
|
|
|
isNaN(normalized.low) || isNaN(normalized.close)) {
|
|
|
throw new Error('Invalid OHLCV values');
|
|
|
}
|
|
|
if (normalized.high < normalized.low) {
|
|
|
throw new Error('Invalid OHLCV: high < low');
|
|
|
}
|
|
|
|
|
|
return normalized;
|
|
|
}).filter(item => item.close > 0);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseTime(time) {
|
|
|
if (typeof time === 'number') {
|
|
|
|
|
|
return time > 10000000000 ? Math.floor(time / 1000) : time;
|
|
|
}
|
|
|
if (typeof time === 'string') {
|
|
|
return Math.floor(new Date(time).getTime() / 1000);
|
|
|
}
|
|
|
throw new Error('Invalid time format');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateChart(ohlcvData) {
|
|
|
if (!this.chart || !this.candlestickSeries) {
|
|
|
console.warn('Chart not initialized, skipping update');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const candleData = ohlcvData.map(item => ({
|
|
|
time: item.time,
|
|
|
open: item.open,
|
|
|
high: item.high,
|
|
|
low: item.low,
|
|
|
close: item.close
|
|
|
}));
|
|
|
|
|
|
|
|
|
const volumeData = ohlcvData.map(item => ({
|
|
|
time: item.time,
|
|
|
value: item.volume,
|
|
|
color: item.close >= item.open ? 'rgba(34, 197, 94, 0.5)' : 'rgba(239, 68, 68, 0.5)'
|
|
|
}));
|
|
|
|
|
|
this.candlestickSeries.setData(candleData);
|
|
|
this.volumeSeries.setData(volumeData);
|
|
|
|
|
|
|
|
|
this.chart.timeScale().fitContent();
|
|
|
|
|
|
console.log('✅ Chart updated with', candleData.length, 'candles');
|
|
|
} catch (error) {
|
|
|
console.error('❌ Chart update error:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updatePriceInfo(latestCandle) {
|
|
|
if (!latestCandle) return;
|
|
|
|
|
|
const priceElement = document.getElementById('current-price');
|
|
|
const changeElement = document.getElementById('price-change');
|
|
|
const highElement = document.getElementById('24h-high');
|
|
|
const lowElement = document.getElementById('24h-low');
|
|
|
const volumeElement = document.getElementById('24h-volume');
|
|
|
|
|
|
if (priceElement) {
|
|
|
priceElement.textContent = safeFormatCurrency(latestCandle.close);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.ohlcvData.length > 1) {
|
|
|
const oldPrice = this.ohlcvData[0].close;
|
|
|
const newPrice = latestCandle.close;
|
|
|
const change = ((newPrice - oldPrice) / oldPrice) * 100;
|
|
|
|
|
|
if (changeElement) {
|
|
|
const arrow = change >= 0 ? '↑' : '↓';
|
|
|
const color = change >= 0 ? '#22c55e' : '#ef4444';
|
|
|
changeElement.textContent = `${arrow} ${Math.abs(change).toFixed(2)}%`;
|
|
|
changeElement.style.color = color;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (highElement && lowElement) {
|
|
|
const prices = this.ohlcvData.map(c => [c.high, c.low]).flat();
|
|
|
highElement.textContent = safeFormatCurrency(Math.max(...prices));
|
|
|
lowElement.textContent = safeFormatCurrency(Math.min(...prices));
|
|
|
}
|
|
|
|
|
|
|
|
|
if (volumeElement) {
|
|
|
const totalVolume = this.ohlcvData.reduce((sum, c) => sum + c.volume, 0);
|
|
|
volumeElement.textContent = safeFormatNumber(totalVolume);
|
|
|
}
|
|
|
|
|
|
|
|
|
const lastUpdateEl = document.getElementById('last-update');
|
|
|
if (lastUpdateEl) {
|
|
|
lastUpdateEl.textContent = `Last update: ${new Date().toLocaleTimeString()}`;
|
|
|
}
|
|
|
|
|
|
|
|
|
const dataSourceEl = document.getElementById('data-source');
|
|
|
if (dataSourceEl) {
|
|
|
dataSourceEl.textContent = `Source: ${this.dataSource}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateIndicators(ohlcvData) {
|
|
|
if (!ohlcvData || ohlcvData.length < 14) {
|
|
|
console.warn('Not enough data for indicators');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
this.indicators.rsi = this.calculateRSI(ohlcvData);
|
|
|
|
|
|
|
|
|
this.indicators.macd = this.calculateMACD(ohlcvData);
|
|
|
|
|
|
|
|
|
this.indicators.ema = this.calculateEMA(ohlcvData, 20);
|
|
|
|
|
|
|
|
|
this.updateIndicatorDisplays();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateRSI(data, period = 14) {
|
|
|
if (data.length < period + 1) return null;
|
|
|
|
|
|
let gains = 0;
|
|
|
let losses = 0;
|
|
|
|
|
|
|
|
|
for (let i = 1; i <= period; i++) {
|
|
|
const change = data[i].close - data[i - 1].close;
|
|
|
if (change > 0) gains += change;
|
|
|
else losses += Math.abs(change);
|
|
|
}
|
|
|
|
|
|
let avgGain = gains / period;
|
|
|
let avgLoss = losses / period;
|
|
|
|
|
|
|
|
|
const rsiValues = [];
|
|
|
|
|
|
for (let i = period + 1; i < data.length; i++) {
|
|
|
const change = data[i].close - data[i - 1].close;
|
|
|
const gain = change > 0 ? change : 0;
|
|
|
const loss = change < 0 ? Math.abs(change) : 0;
|
|
|
|
|
|
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
|
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
|
|
|
|
const rs = avgGain / avgLoss;
|
|
|
const rsi = 100 - (100 / (1 + rs));
|
|
|
rsiValues.push(rsi);
|
|
|
}
|
|
|
|
|
|
return rsiValues.length > 0 ? rsiValues[rsiValues.length - 1] : null;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateMACD(data) {
|
|
|
if (data.length < 26) return null;
|
|
|
|
|
|
const ema12 = this.calculateEMA(data, 12);
|
|
|
const ema26 = this.calculateEMA(data, 26);
|
|
|
|
|
|
if (!ema12 || !ema26) return null;
|
|
|
|
|
|
const macdLine = ema12 - ema26;
|
|
|
|
|
|
return {
|
|
|
value: macdLine,
|
|
|
signal: macdLine > 0 ? 'bullish' : 'bearish'
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateEMA(data, period) {
|
|
|
if (data.length < period) return null;
|
|
|
|
|
|
const k = 2 / (period + 1);
|
|
|
let ema = data[0].close;
|
|
|
|
|
|
for (let i = 1; i < data.length; i++) {
|
|
|
ema = data[i].close * k + ema * (1 - k);
|
|
|
}
|
|
|
|
|
|
return ema;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateIndicatorDisplays() {
|
|
|
|
|
|
const rsiElement = document.getElementById('rsi-value');
|
|
|
if (rsiElement && this.indicators.rsi !== null) {
|
|
|
rsiElement.textContent = this.indicators.rsi.toFixed(2);
|
|
|
|
|
|
|
|
|
if (this.indicators.rsi > 70) {
|
|
|
rsiElement.style.color = '#ef4444';
|
|
|
} else if (this.indicators.rsi < 30) {
|
|
|
rsiElement.style.color = '#22c55e';
|
|
|
} else {
|
|
|
rsiElement.style.color = '#fbbf24';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
const macdElement = document.getElementById('macd-value');
|
|
|
if (macdElement && this.indicators.macd) {
|
|
|
macdElement.textContent = this.indicators.macd.value.toFixed(4);
|
|
|
macdElement.style.color = this.indicators.macd.signal === 'bullish' ? '#22c55e' : '#ef4444';
|
|
|
}
|
|
|
|
|
|
|
|
|
const emaElement = document.getElementById('ema-value');
|
|
|
if (emaElement && this.indicators.ema !== null) {
|
|
|
emaElement.textContent = safeFormatCurrency(this.indicators.ema);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
performAnalysis() {
|
|
|
if (!this.ohlcvData || this.ohlcvData.length === 0) {
|
|
|
console.warn('No data available for analysis');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const resultsContainer = document.getElementById('analysis-results');
|
|
|
if (!resultsContainer) return;
|
|
|
|
|
|
const analysis = this.generateAnalysis();
|
|
|
|
|
|
resultsContainer.innerHTML = `
|
|
|
<div class="analysis-card">
|
|
|
<div class="analysis-header">
|
|
|
<h3>Technical Analysis - ${this.currentSymbol} (${this.currentTimeframe})</h3>
|
|
|
<span class="badge badge-${analysis.signal}">${analysis.signal.toUpperCase()}</span>
|
|
|
</div>
|
|
|
<div class="analysis-body">
|
|
|
<div class="analysis-section">
|
|
|
<h4>Market Trend</h4>
|
|
|
<p class="trend-${analysis.trend.toLowerCase()}">${analysis.trendDescription}</p>
|
|
|
</div>
|
|
|
<div class="analysis-section">
|
|
|
<h4>Key Indicators</h4>
|
|
|
<ul class="indicator-list">
|
|
|
${analysis.indicators.map(ind => `
|
|
|
<li>
|
|
|
<span class="indicator-name">${ind.name}:</span>
|
|
|
<span class="indicator-value ${ind.status}">${ind.value}</span>
|
|
|
<span class="indicator-signal">(${ind.interpretation})</span>
|
|
|
</li>
|
|
|
`).join('')}
|
|
|
</ul>
|
|
|
</div>
|
|
|
<div class="analysis-section">
|
|
|
<h4>Trading Recommendation</h4>
|
|
|
<p class="recommendation">${analysis.recommendation}</p>
|
|
|
</div>
|
|
|
<div class="analysis-section">
|
|
|
<h4>Risk Assessment</h4>
|
|
|
<div class="risk-bar">
|
|
|
<div class="risk-fill risk-${analysis.risk}" style="width: ${analysis.riskScore}%"></div>
|
|
|
</div>
|
|
|
<p class="risk-text">Risk Level: ${analysis.risk.toUpperCase()} (${analysis.riskScore}%)</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateAnalysis() {
|
|
|
const latestCandle = this.ohlcvData[this.ohlcvData.length - 1];
|
|
|
const rsi = this.indicators.rsi;
|
|
|
const macd = this.indicators.macd;
|
|
|
const ema = this.indicators.ema;
|
|
|
|
|
|
|
|
|
let trend = 'neutral';
|
|
|
let trendDescription = 'Market is consolidating';
|
|
|
|
|
|
if (latestCandle.close > ema) {
|
|
|
trend = 'bullish';
|
|
|
trendDescription = 'Price is above EMA - Bullish trend';
|
|
|
} else if (latestCandle.close < ema) {
|
|
|
trend = 'bearish';
|
|
|
trendDescription = 'Price is below EMA - Bearish trend';
|
|
|
}
|
|
|
|
|
|
|
|
|
const indicators = [];
|
|
|
|
|
|
if (rsi !== null) {
|
|
|
let rsiStatus, rsiInterpretation;
|
|
|
if (rsi > 70) {
|
|
|
rsiStatus = 'overbought';
|
|
|
rsiInterpretation = 'Overbought - potential reversal';
|
|
|
} else if (rsi < 30) {
|
|
|
rsiStatus = 'oversold';
|
|
|
rsiInterpretation = 'Oversold - potential bounce';
|
|
|
} else {
|
|
|
rsiStatus = 'neutral';
|
|
|
rsiInterpretation = 'Neutral momentum';
|
|
|
}
|
|
|
indicators.push({
|
|
|
name: 'RSI (14)',
|
|
|
value: rsi.toFixed(2),
|
|
|
status: rsiStatus,
|
|
|
interpretation: rsiInterpretation
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (macd) {
|
|
|
indicators.push({
|
|
|
name: 'MACD',
|
|
|
value: macd.value.toFixed(4),
|
|
|
status: macd.signal,
|
|
|
interpretation: macd.signal === 'bullish' ? 'Bullish crossover' : 'Bearish crossover'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (ema !== null) {
|
|
|
const emaStatus = latestCandle.close > ema ? 'bullish' : 'bearish';
|
|
|
indicators.push({
|
|
|
name: 'EMA (20)',
|
|
|
value: safeFormatCurrency(ema),
|
|
|
status: emaStatus,
|
|
|
interpretation: emaStatus === 'bullish' ? 'Price above EMA' : 'Price below EMA'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
let signal = 'hold';
|
|
|
let recommendation = 'Wait for clearer signals';
|
|
|
|
|
|
const bullishSignals = indicators.filter(i => i.status === 'bullish' || i.status === 'oversold').length;
|
|
|
const bearishSignals = indicators.filter(i => i.status === 'bearish' || i.status === 'overbought').length;
|
|
|
|
|
|
if (bullishSignals > bearishSignals && bullishSignals >= 2) {
|
|
|
signal = 'buy';
|
|
|
recommendation = 'Strong buy signals detected. Consider entering a long position with proper risk management.';
|
|
|
} else if (bearishSignals > bullishSignals && bearishSignals >= 2) {
|
|
|
signal = 'sell';
|
|
|
recommendation = 'Strong sell signals detected. Consider taking profits or shorting with proper risk management.';
|
|
|
}
|
|
|
|
|
|
|
|
|
let riskScore = 50;
|
|
|
let risk = 'medium';
|
|
|
|
|
|
if (rsi !== null) {
|
|
|
if (rsi > 70 || rsi < 30) riskScore += 20;
|
|
|
}
|
|
|
|
|
|
if (trend === 'bullish' && signal === 'buy') {
|
|
|
riskScore -= 10;
|
|
|
} else if (trend === 'bearish' && signal === 'sell') {
|
|
|
riskScore -= 10;
|
|
|
}
|
|
|
|
|
|
riskScore = Math.max(10, Math.min(90, riskScore));
|
|
|
|
|
|
if (riskScore < 40) risk = 'low';
|
|
|
else if (riskScore > 60) risk = 'high';
|
|
|
|
|
|
return {
|
|
|
trend,
|
|
|
trendDescription,
|
|
|
indicators,
|
|
|
signal,
|
|
|
recommendation,
|
|
|
risk,
|
|
|
riskScore
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setupAutoRefresh() {
|
|
|
|
|
|
this.autoRefreshInterval = setInterval(() => {
|
|
|
if (!this.isLoading && !document.hidden) {
|
|
|
this.loadMarketData();
|
|
|
}
|
|
|
}, 30000);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
exportAnalysis() {
|
|
|
const analysis = this.generateAnalysis();
|
|
|
const exportData = {
|
|
|
symbol: this.currentSymbol,
|
|
|
timeframe: this.currentTimeframe,
|
|
|
timestamp: new Date().toISOString(),
|
|
|
dataSource: this.dataSource,
|
|
|
price: this.ohlcvData[this.ohlcvData.length - 1],
|
|
|
indicators: this.indicators,
|
|
|
analysis: analysis
|
|
|
};
|
|
|
|
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const a = document.createElement('a');
|
|
|
a.href = url;
|
|
|
a.download = `${this.currentSymbol}_analysis_${Date.now()}.json`;
|
|
|
a.click();
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
this.showToast('✅ Analysis exported', 'success');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showLoadingState(show) {
|
|
|
const spinner = document.getElementById('loading-spinner');
|
|
|
const analyzeBtn = document.getElementById('analyze-btn');
|
|
|
|
|
|
if (spinner) {
|
|
|
spinner.style.display = show ? 'block' : 'none';
|
|
|
}
|
|
|
if (analyzeBtn) {
|
|
|
analyzeBtn.disabled = show;
|
|
|
analyzeBtn.textContent = show ? 'Loading...' : 'Analyze';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showErrorState(message) {
|
|
|
const resultsContainer = document.getElementById('analysis-results');
|
|
|
if (resultsContainer) {
|
|
|
resultsContainer.innerHTML = `
|
|
|
<div class="error-state">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
|
</svg>
|
|
|
<h3>Unable to Load Data</h3>
|
|
|
<p>${escapeHtml(message)}</p>
|
|
|
<button onclick="location.reload()" class="btn btn-primary">Retry</button>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showToast(message, type = 'info') {
|
|
|
if (typeof Toast !== 'undefined' && Toast.show) {
|
|
|
Toast.show(message, type);
|
|
|
} else {
|
|
|
console.log(`[Toast ${type}]`, message);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
destroy() {
|
|
|
if (this.autoRefreshInterval) {
|
|
|
clearInterval(this.autoRefreshInterval);
|
|
|
}
|
|
|
if (this.chart) {
|
|
|
this.chart.remove();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
let technicalAnalysisInstance = null;
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
|
try {
|
|
|
technicalAnalysisInstance = new TechnicalAnalysisProfessional();
|
|
|
await technicalAnalysisInstance.init();
|
|
|
} catch (error) {
|
|
|
console.error('[TechnicalAnalysis] Fatal error:', error);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
if (technicalAnalysisInstance) {
|
|
|
technicalAnalysisInstance.destroy();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
export { TechnicalAnalysisProfessional };
|
|
|
export default TechnicalAnalysisProfessional;
|
|
|
|
|
|
|