mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 11:17:44 +00:00
79d860e9a8
✨ Major Features Added: • Complete Indonesian & English bilingual support (80+ UI elements) • Global language switcher with persistent preferences • Smart MT5 data integration with web controls • One-click market data download for 50+ instruments • Automated MT5 installer and setup guide • Enhanced settings with profile and preferences management • Professional documentation and troubleshooting guides 🚀 Technical Improvements: • Internationalization (i18n) system with extensible architecture • MT5 environment detection with graceful fallbacks • API architecture for future cross-platform expansion • Improved error handling and user guidance • Cross-browser compatibility and responsive design 📚 Documentation: • 50+ page comprehensive MT5 setup guide • Automated installer with dependency management • Public roadmap announcing QuantumBotX API development • Updated CHANGELOG and platform compatibility notices 🎯 Platform Strategy: • Clear Windows-first positioning for MT5 learning platform • Preparatory work for QuantumBotX API cloud platform • Education-first approach maintaining Indonesian heritage • Foundation for international expansion Co-authored-by: AI Assistant <assistant@quantumbotx.ai>
388 lines
17 KiB
JavaScript
388 lines
17 KiB
JavaScript
// static/js/trading_bots.js - VERSI PERBAIKAN
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// --- Elemen DOM ---
|
|
const tableBody = document.getElementById('bots-table-body');
|
|
const modal = document.getElementById('create-bot-modal');
|
|
const form = document.getElementById('create-bot-form');
|
|
const modalTitle = document.getElementById('modal-title');
|
|
const submitBtn = document.getElementById('submit-bot-btn'); // <-- 1. Ambil elemen tombol
|
|
const createBotBtn = document.getElementById('create-bot-btn');
|
|
const startAllBtn = document.getElementById('start-all-btn');
|
|
const stopAllBtn = document.getElementById('stop-all-btn');
|
|
const cancelBtn = document.getElementById('cancel-create');
|
|
const cancelBtnFooter = document.getElementById('cancel-create-footer'); // Tombol Batal di footer
|
|
const paramsContainer = document.getElementById('strategy-params-container');
|
|
const strategySelect = document.getElementById('strategy');
|
|
let currentBotId = null; // Variabel untuk melacak bot yang sedang diedit
|
|
|
|
// --- Fungsi ---
|
|
|
|
// Fungsi untuk mengisi nilai parameter strategi saat mengedit bot
|
|
function fillStrategyParams(params) {
|
|
if (!params) return;
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
const inputElement = document.getElementById(key);
|
|
if (inputElement) {
|
|
inputElement.value = value;
|
|
}
|
|
});
|
|
}
|
|
// Fungsi untuk memuat daftar strategi ke dalam form
|
|
async function loadStrategies() {
|
|
try {
|
|
const response = await fetch('/api/strategies');
|
|
if (!response.ok) throw new Error('Gagal memuat strategi.');
|
|
const strategies = await response.json();
|
|
|
|
if (strategySelect) {
|
|
strategySelect.innerHTML = '<option value="" disabled selected>Pilih sebuah strategi</option>';
|
|
strategies.forEach(strategy => {
|
|
const option = document.createElement('option');
|
|
option.value = strategy.id;
|
|
option.textContent = strategy.name;
|
|
strategySelect.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading strategies:', error);
|
|
if (strategySelect) {
|
|
strategySelect.innerHTML = '<option value="">Gagal memuat strategi</option>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk mengambil dan menampilkan semua bot
|
|
async function fetchBots() {
|
|
try {
|
|
const response = await fetch('/api/bots');
|
|
let bots = await response.json();
|
|
|
|
// Sort bots: Active bots (status 'Aktif') first, then inactive
|
|
bots.sort((a, b) => {
|
|
if (a.status === 'Aktif' && b.status !== 'Aktif') return -1;
|
|
if (a.status !== 'Aktif' && b.status === 'Aktif') return 1;
|
|
return a.name.localeCompare(b.name); // Secondary sort by name
|
|
});
|
|
|
|
tableBody.innerHTML = ''; // Kosongkan tabel sebelum mengisi
|
|
|
|
if (bots.length === 0) {
|
|
tableBody.innerHTML = '<tr><td colspan="5" class="text-center p-6 text-gray-500">Belum ada bot yang dibuat.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
bots.forEach(bot => {
|
|
const statusClass = bot.status === 'Aktif' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800';
|
|
const startStopButton = bot.status === 'Aktif'
|
|
? `<button data-action="stop" data-id="${bot.id}" class="text-yellow-600 hover:text-yellow-900" title="Hentikan Bot"><i class="fas fa-pause-circle fa-lg"></i></button>`
|
|
: `<button data-action="start" data-id="${bot.id}" class="text-green-600 hover:text-green-900" title="Jalankan Bot"><i class="fas fa-play-circle fa-lg"></i></button>`;
|
|
|
|
const row = `
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-4 py-4">
|
|
<div class="font-medium text-gray-900">${bot.name}</div>
|
|
<div class="text-sm text-gray-500">${bot.market}</div>
|
|
</td>
|
|
<td class="px-4 py-4 text-sm text-gray-500">
|
|
<div>Risk: ${bot.lot_size}%</div>
|
|
<div>SL: ${bot.sl_pips}x ATR | TP: ${bot.tp_pips}x ATR</div>
|
|
</td>
|
|
<td class="px-4 py-4 text-sm text-gray-500">
|
|
<div>Strategi: ${bot.strategy_name}</div>
|
|
<div>TF: ${bot.timeframe} | Interval: ${bot.check_interval_seconds}s</div>
|
|
</td>
|
|
<td class="px-4 py-4">
|
|
<span class="px-2 py-1 rounded-full text-xs font-semibold ${statusClass}">${bot.status}</span>
|
|
</td>
|
|
<td class="px-4 py-4 text-center">
|
|
<div class="flex justify-center items-center gap-4">
|
|
${startStopButton}
|
|
<a href="/bots/${bot.id}" class="text-blue-600 hover:text-blue-900" title="Lihat Detail & Analisis"><i class="fas fa-chart-line"></i></a>
|
|
<button data-action="edit" data-id="${bot.id}" class="text-gray-600 hover:text-gray-900" title="Edit Bot"><i class="fas fa-pencil-alt"></i></button>
|
|
<button data-action="delete" data-id="${bot.id}" class="text-red-600 hover:text-red-900" title="Hapus Bot"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
tableBody.innerHTML += row;
|
|
});
|
|
} catch (err) {
|
|
console.error("Gagal memuat bot:", err);
|
|
tableBody.innerHTML = '<tr><td colspan="5" class="text-center p-6 text-red-500">Gagal memuat data bot.</td></tr>';
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
|
|
// Check for symbol parameter in URL when page loads
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const symbolFromUrl = urlParams.get('symbol');
|
|
|
|
// Buka modal untuk membuat bot baru
|
|
createBotBtn.addEventListener("click", () => {
|
|
currentBotId = null;
|
|
paramsContainer.innerHTML = ''; // Kosongkan parameter
|
|
form.reset();
|
|
submitBtn.textContent = 'Buat Bot'; // <-- 2. Set teks untuk mode 'Create'
|
|
modalTitle.textContent = '🚀 Buat Bot Baru';
|
|
// Set nilai default
|
|
form.elements.risk_percent.value = 1.0;
|
|
form.elements.timeframe.value = 'H1';
|
|
form.elements.sl_atr_multiplier.value = 2.0;
|
|
form.elements.tp_atr_multiplier.value = 4.0;
|
|
form.elements.check_interval_seconds.value = 60;
|
|
|
|
// If there's a symbol from URL, pre-fill the market field
|
|
if (symbolFromUrl) {
|
|
form.elements.market.value = symbolFromUrl;
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
});
|
|
|
|
// Event listener untuk tombol Start All dengan UX Improvement
|
|
startAllBtn.addEventListener('click', async () => {
|
|
if (!confirm('Apakah Anda yakin ingin menjalankan semua bot yang sedang dijeda?')) return;
|
|
|
|
const originalHtml = startAllBtn.innerHTML;
|
|
startAllBtn.disabled = true;
|
|
startAllBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i> Memulai...`;
|
|
|
|
try {
|
|
const res = await fetch('/api/bots/start_all', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (res.ok) {
|
|
alert(`✅ ${result.message}`);
|
|
fetchBots(); // Refresh tabel
|
|
} else {
|
|
alert(`❌ ${result.error}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error starting all bots:', err);
|
|
alert('❌ Gagal terhubung ke server.');
|
|
} finally {
|
|
startAllBtn.disabled = false;
|
|
startAllBtn.innerHTML = originalHtml;
|
|
}
|
|
});
|
|
|
|
// Event listener untuk tombol Stop All dengan UX Improvement
|
|
stopAllBtn.addEventListener('click', async () => {
|
|
if (!confirm('Apakah Anda yakin ingin menghentikan semua bot yang sedang berjalan?')) return;
|
|
|
|
const originalHtml = stopAllBtn.innerHTML;
|
|
stopAllBtn.disabled = true;
|
|
stopAllBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i> Menghentikan...`;
|
|
|
|
try {
|
|
const res = await fetch('/api/bots/stop_all', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (res.ok) {
|
|
alert(`✅ ${result.message}`);
|
|
fetchBots(); // Refresh tabel
|
|
} else {
|
|
alert(`❌ ${result.error}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error stopping all bots:', err);
|
|
alert('❌ Gagal terhubung ke server.');
|
|
} finally {
|
|
stopAllBtn.disabled = false;
|
|
stopAllBtn.innerHTML = originalHtml;
|
|
}
|
|
});
|
|
|
|
// Tutup modal
|
|
function closeModal() {
|
|
modal.classList.add('hidden');
|
|
}
|
|
|
|
cancelBtn.addEventListener("click", closeModal);
|
|
cancelBtnFooter.addEventListener("click", closeModal);
|
|
|
|
// Submit form (untuk membuat atau mengedit bot)
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(form);
|
|
const data = {};
|
|
formData.forEach((value, key) => {
|
|
// Ganti 'lot_size' dengan 'risk_percent'
|
|
if (['risk_percent', 'sl_atr_multiplier', 'tp_atr_multiplier', 'check_interval_seconds'].includes(key)) {
|
|
data[key] = parseFloat(value);
|
|
} else {
|
|
data[key] = value;
|
|
}
|
|
});
|
|
|
|
// Kumpulkan parameter strategi dinamis
|
|
const params = {};
|
|
const paramInputs = paramsContainer.querySelectorAll('input');
|
|
paramInputs.forEach(input => {
|
|
params[input.name] = parseFloat(input.value) || input.value;
|
|
});
|
|
data.params = params;
|
|
|
|
// Tambahkan pengaturan strategy switching
|
|
data.enable_strategy_switching = document.getElementById('enable_strategy_switching').checked;
|
|
|
|
const url = currentBotId ? `/api/bots/${currentBotId}` : '/api/bots';
|
|
const method = currentBotId ? 'PUT' : 'POST';
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
const result = await res.json();
|
|
|
|
if (res.ok) {
|
|
alert(`✅ ${result.message || 'Operasi berhasil'}`);
|
|
modal.classList.add('hidden');
|
|
fetchBots(); // Refresh tabel setelah berhasil
|
|
} else {
|
|
alert(`❌ Gagal: ${result.error || 'Terjadi kesalahan tidak diketahui'}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error submitting bot form:', err);
|
|
alert("❌ Gagal terhubung ke server.");
|
|
}
|
|
});
|
|
|
|
// Event listener untuk tombol aksi di tabel (start, stop, edit, delete)
|
|
tableBody.addEventListener('click', async (e) => {
|
|
const button = e.target.closest('button[data-action]');
|
|
if (!button) return;
|
|
|
|
const action = button.dataset.action;
|
|
const botId = button.dataset.id;
|
|
|
|
if (action === 'edit') {
|
|
// 1. Tampilkan modal & loading overlay segera untuk respons instan
|
|
modalTitle.textContent = '✏️ Memuat Data Bot...';
|
|
submitBtn.textContent = 'Ubah Bot';
|
|
form.reset();
|
|
paramsContainer.innerHTML = '';
|
|
modal.classList.remove('hidden');
|
|
const loadingOverlay = document.getElementById('modal-loading-overlay');
|
|
if(loadingOverlay) loadingOverlay.classList.remove('hidden');
|
|
|
|
try {
|
|
const res = await fetch(`/api/bots/${botId}`);
|
|
const bot = await res.json();
|
|
|
|
if (res.ok) {
|
|
modalTitle.textContent = '✏️ Edit Bot'; // Perbarui judul setelah data dimuat
|
|
currentBotId = botId;
|
|
|
|
// 2. Isi form dengan data yang sudah diambil
|
|
for (const key in bot) {
|
|
if (form.elements[key]) {
|
|
form.elements[key].value = bot[key];
|
|
}
|
|
}
|
|
|
|
// Set the strategy switching checkbox
|
|
if (form.elements.enable_strategy_switching) {
|
|
form.elements.enable_strategy_switching.checked = bot.enable_strategy_switching == 1;
|
|
}
|
|
|
|
// 3. Trigger event untuk memuat parameter strategi
|
|
strategySelect.dispatchEvent(new Event('change', { 'bubbles': true }));
|
|
|
|
// 4. Tunggu sebentar lalu isi parameter strategi
|
|
await new Promise(resolve => setTimeout(() => {
|
|
if (bot.strategy_params) {
|
|
fillStrategyParams(bot.strategy_params);
|
|
}
|
|
resolve();
|
|
}, 250));
|
|
|
|
} else {
|
|
alert(`❌ Gagal memuat data bot: ${bot.error}`);
|
|
modal.classList.add('hidden'); // Sembunyikan modal jika gagal
|
|
}
|
|
} catch (err) {
|
|
console.error(`Error fetching bot ${botId} for edit:`, err);
|
|
alert('❌ Gagal terhubung ke server untuk mengedit.');
|
|
modal.classList.add('hidden'); // Sembunyikan modal jika gagal
|
|
} finally {
|
|
// 5. Sembunyikan loading overlay setelah semua selesai
|
|
if(loadingOverlay) loadingOverlay.classList.add('hidden');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (action === 'delete') {
|
|
if (!confirm('Apakah Anda yakin ingin menghapus bot ini?')) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let endpoint = `/api/bots/${botId}`;
|
|
let method = 'POST';
|
|
|
|
if (action === 'delete') {
|
|
method = 'DELETE';
|
|
} else if (action === 'start' || action === 'stop') {
|
|
endpoint = `/api/bots/${botId}/${action}`;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(endpoint, { method });
|
|
const result = await res.json();
|
|
|
|
if (res.ok && !result.error) {
|
|
alert(`✅ ${result.message || 'Operasi berhasil'}`);
|
|
fetchBots(); // Refresh tabel
|
|
} else {
|
|
alert(`❌ ${result.error || 'Operasi gagal'}`);
|
|
}
|
|
} catch (err) {
|
|
console.error(`Error performing action '${action}' on bot ${botId}:`, err);
|
|
alert("❌ Gagal terhubung ke server untuk melakukan aksi.");
|
|
}
|
|
});
|
|
|
|
// Event listener untuk dropdown strategi
|
|
strategySelect.addEventListener('change', async (e) => {
|
|
const strategyId = e.target.value;
|
|
paramsContainer.innerHTML = '<p class="text-sm text-gray-500">Memuat parameter...</p>';
|
|
if (!strategyId) {
|
|
paramsContainer.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/strategies/${strategyId}/params`);
|
|
const params = await res.json();
|
|
paramsContainer.innerHTML = ''; // Kosongkan lagi
|
|
|
|
if (params.length > 0) {
|
|
params.forEach(param => {
|
|
const paramField = `
|
|
<div class="col-span-1">
|
|
<label for="${param.name}" class="block text-sm font-medium text-gray-700">${param.label}</label>
|
|
<input type="${param.type || 'number'}" name="${param.name}" id="${param.name}" value="${param.default}" step="${param.step || 'any'}"
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
</div>
|
|
`;
|
|
paramsContainer.innerHTML += paramField;
|
|
});
|
|
} else {
|
|
paramsContainer.innerHTML = '<p class="text-sm text-gray-500 col-span-2">Strategi ini tidak memiliki parameter kustom.</p>';
|
|
}
|
|
} catch (err) {
|
|
console.error('Gagal memuat parameter strategi:', err);
|
|
paramsContainer.innerHTML = '<p class="text-sm text-red-500 col-span-2">Gagal memuat parameter.</p>';
|
|
}
|
|
});
|
|
|
|
// --- Panggilan Awal ---
|
|
loadStrategies(); // Muat strategi saat halaman pertama kali dibuka
|
|
fetchBots(); // Ambil data bot saat halaman pertama kali dibuka
|
|
setInterval(fetchBots, 10000); // Refresh data bot setiap 10 detik
|
|
});
|