mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-28 03:07:53 +00:00
79d860e9a8
✨ Major Features Added: • Complete Indonesian & English bilingual support (80+ UI elements) • Global language switcher with persistent preferences • Smart MT5 data integration with web controls • One-click market data download for 50+ instruments • Automated MT5 installer and setup guide • Enhanced settings with profile and preferences management • Professional documentation and troubleshooting guides 🚀 Technical Improvements: • Internationalization (i18n) system with extensible architecture • MT5 environment detection with graceful fallbacks • API architecture for future cross-platform expansion • Improved error handling and user guidance • Cross-browser compatibility and responsive design 📚 Documentation: • 50+ page comprehensive MT5 setup guide • Automated installer with dependency management • Public roadmap announcing QuantumBotX API development • Updated CHANGELOG and platform compatibility notices 🎯 Platform Strategy: • Clear Windows-first positioning for MT5 learning platform • Preparatory work for QuantumBotX API cloud platform • Education-first approach maintaining Indonesian heritage • Foundation for international expansion Co-authored-by: AI Assistant <assistant@quantumbotx.ai>
262 lines
11 KiB
Python
262 lines
11 KiB
Python
# core/routes/api_backtest.py
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import json
|
|
import logging
|
|
import subprocess
|
|
import os
|
|
from flask import Blueprint, request, jsonify
|
|
from core.backtesting.enhanced_engine import run_enhanced_backtest as 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__)
|
|
|
|
def save_backtest_result(strategy_name, filename, params, results):
|
|
# 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)
|
|
|
|
# Enhanced engine provides more detailed results
|
|
profit_to_save = results.get('total_profit_usd', 0)
|
|
spread_costs = results.get('total_spread_costs', 0)
|
|
instrument = results.get('instrument', 'UNKNOWN')
|
|
|
|
# Add enhanced engine info to parameters for tracking
|
|
enhanced_params = params.copy()
|
|
enhanced_params['engine_type'] = 'enhanced'
|
|
enhanced_params['spread_costs'] = spread_costs
|
|
enhanced_params['instrument'] = instrument
|
|
|
|
if 'engine_config' in results:
|
|
engine_config = results['engine_config']
|
|
enhanced_params['realistic_execution'] = engine_config.get('realistic_execution', True)
|
|
enhanced_params['spread_costs_enabled'] = engine_config.get('spread_costs_enabled', True)
|
|
|
|
# Include instrument-specific config for analysis
|
|
if 'instrument_config' in engine_config:
|
|
inst_config = engine_config['instrument_config']
|
|
enhanced_params['max_risk_percent'] = inst_config.get('max_risk_percent', 2.0)
|
|
enhanced_params['typical_spread_pips'] = inst_config.get('typical_spread_pips', 2.0)
|
|
enhanced_params['max_lot_size'] = inst_config.get('max_lot_size', 10.0)
|
|
|
|
try:
|
|
with get_db_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
INSERT INTO backtest_results (
|
|
strategy_name, data_filename, total_profit_usd, total_trades,
|
|
win_rate_percent, max_drawdown_percent, wins, losses, equity_curve, trade_log, parameters
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
strategy_name,
|
|
filename,
|
|
profit_to_save,
|
|
results.get('total_trades', 0),
|
|
results.get('win_rate_percent', 0),
|
|
results.get('max_drawdown_percent', 0),
|
|
results.get('wins', 0),
|
|
results.get('losses', 0),
|
|
json.dumps(results.get('equity_curve', [])),
|
|
json.dumps(results.get('trades', [])),
|
|
json.dumps(enhanced_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():
|
|
if 'file' not in request.files:
|
|
return jsonify({"error": "Tidak ada file data yang diunggah"}), 400
|
|
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({"error": "Nama file kosong"}), 400
|
|
|
|
try:
|
|
df = pd.read_csv(file.stream, parse_dates=['time'])
|
|
strategy_id = request.form.get('strategy')
|
|
params = json.loads(request.form.get('params', '{}'))
|
|
|
|
# Map web interface parameter names to enhanced engine parameter names
|
|
enhanced_params = params.copy()
|
|
|
|
# Map old parameter names to new enhanced engine names
|
|
if 'lot_size' in params and 'risk_percent' not in params:
|
|
enhanced_params['risk_percent'] = float(params['lot_size'])
|
|
|
|
if 'sl_pips' in params and 'sl_atr_multiplier' not in params:
|
|
enhanced_params['sl_atr_multiplier'] = float(params['sl_pips'])
|
|
|
|
if 'tp_pips' in params and 'tp_atr_multiplier' not in params:
|
|
enhanced_params['tp_atr_multiplier'] = float(params['tp_pips'])
|
|
|
|
logger.info(f"Parameter mapping: {params} -> {enhanced_params}")
|
|
|
|
# Extract symbol name from filename for accurate XAUUSD detection
|
|
symbol_name = None
|
|
if file.filename:
|
|
# Try to extract symbol from filename (e.g., "XAUUSD_H1_data.csv" -> "XAUUSD")
|
|
filename_parts = file.filename.replace('.csv', '').split('_')
|
|
if filename_parts:
|
|
symbol_name = filename_parts[0].upper()
|
|
logger.info(f"Detected symbol from filename: {symbol_name}")
|
|
|
|
# Enhanced backtesting with realistic cost modeling and risk management
|
|
# Use enhanced engine for more accurate results
|
|
engine_config = {
|
|
'enable_spread_costs': True, # Model realistic spread costs
|
|
'enable_slippage': True, # Include slippage simulation
|
|
'enable_realistic_execution': True # Realistic bid/ask execution
|
|
}
|
|
|
|
results = run_backtest(strategy_id, enhanced_params, df, symbol_name=symbol_name, engine_config=engine_config)
|
|
|
|
# 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
|
|
|
|
@api_backtest.route('/api/backtest/history', methods=['GET'])
|
|
def get_history_route():
|
|
try:
|
|
history = get_all_backtest_history()
|
|
processed_history = []
|
|
for record in history:
|
|
# Create a mutable copy (dictionary) from the database record
|
|
new_record = dict(record)
|
|
|
|
# Parse JSON fields safely
|
|
if 'trade_log' in new_record and new_record['trade_log']:
|
|
try:
|
|
trades = json.loads(new_record['trade_log'])
|
|
if isinstance(trades, list):
|
|
new_record['trade_log'] = trades
|
|
except (json.JSONDecodeError, TypeError):
|
|
new_record['trade_log'] = []
|
|
else:
|
|
new_record['trade_log'] = []
|
|
|
|
if 'equity_curve' in new_record and new_record['equity_curve']:
|
|
try:
|
|
equity = json.loads(new_record['equity_curve'])
|
|
if isinstance(equity, list):
|
|
new_record['equity_curve'] = equity
|
|
except (json.JSONDecodeError, TypeError):
|
|
new_record['equity_curve'] = []
|
|
else:
|
|
new_record['equity_curve'] = []
|
|
|
|
if 'parameters' in new_record and new_record['parameters']:
|
|
try:
|
|
params = json.loads(new_record['parameters'])
|
|
if isinstance(params, dict):
|
|
new_record['parameters'] = params
|
|
except (json.JSONDecodeError, TypeError):
|
|
new_record['parameters'] = {}
|
|
else:
|
|
new_record['parameters'] = {}
|
|
|
|
processed_history.append(new_record)
|
|
|
|
return jsonify(processed_history)
|
|
except Exception as e:
|
|
logger.error(f"Error processing history: {str(e)}", exc_info=True)
|
|
return jsonify({"error": f"Terjadi kesalahan saat mengambil riwayat: {str(e)}"}), 500
|
|
|
|
@api_backtest.route('/api/download-data', methods=['POST'])
|
|
def download_data_route():
|
|
"""Download historical market data using the standalone script"""
|
|
try:
|
|
# Path to the download script
|
|
script_path = os.path.join(os.path.dirname(__file__), '..', '..', 'lab', 'download_data.py')
|
|
script_path = os.path.abspath(script_path)
|
|
|
|
if not os.path.exists(script_path):
|
|
return jsonify({"error": "Download script not found"}), 404
|
|
|
|
# Check if the script can import MT5 (basic validation)
|
|
try:
|
|
result_check = subprocess.run(
|
|
['python', '-c', 'import MetaTrader5 as mt5; print("MT5 available")'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
if result_check.returncode != 0:
|
|
return jsonify({
|
|
"error": "MetaTrader5 module not available. Make sure you run the download script manually in an environment where MT5 is installed.",
|
|
"solution": "Run 'python lab/download_data.py' from command line instead."
|
|
}), 500
|
|
except subprocess.TimeoutExpired:
|
|
return jsonify({
|
|
"error": "MT5 availability check timed out",
|
|
"solution": "Ensure MT5 terminal is running and accessible"
|
|
}), 408
|
|
|
|
# Run the download script
|
|
logger.info(f"Starting data download with script: {script_path}")
|
|
|
|
# Use subprocess to run the script and capture output
|
|
result = subprocess.run(
|
|
['python', script_path],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300 # 5 minute timeout
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
# Success - parse output to extract info
|
|
output_lines = result.stdout.strip().split('\n')
|
|
downloaded_files = []
|
|
failed_count = 0
|
|
|
|
for line in output_lines:
|
|
if line.startswith('✅') and 'bars saved to' in line:
|
|
# Extract filename from success message
|
|
parts = line.split('bars saved to')
|
|
if len(parts) > 1:
|
|
filepath = parts[1].strip()
|
|
filename = os.path.basename(filepath)
|
|
downloaded_files.append(filename)
|
|
elif line.startswith('❌ Failed downloads:'):
|
|
# Extract failed count
|
|
parts = line.split(':')
|
|
if len(parts) > 1:
|
|
try:
|
|
failed_count = len(parts[1].strip().split(', ')) if parts[1].strip() else 0
|
|
except:
|
|
failed_count = 0
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": f"Downloaded {len(downloaded_files)} files successfully" + (f", {failed_count} failed" if failed_count > 0 else ""),
|
|
"downloaded_files": downloaded_files,
|
|
"failed_count": failed_count,
|
|
"output": result.stdout
|
|
})
|
|
else:
|
|
# Process failed
|
|
error_msg = result.stderr or "Unknown error occurred"
|
|
logger.error(f"Data download failed: {error_msg}")
|
|
return jsonify({
|
|
"error": f"Download failed: {error_msg}",
|
|
"output": result.stderr,
|
|
"stdout": result.stdout
|
|
}), 500
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logger.error("Data download timed out")
|
|
return jsonify({"error": "Download timed out after 5 minutes"}), 408
|
|
except Exception as e:
|
|
logger.error(f"Error in download data route: {str(e)}", exc_info=True)
|
|
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|