|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { escapeHtml, safeFormatNumber, safeFormatCurrency } from '../../shared/js/utils/sanitizer.js'; |
|
|
import HTSEngine from './hts-engine.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const API_CONFIG = { |
|
|
|
|
|
serverBase: window.location.origin, |
|
|
unifiedRate: '/api/service/rate', |
|
|
unifiedOHLC: '/api/market/ohlc', |
|
|
|
|
|
binance: 'https://api.binance.com/api/v3', |
|
|
coingecko: 'https://api.coingecko.com/api/v3', |
|
|
timeout: 10000, |
|
|
retries: 2 |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const API_CACHE = { |
|
|
data: new Map(), |
|
|
ttl: 60000, |
|
|
|
|
|
set(key, value) { |
|
|
this.data.set(key, { |
|
|
value, |
|
|
timestamp: Date.now() |
|
|
}); |
|
|
}, |
|
|
|
|
|
get(key) { |
|
|
const item = this.data.get(key); |
|
|
if (!item) return null; |
|
|
|
|
|
if (Date.now() - item.timestamp > this.ttl) { |
|
|
this.data.delete(key); |
|
|
return null; |
|
|
} |
|
|
|
|
|
return item.value; |
|
|
}, |
|
|
|
|
|
clear() { |
|
|
this.data.clear(); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const STRATEGIES = { |
|
|
'hts-hybrid': { |
|
|
name: '🔥 HTS Hybrid System', |
|
|
description: 'RSI+MACD (40%) + SMC (25%) + Patterns (20%) + Sentiment (10%) + ML (5%)', |
|
|
indicators: ['RSI', 'MACD', 'SMC', 'Patterns', 'Sentiment', 'ML'], |
|
|
timeframes: ['15m', '1h', '4h', '1d'], |
|
|
badge: 'PREMIUM', |
|
|
type: 'hybrid' |
|
|
}, |
|
|
'trend-rsi-macd': { |
|
|
name: 'Trend + RSI + MACD', |
|
|
description: 'Combines trend following with momentum indicators', |
|
|
indicators: ['EMA', 'RSI', 'MACD'], |
|
|
timeframes: ['1h', '4h', '1d'] |
|
|
}, |
|
|
'scalping': { |
|
|
name: 'Scalping Strategy', |
|
|
description: 'Quick trades on small price movements', |
|
|
indicators: ['Bollinger Bands', 'Stochastic', 'Volume'], |
|
|
timeframes: ['1m', '5m', '15m'] |
|
|
}, |
|
|
'swing': { |
|
|
name: 'Swing Trading', |
|
|
description: 'Medium-term position trading', |
|
|
indicators: ['EMA', 'RSI', 'Support/Resistance'], |
|
|
timeframes: ['4h', '1d', '1w'] |
|
|
}, |
|
|
'breakout': { |
|
|
name: 'Breakout Strategy', |
|
|
description: 'Trade price breakouts from consolidation', |
|
|
indicators: ['ATR', 'Volume', 'Bollinger Bands'], |
|
|
timeframes: ['15m', '1h', '4h'] |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const CRYPTOS = [ |
|
|
{ symbol: 'BTC', name: 'Bitcoin', binance: 'BTCUSDT', demoPrice: 43000 }, |
|
|
{ symbol: 'ETH', name: 'Ethereum', binance: 'ETHUSDT', demoPrice: 2300 }, |
|
|
{ symbol: 'BNB', name: 'Binance Coin', binance: 'BNBUSDT', demoPrice: 310 }, |
|
|
{ symbol: 'SOL', name: 'Solana', binance: 'SOLUSDT', demoPrice: 98 }, |
|
|
{ symbol: 'ADA', name: 'Cardano', binance: 'ADAUSDT', demoPrice: 0.58 }, |
|
|
{ symbol: 'XRP', name: 'Ripple', binance: 'XRPUSDT', demoPrice: 0.62 }, |
|
|
{ symbol: 'DOT', name: 'Polkadot', binance: 'DOTUSDT', demoPrice: 7.2 }, |
|
|
{ symbol: 'AVAX', name: 'Avalanche', binance: 'AVAXUSDT', demoPrice: 38 }, |
|
|
{ symbol: 'MATIC', name: 'Polygon', binance: 'MATICUSDT', demoPrice: 0.89 }, |
|
|
{ symbol: 'LINK', name: 'Chainlink', binance: 'LINKUSDT', demoPrice: 14.5 } |
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TradingAssistantProfessional { |
|
|
constructor() { |
|
|
this.selectedCrypto = 'BTC'; |
|
|
this.selectedStrategy = 'trend-rsi-macd'; |
|
|
this.isMonitoring = false; |
|
|
this.monitoringInterval = null; |
|
|
this.signals = []; |
|
|
this.marketData = {}; |
|
|
this.lastUpdate = null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async init() { |
|
|
try { |
|
|
console.log('[TradingAssistant] Initializing Professional Edition...'); |
|
|
|
|
|
this.bindEvents(); |
|
|
this.renderStrategyCards(); |
|
|
this.renderCryptoList(); |
|
|
await this.loadMarketData(); |
|
|
|
|
|
this.showToast('✅ Trading Assistant Ready', 'success'); |
|
|
console.log('[TradingAssistant] Initialization complete'); |
|
|
} catch (error) { |
|
|
console.error('[TradingAssistant] Initialization error:', error); |
|
|
this.showToast('⚠️ Initialization error - using fallback mode', 'warning'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bindEvents() { |
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (e.target.closest('[data-crypto]')) { |
|
|
const cryptoBtn = e.target.closest('[data-crypto]'); |
|
|
this.selectedCrypto = cryptoBtn.dataset.crypto; |
|
|
this.updateCryptoSelection(); |
|
|
this.loadMarketData(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (e.target.closest('[data-strategy]')) { |
|
|
const strategyBtn = e.target.closest('[data-strategy]'); |
|
|
this.selectedStrategy = strategyBtn.dataset.strategy; |
|
|
this.updateStrategySelection(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const getSignalsBtn = document.getElementById('get-signals-btn'); |
|
|
if (getSignalsBtn) { |
|
|
getSignalsBtn.addEventListener('click', () => this.analyzeMarket()); |
|
|
} |
|
|
|
|
|
|
|
|
const toggleMonitorBtn = document.getElementById('toggle-monitor-btn'); |
|
|
if (toggleMonitorBtn) { |
|
|
toggleMonitorBtn.addEventListener('click', () => this.toggleMonitoring()); |
|
|
} |
|
|
|
|
|
|
|
|
const refreshBtn = document.getElementById('refresh-data'); |
|
|
if (refreshBtn) { |
|
|
refreshBtn.addEventListener('click', () => this.loadMarketData(true)); |
|
|
} |
|
|
|
|
|
|
|
|
const exportBtn = document.getElementById('export-signals'); |
|
|
if (exportBtn) { |
|
|
exportBtn.addEventListener('click', () => this.exportSignals()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderStrategyCards() { |
|
|
const container = document.getElementById('strategy-cards'); |
|
|
if (!container) return; |
|
|
|
|
|
const html = Object.entries(STRATEGIES).map(([key, strategy]) => { |
|
|
const badgeText = strategy.badge || `${strategy.indicators.length} indicators`; |
|
|
const badgeClass = strategy.badge === 'PREMIUM' ? 'premium-badge' : 'strategy-badge'; |
|
|
|
|
|
return ` |
|
|
<div class="strategy-card ${key === this.selectedStrategy ? 'active' : ''} ${strategy.type === 'hybrid' ? 'hybrid-strategy' : ''}" data-strategy="${key}"> |
|
|
<div class="strategy-header"> |
|
|
<h4>${escapeHtml(strategy.name)}</h4> |
|
|
<span class="${badgeClass}">${badgeText}</span> |
|
|
</div> |
|
|
<p class="strategy-description">${escapeHtml(strategy.description)}</p> |
|
|
<div class="strategy-indicators"> |
|
|
${strategy.indicators.map(ind => `<span class="indicator-tag">${escapeHtml(ind)}</span>`).join('')} |
|
|
</div> |
|
|
<div class="strategy-timeframes"> |
|
|
<small>Timeframes: ${strategy.timeframes.join(', ')}</small> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
container.innerHTML = html; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderCryptoList() { |
|
|
const container = document.getElementById('crypto-list'); |
|
|
if (!container) return; |
|
|
|
|
|
const html = CRYPTOS.map(crypto => ` |
|
|
<button class="crypto-btn ${crypto.symbol === this.selectedCrypto ? 'active' : ''}" data-crypto="${crypto.symbol}"> |
|
|
<span class="crypto-symbol">${crypto.symbol}</span> |
|
|
<span class="crypto-name">${escapeHtml(crypto.name)}</span> |
|
|
<span class="crypto-price" id="price-${crypto.symbol}">Loading...</span> |
|
|
</button> |
|
|
`).join(''); |
|
|
|
|
|
container.innerHTML = html; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateCryptoSelection() { |
|
|
document.querySelectorAll('[data-crypto]').forEach(btn => { |
|
|
btn.classList.toggle('active', btn.dataset.crypto === this.selectedCrypto); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateStrategySelection() { |
|
|
document.querySelectorAll('[data-strategy]').forEach(card => { |
|
|
card.classList.toggle('active', card.dataset.strategy === this.selectedStrategy); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadMarketData(forceRefresh = false) { |
|
|
try { |
|
|
console.log('[TradingAssistant] Loading market data...'); |
|
|
|
|
|
|
|
|
for (const crypto of CRYPTOS) { |
|
|
try { |
|
|
const price = await this.fetchPrice(crypto.symbol); |
|
|
this.marketData[crypto.symbol] = { price, timestamp: Date.now() }; |
|
|
|
|
|
|
|
|
const priceEl = document.getElementById(`price-${crypto.symbol}`); |
|
|
if (priceEl) { |
|
|
priceEl.textContent = safeFormatCurrency(price); |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`Failed to load price for ${crypto.symbol}:`, error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const ohlcvData = await this.fetchOHLCV(this.selectedCrypto, '4h', 100); |
|
|
this.marketData[this.selectedCrypto].ohlcv = ohlcvData; |
|
|
|
|
|
this.lastUpdate = new Date(); |
|
|
this.updateLastUpdateDisplay(); |
|
|
|
|
|
console.log('✅ Market data loaded'); |
|
|
} catch (error) { |
|
|
console.error('❌ Failed to load market data:', error); |
|
|
this.showToast('Failed to load market data', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchPrice(symbol) { |
|
|
const crypto = CRYPTOS.find(c => c.symbol === symbol); |
|
|
if (!crypto) throw new Error('Symbol not found'); |
|
|
|
|
|
|
|
|
const cacheKey = `price_${symbol}`; |
|
|
const cached = API_CACHE.get(cacheKey); |
|
|
if (cached) { |
|
|
return cached; |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const pair = `${symbol}/USDT`; |
|
|
const url = `${API_CONFIG.serverBase}${API_CONFIG.unifiedRate}?pair=${encodeURIComponent(pair)}`; |
|
|
console.log(`[API] Fetching price from server unified API: ${url}`); |
|
|
|
|
|
const response = await this.fetchWithTimeout(url, 10000); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
const price = parseFloat(data?.data?.price || data?.price || 0); |
|
|
if (price > 0) { |
|
|
API_CACHE.set(cacheKey, price); |
|
|
const source = data?.meta?.source || 'server'; |
|
|
console.log(`[API] ${symbol} price from ${source}: $${price.toFixed(2)}`); |
|
|
return price; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`[API] Server unified API failed for ${symbol}:`, error.message); |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const cgMap = { |
|
|
'BTC': 'bitcoin', |
|
|
'ETH': 'ethereum', |
|
|
'BNB': 'binancecoin', |
|
|
'SOL': 'solana', |
|
|
'XRP': 'ripple', |
|
|
'ADA': 'cardano' |
|
|
}; |
|
|
|
|
|
const coinId = cgMap[symbol]; |
|
|
if (coinId) { |
|
|
const url = `${API_CONFIG.coingecko}/simple/price?ids=${coinId}&vs_currencies=usd`; |
|
|
const response = await this.fetchWithTimeout(url, 8000); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
const price = data[coinId]?.usd; |
|
|
if (price > 0) { |
|
|
API_CACHE.set(cacheKey, price); |
|
|
console.log(`[API] ${symbol} price from CoinGecko (direct): $${price.toFixed(2)}`); |
|
|
return price; |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`[API] CoinGecko direct fetch failed for ${symbol}:`, error.message); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.warn(`[API] All unified sources failed for ${symbol} - server should handle fallbacks`); |
|
|
|
|
|
|
|
|
throw new Error(`Unable to fetch real price for ${symbol} from all sources`); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchOHLCV(symbol, timeframe, limit) { |
|
|
const crypto = CRYPTOS.find(c => c.symbol === symbol); |
|
|
if (!crypto) throw new Error('Symbol not found'); |
|
|
|
|
|
|
|
|
const cacheKey = `ohlcv_${symbol}_${timeframe}_${limit}`; |
|
|
const cached = API_CACHE.get(cacheKey); |
|
|
if (cached) { |
|
|
console.log(`[API] Using cached OHLCV for ${symbol}`); |
|
|
return cached; |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const intervalMap = { |
|
|
'1m': '1m', '5m': '5m', '15m': '15m', |
|
|
'1h': '1h', '4h': '4h', '1d': '1d', '1w': '1w' |
|
|
}; |
|
|
|
|
|
const interval = intervalMap[timeframe] || '4h'; |
|
|
const url = `${API_CONFIG.serverBase}${API_CONFIG.unifiedOHLC}?symbol=${symbol}&interval=${interval}&limit=${limit}`; |
|
|
|
|
|
console.log(`[API] Fetching OHLCV from server unified API: ${url}`); |
|
|
|
|
|
const response = await this.fetchWithTimeout(url, 12000); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
|
|
|
let ohlcvData = null; |
|
|
|
|
|
if (data?.success && data?.data) { |
|
|
ohlcvData = data.data; |
|
|
} else if (data?.data && Array.isArray(data.data)) { |
|
|
ohlcvData = data.data; |
|
|
} else if (Array.isArray(data)) { |
|
|
ohlcvData = data; |
|
|
} |
|
|
|
|
|
if (ohlcvData && ohlcvData.length > 0) { |
|
|
|
|
|
const transformed = ohlcvData.map(candle => { |
|
|
if (Array.isArray(candle)) { |
|
|
|
|
|
return { |
|
|
time: candle[0], |
|
|
open: parseFloat(candle[1]), |
|
|
high: parseFloat(candle[2]), |
|
|
low: parseFloat(candle[3]), |
|
|
close: parseFloat(candle[4]), |
|
|
volume: parseFloat(candle[5]) |
|
|
}; |
|
|
} else { |
|
|
|
|
|
return { |
|
|
time: candle.ts || candle.time || candle.t, |
|
|
open: parseFloat(candle.open || candle.o), |
|
|
high: parseFloat(candle.high || candle.h), |
|
|
low: parseFloat(candle.low || candle.l), |
|
|
close: parseFloat(candle.close || candle.c), |
|
|
volume: parseFloat(candle.volume || candle.v || 0) |
|
|
}; |
|
|
} |
|
|
}); |
|
|
|
|
|
API_CACHE.set(cacheKey, transformed); |
|
|
const source = data?.meta?.source || 'server'; |
|
|
console.log(`[API] ${symbol} OHLCV from ${source}: ${transformed.length} candles`); |
|
|
return transformed; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`[API] Server unified OHLC API failed for ${symbol}:`, error.message); |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const intervalMap = { |
|
|
'1m': '1m', '5m': '5m', '15m': '15m', |
|
|
'1h': '1h', '4h': '4h', '1d': '1d', '1w': '1w' |
|
|
}; |
|
|
|
|
|
const interval = intervalMap[timeframe] || '4h'; |
|
|
const url = `${API_CONFIG.binance}/klines?symbol=${crypto.binance}&interval=${interval}&limit=${limit}`; |
|
|
|
|
|
console.log(`[API] Trying Binance direct for OHLCV: ${url}`); |
|
|
|
|
|
const response = await this.fetchWithTimeout(url, 8000); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
|
|
|
const ohlcv = data.map(item => ({ |
|
|
time: Math.floor(item[0] / 1000), |
|
|
open: parseFloat(item[1]), |
|
|
high: parseFloat(item[2]), |
|
|
low: parseFloat(item[3]), |
|
|
close: parseFloat(item[4]), |
|
|
volume: parseFloat(item[5]) |
|
|
})); |
|
|
|
|
|
API_CACHE.set(cacheKey, ohlcv); |
|
|
console.log(`[API] ${symbol} OHLCV from Binance (direct): ${ohlcv.length} candles`); |
|
|
return ohlcv; |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn('[API] Binance direct OHLCV fetch failed:', error.message); |
|
|
} |
|
|
|
|
|
|
|
|
console.warn(`[API] All sources failed for ${symbol} OHLCV, generating demo data`); |
|
|
return this.generateDemoOHLCV(crypto.demoPrice || 1000, limit); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateDemoOHLCV(basePrice, limit) { |
|
|
const now = Math.floor(Date.now() / 1000); |
|
|
const interval = 14400; |
|
|
const data = []; |
|
|
|
|
|
for (let i = limit - 1; i >= 0; i--) { |
|
|
const volatility = basePrice * 0.02; |
|
|
const trend = (Math.random() - 0.5) * volatility; |
|
|
|
|
|
const open = basePrice + trend; |
|
|
const close = open + (Math.random() - 0.5) * volatility; |
|
|
const high = Math.max(open, close) + Math.random() * volatility * 0.5; |
|
|
const low = Math.min(open, close) - Math.random() * volatility * 0.5; |
|
|
const volume = basePrice * (10000 + Math.random() * 5000); |
|
|
|
|
|
data.push({ |
|
|
time: now - (i * interval), |
|
|
open, |
|
|
high, |
|
|
low, |
|
|
close, |
|
|
volume |
|
|
}); |
|
|
|
|
|
basePrice = close; |
|
|
} |
|
|
|
|
|
return data; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchWithTimeout(url, timeout) { |
|
|
const controller = new AbortController(); |
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout); |
|
|
|
|
|
try { |
|
|
const response = await fetch(url, { |
|
|
signal: controller.signal, |
|
|
headers: { 'Accept': 'application/json' } |
|
|
}); |
|
|
clearTimeout(timeoutId); |
|
|
return response; |
|
|
} catch (error) { |
|
|
clearTimeout(timeoutId); |
|
|
if (error.name === 'AbortError') { |
|
|
throw new Error('Request timeout'); |
|
|
} |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async analyzeMarket() { |
|
|
const analyzeBtn = document.getElementById('get-signals-btn'); |
|
|
if (analyzeBtn) { |
|
|
analyzeBtn.disabled = true; |
|
|
analyzeBtn.textContent = 'Analyzing...'; |
|
|
} |
|
|
|
|
|
try { |
|
|
console.log(`[TradingAssistant] Analyzing ${this.selectedCrypto} with ${this.selectedStrategy}...`); |
|
|
|
|
|
|
|
|
const cryptoData = this.marketData[this.selectedCrypto]; |
|
|
if (!cryptoData || !cryptoData.ohlcv) { |
|
|
await this.loadMarketData(); |
|
|
} |
|
|
|
|
|
const ohlcvData = this.marketData[this.selectedCrypto].ohlcv; |
|
|
if (!ohlcvData || ohlcvData.length < 30) { |
|
|
throw new Error('Insufficient data for analysis'); |
|
|
} |
|
|
|
|
|
|
|
|
const indicators = this.calculateIndicators(ohlcvData); |
|
|
|
|
|
|
|
|
const signal = await this.generateSignal(ohlcvData, indicators, this.selectedStrategy); |
|
|
|
|
|
|
|
|
this.signals.unshift(signal); |
|
|
if (this.signals.length > 50) { |
|
|
this.signals = this.signals.slice(0, 50); |
|
|
} |
|
|
|
|
|
|
|
|
this.renderSignals(); |
|
|
|
|
|
this.showToast(`✅ Signal generated: ${signal.action.toUpperCase()}`, signal.action === 'BUY' ? 'success' : signal.action === 'SELL' ? 'error' : 'info'); |
|
|
} catch (error) { |
|
|
console.error('❌ Analysis error:', error); |
|
|
this.showToast('Analysis failed: ' + error.message, 'error'); |
|
|
} finally { |
|
|
if (analyzeBtn) { |
|
|
analyzeBtn.disabled = false; |
|
|
analyzeBtn.textContent = 'Get Signals'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateIndicators(ohlcvData) { |
|
|
const closes = ohlcvData.map(c => c.close); |
|
|
|
|
|
return { |
|
|
rsi: this.calculateRSI(closes, 14), |
|
|
macd: this.calculateMACD(closes), |
|
|
ema20: this.calculateEMA(closes, 20), |
|
|
ema50: this.calculateEMA(closes, 50), |
|
|
atr: this.calculateATR(ohlcvData, 14), |
|
|
volume: ohlcvData[ohlcvData.length - 1].volume |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateRSI(prices, period = 14) { |
|
|
if (prices.length < period + 1) return null; |
|
|
|
|
|
let gains = 0; |
|
|
let losses = 0; |
|
|
|
|
|
for (let i = 1; i <= period; i++) { |
|
|
const change = prices[i] - prices[i - 1]; |
|
|
if (change > 0) gains += change; |
|
|
else losses += Math.abs(change); |
|
|
} |
|
|
|
|
|
let avgGain = gains / period; |
|
|
let avgLoss = losses / period; |
|
|
|
|
|
for (let i = period + 1; i < prices.length; i++) { |
|
|
const change = prices[i] - prices[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; |
|
|
} |
|
|
|
|
|
const rs = avgGain / avgLoss; |
|
|
return 100 - (100 / (1 + rs)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateMACD(prices) { |
|
|
const ema12 = this.calculateEMA(prices, 12); |
|
|
const ema26 = this.calculateEMA(prices, 26); |
|
|
return ema12 - ema26; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateEMA(prices, period) { |
|
|
if (prices.length < period) return null; |
|
|
|
|
|
const k = 2 / (period + 1); |
|
|
let ema = prices[0]; |
|
|
|
|
|
for (let i = 1; i < prices.length; i++) { |
|
|
ema = prices[i] * k + ema * (1 - k); |
|
|
} |
|
|
|
|
|
return ema; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateATR(ohlcvData, period = 14) { |
|
|
if (ohlcvData.length < period + 1) return null; |
|
|
|
|
|
const trValues = []; |
|
|
for (let i = 1; i < ohlcvData.length; i++) { |
|
|
const high = ohlcvData[i].high; |
|
|
const low = ohlcvData[i].low; |
|
|
const prevClose = ohlcvData[i - 1].close; |
|
|
|
|
|
const tr = Math.max( |
|
|
high - low, |
|
|
Math.abs(high - prevClose), |
|
|
Math.abs(low - prevClose) |
|
|
); |
|
|
trValues.push(tr); |
|
|
} |
|
|
|
|
|
|
|
|
const atr = trValues.slice(-period).reduce((sum, tr) => sum + tr, 0) / period; |
|
|
return atr; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async generateSignal(ohlcvData, indicators, strategy) { |
|
|
const latestCandle = ohlcvData[ohlcvData.length - 1]; |
|
|
const currentPrice = latestCandle.close; |
|
|
|
|
|
let action = 'HOLD'; |
|
|
let confidence = 50; |
|
|
let reasons = []; |
|
|
let htsAnalysis = null; |
|
|
|
|
|
|
|
|
if (strategy === 'hts-hybrid') { |
|
|
try { |
|
|
|
|
|
const htsOHLCV = ohlcvData.map(candle => ({ |
|
|
timestamp: candle.time || candle.timestamp, |
|
|
open: candle.open, |
|
|
high: candle.high, |
|
|
low: candle.low, |
|
|
close: candle.close, |
|
|
volume: candle.volume |
|
|
})); |
|
|
|
|
|
const htsEngine = new HTSEngine(); |
|
|
htsAnalysis = await htsEngine.analyze(htsOHLCV, this.selectedCrypto); |
|
|
|
|
|
action = htsAnalysis.finalSignal.toUpperCase(); |
|
|
confidence = Math.round(htsAnalysis.confidence); |
|
|
|
|
|
|
|
|
reasons = []; |
|
|
if (htsAnalysis.components.rsiMacd.signal !== 'hold') { |
|
|
reasons.push(`RSI+MACD (${Math.round(htsAnalysis.components.rsiMacd.weight * 100)}%): ${htsAnalysis.components.rsiMacd.signal.toUpperCase()}`); |
|
|
} |
|
|
if (htsAnalysis.components.smc.signal !== 'hold') { |
|
|
reasons.push(`SMC (${Math.round(htsAnalysis.components.smc.weight * 100)}%): ${htsAnalysis.components.smc.signal.toUpperCase()}`); |
|
|
} |
|
|
if (htsAnalysis.components.patterns.detected > 0) { |
|
|
reasons.push(`Patterns: ${htsAnalysis.components.patterns.bullish} bullish, ${htsAnalysis.components.patterns.bearish} bearish`); |
|
|
} |
|
|
reasons.push(`Market Regime: ${htsAnalysis.marketRegime || 'neutral'}`); |
|
|
reasons.push(`Final Score: ${htsAnalysis.finalScore.toFixed(1)}/100`); |
|
|
|
|
|
|
|
|
const entryPrice = htsAnalysis.currentPrice; |
|
|
const stopLoss = htsAnalysis.stopLoss; |
|
|
const takeProfits = htsAnalysis.takeProfitLevels; |
|
|
|
|
|
return { |
|
|
timestamp: new Date(), |
|
|
symbol: this.selectedCrypto, |
|
|
strategy: STRATEGIES[strategy].name, |
|
|
action, |
|
|
confidence, |
|
|
reasons, |
|
|
price: currentPrice, |
|
|
entryPrice, |
|
|
stopLoss, |
|
|
takeProfit: takeProfits[0]?.level || entryPrice * (action === 'BUY' ? 1.03 : 0.97), |
|
|
takeProfits: takeProfits, |
|
|
indicators: { |
|
|
rsi: htsAnalysis.indicators.rsi?.toFixed(2), |
|
|
macd: htsAnalysis.indicators.macd?.macd?.toFixed(4), |
|
|
atr: htsAnalysis.indicators.atr?.toFixed(2), |
|
|
regime: htsAnalysis.marketRegime |
|
|
}, |
|
|
htsDetails: { |
|
|
finalScore: htsAnalysis.finalScore, |
|
|
components: htsAnalysis.components, |
|
|
smcLevels: htsAnalysis.smcLevels, |
|
|
patterns: htsAnalysis.patterns |
|
|
} |
|
|
}; |
|
|
} catch (error) { |
|
|
console.error('[HTS] Analysis error:', error); |
|
|
reasons = ['HTS analysis failed, using fallback']; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (strategy === 'trend-rsi-macd') { |
|
|
|
|
|
const bullishSignals = []; |
|
|
if (indicators.rsi < 30) bullishSignals.push('RSI Oversold'); |
|
|
if (indicators.macd > 0) bullishSignals.push('MACD Bullish'); |
|
|
if (currentPrice > indicators.ema20) bullishSignals.push('Above EMA20'); |
|
|
|
|
|
|
|
|
const bearishSignals = []; |
|
|
if (indicators.rsi > 70) bearishSignals.push('RSI Overbought'); |
|
|
if (indicators.macd < 0) bearishSignals.push('MACD Bearish'); |
|
|
if (currentPrice < indicators.ema20) bearishSignals.push('Below EMA20'); |
|
|
|
|
|
if (bullishSignals.length >= 2) { |
|
|
action = 'BUY'; |
|
|
confidence = 60 + (bullishSignals.length * 10); |
|
|
reasons = bullishSignals; |
|
|
} else if (bearishSignals.length >= 2) { |
|
|
action = 'SELL'; |
|
|
confidence = 60 + (bearishSignals.length * 10); |
|
|
reasons = bearishSignals; |
|
|
} else { |
|
|
reasons = ['Mixed signals - no clear trend']; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const entryPrice = currentPrice; |
|
|
const stopLoss = action === 'BUY' |
|
|
? currentPrice - (indicators.atr * 1.5) |
|
|
: currentPrice + (indicators.atr * 1.5); |
|
|
const takeProfit = action === 'BUY' |
|
|
? currentPrice + (indicators.atr * 3) |
|
|
: currentPrice - (indicators.atr * 3); |
|
|
|
|
|
return { |
|
|
timestamp: new Date(), |
|
|
symbol: this.selectedCrypto, |
|
|
strategy: STRATEGIES[strategy].name, |
|
|
action, |
|
|
confidence, |
|
|
reasons, |
|
|
price: currentPrice, |
|
|
entryPrice, |
|
|
stopLoss, |
|
|
takeProfit, |
|
|
indicators: { |
|
|
rsi: indicators.rsi?.toFixed(2), |
|
|
macd: indicators.macd?.toFixed(4), |
|
|
ema20: indicators.ema20?.toFixed(2) |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderSignals() { |
|
|
const container = document.getElementById('signals-list'); |
|
|
if (!container) return; |
|
|
|
|
|
if (this.signals.length === 0) { |
|
|
container.innerHTML = ` |
|
|
<div style="text-align: center; padding: 3rem; color: var(--text-muted);"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: 1rem;"> |
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |
|
|
</svg> |
|
|
<p>No signals yet. Click "Get Signals" to analyze the market.</p> |
|
|
</div> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
const html = this.signals.map(signal => { |
|
|
|
|
|
const isHTS = signal.htsDetails !== undefined; |
|
|
const takeProfitsHTML = signal.takeProfits && signal.takeProfits.length > 0 |
|
|
? signal.takeProfits.map((tp, i) => |
|
|
`<div><strong>${tp.type}:</strong> ${safeFormatCurrency(tp.level)} (${tp.percentage || 33}%)</div>` |
|
|
).join('') |
|
|
: `<div><strong>Take Profit:</strong> ${safeFormatCurrency(signal.takeProfit)}</div>`; |
|
|
|
|
|
const indicatorsHTML = isHTS |
|
|
? ` |
|
|
<span>RSI: ${signal.indicators.rsi || 'N/A'}</span> |
|
|
<span>MACD: ${signal.indicators.macd || 'N/A'}</span> |
|
|
<span>ATR: ${signal.indicators.atr || 'N/A'}</span> |
|
|
${signal.indicators.regime ? `<span>Regime: ${signal.indicators.regime}</span>` : ''} |
|
|
` |
|
|
: ` |
|
|
<span>RSI: ${signal.indicators.rsi}</span> |
|
|
<span>MACD: ${signal.indicators.macd}</span> |
|
|
<span>EMA20: ${signal.indicators.ema20}</span> |
|
|
`; |
|
|
|
|
|
return ` |
|
|
<div class="signal-card signal-${signal.action.toLowerCase()}"> |
|
|
<div class="signal-header"> |
|
|
<div> |
|
|
<span class="signal-badge badge-${signal.action.toLowerCase()}">${signal.action}</span> |
|
|
<span class="signal-symbol">${signal.symbol}</span> |
|
|
<span class="signal-confidence">${signal.confidence}% confidence</span> |
|
|
${isHTS ? '<span class="strategy-badge" style="margin-left: 0.5rem;">HTS</span>' : ''} |
|
|
</div> |
|
|
<div class="signal-time">${signal.timestamp.toLocaleTimeString()}</div> |
|
|
</div> |
|
|
<div class="signal-body"> |
|
|
<div class="signal-price"> |
|
|
<strong>Strategy:</strong> ${escapeHtml(signal.strategy)}<br> |
|
|
<strong>Entry:</strong> ${safeFormatCurrency(signal.entryPrice)} |
|
|
</div> |
|
|
<div class="signal-targets"> |
|
|
<div><strong>Stop Loss:</strong> ${safeFormatCurrency(signal.stopLoss)}</div> |
|
|
${takeProfitsHTML} |
|
|
</div> |
|
|
<div class="signal-reasons"> |
|
|
<strong>Analysis:</strong> |
|
|
<ul> |
|
|
${signal.reasons.map(r => `<li>${escapeHtml(r)}</li>`).join('')} |
|
|
</ul> |
|
|
</div> |
|
|
<div class="signal-indicators"> |
|
|
${indicatorsHTML} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
container.innerHTML = html; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toggleMonitoring() { |
|
|
this.isMonitoring = !this.isMonitoring; |
|
|
|
|
|
const btn = document.getElementById('toggle-monitor-btn'); |
|
|
if (btn) { |
|
|
btn.textContent = this.isMonitoring ? 'Stop Monitoring' : 'Start Monitoring'; |
|
|
btn.classList.toggle('btn-danger', this.isMonitoring); |
|
|
btn.classList.toggle('btn-primary', !this.isMonitoring); |
|
|
} |
|
|
|
|
|
if (this.isMonitoring) { |
|
|
this.startMonitoring(); |
|
|
this.showToast('✅ Monitoring started', 'success'); |
|
|
} else { |
|
|
this.stopMonitoring(); |
|
|
this.showToast('⏹️ Monitoring stopped', 'info'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startMonitoring() { |
|
|
|
|
|
this.monitoringInterval = setInterval(() => { |
|
|
this.analyzeMarket(); |
|
|
}, 5 * 60 * 1000); |
|
|
|
|
|
|
|
|
this.analyzeMarket(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stopMonitoring() { |
|
|
if (this.monitoringInterval) { |
|
|
clearInterval(this.monitoringInterval); |
|
|
this.monitoringInterval = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
exportSignals() { |
|
|
if (this.signals.length === 0) { |
|
|
this.showToast('No signals to export', 'warning'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const exportData = { |
|
|
exportDate: new Date().toISOString(), |
|
|
totalSignals: this.signals.length, |
|
|
signals: this.signals |
|
|
}; |
|
|
|
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = `trading_signals_${Date.now()}.json`; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(url); |
|
|
|
|
|
this.showToast('✅ Signals exported', 'success'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateLastUpdateDisplay() { |
|
|
const el = document.getElementById('last-update-time'); |
|
|
if (el && this.lastUpdate) { |
|
|
el.textContent = `Last update: ${this.lastUpdate.toLocaleTimeString()}`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showToast(message, type = 'info') { |
|
|
console.log(`[Toast ${type}]`, message); |
|
|
|
|
|
|
|
|
const toast = document.createElement('div'); |
|
|
toast.className = `toast toast-${type}`; |
|
|
toast.textContent = message; |
|
|
toast.style.cssText = ` |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
background: ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : '#3b82f6'}; |
|
|
color: white; |
|
|
padding: 1rem 1.5rem; |
|
|
border-radius: 8px; |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); |
|
|
z-index: 10000; |
|
|
animation: slideIn 0.3s ease; |
|
|
`; |
|
|
|
|
|
document.body.appendChild(toast); |
|
|
|
|
|
setTimeout(() => { |
|
|
toast.style.animation = 'slideOut 0.3s ease'; |
|
|
setTimeout(() => toast.remove(), 300); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
destroy() { |
|
|
this.stopMonitoring(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let tradingAssistantInstance = null; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
|
try { |
|
|
tradingAssistantInstance = new TradingAssistantProfessional(); |
|
|
await tradingAssistantInstance.init(); |
|
|
} catch (error) { |
|
|
console.error('[TradingAssistant] Fatal error:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', () => { |
|
|
if (tradingAssistantInstance) { |
|
|
tradingAssistantInstance.destroy(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const style = document.createElement('style'); |
|
|
style.textContent = ` |
|
|
@keyframes slideIn { |
|
|
from { transform: translateX(400px); opacity: 0; } |
|
|
to { transform: translateX(0); opacity: 1; } |
|
|
} |
|
|
@keyframes slideOut { |
|
|
from { transform: translateX(0); opacity: 1; } |
|
|
to { transform: translateX(400px); opacity: 0; } |
|
|
} |
|
|
`; |
|
|
document.head.appendChild(style); |
|
|
|
|
|
export { TradingAssistantProfessional }; |
|
|
export default TradingAssistantProfessional; |
|
|
|
|
|
|