// Bitcoin Trading View Application class TradingView { constructor() { this.symbol = 'BTCUSDT'; this.timeframe = '15m'; this.candleData = []; this.ws = null; this.charts = {}; this.series = {}; this.signals = []; this.gcDcMarkers = []; this.showGCDC = false; this.init(); } async init() { this.setupCharts(); this.setupEventListeners(); await this.loadHistoricalData(); this.connectWebSocket(); } setupCharts() { const chartOptions = { layout: { background: { type: 'solid', color: '#1e222d' }, textColor: '#d1d4dc', }, grid: { vertLines: { color: '#2a2e39' }, horzLines: { color: '#2a2e39' }, }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, }, rightPriceScale: { borderColor: '#2a2e39', scaleMargins: { top: 0.1, bottom: 0.1, }, autoScale: true, }, timeScale: { borderColor: '#2a2e39', timeVisible: true, secondsVisible: false, barSpacing: 8, minBarSpacing: 2, rightOffset: 5, tickMarkFormatter: (timestamp, tickMarkType, locale) => { const date = new Date(timestamp * 1000); const options = { timeZone: 'Asia/Seoul' }; const month = String(date.toLocaleString('ko-KR', { ...options, month: 'numeric' })).replace('월', '').padStart(2, '0'); const day = String(date.toLocaleString('ko-KR', { ...options, day: 'numeric' })).replace('일', '').padStart(2, '0'); const hours = String(date.toLocaleString('ko-KR', { ...options, hour: 'numeric', hour12: false })).replace('시', '').padStart(2, '0'); const minuteStr = String(date.toLocaleString('ko-KR', { ...options, minute: 'numeric' })).replace('분', '').padStart(2, '0'); const minuteNum = parseInt(minuteStr, 10); // For minute-based timeframes, show only 15-minute intervals const isMinuteTimeframe = ['1m', '5m', '15m'].includes(this.timeframe); if (isMinuteTimeframe) { // Show date at midnight (00:00) if (hours === '00' && minuteNum === 0) { return `${month}/${day}`; } // Show date + time at 15-minute intervals if (minuteNum % 15 === 0) { return `${month}/${day} ${hours}:${minuteStr}`; } return ''; } else if (this.timeframe === '1h' || this.timeframe === '4h') { // For hour timeframes, show date + time return `${month}/${day} ${hours}:${minuteStr}`; } else { // For day timeframe, show date only return `${month}/${day}`; } }, }, localization: { timeFormatter: (timestamp) => { const date = new Date(timestamp * 1000); return date.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); }, }, }; // Main Chart (Candlestick + MA + Bollinger Bands) this.charts.main = LightweightCharts.createChart( document.getElementById('main-chart'), { ...chartOptions, height: 450 } ); // Candlestick Series this.series.candle = this.charts.main.addCandlestickSeries({ upColor: '#26a69a', downColor: '#ef5350', borderUpColor: '#26a69a', borderDownColor: '#ef5350', wickUpColor: '#26a69a', wickDownColor: '#ef5350', }); // Volume Series (at bottom of main chart) this.series.volume = this.charts.main.addHistogramSeries({ color: '#26a69a', priceFormat: { type: 'volume', }, priceScaleId: 'volume', scaleMargins: { top: 0.85, bottom: 0, }, }); // Configure volume price scale this.charts.main.priceScale('volume').applyOptions({ scaleMargins: { top: 0.85, bottom: 0, }, }); // Moving Average Series this.series.ma5 = this.charts.main.addLineSeries({ color: '#FFEB3B', lineWidth: 2, title: 'MA5', lastValueVisible: false, priceLineVisible: false }); this.series.ma20 = this.charts.main.addLineSeries({ color: '#1565C0', lineWidth: 2, title: 'MA20', lastValueVisible: false, priceLineVisible: false }); this.series.ma60 = this.charts.main.addLineSeries({ color: '#00E676', lineWidth: 2, title: 'MA60', lastValueVisible: false, priceLineVisible: false }); this.series.ma120 = this.charts.main.addLineSeries({ color: '#FF5252', lineWidth: 2, title: 'MA120', visible: false, lastValueVisible: false, priceLineVisible: false }); this.series.ma200 = this.charts.main.addLineSeries({ color: '#E040FB', lineWidth: 2, title: 'MA200', visible: false, lastValueVisible: false, priceLineVisible: false }); // Bollinger Bands (Upper/Lower only, Middle is same as MA20) this.series.bbUpper = this.charts.main.addLineSeries({ color: '#FF9800', lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, }); this.series.bbLower = this.charts.main.addLineSeries({ color: '#FF9800', lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, }); // Cross markers series this.series.markers = this.series.candle; // MACD Chart const macdOptions = { layout: chartOptions.layout, grid: chartOptions.grid, crosshair: chartOptions.crosshair, rightPriceScale: { ...chartOptions.rightPriceScale, mode: LightweightCharts.PriceScaleMode.Normal, }, timeScale: chartOptions.timeScale, localization: chartOptions.localization, height: 150, }; this.charts.macd = LightweightCharts.createChart( document.getElementById('macd-chart'), macdOptions ); this.series.macdLine = this.charts.macd.addLineSeries({ color: '#2962ff', lineWidth: 2, title: 'MACD', priceScaleId: 'right', }); this.series.signalLine = this.charts.macd.addLineSeries({ color: '#ff6d00', lineWidth: 2, title: 'Signal', priceScaleId: 'right', }); this.series.histogram = this.charts.macd.addHistogramSeries({ color: '#26a69a', title: 'Histogram', priceScaleId: 'right', }); // RSI Chart this.charts.rsi = LightweightCharts.createChart( document.getElementById('rsi-chart'), { ...chartOptions, height: 150 } ); this.series.rsi = this.charts.rsi.addLineSeries({ color: '#FFEB3B', lineWidth: 2, title: 'RSI', }); // RSI Overbought/Oversold lines (more visible) this.series.rsiOverbought = this.charts.rsi.addLineSeries({ color: '#ef5350', lineWidth: 2, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, }); this.series.rsiOversold = this.charts.rsi.addLineSeries({ color: '#26a69a', lineWidth: 2, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, }); // Stochastic Chart this.charts.stochastic = LightweightCharts.createChart( document.getElementById('stochastic-chart'), { ...chartOptions, height: 150 } ); this.series.stochK = this.charts.stochastic.addLineSeries({ color: '#2196f3', lineWidth: 2, title: '%K', }); this.series.stochD = this.charts.stochastic.addLineSeries({ color: '#ff5722', lineWidth: 2, title: '%D', }); // Stochastic Overbought/Oversold lines this.series.stochOverbought = this.charts.stochastic.addLineSeries({ color: 'rgba(239, 83, 80, 0.5)', lineWidth: 1, lineStyle: 2, }); this.series.stochOversold = this.charts.stochastic.addLineSeries({ color: 'rgba(38, 166, 154, 0.5)', lineWidth: 1, lineStyle: 2, }); // Sync all charts this.syncCharts(); } syncCharts() { const charts = [this.charts.main, this.charts.macd, this.charts.rsi, this.charts.stochastic]; let isSyncing = false; charts.forEach((chart, index) => { chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => { if (isSyncing || !logicalRange) return; isSyncing = true; charts.forEach((otherChart, otherIndex) => { if (index !== otherIndex) { otherChart.timeScale().setVisibleLogicalRange(logicalRange); } }); isSyncing = false; }); }); } setupEventListeners() { document.getElementById('timeframe').addEventListener('change', async (e) => { this.timeframe = e.target.value; // Close WebSocket first to prevent data conflicts if (this.ws) { this.ws.close(); this.ws = null; } // Clear all data this.candleData = []; // Clear chart series data to prevent "Cannot update oldest data" error this.series.candle.setData([]); this.series.volume.setData([]); this.series.ma5.setData([]); this.series.ma20.setData([]); this.series.ma60.setData([]); this.series.ma120.setData([]); this.series.ma200.setData([]); this.series.bbUpper.setData([]); this.series.bbLower.setData([]); this.series.macdLine.setData([]); this.series.signalLine.setData([]); this.series.histogram.setData([]); this.series.rsi.setData([]); this.series.rsiOverbought.setData([]); this.series.rsiOversold.setData([]); this.series.stochK.setData([]); this.series.stochD.setData([]); this.series.stochOverbought.setData([]); this.series.stochOversold.setData([]); await this.loadHistoricalData(); this.connectWebSocket(); }); // MA toggle handlers const maToggles = { 'toggle-ma5': 'ma5', 'toggle-ma20': 'ma20', 'toggle-ma60': 'ma60', 'toggle-ma120': 'ma120', 'toggle-ma200': 'ma200', }; Object.entries(maToggles).forEach(([toggleId, seriesName]) => { document.getElementById(toggleId).addEventListener('change', (e) => { this.series[seriesName].applyOptions({ visible: e.target.checked }); }); }); // Bollinger Bands toggle document.getElementById('toggle-bb').addEventListener('change', (e) => { const visible = e.target.checked; this.series.bbUpper.applyOptions({ visible }); this.series.bbLower.applyOptions({ visible }); }); // GC/DC toggle document.getElementById('toggle-gcdc').addEventListener('change', (e) => { this.showGCDC = e.target.checked; this.updateMarkers(); }); // Resize handler window.addEventListener('resize', () => { Object.values(this.charts).forEach(chart => { chart.applyOptions({ width: chart.options().width }); }); }); } async loadHistoricalData() { try { const interval = this.timeframe; const url = `https://api.binance.com/api/v3/klines?symbol=${this.symbol}&interval=${interval}&limit=1000`; const response = await fetch(url); const data = await response.json(); this.candleData = data.map(d => ({ time: Math.floor(d[0] / 1000), open: parseFloat(d[1]), high: parseFloat(d[2]), low: parseFloat(d[3]), close: parseFloat(d[4]), volume: parseFloat(d[5]), })); this.updateAllCharts(); this.updatePriceDisplay(); } catch (error) { console.error('Error loading historical data:', error); } } updateAllCharts() { if (this.candleData.length === 0) return; // Update candlestick this.series.candle.setData(this.candleData); // Update volume const volumeData = this.candleData.map(d => ({ time: d.time, value: d.volume, color: d.close >= d.open ? 'rgba(38, 166, 154, 0.5)' : 'rgba(239, 83, 80, 0.5)' })); this.series.volume.setData(volumeData); // Calculate and update Moving Averages const ma5 = TechnicalIndicators.SMA(this.candleData, 5); const ma20 = TechnicalIndicators.SMA(this.candleData, 20); const ma60 = TechnicalIndicators.SMA(this.candleData, 60); const ma120 = TechnicalIndicators.SMA(this.candleData, 120); const ma200 = TechnicalIndicators.SMA(this.candleData, 200); this.series.ma5.setData(ma5.filter(d => d.value !== null)); this.series.ma20.setData(ma20.filter(d => d.value !== null)); this.series.ma60.setData(ma60.filter(d => d.value !== null)); this.series.ma120.setData(ma120.filter(d => d.value !== null)); this.series.ma200.setData(ma200.filter(d => d.value !== null)); // Calculate and update Bollinger Bands (Middle is same as MA20, so skip it) const bb = TechnicalIndicators.BollingerBands(this.candleData, 20, 2); this.series.bbUpper.setData(bb.upper.filter(d => d.value !== null)); this.series.bbLower.setData(bb.lower.filter(d => d.value !== null)); // Calculate and update MACD const macd = TechnicalIndicators.MACD(this.candleData, 12, 26, 9); // Keep null values as 0 to maintain time alignment this.series.macdLine.setData(macd.macdLine.map(d => ({ time: d.time, value: d.value ?? 0 }))); this.series.signalLine.setData(macd.signalLine.map(d => ({ time: d.time, value: d.value ?? 0 }))); this.series.histogram.setData(macd.histogram.map(d => ({ time: d.time, value: d.value ?? 0, color: d.color }))); // Calculate and update RSI const rsi = TechnicalIndicators.RSI(this.candleData, 14); this.series.rsi.setData(rsi.filter(d => d.value !== null)); // RSI reference lines const rsiLineData = this.candleData.map(d => ({ time: d.time })); this.series.rsiOverbought.setData(rsiLineData.map(d => ({ ...d, value: 70 }))); this.series.rsiOversold.setData(rsiLineData.map(d => ({ ...d, value: 30 }))); // Calculate and update Stochastic const stochastic = TechnicalIndicators.Stochastic(this.candleData, 14, 3, 3); this.series.stochK.setData(stochastic.k.filter(d => d.value !== null)); this.series.stochD.setData(stochastic.d.filter(d => d.value !== null)); // Stochastic reference lines this.series.stochOverbought.setData(rsiLineData.map(d => ({ ...d, value: 80 }))); this.series.stochOversold.setData(rsiLineData.map(d => ({ ...d, value: 20 }))); // Detect and display signals this.detectSignals(ma5, ma20, ma60, rsi, macd, stochastic); // Update analysis panel this.updateAnalysis(ma5, ma20, ma60, bb, macd, rsi, stochastic); // First fit content, then set visible range after a short delay Object.values(this.charts).forEach(chart => { chart.timeScale().fitContent(); }); // Show last 150 candles by default after rendering setTimeout(() => this.setVisibleCandles(150), 100); } setVisibleCandles(count) { if (this.candleData.length > 0) { const lastIndex = this.candleData.length - 1; const fromIndex = Math.max(0, lastIndex - count); Object.values(this.charts).forEach(chart => { chart.timeScale().setVisibleLogicalRange({ from: fromIndex, to: lastIndex + 10, }); }); } } detectSignals(ma5, ma20, ma60, rsi, macd, stochastic) { const allSignals = []; const markers = []; // MA Cross signals (5-20, 20-60) const ma5_20Cross = TechnicalIndicators.detectCrosses(ma5, ma20); const ma20_60Cross = TechnicalIndicators.detectCrosses(ma20, ma60); ma5_20Cross.forEach(signal => { allSignals.push({ ...signal, description: signal.type === 'golden' ? 'MA5-20 Golden Cross' : 'MA5-20 Dead Cross' }); markers.push({ time: signal.time, position: signal.type === 'golden' ? 'belowBar' : 'aboveBar', color: signal.type === 'golden' ? '#26a69a' : '#ef5350', shape: signal.type === 'golden' ? 'arrowUp' : 'arrowDown', text: signal.type === 'golden' ? 'GC' : 'DC' }); }); ma20_60Cross.forEach(signal => { allSignals.push({ ...signal, description: signal.type === 'golden' ? 'MA20-60 Golden Cross' : 'MA20-60 Dead Cross' }); markers.push({ time: signal.time, position: signal.type === 'golden' ? 'belowBar' : 'aboveBar', color: signal.type === 'golden' ? '#26a69a' : '#ef5350', shape: signal.type === 'golden' ? 'arrowUp' : 'arrowDown', text: signal.type === 'golden' ? 'GC20' : 'DC20' }); }); // RSI signals const rsiSignals = TechnicalIndicators.detectRSISignals(rsi); rsiSignals.forEach(signal => { allSignals.push(signal); }); // MACD signals const macdSignals = TechnicalIndicators.detectMACDSignals(macd); macdSignals.forEach(signal => { allSignals.push(signal); }); // Stochastic signals const stochSignals = TechnicalIndicators.detectStochasticSignals(stochastic); stochSignals.forEach(signal => { allSignals.push(signal); }); // Store markers and update display this.gcDcMarkers = markers.sort((a, b) => a.time - b.time); this.updateMarkers(); // Update signal list (last 20 signals) this.signals = allSignals.sort((a, b) => b.time - a.time).slice(0, 20); this.updateSignalList(); } updateMarkers() { if (this.showGCDC) { this.series.candle.setMarkers(this.gcDcMarkers); } else { this.series.candle.setMarkers([]); } } updateSignalList() { // Signal list panel was removed, so this function is now a no-op // Keeping for potential future use } updatePriceDisplay() { if (this.candleData.length === 0) return; const currentCandle = this.candleData[this.candleData.length - 1]; const prevCandle = this.candleData[this.candleData.length - 2]; const priceEl = document.getElementById('current-price'); const changeEl = document.getElementById('price-change'); priceEl.textContent = `$${currentCandle.close.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; if (prevCandle) { const change = currentCandle.close - prevCandle.close; const changePercent = (change / prevCandle.close) * 100; const isUp = change >= 0; changeEl.textContent = `${isUp ? '+' : ''}${change.toFixed(2)} (${isUp ? '+' : ''}${changePercent.toFixed(2)}%)`; changeEl.className = isUp ? 'up' : 'down'; } } connectWebSocket() { const wsUrl = `wss://stream.binance.com:9443/ws/${this.symbol.toLowerCase()}@kline_${this.timeframe}`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.k) { this.handleKlineData(data.k); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; this.ws.onclose = () => { console.log('WebSocket disconnected.'); // Only auto-reconnect if ws is still set (not manually closed) if (this.ws) { console.log('Reconnecting in 5 seconds...'); setTimeout(() => this.connectWebSocket(), 5000); } }; } handleKlineData(kline) { // Validate that candle data exists (prevents errors during timeframe switch) if (this.candleData.length === 0) { return; } const candle = { time: Math.floor(kline.t / 1000), open: parseFloat(kline.o), high: parseFloat(kline.h), low: parseFloat(kline.l), close: parseFloat(kline.c), volume: parseFloat(kline.v), }; // Check if this is an update to existing candle or new candle const lastCandle = this.candleData[this.candleData.length - 1]; // Validate candle time - reject if older than our last candle (stale data from previous timeframe) if (lastCandle && candle.time < lastCandle.time) { console.log('Ignoring stale candle data:', candle.time, 'vs', lastCandle.time); return; } const isNewCandle = !lastCandle || lastCandle.time !== candle.time; if (lastCandle && lastCandle.time === candle.time) { // Update existing candle this.candleData[this.candleData.length - 1] = candle; } else { // New candle started - update analysis with previous candle data before adding new one this.candleData.push(candle); // Keep only last 500 candles if (this.candleData.length > 500) { this.candleData.shift(); } } // Update chart with new data try { this.series.candle.update(candle); // Update volume this.series.volume.update({ time: candle.time, value: candle.volume, color: candle.close >= candle.open ? 'rgba(38, 166, 154, 0.5)' : 'rgba(239, 83, 80, 0.5)' }); } catch (error) { console.warn('Chart update error (may occur during timeframe switch):', error.message); return; } // Update indicators (chart lines) in real-time // Update analysis panel when a new candle starts (previous candle is complete) this.updateIndicatorsRealtime(isNewCandle); this.updatePriceDisplay(); } updateIndicatorsRealtime(updateAnalysisPanel = false) { // Recalculate all indicators with updated data // For performance, we could optimize to only update the last few values const ma5 = TechnicalIndicators.SMA(this.candleData, 5); const ma20 = TechnicalIndicators.SMA(this.candleData, 20); const ma60 = TechnicalIndicators.SMA(this.candleData, 60); const ma120 = TechnicalIndicators.SMA(this.candleData, 120); const ma200 = TechnicalIndicators.SMA(this.candleData, 200); // Update only the last point for real-time const lastIdx = this.candleData.length - 1; if (ma5[lastIdx]?.value) this.series.ma5.update(ma5[lastIdx]); if (ma20[lastIdx]?.value) this.series.ma20.update(ma20[lastIdx]); if (ma60[lastIdx]?.value) this.series.ma60.update(ma60[lastIdx]); if (ma120[lastIdx]?.value) this.series.ma120.update(ma120[lastIdx]); if (ma200[lastIdx]?.value) this.series.ma200.update(ma200[lastIdx]); // Bollinger Bands (Middle is same as MA20, so skip it) const bb = TechnicalIndicators.BollingerBands(this.candleData, 20, 2); if (bb.upper[lastIdx]?.value) this.series.bbUpper.update(bb.upper[lastIdx]); if (bb.lower[lastIdx]?.value) this.series.bbLower.update(bb.lower[lastIdx]); // MACD const macd = TechnicalIndicators.MACD(this.candleData, 12, 26, 9); if (macd.macdLine[lastIdx]?.value) this.series.macdLine.update(macd.macdLine[lastIdx]); if (macd.signalLine[lastIdx]?.value) this.series.signalLine.update(macd.signalLine[lastIdx]); if (macd.histogram[lastIdx]) this.series.histogram.update(macd.histogram[lastIdx]); // RSI const rsi = TechnicalIndicators.RSI(this.candleData, 14); if (rsi[lastIdx]?.value) this.series.rsi.update(rsi[lastIdx]); // Stochastic const stochastic = TechnicalIndicators.Stochastic(this.candleData, 14, 3, 3); if (stochastic.k[lastIdx]?.value) this.series.stochK.update(stochastic.k[lastIdx]); if (stochastic.d[lastIdx]?.value) this.series.stochD.update(stochastic.d[lastIdx]); // Update analysis panel only when candle is closed (completed) if (updateAnalysisPanel) { this.updateAnalysis(ma5, ma20, ma60, bb, macd, rsi, stochastic); } } updateAnalysis(ma5, ma20, ma60, bb, macd, rsi, stochastic) { // Use the last candle for analysis const lastIdx = this.candleData.length - 1; if (lastIdx < 0) return; const lastCandle = this.candleData[lastIdx]; const analyzedPrice = lastCandle.close; let totalScore = 0; let analysisCount = 0; // Update time to show the analyzed candle's time const candleTime = new Date(lastCandle.time * 1000); const timeStr = candleTime.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); document.getElementById('analysis-time').textContent = timeStr; // 1. MA Trend Analysis const ma5Val = ma5[lastIdx]?.value; const ma20Val = ma20[lastIdx]?.value; const ma60Val = ma60[lastIdx]?.value; let maSignal = 'neutral'; let maDetail = ''; let maScore = 0; if (ma5Val && ma20Val && ma60Val) { if (analyzedPrice > ma5Val && ma5Val > ma20Val && ma20Val > ma60Val) { maSignal = 'buy'; maDetail = 'Strong uptrend'; maScore = 2; } else if (analyzedPrice > ma5Val && ma5Val > ma20Val) { maSignal = 'buy'; maDetail = 'Uptrend'; maScore = 1; } else if (analyzedPrice < ma5Val && ma5Val < ma20Val && ma20Val < ma60Val) { maSignal = 'sell'; maDetail = 'Strong downtrend'; maScore = -2; } else if (analyzedPrice < ma5Val && ma5Val < ma20Val) { maSignal = 'sell'; maDetail = 'Downtrend'; maScore = -1; } else { maDetail = 'Consolidating'; } totalScore += maScore; analysisCount++; } this.updateAnalysisItem('analysis-ma', maSignal, maSignal === 'buy' ? 'BUY' : maSignal === 'sell' ? 'SELL' : 'HOLD', maDetail); // 2. Bollinger Bands Analysis const bbUpper = bb.upper[lastIdx]?.value; const bbLower = bb.lower[lastIdx]?.value; let bbSignal = 'neutral'; let bbDetail = ''; let bbScore = 0; if (bbUpper && bbLower) { const bbMiddle = ma20Val; const bbWidth = ((bbUpper - bbLower) / bbMiddle * 100).toFixed(1); if (analyzedPrice >= bbUpper) { bbSignal = 'sell'; bbDetail = `Overbought (${bbWidth}%)`; bbScore = -1; } else if (analyzedPrice <= bbLower) { bbSignal = 'buy'; bbDetail = `Oversold (${bbWidth}%)`; bbScore = 1; } else if (analyzedPrice > bbMiddle) { bbDetail = `Upper zone (${bbWidth}%)`; } else { bbDetail = `Lower zone (${bbWidth}%)`; } totalScore += bbScore; analysisCount++; } this.updateAnalysisItem('analysis-bb', bbSignal, bbSignal === 'buy' ? 'BUY' : bbSignal === 'sell' ? 'SELL' : 'HOLD', bbDetail); // 3. MACD Analysis const macdVal = macd.macdLine[lastIdx]?.value; const signalVal = macd.signalLine[lastIdx]?.value; const histVal = macd.histogram[lastIdx]?.value; let macdSignal = 'neutral'; let macdDetail = ''; let macdScore = 0; if (macdVal !== null && signalVal !== null) { const prevHistVal = macd.histogram[lastIdx - 1]?.value ?? 0; if (macdVal > signalVal && histVal > 0) { macdSignal = 'buy'; macdDetail = histVal > prevHistVal ? 'Bullish momentum+' : 'Bullish'; macdScore = histVal > prevHistVal ? 2 : 1; } else if (macdVal < signalVal && histVal < 0) { macdSignal = 'sell'; macdDetail = histVal < prevHistVal ? 'Bearish momentum+' : 'Bearish'; macdScore = histVal < prevHistVal ? -2 : -1; } else { macdDetail = 'Crossing'; } totalScore += macdScore; analysisCount++; } this.updateAnalysisItem('analysis-macd', macdSignal, macdSignal === 'buy' ? 'BUY' : macdSignal === 'sell' ? 'SELL' : 'HOLD', macdDetail); // 4. RSI Analysis const rsiVal = rsi[lastIdx]?.value; let rsiSignal = 'neutral'; let rsiDetail = ''; let rsiScore = 0; if (rsiVal !== null) { if (rsiVal >= 70) { rsiSignal = 'sell'; rsiDetail = `Overbought (${rsiVal.toFixed(1)})`; rsiScore = -1; } else if (rsiVal <= 30) { rsiSignal = 'buy'; rsiDetail = `Oversold (${rsiVal.toFixed(1)})`; rsiScore = 1; } else if (rsiVal >= 50) { rsiDetail = `Bullish (${rsiVal.toFixed(1)})`; rsiScore = 0.5; } else { rsiDetail = `Bearish (${rsiVal.toFixed(1)})`; rsiScore = -0.5; } totalScore += rsiScore; analysisCount++; } this.updateAnalysisItem('analysis-rsi', rsiSignal, rsiSignal === 'buy' ? 'BUY' : rsiSignal === 'sell' ? 'SELL' : 'HOLD', rsiDetail); // 5. Stochastic Analysis const stochKVal = stochastic.k[lastIdx]?.value; const stochDVal = stochastic.d[lastIdx]?.value; let stochSignal = 'neutral'; let stochDetail = ''; let stochScore = 0; if (stochKVal !== null && stochDVal !== null) { if (stochKVal >= 80) { stochSignal = 'sell'; stochDetail = `Overbought (${stochKVal.toFixed(1)})`; stochScore = -1; } else if (stochKVal <= 20) { stochSignal = 'buy'; stochDetail = `Oversold (${stochKVal.toFixed(1)})`; stochScore = 1; } else if (stochKVal > stochDVal) { stochDetail = `Bullish (${stochKVal.toFixed(1)})`; stochScore = 0.5; } else { stochDetail = `Bearish (${stochKVal.toFixed(1)})`; stochScore = -0.5; } totalScore += stochScore; analysisCount++; } this.updateAnalysisItem('analysis-stoch', stochSignal, stochSignal === 'buy' ? 'BUY' : stochSignal === 'sell' ? 'SELL' : 'HOLD', stochDetail); // Calculate overall signal const avgScore = analysisCount > 0 ? totalScore / analysisCount : 0; let overallSignal = 'NEUTRAL'; let overallClass = 'neutral'; if (avgScore >= 1) { overallSignal = 'STRONG BUY'; overallClass = 'buy'; } else if (avgScore >= 0.3) { overallSignal = 'BUY'; overallClass = 'buy'; } else if (avgScore <= -1) { overallSignal = 'STRONG SELL'; overallClass = 'sell'; } else if (avgScore <= -0.3) { overallSignal = 'SELL'; overallClass = 'sell'; } const overallEl = document.querySelector('#overall-signal .signal-value'); overallEl.textContent = overallSignal; overallEl.className = `signal-value ${overallClass}`; // Update score marker (normalize to 0-100 scale, where 50 is neutral) const normalizedScore = Math.max(0, Math.min(100, 50 + avgScore * 25)); document.getElementById('score-marker').style.left = `${normalizedScore}%`; // Update summary text this.updateSummaryText(avgScore, maSignal, bbSignal, macdSignal, rsiSignal, stochSignal); // Update Korean analysis this.updateKoreanAnalysis(avgScore, maDetail, bbDetail, macdDetail, rsiDetail, stochDetail, rsiVal, stochKVal); } updateAnalysisItem(elementId, signal, value, detail) { const item = document.getElementById(elementId); const valueEl = item.querySelector('.item-value'); const detailEl = item.querySelector('.item-detail'); valueEl.textContent = value; valueEl.className = `item-value ${signal}`; detailEl.textContent = detail; } updateSummaryText(avgScore, ma, bb, macd, rsi, stoch) { let summary = ''; const buySignals = [ma, bb, macd, rsi, stoch].filter(s => s === 'buy').length; const sellSignals = [ma, bb, macd, rsi, stoch].filter(s => s === 'sell').length; if (avgScore >= 1) { summary = `Strong bullish momentum. ${buySignals}/5 indicators show buy signals. Consider long positions with proper risk management.`; } else if (avgScore >= 0.3) { summary = `Moderate bullish bias. ${buySignals}/5 buy signals detected. Market shows upward tendency.`; } else if (avgScore <= -1) { summary = `Strong bearish momentum. ${sellSignals}/5 indicators show sell signals. Consider short positions or wait for reversal.`; } else if (avgScore <= -0.3) { summary = `Moderate bearish bias. ${sellSignals}/5 sell signals detected. Market shows downward tendency.`; } else { summary = `Market is consolidating. Mixed signals from indicators. Wait for clearer direction before entering positions.`; } document.getElementById('analysis-summary-text').textContent = summary; } updateKoreanAnalysis(avgScore, maDetail, bbDetail, macdDetail, rsiDetail, stochDetail, rsiVal, stochKVal) { const parts = []; // 전체 시장 상황 if (avgScore >= 1) { parts.push('현재 시장은 강한 상승 추세입니다.'); } else if (avgScore >= 0.3) { parts.push('현재 시장은 상승 추세입니다.'); } else if (avgScore <= -1) { parts.push('현재 시장은 강한 하락 추세입니다.'); } else if (avgScore <= -0.3) { parts.push('현재 시장은 하락 추세입니다.'); } else { parts.push('현재 시장은 횡보 구간입니다.'); } // 이동평균선 분석 if (maDetail.includes('Strong uptrend')) { parts.push('이동평균선이 정배열로 강한 매수세를 보이고 있습니다.'); } else if (maDetail.includes('Uptrend')) { parts.push('단기 이동평균선이 상승 추세를 나타내고 있습니다.'); } else if (maDetail.includes('Strong downtrend')) { parts.push('이동평균선이 역배열로 강한 매도세를 보이고 있습니다.'); } else if (maDetail.includes('Downtrend')) { parts.push('단기 이동평균선이 하락 추세를 나타내고 있습니다.'); } else if (maDetail.includes('Consolidating')) { parts.push('이동평균선이 수렴하며 방향성을 찾는 중입니다.'); } // 볼린저 밴드 분석 if (bbDetail.includes('Overbought')) { parts.push('가격이 볼린저 밴드 상단에 위치하여 과매수 구간입니다.'); } else if (bbDetail.includes('Oversold')) { parts.push('가격이 볼린저 밴드 하단에 위치하여 과매도 구간입니다.'); } // RSI 분석 if (rsiVal !== null) { if (rsiVal >= 70) { parts.push(`RSI가 ${rsiVal.toFixed(0)}으로 과매수 상태이며, 조정 가능성이 있습니다.`); } else if (rsiVal <= 30) { parts.push(`RSI가 ${rsiVal.toFixed(0)}으로 과매도 상태이며, 반등 가능성이 있습니다.`); } else if (rsiVal >= 60) { parts.push(`RSI가 ${rsiVal.toFixed(0)}으로 매수세가 우세합니다.`); } else if (rsiVal <= 40) { parts.push(`RSI가 ${rsiVal.toFixed(0)}으로 매도세가 우세합니다.`); } } // MACD 분석 if (macdDetail.includes('Bullish momentum+')) { parts.push('MACD 히스토그램이 증가하며 상승 모멘텀이 강화되고 있습니다.'); } else if (macdDetail.includes('Bullish')) { parts.push('MACD가 시그널선 위에서 상승 신호를 유지하고 있습니다.'); } else if (macdDetail.includes('Bearish momentum+')) { parts.push('MACD 히스토그램이 감소하며 하락 모멘텀이 강화되고 있습니다.'); } else if (macdDetail.includes('Bearish')) { parts.push('MACD가 시그널선 아래에서 하락 신호를 유지하고 있습니다.'); } else if (macdDetail.includes('Crossing')) { parts.push('MACD와 시그널선이 교차 중으로 추세 전환 가능성이 있습니다.'); } // 스토캐스틱 분석 if (stochKVal !== null) { if (stochKVal >= 80) { parts.push('스토캐스틱이 과매수 구간에 진입했습니다.'); } else if (stochKVal <= 20) { parts.push('스토캐스틱이 과매도 구간에 진입했습니다.'); } } // 투자 제안 if (avgScore >= 1) { parts.push('매수 포지션 진입을 고려해볼 수 있으나, 리스크 관리에 유의하세요.'); } else if (avgScore >= 0.3) { parts.push('상승 가능성이 높으나 추가 확인 후 진입을 권장합니다.'); } else if (avgScore <= -1) { parts.push('매도 또는 관망을 권장하며, 반등 신호 확인이 필요합니다.'); } else if (avgScore <= -0.3) { parts.push('하락 가능성이 있으니 신규 매수는 신중하게 접근하세요.'); } else { parts.push('명확한 방향성이 나올 때까지 관망을 권장합니다.'); } document.getElementById('korean-analysis-text').textContent = parts.join(' '); } reconnectWebSocket() { if (this.ws) { this.ws.close(); } this.connectWebSocket(); } } // Initialize the application document.addEventListener('DOMContentLoaded', () => { window.tradingView = new TradingView(); });