|
|
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
}
|
|
|
|
|
|
|