mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 19:27:44 +00:00
348acc6f43
This major commit introduces a new advanced trading strategy, resolves critical trading logic errors, and significantly enhances system stability and UI data accuracy.
Key Changes:
- **New Features & Strategies**:
- **Quantum Velocity Strategy**: Added 'quantum_velocity.py', a new hybrid strategy combining a long-term trend filter (EMA 200) with a volatility-based entry trigger (Bollinger Squeeze).
- **Hybrid Pro Backtester**: Added 'lab/backtester_hybrid_pro.py' to facilitate advanced, offline testing and optimization of the QuantumBotX Hybrid strategy.
- **Critical Trading Logic Fixes**:
- Resolved 'Invalid Stops' and 'Unsupported filling mode' errors by refactoring 'core/mt5/trade.py' to use dynamic point calculation and the FOK fill policy.
- Fixed a recurring 'KeyError' in 'BollingerSqueezeStrategy' by ensuring consistent naming for Bollinger Bands columns.
- **System Stability & Robustness**:
- Hardened the graceful shutdown handler in 'app.py' to prevent crashes from repeated Ctrl+C signals, ensuring a clean shutdown process.
- Enhanced the notification system by adding an 'is_notification' flag to logs, allowing for a clear distinction between critical alerts and general activity.
- **Dashboard & UI Enhancements**:
- Fixed the 'Total Bots' card on the dashboard by correcting the backend API ('api_dashboard.py') to send the complete stats payload.
- Replaced static '0' values on dashboard cards with loading spinners for a more professional user experience.
- The 'Create/Edit Bot' modal button now dynamically changes its text to 'Ubah Bot' when in edit mode.
194 lines
6.7 KiB
JavaScript
194 lines
6.7 KiB
JavaScript
// --- PUSAT KONTROL DASHBOARD ---
|
|
|
|
// Formatter untuk nilai USD
|
|
const formatter = new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
});
|
|
|
|
// Update statistik MT5 di dashboard
|
|
async function updateDashboardStats() {
|
|
try {
|
|
const response = await fetch('/api/dashboard/stats');
|
|
if (!response.ok) throw new Error('Gagal mengambil statistik dasbor');
|
|
const stats = await response.json();
|
|
|
|
document.getElementById('total-equity').textContent = formatter.format(stats.equity);
|
|
document.getElementById('todays-profit').textContent = formatter.format(stats.todays_profit);
|
|
document.getElementById('active-bots-count').textContent = stats.active_bots_count;
|
|
document.getElementById('total-bots-count').textContent = stats.total_bots;
|
|
|
|
// Warna profit/loss
|
|
const profitEl = document.getElementById('todays-profit');
|
|
if (profitEl) {
|
|
profitEl.classList.remove('text-green-500', 'text-red-500');
|
|
profitEl.classList.add(stats.todays_profit < 0 ? 'text-red-500' : 'text-green-500');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[DashboardStats] Error:', error);
|
|
}
|
|
}
|
|
|
|
// Tampilkan daftar bot aktif
|
|
async function fetchAllBots() {
|
|
try {
|
|
const response = await fetch('/api/bots');
|
|
if (!response.ok) throw new Error('Gagal mengambil daftar bot');
|
|
const bots = await response.json();
|
|
|
|
const listEl = document.getElementById('active-bots-list');
|
|
listEl.innerHTML = '';
|
|
|
|
const activeBots = bots.filter(bot => bot.status === 'Aktif');
|
|
|
|
if (activeBots.length === 0) {
|
|
listEl.innerHTML = '<p class="p-4 text-gray-500">Tidak ada bot yang sedang aktif.</p>';
|
|
} else {
|
|
const botsHtml = activeBots.map(bot => {
|
|
const badge = bot.strategy === 'MERCY_EDGE'
|
|
? `<span class="ml-2 px-2 py-0.5 text-xs bg-purple-100 text-purple-800 rounded-full">AI</span>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="p-4 hover:bg-gray-50 transition">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600">
|
|
<i class="fas fa-robot"></i>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="font-medium text-gray-800">
|
|
${bot.name} ${badge}
|
|
</p>
|
|
<p class="text-xs text-gray-500">${bot.market}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-xs text-gray-500">
|
|
Running <span class="inline-block w-2 h-2 rounded-full bg-green-500 ml-1"></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
listEl.innerHTML = botsHtml;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[FetchBots] Error:', error);
|
|
}
|
|
}
|
|
|
|
// Grafik harga
|
|
let priceChart;
|
|
async function updatePriceChart(symbol = 'EURUSD') {
|
|
try {
|
|
const response = await fetch(`/api/chart/data?symbol=${symbol}`);
|
|
const chartData = await response.json();
|
|
|
|
const ctx = document.getElementById('priceChart');
|
|
const config = {
|
|
type: 'line',
|
|
data: {
|
|
labels: chartData.labels,
|
|
datasets: [{
|
|
label: symbol,
|
|
data: chartData.data,
|
|
borderColor: '#3B82F6',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.05)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { beginAtZero: false },
|
|
x: { grid: { display: false } }
|
|
}
|
|
}
|
|
};
|
|
|
|
if (priceChart) {
|
|
priceChart.data.labels = chartData.labels;
|
|
priceChart.data.datasets[0].data = chartData.data;
|
|
priceChart.update();
|
|
} else {
|
|
|
|
priceChart = new Chart(ctx, config); // eslint-disable-line no-undef
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[Chart] Gagal update grafik:', error);
|
|
}
|
|
}
|
|
|
|
// Grafik RSI
|
|
let rsiChart;
|
|
async function updateRsiChart(symbol = 'EURUSD') {
|
|
try {
|
|
const response = await fetch(`/api/rsi_data?symbol=${symbol}&timeframe=H1`);
|
|
const rsiData = await response.json();
|
|
|
|
const ctx = document.getElementById('rsiChart');
|
|
const config = {
|
|
type: 'line',
|
|
data: {
|
|
labels: rsiData.timestamps,
|
|
datasets: [{
|
|
label: 'RSI',
|
|
data: rsiData.rsi_values,
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
borderWidth: 2,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: {
|
|
min: 0,
|
|
max: 100,
|
|
ticks: { stepSize: 10 },
|
|
grid: { color: 'rgba(0,0,0,0.05)' }
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (rsiChart) {
|
|
rsiChart.data.labels = rsiData.timestamps;
|
|
rsiChart.data.datasets[0].data = rsiData.rsi_values;
|
|
rsiChart.update();
|
|
} else {
|
|
rsiChart = new Chart(ctx, config); // eslint-disable-line no-undef
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[RSI Chart] Gagal update grafik RSI:', error);
|
|
const ctx = document.getElementById('rsiChart');
|
|
if (ctx && ctx.parentElement) {
|
|
ctx.parentElement.innerHTML = '<p class="text-center text-red-500">Gagal memuat data RSI.</p>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// DOM ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
updateDashboardStats();
|
|
fetchAllBots();
|
|
updatePriceChart();
|
|
updateRsiChart();
|
|
|
|
// Interval refresh
|
|
setInterval(updateDashboardStats, 10000);
|
|
setInterval(fetchAllBots, 5000);
|
|
});
|