Refactor and update forex and stock modal handling in JavaScript

Refactor the JavaScript code to use a single modal for both forex and stock profiles. Update error handling to display server error messages. Remove unused app.py and fetch.py files. Adjust various routes and strategies for consistency.
This commit is contained in:
Reynov Christian
2025-08-08 10:39:03 +08:00
parent 53ae5e8861
commit 7b74743d58
62 changed files with 499387 additions and 1002 deletions
-209
View File
@@ -1,209 +0,0 @@
# app.py - FINAL VERSION
import os
import logging
from logging.handlers import RotatingFileHandler
import atexit # <-- 1. Impor modul atexit
from flask import Flask, render_template, send_from_directory
from dotenv import load_dotenv
# Import modul inti yang diperlukan saat startup
import MetaTrader5 as mt5 # <-- 2. Impor MT5 secara langsung
from core.utils.mt5 import initialize_mt5
from core.bots.controller import ambil_semua_bot
# --- Konfigurasi Awal ---
# Muat environment variables dari file .env
load_dotenv()
# Siapkan sistem logging
log_dir = os.path.join(os.path.dirname(__file__), 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'app.log')
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)
# --- Inisialisasi Aplikasi Flask ---
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-here')
# --- Registrasi Blueprints ---
# Impor blueprints setelah 'app' dibuat untuk menghindari circular import
from core.routes.api_dashboard import api_dashboard
from core.routes.api_chart import api_chart
from core.routes.api_bots import api_bots
from core.routes.api_profile import api_profile
from core.routes.api_portfolio import api_portfolio
from core.routes.api_history import api_history
from core.routes.api_notifications import api_notifications
from core.routes.api_stocks import api_stocks
from core.routes.api_forex import api_forex
from core.routes.api_fundamentals import api_fundamentals
from core.routes.api_backtest import api_backtest
# Buat daftar semua blueprints untuk registrasi yang lebih rapi
blueprints = [
api_dashboard, api_chart, api_bots, api_profile, api_portfolio, api_history,
api_notifications, api_stocks, api_forex, api_fundamentals, api_backtest
]
# Daftarkan setiap blueprint ke aplikasi
for blueprint in blueprints:
app.register_blueprint(blueprint)
# --- Rute Halaman (Views) ---
@app.route('/')
def dashboard():
return render_template('index.html', active_page='dashboard')
@app.route('/trading_bots')
def bots_page():
return render_template('trading_bots.html', active_page='trading_bots')
@app.route('/bots/<int:bot_id>')
def bot_detail_page(bot_id):
return render_template('bot_detail.html', active_page='trading_bots') # Tetap di menu trading_bots
@app.route('/backtesting')
def backtesting_page():
return render_template('backtesting.html', active_page='backtesting')
@app.route('/portfolio')
def portfolio_page():
return render_template('portfolio.html', active_page='portfolio')
@app.route('/history')
def history_page():
return render_template('history.html', active_page='history')
@app.route('/settings')
def settings_page():
return render_template('settings.html', active_page='settings')
@app.route('/profile')
def profile_page():
return render_template('profile.html', active_page='profile') # Asumsi profile punya menu sendiri
@app.route('/notifications')
def notifications_page():
return render_template('notifications.html', active_page='notifications')
@app.route('/stocks')
def stocks_page():
return render_template('stocks.html', active_page='stocks')
@app.route('/forex')
def forex_page():
return render_template('forex.html', active_page='forex')
# --- Error Handlers & Rute Lain-lain ---
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
# Mencatat error ke log untuk debugging
logger.error(f"Internal Server Error: {error}", exc_info=True)
return render_template('500.html'), 500
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
# --- Fungsi Shutdown ---
def shutdown_handler():
"""Fungsi yang akan dipanggil saat aplikasi akan keluar."""
logger.info("Menerima sinyal shutdown. Memulai proses pembersihan...")
# Impor controller di sini untuk menghindari circular import
from core.bots import controller
# Hentikan semua bot yang aktif
active_bot_ids = list(controller.active_bots.keys())
if active_bot_ids:
logger.info(f"Menghentikan {len(active_bot_ids)} bot yang aktif...")
for bot_id in active_bot_ids:
try:
logger.info(f"Memberi sinyal berhenti untuk bot ID {bot_id}...")
controller.stop_bot(bot_id)
except KeyboardInterrupt:
# Abaikan Ctrl+C tambahan saat proses shutdown sedang berlangsung.
logger.warning(f"KeyboardInterrupt diterima saat menghentikan bot {bot_id}. Melanjutkan proses shutdown.")
continue
except Exception as e:
logger.error(f"Error tak terduga saat menghentikan bot {bot_id} selama shutdown: {e}")
mt5.shutdown() # Pastikan koneksi MT5 selalu ditutup
logger.info("Koneksi MetaTrader 5 ditutup. Proses shutdown selesai.")
# --- Filter Log Kustom ---
class RequestLogFilter(logging.Filter):
"""
Filter untuk menyembunyikan log permintaan (polling) yang tidak penting dari terminal.
"""
def filter(self, record):
# Pesan log dari Werkzeug terlihat seperti: "GET /api/path HTTP/1.1" 200 -
msg = record.getMessage()
# Daftar path yang ingin kita sembunyikan dari log konsol
paths_to_ignore = [
"GET /api/notifications/unread-count",
"GET /api/bots/analysis" # <-- Ini juga sering di-poll
]
return not any(path in msg for path in paths_to_ignore)
# --- Titik Eksekusi Utama ---
if __name__ == '__main__':
# Memuat kredensial MT5 dari .env dengan aman
try:
ACCOUNT = int(os.getenv('MT5_LOGIN'))
PASSWORD = os.getenv('MT5_PASSWORD')
SERVER = os.getenv('MT5_SERVER', 'MetaQuotes-Demo')
if not all([ACCOUNT, PASSWORD, SERVER]):
logger.critical("Kredensial MT5 (LOGIN/PASSWORD/SERVER) tidak lengkap di file .env.")
exit(1)
except (ValueError, TypeError):
logger.critical("Kredensial MT5_LOGIN harus berupa angka di file .env.")
exit(1)
# Inisialisasi koneksi ke MetaTrader 5
# PERBAIKAN: Jangan langsung exit. Coba hubungkan, tapi biarkan aplikasi tetap berjalan jika gagal.
# Koneksi akan dicoba lagi saat bot pertama kali dimulai.
if initialize_mt5(ACCOUNT, PASSWORD, SERVER):
logger.info("Berhasil terhubung ke MetaTrader 5.")
# Muat semua bot yang ada di database
try:
ambil_semua_bot()
logger.info("Semua bot dari database berhasil dimuat.")
except Exception as e:
logger.error(f"Terjadi kesalahan saat memuat bot: {e}", exc_info=True)
else:
logger.warning("GAGAL terhubung ke MetaTrader 5 saat startup. Aplikasi akan berjalan tanpa koneksi live.")
logger.warning("Fitur bot live tidak akan berfungsi sampai koneksi MT5 pulih.")
# --- Terapkan Filter Log ---
# Dapatkan logger bawaan Werkzeug dan tambahkan filter kita
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.addFilter(RequestLogFilter())
# --- 3. Daftarkan fungsi shutdown ---
# PERBAIKAN: Pindahkan ke luar blok if/else agar selalu dijalankan.
atexit.register(shutdown_handler)
# Jalankan aplikasi Flask
# PERBAIKAN: Pindahkan ke luar blok if/else agar server selalu berjalan.
app.run(
debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true',
host=os.getenv('FLASK_HOST', '127.0.0.1'),
port=int(os.getenv('FLASK_PORT', 5000)),
use_reloader=False # Penting: False untuk mencegah eksekusi ganda pada background thread
)
+135
View File
@@ -0,0 +1,135 @@
# core/__init__.py
import os
import logging
from logging.handlers import RotatingFileHandler
from flask import Flask, render_template, send_from_directory
from dotenv import load_dotenv
class RequestLogFilter(logging.Filter):
def filter(self, record):
msg = record.getMessage()
paths_to_ignore = [
"GET /api/notifications/unread-count",
"GET /api/bots/analysis"
]
return not any(path in msg for path in paths_to_ignore)
# ============================
# APPLICATION FACTORY FUNCTION
# ============================
def create_app():
"""
Membuat dan mengkonfigurasi instance aplikasi Flask.
"""
load_dotenv()
app = Flask(
__name__,
instance_relative_config=True,
template_folder='../templates',
static_folder='../static'
)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-here')
log_dir = os.path.join(app.root_path, '..', 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'app.log')
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.addFilter(RequestLogFilter())
app.logger.info("Aplikasi QuantumBotX sedang dimulai...")
from .routes.api_dashboard import api_dashboard
from .routes.api_chart import api_chart
from .routes.api_bots import api_bots
from .routes.api_profile import api_profile
from .routes.api_portfolio import api_portfolio
from .routes.api_history import api_history
from .routes.api_notifications import api_notifications
from .routes.api_stocks import api_stocks
from .routes.api_forex import api_forex
from .routes.api_fundamentals import api_fundamentals
from .routes.api_backtest import api_backtest
app.register_blueprint(api_dashboard)
app.register_blueprint(api_chart)
app.register_blueprint(api_bots)
app.register_blueprint(api_profile)
app.register_blueprint(api_portfolio)
app.register_blueprint(api_history)
app.register_blueprint(api_notifications)
app.register_blueprint(api_stocks)
app.register_blueprint(api_forex)
app.register_blueprint(api_fundamentals)
app.register_blueprint(api_backtest)
@app.route('/')
def dashboard():
return render_template('index.html', active_page='dashboard')
@app.route('/trading_bots')
def bots_page():
return render_template('trading_bots.html', active_page='trading_bots')
@app.route('/bots/<int:bot_id>')
def bot_detail_page(bot_id):
return render_template('bot_detail.html', active_page='trading_bots')
@app.route('/backtesting')
def backtesting_page():
return render_template('backtesting.html', active_page='backtesting')
@app.route('/backtest_history')
def backtest_history_page():
return render_template('backtest_history.html', active_page='backtesting')
@app.route('/portfolio')
def portfolio_page():
return render_template('portfolio.html', active_page='portfolio')
@app.route('/history')
def history_page():
return render_template('history.html', active_page='history')
@app.route('/settings')
def settings_page():
return render_template('settings.html', active_page='settings')
@app.route('/profile')
def profile_page():
return render_template('profile.html', active_page='profile')
@app.route('/notifications')
def notifications_page():
return render_template('notifications.html', active_page='notifications')
@app.route('/stocks')
def stocks_page():
return render_template('stocks.html', active_page='stocks')
@app.route('/forex')
def forex_page():
return render_template('forex.html', active_page='forex')
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
app.logger.error(f"Internal Server Error: {error}", exc_info=True)
return render_template('500.html'), 500
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
return app
+65 -55
View File
@@ -5,83 +5,92 @@ from core.strategies.strategy_map import STRATEGY_MAP
def run_backtest(strategy_id, params, historical_data_df):
"""
Menjalankan simulasi backtesting untuk strategi tertentu pada data historis.
VERSI OPTIMIZED: Indikator dihitung sekali di awal.
"""
strategy_class = STRATEGY_MAP.get(strategy_id)
if not strategy_class:
return {"error": "Strategi tidak ditemukan"}
# Inisialisasi state backtesting
trades = []
in_position = False
initial_capital = 10000 # Modal awal virtual $10,000
capital = initial_capital
equity_curve = [initial_capital]
peak_equity = initial_capital
max_drawdown = 0.0
position_type = None
entry_price = 0.0
sl_pips = params.get('sl_pips', 100)
tp_pips = params.get('tp_pips', 200)
# Asumsi pip value sederhana untuk backtesting, bisa disempurnakan nanti
# Untuk pair JPY, point adalah 0.001, untuk yang lain 0.00001
point = 0.001 if 'JPY' in historical_data_df.columns[0].upper() else 0.00001
# Asumsi nilai per pip untuk 0.01 lot
# Ini adalah penyederhanaan besar, tapi cukup untuk backtesting awal
value_per_pip = 0.1 # $0.10 per pip
pip_value = 10 * point
# Mock bot object untuk strategi
# --- LANGKAH 1: Hitung semua indikator SEKALI di awal ---
class MockBot:
def __init__(self):
self.market_for_mt5 = "BACKTEST"
self.timeframe = "H1"
self.tf_map = {}
# Inisialisasi strategi dengan parameter yang diberikan
strategy_instance = strategy_class(bot_instance=MockBot(), params=params)
# Loop melalui setiap bar data historis
for i in range(1, len(historical_data_df)):
# Buat DataFrame "seolah-olah" ini adalah data real-time hingga bar saat ini
# PERBAIKAN: Gunakan .copy() untuk membuat salinan eksplisit dari slice.
# Ini akan menghilangkan SettingWithCopyWarning di semua strategi.
current_market_data = historical_data_df.iloc[:i].copy()
analysis = strategy_instance.analyze(current_market_data)
signal = analysis.get("signal")
current_price = historical_data_df.iloc[i]['close']
# Panggil metode baru 'analyze_df' untuk pra-perhitungan indikator
df_with_indicators = strategy_instance.analyze_df(historical_data_df.copy())
if df_with_indicators.empty:
return {"error": "Gagal menghasilkan data indikator. Periksa panjang data input."}
strategy_name = strategy_instance.name
# --- LANGKAH 2: Inisialisasi state backtesting ---
trades = []
in_position = False
initial_capital = 10000
capital = initial_capital
equity_curve = [initial_capital]
peak_equity = initial_capital
max_drawdown = 0.0
position_type = None
entry_price = 0.0
sl_pips = params.get('sl_pips', 100)
tp_pips = params.get('tp_pips', 200)
symbol_name = historical_data_df.columns[0].upper()
pip_size = 0.0001 # Default untuk Forex standar
if 'JPY' in symbol_name:
pip_size = 0.01
elif 'XAU' in symbol_name or 'XAG' in symbol_name: # Emas atau Perak
pip_size = 0.01
# Tentukan nilai per pip berdasarkan simbol (untuk lot 0.01)
if 'XAU' in symbol_name or 'XAG' in symbol_name: # Emas atau Perak
# Untuk 0.01 lot (1 oz), pergerakan harga $0.01 = profit/loss $0.01
value_per_pip = 0.01
else:
# Untuk Forex (misal EURUSD), 0.01 lot, pergerakan 1 pip = profit/loss $0.1
# Ini adalah asumsi umum, untuk JPY pairs nilainya bisa sedikit berbeda
value_per_pip = 0.1
# --- LANGKAH 3: Loop melalui data yang sudah ada indikatornya ---
for i in range(1, len(df_with_indicators)):
current_bar = df_with_indicators.iloc[i]
signal = current_bar.get("signal", "HOLD")
current_price = current_bar['close']
# Cek SL/TP jika sedang dalam posisi
if in_position:
profit = 0
if position_type == 'BUY':
profit_pips = (current_price - entry_price) / point / 10
if current_price <= entry_price - (sl_pips * pip_value):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'SL'})
capital += profit_pips * value_per_pip
profit_pips = (current_price - entry_price) / pip_size
if current_price <= entry_price - (sl_pips * pip_size):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': -sl_pips, 'reason': 'SL'})
capital -= sl_pips * value_per_pip
in_position = False
elif current_price >= entry_price + (tp_pips * pip_value):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'TP'})
capital += profit_pips * value_per_pip
elif current_price >= entry_price + (tp_pips * pip_size):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': tp_pips, 'reason': 'TP'})
capital += tp_pips * value_per_pip
in_position = False
elif position_type == 'SELL':
profit_pips = (entry_price - current_price) / point / 10
if current_price >= entry_price + (sl_pips * pip_value):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'SL'})
capital += profit_pips * value_per_pip
profit_pips = (entry_price - current_price) / pip_size
if current_price >= entry_price + (sl_pips * pip_size):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': -sl_pips, 'reason': 'SL'})
capital -= sl_pips * value_per_pip
in_position = False
elif current_price <= entry_price - (tp_pips * pip_value):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'TP'})
capital += profit_pips * value_per_pip
elif current_price <= entry_price - (tp_pips * pip_size):
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': tp_pips, 'reason': 'TP'})
capital += tp_pips * value_per_pip
in_position = False
if not in_position: # Jika posisi baru saja ditutup
equity_curve.append(capital)
peak_equity = max(peak_equity, capital)
drawdown = (peak_equity - capital) / peak_equity
drawdown = (peak_equity - capital) / peak_equity if peak_equity > 0 else 0
max_drawdown = max(max_drawdown, drawdown)
# Cek sinyal baru
@@ -96,12 +105,12 @@ def run_backtest(strategy_id, params, historical_data_df):
elif (signal == 'SELL' and in_position and position_type == 'BUY') or \
(signal == 'BUY' and in_position and position_type == 'SELL'):
# Sinyal berlawanan, tutup posisi lama
profit_pips = ((current_price - entry_price) if position_type == 'BUY' else (entry_price - current_price)) / point / 10
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'Signal Flip'})
profit_pips = ((current_price - entry_price) if position_type == 'BUY' else (entry_price - current_price)) / pip_size # Calculate pips
trades.append({'entry': entry_price, 'exit': current_price, 'profit_pips': profit_pips, 'reason': 'Signal Flip'}) # Log trade
capital += profit_pips * value_per_pip
equity_curve.append(capital)
peak_equity = max(peak_equity, capital)
drawdown = (peak_equity - capital) / peak_equity
drawdown = (peak_equity - capital) / peak_equity if peak_equity > 0 else 0
max_drawdown = max(max_drawdown, drawdown)
in_position = False
@@ -112,6 +121,7 @@ def run_backtest(strategy_id, params, historical_data_df):
win_rate = (wins / len(trades) * 100) if trades else 0
return {
"strategy_name": strategy_name,
"total_trades": len(trades),
"total_profit_pips": total_profit_pips,
"win_rate_percent": win_rate,
@@ -119,5 +129,5 @@ def run_backtest(strategy_id, params, historical_data_df):
"losses": losses,
"max_drawdown_percent": max_drawdown * 100,
"equity_curve": equity_curve,
"trades": trades[-20:] # Tampilkan 20 trade terakhir
"trades": trades[-20:]
}
+9 -6
View File
@@ -60,7 +60,7 @@ def mulai_bot(bot_id: int):
queries.update_bot_status(bot_id, 'Error')
return False, f"Gagal memulai bot: {e}"
def stop_bot(bot_id: int):
def hentikan_bot(bot_id: int):
"""Menghentikan thread bot yang sedang berjalan."""
# PERBAIKAN: Gunakan .pop() untuk mengambil dan menghapus bot secara atomik.
# Ini mencegah race condition di mana dua proses mencoba menghentikan bot yang sama.
@@ -78,7 +78,7 @@ def stop_bot(bot_id: int):
queries.update_bot_status(bot_id, 'Dijeda') # Pastikan status di DB adalah 'Dijeda'
return True, f"Bot {bot_id} sudah dihentikan atau tidak sedang berjalan."
def start_all_bots():
def mulai_semua_bot():
"""Memulai semua bot yang statusnya 'Dijeda'."""
all_bots = queries.get_all_bots()
bots_to_start = [bot for bot in all_bots if bot['status'] == 'Dijeda']
@@ -94,22 +94,25 @@ def start_all_bots():
return True, f"Berhasil memulai {started_count} dari {len(bots_to_start)} bot."
def stop_all_bots():
def hentikan_semua_bot():
"""Menghentikan semua bot yang sedang berjalan."""
running_bot_ids = list(active_bots.keys())
if not running_bot_ids:
return False, "Tidak ada bot yang sedang berjalan."
for bot_id in running_bot_ids:
stop_bot(bot_id)
hentikan_bot(bot_id)
return True, f"Sinyal berhenti telah dikirim ke {len(running_bot_ids)} bot."
# Alias untuk dipanggil oleh atexit di run.py, memastikan nama sesuai.
shutdown_all_bots = hentikan_semua_bot
def perbarui_bot(bot_id: int, data: dict):
"""Memperbarui konfigurasi bot di database."""
bot_instance = active_bots.get(bot_id)
if bot_instance and bot_instance.is_alive():
logger.info(f"Menghentikan bot {bot_id} sementara untuk pembaruan.")
stop_bot(bot_id)
hentikan_bot(bot_id)
# --- PERBAIKAN DI SINI ---
# Ganti nama kunci 'check_interval_seconds' dari frontend
@@ -144,7 +147,7 @@ def perbarui_bot(bot_id: int, data: dict):
def hapus_bot(bot_id: int):
"""Menghentikan dan menghapus bot."""
stop_bot(bot_id) # Pastikan thread berhenti sebelum dihapus
hentikan_bot(bot_id) # Pastikan thread berhenti sebelum dihapus
return queries.delete_bot(bot_id)
def add_new_bot_to_controller(bot_id: int):
+31 -15
View File
@@ -24,10 +24,10 @@ class TradingBot(threading.Thread):
self.check_interval = check_interval
self.strategy_name = strategy
self.strategy_params = strategy_params
self.market_for_mt5 = self.market.replace('/', '') # Versi simbol yang bersih untuk MT5
self.market_for_mt5 = None # Akan diisi setelah verifikasi simbol
self.status = status
self.last_analysis = {}
self.last_analysis = {"signal": "MEMUAT", "explanation": "Bot sedang memulai, menunggu analisis pertama..."}
self._stop_event = threading.Event()
self.strategy_instance = None
# Gunakan map yang diimpor untuk menjaga konsistensi
@@ -38,12 +38,23 @@ class TradingBot(threading.Thread):
self.status = 'Aktif'
self.log_activity('START', f"Bot '{self.name}' dimulai.", is_notification=True)
# --- PERBAIKAN: Verifikasi Simbol Cerdas ---
from core.utils.mt5 import find_mt5_symbol
self.market_for_mt5 = find_mt5_symbol(self.market)
if not self.market_for_mt5:
msg = f"Simbol '{self.market}' atau variasinya tidak dapat ditemukan/diaktifkan di Market Watch MT5."
self.log_activity('ERROR', msg, is_notification=True)
self.status = 'Error'
self.last_analysis = {"signal": "ERROR", "explanation": msg}
return # Hentikan eksekusi jika simbol tidak valid
try:
strategy_class = STRATEGY_MAP.get(self.strategy_name)
if not strategy_class:
raise ValueError(f"Strategi '{self.strategy_name}' tidak ditemukan.")
# --- PERBAIKAN: Inisialisasi kelas strategi dengan benar ---
# Inisialisasi kelas strategi dengan benar
self.strategy_instance = strategy_class(bot_instance=self, params=self.strategy_params)
except Exception as e:
@@ -53,26 +64,28 @@ class TradingBot(threading.Thread):
while not self._stop_event.is_set():
try:
if not mt5.symbol_select(self.market_for_mt5, True):
msg = f"Gagal mengaktifkan simbol {self.market} (di MT5: {self.market_for_mt5}). Pastikan simbol ada di Market Watch."
self.log_activity('WARNING', msg)
self.last_analysis = {"signal": "ERROR", "price": None, "explanation": msg}
time.sleep(self.check_interval)
continue
# Simbol sudah diverifikasi, jadi pemeriksaan ini menjadi redundan
# if not mt5.symbol_select(self.market_for_mt5, True): ...
symbol_info = mt5.symbol_info(self.market_for_mt5)
if not symbol_info:
msg = f"Tidak dapat mengambil info untuk simbol {self.market} (di MT5: {self.market_for_mt5})."
msg = f"Tidak dapat mengambil info untuk simbol {self.market_for_mt5}."
self.log_activity('WARNING', msg)
self.last_analysis = {"signal": "ERROR", "price": None, "explanation": msg}
time.sleep(self.check_interval)
continue
# --- PERBAIKAN: Bot sekarang yang mengambil data ---
from core.data.fetch import get_rates
# Bot sekarang yang mengambil data
from core.utils.mt5 import get_rates_mt5
tf_const = self.tf_map.get(self.timeframe, mt5.TIMEFRAME_H1)
# Ambil data yang cukup untuk strategi terkompleks (misal, 250 bar untuk EMA 200)
df = get_rates(self.market_for_mt5, tf_const, 250)
df = get_rates_mt5(self.market_for_mt5, tf_const, 250)
if df.empty:
msg = f"Gagal mengambil data harga untuk {self.market_for_mt5}. Periksa koneksi atau ketersediaan data historis."
self.log_activity('WARNING', msg)
self.last_analysis = {"signal": "ERROR", "explanation": msg}
time.sleep(self.check_interval)
continue
self.last_analysis = self.strategy_instance.analyze(df)
logger.info(f"Bot {self.id} [{self.strategy_name}] - Last Analysis: {self.last_analysis}")
@@ -84,7 +97,10 @@ class TradingBot(threading.Thread):
time.sleep(self.check_interval)
except Exception as e:
self.log_activity('ERROR', f"Error pada loop utama: {e}", exc_info=True, is_notification=True)
error_message = f"Error pada loop utama: {e}"
self.log_activity('ERROR', error_message, exc_info=True, is_notification=True)
# PERBAIKAN: Perbarui status analisis agar error terlihat di UI
self.last_analysis = {"signal": "ERROR", "explanation": str(e)}
time.sleep(self.check_interval * 2)
self.status = 'Dijeda'
-114
View File
@@ -1,114 +0,0 @@
# /core/data/fetch.py
import MetaTrader5 as mt5
import pandas as pd
import logging
from datetime import datetime, timedelta
# Gunakan logger yang konsisten dengan seluruh aplikasi
logger = logging.getLogger(__name__)
# =================================================
# FUNGSI-FUNGSI PENGAMBILAN DATA DARI METATRADER 5
# =================================================
def get_rates(symbol: str, timeframe: int, count: int = 100):
"""
Mengambil data harga historis (rates) dari MT5 dalam bentuk DataFrame.
Ini adalah fungsi utama untuk semua analisis strategi.
"""
try:
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, count)
# Periksa apakah data berhasil diambil
if rates is None or len(rates) == 0:
logger.warning(f"Gagal mengambil data harga untuk {symbol} (Timeframe: {timeframe}). MT5 mengembalikan None atau data kosong.")
return None
# Konversi ke DataFrame
df = pd.DataFrame(rates)
# Konversi kolom 'time' dari Unix timestamp menjadi objek datetime
df['time'] = pd.to_datetime(df['time'], unit='s')
return df
except Exception as e:
logger.error(f"Error tak terduga saat get_rates untuk {symbol}: {e}", exc_info=True)
return None
def get_mt5_account_info():
"""Mengambil informasi akun (saldo, equity, profit) dari MT5."""
try:
info = mt5.account_info()
# Periksa apakah info berhasil diambil sebelum mengubahnya menjadi dictionary
if info:
return info._asdict()
else:
logger.error(f"Gagal mengambil info akun. MT5 mengembalikan None. Error: {mt5.last_error()}")
return None
except Exception as e:
logger.error(f"Error tak terduga saat get_mt5_account_info: {e}", exc_info=True)
return None
def get_todays_profit():
"""Menghitung total profit dari histori trading hari ini."""
try:
from_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
to_date = datetime.now()
deals = mt5.history_deals_get(from_date, to_date)
if deals is None:
return 0.0
# Gunakan generator expression untuk efisiensi
return sum(d.profit for d in deals if d.entry == 1)
except Exception as e:
logger.error(f"Error tak terduga saat get_todays_profit: {e}", exc_info=True)
return 0.0
def get_trade_history_from_mt5(days_history: int = 30):
"""Mengambil riwayat transaksi yang sudah ditutup dari MT5."""
try:
from_date = datetime.now() - timedelta(days=days_history)
to_date = datetime.now()
deals = mt5.history_deals_get(from_date, to_date)
if deals is None:
logger.warning("Gagal mengambil histori deals dari MT5. MT5 mengembalikan None.")
return []
# Proses data hanya jika ada deals yang ditemukan
if len(deals) > 0:
df = pd.DataFrame(list(deals), columns=deals[0]._asdict().keys())
df['time'] = pd.to_datetime(df['time'], unit='s')
closed = df[df['entry'] == 1].sort_values(by='time', ascending=False)
return closed.to_dict('records')
return []
except Exception as e:
logger.error(f"Error tak terduga saat get_trade_history: {e}", exc_info=True)
return []
def get_open_positions_from_mt5():
"""Mengambil semua posisi trading yang sedang terbuka dari akun MT5."""
try:
positions = mt5.positions_get()
if positions is None:
# Tidak perlu print error jika hanya tidak ada posisi
return []
# Ubah tuple of objects menjadi list of dictionaries
return [
{
"ticket": pos.ticket, "symbol": pos.symbol, "type": pos.type,
"volume": pos.volume, "price_open": pos.price_open,
"price_current": pos.price_current, "profit": pos.profit,
"magic": pos.magic,
"time": datetime.fromtimestamp(pos.time).isoformat()
}
for pos in positions
]
except Exception as e:
logger.error(f"Error tak terduga saat get_open_positions: {e}", exc_info=True)
return []
+12 -2
View File
@@ -109,7 +109,7 @@ def get_notifications():
notifications = conn.execute('''
SELECT h.id, h.action, h.details, h.is_read, h.timestamp, b.name as bot_name
FROM trade_history h
JOIN bots b ON h.bot_id = b.id
LEFT JOIN bots b ON h.bot_id = b.id
WHERE h.is_notification = 1
ORDER BY h.timestamp DESC
''').fetchall()
@@ -132,4 +132,14 @@ def mark_notifications_as_read():
"""Menandai semua notifikasi sebagai sudah dibaca."""
with get_db_connection() as conn:
conn.execute('UPDATE trade_history SET is_read = 1 WHERE is_notification = 1 AND is_read = 0')
conn.commit()
conn.commit()
def get_all_backtest_history():
"""Mengambil semua riwayat hasil backtest dari database."""
try:
with get_db_connection() as conn:
history = conn.execute('SELECT * FROM backtest_results ORDER BY timestamp DESC').fetchall()
return [dict(row) for row in history]
except sqlite3.Error as e:
logger.error(f"Database error saat mengambil riwayat backtest: {e}")
return []
+54 -4
View File
@@ -2,10 +2,48 @@
import pandas as pd
import json
import logging
from flask import Blueprint, request, jsonify
from core.backtesting.engine import run_backtest
from core.db.queries import get_all_backtest_history
from core.db.connection import get_db_connection
api_backtest = Blueprint('api_backtest', __name__)
logger = logging.getLogger(__name__)
import numpy as np
# ... (kode lainnya)
def save_backtest_result(strategy_name, filename, params, results):
# ... (kode di dalam fungsi)
# Sanitasi data sebelum menyimpan
for key, value in results.items():
if isinstance(value, (np.floating, float)) and (np.isinf(value) or np.isnan(value)):
results[key] = None # Ganti inf/nan dengan None (NULL di DB)
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO backtest_results (
strategy_name, data_filename, total_profit_pips, total_trades,
win_rate_percent, max_drawdown_percent, equity_curve, trade_log, parameters
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
strategy_name,
filename,
results.get('total_profit_pips', 0),
results.get('total_trades', 0),
results.get('win_rate_percent', 0),
results.get('max_drawdown_percent', 0),
json.dumps(results.get('equity_curve', [])),
json.dumps(results.get('trades', [])),
json.dumps(params)
))
conn.commit()
except Exception as e:
logger.error(f"[DB ERROR] Gagal menyimpan hasil backtest: {e}", exc_info=True)
@api_backtest.route('/api/backtest/run', methods=['POST'])
def run_backtest_route():
@@ -17,14 +55,26 @@ def run_backtest_route():
return jsonify({"error": "Nama file kosong"}), 400
try:
# Baca data CSV yang diunggah
df = pd.read_csv(file, parse_dates=['time'])
# Ambil parameter dari form data
strategy_id = request.form.get('strategy')
params = json.loads(request.form.get('params', '{}'))
# Jalankan backtest
results = run_backtest(strategy_id, params, df)
# Simpan hasil jika berhasil
if results and not results.get('error'):
strategy_name = results.get('strategy_name', strategy_id)
save_backtest_result(strategy_name, file.filename, params, results)
return jsonify(results)
except Exception as e:
return jsonify({"error": f"Terjadi kesalahan saat backtesting: {str(e)}"}), 500
return jsonify({"error": f"Terjadi kesalahan saat backtesting: {str(e)}"}), 500
@api_backtest.route('/api/backtest/history', methods=['GET'])
def get_history_route():
try:
history = get_all_backtest_history()
return jsonify(history)
except Exception as e:
return jsonify({"error": f"Terjadi kesalahan saat mengambil riwayat: {str(e)}"}), 500
+12 -11
View File
@@ -1,13 +1,12 @@
# core/routes/api_bots.py - VERSI PERBAIKAN LENGKAP
# core/routes/api_bots.py
import json
import logging
from flask import Blueprint, jsonify, request
import pandas_ta as ta
import MetaTrader5 as mt5
from core.bots import controller
from core.db import queries
from core.data.fetch import get_rates
from core.utils.mt5 import get_rates_mt5
from core.utils.mt5 import TIMEFRAME_MAP
from core.strategies.strategy_map import STRATEGY_MAP
@@ -117,19 +116,19 @@ def start_bot_route(bot_id):
@api_bots.route('/api/bots/<int:bot_id>/stop', methods=['POST'])
def stop_bot_route(bot_id):
"""Menghentikan bot."""
success, message = controller.stop_bot(bot_id)
success, message = controller.hentikan_bot(bot_id)
return jsonify({'message': message}) if success else (jsonify({'error': message}), 500)
@api_bots.route('/api/bots/start_all', methods=['POST'])
def start_all_bots_route():
"""Memulai semua bot yang dijeda."""
success, message = controller.start_all_bots()
success, message = controller.mulai_semua_bot()
return jsonify({'message': message}) if success else (jsonify({'error': message}), 400)
@api_bots.route('/api/bots/stop_all', methods=['POST'])
def stop_all_bots_route():
"""Menghentikan semua bot yang aktif."""
success, message = controller.stop_all_bots()
success, message = controller.hentikan_semua_bot()
return jsonify({'message': message}) if success else (jsonify({'error': message}), 400)
@api_bots.route('/api/bots/<int:bot_id>/analysis', methods=['GET'])
@@ -150,18 +149,20 @@ def get_rsi_data_route():
symbol = request.args.get('symbol', 'EURUSD', type=str)
timeframe_str = request.args.get('timeframe', 'H1', type=str)
timeframe = TIMEFRAME_MAP.get(timeframe_str, mt5.TIMEFRAME_H1)
timeframe = TIMEFRAME_MAP.get(timeframe_str.upper(), mt5.TIMEFRAME_H1)
df = get_rates(symbol, timeframe, 100)
df = get_rates_mt5(symbol, timeframe, 100)
if df is None or df.empty:
return jsonify({"error": f"Tidak dapat mengambil data untuk {symbol}"}), 404
df['rsi'] = ta.rsi(df['close'], length=14)
df.ta.rsi(length=14, append=True)
df.dropna(inplace=True)
last_50_rows = df.tail(50)
chart_data = {
'timestamps': [dt.strftime('%H:%M') for dt in df['time'].tail(50)],
'rsi_values': list(df['rsi'].tail(50))
'timestamps': [ts.strftime('%H:%M') for ts in last_50_rows.index],
'rsi_values': list(last_50_rows.iloc[:, -1])
}
return jsonify(chart_data)
+2 -2
View File
@@ -1,7 +1,7 @@
# core/routes/api_chart.py
from flask import Blueprint, jsonify, request
from core.utils.mt5 import get_rates_from_mt5
from core.utils.mt5 import get_rates_mt5
import MetaTrader5 as mt5
api_chart = Blueprint('api_chart', __name__)
@@ -9,7 +9,7 @@ api_chart = Blueprint('api_chart', __name__)
@api_chart.route('/api/chart/data')
def api_chart_data():
symbol = request.args.get('symbol', 'EURUSD')
df = get_rates_from_mt5(symbol, mt5.TIMEFRAME_H1, 100)
df = get_rates_mt5(symbol, mt5.TIMEFRAME_H1, 100)
if df is None or df.empty:
return jsonify({"error": "Gagal mengambil data grafik"}), 500
+3 -3
View File
@@ -1,7 +1,7 @@
# core/routes/api_dashboard.py
from flask import Blueprint, jsonify
from core.utils.mt5 import get_mt5_account_info, get_todays_profit
from core.utils.mt5 import get_account_info_mt5, get_todays_profit_mt5
from core.db import queries # <-- 1. Impor modul queries
api_dashboard = Blueprint('api_dashboard', __name__)
@@ -9,8 +9,8 @@ api_dashboard = Blueprint('api_dashboard', __name__)
@api_dashboard.route('/api/dashboard/stats')
def api_dashboard_stats():
try:
account_info = get_mt5_account_info()
todays_profit = get_todays_profit()
account_info = get_account_info_mt5()
todays_profit = get_todays_profit_mt5()
# 2. Ambil semua bot sekali saja untuk efisiensi
all_bots = queries.get_all_bots()
+5 -8
View File
@@ -8,10 +8,12 @@ api_forex = Blueprint('api_forex', __name__)
@api_forex.route('/api/forex-data')
def get_forex_data():
"""
Mengembalikan daftar simbol forex, diurutkan berdasarkan popularitas (volume harian).
Formatnya adalah array JSON, bukan dictionary, agar urutan tetap terjaga.
"""
forex_symbols = get_forex_symbols()
if forex_symbols:
return jsonify({s['name']: s for s in forex_symbols})
return jsonify({})
return jsonify(forex_symbols)
@api_forex.route('/api/forex/<symbol>/profile')
def get_forex_profile(symbol):
@@ -19,8 +21,3 @@ def get_forex_profile(symbol):
if profile:
return jsonify(profile)
return jsonify({"error": "Could not fetch symbol profile from MT5"}), 404
def get_forex_data():
forex_symbols = get_forex_symbols()
if forex_symbols:
return jsonify({s['name']: s for s in forex_symbols})
return jsonify({})
+1 -1
View File
@@ -15,5 +15,5 @@ def get_bot_fundamentals(bot_id):
if '/' in bot['market']:
return jsonify({}) # Skip if not stock
symbol = bot['market']
bot['market']
return jsonify({})
+22 -2
View File
@@ -1,13 +1,33 @@
# core/routes/api_history.py
import os
from flask import Blueprint, jsonify
from core.data.fetch import get_trade_history_from_mt5
from core.utils.mt5 import get_trade_history_mt5
import sqlite3
api_history = Blueprint('api_history', __name__)
# Gunakan path absolut untuk file database untuk menghindari masalah CWD
DB_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "bots.db")
@api_history.route('/api/backtest/history')
def get_backtest_history():
"""Mengambil semua hasil backtest yang tersimpan dari database."""
try:
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row # Ini memungkinkan akses kolom berdasarkan nama
cursor = conn.cursor()
cursor.execute("SELECT * FROM backtest_results ORDER BY timestamp DESC")
rows = cursor.fetchall()
# Ubah baris menjadi list of dictionaries
results = [dict(row) for row in rows]
return jsonify(results)
except Exception as e:
return jsonify({"error": f"Gagal mengambil riwayat backtest: {str(e)}"}), 500
@api_history.route('/api/history')
def api_global_history():
history = get_trade_history_from_mt5()
history = get_trade_history_mt5()
return jsonify(history)
@api_history.route('/api/bots/<int:bot_id>/history')
+2 -2
View File
@@ -1,7 +1,7 @@
# core/routes/api_indicators.py
from flask import Blueprint, request, jsonify
from core.data.fetch import get_rates as get_rates_from_mt5
from core.utils.mt5 import get_rates_mt5
import MetaTrader5 as mt5
import pandas_ta as ta
@@ -22,7 +22,7 @@ def get_rsi_data():
}
timeframe = tf_map.get(tf.upper(), mt5.TIMEFRAME_H1)
df = get_rates_from_mt5(symbol, timeframe, 100)
df = get_rates_mt5(symbol, timeframe, 100)
if df is None or len(df) < 20:
return jsonify({'timestamps': [], 'rsi_values': []})
+51 -5
View File
@@ -1,11 +1,57 @@
# core/routes/api_portfolio.py
from flask import Blueprint, jsonify
from core.data.fetch import get_open_positions_from_mt5
from core.utils.mt5 import get_open_positions_mt5
api_portfolio = Blueprint('api_portfolio', __name__)
# Blueprint didefinisikan dengan url_prefix untuk konsistensi
api_portfolio = Blueprint('api_portfolio', __name__, url_prefix='/api/portfolio')
@api_portfolio.route('/api/portfolio/open-positions')
@api_portfolio.route('/open-positions')
def api_open_positions():
positions = get_open_positions_from_mt5()
return jsonify(positions)
"""Endpoint untuk menyediakan daftar posisi terbuka secara real-time."""
try:
positions = get_open_positions_mt5()
return jsonify(positions)
except Exception as e:
# Mengembalikan error 500 jika ada masalah di backend
return jsonify({"error": str(e)}), 500
@api_portfolio.route('/allocation')
def get_asset_allocation():
"""Endpoint untuk menghitung dan mengembalikan alokasi aset."""
try:
positions = get_open_positions_mt5()
# Logika untuk mengklasifikasikan aset berdasarkan simbol
allocation_summary = {
"Forex": 0.0, "Emas": 0.0, "Saham": 0.0,
"Crypto": 0.0, "Lainnya": 0.0
}
if positions:
for pos in positions:
symbol = pos.get('symbol', '').upper()
volume = pos.get('volume', 0.0)
if 'USD' in symbol and 'XAU' not in symbol and 'BTC' not in symbol:
allocation_summary["Forex"] += volume
elif 'XAU' in symbol:
allocation_summary["Emas"] += volume
elif any(stock in symbol for stock in ['AAPL', 'GOOGL', 'TSLA', 'ND100', 'SP500']):
allocation_summary["Saham"] += volume
elif 'BTC' in symbol or 'ETH' in symbol:
allocation_summary["Crypto"] += volume
else:
allocation_summary["Lainnya"] += volume
# Hapus kategori dengan nilai nol untuk chart yang lebih bersih
final_allocation = {k: v for k, v in allocation_summary.items() if v > 0}
data = {
"labels": list(final_allocation.keys()) or ["Belum Ada Posisi"],
"values": list(final_allocation.values()) or [1]
}
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
+55 -14
View File
@@ -1,12 +1,15 @@
# core/routes/api_stocks.py
import logging
from datetime import datetime
from flask import Blueprint, jsonify
import MetaTrader5 as mt5 # Dipertahankan untuk mt5.terminal_info() jika diperlukan
import MetaTrader5 as mt5
from core.utils.symbols import get_stock_symbols
from core.utils.external import get_mt5_symbol_profile
from core.data.fetch import get_rates # <-- Impor fungsi terpusat
from core.utils.mt5 import get_rates_mt5
api_stocks = Blueprint('api_stocks', __name__)
logger = logging.getLogger(__name__)
@api_stocks.route('/api/stocks/<symbol>/profile')
def get_stock_profile(symbol):
@@ -17,30 +20,54 @@ def get_stock_profile(symbol):
@api_stocks.route('/api/stocks')
def get_stocks():
stock_symbols = get_stock_symbols()
"""
Mengambil daftar harga saham terkini.
Metode ini diubah agar lebih tangguh, terutama saat pasar tutup.
Perubahan dihitung dari harga pembukaan harian (Open D1) ke harga ask terakhir.
"""
# Ambil 20 saham paling populer (sudah diurutkan berdasarkan volume oleh fungsi)
stock_symbols = get_stock_symbols(limit=20)
if not stock_symbols:
logger.warning("get_stock_symbols() tidak mengembalikan simbol saham.")
return jsonify([])
result = []
symbols_to_process = [stock['name'] for stock in stock_symbols]
for stock in stock_symbols[:20]: # Max 20 untuk UI
symbol = stock['name']
# Gunakan fungsi dari fetch.py
df = get_rates(symbol, mt5.TIMEFRAME_M1, 2) # Ambil 2 bar untuk hitung change
for symbol in symbols_to_process:
try:
# 1. Ambil tick terakhir untuk harga saat ini
tick = mt5.symbol_info_tick(symbol)
if not tick or tick.ask == 0:
logger.debug(f"Melewatkan {symbol}: tidak ada data tick atau harga ask 0.")
continue
# 2. Ambil data bar harian (D1) untuk harga pembukaan
rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, 0, 1)
if rates is None or len(rates) == 0:
logger.warning(f"Melewatkan {symbol}: tidak bisa mendapatkan data harian (D1).")
continue
daily_open = rates[0]['open']
last_price = tick.ask
change = last_price - daily_open
if df is not None and not df.empty and len(df) > 1:
last_row = df.iloc[-1]
prev_row = df.iloc[-2]
result.append({
'symbol': symbol,
'last_price': last_row['close'],
'change': round(last_row['close'] - prev_row['close'], 2), # Perubahan dari close sebelumnya
'time': last_row['time'].strftime('%H:%M')
'last_price': last_price,
'change': round(change, 2),
'time': datetime.fromtimestamp(tick.time).strftime('%H:%M:%S')
})
except Exception as e:
# Log error jika terjadi masalah tak terduga saat memproses satu simbol
logger.error(f"Error saat memproses simbol saham {symbol}: {e}", exc_info=True)
return jsonify(result)
@api_stocks.route('/api/stocks/<symbol>')
def get_stock_detail(symbol):
# Gunakan fungsi dari fetch.py
df = get_rates(symbol, mt5.TIMEFRAME_M1, 1)
df = get_rates_mt5(symbol, mt5.TIMEFRAME_M1, 1)
if df is None or df.empty:
return jsonify({"error": f"Tidak bisa mengambil data untuk {symbol}"}), 404
@@ -55,3 +82,17 @@ def get_stock_detail(symbol):
"close": last['close'],
"volume": last['tick_volume']
})
@api_stocks.route('/api/symbols/all')
def get_all_symbols_with_path():
"""
Endpoint diagnostik untuk menampilkan SEMUA simbol dari MT5 beserta path-nya.
Ini digunakan untuk mengidentifikasi path yang benar untuk saham di broker Anda.
"""
try:
all_symbols = mt5.symbols_get()
symbols_info = [{"name": s.name, "path": s.path} for s in all_symbols]
return jsonify(symbols_info)
except Exception as e:
logger.error(f"Gagal mengambil daftar simbol dari MT5: {e}", exc_info=True)
return jsonify({"error": "Tidak dapat terhubung atau mengambil data dari MT5."}), 500
+47 -32
View File
@@ -1,61 +1,76 @@
# /core/strategies/bollinger_bands.py
import pandas_ta as ta
import numpy as np
from .base_strategy import BaseStrategy
class BollingerBandsStrategy(BaseStrategy):
name = 'Bollinger Bands Reversion'
description = 'Sinyal berdasarkan harga yang menyentuh atau melintasi batas atas atau bawah Bollinger Bands (Mean Reversion).'
description = 'Sinyal berdasarkan harga yang menyentuh atau melintasi batas atas atau bawah Bollinger Bands (Mean Reversion), dengan filter tren jangka panjang.'
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "bb_length", "label": "Panjang BB", "type": "number", "default": 20},
{"name": "bb_std", "label": "Standar Deviasi BB", "type": "number", "default": 2.0, "step": 0.1}
{"name": "bb_std", "label": "Standar Deviasi BB", "type": "number", "default": 2.0, "step": 0.1},
{"name": "trend_filter_period", "label": "Periode Filter Tren (SMA)", "type": "number", "default": 200}
]
def analyze(self, df):
"""
Menganalisis pasar menggunakan strategi Bollinger Bands® Mean Reversion.
Ideal untuk pasar ranging yang volatil seperti EURUSD.
"""
if df is None or df.empty or len(df) < 21:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Bollinger Bands."}
# --- Hitung Indikator ---
"""Metode untuk LIVE TRADING."""
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
trend_filter_period = self.params.get('trend_filter_period', 200)
# PERBAIKAN: Paksa format float dengan satu desimal pada nama kolom
if df is None or df.empty or len(df) < trend_filter_period:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk filter tren."}
bb_std = self.params.get('bb_std', 2.0)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
trend_filter_col = f'SMA_{trend_filter_period}'
df.ta.bbands(length=bb_length, std=bb_std, append=True) # pandas-ta akan membuat kolom dengan format yang sama
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df[trend_filter_col] = ta.sma(df['close'], length=trend_filter_period)
df.dropna(inplace=True)
if len(df) < 1:
if df.empty:
return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."}
last = df.iloc[-1]
price = last["close"]
signal = "HOLD"
explanation = f"Harga [{price:.4f}] di dalam Bands. Tidak ada sinyal."
explanation = f"Harga di dalam Bands atau tren tidak sesuai."
is_uptrend = price > last[trend_filter_col]
is_downtrend = price < last[trend_filter_col]
# --- Logika Sinyal ---
# Sinyal Beli: Harga menyentuh atau menembus Band Bawah
if last['low'] <= last[bbl_col]:
if is_uptrend and last['low'] <= last[bbl_col]:
signal = "BUY"
explanation = f"Oversold: Harga [{last['low']:.4f}] menyentuh Band Bawah [{last[bbl_col]:.4f}]"
# Sinyal Jual: Harga menyentuh atau menembus Band Atas
elif last['high'] >= last[bbu_col]:
explanation = f"Uptrend & Oversold: Harga menyentuh Band Bawah."
elif is_downtrend and last['high'] >= last[bbu_col]:
signal = "SELL"
explanation = f"Overbought: Harga [{last['high']:.4f}] menyentuh Band Atas [{last[bbu_col]:.4f}]"
explanation = f"Downtrend & Overbought: Harga menyentuh Band Atas."
analysis_data = {
"signal": signal,
"price": price,
"explanation": explanation,
"BB_Upper": last.get(f'BBU_{bb_length}_{bb_std:.1f}'),
"BB_Middle": last.get(f'BBM_{bb_length}_{bb_std:.1f}'),
"BB_Lower": last.get(f'BBL_{bb_length}_{bb_std:.1f}'),
}
return analysis_data
return {"signal": signal, "price": price, "explanation": explanation}
def analyze_df(self, df):
"""Metode untuk BACKTESTING."""
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
trend_filter_period = self.params.get('trend_filter_period', 200)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
trend_filter_col = f'SMA_{trend_filter_period}'
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df[trend_filter_col] = ta.sma(df['close'], length=trend_filter_period)
is_uptrend = df['close'] > df[trend_filter_col]
is_downtrend = df['close'] < df[trend_filter_col]
buy_signal = is_uptrend & (df['low'] <= df[bbl_col])
sell_signal = is_downtrend & (df['high'] >= df[bbu_col])
df['signal'] = np.where(buy_signal, 'BUY', np.where(sell_signal, 'SELL', 'HOLD'))
return df
+35 -64
View File
@@ -9,62 +9,36 @@ class BollingerSqueezeStrategy(BaseStrategy):
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "bb_length", "label": "Panjang BB", "type": "number", "default": 20},
{"name": "bb_std", "label": "Std Dev BB", "type": "number", "default": 2.0, "step": 0.1},
{"name": "squeeze_window", "label": "Window Squeeze", "type": "number", "default": 10},
{"name": "squeeze_factor", "label": "Faktor Squeeze", "type": "number", "default": 0.7, "step": 0.1},
{"name": "rsi_period", "label": "Periode RSI", "type": "number", "default": 14},
{"name": "volume_factor", "label": "Faktor Volume", "type": "number", "default": 1.5, "step": 0.1}
]
def analyze(self, df):
"""
Menganalisis pasar menggunakan strategi Bollinger Band Squeeze.
Strategi ini menunggu periode volatilitas rendah ("squeeze") lalu
masuk saat harga breakout.
"""
if df is None or df.empty or len(df) < 121:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Bollinger Squeeze."}
"""Metode untuk LIVE TRADING."""
if df is None or df.empty or len(df) < self.params.get('bb_length', 20) + self.params.get('squeeze_window', 10):
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup."}
# --- Hitung Indikator ---
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
squeeze_window = self.params.get('squeeze_window', 10)
squeeze_factor = self.params.get('squeeze_factor', 0.7)
rsi_period = self.params.get('rsi_period', 14)
volume_factor = self.params.get('volume_factor', 1.5)
# PERBAIKAN: Paksa format float dengan satu desimal pada nama kolom
# untuk mencegah KeyError (misal: 'BBU_20_2' vs 'BBU_20_2.0')
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbm_col = f'BBM_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df['BB_WIDTH'] = df[bbu_col] - df[bbl_col]
# FIX 1: Mencegah ZeroDivisionError dengan pembagian yang aman
df['BB_BANDWIDTH'] = np.where(
df[bbm_col] != 0,
(df[bbu_col] - df[bbl_col]) / df[bbm_col] * 100,
0 # Jika middle band 0, anggap bandwidth 0
)
df['BB_BANDWIDTH'] = np.where(df[bbm_col] != 0, (df[bbu_col] - df[bbl_col]) / df[bbm_col] * 100, 0)
df['AVG_BANDWIDTH'] = df['BB_BANDWIDTH'].rolling(window=squeeze_window).mean()
df['SQUEEZE_LEVEL'] = df['AVG_BANDWIDTH'] * squeeze_factor
df['SQUEEZE'] = df['BB_BANDWIDTH'] < df['SQUEEZE_LEVEL']
df['RSI'] = ta.rsi(df['close'], length=rsi_period)
# FIX 2: Menggunakan nama kolom volume yang benar ('tick_volume')
if 'tick_volume' in df.columns:
df['AVG_VOLUME'] = df['tick_volume'].rolling(window=squeeze_window).mean()
df['VOLUME_SURGE'] = df['tick_volume'] > df['AVG_VOLUME'] * volume_factor
df['VOLUME_SURGE'] = df['VOLUME_SURGE'].astype(int)
else:
df['VOLUME_SURGE'] = 0
df.dropna(inplace=True)
if len(df) < 2:
@@ -72,46 +46,43 @@ class BollingerSqueezeStrategy(BaseStrategy):
last = df.iloc[-1]
prev = df.iloc[-2]
price = last["close"]
signal = "HOLD"
explanation = "Tidak ada kondisi Squeeze & Breakout yang terdeteksi."
explanation = "Tidak ada Squeeze & Breakout."
# --- Logika Sinyal ---
# Kondisi 1: Apakah candle SEBELUMNYA berada dalam kondisi "Squeeze"?
is_in_squeeze = prev['SQUEEZE']
if is_in_squeeze:
explanation = f"Squeeze terdeteksi (Lebar: {prev['BB_WIDTH']:.4f}). Menunggu breakout."
# Kondisi 2: Jika ya, apakah candle SEKARANG breakout dari Bands SEBELUMNYA?
if prev['SQUEEZE']:
if last['close'] > prev[bbu_col] and last['RSI'] < 70:
signal = "BUY"
explanation = f"Squeeze & Breakout NAIK! Harga [{last['close']:.2f}] > Band Atas [{prev[bbu_col]:.2f}]"
explanation = "Squeeze & Breakout NAIK!"
elif last['close'] < prev[bbl_col] and last['RSI'] > 30:
signal = "SELL"
explanation = f"Squeeze & Breakout TURUN! Harga [{last['close']:.2f}] < Band Bawah [{prev[bbl_col]:.2f}]"
else:
# Kondisi 3: Post-Squeeze Momentum
if prev['BB_BANDWIDTH'] > prev['AVG_BANDWIDTH'] * 1.2: # Bands expanding
if last['close'] > prev[bbu_col] and last['VOLUME_SURGE']:
signal = 'BUY'
explanation = f"Momentum NAIK! Harga [{last['close']:.2f}] > Band Atas [{prev[bbu_col]:.2f}] dengan volume"
elif last['close'] < prev[bbl_col] and last['VOLUME_SURGE']:
signal = 'SELL'
explanation = f"Momentum TURUN! Harga [{last['close']:.2f}] < Band Bawah [{prev[bbl_col]:.2f}] dengan volume"
explanation = "Squeeze & Breakout TURUN!"
# --- PERBAIKAN: Kembalikan semua data analisis yang relevan ---
analysis_data = {
"signal": signal,
"price": price,
"explanation": explanation,
"RSI": last.get('RSI'),
"BB_Width": last.get('BB_WIDTH'),
"BB_Bandwidth": last.get('BB_BANDWIDTH'),
"Is_Squeeze": bool(last.get('SQUEEZE', False)), # FIX: Konversi numpy.bool_ ke bool standar
"Volume_Surge": bool(last.get('VOLUME_SURGE', 0))
}
return {"signal": signal, "price": price, "explanation": explanation}
return analysis_data
def analyze_df(self, df):
"""Metode untuk BACKTESTING."""
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
squeeze_window = self.params.get('squeeze_window', 10)
squeeze_factor = self.params.get('squeeze_factor', 0.7)
rsi_period = self.params.get('rsi_period', 14)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbm_col = f'BBM_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df['BB_BANDWIDTH'] = np.where(df[bbm_col] != 0, (df[bbu_col] - df[bbl_col]) / df[bbm_col] * 100, 0)
df['AVG_BANDWIDTH'] = df['BB_BANDWIDTH'].rolling(window=squeeze_window).mean()
df['SQUEEZE_LEVEL'] = df['AVG_BANDWIDTH'] * squeeze_factor
df['SQUEEZE'] = df['BB_BANDWIDTH'] < df['SQUEEZE_LEVEL']
df['RSI'] = ta.rsi(df['close'], length=rsi_period)
is_in_squeeze = df['SQUEEZE'].shift(1)
buy_signal = is_in_squeeze & (df['close'] > df[bbu_col].shift(1)) & (df['RSI'] < 70)
sell_signal = is_in_squeeze & (df['close'] < df[bbl_col].shift(1)) & (df['RSI'] > 30)
df['signal'] = np.where(buy_signal, 'BUY', np.where(sell_signal, 'SELL', 'HOLD'))
return df
+22 -18
View File
@@ -1,5 +1,6 @@
# /core/strategies/ma_crossover.py
import pandas_ta as ta
import numpy as np
from .base_strategy import BaseStrategy
class MACrossoverStrategy(BaseStrategy):
@@ -8,22 +9,16 @@ class MACrossoverStrategy(BaseStrategy):
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "fast_period", "label": "Periode MA Cepat", "type": "number", "default": 20},
{"name": "slow_period", "label": "Periode MA Lambat", "type": "number", "default": 50}
]
def analyze(self, df):
"""
Menganalisis pasar menggunakan strategi Moving Average Crossover (20/50).
Ideal untuk pasar dengan tren kuat seperti XAUUSD.
"""
if df is None or df.empty or len(df) < 51:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk MA Crossover."}
"""Metode untuk LIVE TRADING. Menganalisis beberapa bar data terakhir."""
if df is None or df.empty or len(df) < self.params.get('slow_period', 50) + 1:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup."}
# --- Hitung Indikator ---
# Gunakan parameter dinamis, dengan fallback ke nilai default
fast_period = self.params.get('fast_period', 20)
slow_period = self.params.get('slow_period', 50)
@@ -41,17 +36,26 @@ class MACrossoverStrategy(BaseStrategy):
signal = "HOLD"
explanation = f"MA({fast_period}): {last['ma_fast']:.2f}, MA({slow_period}): {last['ma_slow']:.2f}. Tidak ada sinyal."
# --- Logika Sinyal ---
# Golden Cross (Sinyal Beli)
if prev["ma_fast"] <= prev["ma_slow"] and last["ma_fast"] > last["ma_slow"]:
signal = "BUY"
explanation = f"Golden Cross: MA({fast_period}) [{last['ma_fast']:.2f}] memotong ke atas MA({slow_period}) [{last['ma_slow']:.2f}]"
# Death Cross (Sinyal Jual)
explanation = f"Golden Cross: MA({fast_period}) memotong ke atas MA({slow_period})"
elif prev["ma_fast"] >= prev["ma_slow"] and last["ma_fast"] < last["ma_slow"]:
signal = "SELL"
explanation = f"Death Cross: MA({fast_period}) [{last['ma_fast']:.2f}] memotong ke bawah MA({slow_period}) [{last['ma_slow']:.2f}]"
explanation = f"Death Cross: MA({fast_period}) memotong ke bawah MA({slow_period})"
return {
"signal": signal, "price": price, "explanation": explanation,
"ma_fast": last['ma_fast'], "ma_slow": last['ma_slow']
}
return {"signal": signal, "price": price, "explanation": explanation}
def analyze_df(self, df):
"""Metode untuk BACKTESTING. Menganalisis seluruh DataFrame."""
fast_period = self.params.get('fast_period', 20)
slow_period = self.params.get('slow_period', 50)
df["ma_fast"] = ta.sma(df["close"], length=fast_period)
df["ma_slow"] = ta.sma(df["close"], length=slow_period)
golden_cross = (df["ma_fast"].shift(1) <= df["ma_slow"].shift(1)) & (df["ma_fast"] > df["ma_slow"])
death_cross = (df["ma_fast"].shift(1) >= df["ma_slow"].shift(1)) & (df["ma_fast"] < df["ma_slow"])
df['signal'] = np.where(golden_cross, 'BUY', np.where(death_cross, 'SELL', 'HOLD'))
return df
+13 -16
View File
@@ -1,7 +1,9 @@
# core/strategies/mercy_edge.py
import MetaTrader5 as mt5
import pandas_ta as ta
import numpy as np
from .base_strategy import BaseStrategy
from core.data.fetch import get_rates
from core.utils.mt5 import get_rates_mt5
class MercyEdgeStrategy(BaseStrategy):
name = 'Mercy Edge (AI)'
@@ -9,7 +11,6 @@ class MercyEdgeStrategy(BaseStrategy):
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "macd_fast", "label": "MACD Fast", "type": "number", "default": 12},
{"name": "macd_slow", "label": "MACD Slow", "type": "number", "default": 26},
@@ -20,26 +21,24 @@ class MercyEdgeStrategy(BaseStrategy):
]
def analyze(self, df_h1):
# Ambil parameter dinamis
"""Metode untuk LIVE TRADING."""
# ... (logika live trading yang ada tetap di sini)
macd_fast = self.params.get('macd_fast', 12)
macd_slow = self.params.get('macd_slow', 26)
macd_signal_p = self.params.get('macd_signal', 9) # 'signal' adalah nama variabel yang sudah ada
macd_signal_p = self.params.get('macd_signal', 9)
stoch_k = self.params.get('stoch_k', 14)
stoch_d = self.params.get('stoch_d', 3)
stoch_smooth = self.params.get('stoch_smooth', 3)
# Ambil data sekunder (D1), data primer (H1) sudah diberikan
df_d1 = get_rates(self.bot.market_for_mt5, mt5.TIMEFRAME_D1, 200)
df_d1 = get_rates_mt5(self.bot.market_for_mt5, mt5.TIMEFRAME_D1, 200)
if df_d1 is None or df_h1 is None or len(df_d1) < 50 or len(df_h1) < 30:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup"}
# Buat nama kolom indikator secara dinamis
macd_hist_col = f'MACDh_{macd_fast}_{macd_slow}_{macd_signal_p}'
stoch_k_col = f'STOCHk_{stoch_k}_{stoch_d}_{stoch_smooth}'
stoch_d_col = f'STOCHd_{stoch_k}_{stoch_d}_{stoch_smooth}'
# Hitung indikator dengan parameter dinamis
df_d1.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal_p, append=True)
df_h1.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal_p, append=True)
df_h1.ta.stoch(k=stoch_k, d=stoch_d, smooth_k=stoch_smooth, append=True)
@@ -54,7 +53,6 @@ class MercyEdgeStrategy(BaseStrategy):
last_h1 = df_h1.iloc[-1]
prev_h1 = df_h1.iloc[-2]
# Gunakan nama kolom dinamis dalam logika
ta_suggestion = "HOLD"
if (last_d1[macd_hist_col] > 0 and last_h1[macd_hist_col] > 0 and
last_h1[stoch_k_col] > last_h1[stoch_d_col] and prev_h1[stoch_k_col] <= prev_h1[stoch_d_col]):
@@ -63,12 +61,11 @@ class MercyEdgeStrategy(BaseStrategy):
last_h1[stoch_k_col] < last_h1[stoch_d_col] and prev_h1[stoch_k_col] >= prev_h1[stoch_d_col]):
ta_suggestion = "SELL"
# AI functionality is temporarily disabled.
final_signal = ta_suggestion
return {
"signal": final_signal, "price": last_h1["close"],
"explanation": f"TA: {ta_suggestion} (AI disabled)",
"D1_MACDh": last_d1[macd_hist_col], "H1_MACDh": last_h1[macd_hist_col],
"H1_STOCHk": last_h1[stoch_k_col], "H1_STOCHd": last_h1[stoch_d_col]
}
return {"signal": final_signal, "price": last_h1["close"], "explanation": f"TA: {ta_suggestion}"}
def analyze_df(self, df):
"""Metode untuk BACKTESTING. Dinonaktifkan untuk strategi ini."""
df['signal'] = 'HOLD'
return df
+9 -9
View File
@@ -1,5 +1,6 @@
# core/strategies/pulse_sync.py
import pandas_ta as ta
import numpy as np
from .base_strategy import BaseStrategy
class PulseSyncStrategy(BaseStrategy):
@@ -7,6 +8,7 @@ class PulseSyncStrategy(BaseStrategy):
description = 'Menggunakan AI untuk menganalisis momentum harga berdasarkan Simple Moving Average (SMA).'
def analyze(self, df):
"""Metode untuk LIVE TRADING."""
if df is None or df.empty or len(df) < 30:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup"}
@@ -19,14 +21,12 @@ class PulseSyncStrategy(BaseStrategy):
last = df.iloc[-1]
price = last["close"]
# AI functionality is temporarily disabled.
signal = "HOLD"
explanation = "AI functionality is currently disabled for performance reasons."
explanation = "AI functionality is currently disabled."
analysis_data = {
"signal": signal,
"price": price,
"explanation": explanation,
"SMA_21": last.get('SMA21'),
}
return analysis_data
return {"signal": signal, "price": price, "explanation": explanation}
def analyze_df(self, df):
"""Metode untuk BACKTESTING. Dinonaktifkan untuk strategi ini."""
df['signal'] = 'HOLD'
return df
+40 -38
View File
@@ -10,7 +10,6 @@ class QuantumVelocityStrategy(BaseStrategy):
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "ema_period", "label": "Periode EMA Tren", "type": "number", "default": 200},
{"name": "bb_length", "label": "Panjang BB", "type": "number", "default": 20},
@@ -20,20 +19,16 @@ class QuantumVelocityStrategy(BaseStrategy):
]
def analyze(self, df):
"""
Menganalisis pasar dengan filter tren jangka panjang dan pemicu breakout.
"""
if df is None or df.empty or len(df) < 201:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk Quantum Velocity."}
"""Metode untuk LIVE TRADING."""
if df is None or df.empty or len(df) < self.params.get('ema_period', 200) + 1:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup."}
# --- Ambil Parameter ---
ema_period = self.params.get('ema_period', 200)
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
squeeze_window = self.params.get('squeeze_window', 10)
squeeze_factor = self.params.get('squeeze_factor', 0.7)
# --- Hitung Indikator ---
df[f'EMA_{ema_period}'] = ta.ema(df['close'], length=ema_period)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
@@ -56,38 +51,45 @@ class QuantumVelocityStrategy(BaseStrategy):
price = last["close"]
signal = "HOLD"
explanation = "Tidak ada kondisi yang terpenuhi."
trend_regime = "N/A"
# --- FILTER 1: Tentukan Rezim Tren (EMA 200) ---
if price > last[f'EMA_{ema_period}']:
trend_regime = "Bullish"
elif price < last[f'EMA_{ema_period}']:
trend_regime = "Bearish"
else:
trend_regime = "Choppy"
# --- FILTER 2: Cari Sinyal Pemicu (Squeeze Breakout) ---
is_in_squeeze = prev['SQUEEZE']
if is_in_squeeze:
explanation = f"Rezim: {trend_regime}. Squeeze terdeteksi, menunggu breakout."
# Hanya cari sinyal BUY jika dalam Rezim Bullish
if trend_regime == "Bullish" and last['close'] > prev[bbu_col]:
if prev['SQUEEZE'] and last['close'] > prev[bbu_col]:
signal = "BUY"
explanation = f"Rezim: {trend_regime}. Sinyal: Squeeze & Breakout NAIK!"
# Hanya cari sinyal SELL jika dalam Rezim Bearish
elif trend_regime == "Bearish" and last['close'] < prev[bbl_col]:
explanation = "Squeeze & Breakout NAIK!"
elif price < last[f'EMA_{ema_period}']:
if prev['SQUEEZE'] and last['close'] < prev[bbl_col]:
signal = "SELL"
explanation = f"Rezim: {trend_regime}. Sinyal: Squeeze & Breakout TURUN!"
explanation = "Squeeze & Breakout TURUN!"
analysis_data = {
"signal": signal,
"price": price,
"explanation": explanation,
"Trend_Regime": trend_regime,
f"EMA_{ema_period}": last.get(f'EMA_{ema_period}'),
"Is_Squeeze": bool(last.get('SQUEEZE', False)),
}
return analysis_data
return {"signal": signal, "price": price, "explanation": explanation}
def analyze_df(self, df):
"""Metode untuk BACKTESTING."""
ema_period = self.params.get('ema_period', 200)
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
squeeze_window = self.params.get('squeeze_window', 10)
squeeze_factor = self.params.get('squeeze_factor', 0.7)
df[f'EMA_{ema_period}'] = ta.ema(df['close'], length=ema_period)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbm_col = f'BBM_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df['BB_BANDWIDTH'] = np.where(df[bbm_col] != 0, (df[bbu_col] - df[bbl_col]) / df[bbm_col] * 100, 0)
df['AVG_BANDWIDTH'] = df['BB_BANDWIDTH'].rolling(window=squeeze_window).mean()
df['SQUEEZE_LEVEL'] = df['AVG_BANDWIDTH'] * squeeze_factor
df['SQUEEZE'] = df['BB_BANDWIDTH'] < df['SQUEEZE_LEVEL']
trend_regime_bullish = df['close'] > df[f'EMA_{ema_period}']
trend_regime_bearish = df['close'] < df[f'EMA_{ema_period}']
is_in_squeeze = df['SQUEEZE'].shift(1)
buy_signal = trend_regime_bullish & is_in_squeeze & (df['close'] > df[bbu_col].shift(1))
sell_signal = trend_regime_bearish & is_in_squeeze & (df['close'] < df[bbl_col].shift(1))
df['signal'] = np.where(buy_signal, 'BUY', np.where(sell_signal, 'SELL', 'HOLD'))
return df
+62 -48
View File
@@ -1,35 +1,30 @@
# /core/strategies/quantumbotx_hybrid.py
import pandas_ta as ta
import numpy as np
from .base_strategy import BaseStrategy
class QuantumBotXHybridStrategy(BaseStrategy):
name = 'QuantumBotX Hybrid'
description = 'Strategi eksklusif yang menggabungkan beberapa indikator untuk performa optimal di berbagai kondisi pasar.'
description = 'Strategi eksklusif yang menggabungkan beberapa indikator untuk performa optimal, kini dengan filter tren jangka panjang.'
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "adx_period", "label": "Periode ADX", "type": "number", "default": 14},
{"name": "adx_threshold", "label": "Ambang ADX", "type": "number", "default": 25},
{"name": "ma_fast_period", "label": "Periode MA Cepat", "type": "number", "default": 20},
{"name": "ma_slow_period", "label": "Periode MA Lambat", "type": "number", "default": 50},
{"name": "bb_length", "label": "Panjang BB", "type": "number", "default": 20},
{"name": "bb_std", "label": "Std Dev BB", "type": "number", "default": 2.0, "step": 0.1}
{"name": "bb_std", "label": "Std Dev BB", "type": "number", "default": 2.0, "step": 0.1},
{"name": "trend_filter_period", "label": "Periode Filter Tren (SMA)", "type": "number", "default": 200}
]
def analyze(self, df):
"""
Menganalisis pasar menggunakan strategi Hybrid yang adaptif.
Menggunakan MA Crossover saat trending (ADX > 25) dan
Bollinger Bands saat ranging (ADX < 25).
"""
required_data_points = 51 # Tetap butuh minimal 51 untuk SMA(50)
"""Metode untuk LIVE TRADING."""
trend_filter_period = self.params.get('trend_filter_period', 200)
if df is None or df.empty or len(df) < trend_filter_period:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk filter tren."}
if df is None or df.empty or len(df) < required_data_points:
return {"signal": "HOLD", "price": None, "explanation": f"Data tidak cukup ({len(df) if df is not None else 0}/{required_data_points} bar)."}
# --- Ambil Parameter Dinamis ---
adx_period = self.params.get('adx_period', 14)
adx_threshold = self.params.get('adx_threshold', 25)
ma_fast_period = self.params.get('ma_fast_period', 20)
@@ -37,63 +32,82 @@ class QuantumBotXHybridStrategy(BaseStrategy):
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
# PERBAIKAN: Paksa format float dengan satu desimal pada nama kolom
# untuk mencegah KeyError (misal: 'BBL_20_2' vs 'BBL_20_2.0')
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
trend_filter_col = f'SMA_{trend_filter_period}'
# --- Hitung SEMUA Indikator yang Dibutuhkan ---
df.ta.adx(length=adx_period, append=True)
df[f'SMA_{ma_fast_period}'] = ta.sma(df['close'], length=ma_fast_period)
df[f'SMA_{ma_slow_period}'] = ta.sma(df['close'], length=ma_slow_period)
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df[trend_filter_col] = ta.sma(df['close'], length=trend_filter_period)
# Simpan panjang sebelum dropna untuk debugging
len_before_drop = len(df)
df.dropna(inplace=True)
if len(df) < 2:
return {"signal": "HOLD", "price": None, "explanation": f"Indikator belum matang setelah dropna (dari {len_before_drop} menjadi {len(df)} bar)."}
return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."}
last = df.iloc[-1]
prev = df.iloc[-2]
price = last["close"]
signal = "HOLD"
explanation = "Kondisi pasar tidak memenuhi syarat."
market_mode = "N/A"
# --- Logika "Wasit Pasar" (ADX) ---
is_uptrend = price > last[trend_filter_col]
is_downtrend = price < last[trend_filter_col]
adx_value = last[f'ADX_{adx_period}']
# KONDISI 1: PASAR TRENDING
if adx_value > adx_threshold:
market_mode = "Trending"
explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Menunggu Crossover."
if prev[f'SMA_{ma_fast_period}'] <= prev[f'SMA_{ma_slow_period}'] and last[f'SMA_{ma_fast_period}'] > last[f'SMA_{ma_slow_period}']:
if adx_value > adx_threshold: # Mode Trending
if is_uptrend and prev[f'SMA_{ma_fast_period}'] <= prev[f'SMA_{ma_slow_period}'] and last[f'SMA_{ma_fast_period}'] > last[f'SMA_{ma_slow_period}']:
signal = "BUY"
explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Golden Cross."
elif prev[f'SMA_{ma_fast_period}'] >= prev[f'SMA_{ma_slow_period}'] and last[f'SMA_{ma_fast_period}'] < last[f'SMA_{ma_slow_period}']:
explanation = f"Uptrend & Trending: Golden Cross."
elif is_downtrend and prev[f'SMA_{ma_fast_period}'] >= prev[f'SMA_{ma_slow_period}'] and last[f'SMA_{ma_fast_period}'] < last[f'SMA_{ma_slow_period}']:
signal = "SELL"
explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Death Cross."
# KONDISI 2: PASAR SIDEWAYS
elif adx_value < adx_threshold:
market_mode = "Ranging"
explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Menunggu pantulan Bands."
if last['low'] <= last[bbl_col]:
explanation = f"Downtrend & Trending: Death Cross."
else: # Mode Ranging
if is_uptrend and last['low'] <= last[bbl_col]:
signal = "BUY"
explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Oversold di Band Bawah."
elif last['high'] >= last[bbu_col]:
explanation = f"Uptrend & Ranging: Oversold."
elif is_downtrend and last['high'] >= last[bbu_col]:
signal = "SELL"
explanation = f"Mode: {market_mode} (ADX {adx_value:.1f}). Sinyal: Overbought di Band Atas."
explanation = f"Downtrend & Ranging: Overbought."
analysis_data = {
"signal": signal,
"price": price,
"explanation": explanation,
"Market_Mode": market_mode,
f"ADX_{adx_period}": adx_value,
f"SMA_{ma_fast_period}": last.get(f'SMA_{ma_fast_period}'),
f"SMA_{ma_slow_period}": last.get(f'SMA_{ma_slow_period}'),
}
return analysis_data
return {"signal": signal, "price": price, "explanation": explanation}
def analyze_df(self, df):
"""Metode untuk BACKTESTING."""
adx_period = self.params.get('adx_period', 14)
adx_threshold = self.params.get('adx_threshold', 25)
ma_fast_period = self.params.get('ma_fast_period', 20)
ma_slow_period = self.params.get('ma_slow_period', 50)
bb_length = self.params.get('bb_length', 20)
bb_std = self.params.get('bb_std', 2.0)
trend_filter_period = self.params.get('trend_filter_period', 200)
bbu_col = f'BBU_{bb_length}_{bb_std:.1f}'
bbl_col = f'BBL_{bb_length}_{bb_std:.1f}'
trend_filter_col = f'SMA_{trend_filter_period}'
df.ta.adx(length=adx_period, append=True)
df[f'SMA_{ma_fast_period}'] = ta.sma(df['close'], length=ma_fast_period)
df[f'SMA_{ma_slow_period}'] = ta.sma(df['close'], length=ma_slow_period)
df.ta.bbands(length=bb_length, std=bb_std, append=True)
df[trend_filter_col] = ta.sma(df['close'], length=trend_filter_period)
is_trending = df[f'ADX_{adx_period}'] > adx_threshold
is_ranging = ~is_trending
is_uptrend = df['close'] > df[trend_filter_col]
is_downtrend = df['close'] < df[trend_filter_col]
golden_cross = (df[f'SMA_{ma_fast_period}'].shift(1) <= df[f'SMA_{ma_slow_period}'].shift(1)) & (df[f'SMA_{ma_fast_period}'] > df[f'SMA_{ma_slow_period}'])
death_cross = (df[f'SMA_{ma_fast_period}'].shift(1) >= df[f'SMA_{ma_slow_period}'].shift(1)) & (df[f'SMA_{ma_fast_period}'] < df[f'SMA_{ma_slow_period}'])
trending_buy = is_uptrend & is_trending & golden_cross
trending_sell = is_downtrend & is_trending & death_cross
ranging_buy = is_uptrend & is_ranging & (df['low'] <= df[bbl_col])
ranging_sell = is_downtrend & is_ranging & (df['high'] >= df[bbu_col])
df['signal'] = np.where(trending_buy | ranging_buy, 'BUY', np.where(trending_sell | ranging_sell, 'SELL', 'HOLD'))
return df
+23 -12
View File
@@ -1,5 +1,6 @@
# /core/strategies/rsi_breakout.py
import pandas_ta as ta
import numpy as np
from .base_strategy import BaseStrategy
class RSIBreakoutStrategy(BaseStrategy):
@@ -8,7 +9,6 @@ class RSIBreakoutStrategy(BaseStrategy):
@classmethod
def get_definable_params(cls):
"""Mengembalikan parameter yang bisa diatur untuk strategi ini."""
return [
{"name": "rsi_period", "label": "Periode RSI", "type": "number", "default": 14},
{"name": "overbought_level", "label": "Level Overbought", "type": "number", "default": 70},
@@ -16,35 +16,46 @@ class RSIBreakoutStrategy(BaseStrategy):
]
def analyze(self, df):
"""Metode untuk LIVE TRADING."""
rsi_period = self.params.get('rsi_period', 14)
overbought_level = self.params.get('overbought_level', 70)
oversold_level = self.params.get('oversold_level', 30)
if df is None or df.empty or len(df) < rsi_period + 2:
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup untuk RSI."}
return {"signal": "HOLD", "price": None, "explanation": "Data tidak cukup."}
df['RSI'] = ta.rsi(df['close'], length=rsi_period)
df.dropna(inplace=True)
if len(df) < 2:
return {"signal": "HOLD", "price": None, "explanation": "Indikator RSI belum matang."}
return {"signal": "HOLD", "price": None, "explanation": "Indikator belum matang."}
last = df.iloc[-1]
prev = df.iloc[-2]
price = last["close"]
signal = "HOLD"
explanation = f"RSI ({last['RSI']:.2f}) berada di zona netral."
explanation = f"RSI ({last['RSI']:.2f}) netral."
# Sinyal Beli: RSI melintasi ke atas dari level oversold
if prev['RSI'] < oversold_level and last['RSI'] >= oversold_level:
signal = "BUY"
explanation = f"RSI Breakout NAIK! RSI ({last['RSI']:.2f}) melintasi ke atas level oversold ({oversold_level})."
# Sinyal Jual: RSI melintasi ke bawah dari level overbought
explanation = f"RSI Breakout NAIK!"
elif prev['RSI'] > overbought_level and last['RSI'] <= overbought_level:
signal = "SELL"
explanation = f"RSI Breakout TURUN! RSI ({last['RSI']:.2f}) melintasi ke bawah level overbought ({overbought_level})."
explanation = f"RSI Breakout TURUN!"
return {
"signal": signal, "price": price, "explanation": explanation,
"RSI": last['RSI'], "Overbought_Level": overbought_level, "Oversold_Level": oversold_level
}
return {"signal": signal, "price": price, "explanation": explanation}
def analyze_df(self, df):
"""Metode untuk BACKTESTING."""
rsi_period = self.params.get('rsi_period', 14)
overbought_level = self.params.get('overbought_level', 70)
oversold_level = self.params.get('oversold_level', 30)
df['RSI'] = ta.rsi(df['close'], length=rsi_period)
buy_signal = (df['RSI'].shift(1) < oversold_level) & (df['RSI'] >= oversold_level)
sell_signal = (df['RSI'].shift(1) > overbought_level) & (df['RSI'] <= overbought_level)
df['signal'] = np.where(buy_signal, 'BUY', np.where(sell_signal, 'SELL', 'HOLD'))
return df
+2 -6
View File
@@ -1,6 +1,5 @@
# core/strategies/strategy_map.py
# Import the STRATEGY CLASSES, not the old analyze functions.
from .ma_crossover import MACrossoverStrategy
from .quantumbotx_hybrid import QuantumBotXHybridStrategy
from .rsi_breakout import RSIBreakoutStrategy
@@ -11,15 +10,12 @@ from .quantum_velocity import QuantumVelocityStrategy
from .pulse_sync import PulseSyncStrategy
STRATEGY_MAP = {
# The map is now a simple key-to-class mapping.
'MA_CROSSOVER': MACrossoverStrategy,
'QUANTUMBOTX_HYBRID': QuantumBotXHybridStrategy,
'RSI_BREAKOUT': RSIBreakoutStrategy,
'BOLLINGER_BANDS': BollingerBandsStrategy,
'BOLLINGER_SQUEEZE': BollingerSqueezeStrategy,
'MERCY_EDGE': MercyEdgeStrategy,
'quantum_velocity': QuantumVelocityStrategy, # <-- Tambahkan strategi baru
'quantum_velocity': QuantumVelocityStrategy,
'PULSE_SYNC': PulseSyncStrategy,
}
# NOTE: You will need to refactor your other strategy files
}
+5 -3
View File
@@ -45,12 +45,14 @@ def get_crypto_data_from_cmc():
return []
def get_mt5_symbol_profile(symbol):
if not mt5.initialize():
print("MT5 initialization failed in external.py")
# PERBAIKAN: Jangan melakukan initialize/shutdown di sini.
# Koneksi sudah dikelola secara terpusat oleh run.py.
# Cukup periksa apakah koneksi ada.
if not mt5.terminal_info():
logger.error("Koneksi MT5 tidak aktif saat mencoba get_mt5_symbol_profile.")
return None
symbol_info = mt5.symbol_info(symbol)
mt5.shutdown()
if symbol_info:
return {
+110 -42
View File
@@ -1,16 +1,16 @@
# core/utils/mt5.py
# core/utils/mt5.py (VERSI FINAL LENGKAP)
import MetaTrader5 as mt5
from datetime import datetime
from datetime import datetime, timedelta
import pandas as pd
import logging
logger = logging.getLogger(__name__)
# --- PERBAIKAN: Definisikan TIMEFRAME_MAP di sini ---
# Definisikan konstanta di satu tempat
TIMEFRAME_MAP = {
"M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5,
"M15": mt5.TIMEFRAME_M15, "H1": mt5.TIMEFRAME_H1,
"H4": mt5.TIMEFRAME_H4, "D1": mt5.TIMEFRAME_D1,
"M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5, "M15": mt5.TIMEFRAME_M15,
"H1": mt5.TIMEFRAME_H1, "H4": mt5.TIMEFRAME_H4, "D1": mt5.TIMEFRAME_D1,
"W1": mt5.TIMEFRAME_W1, "MN1": mt5.TIMEFRAME_MN1
}
@@ -19,59 +19,127 @@ def initialize_mt5(account, password, server):
if not mt5.initialize(login=account, password=password, server=server):
logger.error(f"Inisialisasi atau Login MT5 gagal: {mt5.last_error()}")
return False
logger.info(f"Berhasil login ke MT5 ({account}) di server {server}")
return True
def get_mt5_account_info():
"""Mengambil info akun dari MetaTrader 5."""
def get_account_info_mt5():
"""Mengambil informasi akun (saldo, equity, profit) dari MT5."""
try:
info = mt5.account_info()
if info is not None:
if info:
return info._asdict()
else:
logger.error(f"Gagal mengambil account_info(): {mt5.last_error()}")
logger.warning(f"Gagal mengambil info akun. Error: {mt5.last_error()}")
return None
except Exception as e:
logger.error(f"Error saat mengambil info akun MT5: {e}", exc_info=True)
logger.error(f"Error saat get_account_info_mt5: {e}", exc_info=True)
return None
def get_todays_profit():
"""Menghitung total profit dari histori trading hari ini."""
try:
# Tentukan rentang waktu dari jam 00:00 hari ini sampai sekarang
from_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
to_date = datetime.now()
# Ambil histori order yang sudah selesai
history_deals = mt5.history_deals_get(from_date, to_date)
if history_deals is None:
return 0.0
total_profit = 0.0
for deal in history_deals:
# Hanya hitung deal yang merupakan 'penutupan' posisi
if deal.entry == 1: # 0 = in, 1 = out, 2 = in/out
total_profit += deal.profit
return total_profit
except Exception as e:
logger.error(f"Gagal menghitung profit hari ini: {e}", exc_info=True)
return 0.0
def get_rates_from_mt5(symbol, timeframe, count):
"""Fungsi utama untuk mengambil data harga historis langsung dari MT5."""
def get_rates_mt5(symbol: str, timeframe: int, count: int = 100):
"""Mengambil data harga historis (rates) dari MT5 dalam bentuk DataFrame."""
try:
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, count)
if rates is None or len(rates) == 0:
logger.warning(f"Tidak ada data yang diterima dari MT5 untuk {symbol}")
return pd.DataFrame() # Kembalikan DataFrame kosong agar tidak error
logger.warning(f"Gagal mengambil data harga untuk {symbol} (Timeframe: {timeframe}).")
return pd.DataFrame()
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.set_index('time', inplace=True)
df.set_index('time', inplace=True) # Jadikan kolom 'time' sebagai index DataFrame
return df
except Exception as e:
logger.error(f"Error saat mengambil data dari MT5 untuk {symbol}: {e}", exc_info=True)
logger.error(f"Error saat get_rates_mt5 untuk {symbol}: {e}", exc_info=True)
return pd.DataFrame()
def get_open_positions_mt5():
"""Mengambil semua posisi trading yang sedang terbuka dari akun MT5."""
try:
positions = mt5.positions_get()
if positions is None:
return []
# Mengubah tuple objek menjadi list dictionary
return [pos._asdict() for pos in positions]
except Exception as e:
logger.error(f"Error saat get_open_positions_mt5: {e}", exc_info=True)
return []
def get_trade_history_mt5(days: int = 30):
"""Mengambil riwayat transaksi yang sudah ditutup dari MT5."""
try:
from_date = datetime.now() - timedelta(days=days)
deals = mt5.history_deals_get(from_date, datetime.now())
if deals is None:
logger.warning("Gagal mengambil histori deals dari MT5.")
return []
# Filter hanya deal penutupan (entry == 1) dan konversi ke dict
closed_deals = [d._asdict() for d in deals if d.entry == 1]
return closed_deals
except Exception as e:
logger.error(f"Error saat get_trade_history_mt5: {e}", exc_info=True)
return []
def get_todays_profit_mt5():
"""Menghitung total profit dari histori trading hari ini."""
try:
from_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
to_date = datetime.now()
deals = mt5.history_deals_get(from_date, to_date)
if deals is None:
logger.warning("Gagal mengambil histori deals untuk profit hari ini.")
return 0.0
# Gunakan generator expression untuk efisiensi dan filter deal penutupan
return sum(d.profit for d in deals if d.entry == 1)
except Exception as e:
logger.error(f"Error saat get_todays_profit_mt5: {e}", exc_info=True)
return 0.0
def find_mt5_symbol(base_symbol: str) -> str | None:
"""
Mencari nama simbol yang benar di MT5 berdasarkan nama dasar.
Fungsi ini mencoba mencocokkan variasi umum (suffix, prefix, nama alternatif)
dan memastikan simbol tersebut terlihat di Market Watch.
Args:
base_symbol (str): Nama simbol dasar (misal, "XAUUSD", "EURUSD").
Returns:
str | None: Nama simbol yang valid dan terlihat di MT5, atau None jika tidak ditemukan.
"""
import re
base_symbol_cleaned = re.sub(r'[^A-Z0-9]', '', base_symbol.upper())
try:
all_symbols = mt5.symbols_get()
if all_symbols is None:
logger.error("Gagal mengambil daftar simbol dari MT5.")
return None
except Exception as e:
logger.error(f"Error saat mengambil daftar simbol dari MT5: {e}")
return None
visible_symbols = {s.name for s in all_symbols if s.visible}
# 1. Cek kecocokan langsung (paling umum)
if base_symbol_cleaned in visible_symbols:
logger.info(f"Simbol '{base_symbol_cleaned}' ditemukan secara langsung.")
return base_symbol_cleaned
# 2. Buat pola regex untuk mencari variasi
pattern = re.compile(f"^[a-zA-Z]*{base_symbol_cleaned}[a-zA-Z0-9._-]*$", re.IGNORECASE)
# Cari di antara simbol yang terlihat
for symbol_name in visible_symbols:
if pattern.match(symbol_name):
logger.info(f"Variasi simbol '{symbol_name}' ditemukan untuk basis '{base_symbol_cleaned}'.")
if mt5.symbol_select(symbol_name, True):
return symbol_name
else:
logger.warning(f"Simbol '{symbol_name}' ditemukan tapi gagal diaktifkan.")
logger.warning(f"Tidak ada variasi simbol yang valid dan terlihat untuk '{base_symbol}' ditemukan di Market Watch.")
return None
+75 -54
View File
@@ -1,64 +1,85 @@
# core\utils\symbols.py
import MetaTrader5 as mt5
import logging
def get_stock_symbols():
"""
Mengambil semua simbol saham dari MT5 dan mengembalikannya sebagai list of dicts.
"""
logger = logging.getLogger(__name__)
# --- KONFIGURASI PENTING ---
# Cukup gunakan kata kunci umum yang ada di dalam path saham.
# Logika baru akan mencari kata-kata ini di mana saja dalam path.
# Contoh path: "Nasdaq\\Stock\\ZDAI" -> akan cocok dengan 'stock'.
STOCK_KEYWORDS = ['stock', 'share', 'equity', 'saham']
# Prefix untuk Forex, berdasarkan output Anda: "path": "Forex\\AUDCAD"
FOREX_PREFIX = 'forex\\'
def get_all_symbols_from_mt5():
"""Fungsi helper untuk mengambil semua simbol dari MT5 dengan aman."""
try:
if not mt5.terminal_info():
if not mt5.initialize():
print("initialize() failed, error code =", mt5.last_error())
return []
# Koneksi sudah diinisialisasi saat aplikasi pertama kali berjalan.
# Kita tidak perlu melakukan initialize() di sini lagi.
symbols = mt5.symbols_get()
stock_list = []
if symbols:
for s in symbols:
if "stocks" in s.path.lower() or "shares" in s.path.lower():
stock_list.append({
"name": s.name,
"description": s.description,
"path": s.path,
"ask": s.ask,
"bid": s.bid,
"spread": s.spread,
"digits": s.digits
})
return stock_list
except Exception as e:
print(f"Error getting stock symbols: {e}")
return symbols
logger.warning("mt5.symbols_get() tidak mengembalikan simbol apapun.")
return []
except Exception as e:
logger.error(f"Error saat mengambil simbol dari MT5: {e}", exc_info=True)
return []
def get_stock_symbols(limit=20):
"""
Mengambil simbol saham, mengurutkannya berdasarkan volume harian tertinggi,
dan mengembalikan sejumlah 'limit' teratas.
"""
all_symbols = get_all_symbols_from_mt5()
stock_details = []
if not all_symbols:
return []
for s in all_symbols:
if any(keyword in s.path.lower() for keyword in STOCK_KEYWORDS):
# Ambil info detail untuk mendapatkan volume
info = mt5.symbol_info(s.name)
if info:
stock_details.append({
"name": s.name,
"description": s.description,
# Gunakan volumehigh sebagai proksi untuk aktivitas/popularitas harian
"daily_volume": info.volumehigh
})
if not stock_details:
logger.warning(f"Tidak ada simbol saham yang ditemukan dengan kata kunci: {STOCK_KEYWORDS}. Periksa path simbol Anda dan konfigurasi di core/utils/symbols.py")
return []
# Urutkan daftar saham berdasarkan volume harian, dari tertinggi ke terendah
sorted_stocks = sorted(stock_details, key=lambda x: x.get('daily_volume', 0), reverse=True)
# Kembalikan hanya sejumlah 'limit' teratas
return sorted_stocks[:limit]
def get_forex_symbols():
"""
Mengambil semua simbol forex dari MT5 dan mengembalikannya sebagai list of dicts.
"""
try:
if not mt5.terminal_info():
if not mt5.initialize():
print("initialize() failed, error code =", mt5.last_error())
return []
"""Mengambil simbol forex berdasarkan filter path."""
all_symbols = get_all_symbols_from_mt5()
forex_list = []
symbols = mt5.symbols_get(group="*\\Forex*")
if not symbols:
symbols = mt5.symbols_get()
forex_list = []
if symbols:
for s in symbols:
if "forex" in s.path.lower():
forex_list.append({
"name": s.name,
"description": s.description,
"path": s.path,
"ask": s.ask,
"bid": s.bid,
"spread": s.spread,
"digits": s.digits,
"volume_min": s.volume_min,
"volume_max": s.volume_max
})
return forex_list
except Exception as e:
print(f"Error getting forex symbols: {e}")
if not all_symbols:
return []
for s in all_symbols:
if s.path.lower().startswith(FOREX_PREFIX):
tick = mt5.symbol_info_tick(s.name)
if tick:
forex_list.append({
"name": s.name,
"description": s.description,
"ask": tick.ask,
"bid": tick.bid,
"spread": s.spread,
"digits": s.digits,
})
return forex_list
+15 -2
View File
@@ -5,12 +5,25 @@ import markdown from "@eslint/markdown";
import css from "@eslint/css";
import { defineConfig } from "eslint/config";
// CARA BARU YANG BENAR
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
{
files: ["**/*.{js,mjs,cjs}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: {
// Gabungkan globals browser dengan global kustom kita
globals: {
...globals.browser, // ... (spread operator) untuk memasukkan semua globals browser yang sudah ada
"Chart": "readonly" // Tambahkan 'Chart' sebagai global yang hanya bisa dibaca
}
}
},
// ... sisa konfigurasi lainnya tetap sama ...
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
{ files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] },
{ files: ["**/*.jsonc"], plugins: { json }, language: "json/jsonc", extends: ["json/recommended"] },
{ files: ["**/*.json5"], plugins: { json }, language: "json/json5", extends: ["json/recommended"] },
{ files: ["**/*.md"], plugins: { markdown }, language: "markdown/gfm", extends: ["markdown/recommended"] },
{ files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] },
]);
]);
+26 -14
View File
@@ -42,7 +42,8 @@ def main():
tp_pips INTEGER NOT NULL DEFAULT 200,
timeframe TEXT NOT NULL DEFAULT 'H1',
check_interval_seconds INTEGER NOT NULL DEFAULT 60,
strategy TEXT NOT NULL
strategy TEXT NOT NULL,
strategy_params TEXT
);
"""
@@ -54,22 +55,14 @@ def main():
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
action TEXT NOT NULL,
details TEXT,
FOREIGN KEY (bot_id) REFERENCES bots (id) ON DELETE CASCADE
);
"""
# SQL statement untuk membuat tabel 'notifications'
sql_create_notifications_table = """
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_id INTEGER,
message TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
is_notification INTEGER NOT NULL DEFAULT 0,
is_read INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (bot_id) REFERENCES bots (id) ON DELETE CASCADE
);
"""
# Buat koneksi database
conn = create_connection(DB_FILE)
@@ -81,8 +74,27 @@ def main():
print("\nMembuat tabel 'trade_history'...")
create_table(conn, sql_create_history_table)
print("\nMembuat tabel 'notifications'...")
create_table(conn, sql_create_notifications_table)
# --- TAMBAHKAN INI ---
print("\nMembuat tabel 'backtest_results'...")
sql_create_backtest_results_table = """
CREATE TABLE IF NOT EXISTS backtest_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
strategy_name TEXT NOT NULL,
data_filename TEXT NOT NULL,
total_profit_pips REAL NOT NULL,
total_trades INTEGER NOT NULL,
win_rate_percent REAL NOT NULL,
max_drawdown_percent REAL NOT NULL,
equity_curve TEXT, -- Disimpan sebagai JSON
trade_log TEXT, -- Disimpan sebagai JSON
parameters TEXT -- Disimpan sebagai JSON
);
"""
create_table(conn, sql_create_backtest_results_table)
# --- SELESAI PENAMBAHAN ---
conn.close()
print(f"\nDatabase '{DB_FILE}' berhasil dibuat dengan semua tabel yang diperlukan.")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+299
View File
@@ -34518,3 +34518,302 @@ time,open,high,low,close,tick_volume,spread,real_volume
2025-07-18 21:00:00,148.706,148.777,148.693,148.76,2431,13,0
2025-07-18 22:00:00,148.763,148.8,148.728,148.736,2082,13,0
2025-07-18 23:00:00,148.737,148.845,148.688,148.809,1769,13,0
2025-07-21 00:00:00,147.877,148.247,147.877,148.185,1040,20,0
2025-07-21 01:00:00,148.193,148.654,148.193,148.583,3422,13,0
2025-07-21 02:00:00,148.58,148.648,148.366,148.446,2569,13,0
2025-07-21 03:00:00,148.446,148.477,148.236,148.378,3266,4,0
2025-07-21 04:00:00,148.387,148.533,148.325,148.506,3073,13,0
2025-07-21 05:00:00,148.507,148.552,148.43,148.496,2163,13,0
2025-07-21 06:00:00,148.496,148.536,148.384,148.453,1715,13,0
2025-07-21 07:00:00,148.453,148.515,148.415,148.437,1410,13,0
2025-07-21 08:00:00,148.437,148.441,148.253,148.288,2298,13,0
2025-07-21 09:00:00,148.293,148.3,147.766,147.926,3820,13,0
2025-07-21 10:00:00,147.922,148.236,147.909,148.095,4242,13,0
2025-07-21 11:00:00,148.09,148.126,147.686,147.888,3485,13,0
2025-07-21 12:00:00,147.89,147.964,147.841,147.911,2530,13,0
2025-07-21 13:00:00,147.909,147.969,147.748,147.749,2429,13,0
2025-07-21 14:00:00,147.745,147.758,147.5,147.538,3362,13,0
2025-07-21 15:00:00,147.538,147.545,147.33,147.484,3443,13,0
2025-07-21 16:00:00,147.484,147.715,147.484,147.519,3985,13,0
2025-07-21 17:00:00,147.52,147.539,147.281,147.416,4882,13,0
2025-07-21 18:00:00,147.418,147.42,147.249,147.26,4041,13,0
2025-07-21 19:00:00,147.26,147.323,147.076,147.155,3529,13,0
2025-07-21 20:00:00,147.155,147.301,147.106,147.246,2696,13,0
2025-07-21 21:00:00,147.246,147.374,147.208,147.32,2152,13,0
2025-07-21 22:00:00,147.32,147.366,147.264,147.351,2310,13,0
2025-07-21 23:00:00,147.351,147.429,147.281,147.281,1502,13,0
2025-07-22 00:00:00,147.256,147.326,147.208,147.295,487,25,0
2025-07-22 01:00:00,147.281,147.405,147.281,147.391,873,13,0
2025-07-22 02:00:00,147.39,147.488,147.382,147.393,1486,13,0
2025-07-22 03:00:00,147.392,147.704,147.388,147.48,4055,13,0
2025-07-22 04:00:00,147.479,147.633,147.446,147.575,2692,13,0
2025-07-22 05:00:00,147.576,147.577,147.404,147.456,2058,13,0
2025-07-22 06:00:00,147.457,147.603,147.411,147.567,2212,13,0
2025-07-22 07:00:00,147.567,147.664,147.533,147.63,1966,13,0
2025-07-22 08:00:00,147.631,147.8,147.596,147.764,2170,12,0
2025-07-22 09:00:00,147.765,147.923,147.713,147.814,2652,13,0
2025-07-22 10:00:00,147.811,147.939,147.696,147.701,2989,13,0
2025-07-22 11:00:00,147.703,147.755,147.601,147.607,3162,13,0
2025-07-22 12:00:00,147.605,147.605,147.417,147.428,2684,13,0
2025-07-22 13:00:00,147.429,147.547,147.355,147.527,1929,13,0
2025-07-22 14:00:00,147.528,147.562,147.148,147.189,3037,13,0
2025-07-22 15:00:00,147.191,147.246,146.733,146.793,4204,13,0
2025-07-22 16:00:00,146.793,146.882,146.499,146.566,5251,13,0
2025-07-22 17:00:00,146.566,146.665,146.317,146.44,5750,13,0
2025-07-22 18:00:00,146.439,146.522,146.336,146.36,4854,13,0
2025-07-22 19:00:00,146.357,146.466,146.303,146.418,3350,13,0
2025-07-22 20:00:00,146.418,146.577,146.418,146.548,2561,13,0
2025-07-22 21:00:00,146.545,146.585,146.519,146.557,2235,13,0
2025-07-22 22:00:00,146.558,146.596,146.482,146.553,1927,13,0
2025-07-22 23:00:00,146.552,146.675,146.543,146.562,1488,13,0
2025-07-23 00:00:00,146.557,146.612,146.545,146.581,771,26,0
2025-07-23 01:00:00,146.581,146.769,146.561,146.685,1167,13,0
2025-07-23 02:00:00,146.678,146.938,146.239,146.754,4252,13,0
2025-07-23 03:00:00,146.762,146.804,146.191,146.431,5916,13,0
2025-07-23 04:00:00,146.432,146.717,146.383,146.601,5213,13,0
2025-07-23 05:00:00,146.603,147.195,146.413,146.926,4675,4,0
2025-07-23 06:00:00,146.928,147.01,146.739,146.916,2845,13,0
2025-07-23 07:00:00,146.916,147.068,146.904,146.937,2651,13,0
2025-07-23 08:00:00,146.937,147.197,146.849,147.155,3305,13,0
2025-07-23 09:00:00,147.155,147.205,146.649,146.85,4363,13,0
2025-07-23 10:00:00,146.855,146.876,146.592,146.745,4066,13,0
2025-07-23 11:00:00,146.743,146.895,146.633,146.785,3536,13,0
2025-07-23 12:00:00,146.784,146.791,146.459,146.495,3029,4,0
2025-07-23 13:00:00,146.495,146.54,146.272,146.401,2810,13,0
2025-07-23 14:00:00,146.401,146.409,146.101,146.199,2703,13,0
2025-07-23 15:00:00,146.193,146.487,146.157,146.406,3239,13,0
2025-07-23 16:00:00,146.406,146.718,146.364,146.537,4836,13,0
2025-07-23 17:00:00,146.536,146.585,146.283,146.409,3992,13,0
2025-07-23 18:00:00,146.409,146.56,146.229,146.544,4024,13,0
2025-07-23 19:00:00,146.543,146.607,146.403,146.569,5102,13,0
2025-07-23 20:00:00,146.57,146.615,146.466,146.515,3319,13,0
2025-07-23 21:00:00,146.515,146.535,146.364,146.457,2651,13,0
2025-07-23 22:00:00,146.456,146.568,146.418,146.522,2379,13,0
2025-07-23 23:00:00,146.521,146.529,146.398,146.422,1824,13,0
2025-07-24 00:00:00,146.406,146.532,146.358,146.409,886,37,0
2025-07-24 01:00:00,146.435,146.511,146.344,146.443,1225,13,0
2025-07-24 02:00:00,146.442,146.443,146.236,146.339,2319,13,0
2025-07-24 03:00:00,146.344,146.484,146.259,146.421,3959,4,0
2025-07-24 04:00:00,146.426,146.429,145.847,145.917,4325,13,0
2025-07-24 05:00:00,145.923,146.142,145.923,146.036,2795,13,0
2025-07-24 06:00:00,146.036,146.101,145.89,145.998,2702,13,0
2025-07-24 07:00:00,145.998,146.073,145.927,146.013,2034,13,0
2025-07-24 08:00:00,146.008,146.104,145.95,146.007,3052,13,0
2025-07-24 09:00:00,146.007,146.296,145.983,146.268,3180,13,0
2025-07-24 10:00:00,146.266,146.449,146.136,146.428,3633,13,0
2025-07-24 11:00:00,146.428,146.558,146.303,146.537,3470,13,0
2025-07-24 12:00:00,146.539,146.639,146.518,146.547,3448,13,0
2025-07-24 13:00:00,146.545,146.652,146.52,146.564,2719,13,0
2025-07-24 14:00:00,146.563,146.616,146.457,146.475,2907,13,0
2025-07-24 15:00:00,146.474,146.824,146.441,146.788,4662,13,0
2025-07-24 16:00:00,146.787,146.865,146.473,146.536,6014,13,0
2025-07-24 17:00:00,146.538,146.81,146.349,146.773,5531,13,0
2025-07-24 18:00:00,146.773,146.938,146.73,146.918,4492,13,0
2025-07-24 19:00:00,146.916,146.945,146.778,146.892,3568,13,0
2025-07-24 20:00:00,146.893,146.894,146.746,146.805,3143,13,0
2025-07-24 21:00:00,146.806,146.921,146.788,146.913,2875,13,0
2025-07-24 22:00:00,146.912,146.96,146.842,146.929,2759,13,0
2025-07-24 23:00:00,146.93,147.017,146.877,146.981,2943,13,0
2025-07-25 00:00:00,146.971,147.092,146.936,147.088,280,13,0
2025-07-25 01:00:00,147.079,147.229,147.025,147.048,1292,13,0
2025-07-25 02:00:00,147.052,147.109,146.955,146.999,1917,13,0
2025-07-25 03:00:00,147.0,147.333,146.983,147.201,4191,4,0
2025-07-25 04:00:00,147.201,147.453,147.155,147.353,3744,13,0
2025-07-25 05:00:00,147.355,147.483,147.342,147.408,2712,13,0
2025-07-25 06:00:00,147.409,147.43,147.183,147.21,2445,13,0
2025-07-25 07:00:00,147.207,147.265,147.092,147.132,2486,13,0
2025-07-25 08:00:00,147.132,147.154,146.805,146.926,2595,13,0
2025-07-25 09:00:00,146.918,147.088,146.842,147.051,3966,13,0
2025-07-25 10:00:00,147.046,147.407,147.033,147.375,4086,13,0
2025-07-25 11:00:00,147.375,147.823,147.352,147.746,4548,13,0
2025-07-25 12:00:00,147.747,147.886,147.72,147.861,2799,13,0
2025-07-25 13:00:00,147.861,147.893,147.744,147.766,2528,13,0
2025-07-25 14:00:00,147.767,147.931,147.636,147.67,3053,13,0
2025-07-25 15:00:00,147.669,147.927,147.597,147.847,3279,4,0
2025-07-25 16:00:00,147.846,147.912,147.596,147.666,6379,13,0
2025-07-25 17:00:00,147.666,147.755,147.521,147.715,5004,4,0
2025-07-25 18:00:00,147.718,147.798,147.652,147.774,3929,13,0
2025-07-25 19:00:00,147.775,147.828,147.722,147.75,3043,13,0
2025-07-25 20:00:00,147.751,147.772,147.651,147.667,2608,13,0
2025-07-25 21:00:00,147.668,147.675,147.54,147.556,2748,13,0
2025-07-25 22:00:00,147.557,147.607,147.523,147.604,2289,13,0
2025-07-25 23:00:00,147.617,147.682,147.617,147.646,1802,14,0
2025-07-28 00:00:00,147.674,147.72,147.648,147.666,361,31,0
2025-07-28 01:00:00,147.666,147.819,147.613,147.806,1723,13,0
2025-07-28 02:00:00,147.806,147.869,147.737,147.777,2025,13,0
2025-07-28 03:00:00,147.777,147.803,147.593,147.608,4172,13,0
2025-07-28 04:00:00,147.607,147.869,147.511,147.858,3481,13,0
2025-07-28 05:00:00,147.857,148.014,147.821,147.829,2840,13,0
2025-07-28 06:00:00,147.823,147.862,147.682,147.708,2207,13,0
2025-07-28 07:00:00,147.707,147.785,147.618,147.737,2181,13,0
2025-07-28 08:00:00,147.747,147.868,147.725,147.846,2279,13,0
2025-07-28 09:00:00,147.851,148.202,147.832,148.009,4112,13,0
2025-07-28 10:00:00,148.011,148.329,147.781,148.281,4743,13,0
2025-07-28 11:00:00,148.281,148.364,148.153,148.266,3876,13,0
2025-07-28 12:00:00,148.265,148.405,148.218,148.378,3482,13,0
2025-07-28 13:00:00,148.38,148.426,148.286,148.403,3152,13,0
2025-07-28 14:00:00,148.403,148.437,148.066,148.143,3632,13,0
2025-07-28 15:00:00,148.141,148.178,148.021,148.051,4767,13,0
2025-07-28 16:00:00,148.051,148.32,147.931,148.279,4622,13,0
2025-07-28 17:00:00,148.28,148.313,148.091,148.266,4447,13,0
2025-07-28 18:00:00,148.266,148.486,148.249,148.451,4012,13,0
2025-07-28 19:00:00,148.451,148.512,148.435,148.478,2945,13,0
2025-07-28 20:00:00,148.479,148.538,148.384,148.516,2842,13,0
2025-07-28 21:00:00,148.515,148.568,148.471,148.549,2374,13,0
2025-07-28 22:00:00,148.548,148.551,148.514,148.542,1916,13,0
2025-07-28 23:00:00,148.54,148.568,148.473,148.48,1278,13,0
2025-07-29 00:00:00,148.439,148.545,148.424,148.537,215,35,0
2025-07-29 01:00:00,148.544,148.575,148.484,148.494,987,13,0
2025-07-29 02:00:00,148.494,148.629,148.449,148.45,1505,13,0
2025-07-29 03:00:00,148.45,148.703,148.434,148.677,3649,13,0
2025-07-29 04:00:00,148.676,148.696,148.461,148.584,2828,13,0
2025-07-29 05:00:00,148.584,148.584,148.282,148.337,2696,13,0
2025-07-29 06:00:00,148.339,148.44,148.3,148.348,2262,13,0
2025-07-29 07:00:00,148.351,148.49,148.321,148.34,2235,13,0
2025-07-29 08:00:00,148.34,148.398,148.166,148.233,2216,13,0
2025-07-29 09:00:00,148.232,148.532,148.15,148.511,3970,13,0
2025-07-29 10:00:00,148.512,148.735,148.482,148.57,4640,13,0
2025-07-29 11:00:00,148.572,148.598,148.348,148.545,3551,13,0
2025-07-29 12:00:00,148.545,148.564,148.442,148.544,3190,13,0
2025-07-29 13:00:00,148.548,148.604,148.377,148.535,2742,13,0
2025-07-29 14:00:00,148.537,148.746,148.508,148.708,3109,13,0
2025-07-29 15:00:00,148.708,148.764,148.596,148.688,4006,13,0
2025-07-29 16:00:00,148.696,148.8,148.521,148.556,4306,13,0
2025-07-29 17:00:00,148.556,148.74,148.428,148.471,5325,13,0
2025-07-29 18:00:00,148.471,148.614,148.344,148.568,4413,13,0
2025-07-29 19:00:00,148.568,148.595,148.446,148.552,3804,13,0
2025-07-29 20:00:00,148.552,148.577,148.329,148.479,3753,13,0
2025-07-29 21:00:00,148.478,148.548,148.423,148.425,2734,13,0
2025-07-29 22:00:00,148.423,148.496,148.338,148.492,2514,13,0
2025-07-29 23:00:00,148.492,148.504,148.387,148.395,1229,13,0
2025-07-30 00:00:00,148.373,148.476,148.368,148.421,244,68,0
2025-07-30 01:00:00,148.427,148.489,148.427,148.46,761,13,0
2025-07-30 02:00:00,148.459,148.518,148.411,148.416,1158,13,0
2025-07-30 03:00:00,148.417,148.439,148.032,148.048,3929,13,0
2025-07-30 04:00:00,148.048,148.154,147.938,148.132,3670,13,0
2025-07-30 05:00:00,148.134,148.144,148.021,148.037,2675,13,0
2025-07-30 06:00:00,148.037,148.037,147.838,147.965,2643,13,0
2025-07-30 07:00:00,147.965,148.163,147.959,148.15,1907,13,0
2025-07-30 08:00:00,148.149,148.164,147.847,148.018,2794,13,0
2025-07-30 09:00:00,148.018,148.124,147.853,147.889,3474,13,0
2025-07-30 10:00:00,147.889,148.074,147.798,148.074,3824,13,0
2025-07-30 11:00:00,148.074,148.108,147.915,147.954,3468,13,0
2025-07-30 12:00:00,147.954,148.062,147.848,148.029,3172,13,0
2025-07-30 13:00:00,148.028,148.286,147.973,148.166,2972,13,0
2025-07-30 14:00:00,148.166,148.351,148.107,148.304,2700,13,0
2025-07-30 15:00:00,148.304,148.875,148.258,148.856,5481,13,0
2025-07-30 16:00:00,148.857,149.092,148.822,149.046,4962,13,0
2025-07-30 17:00:00,149.047,149.049,148.863,148.913,4252,13,0
2025-07-30 18:00:00,148.913,149.104,148.84,149.068,3618,13,0
2025-07-30 19:00:00,149.065,149.123,148.92,148.925,2791,13,0
2025-07-30 20:00:00,148.924,148.924,148.775,148.775,2731,13,0
2025-07-30 21:00:00,148.763,149.344,148.515,149.341,8010,13,0
2025-07-30 22:00:00,149.344,149.428,149.218,149.408,5304,4,0
2025-07-30 23:00:00,149.409,149.546,149.356,149.473,2722,13,0
2025-07-31 00:00:00,149.445,149.465,149.427,149.448,206,11,0
2025-07-31 01:00:00,149.448,149.461,149.291,149.308,1455,13,0
2025-07-31 02:00:00,149.308,149.387,149.203,149.213,1969,4,0
2025-07-31 03:00:00,149.218,149.385,149.172,149.298,3593,13,0
2025-07-31 04:00:00,149.298,149.332,148.956,148.969,3390,13,0
2025-07-31 05:00:00,148.969,148.991,148.641,148.825,2542,4,0
2025-07-31 06:00:00,148.827,148.91,148.579,148.738,3160,13,0
2025-07-31 07:00:00,148.738,148.95,148.724,148.879,2883,13,0
2025-07-31 08:00:00,148.879,148.917,148.733,148.778,2768,13,0
2025-07-31 09:00:00,148.789,149.083,148.677,148.876,4781,13,0
2025-07-31 10:00:00,148.868,149.714,148.846,149.389,5158,13,0
2025-07-31 11:00:00,149.389,149.69,149.292,149.625,4575,13,0
2025-07-31 12:00:00,149.626,149.95,149.614,149.945,3639,13,0
2025-07-31 13:00:00,149.945,150.038,149.865,149.936,3353,13,0
2025-07-31 14:00:00,149.933,150.093,149.807,149.868,4157,13,0
2025-07-31 15:00:00,149.868,150.25,149.804,150.182,5325,13,0
2025-07-31 16:00:00,150.181,150.575,150.168,150.558,5157,13,0
2025-07-31 17:00:00,150.557,150.625,150.369,150.47,6178,13,0
2025-07-31 18:00:00,150.47,150.595,150.339,150.593,5367,13,0
2025-07-31 19:00:00,150.593,150.782,150.586,150.677,3755,13,0
2025-07-31 20:00:00,150.676,150.715,150.55,150.653,4040,13,0
2025-07-31 21:00:00,150.653,150.778,150.653,150.748,3546,13,0
2025-07-31 22:00:00,150.749,150.793,150.684,150.786,3081,13,0
2025-07-31 23:00:00,150.787,150.831,150.7,150.709,2646,13,0
2025-08-01 00:00:00,150.705,150.741,150.638,150.677,217,27,0
2025-08-01 01:00:00,150.683,150.794,150.681,150.736,1380,13,0
2025-08-01 02:00:00,150.735,150.87,150.631,150.803,2768,13,0
2025-08-01 03:00:00,150.803,150.886,150.586,150.728,4409,13,0
2025-08-01 04:00:00,150.729,150.911,150.624,150.897,4395,13,0
2025-08-01 05:00:00,150.898,150.903,150.702,150.753,3253,4,0
2025-08-01 06:00:00,150.756,150.77,150.546,150.581,2572,13,0
2025-08-01 07:00:00,150.578,150.651,150.544,150.599,1858,13,0
2025-08-01 08:00:00,150.602,150.65,150.407,150.466,2476,13,0
2025-08-01 09:00:00,150.467,150.638,150.442,150.598,3686,13,0
2025-08-01 10:00:00,150.597,150.614,150.448,150.531,4498,13,0
2025-08-01 11:00:00,150.529,150.618,150.499,150.602,3595,13,0
2025-08-01 12:00:00,150.602,150.622,150.41,150.501,3327,13,0
2025-08-01 13:00:00,150.499,150.505,150.313,150.351,3326,5,0
2025-08-01 14:00:00,150.355,150.52,150.274,150.51,3298,13,0
2025-08-01 15:00:00,150.51,150.589,148.556,148.626,7636,13,0
2025-08-01 16:00:00,148.627,148.653,147.983,148.119,9592,13,0
2025-08-01 17:00:00,148.095,148.216,147.48,148.081,9895,13,0
2025-08-01 18:00:00,148.081,148.176,147.921,148.047,7120,13,0
2025-08-01 19:00:00,148.048,148.1,147.775,147.819,5145,13,0
2025-08-01 20:00:00,147.819,147.941,147.719,147.869,5208,13,0
2025-08-01 21:00:00,147.869,147.893,147.673,147.844,4777,13,0
2025-08-01 22:00:00,147.843,147.854,147.319,147.43,4955,0,0
2025-08-01 23:00:00,147.423,147.452,147.284,147.342,2998,0,0
2025-08-04 00:00:00,147.241,147.25,147.172,147.204,266,6,0
2025-08-04 01:00:00,147.195,147.295,147.057,147.244,1619,3,0
2025-08-04 02:00:00,147.243,147.384,147.116,147.343,1890,2,0
2025-08-04 03:00:00,147.343,147.691,147.269,147.65,3048,2,0
2025-08-04 04:00:00,147.652,147.905,147.621,147.766,2546,2,0
2025-08-04 05:00:00,147.764,147.873,147.627,147.793,1932,3,0
2025-08-04 06:00:00,147.791,147.791,147.588,147.69,1656,3,0
2025-08-04 07:00:00,147.693,147.707,147.555,147.685,1461,3,0
2025-08-04 08:00:00,147.684,147.867,147.654,147.781,1660,4,0
2025-08-04 09:00:00,147.773,147.845,147.532,147.64,2438,3,0
2025-08-04 10:00:00,147.644,148.044,147.597,147.884,2867,3,0
2025-08-04 11:00:00,147.877,148.087,147.863,147.892,2581,3,0
2025-08-04 12:00:00,147.893,147.942,147.679,147.812,2154,3,0
2025-08-04 13:00:00,147.809,147.85,147.564,147.575,2031,3,0
2025-08-04 14:00:00,147.572,147.596,147.342,147.344,2511,3,0
2025-08-04 15:00:00,147.341,147.436,147.008,147.194,2614,3,0
2025-08-04 16:00:00,147.192,147.315,146.959,147.086,3157,3,0
2025-08-04 17:00:00,147.085,147.26,146.863,147.241,3405,2,0
2025-08-04 18:00:00,147.244,147.353,147.042,147.096,2756,3,0
2025-08-04 19:00:00,147.093,147.216,147.003,147.037,2135,2,0
2025-08-04 20:00:00,147.037,147.096,146.96,146.965,1836,2,0
2025-08-04 21:00:00,146.964,147.006,146.911,146.944,1982,2,0
2025-08-04 22:00:00,146.945,147.057,146.91,147.027,1536,3,0
2025-08-04 23:00:00,147.026,147.107,146.954,147.057,1114,4,0
2025-08-05 00:00:00,147.004,147.105,146.984,147.091,206,23,0
2025-08-05 01:00:00,147.101,147.101,146.862,146.915,989,3,0
2025-08-05 02:00:00,146.912,146.934,146.696,146.793,1473,3,0
2025-08-05 03:00:00,146.782,147.034,146.618,146.927,2434,2,0
2025-08-05 04:00:00,146.926,147.112,146.884,147.051,2241,3,0
2025-08-05 05:00:00,147.054,147.227,147.04,147.186,1827,3,0
2025-08-05 06:00:00,147.188,147.259,147.134,147.213,1291,3,0
2025-08-05 07:00:00,147.214,147.26,147.004,147.083,1289,3,0
2025-08-05 08:00:00,147.086,147.238,147.059,147.173,1602,2,0
2025-08-05 09:00:00,147.173,147.34,147.031,147.328,2002,3,0
2025-08-05 10:00:00,147.324,147.484,147.154,147.386,2356,2,0
2025-08-05 11:00:00,147.386,147.634,147.335,147.583,2324,3,0
2025-08-05 12:00:00,147.582,147.638,147.45,147.627,1916,3,0
2025-08-05 13:00:00,147.628,147.778,147.58,147.616,1554,2,0
2025-08-05 14:00:00,147.623,147.694,147.546,147.632,1806,3,0
2025-08-05 15:00:00,147.63,147.753,147.546,147.664,2754,3,0
2025-08-05 16:00:00,147.66,147.834,147.506,147.707,2965,2,0
2025-08-05 17:00:00,147.581,147.731,147.354,147.45,4087,2,0
2025-08-05 18:00:00,147.447,147.568,147.305,147.494,3064,3,0
2025-08-05 19:00:00,147.495,147.591,147.428,147.545,2170,3,0
2025-08-05 20:00:00,147.54,147.651,147.484,147.619,1682,3,0
2025-08-05 21:00:00,147.62,147.679,147.591,147.607,1800,3,0
2025-08-05 22:00:00,147.606,147.702,147.581,147.606,1386,3,0
2025-08-05 23:00:00,147.608,147.618,147.5,147.558,888,3,0
2025-08-06 00:00:00,147.544,147.581,147.5,147.568,236,23,0
2025-08-06 01:00:00,147.552,147.638,147.545,147.562,771,3,0
2025-08-06 02:00:00,147.562,147.644,147.517,147.527,1075,3,0
2025-08-06 03:00:00,147.524,147.621,147.448,147.583,2167,3,0
2025-08-06 04:00:00,147.583,147.726,147.583,147.693,1791,2,0
2025-08-06 05:00:00,147.694,147.748,147.527,147.554,1689,2,0
2025-08-06 06:00:00,147.552,147.557,147.334,147.4,1483,3,0
2025-08-06 07:00:00,147.397,147.445,147.305,147.345,1198,3,0
2025-08-06 08:00:00,147.345,147.543,147.335,147.45,1417,3,0
2025-08-06 09:00:00,147.454,147.55,147.363,147.55,1976,2,0
2025-08-06 10:00:00,147.554,147.691,147.531,147.562,2170,3,0
Can't render this file because it is too large.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+286
View File
@@ -32773,3 +32773,289 @@ time,open,high,low,close,tick_volume,spread,real_volume
2025-07-18 21:00:00,3352.32,3353.41,3350.17,3351.3,2801,10,0
2025-07-18 22:00:00,3351.27,3352.74,3347.65,3348.22,2792,9,0
2025-07-18 23:00:00,3348.26,3352.34,3347.81,3350.85,1703,5,0
2025-07-21 01:00:00,3350.01,3350.06,3345.0,3347.76,1849,5,0
2025-07-21 02:00:00,3347.67,3349.49,3345.45,3349.15,1868,14,0
2025-07-21 03:00:00,3349.16,3354.56,3347.74,3352.27,2622,10,0
2025-07-21 04:00:00,3352.1,3358.58,3351.99,3356.54,3367,8,0
2025-07-21 05:00:00,3356.21,3357.39,3352.35,3354.09,2906,8,0
2025-07-21 06:00:00,3354.08,3356.89,3351.08,3351.25,2460,5,0
2025-07-21 07:00:00,3351.21,3358.67,3350.72,3357.79,2252,8,0
2025-07-21 08:00:00,3357.78,3369.62,3357.63,3366.65,3884,6,0
2025-07-21 09:00:00,3366.72,3370.76,3365.1,3368.35,3600,6,0
2025-07-21 10:00:00,3368.36,3370.46,3363.95,3364.75,3749,8,0
2025-07-21 11:00:00,3364.74,3366.87,3363.11,3365.92,3367,5,0
2025-07-21 12:00:00,3365.97,3366.43,3362.85,3366.06,2936,11,0
2025-07-21 13:00:00,3366.17,3367.99,3362.86,3366.04,2810,10,0
2025-07-21 14:00:00,3366.12,3375.63,3365.26,3375.37,3741,5,0
2025-07-21 15:00:00,3375.32,3388.6,3374.56,3387.2,4716,5,0
2025-07-21 16:00:00,3387.41,3395.17,3383.71,3388.64,5135,5,0
2025-07-21 17:00:00,3388.65,3401.59,3386.17,3400.34,5516,5,0
2025-07-21 18:00:00,3400.48,3401.04,3395.65,3397.98,4793,10,0
2025-07-21 19:00:00,3397.93,3399.01,3393.81,3395.63,4171,7,0
2025-07-21 20:00:00,3395.65,3395.81,3391.56,3393.27,3803,8,0
2025-07-21 21:00:00,3393.26,3395.74,3393.06,3395.37,3512,9,0
2025-07-21 22:00:00,3395.46,3399.74,3395.13,3398.81,3926,13,0
2025-07-21 23:00:00,3399.11,3399.29,3396.8,3397.41,1933,9,0
2025-07-22 01:00:00,3399.04,3401.24,3398.43,3399.39,1299,11,0
2025-07-22 02:00:00,3399.54,3402.67,3393.34,3396.55,2365,7,0
2025-07-22 03:00:00,3396.58,3397.56,3390.52,3394.58,3475,10,0
2025-07-22 04:00:00,3394.62,3395.77,3385.17,3385.94,4105,5,0
2025-07-22 05:00:00,3385.93,3391.97,3385.3,3391.5,3710,8,0
2025-07-22 06:00:00,3391.53,3393.67,3388.89,3392.67,3195,5,0
2025-07-22 07:00:00,3392.62,3392.62,3387.99,3391.08,2450,12,0
2025-07-22 08:00:00,3391.4,3391.9,3384.64,3385.55,3526,7,0
2025-07-22 09:00:00,3385.56,3387.84,3384.39,3384.78,3882,5,0
2025-07-22 10:00:00,3385.09,3387.55,3383.26,3387.55,3515,12,0
2025-07-22 11:00:00,3387.41,3390.08,3386.17,3389.0,3815,8,0
2025-07-22 12:00:00,3388.97,3390.61,3384.65,3386.16,2901,5,0
2025-07-22 13:00:00,3386.17,3388.64,3385.89,3386.99,2666,8,0
2025-07-22 14:00:00,3387.05,3391.72,3386.66,3391.33,3362,6,0
2025-07-22 15:00:00,3391.47,3407.84,3391.07,3407.63,4945,5,0
2025-07-22 16:00:00,3407.55,3420.15,3406.62,3412.34,6002,5,0
2025-07-22 17:00:00,3412.35,3427.7,3405.19,3426.27,6129,5,0
2025-07-22 18:00:00,3426.28,3430.55,3423.67,3426.59,5269,7,0
2025-07-22 19:00:00,3426.66,3432.97,3426.3,3432.2,4738,5,0
2025-07-22 20:00:00,3432.26,3433.53,3426.55,3429.21,4813,7,0
2025-07-22 21:00:00,3429.23,3430.97,3427.89,3429.63,3963,10,0
2025-07-22 22:00:00,3429.53,3432.47,3428.96,3431.43,4082,5,0
2025-07-22 23:00:00,3431.54,3432.65,3427.78,3429.58,2424,7,0
2025-07-23 01:00:00,3430.9,3431.09,3427.45,3427.88,1580,10,0
2025-07-23 02:00:00,3427.88,3433.58,3426.26,3427.46,2568,6,0
2025-07-23 03:00:00,3427.06,3438.77,3427.06,3428.85,4052,5,0
2025-07-23 04:00:00,3428.9,3430.61,3421.05,3421.09,4602,7,0
2025-07-23 05:00:00,3421.02,3425.03,3419.26,3422.99,4086,5,0
2025-07-23 06:00:00,3422.97,3428.68,3422.91,3423.09,3748,5,0
2025-07-23 07:00:00,3423.09,3425.42,3422.06,3423.51,3138,9,0
2025-07-23 08:00:00,3423.56,3423.8,3416.41,3420.43,3857,5,0
2025-07-23 09:00:00,3420.56,3425.14,3419.58,3425.06,4247,10,0
2025-07-23 10:00:00,3424.82,3429.64,3419.96,3420.66,4356,10,0
2025-07-23 11:00:00,3420.69,3428.75,3420.64,3428.4,3637,7,0
2025-07-23 12:00:00,3428.45,3433.84,3428.35,3431.13,2938,6,0
2025-07-23 13:00:00,3431.14,3431.37,3421.21,3422.74,3408,6,0
2025-07-23 14:00:00,3422.54,3429.79,3420.33,3428.12,3187,5,0
2025-07-23 15:00:00,3428.11,3429.96,3419.95,3419.98,3917,5,0
2025-07-23 16:00:00,3419.97,3420.54,3405.56,3413.51,5274,5,0
2025-07-23 17:00:00,3413.37,3419.84,3410.74,3413.53,4581,6,0
2025-07-23 18:00:00,3413.51,3415.89,3398.24,3398.24,4054,5,0
2025-07-23 19:00:00,3398.03,3398.03,3381.52,3387.36,5712,5,0
2025-07-23 20:00:00,3387.36,3394.84,3385.76,3393.23,4290,5,0
2025-07-23 21:00:00,3393.29,3395.87,3392.05,3393.6,3492,8,0
2025-07-23 22:00:00,3393.71,3395.88,3388.59,3390.06,3494,5,0
2025-07-23 23:00:00,3390.11,3390.35,3387.86,3388.1,1628,12,0
2025-07-24 01:00:00,3390.56,3392.49,3386.69,3386.7,1416,10,0
2025-07-24 02:00:00,3386.71,3391.52,3386.44,3389.96,1874,8,0
2025-07-24 03:00:00,3389.97,3393.19,3388.62,3391.42,2871,5,0
2025-07-24 04:00:00,3391.63,3393.35,3384.13,3387.65,4683,5,0
2025-07-24 05:00:00,3387.71,3388.87,3380.95,3383.84,4467,5,0
2025-07-24 06:00:00,3383.8,3386.03,3374.79,3383.83,4257,8,0
2025-07-24 07:00:00,3383.75,3385.36,3381.01,3382.82,2973,7,0
2025-07-24 08:00:00,3382.63,3383.0,3374.5,3379.13,3756,5,0
2025-07-24 09:00:00,3379.16,3379.57,3370.06,3375.96,4142,6,0
2025-07-24 10:00:00,3375.9,3377.53,3366.09,3367.76,3653,5,0
2025-07-24 11:00:00,3367.76,3374.7,3365.88,3369.69,3666,5,0
2025-07-24 12:00:00,3369.51,3370.06,3362.03,3363.65,3346,5,0
2025-07-24 13:00:00,3363.63,3367.25,3362.14,3364.3,2717,6,0
2025-07-24 14:00:00,3364.34,3366.47,3361.25,3362.26,3088,9,0
2025-07-24 15:00:00,3362.16,3365.51,3352.66,3353.39,4705,5,0
2025-07-24 16:00:00,3353.21,3372.1,3351.39,3366.66,5610,5,0
2025-07-24 17:00:00,3366.7,3377.39,3365.72,3373.75,5015,8,0
2025-07-24 18:00:00,3373.59,3375.18,3370.33,3372.5,4470,8,0
2025-07-24 19:00:00,3372.48,3373.69,3366.99,3366.99,3262,10,0
2025-07-24 20:00:00,3366.95,3371.77,3366.23,3370.03,3034,8,0
2025-07-24 21:00:00,3370.04,3373.34,3367.41,3372.02,2807,5,0
2025-07-24 22:00:00,3371.91,3373.05,3370.05,3370.47,2660,5,0
2025-07-24 23:00:00,3370.52,3371.22,3367.28,3367.5,1575,5,0
2025-07-25 01:00:00,3369.25,3369.61,3365.3,3368.87,1884,5,0
2025-07-25 02:00:00,3368.81,3371.38,3366.89,3369.44,1959,5,0
2025-07-25 03:00:00,3369.49,3373.55,3368.42,3372.25,2361,5,0
2025-07-25 04:00:00,3372.3,3372.71,3365.62,3370.35,3362,5,0
2025-07-25 05:00:00,3370.38,3370.54,3362.51,3363.21,2869,5,0
2025-07-25 06:00:00,3362.99,3363.46,3358.85,3359.11,2452,5,0
2025-07-25 07:00:00,3359.01,3363.48,3357.81,3361.01,2400,5,0
2025-07-25 08:00:00,3361.02,3362.37,3355.27,3358.71,2674,11,0
2025-07-25 09:00:00,3358.57,3359.69,3354.86,3359.46,3061,5,0
2025-07-25 10:00:00,3359.24,3360.16,3352.71,3354.05,2614,8,0
2025-07-25 11:00:00,3354.02,3355.3,3344.55,3345.51,2801,5,0
2025-07-25 12:00:00,3345.48,3348.92,3343.14,3345.85,2362,5,0
2025-07-25 13:00:00,3345.84,3348.04,3341.01,3342.79,2388,5,0
2025-07-25 14:00:00,3343.13,3345.15,3338.67,3341.07,2694,5,0
2025-07-25 15:00:00,3341.02,3344.09,3336.28,3340.79,3207,5,0
2025-07-25 16:00:00,3340.82,3347.49,3336.81,3343.97,4460,5,0
2025-07-25 17:00:00,3344.17,3344.92,3330.52,3331.87,4740,10,0
2025-07-25 18:00:00,3331.58,3335.91,3325.08,3327.82,4425,7,0
2025-07-25 19:00:00,3327.87,3331.39,3326.27,3329.68,3037,10,0
2025-07-25 20:00:00,3329.82,3336.74,3329.62,3335.92,2731,10,0
2025-07-25 21:00:00,3335.93,3339.33,3335.84,3338.73,2177,12,0
2025-07-25 22:00:00,3338.74,3340.21,3337.3,3337.74,2590,5,0
2025-07-25 23:00:00,3337.78,3339.15,3336.19,3336.65,1564,5,0
2025-07-28 01:00:00,3331.05,3335.43,3323.89,3329.65,2749,5,0
2025-07-28 02:00:00,3329.6,3337.33,3329.46,3333.67,2741,5,0
2025-07-28 03:00:00,3333.69,3336.03,3327.64,3333.38,2996,5,0
2025-07-28 04:00:00,3333.11,3338.78,3329.98,3331.95,3852,5,0
2025-07-28 05:00:00,3331.93,3336.44,3330.58,3335.63,2974,5,0
2025-07-28 06:00:00,3335.53,3343.21,3334.3,3343.21,2674,5,0
2025-07-28 07:00:00,3343.16,3343.98,3339.45,3339.65,2419,5,0
2025-07-28 08:00:00,3339.62,3344.18,3338.09,3343.39,2954,5,0
2025-07-28 09:00:00,3343.2,3345.2,3335.85,3340.25,3482,5,0
2025-07-28 10:00:00,3340.21,3341.85,3334.04,3334.97,3131,9,0
2025-07-28 11:00:00,3334.99,3338.83,3332.38,3335.98,2747,7,0
2025-07-28 12:00:00,3335.99,3340.72,3335.98,3337.82,2791,9,0
2025-07-28 13:00:00,3337.78,3338.78,3334.21,3335.82,2704,9,0
2025-07-28 14:00:00,3335.97,3338.27,3334.45,3336.52,2464,6,0
2025-07-28 15:00:00,3336.85,3338.36,3325.5,3328.46,3884,5,0
2025-07-28 16:00:00,3328.28,3330.62,3301.89,3302.25,4493,5,0
2025-07-28 17:00:00,3302.29,3314.68,3301.8,3307.95,4931,5,0
2025-07-28 18:00:00,3307.8,3317.18,3306.6,3314.83,4016,5,0
2025-07-28 19:00:00,3314.71,3315.03,3311.52,3312.25,3102,10,0
2025-07-28 20:00:00,3312.22,3313.19,3308.81,3310.74,2912,8,0
2025-07-28 21:00:00,3310.69,3315.55,3310.2,3313.63,2740,5,0
2025-07-28 22:00:00,3313.61,3318.84,3313.03,3317.11,3111,10,0
2025-07-28 23:00:00,3317.03,3318.42,3314.46,3314.78,1633,14,0
2025-07-29 01:00:00,3314.18,3315.21,3312.02,3314.58,1920,8,0
2025-07-29 02:00:00,3314.45,3314.74,3310.6,3312.54,1599,5,0
2025-07-29 03:00:00,3312.49,3315.82,3307.94,3311.66,2786,5,0
2025-07-29 04:00:00,3311.53,3320.82,3308.58,3318.93,3500,5,0
2025-07-29 05:00:00,3318.95,3320.27,3317.59,3318.91,2849,5,0
2025-07-29 06:00:00,3318.59,3318.87,3311.18,3312.53,2790,5,0
2025-07-29 07:00:00,3312.52,3317.42,3308.96,3316.55,2668,5,0
2025-07-29 08:00:00,3316.57,3320.15,3314.37,3319.82,2782,5,0
2025-07-29 09:00:00,3319.92,3322.37,3316.28,3316.92,3675,5,0
2025-07-29 10:00:00,3317.21,3318.67,3311.29,3317.08,3335,12,0
2025-07-29 11:00:00,3317.06,3329.81,3316.64,3326.42,3352,9,0
2025-07-29 12:00:00,3326.38,3328.24,3320.92,3324.52,3283,5,0
2025-07-29 13:00:00,3324.55,3326.39,3322.03,3322.14,2255,5,0
2025-07-29 14:00:00,3322.17,3322.17,3314.97,3315.11,3088,5,0
2025-07-29 15:00:00,3315.17,3324.53,3315.17,3323.26,3569,8,0
2025-07-29 16:00:00,3323.37,3324.23,3312.9,3317.61,4354,5,0
2025-07-29 17:00:00,3317.45,3329.51,3311.99,3328.89,4488,5,0
2025-07-29 18:00:00,3328.91,3334.14,3319.69,3320.74,4215,5,0
2025-07-29 19:00:00,3320.72,3324.7,3319.09,3323.12,3243,12,0
2025-07-29 20:00:00,3323.1,3329.24,3322.98,3328.31,3023,5,0
2025-07-29 21:00:00,3328.3,3330.89,3325.35,3325.86,2889,8,0
2025-07-29 22:00:00,3325.88,3328.73,3322.43,3325.12,3228,10,0
2025-07-29 23:00:00,3324.92,3326.64,3324.72,3325.5,1563,6,0
2025-07-30 01:00:00,3327.36,3329.21,3325.98,3327.86,1066,5,0
2025-07-30 02:00:00,3327.85,3328.3,3326.13,3327.59,1309,8,0
2025-07-30 03:00:00,3327.53,3333.61,3327.31,3330.21,2666,5,0
2025-07-30 04:00:00,3330.2,3333.33,3321.92,3323.39,3456,8,0
2025-07-30 05:00:00,3323.3,3332.37,3323.3,3329.45,3204,5,0
2025-07-30 06:00:00,3329.43,3329.79,3325.97,3328.45,2407,5,0
2025-07-30 07:00:00,3328.43,3329.82,3325.84,3327.21,2164,10,0
2025-07-30 08:00:00,3326.81,3330.68,3324.36,3324.96,2768,14,0
2025-07-30 09:00:00,3324.94,3327.56,3323.25,3327.15,3098,5,0
2025-07-30 10:00:00,3327.26,3332.17,3325.67,3327.73,2820,8,0
2025-07-30 11:00:00,3327.84,3333.4,3327.58,3332.23,2586,5,0
2025-07-30 12:00:00,3332.04,3334.05,3329.29,3331.33,2518,10,0
2025-07-30 13:00:00,3331.29,3332.94,3326.68,3328.45,3087,5,0
2025-07-30 14:00:00,3328.53,3331.63,3324.01,3330.85,3217,5,0
2025-07-30 15:00:00,3330.74,3331.85,3310.74,3313.36,4047,5,0
2025-07-30 16:00:00,3312.43,3316.05,3298.49,3303.06,4894,6,0
2025-07-30 17:00:00,3303.09,3308.39,3301.21,3302.13,3941,5,0
2025-07-30 18:00:00,3302.13,3303.36,3288.7,3290.39,3895,5,0
2025-07-30 19:00:00,3290.41,3298.53,3289.91,3298.06,3005,6,0
2025-07-30 20:00:00,3298.15,3302.03,3297.74,3301.4,2788,5,0
2025-07-30 21:00:00,3301.41,3304.72,3279.69,3280.15,5150,6,0
2025-07-30 22:00:00,3280.27,3282.57,3268.17,3269.38,4979,5,0
2025-07-30 23:00:00,3269.21,3276.2,3269.04,3275.86,2930,5,0
2025-07-31 01:00:00,3277.45,3280.14,3276.45,3278.95,1991,11,0
2025-07-31 02:00:00,3278.97,3285.71,3277.88,3285.3,2059,5,0
2025-07-31 03:00:00,3285.08,3288.46,3282.77,3286.34,3487,5,0
2025-07-31 04:00:00,3286.52,3293.97,3285.27,3293.9,4230,7,0
2025-07-31 05:00:00,3293.82,3297.51,3290.5,3297.24,3525,5,0
2025-07-31 06:00:00,3297.25,3298.52,3293.47,3295.06,3206,5,0
2025-07-31 07:00:00,3294.98,3298.61,3293.77,3298.17,2456,5,0
2025-07-31 08:00:00,3298.17,3301.55,3294.7,3299.68,2985,5,0
2025-07-31 09:00:00,3299.56,3304.94,3298.16,3299.01,3854,5,0
2025-07-31 10:00:00,3299.04,3309.66,3298.08,3304.61,3416,5,0
2025-07-31 11:00:00,3304.72,3314.81,3303.6,3310.04,3678,8,0
2025-07-31 12:00:00,3309.91,3313.14,3304.88,3306.66,3428,5,0
2025-07-31 13:00:00,3306.64,3308.0,3293.8,3296.27,3798,7,0
2025-07-31 14:00:00,3296.51,3307.72,3295.19,3306.12,4145,8,0
2025-07-31 15:00:00,3305.83,3311.06,3303.93,3307.59,4357,5,0
2025-07-31 16:00:00,3307.64,3310.28,3300.59,3301.37,5269,5,0
2025-07-31 17:00:00,3301.13,3303.04,3291.4,3296.18,5147,5,0
2025-07-31 18:00:00,3296.14,3299.0,3291.38,3295.5,4385,8,0
2025-07-31 19:00:00,3295.49,3297.29,3289.65,3295.03,4283,5,0
2025-07-31 20:00:00,3294.88,3298.39,3294.02,3294.95,3760,6,0
2025-07-31 21:00:00,3294.98,3297.06,3292.81,3295.72,3505,6,0
2025-07-31 22:00:00,3295.71,3296.39,3290.55,3290.99,3790,7,0
2025-07-31 23:00:00,3291.03,3293.7,3290.08,3290.68,1741,5,0
2025-08-01 01:00:00,3291.51,3295.14,3290.29,3295.0,1246,8,0
2025-08-01 02:00:00,3295.01,3295.11,3288.06,3288.57,2201,9,0
2025-08-01 03:00:00,3288.46,3293.77,3286.47,3290.25,3058,5,0
2025-08-01 04:00:00,3290.2,3293.58,3281.59,3283.8,3549,8,0
2025-08-01 05:00:00,3283.7,3292.46,3283.35,3291.78,3334,7,0
2025-08-01 06:00:00,3291.78,3297.08,3290.54,3291.39,2808,7,0
2025-08-01 07:00:00,3291.38,3295.94,3290.07,3292.07,2669,5,0
2025-08-01 08:00:00,3292.09,3300.3,3291.81,3296.71,2860,5,0
2025-08-01 09:00:00,3296.63,3297.37,3290.56,3292.36,3337,5,0
2025-08-01 10:00:00,3292.22,3293.44,3285.31,3286.87,3492,13,0
2025-08-01 11:00:00,3286.71,3294.37,3285.43,3294.26,3285,10,0
2025-08-01 12:00:00,3294.35,3296.39,3292.79,3294.49,3205,7,0
2025-08-01 13:00:00,3294.57,3302.97,3293.34,3298.28,2921,5,0
2025-08-01 14:00:00,3298.7,3301.32,3296.87,3300.36,2950,5,0
2025-08-01 15:00:00,3300.44,3345.22,3297.37,3341.26,4845,5,0
2025-08-01 16:00:00,3341.5,3354.46,3338.85,3342.07,5581,5,0
2025-08-01 17:00:00,3344.94,3355.07,3341.39,3345.88,5297,5,0
2025-08-01 18:00:00,3346.08,3351.26,3341.59,3348.55,4522,5,0
2025-08-01 19:00:00,3348.53,3349.36,3340.55,3347.28,3992,5,0
2025-08-01 20:00:00,3347.24,3349.45,3345.57,3348.25,3670,6,0
2025-08-01 21:00:00,3348.39,3351.29,3345.93,3347.59,3159,6,0
2025-08-01 22:00:00,3347.8,3361.27,3347.71,3357.17,3606,5,0
2025-08-01 23:00:00,3357.13,3363.45,3357.08,3363.23,2336,0,0
2025-08-04 01:00:00,3362.95,3364.62,3357.61,3359.01,2477,5,0
2025-08-04 02:00:00,3359.11,3362.0,3354.59,3362.0,2243,5,0
2025-08-04 03:00:00,3361.96,3362.39,3350.26,3352.18,3160,5,0
2025-08-04 04:00:00,3352.05,3353.61,3345.07,3352.28,3876,5,0
2025-08-04 05:00:00,3352.24,3355.21,3350.25,3354.35,2832,11,0
2025-08-04 06:00:00,3354.42,3360.84,3354.42,3360.4,2921,5,0
2025-08-04 07:00:00,3360.28,3362.16,3358.83,3358.97,2290,5,0
2025-08-04 08:00:00,3358.97,3361.0,3355.51,3357.56,2516,5,0
2025-08-04 09:00:00,3357.29,3362.65,3353.64,3362.6,3497,5,0
2025-08-04 10:00:00,3362.46,3362.52,3351.7,3355.81,3797,7,0
2025-08-04 11:00:00,3356.0,3359.75,3354.13,3358.43,3314,5,0
2025-08-04 12:00:00,3358.41,3361.73,3356.93,3357.97,3269,5,0
2025-08-04 13:00:00,3357.86,3358.95,3353.03,3357.81,2992,10,0
2025-08-04 14:00:00,3357.87,3370.66,3357.72,3370.27,3575,5,0
2025-08-04 15:00:00,3370.28,3371.48,3361.04,3367.78,3959,5,0
2025-08-04 16:00:00,3367.83,3385.37,3367.13,3383.23,4665,5,0
2025-08-04 17:00:00,3383.51,3384.62,3371.92,3374.34,4476,5,0
2025-08-04 18:00:00,3372.62,3376.08,3370.71,3374.27,3760,10,0
2025-08-04 19:00:00,3374.28,3378.12,3370.58,3370.96,3290,7,0
2025-08-04 20:00:00,3370.95,3374.27,3370.7,3374.04,2861,7,0
2025-08-04 21:00:00,3374.03,3376.78,3373.22,3375.72,2561,12,0
2025-08-04 22:00:00,3375.69,3377.06,3374.43,3376.0,2897,8,0
2025-08-04 23:00:00,3375.81,3376.85,3373.42,3374.01,1427,12,0
2025-08-05 01:00:00,3376.19,3376.46,3373.6,3374.74,1085,12,0
2025-08-05 02:00:00,3374.74,3379.64,3374.72,3377.5,1953,10,0
2025-08-05 03:00:00,3377.41,3381.27,3375.93,3379.81,2417,5,0
2025-08-05 04:00:00,3379.89,3382.41,3378.01,3379.13,2942,13,0
2025-08-05 05:00:00,3379.06,3381.4,3373.75,3373.75,2792,5,0
2025-08-05 06:00:00,3373.68,3375.36,3372.12,3372.14,2231,5,0
2025-08-05 07:00:00,3372.05,3373.26,3371.08,3371.16,1924,5,0
2025-08-05 08:00:00,3371.15,3374.07,3367.53,3374.07,2982,5,0
2025-08-05 09:00:00,3374.05,3374.15,3365.72,3366.27,3016,5,0
2025-08-05 10:00:00,3366.28,3375.47,3366.27,3373.65,3303,5,0
2025-08-05 11:00:00,3373.6,3374.62,3366.07,3366.09,2911,7,0
2025-08-05 12:00:00,3366.13,3366.3,3359.03,3359.69,2943,5,0
2025-08-05 13:00:00,3359.73,3361.78,3352.94,3353.19,3217,5,0
2025-08-05 14:00:00,3353.15,3354.28,3349.99,3351.38,3080,6,0
2025-08-05 15:00:00,3351.39,3366.98,3349.99,3366.08,3882,5,0
2025-08-05 16:00:00,3366.38,3379.58,3365.95,3373.78,4360,5,0
2025-08-05 17:00:00,3376.47,3386.57,3370.56,3385.21,5168,5,0
2025-08-05 18:00:00,3385.28,3390.01,3382.2,3389.47,3721,5,0
2025-08-05 19:00:00,3389.52,3390.38,3377.0,3382.06,3477,5,0
2025-08-05 20:00:00,3382.07,3382.77,3379.02,3380.03,2717,14,0
2025-08-05 21:00:00,3380.02,3381.13,3378.47,3380.01,2617,6,0
2025-08-05 22:00:00,3380.0,3381.38,3378.15,3378.92,2646,15,0
2025-08-05 23:00:00,3378.99,3380.81,3378.44,3380.47,1234,6,0
2025-08-06 01:00:00,3381.14,3381.14,3378.19,3379.63,1111,15,0
2025-08-06 02:00:00,3379.62,3383.95,3378.9,3383.2,1770,12,0
2025-08-06 03:00:00,3383.21,3385.62,3380.85,3383.44,2629,5,0
2025-08-06 04:00:00,3383.28,3383.59,3372.69,3376.04,3651,5,0
2025-08-06 05:00:00,3375.88,3377.81,3374.25,3376.31,2806,5,0
2025-08-06 06:00:00,3376.33,3378.85,3373.4,3374.57,2395,5,0
2025-08-06 07:00:00,3374.63,3376.45,3372.11,3372.42,1893,5,0
2025-08-06 08:00:00,3372.25,3374.72,3370.66,3374.23,2650,5,0
2025-08-06 09:00:00,3374.23,3378.58,3373.02,3375.16,3108,5,0
2025-08-06 10:00:00,3375.17,3377.06,3369.06,3373.6,3099,8,0
Can't render this file because it is too large.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,4 +1,4 @@
# backtester.py (VERSI DIPERBAIKI)
# backtester.py
import pandas as pd
import pandas_ta as ta
import matplotlib.pyplot as plt
@@ -111,9 +111,7 @@ def run_backtest(data_path, symbol, initial_balance=10000):
# --- Jalankan Backtest ---
if __name__ == '__main__':
# --- KONFIGURASI PENGUJIAN ---
symbol_to_test = "XAUUSD" # Ubah ini ke "EURUSD" jika ingin menguji EURUSD
# Gunakan nama file yang sesuai dengan data yang Anda download
symbol_to_test = "GBPJPY"
file_name = "lab/XAUUSD_16385_data.csv"
run_backtest(file_name, symbol=symbol_to_test)
+1 -1
View File
@@ -16,7 +16,7 @@ else:
print("Berhasil terhubung ke MT5")
# --- Parameter Download ---
symbol = "SP500m" # Ganti dengan simbol yang Anda inginkan
symbol = "GBPCHF" # Ganti dengan simbol yang Anda inginkan
timeframe = mt5.TIMEFRAME_H1 # Timeframe 1 Jam
start_date = datetime(2020, 1, 1) # Mulai dari 1 Januari 2020
end_date = datetime.now() # Sampai sekarang
+43
View File
@@ -0,0 +1,43 @@
# run.py
import os
import atexit
import logging
import MetaTrader5 as mt5
from core import create_app
from core.utils.mt5 import initialize_mt5
from core.bots.controller import shutdown_all_bots, ambil_semua_bot
from dotenv import load_dotenv
load_dotenv()
def shutdown_app():
"""Fungsi shutdown terpusat."""
logging.info("Memulai proses shutdown aplikasi...")
shutdown_all_bots()
mt5.shutdown()
logging.info("Koneksi MetaTrader 5 ditutup. Aplikasi berhenti.")
# Panggil pabrik untuk membuat aplikasi kita
app = create_app()
if __name__ == '__main__':
# --- Inisialisasi MT5 Terpusat ---
# Dilakukan di sini untuk memastikan hanya berjalan sekali.
try:
ACCOUNT = int(os.getenv('MT5_LOGIN'))
PASSWORD = os.getenv('MT5_PASSWORD')
SERVER = os.getenv('MT5_SERVER', 'MetaQuotes-Demo')
if initialize_mt5(ACCOUNT, PASSWORD, SERVER):
logging.info("Koneksi MT5 berhasil diinisialisasi dari run.py.")
ambil_semua_bot() # Muat bot setelah koneksi berhasil
atexit.register(shutdown_app) # Daftarkan shutdown HANYA jika koneksi berhasil
except Exception as e:
logging.critical(f"GAGAL total saat inisialisasi MT5 di run.py: {e}", exc_info=True)
app.run(
debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true',
host=os.getenv('FLASK_HOST', '127.0.0.1'),
port=int(os.getenv('FLASK_PORT', 5000)),
use_reloader=False # Ini tetap sangat penting
)
+169
View File
@@ -0,0 +1,169 @@
// static/js/backtest_history.js
document.addEventListener('DOMContentLoaded', () => {
const historyListContainer = document.getElementById('history-list-container');
const detailContainer = document.getElementById('detail-container');
const detailView = document.getElementById('detail-view');
const detailPlaceholder = document.getElementById('detail-placeholder');
const detailId = document.getElementById('detail-id');
const detailTimestamp = document.getElementById('detail-timestamp');
const detailSummary = document.getElementById('detail-summary');
const detailParams = document.getElementById('detail-params');
const detailLog = document.getElementById('detail-log');
let detailEquityChart = null; // Variabel untuk menyimpan instance grafik detail
// Format timestamp dari ISO string ke format lokal
const formatTimestamp = (isoString) => {
return new Date(isoString).toLocaleString('id-ID', {
day: '2-digit', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
};
// Muat daftar riwayat backtest
async function loadHistoryList() {
try {
const response = await fetch('/api/backtest/history');
if (!response.ok) throw new Error('Gagal memuat riwayat backtest.');
const history = await response.json();
historyListContainer.innerHTML = '';
if (history.length === 0) {
historyListContainer.innerHTML = '<p class="text-gray-500 text-center py-4">Tidak ada riwayat backtest.</p>';
return;
}
// Urutkan berdasarkan timestamp terbaru
history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
history.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'p-3 mb-2 bg-gray-50 rounded cursor-pointer hover:bg-gray-100 border border-gray-200';
itemElement.innerHTML = `
<p class="font-medium text-gray-800">${item.strategy_name || 'Tidak Diketahui'}</p>
<p class="text-xs text-gray-500">${formatTimestamp(item.timestamp)}</p>
<p class="text-sm mt-1"><span class="font-semibold">Profit:</span> ${parseFloat(item.total_profit_pips).toFixed(2)} pips</p>
`;
itemElement.addEventListener('click', () => showDetail(item));
historyListContainer.appendChild(itemElement);
});
} catch (error) {
console.error('Error loading history list:', error);
historyListContainer.innerHTML = '<p class="text-red-500 text-center py-4">Gagal memuat riwayat: ' + error.message + '</p>';
}
}
// Tampilkan detail backtest
function showDetail(item) {
// Sembunyikan placeholder, tampilkan detail view
detailPlaceholder.classList.add('hidden');
detailView.classList.remove('hidden');
// Isi data dasar
detailId.textContent = item.id;
detailTimestamp.textContent = formatTimestamp(item.timestamp);
// Isi ringkasan
detailSummary.innerHTML = `
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Profit</p><p class="font-bold">${parseFloat(item.total_profit_pips).toFixed(2)} pips</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Max Drawdown</p><p class="font-bold">${parseFloat(item.max_drawdown_percent).toFixed(2)}%</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Win Rate</p><p class="font-bold">${parseFloat(item.win_rate_percent).toFixed(2)}%</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Total Trades</p><p class="font-bold">${item.total_trades}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Wins</p><p class="font-bold">${item.wins}</p></div>
<div class="p-3 bg-gray-50 rounded"><p class="text-xs text-gray-500">Losses</p><p class="font-bold">${item.losses}</p></div>
`;
// Isi parameter (jika ada)
try {
const params = JSON.parse(item.parameters || '{}');
let paramsHtml = '<h4 class="font-semibold mb-2">Parameter</h4><ul class="list-disc pl-5 text-sm">';
for (const [key, value] of Object.entries(params)) {
paramsHtml += `<li><span class="font-medium">${key}:</span> ${value}</li>`;
}
paramsHtml += '</ul>';
detailParams.innerHTML = paramsHtml;
} catch (e) {
detailParams.innerHTML = '<h4 class="font-semibold mb-2">Parameter</h4><p class="text-gray-500">Tidak ada parameter atau format tidak valid.</p>';
}
// Isi log (jika ada)
try {
const trades = JSON.parse(item.trade_log || '[]');
if (Array.isArray(trades) && trades.length > 0) {
let logHtml = '<h4 class="font-semibold mt-4 mb-2">Log Trade</h4><div class="text-xs font-mono border rounded p-3 bg-gray-50 max-h-40 overflow-y-auto">';
trades.forEach(trade => {
const profitClass = trade.profit_pips > 0 ? 'text-green-600' : 'text-red-600';
logHtml += `<p>Entry: ${trade.entry.toFixed(4)} | Exit: ${trade.exit.toFixed(4)} | Profit: <span class="${profitClass}">${trade.profit_pips.toFixed(2)} pips</span> | Reason: ${trade.reason}</p>`;
});
logHtml += '</div>';
detailLog.innerHTML = logHtml;
} else {
detailLog.innerHTML = '<h4 class="font-semibold mt-4 mb-2">Log Trade</h4><p class="text-gray-500">Tidak ada log trade untuk ditampilkan.</p>';
}
} catch (e) {
console.error("Gagal memproses log trade:", e);
detailLog.innerHTML = '<h4 class="font-semibold mt-4 mb-2">Log Trade</h4><p class="text-red-500">Gagal memuat log trade.</p>';
}
// Tampilkan grafik kurva ekuitas (jika ada data)
try {
const equityData = JSON.parse(item.equity_curve || '[]');
if (Array.isArray(equityData) && equityData.length > 0) {
displayDetailEquityChart(equityData);
} else {
// Jika tidak ada data, hancurkan chart yang mungkin ada sebelumnya
if (detailEquityChart) {
detailEquityChart.destroy();
detailEquityChart = null;
}
// Opsional: Tampilkan pesan bahwa tidak ada data chart
// const chartCtx = document.getElementById('detail-equity-chart').getContext('2d');
// chartCtx.clearRect(0, 0, chartCtx.canvas.width, chartCtx.canvas.height);
// Atau biarkan canvas kosong
}
} catch (e) {
console.error("Gagal memproses data kurva ekuitas:", e);
if (detailEquityChart) {
detailEquityChart.destroy();
detailEquityChart = null;
}
}
}
// Tampilkan grafik kurva ekuitas di detail view
function displayDetailEquityChart(equityData) {
const ctx = document.getElementById('detail-equity-chart').getContext('2d');
if (detailEquityChart) {
detailEquityChart.destroy(); // Hancurkan grafik lama
}
detailEquityChart = new Chart(ctx, {
type: 'line',
data: {
labels: Array.from({ length: equityData.length }, (_, i) => i + 1),
datasets: [{
label: 'Equity Curve',
data: equityData,
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.1,
pointRadius: 0,
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
title: { display: true, text: 'Pertumbuhan Modal (Equity Curve)' }
},
scales: { y: { beginAtZero: false } }
}
});
}
// Inisialisasi
loadHistoryList();
});
+15 -24
View File
@@ -98,46 +98,37 @@ document.addEventListener('DOMContentLoaded', function() {
}
async function fetchAndDisplayAnalysis() {
if (!botData) {
console.log("Menunggu detail bot...");
return;
}
try {
console.log(`Fetching analysis for bot ID: ${botId}`);
const res = await fetch(`/api/bots/${botId}/analysis`);
console.log('Response:', res);
if (!res.ok) throw new Error('Gagal memuat data analisis.');
const analysis = await res.json();
console.log('Analysis data:', analysis);
if (!res.ok) throw new Error('Gagal memuat data analisis MT5.');
// Tampilkan sinyal utama
const signal = analysis.signal || "TAHAN";
const signal = (analysis.signal || "TAHAN").toUpperCase();
analysisSignal.textContent = signal;
let color = 'bg-gray-200 text-gray-800';
if (signal.includes('BUY')) color = 'bg-green-100 text-green-800';
if (signal.includes('SELL')) color = 'bg-red-100 text-red-800';
else if (signal.includes('SELL')) color = 'bg-red-100 text-red-800';
analysisSignal.className = `mt-4 text-center font-bold text-lg p-2 rounded-md ${color}`;
// --- PERBAIKAN: Tampilkan semua detail analisis secara dinamis ---
analysisContainer.innerHTML = ""; // Kosongkan kontainer
const specialKeys = ['signal', 'explanation']; // Kunci yang tidak perlu ditampilkan di sini
analysisContainer.innerHTML = ""; // Kosongkan untuk refresh
const specialKeys = ['signal', 'price', 'explanation'];
Object.entries(analysis).forEach(([key, value]) => {
if (!specialKeys.includes(key) && value !== null && value !== undefined) {
let formattedValue = value;
// Format angka menjadi 2-5 desimal, boolean menjadi Ya/Tidak
if (typeof value === 'number') {
formattedValue = value.toFixed(key === 'RSI' ? 2 : 5);
} else if (typeof value === 'boolean') {
formattedValue = value ? 'Ya' : 'Tidak';
}
analysisContainer.innerHTML += `<div class="flex justify-between py-1"><span class="text-gray-500">${key.replace(/_/g, ' ')}</span><span class="font-semibold text-gray-800">${formattedValue}</span></div>`;
if (!specialKeys.includes(key) && value !== null) {
let formattedValue = typeof value === 'number' ? value.toFixed(4) : value;
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
analysisContainer.innerHTML += `<div class="flex justify-between py-1"><span class="text-gray-500">${label}</span><span class="font-semibold text-gray-800">${formattedValue}</span></div>`;
}
});
if (analysis.explanation) {
analysisContainer.innerHTML += `<div class="mt-2 text-xs text-gray-500 italic">${analysis.explanation}</div>`;
}
} catch (e) {
console.error('Error fetching analysis:', e);
analysisSignal.textContent = 'Error';
analysisSignal.textContent = 'ERROR';
analysisSignal.className = `mt-4 text-center font-bold text-lg p-2 rounded-md bg-red-200 text-red-900`;
analysisContainer.innerHTML = `<p class="text-center text-red-500">${e.message}</p>`;
}
}
+60 -59
View File
@@ -1,59 +1,59 @@
document.addEventListener('DOMContentLoaded', function() {
fetchForexData();
// Optional: Refresh data every 30 seconds
// setInterval(fetchForexData, 30000);
// Refresh data setiap 10 detik untuk konsistensi dengan halaman Saham
setInterval(fetchForexData, 10000);
});
function fetchForexData() {
async function fetchForexData() {
const tableBody = document.getElementById('forex-table-body');
fetch('/api/forex-data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!tableBody) {
console.error('Container element #forex-table-body not found.');
return;
}
// Tampilkan pesan loading jika tabel masih kosong
if (!tableBody.querySelector('tr')) {
tableBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-gray-500">Memuat data forex...</td></tr>';
}
const symbols = Object.keys(data);
try {
const response = await fetch('/api/forex-data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const forexPairs = await response.json(); // API sekarang mengembalikan array
if (symbols.length === 0) {
tableBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-gray-500">Tidak ada data forex dari MT5.</td></tr>';
return;
}
if (!forexPairs || forexPairs.length === 0) {
tableBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-gray-500">Tidak ada data forex dari MT5.</td></tr>';
return;
}
const rowsHtml = symbols.map(symbolKey => {
const pair = data[symbolKey];
const spreadInPips = pair.spread / (pair.digits > 3 ? 10 : 1);
return `
<tr>
<td class="px-6 py-4 whitespace-nowrap"><div class="font-medium text-gray-900">${pair.name}</div></td>
<td class="px-6 py-4 whitespace-nowrap font-medium">${pair.bid.toFixed(pair.digits)}</td>
<td class="px-6 py-4 whitespace-nowrap font-medium">${pair.ask.toFixed(pair.digits)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${spreadInPips.toFixed(1)} pips</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<button class="bg-blue-600 text-white py-1 px-3 rounded-md text-sm font-medium hover:bg-blue-700">Trade</button>
<button class="bg-gray-600 text-white py-1 px-3 rounded-md text-sm font-medium hover:bg-gray-700 details-btn" data-symbol="${symbolKey}">Details</button>
</td>
</tr>
`;
}).join('');
const rowsHtml = forexPairs.map(pair => {
// Spread dihitung berdasarkan poin, bukan pips. Untuk konversi ke pips:
// Jika digits 5 (misal EURUSD 1.23456), 1 pip = 10 poin.
// Jika digits 3 (misal USDJPY 123.456), 1 pip = 10 poin.
// Jika digits 2 atau 4, 1 pip = 1 poin.
// Karena data spread sudah dalam poin, dan 1 pip umumnya 10 poin untuk 5/3 digit,
// atau 1 poin untuk 2/4 digit, kita bisa langsung bagi dengan 10 untuk konversi umum.
// Ini adalah penyederhanaan karena kita tidak memiliki akses ke mt5.symbol_info di frontend.
return `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap"><div class="font-medium text-gray-900">${pair.name}</div></td>
<td class="px-6 py-4 whitespace-nowrap font-medium">${pair.bid.toFixed(pair.digits)}</td>
<td class="px-6 py-4 whitespace-nowrap font-medium">${pair.ask.toFixed(pair.digits)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${(pair.spread / 10).toFixed(1)} pips</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<button class="bg-blue-600 text-white py-1 px-3 rounded-md text-sm font-medium hover:bg-blue-700">Trade</button>
<button class="bg-gray-600 text-white py-1 px-3 rounded-md text-sm font-medium hover:bg-gray-700 details-btn" data-symbol="${pair.name}">Details</button>
</td>
</tr>
`;
}).join('');
tableBody.innerHTML = rowsHtml;
})
.catch(error => {
console.error('Error fetching forex data:', error);
if (tableBody) {
tableBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-red-500">Gagal memuat data.</td></tr>';
}
});
tableBody.innerHTML = rowsHtml;
} catch (error) {
console.error('Error fetching forex data:', error);
if (tableBody) {
tableBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-red-500">Gagal memuat data.</td></tr>';
}
}
}
document.addEventListener('click', function(e) {
@@ -64,11 +64,11 @@ document.addEventListener('click', function(e) {
});
document.getElementById('close-modal').addEventListener('click', function() {
document.getElementById('forex-modal').classList.add('hidden');
document.getElementById('stock-modal').classList.add('hidden'); // Menggunakan modal yang sama dengan stocks
});
async function fetchSymbolProfile(symbol) {
const modal = document.getElementById('forex-modal');
const modal = document.getElementById('stock-modal'); // Menggunakan modal yang sama dengan stocks
const modalTitle = document.getElementById('modal-title');
const modalContent = document.getElementById('modal-content');
@@ -79,23 +79,24 @@ async function fetchSymbolProfile(symbol) {
try {
const response = await fetch(`/api/forex/${symbol}/profile`);
if (!response.ok) {
throw new Error('Failed to fetch symbol profile.');
const errData = await response.json();
throw new Error(errData.error || 'Failed to fetch symbol profile.');
}
const profile = await response.json();
modalTitle.textContent = profile.name;
modalContent.innerHTML = `
<p><strong>Symbol:</strong> ${profile.symbol}</p>
<p><strong>Base Currency:</strong> ${profile.currency_base}</p>
<p><strong>Profit Currency:</strong> ${profile.currency_profit}</p>
<p><strong>Digits:</strong> ${profile.digits}</p>
<p><strong>Spread:</strong> ${profile.spread}</p>
<p><strong>Contract Size:</strong> ${profile.trade_contract_size}</p>
<p><strong>Min Volume:</strong> ${profile.volume_min}</p>
<p><strong>Max Volume:</strong> ${profile.volume_max}</p>
<p><strong>Volume Step:</strong> ${profile.volume_step}</p>
<p><strong>Initial Margin:</strong> ${profile.margin_initial}</p>
<p><strong>Maintenance Margin:</strong> ${profile.margin_maintenance}</p>
<div class="space-y-2 text-left">
<p><strong>Symbol:</strong> ${profile.symbol}</p>
<p><strong>Base Currency:</strong> ${profile.currency_base}</p>
<p><strong>Profit Currency:</strong> ${profile.currency_profit}</p>
<p><strong>Digits:</strong> ${profile.digits}</p>
<p><strong>Spread:</strong> ${profile.spread} points</p>
<p><strong>Contract Size:</strong> ${profile.trade_contract_size}</p>
<p><strong>Min Volume:</strong> ${profile.volume_min}</p>
<p><strong>Max Volume:</strong> ${profile.volume_max}</p>
<p><strong>Volume Step:</strong> ${profile.volume_step}</p>
</div>
`;
} catch (error) {
+3 -3
View File
@@ -3,8 +3,9 @@
document.addEventListener('DOMContentLoaded', function() {
const historyTableBody = document.getElementById('history-table-body');
const formatTimestamp = (isoString) => {
return new Date(isoString).toLocaleString('id-ID', {
const formatTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000);
return date.toLocaleString('id-ID', {
day: '2-digit', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
@@ -48,4 +49,3 @@ document.addEventListener('DOMContentLoaded', function() {
fetchGlobalHistory();
});
+94 -65
View File
@@ -1,60 +1,90 @@
// static/js/portfolio.js
document.addEventListener('DOMContentLoaded', function() {
// --- Variabel Global ---
// --- Elemen DOM & Variabel Global ---
const portfolioTableBody = document.getElementById('portfolio-table-body');
const portfolioSummary = document.getElementById('portfolio-summary');
let pnlChart = null; // Variabel untuk menyimpan objek grafik
let previousTotalProfit = 0; // Untuk melacak tren P/L
const pnlCanvas = document.getElementById('pnlChart');
const assetCanvas = document.getElementById('assetAllocationChart');
let pnlChart = null;
let assetAllocationChart = null;
let previousTotalProfit = 0;
const MAX_CHART_POINTS = 60; // Tampilkan 60 data point terakhir
// Data awal untuk grafik
const chartData = {
labels: [],
datasets: [{
label: 'Total P/L ($)',
data: [],
borderColor: 'rgba(59, 130, 246, 1)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 0 // Sembunyikan titik agar grafik lebih mulus
}]
// --- Fungsi Pembantu ---
const formatCurrency = (value) => {
const sign = value >= 0 ? '+' : '-';
return `${sign}${Math.abs(value).toFixed(2)}`;
};
const MAX_CHART_POINTS = 60; // Hanya tampilkan 60 data point terakhir (sekitar 5 menit)
// --- Fungsi Helper ---
function initChart() {
const ctx = document.getElementById('pnlChart').getContext('2d');
pnlChart = new Chart(ctx, {
// --- Inisialisasi Chart ---
function initPnlChart() {
if (!pnlCanvas) return;
pnlChart = new Chart(pnlCanvas, {
type: 'line',
data: chartData,
data: {
labels: [],
datasets: [{
label: 'Total P/L ($)',
data: [],
borderColor: 'rgba(59, 130, 246, 1)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true, tension: 0.4, pointRadius: 0
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { x: { type: 'time', time: { unit: 'minute', displayFormats: { minute: 'HH:mm:ss' } } } },
scales: { x: { type: 'time', time: { unit: 'second', displayFormats: { second: 'HH:mm:ss' } } } },
plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }
}
});
}
const formatCurrency = (value) => {
const sign = value >= 0 ? '+' : '-';
return `${sign}$${Math.abs(value).toFixed(2)}`;
};
async function fetchOpenPositions() {
async function updateAssetAllocationChart() {
if (!assetCanvas) return;
try {
// PERBAIKAN: Menggunakan URL API yang benar
const response = await fetch('/api/portfolio/open-positions'); // Ini sudah benar
if (!response.ok) throw new Error('Gagal memuat data portfolio');
const response = await fetch('/api/portfolio/allocation');
if (!response.ok) throw new Error('Gagal mengambil data alokasi');
const data = await response.json();
if (assetAllocationChart) {
assetAllocationChart.data.labels = data.labels;
assetAllocationChart.data.datasets[0].data = data.values;
assetAllocationChart.update();
} else {
assetAllocationChart = new Chart(assetCanvas, {
type: 'doughnut',
data: {
labels: data.labels,
datasets: [{
label: 'Alokasi Aset',
data: data.values,
backgroundColor: ['#36A2EB', '#FFCD56', '#4BC0C0', '#FF6384', '#9966FF', '#FF9F40'],
hoverOffset: 4
}]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' } } }
});
}
} catch (error) {
console.error("Gagal mengupdate chart alokasi:", error);
// Opsi: Tampilkan pesan error di canvas
}
}
// --- Fungsi Utama Pembaruan Data ---
async function updatePortfolioData() {
try {
const response = await fetch('/api/portfolio/open-positions');
if (!response.ok) throw new Error('Gagal memuat posisi terbuka');
const positions = await response.json();
portfolioTableBody.innerHTML = ''; // Kosongkan tabel
let totalProfit = 0;
portfolioTableBody.innerHTML = ''; // Kosongkan tabel sebelum diisi
if (positions.length === 0) {
// PERBAIKAN: colspan disesuaikan menjadi 6 kolom
portfolioTableBody.innerHTML = '<tr><td colspan="6" class="p-4 text-center text-gray-500">Tidak ada posisi yang sedang terbuka.</td></tr>';
// Tetap update summary dan chart ke 0
portfolioTableBody.innerHTML = '<tr><td colspan="6" class="p-4 text-center text-gray-500">Tidak ada posisi terbuka.</td></tr>';
} else {
positions.forEach(pos => {
totalProfit += pos.profit;
@@ -70,43 +100,32 @@ document.addEventListener('DOMContentLoaded', function() {
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${pos.price_open.toFixed(5)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold ${profitClass}">${formatCurrency(pos.profit)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${pos.magic}</td>
</tr>
`;
</tr>`;
portfolioTableBody.innerHTML += row;
});
}
// Update summary total P/L
// Update Summary
const totalProfitClass = totalProfit >= 0 ? 'text-green-600' : 'text-red-600';
// Tentukan ikon tren
let trendIcon = '<i class="fas fa-minus text-gray-400"></i>'; // Netral
if (totalProfit > previousTotalProfit) {
trendIcon = '<i class="fas fa-arrow-up text-green-500"></i>';
} else if (totalProfit < previousTotalProfit) {
trendIcon = '<i class="fas fa-arrow-down text-red-500"></i>';
}
let trendIcon = '<i class="fas fa-minus text-gray-400"></i>';
if (totalProfit > previousTotalProfit) trendIcon = '<i class="fas fa-arrow-up text-green-500"></i>';
else if (totalProfit < previousTotalProfit) trendIcon = '<i class="fas fa-arrow-down text-red-500"></i>';
portfolioSummary.innerHTML = `
<p class="text-sm text-gray-500">Total P/L Terbuka</p>
<p class="text-2xl font-bold ${totalProfitClass}">
${formatCurrency(totalProfit)}
<span class="ml-2 text-lg">${trendIcon}</span>
</p>
`;
<p class="text-2xl font-bold ${totalProfitClass}">${formatCurrency(totalProfit)} <span class="ml-2 text-lg">${trendIcon}</span></p>`;
previousTotalProfit = totalProfit;
// --- Update Grafik ---
// Update Grafik P/L
if (pnlChart) {
const now = new Date();
chartData.labels.push(now);
chartData.datasets[0].data.push(totalProfit);
// Batasi jumlah data point agar grafik tidak terlalu padat
if (chartData.labels.length > MAX_CHART_POINTS) {
chartData.labels.shift();
chartData.datasets[0].data.shift();
pnlChart.data.labels.push(now);
pnlChart.data.datasets[0].data.push(totalProfit);
if (pnlChart.data.labels.length > MAX_CHART_POINTS) {
pnlChart.data.labels.shift();
pnlChart.data.datasets[0].data.shift();
}
pnlChart.update('none'); // 'none' untuk animasi yang lebih halus
pnlChart.update('none');
}
} catch (error) {
@@ -115,8 +134,18 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// Panggil pertama kali, lalu refresh setiap 5 detik agar terasa real-time
initChart();
fetchOpenPositions();
setInterval(fetchOpenPositions, 5000);
});
// --- Inisialisasi dan Eksekusi ---
async function initializePage() {
initPnlChart(); // Inisialisasi chart P/L kosong
await updatePortfolioData(); // Panggil data portfolio pertama kali (termasuk update P/L)
await updateAssetAllocationChart(); // Panggil data alokasi pertama kali
// Set interval untuk pembaruan data
setInterval(async () => {
await updatePortfolioData();
await updateAssetAllocationChart(); // Alokasi juga di-refresh
}, 5000);
}
initializePage();
});
+43
View File
@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Riwayat Backtest{% endblock %}
{% block content %}
<h2 class="text-2xl font-bold text-gray-800 mb-4">Riwayat Hasil Backtest</h2>
<p class="text-gray-600 mb-6">Daftar semua simulasi backtest yang telah dijalankan dan disimpan.</p>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Kolom Daftar Riwayat -->
<div class="lg:col-span-1 bg-white p-4 rounded-lg shadow">
<h3 class="font-bold text-lg mb-2">Daftar Pengujian</h3>
<div id="history-list-container" class="max-h-[600px] overflow-y-auto">
<!-- Daftar akan dimuat di sini oleh JavaScript -->
<p>Memuat riwayat...</p>
</div>
</div>
<!-- Kolom Detail Hasil -->
<div id="detail-container" class="lg:col-span-2 bg-white p-6 rounded-lg shadow">
<div id="detail-view" class="hidden">
<h3 class="text-xl font-bold mb-2">Detail Hasil Backtest</h3>
<p class="text-sm text-gray-500 mb-4">ID: <span id="detail-id"></span> | Tanggal: <span id="detail-timestamp"></span></p>
<div id="detail-summary" class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6"></div>
<div class="mb-6">
<canvas id="detail-equity-chart"></canvas>
</div>
<div id="detail-params"></div>
<div id="detail-log"></div>
</div>
<div id="detail-placeholder" class="text-center text-gray-500 py-10">
<i class="fas fa-search-plus text-4xl mb-4"></i>
<p>Pilih salah satu hasil dari daftar di samping untuk melihat detailnya.</p>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/backtest_history.js') }}"></script>
{% endblock %}
+6 -1
View File
@@ -4,7 +4,12 @@
{% block title %}Backtester{% endblock %}
{% block content %}
<h2 class="text-2xl font-bold text-gray-800 mb-4">Strategy Backtester</h2>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-gray-800">Strategy Backtester</h2>
<a href="{{ url_for('backtest_history_page') }}" class="bg-gray-200 text-gray-800 py-2 px-4 rounded-lg font-medium hover:bg-gray-300">
<i class="fas fa-history mr-2"></i>Lihat Riwayat
</a>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Kolom Konfigurasi -->
+33 -34
View File
@@ -1,16 +1,7 @@
<!-- templates/portfolio.html (Setelah Refactor) -->
<!-- templates/portfolio.html -->
{% extends "base.html" %}
{% block title %}Portfolio Real-Time{% endblock %}
{% block head_extra %}
<!--
Chart.js sudah ada di base.html. Kita hanya perlu menambahkan adapter tanggal.
Namun, lebih baik memuat semua JS di akhir, jadi kita akan letakkan ini di blok 'scripts'.
Blok ini dibiarkan sebagai contoh jika butuh CSS khusus.
-->
{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-6">
<div>
@@ -22,35 +13,43 @@
</div>
</div>
<div class="mb-6 bg-white rounded-lg shadow p-4">
<h3 class="text-lg font-semibold text-gray-800 mb-2">Grafik Profit/Loss Terbuka (Real-Time)</h3>
<div style="height: 250px;"><canvas id="pnlChart"></canvas></div>
<!-- Grid untuk menampung dua chart -->
<div class="grid grid-cols-1 lg:grid-cols-5 gap-6 mb-6">
<!-- Chart P/L (lebih lebar) -->
<div class="lg:col-span-3 bg-white rounded-lg shadow p-4">
<h3 class="text-lg font-semibold text-gray-800 mb-2">Grafik Profit/Loss Terbuka (Real-Time)</h3>
<div style="height: 250px;"><canvas id="pnlChart"></canvas></div>
</div>
<!-- Chart Alokasi Aset (lebih kecil) -->
<div class="lg:col-span-2 bg-white rounded-lg shadow p-4">
<h3 class="text-lg font-semibold text-gray-800 mb-2">Alokasi Aset</h3>
<div style="height: 250px;"><canvas id="assetAllocationChart"></canvas></div>
</div>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Simbol</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipe</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Volume</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Harga Buka</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Profit/Loss</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Magic Number</th>
</tr>
</thead>
<tbody id="portfolio-table-body" class="bg-white divide-y divide-gray-200">
<tr><td colspan="6" class="p-4 text-center text-gray-500">Memuat posisi terbuka...</td></tr>
</tbody>
</table>
</div>
<!-- Tabel Posisi Terbuka -->
<table class="min-w-full bg-white rounded-lg shadow overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Simbol</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipe</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Volume</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Harga Buka</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Profit/Loss</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Magic Number</th>
</tr>
</thead>
<tbody id="portfolio-table-body" class="bg-white divide-y divide-gray-200">
<tr><td colspan="6" class="p-4 text-center text-gray-500">Memuat posisi terbuka...</td></tr>
</tbody>
</table>
{% endblock %}
{% block scripts %}
<!--
Memuat adapter tanggal untuk Chart.js yang diperlukan di halaman ini,
diikuti dengan skrip logika halaman portfolio.
-->
<!-- Library Chart.js utama, adapter, dan skrip halaman dimuat bersamaan -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/portfolio.js') }}"></script>
{% endblock %}