|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TradingPro {
|
|
|
constructor() {
|
|
|
this.symbol = 'BTCUSDT';
|
|
|
this.timeframe = '4h';
|
|
|
this.chart = null;
|
|
|
this.candlestickSeries = null;
|
|
|
this.volumeSeries = null;
|
|
|
this.indicators = {
|
|
|
rsi: { enabled: true, series: null },
|
|
|
macd: { enabled: true, series: null },
|
|
|
bb: { enabled: false, upper: null, lower: null, middle: null },
|
|
|
ema: { enabled: true, ema20: null, ema50: null, ema200: null },
|
|
|
volume: { enabled: true, series: null },
|
|
|
ichimoku: { enabled: false, series: [] }
|
|
|
};
|
|
|
this.patterns = {
|
|
|
hs: true,
|
|
|
double: true,
|
|
|
triangle: true,
|
|
|
wedge: false
|
|
|
};
|
|
|
this.drawings = [];
|
|
|
this.currentTool = null;
|
|
|
this.data = [];
|
|
|
this.updateInterval = null;
|
|
|
}
|
|
|
|
|
|
async init() {
|
|
|
try {
|
|
|
console.log('[TradingPro] Initializing Professional Trading Terminal...');
|
|
|
|
|
|
this.initChart();
|
|
|
this.bindEvents();
|
|
|
await this.loadData();
|
|
|
|
|
|
|
|
|
this.updateInterval = setInterval(() => this.loadData(true), 30000);
|
|
|
|
|
|
console.log('[TradingPro] Ready!');
|
|
|
} catch (error) {
|
|
|
console.error('[TradingPro] Init error:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
initChart() {
|
|
|
const container = document.getElementById('tradingChart');
|
|
|
if (!container) {
|
|
|
console.error('[TradingPro] Chart container not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
this.chart = LightweightCharts.createChart(container, {
|
|
|
layout: {
|
|
|
background: { color: '#0f1429' },
|
|
|
textColor: '#d1d4dc',
|
|
|
},
|
|
|
grid: {
|
|
|
vertLines: { color: 'rgba(255, 255, 255, 0.05)' },
|
|
|
horzLines: { color: 'rgba(255, 255, 255, 0.05)' },
|
|
|
},
|
|
|
crosshair: {
|
|
|
mode: LightweightCharts.CrosshairMode.Normal,
|
|
|
vertLine: {
|
|
|
color: '#2dd4bf',
|
|
|
width: 1,
|
|
|
style: LightweightCharts.LineStyle.Dashed,
|
|
|
},
|
|
|
horzLine: {
|
|
|
color: '#2dd4bf',
|
|
|
width: 1,
|
|
|
style: LightweightCharts.LineStyle.Dashed,
|
|
|
},
|
|
|
},
|
|
|
rightPriceScale: {
|
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
|
},
|
|
|
timeScale: {
|
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
|
timeVisible: true,
|
|
|
secondsVisible: false,
|
|
|
},
|
|
|
watermark: {
|
|
|
visible: true,
|
|
|
fontSize: 48,
|
|
|
horzAlign: 'center',
|
|
|
vertAlign: 'center',
|
|
|
color: 'rgba(255, 255, 255, 0.03)',
|
|
|
text: 'CRYPTO PRO',
|
|
|
},
|
|
|
});
|
|
|
|
|
|
|
|
|
this.candlestickSeries = this.chart.addCandlestickSeries({
|
|
|
upColor: '#22c55e',
|
|
|
downColor: '#ef4444',
|
|
|
borderUpColor: '#22c55e',
|
|
|
borderDownColor: '#ef4444',
|
|
|
wickUpColor: '#22c55e',
|
|
|
wickDownColor: '#ef4444',
|
|
|
});
|
|
|
|
|
|
|
|
|
const resizeObserver = new ResizeObserver(entries => {
|
|
|
if (entries.length === 0 || !entries[0].target) return;
|
|
|
const { width, height } = entries[0].contentRect;
|
|
|
this.chart.applyOptions({ width, height });
|
|
|
});
|
|
|
|
|
|
resizeObserver.observe(container);
|
|
|
|
|
|
console.log('[TradingPro] Chart initialized');
|
|
|
}
|
|
|
|
|
|
bindEvents() {
|
|
|
|
|
|
document.getElementById('symbolInput')?.addEventListener('change', (e) => {
|
|
|
this.symbol = e.target.value.toUpperCase();
|
|
|
this.loadData();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.timeframe-btn').forEach(btn => {
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
document.querySelectorAll('.timeframe-btn').forEach(b => b.classList.remove('active'));
|
|
|
e.target.classList.add('active');
|
|
|
this.timeframe = e.target.dataset.timeframe;
|
|
|
this.loadData();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.tool-btn').forEach(btn => {
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
|
|
|
e.currentTarget.classList.add('active');
|
|
|
this.currentTool = e.currentTarget.dataset.tool;
|
|
|
this.activateDrawingTool(this.currentTool);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.toggle-switch[data-indicator]').forEach(toggle => {
|
|
|
toggle.addEventListener('click', (e) => {
|
|
|
const indicator = e.currentTarget.dataset.indicator;
|
|
|
const isOn = toggle.classList.toggle('on');
|
|
|
this.indicators[indicator].enabled = isOn;
|
|
|
this.updateIndicators();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.toggle-switch[data-pattern]').forEach(toggle => {
|
|
|
toggle.addEventListener('click', (e) => {
|
|
|
const pattern = e.currentTarget.dataset.pattern;
|
|
|
const isOn = toggle.classList.toggle('on');
|
|
|
this.patterns[pattern] = isOn;
|
|
|
this.detectPatterns();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.strategy-tab').forEach(tab => {
|
|
|
tab.addEventListener('click', (e) => {
|
|
|
document.querySelectorAll('.strategy-tab').forEach(t => t.classList.remove('active'));
|
|
|
e.target.classList.add('active');
|
|
|
const tabType = e.target.dataset.tab;
|
|
|
this.loadStrategyTab(tabType);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.strategy-item').forEach(item => {
|
|
|
item.addEventListener('click', (e) => {
|
|
|
document.querySelectorAll('.strategy-item').forEach(i => i.classList.remove('active'));
|
|
|
e.currentTarget.classList.add('active');
|
|
|
this.applyStrategy(e.currentTarget);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async loadData(silent = false) {
|
|
|
if (!silent) {
|
|
|
document.getElementById('loadingOverlay')?.classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const intervalMap = {
|
|
|
'1m': '1m', '5m': '5m', '15m': '15m',
|
|
|
'1h': '1h', '4h': '4h',
|
|
|
'1d': '1d', '1w': '1w'
|
|
|
};
|
|
|
|
|
|
const interval = intervalMap[this.timeframe] || '4h';
|
|
|
const symbol = this.symbol.replace('USDT', '').toLowerCase();
|
|
|
|
|
|
|
|
|
let response;
|
|
|
try {
|
|
|
response = await fetch(`/api/ohlcv?symbol=${encodeURIComponent(symbol)}&timeframe=${encodeURIComponent(interval)}&limit=500`, {
|
|
|
signal: AbortSignal.timeout(10000)
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const backendData = await response.json();
|
|
|
|
|
|
|
|
|
if (!backendData || typeof backendData !== 'object') {
|
|
|
throw new Error('Invalid response format');
|
|
|
}
|
|
|
|
|
|
|
|
|
if (backendData.success === false || backendData.error === true) {
|
|
|
throw new Error(backendData.message || 'Failed to fetch OHLCV data');
|
|
|
}
|
|
|
|
|
|
|
|
|
const ohlcvData = backendData.data || backendData.ohlcv || [];
|
|
|
if (!Array.isArray(ohlcvData) || ohlcvData.length === 0) {
|
|
|
throw new Error('No OHLCV data available');
|
|
|
}
|
|
|
|
|
|
this.data = this.parseBackendData(ohlcvData);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.warn('[TradingPro] Backend fetch failed, trying Binance directly:', error);
|
|
|
|
|
|
|
|
|
try {
|
|
|
response = await fetch(
|
|
|
`https://api.binance.com/api/v3/klines?symbol=${this.symbol}&interval=${interval}&limit=500`,
|
|
|
{ signal: AbortSignal.timeout(10000) }
|
|
|
);
|
|
|
|
|
|
if (response.ok) {
|
|
|
const binanceData = await response.json();
|
|
|
this.data = this.parseBinanceData(binanceData);
|
|
|
} else {
|
|
|
throw new Error(`Binance API returned ${response.status}`);
|
|
|
}
|
|
|
} catch (binanceError) {
|
|
|
console.error('[TradingPro] All data sources failed:', binanceError);
|
|
|
this.data = [];
|
|
|
this.showError('Unable to load chart data. Please try again later.');
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!this.data || this.data.length === 0) {
|
|
|
this.showError('No data available for this symbol');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
const firstCandle = this.data[0];
|
|
|
if (!firstCandle || typeof firstCandle.open !== 'number' || typeof firstCandle.close !== 'number') {
|
|
|
this.showError('Invalid data format received');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.updateChart();
|
|
|
this.calculateIndicators();
|
|
|
this.detectPatterns();
|
|
|
this.updatePriceDisplay();
|
|
|
this.updateAnalysis();
|
|
|
this.updateTimestamp();
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('[TradingPro] Load data error:', error);
|
|
|
this.showError('Failed to load chart data');
|
|
|
} finally {
|
|
|
if (!silent) {
|
|
|
document.getElementById('loadingOverlay')?.classList.add('hidden');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
parseBinanceData(data) {
|
|
|
return data.map(candle => ({
|
|
|
time: Math.floor(candle[0] / 1000),
|
|
|
open: parseFloat(candle[1]),
|
|
|
high: parseFloat(candle[2]),
|
|
|
low: parseFloat(candle[3]),
|
|
|
close: parseFloat(candle[4]),
|
|
|
volume: parseFloat(candle[5])
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
parseBackendData(data) {
|
|
|
|
|
|
const ohlcvData = Array.isArray(data) ? data : (data.data || data.ohlcv || []);
|
|
|
if (!Array.isArray(ohlcvData)) return [];
|
|
|
|
|
|
return ohlcvData.map(candle => {
|
|
|
|
|
|
let timestamp = candle.t || candle.time || candle.timestamp || 0;
|
|
|
|
|
|
if (timestamp > 1e10) timestamp = Math.floor(timestamp / 1000);
|
|
|
|
|
|
return {
|
|
|
time: timestamp,
|
|
|
open: parseFloat(candle.o || candle.open || 0),
|
|
|
high: parseFloat(candle.h || candle.high || 0),
|
|
|
low: parseFloat(candle.l || candle.low || 0),
|
|
|
close: parseFloat(candle.c || candle.close || 0),
|
|
|
volume: parseFloat(candle.v || candle.volume || 0)
|
|
|
};
|
|
|
}).filter(candle => candle.time > 0 && candle.open > 0);
|
|
|
}
|
|
|
|
|
|
updateChart() {
|
|
|
if (!this.candlestickSeries) {
|
|
|
console.warn('[TradingPro] Chart not initialized');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!this.data || this.data.length === 0) {
|
|
|
this.showError('No data available to display');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
this.candlestickSeries.setData(this.data);
|
|
|
|
|
|
|
|
|
this.chart.timeScale().fitContent();
|
|
|
}
|
|
|
|
|
|
calculateIndicators() {
|
|
|
if (this.data.length === 0) return;
|
|
|
|
|
|
|
|
|
if (this.indicators.rsi.enabled) {
|
|
|
this.calculateRSI();
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.indicators.macd.enabled) {
|
|
|
this.calculateMACD();
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.indicators.bb.enabled) {
|
|
|
this.calculateBollingerBands();
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.indicators.ema.enabled) {
|
|
|
this.calculateEMAs();
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.indicators.volume.enabled) {
|
|
|
this.calculateVolume();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
calculateRSI(period = 14) {
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const rsi = [];
|
|
|
|
|
|
let gains = 0;
|
|
|
let losses = 0;
|
|
|
|
|
|
|
|
|
for (let i = 1; i <= period; i++) {
|
|
|
const change = closes[i] - closes[i - 1];
|
|
|
if (change > 0) gains += change;
|
|
|
else losses += Math.abs(change);
|
|
|
}
|
|
|
|
|
|
let avgGain = gains / period;
|
|
|
let avgLoss = losses / period;
|
|
|
let rs = avgGain / avgLoss;
|
|
|
rsi.push({ time: this.data[period].time, value: 100 - (100 / (1 + rs)) });
|
|
|
|
|
|
|
|
|
for (let i = period + 1; i < closes.length; i++) {
|
|
|
const change = closes[i] - closes[i - 1];
|
|
|
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;
|
|
|
rs = avgGain / avgLoss;
|
|
|
|
|
|
rsi.push({
|
|
|
time: this.data[i].time,
|
|
|
value: 100 - (100 / (1 + rs))
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
const latestRSI = rsi[rsi.length - 1]?.value || 50;
|
|
|
const rsiEl = document.getElementById('rsiValue');
|
|
|
if (rsiEl) {
|
|
|
rsiEl.textContent = latestRSI.toFixed(1);
|
|
|
rsiEl.className = 'metric-value';
|
|
|
if (latestRSI > 70) rsiEl.classList.add('bearish');
|
|
|
else if (latestRSI < 30) rsiEl.classList.add('bullish');
|
|
|
else rsiEl.classList.add('neutral');
|
|
|
}
|
|
|
|
|
|
return rsi;
|
|
|
}
|
|
|
|
|
|
calculateMACD() {
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const ema12 = this.calculateEMA(closes, 12);
|
|
|
const ema26 = this.calculateEMA(closes, 26);
|
|
|
|
|
|
const macdLine = ema12.map((val, i) => val - ema26[i]);
|
|
|
const signalLine = this.calculateEMA(macdLine, 9);
|
|
|
const histogram = macdLine.map((val, i) => val - signalLine[i]);
|
|
|
|
|
|
|
|
|
const latestHistogram = histogram[histogram.length - 1];
|
|
|
const macdEl = document.getElementById('macdValue');
|
|
|
if (macdEl) {
|
|
|
if (latestHistogram > 0) {
|
|
|
macdEl.textContent = 'Bullish';
|
|
|
macdEl.className = 'metric-value bullish';
|
|
|
} else {
|
|
|
macdEl.textContent = 'Bearish';
|
|
|
macdEl.className = 'metric-value bearish';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return { macdLine, signalLine, histogram };
|
|
|
}
|
|
|
|
|
|
calculateEMA(values, period) {
|
|
|
const k = 2 / (period + 1);
|
|
|
const ema = [values[0]];
|
|
|
|
|
|
for (let i = 1; i < values.length; i++) {
|
|
|
ema.push(values[i] * k + ema[i - 1] * (1 - k));
|
|
|
}
|
|
|
|
|
|
return ema;
|
|
|
}
|
|
|
|
|
|
calculateBollingerBands(period = 20, stdDev = 2) {
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const sma = this.calculateSMA(closes, period);
|
|
|
const upper = [];
|
|
|
const lower = [];
|
|
|
|
|
|
for (let i = period - 1; i < closes.length; i++) {
|
|
|
const slice = closes.slice(i - period + 1, i + 1);
|
|
|
const mean = sma[i];
|
|
|
const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;
|
|
|
const sd = Math.sqrt(variance);
|
|
|
|
|
|
upper.push(mean + stdDev * sd);
|
|
|
lower.push(mean - stdDev * sd);
|
|
|
}
|
|
|
|
|
|
return { upper, middle: sma, lower };
|
|
|
}
|
|
|
|
|
|
calculateSMA(values, period) {
|
|
|
const sma = [];
|
|
|
for (let i = period - 1; i < values.length; i++) {
|
|
|
const sum = values.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
|
|
|
sma.push(sum / period);
|
|
|
}
|
|
|
return sma;
|
|
|
}
|
|
|
|
|
|
calculateEMAs() {
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const ema20 = this.calculateEMA(closes, 20);
|
|
|
const ema50 = this.calculateEMA(closes, 50);
|
|
|
const ema200 = this.calculateEMA(closes, 200);
|
|
|
|
|
|
|
|
|
if (!this.indicators.ema.ema20) {
|
|
|
this.indicators.ema.ema20 = this.chart.addLineSeries({
|
|
|
color: '#2dd4bf',
|
|
|
lineWidth: 2,
|
|
|
title: 'EMA 20',
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (!this.indicators.ema.ema50) {
|
|
|
this.indicators.ema.ema50 = this.chart.addLineSeries({
|
|
|
color: '#818cf8',
|
|
|
lineWidth: 2,
|
|
|
title: 'EMA 50',
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (!this.indicators.ema.ema200) {
|
|
|
this.indicators.ema.ema200 = this.chart.addLineSeries({
|
|
|
color: '#ec4899',
|
|
|
lineWidth: 2,
|
|
|
title: 'EMA 200',
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
this.indicators.ema.ema20.setData(
|
|
|
ema20.map((val, i) => ({ time: this.data[i].time, value: val }))
|
|
|
);
|
|
|
this.indicators.ema.ema50.setData(
|
|
|
ema50.map((val, i) => ({ time: this.data[i].time, value: val }))
|
|
|
);
|
|
|
this.indicators.ema.ema200.setData(
|
|
|
ema200.map((val, i) => ({ time: this.data[i].time, value: val }))
|
|
|
);
|
|
|
|
|
|
|
|
|
const latest = {
|
|
|
ema20: ema20[ema20.length - 1],
|
|
|
ema50: ema50[ema50.length - 1],
|
|
|
ema200: ema200[ema200.length - 1]
|
|
|
};
|
|
|
|
|
|
const emaEl = document.getElementById('emaValue');
|
|
|
if (emaEl) {
|
|
|
if (latest.ema20 > latest.ema50 && latest.ema50 > latest.ema200) {
|
|
|
emaEl.textContent = 'Strong Uptrend';
|
|
|
emaEl.className = 'metric-value bullish';
|
|
|
} else if (latest.ema20 < latest.ema50 && latest.ema50 < latest.ema200) {
|
|
|
emaEl.textContent = 'Strong Downtrend';
|
|
|
emaEl.className = 'metric-value bearish';
|
|
|
} else {
|
|
|
emaEl.textContent = 'Mixed';
|
|
|
emaEl.className = 'metric-value neutral';
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
calculateVolume() {
|
|
|
if (!this.indicators.volume.series) {
|
|
|
this.indicators.volume.series = this.chart.addHistogramSeries({
|
|
|
color: '#26a69a',
|
|
|
priceFormat: {
|
|
|
type: 'volume',
|
|
|
},
|
|
|
priceScaleId: 'volume',
|
|
|
});
|
|
|
|
|
|
this.chart.priceScale('volume').applyOptions({
|
|
|
scaleMargins: {
|
|
|
top: 0.8,
|
|
|
bottom: 0,
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const volumeData = this.data.map(d => ({
|
|
|
time: d.time,
|
|
|
value: d.volume,
|
|
|
color: d.close > d.open ? 'rgba(34, 197, 94, 0.5)' : 'rgba(239, 68, 68, 0.5)'
|
|
|
}));
|
|
|
|
|
|
this.indicators.volume.series.setData(volumeData);
|
|
|
}
|
|
|
|
|
|
updateIndicators() {
|
|
|
|
|
|
Object.keys(this.indicators).forEach(key => {
|
|
|
const indicator = this.indicators[key];
|
|
|
if (!indicator.enabled) {
|
|
|
if (indicator.series) {
|
|
|
this.chart.removeSeries(indicator.series);
|
|
|
indicator.series = null;
|
|
|
}
|
|
|
if (indicator.ema20) {
|
|
|
this.chart.removeSeries(indicator.ema20);
|
|
|
this.chart.removeSeries(indicator.ema50);
|
|
|
this.chart.removeSeries(indicator.ema200);
|
|
|
indicator.ema20 = null;
|
|
|
indicator.ema50 = null;
|
|
|
indicator.ema200 = null;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
this.calculateIndicators();
|
|
|
}
|
|
|
|
|
|
detectPatterns() {
|
|
|
const patterns = [];
|
|
|
|
|
|
if (this.data.length < 50) return patterns;
|
|
|
|
|
|
|
|
|
if (this.patterns.hs) {
|
|
|
const hs = this.detectHeadAndShoulders();
|
|
|
if (hs) patterns.push(hs);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.patterns.double) {
|
|
|
const double = this.detectDoubleTops();
|
|
|
if (double) patterns.push(double);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.patterns.triangle) {
|
|
|
const triangle = this.detectTriangles();
|
|
|
if (triangle) patterns.push(triangle);
|
|
|
}
|
|
|
|
|
|
|
|
|
patterns.forEach(pattern => {
|
|
|
this.addPatternMarker(pattern);
|
|
|
});
|
|
|
|
|
|
return patterns;
|
|
|
}
|
|
|
|
|
|
detectHeadAndShoulders() {
|
|
|
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const len = closes.length;
|
|
|
|
|
|
if (len < 30) return null;
|
|
|
|
|
|
|
|
|
const recent = closes.slice(-30);
|
|
|
const max = Math.max(...recent);
|
|
|
const maxIdx = recent.lastIndexOf(max);
|
|
|
|
|
|
|
|
|
if (maxIdx > 5 && maxIdx < 25) {
|
|
|
const leftPeak = Math.max(...recent.slice(0, maxIdx - 3));
|
|
|
const rightPeak = Math.max(...recent.slice(maxIdx + 3));
|
|
|
|
|
|
if (leftPeak < max * 0.98 && rightPeak < max * 0.98 &&
|
|
|
Math.abs(leftPeak - rightPeak) < max * 0.02) {
|
|
|
return {
|
|
|
type: 'head_shoulders',
|
|
|
signal: 'sell',
|
|
|
confidence: 0.7,
|
|
|
index: len - 30 + maxIdx
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
detectDoubleTops() {
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const len = closes.length;
|
|
|
|
|
|
if (len < 20) return null;
|
|
|
|
|
|
const recent = closes.slice(-20);
|
|
|
const peaks = [];
|
|
|
|
|
|
for (let i = 1; i < recent.length - 1; i++) {
|
|
|
if (recent[i] > recent[i - 1] && recent[i] > recent[i + 1]) {
|
|
|
peaks.push({ value: recent[i], index: i });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (peaks.length >= 2) {
|
|
|
const lastTwo = peaks.slice(-2);
|
|
|
const diff = Math.abs(lastTwo[0].value - lastTwo[1].value);
|
|
|
if (diff < lastTwo[0].value * 0.02) {
|
|
|
return {
|
|
|
type: 'double_top',
|
|
|
signal: 'sell',
|
|
|
confidence: 0.75,
|
|
|
index: len - 20 + lastTwo[1].index
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
detectTriangles() {
|
|
|
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
const highs = this.data.map(d => d.high);
|
|
|
const lows = this.data.map(d => d.low);
|
|
|
|
|
|
if (closes.length < 20) return null;
|
|
|
|
|
|
const recent = closes.slice(-20);
|
|
|
const recentHighs = highs.slice(-20);
|
|
|
const recentLows = lows.slice(-20);
|
|
|
|
|
|
const maxHigh = Math.max(...recentHighs);
|
|
|
const minLow = Math.min(...recentLows);
|
|
|
const range = maxHigh - minLow;
|
|
|
|
|
|
const recentRange = Math.max(...recent.slice(-5)) - Math.min(...recent.slice(-5));
|
|
|
|
|
|
if (recentRange < range * 0.3) {
|
|
|
return {
|
|
|
type: 'triangle',
|
|
|
signal: 'breakout_pending',
|
|
|
confidence: 0.65,
|
|
|
index: closes.length - 10
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
addPatternMarker(pattern) {
|
|
|
|
|
|
console.log('[TradingPro] Pattern detected:', pattern.type, 'Confidence:', pattern.confidence);
|
|
|
|
|
|
}
|
|
|
|
|
|
activateDrawingTool(tool) {
|
|
|
console.log('[TradingPro] Activated drawing tool:', tool);
|
|
|
|
|
|
switch (tool) {
|
|
|
case 'trendline':
|
|
|
this.showToast('Click two points to draw trend line', 'info');
|
|
|
break;
|
|
|
case 'horizontal':
|
|
|
this.showToast('Click to draw horizontal line', 'info');
|
|
|
break;
|
|
|
case 'fibonacci':
|
|
|
this.showToast('Click two points for Fibonacci retracement', 'info');
|
|
|
break;
|
|
|
case 'rectangle':
|
|
|
this.showToast('Click two points to draw rectangle', 'info');
|
|
|
break;
|
|
|
case 'triangle':
|
|
|
this.showToast('Click three points to draw triangle', 'info');
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updatePriceDisplay() {
|
|
|
if (this.data.length === 0) return;
|
|
|
|
|
|
const latest = this.data[this.data.length - 1];
|
|
|
const previous = this.data[this.data.length - 2];
|
|
|
|
|
|
const currentPrice = latest.close;
|
|
|
const change = ((latest.close - previous.close) / previous.close) * 100;
|
|
|
|
|
|
const priceEl = document.getElementById('currentPrice');
|
|
|
const changeEl = document.getElementById('priceChange');
|
|
|
|
|
|
if (priceEl) {
|
|
|
priceEl.textContent = `$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
|
}
|
|
|
|
|
|
if (changeEl) {
|
|
|
changeEl.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`;
|
|
|
changeEl.className = 'price-change';
|
|
|
changeEl.classList.add(change >= 0 ? 'positive' : 'negative');
|
|
|
}
|
|
|
|
|
|
|
|
|
const cpEl = document.getElementById('cp');
|
|
|
if (cpEl) {
|
|
|
cpEl.textContent = `$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateAnalysis() {
|
|
|
if (this.data.length === 0) return;
|
|
|
|
|
|
const latest = this.data[this.data.length - 1];
|
|
|
const closes = this.data.map(d => d.close);
|
|
|
|
|
|
|
|
|
const recentData = this.data.slice(-50);
|
|
|
const highs = recentData.map(d => d.high);
|
|
|
const lows = recentData.map(d => d.low);
|
|
|
|
|
|
const resistance = Math.max(...highs);
|
|
|
const support = Math.min(...lows);
|
|
|
|
|
|
const r1El = document.getElementById('r1');
|
|
|
const s1El = document.getElementById('s1');
|
|
|
|
|
|
if (r1El) r1El.textContent = `$${resistance.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
|
|
|
if (s1El) s1El.textContent = `$${support.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
|
|
|
|
|
|
|
|
|
const rsi = this.calculateRSI();
|
|
|
const latestRSI = rsi[rsi.length - 1]?.value || 50;
|
|
|
|
|
|
const ema20 = this.calculateEMA(closes, 20);
|
|
|
const ema50 = this.calculateEMA(closes, 50);
|
|
|
|
|
|
let signal = 'HOLD';
|
|
|
let confidence = 50;
|
|
|
|
|
|
|
|
|
if (ema20[ema20.length - 1] > ema50[ema50.length - 1] && latestRSI > 50 && latestRSI < 70) {
|
|
|
signal = 'STRONG BUY';
|
|
|
confidence = 85;
|
|
|
} else if (ema20[ema20.length - 1] > ema50[ema50.length - 1] && latestRSI < 70) {
|
|
|
signal = 'BUY';
|
|
|
confidence = 70;
|
|
|
} else if (ema20[ema20.length - 1] < ema50[ema50.length - 1] && latestRSI < 50 && latestRSI > 30) {
|
|
|
signal = 'STRONG SELL';
|
|
|
confidence = 85;
|
|
|
} else if (ema20[ema20.length - 1] < ema50[ema50.length - 1] && latestRSI > 30) {
|
|
|
signal = 'SELL';
|
|
|
confidence = 70;
|
|
|
}
|
|
|
|
|
|
const signalEl = document.getElementById('currentSignal');
|
|
|
const confidenceEl = document.getElementById('confidence');
|
|
|
const strengthEl = document.getElementById('strength');
|
|
|
|
|
|
if (signalEl) {
|
|
|
signalEl.textContent = signal;
|
|
|
signalEl.className = 'signal-badge';
|
|
|
if (signal.includes('BUY')) signalEl.classList.add('buy');
|
|
|
else if (signal.includes('SELL')) signalEl.classList.add('sell');
|
|
|
else signalEl.classList.add('hold');
|
|
|
}
|
|
|
|
|
|
if (confidenceEl) {
|
|
|
confidenceEl.textContent = `${confidence}%`;
|
|
|
confidenceEl.className = 'metric-value';
|
|
|
if (confidence > 75) confidenceEl.classList.add('bullish');
|
|
|
else if (confidence < 50) confidenceEl.classList.add('bearish');
|
|
|
else confidenceEl.classList.add('neutral');
|
|
|
}
|
|
|
|
|
|
if (strengthEl) {
|
|
|
const strength = confidence > 75 ? 'Strong' : confidence > 60 ? 'Medium' : 'Weak';
|
|
|
strengthEl.textContent = strength;
|
|
|
strengthEl.className = 'metric-value';
|
|
|
if (confidence > 75) strengthEl.classList.add('bullish');
|
|
|
else strengthEl.classList.add('neutral');
|
|
|
}
|
|
|
|
|
|
|
|
|
this.loadMarketStats();
|
|
|
}
|
|
|
|
|
|
async loadMarketStats() {
|
|
|
try {
|
|
|
const symbol = this.symbol.replace('USDT', '').toLowerCase();
|
|
|
const response = await fetch(`/api/coins/top?limit=100`);
|
|
|
|
|
|
if (response.ok) {
|
|
|
const data = await response.json();
|
|
|
const coins = data.data || data.coins || [];
|
|
|
const coin = coins.find(c => c.symbol?.toUpperCase() === symbol.toUpperCase());
|
|
|
|
|
|
if (coin) {
|
|
|
const vol24hEl = document.getElementById('volume24h');
|
|
|
const mcapEl = document.getElementById('marketCap');
|
|
|
|
|
|
if (vol24hEl && coin.total_volume) {
|
|
|
vol24hEl.textContent = this.formatCurrency(coin.total_volume);
|
|
|
}
|
|
|
|
|
|
if (mcapEl && coin.market_cap) {
|
|
|
mcapEl.textContent = this.formatCurrency(coin.market_cap);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('[TradingPro] Market stats error:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateTimestamp() {
|
|
|
const now = new Date();
|
|
|
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
|
const updateEl = document.getElementById('lastUpdate');
|
|
|
if (updateEl) {
|
|
|
updateEl.textContent = timeStr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
loadStrategyTab(tabType) {
|
|
|
const container = document.querySelector('.strategy-content');
|
|
|
if (!container) return;
|
|
|
|
|
|
switch (tabType) {
|
|
|
case 'strategies':
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'signals':
|
|
|
container.innerHTML = `
|
|
|
<div class="strategy-list">
|
|
|
<div class="analysis-card">
|
|
|
<h3>🎯 Active Trading Signals</h3>
|
|
|
<div class="metric-row">
|
|
|
<span class="metric-label">BTC/USDT</span>
|
|
|
<span class="signal-badge buy">BUY</span>
|
|
|
</div>
|
|
|
<div class="metric-row">
|
|
|
<span class="metric-label">Entry: $42,150</span>
|
|
|
<span class="metric-label">Target: $44,200</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
break;
|
|
|
|
|
|
case 'history':
|
|
|
container.innerHTML = `
|
|
|
<div class="strategy-list">
|
|
|
<div class="analysis-card">
|
|
|
<h3>📜 Recent Trades</h3>
|
|
|
<p style="color: var(--text-secondary);">No trade history available yet.</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
break;
|
|
|
|
|
|
case 'backtests':
|
|
|
container.innerHTML = `
|
|
|
<div class="strategy-list">
|
|
|
<div class="analysis-card">
|
|
|
<h3>📊 Backtest Results</h3>
|
|
|
<div class="metric-row">
|
|
|
<span class="metric-label">Total Trades</span>
|
|
|
<span class="metric-value">1,247</span>
|
|
|
</div>
|
|
|
<div class="metric-row">
|
|
|
<span class="metric-label">Win Rate</span>
|
|
|
<span class="metric-value bullish">67.3%</span>
|
|
|
</div>
|
|
|
<div class="metric-row">
|
|
|
<span class="metric-label">Profit Factor</span>
|
|
|
<span class="metric-value bullish">2.41</span>
|
|
|
</div>
|
|
|
<div class="metric-row">
|
|
|
<span class="metric-label">Max Drawdown</span>
|
|
|
<span class="metric-value bearish">-12.5%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
applyStrategy(strategyElement) {
|
|
|
const strategyName = strategyElement.querySelector('.strategy-name')?.textContent;
|
|
|
console.log('[TradingPro] Applying strategy:', strategyName);
|
|
|
this.showToast(`Strategy "${strategyName}" applied to chart`, 'success');
|
|
|
|
|
|
|
|
|
this.updateAnalysis();
|
|
|
}
|
|
|
|
|
|
zoomIn() {
|
|
|
if (this.chart) {
|
|
|
const timeScale = this.chart.timeScale();
|
|
|
const range = timeScale.getVisibleLogicalRange();
|
|
|
if (range) {
|
|
|
const newRange = {
|
|
|
from: range.from + (range.to - range.from) * 0.1,
|
|
|
to: range.to - (range.to - range.from) * 0.1
|
|
|
};
|
|
|
timeScale.setVisibleLogicalRange(newRange);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
zoomOut() {
|
|
|
if (this.chart) {
|
|
|
const timeScale = this.chart.timeScale();
|
|
|
const range = timeScale.getVisibleLogicalRange();
|
|
|
if (range) {
|
|
|
const newRange = {
|
|
|
from: range.from - (range.to - range.from) * 0.1,
|
|
|
to: range.to + (range.to - range.from) * 0.1
|
|
|
};
|
|
|
timeScale.setVisibleLogicalRange(newRange);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
takeScreenshot() {
|
|
|
this.showToast('Screenshot feature coming soon!', 'info');
|
|
|
}
|
|
|
|
|
|
formatCurrency(value) {
|
|
|
if (!value) return '$0';
|
|
|
|
|
|
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
|
|
|
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
|
|
|
if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
|
|
|
|
|
|
return `$${value.toFixed(2)}`;
|
|
|
}
|
|
|
|
|
|
showToast(message, type = 'info') {
|
|
|
console.log(`[TradingPro] ${type.toUpperCase()}: ${message}`);
|
|
|
}
|
|
|
|
|
|
showError(message) {
|
|
|
console.error('[TradingPro] ERROR:', message);
|
|
|
|
|
|
|
|
|
const chartContainer = document.getElementById('chart-container') || document.querySelector('.chart-container');
|
|
|
if (chartContainer) {
|
|
|
const errorDiv = document.createElement('div');
|
|
|
errorDiv.className = 'error-message';
|
|
|
errorDiv.style.cssText = 'padding: 2rem; text-align: center; color: #ef4444; background: rgba(239, 68, 68, 0.1); border-radius: 8px; margin: 1rem;';
|
|
|
errorDiv.innerHTML = `
|
|
|
<div style="font-size: 1.2rem; font-weight: 600; margin-bottom: 0.5rem;">⚠️ ${message}</div>
|
|
|
<div style="font-size: 0.9rem; opacity: 0.8;">Please try again or select a different symbol/timeframe</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
chartContainer.querySelectorAll('.error-message').forEach(el => el.remove());
|
|
|
chartContainer.appendChild(errorDiv);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (window.showToast) {
|
|
|
window.showToast(message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
destroy() {
|
|
|
if (this.updateInterval) {
|
|
|
clearInterval(this.updateInterval);
|
|
|
}
|
|
|
if (this.chart) {
|
|
|
this.chart.remove();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
window.tradingPro = new TradingPro();
|
|
|
window.tradingPro.init();
|
|
|
});
|
|
|
} else {
|
|
|
window.tradingPro = new TradingPro();
|
|
|
window.tradingPro.init();
|
|
|
}
|
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
window.tradingPro?.destroy();
|
|
|
});
|
|
|
|
|
|
|