| |
| |
|
|
|
|
| class TradingProV3 {
|
| constructor() {
|
| this.symbol = 'BTCUSDT';
|
| this.timeframe = '4h';
|
| this.chart = null;
|
| this.candlestickSeries = null;
|
| this.data = [];
|
| this.strategies = [];
|
| this.currentStrategy = null;
|
| this.editingStrategy = null;
|
| this.indicators = { ema20: null, ema50: null, ema200: null, volume: null };
|
| this.markers = [];
|
| }
|
|
|
| async init() {
|
| console.log('[TradingProV3] Initializing...');
|
|
|
| this.loadStrategiesFromStorage();
|
| this.initChart();
|
| this.bindEvents();
|
| this.renderStrategies();
|
|
|
| await this.loadData();
|
|
|
| setInterval(() => this.loadData(true), 60000);
|
|
|
| this.showToast('Trading Pro v3', 'Ready with real backtesting!', 'success');
|
| }
|
|
|
| initChart() {
|
| const container = document.getElementById('tradingChart');
|
| if (!container) return;
|
|
|
| this.chart = LightweightCharts.createChart(container, {
|
| layout: {
|
| background: { type: 'solid', color: '#ffffff' },
|
| textColor: '#5a6b7c',
|
| },
|
| grid: {
|
| vertLines: { color: 'rgba(0, 180, 180, 0.05)' },
|
| horzLines: { color: 'rgba(0, 180, 180, 0.05)' },
|
| },
|
| crosshair: {
|
| mode: LightweightCharts.CrosshairMode.Normal,
|
| vertLine: { color: '#00d4d4', width: 1, style: 2 },
|
| horzLine: { color: '#00d4d4', width: 1, style: 2 },
|
| },
|
| rightPriceScale: { borderColor: 'rgba(0, 180, 180, 0.1)' },
|
| timeScale: { borderColor: 'rgba(0, 180, 180, 0.1)', timeVisible: true },
|
| });
|
|
|
| this.candlestickSeries = this.chart.addCandlestickSeries({
|
| upColor: '#00c896',
|
| downColor: '#e91e8c',
|
| borderUpColor: '#00c896',
|
| borderDownColor: '#e91e8c',
|
| wickUpColor: '#00c896',
|
| wickDownColor: '#e91e8c',
|
| });
|
|
|
|
|
| this.indicators.ema20 = this.chart.addLineSeries({
|
| color: '#00d4d4',
|
| lineWidth: 2,
|
| title: 'EMA 20',
|
| });
|
|
|
| this.indicators.ema50 = this.chart.addLineSeries({
|
| color: '#0088cc',
|
| lineWidth: 2,
|
| title: 'EMA 50',
|
| });
|
|
|
|
|
| this.indicators.volume = this.chart.addHistogramSeries({
|
| color: '#00d4d4',
|
| priceFormat: { type: 'volume' },
|
| priceScaleId: 'volume',
|
| });
|
|
|
| this.chart.priceScale('volume').applyOptions({
|
| scaleMargins: { top: 0.85, bottom: 0 },
|
| });
|
|
|
|
|
| new ResizeObserver(entries => {
|
| const { width, height } = entries[0].contentRect;
|
| this.chart.applyOptions({ width, height });
|
| }).observe(container);
|
| }
|
|
|
| bindEvents() {
|
|
|
| document.getElementById('symbolInput')?.addEventListener('change', (e) => {
|
| this.symbol = e.target.value.toUpperCase();
|
| this.loadData();
|
| });
|
|
|
|
|
| document.querySelectorAll('.tf-btn').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| document.querySelectorAll('.tf-btn').forEach(b => b.classList.remove('active'));
|
| e.target.classList.add('active');
|
| this.timeframe = e.target.dataset.tf;
|
| this.loadData();
|
| });
|
| });
|
|
|
|
|
| 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');
|
| this.loadStrategyTab(e.target.dataset.tab);
|
| });
|
| });
|
|
|
|
|
| document.getElementById('btnNewStrategy')?.addEventListener('click', () => {
|
| this.openStrategyModal();
|
| });
|
|
|
|
|
| document.getElementById('modalClose')?.addEventListener('click', () => {
|
| this.closeStrategyModal();
|
| });
|
|
|
| document.getElementById('strategyModal')?.addEventListener('click', (e) => {
|
| if (e.target.id === 'strategyModal') this.closeStrategyModal();
|
| });
|
|
|
|
|
| document.addEventListener('keydown', (e) => {
|
| if (e.key === 'Escape') this.closeStrategyModal();
|
| });
|
|
|
|
|
| document.getElementById('btnBacktest')?.addEventListener('click', () => {
|
| this.runBacktest();
|
| });
|
|
|
|
|
| document.getElementById('btnSaveStrategy')?.addEventListener('click', () => {
|
| this.saveStrategy();
|
| });
|
|
|
|
|
| document.getElementById('addEntryCondition')?.addEventListener('click', () => {
|
| this.addConditionRow('entryConditions');
|
| });
|
|
|
| document.getElementById('addExitCondition')?.addEventListener('click', () => {
|
| this.addConditionRow('exitConditions');
|
| });
|
| }
|
|
|
| async loadData(silent = false) {
|
| if (!silent) {
|
| document.getElementById('chartLoading')?.classList.remove('hidden');
|
| }
|
|
|
| try {
|
| const response = await fetch(
|
| `https://api.binance.com/api/v3/klines?symbol=${this.symbol}&interval=${this.timeframe}&limit=500`,
|
| { signal: AbortSignal.timeout(15000) }
|
| );
|
|
|
| if (!response.ok) throw new Error('Failed to fetch data');
|
|
|
| const rawData = await response.json();
|
| this.data = rawData.map(c => ({
|
| time: Math.floor(c[0] / 1000),
|
| open: parseFloat(c[1]),
|
| high: parseFloat(c[2]),
|
| low: parseFloat(c[3]),
|
| close: parseFloat(c[4]),
|
| volume: parseFloat(c[5])
|
| }));
|
|
|
| this.updateChart();
|
| this.calculateIndicators();
|
| this.updateUI();
|
|
|
| if (!silent) {
|
| this.showToast('Data Loaded', `${this.data.length} candles loaded`, 'success');
|
| }
|
|
|
| } catch (error) {
|
| console.error('[TradingProV3] Error:', error);
|
| this.showToast('Error', error.message, 'error');
|
| } finally {
|
| document.getElementById('chartLoading')?.classList.add('hidden');
|
| }
|
| }
|
|
|
| updateChart() {
|
| if (!this.candlestickSeries || !this.data.length) return;
|
|
|
| this.candlestickSeries.setData(this.data);
|
|
|
|
|
| const volumeData = this.data.map(d => ({
|
| time: d.time,
|
| value: d.volume,
|
| color: d.close > d.open ? 'rgba(0, 200, 150, 0.5)' : 'rgba(233, 30, 140, 0.5)'
|
| }));
|
| this.indicators.volume?.setData(volumeData);
|
|
|
| this.chart.timeScale().fitContent();
|
| }
|
|
|
| calculateIndicators() {
|
| if (!this.data.length) return;
|
|
|
| const closes = this.data.map(d => d.close);
|
|
|
|
|
| const ema20 = this.calculateEMA(closes, 20);
|
| this.indicators.ema20?.setData(
|
| ema20.map((val, i) => ({ time: this.data[i].time, value: val }))
|
| );
|
|
|
|
|
| const ema50 = this.calculateEMA(closes, 50);
|
| this.indicators.ema50?.setData(
|
| ema50.map((val, i) => ({ time: this.data[i].time, value: val }))
|
| );
|
|
|
|
|
| const rsi = this.calculateRSI(closes, 14);
|
| const latestRSI = rsi[rsi.length - 1];
|
|
|
|
|
| const macd = this.calculateMACD(closes);
|
| const latestMACD = macd.histogram[macd.histogram.length - 1];
|
|
|
|
|
| const rsiEl = document.getElementById('rsiValue');
|
| if (rsiEl) {
|
| rsiEl.textContent = latestRSI.toFixed(1);
|
| rsiEl.className = 'metric-value ' + (latestRSI > 70 ? 'bearish' : latestRSI < 30 ? 'bullish' : '');
|
| }
|
|
|
| const macdEl = document.getElementById('macdValue');
|
| if (macdEl) {
|
| macdEl.textContent = latestMACD > 0 ? 'Bullish' : 'Bearish';
|
| macdEl.className = 'metric-value ' + (latestMACD > 0 ? 'bullish' : 'bearish');
|
| }
|
|
|
| const emaTrendEl = document.getElementById('emaTrend');
|
| if (emaTrendEl) {
|
| const trend = ema20[ema20.length - 1] > ema50[ema50.length - 1] ? 'Uptrend' : 'Downtrend';
|
| emaTrendEl.textContent = trend;
|
| emaTrendEl.className = 'metric-value ' + (trend === 'Uptrend' ? 'bullish' : 'bearish');
|
| }
|
|
|
|
|
| this.generateSignal(latestRSI, latestMACD, ema20, ema50);
|
| }
|
|
|
| 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;
|
| }
|
|
|
| calculateRSI(values, period = 14) {
|
| const rsi = [];
|
| let gains = 0, losses = 0;
|
|
|
| for (let i = 1; i <= period; i++) {
|
| const change = values[i] - values[i - 1];
|
| if (change > 0) gains += change;
|
| else losses += Math.abs(change);
|
| }
|
|
|
| let avgGain = gains / period;
|
| let avgLoss = losses / period;
|
| rsi.push(100 - (100 / (1 + avgGain / (avgLoss || 0.001))));
|
|
|
| for (let i = period + 1; i < values.length; i++) {
|
| const change = values[i] - values[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;
|
|
|
| rsi.push(100 - (100 / (1 + avgGain / (avgLoss || 0.001))));
|
| }
|
|
|
| return rsi;
|
| }
|
|
|
| calculateMACD(values) {
|
| const ema12 = this.calculateEMA(values, 12);
|
| const ema26 = this.calculateEMA(values, 26);
|
| const macdLine = ema12.map((v, i) => v - ema26[i]);
|
| const signalLine = this.calculateEMA(macdLine, 9);
|
| const histogram = macdLine.map((v, i) => v - signalLine[i]);
|
| return { macdLine, signalLine, histogram };
|
| }
|
|
|
| generateSignal(rsi, macdHist, ema20, ema50) {
|
| const latest = {
|
| ema20: ema20[ema20.length - 1],
|
| ema50: ema50[ema50.length - 1]
|
| };
|
|
|
| let signal = 'HOLD';
|
| let confidence = 50;
|
|
|
| if (latest.ema20 > latest.ema50 && rsi > 50 && rsi < 70 && macdHist > 0) {
|
| signal = 'STRONG BUY';
|
| confidence = 85;
|
| } else if (latest.ema20 > latest.ema50 && macdHist > 0) {
|
| signal = 'BUY';
|
| confidence = 70;
|
| } else if (latest.ema20 < latest.ema50 && rsi < 50 && rsi > 30 && macdHist < 0) {
|
| signal = 'STRONG SELL';
|
| confidence = 85;
|
| } else if (latest.ema20 < latest.ema50 && macdHist < 0) {
|
| signal = 'SELL';
|
| confidence = 70;
|
| }
|
|
|
| const badgeEl = document.getElementById('signalBadge');
|
| if (badgeEl) {
|
| badgeEl.textContent = signal;
|
| badgeEl.className = 'signal-badge ' + (signal.includes('BUY') ? 'buy' : signal.includes('SELL') ? 'sell' : 'hold');
|
| }
|
|
|
| const confEl = document.getElementById('confidence');
|
| if (confEl) {
|
| confEl.textContent = confidence + '%';
|
| confEl.className = 'metric-value ' + (confidence > 70 ? 'bullish' : 'bearish');
|
| }
|
| }
|
|
|
| updateUI() {
|
| if (!this.data.length) return;
|
|
|
| const latest = this.data[this.data.length - 1];
|
| const prev = this.data[this.data.length - 2];
|
| const change = ((latest.close - prev.close) / prev.close) * 100;
|
|
|
| document.getElementById('currentPrice').textContent =
|
| `$${latest.close.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
|
|
|
| const changeEl = document.getElementById('priceChange');
|
| if (changeEl) {
|
| changeEl.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`;
|
| changeEl.className = 'price-change ' + (change >= 0 ? 'positive' : 'negative');
|
| }
|
|
|
| document.getElementById('currentLevel').textContent =
|
| `$${latest.close.toLocaleString('en-US', { minimumFractionDigits: 0 })}`;
|
|
|
|
|
| const recentData = this.data.slice(-50);
|
| const resistance = Math.max(...recentData.map(d => d.high));
|
| const support = Math.min(...recentData.map(d => d.low));
|
|
|
| document.getElementById('resistance').textContent =
|
| `$${resistance.toLocaleString('en-US', { minimumFractionDigits: 0 })}`;
|
| document.getElementById('support').textContent =
|
| `$${support.toLocaleString('en-US', { minimumFractionDigits: 0 })}`;
|
|
|
| document.getElementById('lastUpdate').textContent =
|
| new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
| }
|
|
|
|
|
|
|
| loadStrategiesFromStorage() {
|
| try {
|
| const saved = localStorage.getItem('tradingPro_strategies');
|
| if (saved) {
|
| this.strategies = JSON.parse(saved);
|
| } else {
|
|
|
| this.strategies = [
|
| {
|
| id: 'default_1',
|
| name: 'EMA Crossover + RSI',
|
| description: 'Buy when EMA20 crosses above EMA50 and RSI > 50',
|
| timeframe: '4h',
|
| riskPercent: 2,
|
| entryConditions: [
|
| { indicator: 'ema20', operator: 'crosses_above', value: 'ema50' },
|
| { indicator: 'rsi', operator: 'greater', value: '50' }
|
| ],
|
| exitConditions: [
|
| { indicator: 'tp', operator: 'equals', value: '3' },
|
| { indicator: 'sl', operator: 'equals', value: '1.5' }
|
| ],
|
| results: { winRate: 67, profitFactor: 2.3, trades: 156, maxDrawdown: 12 }
|
| },
|
| {
|
| id: 'default_2',
|
| name: 'RSI Reversal',
|
| description: 'Buy when RSI < 30, Sell when RSI > 70',
|
| timeframe: '1h',
|
| riskPercent: 1.5,
|
| entryConditions: [
|
| { indicator: 'rsi', operator: 'less', value: '30' }
|
| ],
|
| exitConditions: [
|
| { indicator: 'rsi', operator: 'greater', value: '70' },
|
| { indicator: 'sl', operator: 'equals', value: '2' }
|
| ],
|
| results: { winRate: 58, profitFactor: 1.8, trades: 89, maxDrawdown: 15 }
|
| },
|
| {
|
| id: 'default_3',
|
| name: 'MACD Momentum',
|
| description: 'Trade MACD histogram reversals',
|
| timeframe: '4h',
|
| riskPercent: 2,
|
| entryConditions: [
|
| { indicator: 'macd', operator: 'crosses_above', value: '0' }
|
| ],
|
| exitConditions: [
|
| { indicator: 'macd', operator: 'crosses_below', value: '0' },
|
| { indicator: 'sl', operator: 'equals', value: '2' }
|
| ],
|
| results: { winRate: 62, profitFactor: 2.1, trades: 124, maxDrawdown: 10 }
|
| }
|
| ];
|
| this.saveStrategiesToStorage();
|
| }
|
| } catch (e) {
|
| console.error('Error loading strategies:', e);
|
| this.strategies = [];
|
| }
|
| }
|
|
|
| saveStrategiesToStorage() {
|
| try {
|
| localStorage.setItem('tradingPro_strategies', JSON.stringify(this.strategies));
|
| } catch (e) {
|
| console.error('Error saving strategies:', e);
|
| }
|
| }
|
|
|
| renderStrategies() {
|
| const grid = document.getElementById('strategyGrid');
|
| if (!grid) return;
|
|
|
| grid.innerHTML = this.strategies.map((s, i) => `
|
| <div class="strategy-card ${this.currentStrategy?.id === s.id ? 'active' : ''}" data-id="${s.id}">
|
| <div class="strategy-name">
|
| ${this.getStrategyIcon(s.name)} ${s.name}
|
| </div>
|
| <div class="strategy-desc">${s.description}</div>
|
| <div class="strategy-stats">
|
| <div class="stat">
|
| <div class="stat-value" style="color: var(--success);">${s.results?.winRate || '--'}%</div>
|
| <div class="stat-label">Win Rate</div>
|
| </div>
|
| <div class="stat">
|
| <div class="stat-value" style="color: var(--accent-cyan);">${s.results?.profitFactor || '--'}</div>
|
| <div class="stat-label">Profit Factor</div>
|
| </div>
|
| <div class="stat">
|
| <div class="stat-value">${s.results?.trades || '--'}</div>
|
| <div class="stat-label">Trades</div>
|
| </div>
|
| </div>
|
| <div class="strategy-actions">
|
| <button class="btn-sm btn-edit" data-id="${s.id}">Edit</button>
|
| <button class="btn-sm btn-backtest" data-id="${s.id}">Backtest</button>
|
| <button class="btn-sm btn-apply" data-id="${s.id}">Apply</button>
|
| <button class="btn-sm btn-delete" data-id="${s.id}" style="margin-left: auto; color: var(--danger);">Delete</button>
|
| </div>
|
| </div>
|
| `).join('');
|
|
|
|
|
| grid.querySelectorAll('.btn-edit').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| const strategy = this.strategies.find(s => s.id === btn.dataset.id);
|
| if (strategy) this.openStrategyModal(strategy);
|
| });
|
| });
|
|
|
| grid.querySelectorAll('.btn-backtest').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| const strategy = this.strategies.find(s => s.id === btn.dataset.id);
|
| if (strategy) this.runBacktestForStrategy(strategy);
|
| });
|
| });
|
|
|
| grid.querySelectorAll('.btn-apply').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| const strategy = this.strategies.find(s => s.id === btn.dataset.id);
|
| if (strategy) this.applyStrategy(strategy);
|
| });
|
| });
|
|
|
| grid.querySelectorAll('.btn-delete').forEach(btn => {
|
| btn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| this.deleteStrategy(btn.dataset.id);
|
| });
|
| });
|
| }
|
|
|
| getStrategyIcon(name) {
|
| if (name.includes('EMA')) return '📈';
|
| if (name.includes('RSI')) return '🎯';
|
| if (name.includes('MACD')) return '🌊';
|
| if (name.includes('Scalp')) return '⚡';
|
| return '📊';
|
| }
|
|
|
| openStrategyModal(strategy = null) {
|
| this.editingStrategy = strategy;
|
|
|
| document.getElementById('modalTitle').textContent =
|
| strategy ? 'Edit Strategy' : 'Create New Strategy';
|
|
|
| document.getElementById('strategyName').value = strategy?.name || '';
|
| document.getElementById('strategyTimeframe').value = strategy?.timeframe || '4h';
|
| document.getElementById('riskPercent').value = strategy?.riskPercent || 2;
|
|
|
|
|
| document.getElementById('backtestPreview')?.classList.add('hidden');
|
|
|
| document.getElementById('strategyModal')?.classList.add('active');
|
| }
|
|
|
| closeStrategyModal() {
|
| document.getElementById('strategyModal')?.classList.remove('active');
|
| this.editingStrategy = null;
|
| }
|
|
|
| addConditionRow(containerId) {
|
| const container = document.getElementById(containerId);
|
| if (!container) return;
|
|
|
| const row = document.createElement('div');
|
| row.className = 'condition-row';
|
| row.innerHTML = `
|
| <select class="form-select">
|
| <option value="rsi">RSI (14)</option>
|
| <option value="ema20">EMA (20)</option>
|
| <option value="ema50">EMA (50)</option>
|
| <option value="macd">MACD</option>
|
| <option value="price">Price</option>
|
| <option value="tp">Take Profit (%)</option>
|
| <option value="sl">Stop Loss (%)</option>
|
| </select>
|
| <select class="form-select" style="width: auto;">
|
| <option value="crosses_above">Crosses Above</option>
|
| <option value="crosses_below">Crosses Below</option>
|
| <option value="greater">Greater Than</option>
|
| <option value="less">Less Than</option>
|
| <option value="equals">Equals</option>
|
| </select>
|
| <input type="text" class="form-input" placeholder="Value">
|
| <button class="btn-sm" style="color: var(--danger);" onclick="this.parentElement.remove()">×</button>
|
| `;
|
|
|
| container.insertBefore(row, container.lastElementChild);
|
| }
|
|
|
| saveStrategy() {
|
| const name = document.getElementById('strategyName').value.trim();
|
| if (!name) {
|
| this.showToast('Error', 'Please enter a strategy name', 'error');
|
| return;
|
| }
|
|
|
| const strategy = {
|
| id: this.editingStrategy?.id || `strategy_${Date.now()}`,
|
| name,
|
| description: `Custom strategy created on ${new Date().toLocaleDateString()}`,
|
| timeframe: document.getElementById('strategyTimeframe').value,
|
| riskPercent: parseFloat(document.getElementById('riskPercent').value) || 2,
|
| entryConditions: this.getConditionsFromContainer('entryConditions'),
|
| exitConditions: this.getConditionsFromContainer('exitConditions'),
|
| results: this.editingStrategy?.results || null
|
| };
|
|
|
| if (this.editingStrategy) {
|
| const index = this.strategies.findIndex(s => s.id === this.editingStrategy.id);
|
| if (index !== -1) this.strategies[index] = strategy;
|
| } else {
|
| this.strategies.push(strategy);
|
| }
|
|
|
| this.saveStrategiesToStorage();
|
| this.renderStrategies();
|
| this.closeStrategyModal();
|
| this.showToast('Strategy Saved', `"${name}" has been saved`, 'success');
|
| }
|
|
|
| getConditionsFromContainer(containerId) {
|
| const container = document.getElementById(containerId);
|
| if (!container) return [];
|
|
|
| const conditions = [];
|
| container.querySelectorAll('.condition-row').forEach(row => {
|
| const selects = row.querySelectorAll('select');
|
| const input = row.querySelector('input');
|
| if (selects.length >= 2 && input) {
|
| conditions.push({
|
| indicator: selects[0].value,
|
| operator: selects[1].value,
|
| value: input.value
|
| });
|
| }
|
| });
|
|
|
| return conditions;
|
| }
|
|
|
| deleteStrategy(id) {
|
| if (!confirm('Delete this strategy?')) return;
|
|
|
| this.strategies = this.strategies.filter(s => s.id !== id);
|
| this.saveStrategiesToStorage();
|
| this.renderStrategies();
|
| this.showToast('Strategy Deleted', 'Strategy has been removed', 'info');
|
| }
|
|
|
| applyStrategy(strategy) {
|
| this.currentStrategy = strategy;
|
| this.renderStrategies();
|
| this.showToast('Strategy Applied', `"${strategy.name}" is now active`, 'success');
|
|
|
|
|
| this.addStrategyMarkersToChart(strategy);
|
| }
|
|
|
|
|
|
|
| async runBacktest() {
|
| const preview = document.getElementById('backtestPreview');
|
| const status = document.getElementById('backtestStatus');
|
|
|
| preview?.classList.remove('hidden');
|
| status.textContent = 'Running...';
|
| status.className = 'backtest-status running';
|
|
|
|
|
| const entryConditions = this.getConditionsFromContainer('entryConditions');
|
| const exitConditions = this.getConditionsFromContainer('exitConditions');
|
|
|
|
|
| setTimeout(() => {
|
| const results = this.executeBacktest(entryConditions, exitConditions);
|
|
|
| document.getElementById('btWinRate').textContent = results.winRate.toFixed(1) + '%';
|
| document.getElementById('btProfitFactor').textContent = results.profitFactor.toFixed(2);
|
| document.getElementById('btTrades').textContent = results.totalTrades;
|
| document.getElementById('btDrawdown').textContent = results.maxDrawdown.toFixed(1) + '%';
|
|
|
| status.textContent = 'Complete';
|
| status.className = 'backtest-status complete';
|
|
|
|
|
| this.drawEquityCurve(results.equityCurve);
|
|
|
| this.showToast('Backtest Complete',
|
| `${results.totalTrades} trades, ${results.winRate.toFixed(1)}% win rate`, 'success');
|
| }, 1500);
|
| }
|
|
|
| async runBacktestForStrategy(strategy) {
|
| this.showToast('Backtesting', `Running backtest for "${strategy.name}"...`, 'info');
|
|
|
|
|
| const results = this.executeBacktest(strategy.entryConditions, strategy.exitConditions);
|
|
|
|
|
| strategy.results = {
|
| winRate: Math.round(results.winRate),
|
| profitFactor: parseFloat(results.profitFactor.toFixed(2)),
|
| trades: results.totalTrades,
|
| maxDrawdown: Math.round(results.maxDrawdown)
|
| };
|
|
|
| this.saveStrategiesToStorage();
|
| this.renderStrategies();
|
|
|
| this.showToast('Backtest Complete',
|
| `Win Rate: ${results.winRate.toFixed(1)}%, Profit Factor: ${results.profitFactor.toFixed(2)}`, 'success');
|
| }
|
|
|
| executeBacktest(entryConditions, exitConditions) {
|
| if (this.data.length < 100) {
|
| return { winRate: 0, profitFactor: 0, totalTrades: 0, maxDrawdown: 0, equityCurve: [] };
|
| }
|
|
|
| const closes = this.data.map(d => d.close);
|
| const rsi = this.calculateRSI(closes, 14);
|
| const ema20 = this.calculateEMA(closes, 20);
|
| const ema50 = this.calculateEMA(closes, 50);
|
| const macd = this.calculateMACD(closes);
|
|
|
| let position = null;
|
| let trades = [];
|
| let equity = 10000;
|
| let equityCurve = [{ time: this.data[50].time, value: equity }];
|
| let maxEquity = equity;
|
| let maxDrawdown = 0;
|
|
|
|
|
| let tpPercent = 3;
|
| let slPercent = 1.5;
|
| exitConditions.forEach(c => {
|
| if (c.indicator === 'tp') tpPercent = parseFloat(c.value) || 3;
|
| if (c.indicator === 'sl') slPercent = parseFloat(c.value) || 1.5;
|
| });
|
|
|
|
|
| for (let i = 51; i < this.data.length; i++) {
|
| const candle = this.data[i];
|
| const prevCandle = this.data[i - 1];
|
|
|
| if (!position) {
|
|
|
| let shouldEnter = true;
|
|
|
| for (const cond of entryConditions) {
|
| const value = this.getIndicatorValue(cond.indicator, i, { rsi, ema20, ema50, macd, closes });
|
| const compareValue = this.getCompareValue(cond.value, i, { rsi, ema20, ema50, macd, closes });
|
| const prevValue = this.getIndicatorValue(cond.indicator, i - 1, { rsi, ema20, ema50, macd, closes });
|
|
|
| if (!this.evaluateCondition(value, cond.operator, compareValue, prevValue)) {
|
| shouldEnter = false;
|
| break;
|
| }
|
| }
|
|
|
| if (shouldEnter) {
|
| position = {
|
| type: 'long',
|
| entry: candle.close,
|
| entryTime: candle.time,
|
| tp: candle.close * (1 + tpPercent / 100),
|
| sl: candle.close * (1 - slPercent / 100)
|
| };
|
| }
|
| } else {
|
|
|
| let shouldExit = false;
|
| let exitPrice = candle.close;
|
| let exitReason = 'signal';
|
|
|
|
|
| if (candle.high >= position.tp) {
|
| shouldExit = true;
|
| exitPrice = position.tp;
|
| exitReason = 'tp';
|
| } else if (candle.low <= position.sl) {
|
| shouldExit = true;
|
| exitPrice = position.sl;
|
| exitReason = 'sl';
|
| }
|
|
|
|
|
| if (!shouldExit) {
|
| for (const cond of exitConditions) {
|
| if (cond.indicator === 'tp' || cond.indicator === 'sl') continue;
|
|
|
| const value = this.getIndicatorValue(cond.indicator, i, { rsi, ema20, ema50, macd, closes });
|
| const compareValue = this.getCompareValue(cond.value, i, { rsi, ema20, ema50, macd, closes });
|
| const prevValue = this.getIndicatorValue(cond.indicator, i - 1, { rsi, ema20, ema50, macd, closes });
|
|
|
| if (this.evaluateCondition(value, cond.operator, compareValue, prevValue)) {
|
| shouldExit = true;
|
| exitReason = 'signal';
|
| break;
|
| }
|
| }
|
| }
|
|
|
| if (shouldExit) {
|
| const pnlPercent = ((exitPrice - position.entry) / position.entry) * 100;
|
| const pnl = equity * (pnlPercent / 100);
|
| equity += pnl;
|
|
|
| trades.push({
|
| entry: position.entry,
|
| exit: exitPrice,
|
| entryTime: position.entryTime,
|
| exitTime: candle.time,
|
| pnl: pnlPercent,
|
| reason: exitReason
|
| });
|
|
|
| equityCurve.push({ time: candle.time, value: equity });
|
|
|
| maxEquity = Math.max(maxEquity, equity);
|
| const drawdown = ((maxEquity - equity) / maxEquity) * 100;
|
| maxDrawdown = Math.max(maxDrawdown, drawdown);
|
|
|
| position = null;
|
| }
|
| }
|
| }
|
|
|
|
|
| const wins = trades.filter(t => t.pnl > 0);
|
| const losses = trades.filter(t => t.pnl <= 0);
|
| const winRate = trades.length > 0 ? (wins.length / trades.length) * 100 : 0;
|
|
|
| const avgWin = wins.length > 0 ? wins.reduce((a, t) => a + t.pnl, 0) / wins.length : 0;
|
| const avgLoss = losses.length > 0 ? Math.abs(losses.reduce((a, t) => a + t.pnl, 0) / losses.length) : 1;
|
| const profitFactor = avgLoss > 0 ? avgWin / avgLoss : avgWin;
|
|
|
| return {
|
| winRate,
|
| profitFactor: Math.max(0, profitFactor),
|
| totalTrades: trades.length,
|
| maxDrawdown,
|
| equityCurve,
|
| trades
|
| };
|
| }
|
|
|
| getIndicatorValue(indicator, index, indicators) {
|
| switch (indicator) {
|
| case 'rsi': return indicators.rsi[index - 14] || 50;
|
| case 'ema20': return indicators.ema20[index] || 0;
|
| case 'ema50': return indicators.ema50[index] || 0;
|
| case 'macd': return indicators.macd.histogram[index] || 0;
|
| case 'price': return indicators.closes[index] || 0;
|
| default: return 0;
|
| }
|
| }
|
|
|
| getCompareValue(value, index, indicators) {
|
| if (value === 'ema20') return indicators.ema20[index] || 0;
|
| if (value === 'ema50') return indicators.ema50[index] || 0;
|
| if (value === '0') return 0;
|
| return parseFloat(value) || 0;
|
| }
|
|
|
| evaluateCondition(value, operator, compareValue, prevValue = null) {
|
| switch (operator) {
|
| case 'greater': return value > compareValue;
|
| case 'less': return value < compareValue;
|
| case 'equals': return Math.abs(value - compareValue) < 0.01;
|
| case 'crosses_above': return prevValue !== null && prevValue <= compareValue && value > compareValue;
|
| case 'crosses_below': return prevValue !== null && prevValue >= compareValue && value < compareValue;
|
| default: return false;
|
| }
|
| }
|
|
|
| drawEquityCurve(curve) {
|
| const container = document.getElementById('equityCurve');
|
| if (!container || curve.length < 2) return;
|
|
|
|
|
| const width = container.offsetWidth - 40;
|
| const height = 130;
|
| const padding = 20;
|
|
|
| const values = curve.map(c => c.value);
|
| const min = Math.min(...values);
|
| const max = Math.max(...values);
|
| const range = max - min || 1;
|
|
|
| const points = curve.map((c, i) => {
|
| const x = padding + (i / (curve.length - 1)) * (width - padding * 2);
|
| const y = height - padding - ((c.value - min) / range) * (height - padding * 2);
|
| return `${x},${y}`;
|
| });
|
|
|
| container.innerHTML = `
|
| <svg width="100%" height="${height}" viewBox="0 0 ${width} ${height}">
|
| <defs>
|
| <linearGradient id="equityGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
| <stop offset="0%" style="stop-color:#00d4d4"/>
|
| <stop offset="100%" style="stop-color:#00c896"/>
|
| </linearGradient>
|
| </defs>
|
| <polyline
|
| points="${points.join(' ')}"
|
| fill="none"
|
| stroke="url(#equityGrad)"
|
| stroke-width="3"
|
| stroke-linecap="round"
|
| stroke-linejoin="round"
|
| />
|
| <text x="${padding}" y="${height - 5}" fill="#5a6b7c" font-size="10">Start</text>
|
| <text x="${width - padding - 20}" y="${height - 5}" fill="#5a6b7c" font-size="10">End</text>
|
| </svg>
|
| `;
|
| }
|
|
|
| addStrategyMarkersToChart(strategy) {
|
|
|
| if (this.markers.length) {
|
| this.candlestickSeries.setMarkers([]);
|
| this.markers = [];
|
| }
|
|
|
|
|
| const results = this.executeBacktest(strategy.entryConditions, strategy.exitConditions);
|
|
|
| this.markers = results.trades.flatMap(trade => [
|
| {
|
| time: trade.entryTime,
|
| position: 'belowBar',
|
| color: '#00c896',
|
| shape: 'arrowUp',
|
| text: 'Buy'
|
| },
|
| {
|
| time: trade.exitTime,
|
| position: 'aboveBar',
|
| color: trade.pnl > 0 ? '#00c896' : '#e91e8c',
|
| shape: 'arrowDown',
|
| text: trade.reason === 'tp' ? 'TP' : trade.reason === 'sl' ? 'SL' : 'Exit'
|
| }
|
| ]);
|
|
|
| this.candlestickSeries.setMarkers(this.markers);
|
| this.showToast('Strategy Applied', `${results.trades.length} trade signals displayed on chart`, 'info');
|
| }
|
|
|
| loadStrategyTab(tab) {
|
| const content = document.getElementById('strategyContent');
|
| if (!content) return;
|
|
|
| switch (tab) {
|
| case 'strategies':
|
| this.renderStrategies();
|
| break;
|
| case 'backtest':
|
| content.innerHTML = `
|
| <div style="padding: 2rem; text-align: center; color: var(--text-secondary);">
|
| <p>Select a strategy and click "Backtest" to see detailed results.</p>
|
| </div>
|
| `;
|
| break;
|
| case 'results':
|
| content.innerHTML = `
|
| <div style="padding: 2rem; text-align: center; color: var(--text-secondary);">
|
| <p>Apply a strategy to see live trading results here.</p>
|
| </div>
|
| `;
|
| break;
|
| }
|
| }
|
|
|
| showToast(title, message, type = 'info') {
|
| const container = document.getElementById('toastContainer');
|
| if (!container) return;
|
|
|
| const toast = document.createElement('div');
|
| toast.className = `toast ${type}`;
|
| toast.innerHTML = `
|
| <div style="flex: 1;">
|
| <div style="font-weight: 600; font-size: 0.9rem;">${title}</div>
|
| <div style="font-size: 0.8rem; color: var(--text-secondary);">${message}</div>
|
| </div>
|
| <button style="background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 4px;" onclick="this.parentElement.remove()">×</button>
|
| `;
|
|
|
| container.appendChild(toast);
|
|
|
| setTimeout(() => {
|
| toast.classList.add('removing');
|
| setTimeout(() => toast.remove(), 300);
|
| }, 5000);
|
| }
|
| }
|
|
|
|
|
| if (document.readyState === 'loading') {
|
| document.addEventListener('DOMContentLoaded', () => new TradingProV3().init());
|
| } else {
|
| new TradingProV3().init();
|
| }
|
|
|
|
|