mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-27 18:57:47 +00:00
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:
@@ -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
|
||||
)
|
||||
@@ -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
@@ -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:]
|
||||
}
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
@@ -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 []
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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({})
|
||||
|
||||
@@ -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({})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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': []})
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
@@ -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
@@ -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,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
@@ -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();
|
||||
});
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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 %}
|
||||
Reference in New Issue
Block a user