Files
bini-trading-view/app.js
kihong.kim dc8d2f4407 Calculate price change based on KST 09:00 daily open
- Add loadDailyOpenPrice() to fetch daily candle open price
- Change price change calculation from previous candle to KST 09:00 basis
- Binance daily candle starts at UTC 00:00 (= KST 09:00)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 11:07:54 +09:00

1064 lines
42 KiB
JavaScript

// 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.dailyOpenPrice = null; // KST 09:00 기준 시가
this.dailyOpenTime = null; // 기준 시간
this.init();
}
async init() {
this.setupCharts();
this.setupEventListeners();
await this.loadDailyOpenPrice();
await this.loadHistoricalData();
this.connectWebSocket();
}
// KST 오전 9시 기준 시가를 가져오는 함수
async loadDailyOpenPrice() {
try {
// 현재 KST 시간 계산
const now = new Date();
const kstOffset = 9 * 60 * 60 * 1000; // UTC+9
const kstNow = new Date(now.getTime() + kstOffset);
// 오늘 KST 09:00 시간 계산
const kstToday9am = new Date(kstNow);
kstToday9am.setUTCHours(0, 0, 0, 0); // KST 09:00 = UTC 00:00
// 현재 시간이 KST 09:00 이전이면 어제 09:00 기준
if (kstNow.getUTCHours() < 0 || (kstNow.getUTCHours() === 0 && kstNow.getUTCMinutes() === 0)) {
kstToday9am.setUTCDate(kstToday9am.getUTCDate() - 1);
}
// UTC 기준으로 변환 (KST 09:00 = UTC 00:00)
const startTime = kstToday9am.getTime() - kstOffset;
// 바이낸스 1일봉 데이터 가져오기 (limit=2로 오늘과 어제)
const url = `https://api.binance.com/api/v3/klines?symbol=${this.symbol}&interval=1d&limit=2`;
const response = await fetch(url);
const data = await response.json();
if (data && data.length > 0) {
// 가장 최근 1일봉의 시가 (UTC 00:00 = KST 09:00 기준)
const latestCandle = data[data.length - 1];
this.dailyOpenPrice = parseFloat(latestCandle[1]); // Open price
this.dailyOpenTime = new Date(latestCandle[0]); // Open time (UTC)
console.log('Daily open price (KST 09:00):', this.dailyOpenPrice);
}
} catch (error) {
console.error('Error loading daily open price:', error);
}
}
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 priceEl = document.getElementById('current-price');
const changeEl = document.getElementById('price-change');
priceEl.textContent = `$${currentCandle.close.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}`;
// KST 09:00 기준 변동률 계산
if (this.dailyOpenPrice) {
const change = currentCandle.close - this.dailyOpenPrice;
const changePercent = (change / this.dailyOpenPrice) * 100;
const isUp = change >= 0;
changeEl.textContent = `${isUp ? '+' : ''}${change.toFixed(2)} (${isUp ? '+' : ''}${changePercent.toFixed(2)}%)`;
changeEl.className = isUp ? 'up' : 'down';
changeEl.title = 'KST 09:00 기준 변동률';
}
}
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();
});