diff --git a/README.md b/README.md index b51d70f..4f129be 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Designed to be elegant, powerful, and flexible โ whether you're a scalper, swi ### ๐ฏ **Core Trading Engine** - โ **Modular Strategy System**: 12+ professional trading strategies with plug-and-play architecture - โ **ATR-Based Risk Management**: Dynamic position sizing that adapts to market volatility -- โ **Multi-Broker Support**: Automatic symbol migration (XM Global, MetaTrader Demo, Exness, Alpari) +- โ **Multi-Broker Support**: Automatic symbol migration (XM Global, MetaTrader, Exness, Alpari) - โ **Real-Time Trading**: 4 concurrent bots with live MT5 integration - โ **Emergency Protection**: XAUUSD safeguards prevent account blowouts @@ -40,6 +40,13 @@ Designed to be elegant, powerful, and flexible โ whether you're a scalper, swi - โ **Indonesian Market Ready**: XM Indonesia integration with IDR pairs support - โ **Cross-Platform Foundation**: cTrader, Interactive Brokers architecture +### ๐ **Culturally-Aware Features** +- โ **Automatic Holiday Detection**: Christmas and Ramadan modes activate automatically +- โ **Ramadan Trading Mode**: Respects prayer times with automatic trading pauses +- โ **Cultural Sensitivity**: UI themes and greetings for both Christian and Muslim traders +- โ **Islamic Finance Features**: Zakat calculator and charity tracker during Ramadan +- โ **Seasonal Adjustments**: Risk management adapts to holiday market conditions + ### ๐ก๏ธ **Professional Safety** - โ **Automated Risk Control**: 1% max risk per trade with emergency brake system - โ **Volatility Protection**: ATR-based position scaling during market turbulence @@ -132,6 +139,7 @@ Designed to be elegant, powerful, and flexible โ whether you're a scalper, swi - โ **Windows Optimization**: Clean logging and professional error handling - โ **Comprehensive Backtesting**: Interactive Chart.js visualizations - โ **Indonesian Market Support**: XM Indonesia integration with IDR pairs +- โ **Culturally-Aware Trading**: Automatic holiday detection for Christmas and Ramadan ### ๐ง **v2.1 - Intelligence Enhancement (IN DEVELOPMENT)** - [ ] **Advanced Strategy**: `MACD_STOCH_FILTER` for more precise, filtered entries @@ -224,7 +232,7 @@ This software is provided "as is" for educational and research purposes. The aut ## ๐ง Author Developed with ๐ by **Chrisnov IT Solutions** -Concept, Logic & Execution: `@reynov` aka BabyDev +Concept, Logic & Execution: `@chrisnov` aka BabyDev --- diff --git a/core/__init__.py b/core/__init__.py index acf9023..a074f61 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -113,6 +113,9 @@ def create_app(): from .routes.api_fundamentals import api_fundamentals from .routes.api_backtest import api_backtest from .routes.ai_mentor import ai_mentor_bp + from .routes.api_strategy_switcher import api_strategy_switcher + from .routes.api_ramadan import api_ramadan + from .routes.api_holiday import api_holiday app.register_blueprint(api_dashboard) app.register_blueprint(api_chart) @@ -126,6 +129,9 @@ def create_app(): app.register_blueprint(api_fundamentals) app.register_blueprint(api_backtest) app.register_blueprint(ai_mentor_bp) + app.register_blueprint(api_strategy_switcher) + app.register_blueprint(api_ramadan) + app.register_blueprint(api_holiday) @app.route('/') def dashboard(): @@ -175,6 +181,14 @@ def create_app(): def forex_page(): return render_template('forex.html', active_page='forex') + @app.route('/strategy-switcher') + def strategy_switcher_page(): + return render_template('strategy_switcher/dashboard.html', active_page='strategy_switcher') + + @app.route('/ramadan') + def ramadan_page(): + return render_template('ramadan.html', active_page='ramadan') + @app.errorhandler(404) def not_found_error(error): return render_template('404.html'), 404 diff --git a/core/backtesting/enhanced_engine.py b/core/backtesting/enhanced_engine.py index f4d313d..ac0c2b4 100644 --- a/core/backtesting/enhanced_engine.py +++ b/core/backtesting/enhanced_engine.py @@ -4,7 +4,6 @@ import math import logging import os -import pandas as pd from core.strategies.strategy_map import STRATEGY_MAP logger = logging.getLogger(__name__) @@ -58,12 +57,27 @@ class InstrumentConfig: 'slippage_pips': 0.5 # Reduced from 1.0 } + INDICES = { + 'contract_size': 1, # 1 point = $1 for index CFDs + 'pip_size': 0.01, # 0.01 point = 1 pip + 'typical_spread_pips': 3.0, # Index spreads are typically higher + 'max_risk_percent': 0.5, # Very conservative for indices + 'max_lot_size': 0.1, # Small lot sizes for indices + 'slippage_pips': 0.5, + 'atr_volatility_threshold_high': 50.0, # Index-specific thresholds + 'atr_volatility_threshold_extreme': 100.0, + 'emergency_brake_percent': 0.1 # 10% emergency brake + } + @classmethod def get_config(cls, symbol_name): """Get configuration for a specific instrument""" symbol_upper = symbol_name.upper() - if 'XAU' in symbol_upper or 'GOLD' in symbol_upper: + # Index detection (US30, US100, US500, DE30, etc.) + if any(index in symbol_upper for index in ['US30', 'US100', 'US500', 'DE30', 'UK100', 'JP225', 'NAS100', 'SPX500']): + return cls.INDICES + elif 'XAU' in symbol_upper or 'GOLD' in symbol_upper: return cls.GOLD elif any(jpy in symbol_upper for jpy in ['JPY', 'USDJPY', 'EURJPY', 'GBPJPY']): return cls.FOREX_JPY @@ -112,9 +126,11 @@ class EnhancedBacktestEngine: amount_to_risk = capital * (risk_percent / 100.0) - # Special handling for GOLD (XAUUSD) + # Special handling for high-risk instruments if config == InstrumentConfig.GOLD: return self._calculate_gold_position_size(risk_percent, atr_value, amount_to_risk, sl_distance, config) + elif config == InstrumentConfig.INDICES: + return self._calculate_index_position_size(risk_percent, atr_value, amount_to_risk, sl_distance, config) else: return self._calculate_standard_position_size(amount_to_risk, sl_distance, config) @@ -151,6 +167,41 @@ class EnhancedBacktestEngine: return round(lot_size, 2) + def _calculate_index_position_size(self, risk_percent, atr_value, amount_to_risk, sl_distance, config): + """Ultra-conservative position sizing for stock indices (US500, US30, etc.)""" + + # Base lot size for indices (extremely conservative) + if risk_percent <= 0.25: + base_lot_size = 0.01 + elif risk_percent <= 0.5: + base_lot_size = 0.01 + elif risk_percent <= 0.75: + base_lot_size = 0.02 + elif risk_percent <= 1.0: + base_lot_size = 0.02 + else: + base_lot_size = 0.03 # Maximum for any index trade + + # ATR-based volatility adjustments for indices + atr_threshold_high = config.get('atr_volatility_threshold_high', 50.0) + atr_threshold_extreme = config.get('atr_volatility_threshold_extreme', 100.0) + + if atr_value > atr_threshold_extreme: + lot_size = 0.01 # Extreme volatility - minimum size + logger.warning(f"INDEX EXTREME VOLATILITY: ATR={atr_value:.1f}, lot=0.01") + elif atr_value > atr_threshold_high: + lot_size = max(0.01, base_lot_size * 0.5) # High volatility - reduce size + logger.warning(f"INDEX HIGH VOLATILITY: ATR={atr_value:.1f}, lot={lot_size}") + else: + lot_size = base_lot_size # Normal volatility + + # Final safety cap + lot_size = min(lot_size, config['max_lot_size']) + + logger.debug(f"INDEX POSITION: Risk={risk_percent}%, ATR={atr_value:.1f}, Lot={lot_size}") + + return round(lot_size, 2) + def _calculate_standard_position_size(self, amount_to_risk, sl_distance, config): """Standard position sizing for forex and other instruments""" @@ -174,18 +225,18 @@ class EnhancedBacktestEngine: if not self.enable_spread_costs: return 0 - # FIXED: More realistic spread cost calculation - # For forex majors: ~$10 per pip per standard lot (1.0) - # For gold: ~$10 per pip per standard lot (1.0) - - # Calculate pip value per lot based on contract size - if config['contract_size'] == 100: # Gold - # For gold: $1 per 0.01 pip per 1 oz, so $0.01 per pip per lot - pip_value_per_lot = 1.0 # More reasonable for gold + # Calculate pip value per lot based on instrument type + if config == InstrumentConfig.GOLD: + # For gold: $1 per 0.01 pip per 1 oz + pip_value_per_lot = 1.0 + elif config == InstrumentConfig.INDICES: + # For indices: $1 per point per lot (very conservative for backtesting) + pip_value_per_lot = 0.1 # Much more conservative for indices + elif config['contract_size'] == 100: # Other instruments with 100 contract size + pip_value_per_lot = 1.0 else: # Forex - # For major pairs: $10 per pip per standard lot is too high for backtesting - # Use more realistic $1 per pip per standard lot for backtesting - pip_value_per_lot = 1.0 # Much more reasonable + # For major pairs: Use conservative pip value for backtesting + pip_value_per_lot = 1.0 spread_cost = spread_pips * pip_value_per_lot * lot_size diff --git a/core/bots/controller.py b/core/bots/controller.py index 21455d3..de717ae 100644 --- a/core/bots/controller.py +++ b/core/bots/controller.py @@ -130,7 +130,8 @@ def mulai_bot(bot_id: int): risk_percent=bot_data['lot_size'], sl_pips=bot_data['sl_pips'], tp_pips=bot_data['tp_pips'], timeframe=bot_data['timeframe'], check_interval=bot_data['check_interval_seconds'], strategy=bot_data['strategy'], - strategy_params=params_dict + strategy_params=params_dict, + enable_strategy_switching=bool(bot_data.get('enable_strategy_switching', 0)) ) bot_thread.start() active_bots[bot_id] = bot_thread diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index 7bcc708..fb466d5 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class TradingBot(threading.Thread): - def __init__(self, id, name, market, risk_percent, sl_pips, tp_pips, timeframe, check_interval, strategy, strategy_params={}, status='Dijeda'): + def __init__(self, id, name, market, risk_percent, sl_pips, tp_pips, timeframe, check_interval, strategy, strategy_params={}, status='Dijeda', enable_strategy_switching=False): super().__init__() self.id = id self.name = name @@ -26,6 +26,7 @@ class TradingBot(threading.Thread): self.check_interval = check_interval self.strategy_name = strategy self.strategy_params = strategy_params + self.enable_strategy_switching = enable_strategy_switching self.market_for_mt5 = None # Akan diisi setelah verifikasi simbol self.status = status diff --git a/core/db/queries.py b/core/db/queries.py index 9831046..f20e2f6 100644 --- a/core/db/queries.py +++ b/core/db/queries.py @@ -26,31 +26,31 @@ def get_bot_by_id(bot_id): logger.error(f"Database error saat mengambil bot {bot_id}: {e}") return None -def add_bot(name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params='{}'): +def add_bot(name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params='{}', enable_strategy_switching=0): """Menambahkan bot baru ke database.""" try: with get_db_connection() as conn: cursor = conn.cursor() cursor.execute(''' - INSERT INTO bots (name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval_seconds, strategy, strategy_params, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'Dijeda') - ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params)) + INSERT INTO bots (name, market, lot_size, sl_pips, tp_pips, timeframe, check_interval_seconds, strategy, strategy_params, enable_strategy_switching, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Dijeda') + ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params, enable_strategy_switching)) conn.commit() return cursor.lastrowid except sqlite3.Error as e: logger.error(f"Gagal menambah bot ke DB: {e}", exc_info=True) return None -def update_bot(bot_id, name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params='{}'): +def update_bot(bot_id, name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params='{}', enable_strategy_switching=0): """Memperbarui data bot yang sudah ada di database.""" try: with get_db_connection() as conn: conn.execute(''' UPDATE bots SET name = ?, market = ?, lot_size = ?, sl_pips = ?, tp_pips = ?, - timeframe = ?, check_interval_seconds = ?, strategy = ?, strategy_params = ? + timeframe = ?, check_interval_seconds = ?, strategy = ?, strategy_params = ?, enable_strategy_switching = ? WHERE id = ? - ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params, bot_id)) + ''', (name, market, lot_size, sl_pips, tp_pips, timeframe, interval, strategy, strategy_params, enable_strategy_switching, bot_id)) conn.commit() return True except sqlite3.Error as e: diff --git a/core/routes/ai_mentor.py b/core/routes/ai_mentor.py index f6fae00..df88820 100644 --- a/core/routes/ai_mentor.py +++ b/core/routes/ai_mentor.py @@ -149,6 +149,31 @@ def history(): days = request.args.get('days', 30, type=int) reports = get_recent_mentor_reports(days) + # Ensure all reports have the required fields for the template + for report in reports: + if 'total_profit_loss' not in report: + report['total_profit_loss'] = report.get('profit_loss', 0.0) + + # Date normalization to prevent strftime errors + raw_date = report.get('date') or report.get('session_date') + if isinstance(raw_date, str): + try: + # Handles both 'YYYY-MM-DD' and 'YYYY-MM-DD HH:MM:SS' + report['date'] = datetime.strptime(raw_date.split(' ')[0], '%Y-%m-%d').date() + except (ValueError, TypeError): + report['date'] = date.today() # Fallback for safety + elif isinstance(raw_date, datetime): + report['date'] = raw_date.date() + elif isinstance(raw_date, date): + report['date'] = raw_date + else: + report['date'] = date.today() + + if 'total_trades' not in report: + report['total_trades'] = report.get('total_trades', 0) + if 'emotions' not in report: + report['emotions'] = report.get('emotions', 'netral') + return render_template('ai_mentor/history.html', reports=reports, days=days) @@ -326,10 +351,17 @@ def get_ai_mentor_summary(): today_session = get_trading_session_data(date.today()) recent_reports = get_recent_mentor_reports(3) + # Ensure recent reports have consistent field names + for report in recent_reports: + if 'total_profit_loss' not in report and 'profit_loss' in report: + report['total_profit_loss'] = report['profit_loss'] + if 'date' not in report and 'session_date' in report: + report['date'] = report['session_date'] + return { 'today_has_data': today_session is not None, - 'today_profit_loss': today_session['total_profit_loss'] if today_session else 0, - 'today_emotions': today_session['emotions'] if today_session else 'netral', + 'today_profit_loss': today_session.get('total_profit_loss', 0) if today_session else 0, + 'today_emotions': today_session.get('emotions', 'netral') if today_session else 'netral', 'recent_performance': recent_reports[:3] if recent_reports else [] } except Exception as e: diff --git a/core/routes/api_bots.py b/core/routes/api_bots.py index 2b08b9f..1e2de73 100644 --- a/core/routes/api_bots.py +++ b/core/routes/api_bots.py @@ -38,7 +38,21 @@ def get_strategy_params_route(strategy_id): # Panggil metode class untuk mendapatkan parameter if hasattr(strategy_class, 'get_definable_params'): params = strategy_class.get_definable_params() - return jsonify(params) + + # Normalize parameter format for frontend compatibility + # Frontend expects 'label' but some strategies use 'display_name' + normalized_params = [] + for param in params: + normalized_param = param.copy() + # If display_name exists but label doesn't, use display_name as label + if 'display_name' in param and 'label' not in param: + normalized_param['label'] = param['display_name'] + # If neither exists, create a label from the name + elif 'label' not in param and 'display_name' not in param: + normalized_param['label'] = param['name'].replace('_', ' ').title() + normalized_params.append(normalized_param) + + return jsonify(normalized_params) return jsonify([]) # Kembalikan array kosong jika tidak ada parameter @@ -63,6 +77,9 @@ def get_single_bot_route(bot_id): if bot and bot.get('strategy_params'): # Ubah string JSON menjadi objek untuk frontend bot['strategy_params'] = json.loads(bot['strategy_params']) + # Convert enable_strategy_switching to integer for frontend + if bot and 'enable_strategy_switching' in bot: + bot['enable_strategy_switching'] = int(bot['enable_strategy_switching']) return jsonify(bot) if bot else (jsonify({"error": "Bot tidak ditemukan"}), 404) @api_bots.route('/api/bots', methods=['POST']) @@ -70,12 +87,15 @@ def add_bot_route(): """Membuat bot baru.""" data = request.get_json() params_json = json.dumps(data.get('params', {})) + + # Get strategy switching setting + enable_strategy_switching = int(data.get('enable_strategy_switching', False)) new_bot_id = queries.add_bot( name=data.get('name'), market=data.get('market'), lot_size=data.get('risk_percent'), sl_pips=data.get('sl_atr_multiplier'), tp_pips=data.get('tp_atr_multiplier'), timeframe=data.get('timeframe'), interval=data.get('check_interval_seconds'), strategy=data.get('strategy'), - strategy_params=params_json + strategy_params=params_json, enable_strategy_switching=enable_strategy_switching ) if new_bot_id: controller.add_new_bot_to_controller(new_bot_id) @@ -89,10 +109,28 @@ def update_bot_route(bot_id): bot_instance = controller.get_bot_instance_by_id(bot_id) bot_was_running = bot_instance and bot_instance.status == 'Aktif' + + # Get strategy switching setting + enable_strategy_switching = int(bot_data.get('enable_strategy_switching', False)) success, result = controller.perbarui_bot(bot_id, bot_data) + # Update the database with the strategy switching setting if success: + queries.update_bot( + bot_id=bot_id, + name=bot_data.get('name'), + market=bot_data.get('market'), + lot_size=bot_data.get('risk_percent'), + sl_pips=bot_data.get('sl_atr_multiplier'), + tp_pips=bot_data.get('tp_atr_multiplier'), + timeframe=bot_data.get('timeframe'), + interval=bot_data.get('check_interval_seconds'), + strategy=bot_data.get('strategy'), + strategy_params=json.dumps(bot_data.get('params', {})), + enable_strategy_switching=enable_strategy_switching + ) + if bot_was_running: controller.mulai_bot(bot_id) return jsonify({"message": "Bot berhasil diperbarui."}), 200 diff --git a/core/routes/api_ramadan.py b/core/routes/api_ramadan.py new file mode 100644 index 0000000..51e554e --- /dev/null +++ b/core/routes/api_ramadan.py @@ -0,0 +1,141 @@ +# core/routes/api_ramadan.py +""" +API endpoints for Ramadan Trading Mode features +""" + +from flask import Blueprint, jsonify +from core.seasonal.holiday_manager import holiday_manager +import logging + +api_ramadan = Blueprint('api_ramadan', __name__) +logger = logging.getLogger(__name__) + +@api_ramadan.route('/api/ramadan/status') +def get_ramadan_status(): + """Get current Ramadan trading mode status""" + try: + current_holiday = holiday_manager.get_current_holiday_mode() + + if not current_holiday or current_holiday.name != "Ramadan Trading Mode": + return jsonify({ + 'success': True, + 'is_ramadan': False, + 'message': 'Currently not in Ramadan trading mode' + }) + + return jsonify({ + 'success': True, + 'is_ramadan': True, + 'holiday_name': current_holiday.name, + 'start_date': current_holiday.start_date.isoformat(), + 'end_date': current_holiday.end_date.isoformat(), + 'trading_adjustments': current_holiday.trading_adjustments, + 'ui_theme': current_holiday.ui_theme, + 'greeting': holiday_manager.get_holiday_greeting() + }) + except Exception as e: + logger.error(f"Error getting Ramadan status: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_ramadan.route('/api/ramadan/features') +def get_ramadan_features(): + """Get Ramadan-specific features data""" + try: + ramadan_features = holiday_manager.get_ramadan_features() + + if not ramadan_features: + return jsonify({ + 'success': True, + 'is_ramadan': False, + 'message': 'Currently not in Ramadan trading mode' + }) + + return jsonify({ + 'success': True, + 'is_ramadan': True, + 'features': ramadan_features + }) + except Exception as e: + logger.error(f"Error getting Ramadan features: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_ramadan.route('/api/ramadan/pause-status') +def get_ramadan_pause_status(): + """Check if trading is currently paused due to Ramadan prayer times""" + try: + is_paused = holiday_manager.is_trading_paused() + pause_reason = "" + + if is_paused: + # Determine which prayer time is causing the pause + current_holiday = holiday_manager.get_current_holiday_mode() + if current_holiday and current_holiday.name == "Ramadan Trading Mode": + from datetime import datetime + now = datetime.now() + current_time = (now.hour, now.minute) + + adjustments = current_holiday.trading_adjustments + + # Check Sahur pause + if 'sahur_pause' in adjustments: + start_hour, start_min, end_hour, end_min = adjustments['sahur_pause'] + if (start_hour, start_min) <= current_time <= (end_hour, end_min): + pause_reason = "Sahur time - time for spiritual reflection and preparation" + + # Check Iftar pause + if 'iftar_pause' in adjustments: + start_hour, start_min, end_hour, end_min = adjustments['iftar_pause'] + if (start_hour, start_min) <= current_time <= (end_hour, end_min): + pause_reason = "Iftar time - breaking fast and family time" + + # Check Tarawih pause + if 'tarawih_pause' in adjustments: + start_hour, start_min, end_hour, end_min = adjustments['tarawih_pause'] + if (start_hour, start_min) <= current_time <= (end_hour, end_min): + pause_reason = "Tarawih prayers - spiritual devotion time" + + return jsonify({ + 'success': True, + 'is_paused': is_paused, + 'pause_reason': pause_reason, + 'message': pause_reason if is_paused else "Trading is active" + }) + except Exception as e: + logger.error(f"Error checking Ramadan pause status: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_ramadan.route('/api/ramadan/zakat-calculator') +def get_zakat_calculator(): + """Get Zakat calculation information""" + try: + current_holiday = holiday_manager.get_current_holiday_mode() + + if not current_holiday or current_holiday.name != "Ramadan Trading Mode": + return jsonify({ + 'success': True, + 'is_ramadan': False, + 'message': 'Zakat calculator only available during Ramadan' + }) + + zakat_info = holiday_manager._calculate_zakat_info() + + return jsonify({ + 'success': True, + 'is_ramadan': True, + 'zakat_info': zakat_info + }) + except Exception as e: + logger.error(f"Error getting Zakat calculator: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 \ No newline at end of file diff --git a/core/routes/api_strategy_switcher.py b/core/routes/api_strategy_switcher.py new file mode 100644 index 0000000..4504ed8 --- /dev/null +++ b/core/routes/api_strategy_switcher.py @@ -0,0 +1,408 @@ +# core/routes/api_strategy_switcher.py +""" +API endpoints for the automatic strategy switching system + +This module provides REST API endpoints for monitoring and controlling +the automatic strategy switching system. +""" + +from flask import Blueprint, jsonify, request +import pandas as pd +import os +from datetime import datetime + +from ..strategies.strategy_switcher import ( + strategy_switcher, + evaluate_strategy_switch, + get_switcher_status, + get_recent_switches +) +from ..strategies.market_condition_detector import get_market_condition +from ..strategies.performance_scorer import calculate_strategy_score, rank_strategies +from ..backtesting.enhanced_engine import run_enhanced_backtest +from ..strategies.strategy_map import STRATEGY_MAP + +# Create blueprint +api_strategy_switcher = Blueprint('api_strategy_switcher', __name__, url_prefix='/api/strategy-switcher') + +@api_strategy_switcher.route('/status', methods=['GET']) +def get_status(): + """Get current strategy switcher status""" + try: + status = get_switcher_status() + return jsonify({ + 'success': True, + 'data': status + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/recent-switches', methods=['GET']) +def get_recent_switches_api(): + """Get recent strategy switches""" + try: + count = request.args.get('count', 10, type=int) + switches = get_recent_switches(count) + return jsonify({ + 'success': True, + 'data': switches + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/evaluate', methods=['POST']) +def evaluate_switch(): + """Manually trigger strategy evaluation""" + try: + # Load current market data for monitored instruments + current_data = {} + data_directory = strategy_switcher.config.get('data_directory', 'lab/backtest_data') + + for symbol in strategy_switcher.monitored_instruments: + file_path = os.path.join(data_directory, f'{symbol}_H1_data.csv') + if os.path.exists(file_path): + try: + df = pd.read_csv(file_path, parse_dates=['time']) + current_data[symbol] = df + except Exception as e: + print(f"Error loading data for {symbol}: {e}") + continue + + if not current_data: + return jsonify({ + 'success': False, + 'error': 'No market data available for evaluation' + }), 400 + + # Evaluate and potentially switch + switch_decision = evaluate_strategy_switch(current_data) + + return jsonify({ + 'success': True, + 'data': { + 'switch_decision': switch_decision, + 'evaluation_time': datetime.now().isoformat() + } + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/rankings', methods=['GET']) +def get_strategy_rankings(): + """Get current strategy performance rankings""" + try: + # Load current market data + current_data = {} + data_directory = strategy_switcher.config.get('data_directory', 'lab/backtest_data') + + for symbol in strategy_switcher.monitored_instruments: + file_path = os.path.join(data_directory, f'{symbol}_H1_data.csv') + if os.path.exists(file_path): + try: + df = pd.read_csv(file_path, parse_dates=['time']) + current_data[symbol] = df + except Exception as e: + print(f"Error loading data for {symbol}: {e}") + continue + + if not current_data: + return jsonify({ + 'success': False, + 'error': 'No market data available for evaluation' + }), 400 + + # Evaluate all combinations + performance_scores = [] + + for symbol, df in current_data.items(): + if df.empty: + continue + + # Get market condition for this symbol + market_condition = get_market_condition(df, symbol) + + # Test each strategy on this symbol + for strategy_id in strategy_switcher.test_strategies: + if strategy_id not in STRATEGY_MAP: + continue + + try: + # Run backtest with recent data + test_df = df.tail(strategy_switcher.config['performance_evaluation_period']).copy() + + # Get strategy-specific parameters + strategy_params = strategy_switcher._get_strategy_parameters(strategy_id, symbol) + + # Run backtest + backtest_results = run_enhanced_backtest( + strategy_id, + strategy_params, + test_df, + symbol_name=symbol + ) + + if 'error' in backtest_results: + continue + + # Calculate performance score + score = calculate_strategy_score( + backtest_results, market_condition, strategy_id, symbol + ) + + performance_scores.append(score) + + except Exception as e: + print(f"Error evaluating {strategy_id}/{symbol}: {e}") + continue + + # Rank combinations + ranked_combinations = rank_strategies(performance_scores) + + return jsonify({ + 'success': True, + 'data': ranked_combinations[:20] # Top 20 rankings + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/market-conditions', methods=['GET']) +def get_market_conditions(): + """Get current market conditions for all monitored instruments""" + try: + # Load current market data + current_data = {} + data_directory = strategy_switcher.config.get('data_directory', 'lab/backtest_data') + + for symbol in strategy_switcher.monitored_instruments: + file_path = os.path.join(data_directory, f'{symbol}_H1_data.csv') + if os.path.exists(file_path): + try: + df = pd.read_csv(file_path, parse_dates=['time']) + current_data[symbol] = df + except Exception as e: + print(f"Error loading data for {symbol}: {e}") + continue + + market_conditions = {} + + for symbol, df in current_data.items(): + if not df.empty: + condition = get_market_condition(df, symbol) + market_conditions[symbol] = condition + + return jsonify({ + 'success': True, + 'data': market_conditions + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/configuration', methods=['GET', 'PUT']) +def manage_configuration(): + """Get or update strategy switcher configuration""" + if request.method == 'GET': + try: + return jsonify({ + 'success': True, + 'data': strategy_switcher.config + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + elif request.method == 'PUT': + try: + new_config = request.get_json() + + if not new_config: + return jsonify({ + 'success': False, + 'error': 'No configuration data provided' + }), 400 + + # Update configuration + strategy_switcher.config.update(new_config) + + # Save to file + try: + with open(strategy_switcher.config_file, 'w') as f: + import json + json.dump(strategy_switcher.config, f, indent=2) + except Exception as e: + print(f"Warning: Could not save configuration to file: {e}") + + return jsonify({ + 'success': True, + 'data': strategy_switcher.config, + 'message': 'Configuration updated successfully' + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/test-combination', methods=['POST']) +def test_strategy_combination(): + """Test a specific strategy/symbol combination""" + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': 'No test data provided' + }), 400 + + strategy_id = data.get('strategy_id') + symbol = data.get('symbol') + + if not strategy_id or not symbol: + return jsonify({ + 'success': False, + 'error': 'Both strategy_id and symbol are required' + }), 400 + + if strategy_id not in STRATEGY_MAP: + return jsonify({ + 'success': False, + 'error': f'Strategy {strategy_id} not found' + }), 404 + + # Load data for symbol + data_directory = strategy_switcher.config.get('data_directory', 'lab/backtest_data') + file_path = os.path.join(data_directory, f'{symbol}_H1_data.csv') + + if not os.path.exists(file_path): + return jsonify({ + 'success': False, + 'error': f'Data file for {symbol} not found' + }), 404 + + try: + df = pd.read_csv(file_path, parse_dates=['time']) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'Error loading data for {symbol}: {e}' + }), 500 + + if df.empty: + return jsonify({ + 'success': False, + 'error': f'No data available for {symbol}' + }), 400 + + # Get market condition + market_condition = get_market_condition(df, symbol) + + # Get strategy parameters + strategy_params = strategy_switcher._get_strategy_parameters(strategy_id, symbol) + + # Run backtest + test_df = df.tail(strategy_switcher.config['performance_evaluation_period']).copy() + backtest_results = run_enhanced_backtest( + strategy_id, + strategy_params, + test_df, + symbol_name=symbol + ) + + if 'error' in backtest_results: + return jsonify({ + 'success': False, + 'error': backtest_results['error'] + }), 500 + + # Calculate performance score + score = calculate_strategy_score( + backtest_results, market_condition, strategy_id, symbol + ) + + return jsonify({ + 'success': True, + 'data': { + 'backtest_results': backtest_results, + 'performance_score': score, + 'market_condition': market_condition + } + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_strategy_switcher.route('/manual-trigger', methods=['POST']) +def manual_trigger(): + """Manually trigger strategy evaluation and switch if needed""" + try: + # Load current market data for monitored instruments + current_data = {} + data_directory = strategy_switcher.config.get('data_directory', 'lab/backtest_data') + + for symbol in strategy_switcher.monitored_instruments: + file_path = os.path.join(data_directory, f'{symbol}_H1_data.csv') + if os.path.exists(file_path): + try: + df = pd.read_csv(file_path, parse_dates=['time']) + current_data[symbol] = df + except Exception as e: + print(f"Error loading data for {symbol}: {e}") + continue + + if not current_data: + return jsonify({ + 'success': False, + 'error': 'No market data available for evaluation' + }), 400 + + # Evaluate and potentially switch + switch_decision = evaluate_strategy_switch(current_data) + + # Create notification about the manual trigger + from core.db.queries import add_history_log + if switch_decision: + action = "MANUAL_STRATEGY_EVALUATION" + details = f"Manual strategy evaluation completed. Switch decision: {switch_decision['action']}" + else: + action = "MANUAL_STRATEGY_EVALUATION" + details = "Manual strategy evaluation completed. No switch needed." + + add_history_log( + bot_id=0, + action=action, + details=details, + is_notification=True + ) + + return jsonify({ + 'success': True, + 'data': { + 'switch_decision': switch_decision, + 'evaluation_time': datetime.now().isoformat() + }, + 'message': 'Manual strategy evaluation completed successfully' + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 diff --git a/core/seasonal/holiday_manager.py b/core/seasonal/holiday_manager.py index 155074a..59eb72f 100644 --- a/core/seasonal/holiday_manager.py +++ b/core/seasonal/holiday_manager.py @@ -7,7 +7,6 @@ Built specifically for Indonesian Catholic and Muslim traders from datetime import datetime, date from typing import Dict, List, Any, Optional -import calendar from dataclasses import dataclass @dataclass @@ -17,7 +16,7 @@ class HolidayConfig: start_date: date end_date: date trading_adjustments: Dict[str, Any] - ui_theme: Dict[str, str] + ui_theme: Dict[str, Any] greetings: List[str] class IndonesianHolidayManager: @@ -94,7 +93,11 @@ class IndonesianHolidayManager: 'risk_reduction': 0.8, # 20% risk reduction during fasting 'optimal_hours': [(22, 0), (3, 0)], # 22:00-03:00 WIB 'patience_mode': True, - 'halal_focus': True + 'halal_focus': True, + 'zakat_calculator': True, + 'charity_tracker': True, + 'reduced_risk_mode': True, + 'iftar_countdown': True }, ui_theme={ 'primary_color': '#006600', # Islamic green @@ -102,7 +105,8 @@ class IndonesianHolidayManager: 'accent_color': '#ffffff', # White 'background_gradient': 'linear-gradient(135deg, #006600 0%, #ffd700 100%)', 'crescent_moon': True, - 'islamic_patterns': True + 'islamic_patterns': True, + 'ramadan_decorations': True }, greetings=[ "๐ Ramadan Mubarak! Semoga trading dan ibadah berkah", @@ -110,7 +114,10 @@ class IndonesianHolidayManager: "โจ Lailatul Qadar trading wisdom: Quality over quantity", "๐คฒ Barakallahu fiikum dalam trading bulan suci ini", "๐ฐ Ingat zakat dari profit trading - berkah berlipat", - "๐ Sahur dengan doa, trading dengan tawakal" + "๐ Sahur dengan doa, trading dengan tawakal", + "๐ Puasa hari ini, profit berkah selalu", + "๐ Trading dengan sabar seperti puasa - hasil terbaik", + "๐ Bulan suci, hati tenang, trading profit" ] ) @@ -203,8 +210,113 @@ class IndonesianHolidayManager: if current_holiday and 'pause_dates' in current_holiday.trading_adjustments: return today in current_holiday.trading_adjustments['pause_dates'] + # Check for Ramadan-specific pauses + if current_holiday and current_holiday.name == "Ramadan Trading Mode": + return self._is_ramadan_pause_time() + return False + def _is_ramadan_pause_time(self) -> bool: + """Check if current time is during Ramadan pause periods (Sahur, Iftar, Tarawih)""" + from datetime import datetime + now = datetime.now() + current_time = (now.hour, now.minute) + + # Get current holiday config + current_holiday = self.get_current_holiday_mode() + if not current_holiday: + return False + + adjustments = current_holiday.trading_adjustments + + # Check Sahur pause (03:30-05:00 WIB) + if 'sahur_pause' in adjustments: + start_hour, start_min, end_hour, end_min = adjustments['sahur_pause'] + if (start_hour, start_min) <= current_time <= (end_hour, end_min): + return True + + # Check Iftar pause (18:00-19:30 WIB) + if 'iftar_pause' in adjustments: + start_hour, start_min, end_hour, end_min = adjustments['iftar_pause'] + if (start_hour, start_min) <= current_time <= (end_hour, end_min): + return True + + # Check Tarawih pause (20:00-21:30 WIB) + if 'tarawih_pause' in adjustments: + start_hour, start_min, end_hour, end_min = adjustments['tarawih_pause'] + if (start_hour, start_min) <= current_time <= (end_hour, end_min): + return True + + return False + + def get_ramadan_features(self) -> Dict[str, Any]: + """Get Ramadan-specific features and data""" + current_holiday = self.get_current_holiday_mode() + + if not current_holiday or current_holiday.name != "Ramadan Trading Mode": + return {} + + from datetime import datetime, timedelta + now = datetime.now() + + # Calculate Iftar countdown + iftar_time = datetime(now.year, now.month, now.day, 18, 0) # 18:00 WIB + if now > iftar_time: + # If it's past Iftar today, set for tomorrow + iftar_time += timedelta(days=1) + + countdown_seconds = (iftar_time - now).total_seconds() + countdown_hours = int(countdown_seconds // 3600) + countdown_minutes = int((countdown_seconds % 3600) // 60) + + # Get next prayer times (simplified) + next_prayer = "Iftar" if now.hour < 18 else "Sahur (besok)" + + return { + 'iftar_countdown': { + 'hours': countdown_hours, + 'minutes': countdown_minutes, + 'next_prayer': next_prayer + }, + 'zakat_info': self._calculate_zakat_info(), + 'charity_tracker': self._get_charity_tracker_data(), + 'patience_reminder': self._get_patience_reminder(), + 'optimal_trading_hours': current_holiday.trading_adjustments.get('optimal_hours', []) + } + + def _calculate_zakat_info(self) -> Dict[str, Any]: + """Calculate Zakat information based on trading profits""" + # This would integrate with actual trading data in a real implementation + return { + 'nissab_gold': 85, # grams of gold (simplified) + 'nissab_silver': 595, # grams of silver (simplified) + 'zakat_percentage': 2.5, # 2.5% of eligible wealth + 'reminder': 'Zakat perdagangan: 2.5% dari profit trading selama 1 tahun hijriah' + } + + def _get_charity_tracker_data(self) -> Dict[str, Any]: + """Get charity tracking data""" + # This would integrate with actual charity donations in a real implementation + return { + 'total_donated': 0.0, # Would come from actual data + 'monthly_target': 100.0, # User-configurable target + 'progress_percentage': 0, # Would be calculated from actual data + 'suggested_amount': 10.0 # Suggested donation amount + } + + def _get_patience_reminder(self) -> str: + """Get a patience reminder for Ramadan trading""" + patience_reminders = [ + "๐ง Sabar dalam trading seperti puasa - hasil terbaik datang dengan kesabaran", + "๐ Puasa mengajarkan kontrol diri - kontrol emosi trading juga penting", + "๐ Seperti salat, konsistensi dalam trading menghasilkan profit berkelanjutan", + "โจ Lailatul Qadar: Kualitas lebih penting dari kuantitas dalam trading", + "๐คฒ Tawakal dalam setiap keputusan trading, percayalah pada proses", + "๐ Puasa hari ini, profit berkah selalu - trading dengan niat ikhlas" + ] + import random + return random.choice(patience_reminders) + def get_risk_multiplier(self) -> float: """Get risk reduction multiplier for current holiday""" current_holiday = self.get_current_holiday_mode() @@ -269,4 +381,8 @@ def get_holiday_risk_multiplier() -> float: def get_holiday_greeting() -> str: """Get current holiday greeting""" - return holiday_manager.get_holiday_greeting() \ No newline at end of file + return holiday_manager.get_holiday_greeting() + +def get_current_holiday_mode() -> Optional[HolidayConfig]: + """Get current holiday mode for dashboard integration""" + return holiday_manager.get_current_holiday_mode() \ No newline at end of file diff --git a/core/strategies/index_breakout_pro.py b/core/strategies/index_breakout_pro.py index c0650e2..a6ba30e 100644 --- a/core/strategies/index_breakout_pro.py +++ b/core/strategies/index_breakout_pro.py @@ -1,9 +1,7 @@ # core/strategies/index_breakout_pro.py import pandas as pd -import numpy as np import pandas_ta as ta -from datetime import datetime, time from .base_strategy import BaseStrategy import logging @@ -27,16 +25,19 @@ class IndexBreakoutProStrategy(BaseStrategy): Best for: Experienced traders seeking to capture major index movements """ + name = 'INDEX_BREAKOUT_PRO' + description = 'Advanced breakout strategy with institutional pattern recognition for stock indices' + def __init__(self, bot_instance, params=None): # Advanced parameters for professional breakout trading default_params = { 'breakout_period': 20, # Lookback period for breakout levels - 'volume_surge_multiplier': 2.0, # Volume surge detection + 'volume_surge_multiplier': 1.5, # Volume surge detection (reduced from 2.0 for more signals) 'confirmation_candles': 2, # Candles to confirm breakout 'support_resistance_strength': 3, # Minimum touches for S/R level 'atr_multiplier_sl': 2.0, # ATR multiplier for stop loss 'atr_multiplier_tp': 4.0, # ATR multiplier for take profit - 'min_breakout_size': 0.3, # Minimum breakout size (% of ATR) + 'min_breakout_size': 0.2, # Minimum breakout size (reduced from 0.3 for sensitivity) 'max_risk_per_trade': 1.0, # Maximum risk per trade (%) 'trend_filter_period': 50, # Long-term trend filter 'vwap_filter': True, # Use VWAP as trend filter @@ -124,41 +125,94 @@ class IndexBreakoutProStrategy(BaseStrategy): def _calculate_advanced_indicators(self, df): """Calculate advanced technical indicators for professional analysis""" + # Handle volume column compatibility (CSV files use 'volume', MT5 uses 'tick_volume') + if 'volume' in df.columns and 'tick_volume' not in df.columns: + df['tick_volume'] = df['volume'] + elif 'tick_volume' not in df.columns and 'volume' not in df.columns: + # Fallback: create dummy volume data + df['tick_volume'] = df['close'] * 0 + 1 # Dummy volume + logger.warning("No volume data found, using dummy volume for calculations") + # Basic indicators df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14) df['ema_20'] = ta.ema(df['close'], length=20) df['ema_50'] = ta.ema(df['close'], length=50) - # Volume indicators - df['volume_avg'] = df['tick_volume'].rolling(20).mean() - df['volume_ratio'] = df['tick_volume'] / df['volume_avg'] + # More practical volume indicators for backtesting + # Use a shorter period and ensure we always have valid volume ratios + volume_period = min(10, len(df) // 4) # Adaptive period + if volume_period < 5: + volume_period = 5 + + df['volume_avg'] = df['tick_volume'].rolling(volume_period, min_periods=1).mean() + + # Prevent division by zero and ensure realistic volume ratios + df['volume_ratio'] = df['tick_volume'] / df['volume_avg'].replace(0, df['tick_volume'].mean()) + + # Fill any NaN values in volume_ratio + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) # VWAP calculation df['vwap'] = self._calculate_vwap(df) # Bollinger Bands for volatility bb = ta.bbands(df['close'], length=20) - df['bb_upper'] = bb['BBU_20_2.0'] - df['bb_lower'] = bb['BBL_20_2.0'] - df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['close'] * 100 + if bb is not None: + df['bb_upper'] = bb['BBU_20_2.0'] + df['bb_lower'] = bb['BBL_20_2.0'] + df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['close'] * 100 + else: + # Fallback calculation when ta.bbands returns None + sma_20 = df['close'].rolling(20).mean() + std_20 = df['close'].rolling(20).std() + df['bb_upper'] = sma_20 + (std_20 * 2) + df['bb_lower'] = sma_20 - (std_20 * 2) + df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['close'] * 100 # Market strength indicators - df['rsi'] = ta.rsi(df['close'], length=14) - df['adx'] = ta.adx(df['high'], df['low'], df['close'], length=14)['ADX_14'] + rsi_result = ta.rsi(df['close'], length=14) + if rsi_result is not None: + df['rsi'] = rsi_result + else: + # Fallback RSI calculation + delta = df['close'].diff() + gain = (delta.where(delta > 0, 0)).rolling(window=14).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() + rs = gain / loss + df['rsi'] = 100 - (100 / (1 + rs)) + adx_result = ta.adx(df['high'], df['low'], df['close'], length=14) + if adx_result is not None and 'ADX_14' in adx_result.columns: + df['adx'] = adx_result['ADX_14'] + else: + # Fallback: use a simple trend strength indicator + df['adx'] = abs(df['close'].pct_change(14)) * 100 # Price momentum df['momentum'] = df['close'] / df['close'].shift(10) - 1 - df['rate_of_change'] = ta.roc(df['close'], length=10) + roc_result = ta.roc(df['close'], length=10) + if roc_result is not None: + df['rate_of_change'] = roc_result + else: + # Fallback ROC calculation + df['rate_of_change'] = (df['close'] / df['close'].shift(10) - 1) * 100 return df def _calculate_vwap(self, df): """Calculate Volume Weighted Average Price""" try: + # Use tick_volume if available, otherwise use volume, otherwise fallback + volume_col = 'tick_volume' if 'tick_volume' in df.columns else ('volume' if 'volume' in df.columns else None) + + if volume_col is None: + logger.warning("No volume data available for VWAP calculation, using simple MA") + return df['close'].rolling(20).mean() + typical_price = (df['high'] + df['low'] + df['close']) / 3 - vwap = (typical_price * df['tick_volume']).cumsum() / df['tick_volume'].cumsum() + vwap = (typical_price * df[volume_col]).cumsum() / df[volume_col].cumsum() return vwap - except Exception: + except Exception as e: + logger.warning(f"VWAP calculation failed: {e}, using simple MA fallback") return df['close'].rolling(20).mean() # Fallback to simple MA def _analyze_market_structure(self, df, config): @@ -301,7 +355,7 @@ class IndexBreakoutProStrategy(BaseStrategy): def _generate_professional_signal(self, df, market_structure, sr_levels, vpa_signal, mtf_signal, config): """Generate sophisticated trading signal based on all analyses""" current_price = df['close'].iloc[-1] - current_atr = df['atr'].iloc[-1] + df['atr'].iloc[-1] # Initialize signal components signal_components = [] @@ -381,7 +435,7 @@ class IndexBreakoutProStrategy(BaseStrategy): min_breakout_distance = current_atr * self.params['min_breakout_size'] for level in sr_levels: - distance = abs(current_price - level['level']) + abs(current_price - level['level']) if level['type'] == 'resistance' and current_price > level['level'] + min_breakout_distance: # Bullish breakout @@ -436,6 +490,14 @@ class IndexBreakoutProStrategy(BaseStrategy): # Process each row for backtesting for i in range(60, len(df)): + # Skip if we don't have enough data or NaN values + if (pd.isna(df['atr'].iloc[i]) or + pd.isna(df['volume_ratio'].iloc[i]) or + pd.isna(df['ema_20'].iloc[i]) or + pd.isna(df['ema_50'].iloc[i])): + continue + + # Get current data slice for analysis current_df = df.iloc[:i+1].copy() # Simplified analysis for backtesting performance @@ -446,7 +508,11 @@ class IndexBreakoutProStrategy(BaseStrategy): df.loc[df.index[i], 'explanation'] = signal_info['explanation'] if signal_info['signal'] in ['BUY', 'SELL']: - df.loc[df.index[i], 'signal_strength'] = 0.8 # High confidence for pro strategy + # Calculate signal strength based on volume and momentum + volume_strength = min(2.0, df['volume_ratio'].iloc[i]) - 1.0 + momentum_strength = abs(df['momentum'].iloc[i]) if not pd.isna(df['momentum'].iloc[i]) else 0.0 + signal_strength = min(1.0, (volume_strength + momentum_strength) * 0.4 + 0.6) + df.loc[df.index[i], 'signal_strength'] = signal_strength return df @@ -457,38 +523,154 @@ class IndexBreakoutProStrategy(BaseStrategy): def _simplified_analysis(self, df): """Simplified analysis for backtesting performance""" try: + if len(df) < 20: + return { + "signal": "HOLD", + "price": df['close'].iloc[-1], + "explanation": "Insufficient data for analysis" + } + current_price = df['close'].iloc[-1] - # Simple breakout detection for backtesting - lookback = 20 + # Practical breakout detection for backtesting + lookback = min(self.params.get('breakout_period', 20), len(df) - 1) recent_high = df['high'].iloc[-lookback:].max() recent_low = df['low'].iloc[-lookback:].min() - volume_surge = df['volume_ratio'].iloc[-1] > 1.5 + # Calculate price position relative to range + price_range = recent_high - recent_low + price_position = (current_price - recent_low) / price_range if price_range > 0 else 0.5 - if current_price > recent_high and volume_surge: + # Volume analysis (more lenient) + volume_ratio = df['volume_ratio'].iloc[-1] if not pd.isna(df['volume_ratio'].iloc[-1]) else 1.0 + volume_threshold = self.params.get('volume_surge_multiplier', 1.5) * 0.8 # Even more lenient + volume_confirmed = volume_ratio >= volume_threshold + + # Trend analysis using EMAs + ema_20 = df['ema_20'].iloc[-1] + ema_50 = df['ema_50'].iloc[-1] + + if not pd.isna(ema_20) and not pd.isna(ema_50): + bullish_trend = ema_20 > ema_50 + bearish_trend = ema_20 < ema_50 + abs(ema_20 - ema_50) / ema_50 if ema_50 > 0 else 0 + else: + # Fallback trend detection using simple price comparison + bullish_trend = current_price > df['close'].iloc[-10] if len(df) > 10 else True + bearish_trend = current_price < df['close'].iloc[-10] if len(df) > 10 else True + + # Price momentum analysis + if len(df) >= 5: + momentum_5 = (current_price - df['close'].iloc[-5]) / df['close'].iloc[-5] + momentum_10 = (current_price - df['close'].iloc[-10]) / df['close'].iloc[-10] if len(df) >= 10 else momentum_5 + else: + momentum_5 = momentum_10 = 0 + + # ATR-based breakout threshold (more sensitive) + atr = df['atr'].iloc[-1] + if not pd.isna(atr) and atr > 0: + breakout_threshold = atr * self.params.get('min_breakout_size', 0.2) * 0.5 # Half the normal threshold + else: + breakout_threshold = price_range * 0.002 # 0.2% of range + + # Generate signals with multiple criteria (more flexible) + + # Strong breakout signals (primary) + if current_price > recent_high + breakout_threshold: + if volume_confirmed and bullish_trend: + return { + "signal": "BUY", + "price": current_price, + "explanation": f"Strong bullish breakout: price {current_price:.2f} > high {recent_high:.2f}, vol {volume_ratio:.2f}x, trend up" + } + elif volume_confirmed: + return { + "signal": "BUY", + "price": current_price, + "explanation": f"Volume breakout: price {current_price:.2f} > high {recent_high:.2f}, vol {volume_ratio:.2f}x" + } + elif momentum_5 > 0.003: # 0.3% momentum + return { + "signal": "BUY", + "price": current_price, + "explanation": f"Momentum breakout: price {current_price:.2f} > high {recent_high:.2f}, momentum {momentum_5:.1%}" + } + + elif current_price < recent_low - breakout_threshold: + if volume_confirmed and bearish_trend: + return { + "signal": "SELL", + "price": current_price, + "explanation": f"Strong bearish breakdown: price {current_price:.2f} < low {recent_low:.2f}, vol {volume_ratio:.2f}x, trend down" + } + elif volume_confirmed: + return { + "signal": "SELL", + "price": current_price, + "explanation": f"Volume breakdown: price {current_price:.2f} < low {recent_low:.2f}, vol {volume_ratio:.2f}x" + } + elif momentum_5 < -0.003: # -0.3% momentum + return { + "signal": "SELL", + "price": current_price, + "explanation": f"Momentum breakdown: price {current_price:.2f} < low {recent_low:.2f}, momentum {momentum_5:.1%}" + } + + # Secondary signals: Range position based (more signals) + elif price_position > 0.85 and bullish_trend and momentum_5 > 0.001: return { "signal": "BUY", "price": current_price, - "explanation": "Breakout above recent high with volume" + "explanation": f"Range top breakout setup: {price_position:.0%} of range, trend up, momentum {momentum_5:.1%}" } - elif current_price < recent_low and volume_surge: + + elif price_position < 0.15 and bearish_trend and momentum_5 < -0.001: return { - "signal": "SELL", + "signal": "SELL", "price": current_price, - "explanation": "Breakdown below recent low with volume" + "explanation": f"Range bottom breakdown setup: {price_position:.0%} of range, trend down, momentum {momentum_5:.1%}" } - return { - "signal": "HOLD", - "price": current_price, - "explanation": "No clear breakout signal" - } + # Tertiary signals: Strong momentum (even without breakouts) + elif abs(momentum_5) > 0.005 and abs(momentum_10) > 0.008: # Strong momentum + if momentum_5 > 0 and momentum_10 > 0 and bullish_trend: + return { + "signal": "BUY", + "price": current_price, + "explanation": f"Strong momentum: 5-period {momentum_5:.1%}, 10-period {momentum_10:.1%}, trend aligned" + } + elif momentum_5 < 0 and momentum_10 < 0 and bearish_trend: + return { + "signal": "SELL", + "price": current_price, + "explanation": f"Strong negative momentum: 5-period {momentum_5:.1%}, 10-period {momentum_10:.1%}, trend aligned" + } - except Exception: + # Provide informative HOLD explanation + if not volume_confirmed: + return { + "signal": "HOLD", + "price": current_price, + "explanation": f"Low volume: {volume_ratio:.2f}x (need {volume_threshold:.2f}x), price at {price_position:.0%} of range" + } + elif price_range / current_price < 0.01: # Range too small + return { + "signal": "HOLD", + "price": current_price, + "explanation": f"Narrow range: ${price_range:.2f} ({price_range/current_price:.1%}), waiting for volatility" + } + else: + return { + "signal": "HOLD", + "price": current_price, + "explanation": f"No clear signal: {price_position:.0%} range position, vol {volume_ratio:.2f}x, momentum {momentum_5:.1%}" + } + + except Exception as e: + logger.error(f"Simplified analysis error: {e}") return { "signal": "HOLD", - "price": df['close'].iloc[-1], + "price": df['close'].iloc[-1] if not df.empty else 0, "explanation": "Analysis error" } @@ -509,10 +691,10 @@ class IndexBreakoutProStrategy(BaseStrategy): 'name': 'volume_surge_multiplier', 'display_name': 'Volume Surge Multiplier', 'type': 'float', - 'default': 2.0, - 'min': 1.5, - 'max': 5.0, - 'description': 'Volume multiplier to detect institutional activity' + 'default': 1.5, + 'min': 1.2, + 'max': 3.0, + 'description': 'Volume multiplier to detect institutional activity (lower = more signals)' }, { 'name': 'confirmation_candles', @@ -545,10 +727,10 @@ class IndexBreakoutProStrategy(BaseStrategy): 'name': 'min_breakout_size', 'display_name': 'Minimum Breakout Size', 'type': 'float', - 'default': 0.3, + 'default': 0.2, 'min': 0.1, - 'max': 1.0, - 'description': 'Minimum breakout size as fraction of ATR' + 'max': 0.8, + 'description': 'Minimum breakout size as fraction of ATR (lower = more sensitive)' }, { 'name': 'vwap_filter', @@ -563,5 +745,41 @@ class IndexBreakoutProStrategy(BaseStrategy): 'type': 'bool', 'default': True, 'description': 'Detect and use institutional support/resistance levels' + }, + { + 'name': 'support_resistance_strength', + 'display_name': 'S/R Level Strength', + 'type': 'int', + 'default': 3, + 'min': 2, + 'max': 10, + 'description': 'Minimum number of touches required for valid support/resistance level' + }, + { + 'name': 'max_risk_per_trade', + 'display_name': 'Maximum Risk Per Trade (%)', + 'type': 'float', + 'default': 1.0, + 'min': 0.5, + 'max': 5.0, + 'description': 'Maximum risk percentage per individual trade' + }, + { + 'name': 'trend_filter_period', + 'display_name': 'Trend Filter Period', + 'type': 'int', + 'default': 50, + 'min': 20, + 'max': 100, + 'description': 'Period for long-term trend filter analysis' + }, + { + 'name': 'gap_multiplier', + 'display_name': 'Gap Trading Multiplier', + 'type': 'float', + 'default': 1.5, + 'min': 1.0, + 'max': 3.0, + 'description': 'Multiplier for gap trading opportunity sizing' } ] \ No newline at end of file diff --git a/core/strategies/index_momentum.py b/core/strategies/index_momentum.py index bac414a..c7e9ab4 100644 --- a/core/strategies/index_momentum.py +++ b/core/strategies/index_momentum.py @@ -1,9 +1,8 @@ # core/strategies/index_momentum.py import pandas as pd -import numpy as np import pandas_ta as ta -from datetime import datetime, time +from datetime import datetime from .base_strategy import BaseStrategy import logging @@ -26,6 +25,9 @@ class IndexMomentumStrategy(BaseStrategy): Best for: Trending index movements during active trading sessions """ + name = 'INDEX_MOMENTUM' + description = 'Specialized momentum strategy for stock indices with session awareness and gap trading' + def __init__(self, bot_instance, params=None): # Default parameters optimized for stock indices default_params = { @@ -100,6 +102,14 @@ class IndexMomentumStrategy(BaseStrategy): def _calculate_indicators(self, df): """Calculate all required technical indicators""" + # Handle volume column compatibility (CSV files use 'volume', MT5 uses 'tick_volume') + if 'volume' in df.columns and 'tick_volume' not in df.columns: + df['tick_volume'] = df['volume'] + elif 'tick_volume' not in df.columns and 'volume' not in df.columns: + # Fallback: create dummy volume data + df['tick_volume'] = df['close'] * 0 + 1 # Dummy volume + logger.warning("No volume data found, using dummy volume for calculations") + # RSI for momentum df['rsi'] = ta.rsi(df['close'], length=self.params['momentum_period']) @@ -263,9 +273,17 @@ class IndexMomentumStrategy(BaseStrategy): df['signal_strength'] = 0.0 df['explanation'] = '' + # Get symbol configuration + symbol = getattr(self.bot, 'market_for_mt5', 'US500') + self.index_configs.get(symbol, self.index_configs['US500']) + # Process each row for backtesting for i in range(30, len(df)): - current_df = df.iloc[:i+1].copy() + # Skip if we don't have enough data or NaN values + if (pd.isna(df['rsi'].iloc[i]) or + pd.isna(df['volume_ratio'].iloc[i]) or + pd.isna(df['price_change'].iloc[i])): + continue # Simple gap detection for backtesting gap_info = {'has_gap': False, 'gap_size': 0, 'gap_direction': None} @@ -280,19 +298,69 @@ class IndexMomentumStrategy(BaseStrategy): 'gap_direction': 'up' if current_open > prev_close else 'down' } - # Generate signal - symbol = getattr(self.bot, 'market_for_mt5', 'US500') - config = self.index_configs.get(symbol, self.index_configs['US500']) - signal_info = self._generate_signal(current_df, gap_info, config) + # Generate signal using current row data + df['close'].iloc[i] + current_rsi = df['rsi'].iloc[i] + volume_ratio = df['volume_ratio'].iloc[i] + price_change = df['price_change'].iloc[i] + + # Volume confirmation + volume_confirmed = volume_ratio >= self.params['volume_multiplier'] + + # Momentum conditions + bullish_momentum = (current_rsi > 50 and + current_rsi < self.params['momentum_overbought'] and + price_change > 0) + + bearish_momentum = (current_rsi < 50 and + current_rsi > self.params['momentum_oversold'] and + price_change < 0) + + # Gap trading logic + gap_signal = self._analyze_gap_opportunity(gap_info, current_rsi) + + # Signal generation + signal = "HOLD" + explanation = "Waiting for clear momentum signal" + + # Buy conditions + if (bullish_momentum and volume_confirmed) or gap_signal == "BUY": + signal = "BUY" + reasons = [] + if bullish_momentum: + reasons.append(f"Bullish momentum (RSI: {current_rsi:.1f})") + if volume_confirmed: + reasons.append(f"Volume confirmed ({volume_ratio:.2f}x)") + if gap_signal == "BUY": + reasons.append(f"Gap opportunity ({gap_info['gap_direction']} {gap_info['gap_size']:.2f}%)") + explanation = f"BUY: {', '.join(reasons)}" + + # Sell conditions + elif (bearish_momentum and volume_confirmed) or gap_signal == "SELL": + signal = "SELL" + reasons = [] + if bearish_momentum: + reasons.append(f"Bearish momentum (RSI: {current_rsi:.1f})") + if volume_confirmed: + reasons.append(f"Volume confirmed ({volume_ratio:.2f}x)") + if gap_signal == "SELL": + reasons.append(f"Gap opportunity ({gap_info['gap_direction']} {gap_info['gap_size']:.2f}%)") + explanation = f"SELL: {', '.join(reasons)}" + + # Override conditions + if current_rsi > self.params['momentum_overbought']: + signal = "HOLD" + explanation = f"Overbought condition (RSI: {current_rsi:.1f})" + elif current_rsi < self.params['momentum_oversold']: + signal = "HOLD" + explanation = f"Oversold condition (RSI: {current_rsi:.1f})" # Store results - df.loc[df.index[i], 'signal'] = signal_info['signal'] - df.loc[df.index[i], 'explanation'] = signal_info['explanation'] + df.loc[df.index[i], 'signal'] = signal + df.loc[df.index[i], 'explanation'] = explanation # Calculate signal strength - if signal_info['signal'] in ['BUY', 'SELL']: - rsi = df['rsi'].iloc[i] - volume_ratio = df['volume_ratio'].iloc[i] + if signal in ['BUY', 'SELL']: strength = min(1.0, (volume_ratio - 1.0) * 0.5 + 0.5) df.loc[df.index[i], 'signal_strength'] = strength @@ -324,6 +392,24 @@ class IndexMomentumStrategy(BaseStrategy): 'max': 50, 'description': 'Period for volume average calculation' }, + { + 'name': 'momentum_oversold', + 'display_name': 'RSI Oversold Level', + 'type': 'int', + 'default': 30, + 'min': 10, + 'max': 40, + 'description': 'RSI level considered oversold (signals may reverse)' + }, + { + 'name': 'momentum_overbought', + 'display_name': 'RSI Overbought Level', + 'type': 'int', + 'default': 70, + 'min': 60, + 'max': 90, + 'description': 'RSI level considered overbought (signals may reverse)' + }, { 'name': 'gap_threshold', 'display_name': 'Gap Threshold (%)', diff --git a/core/strategies/market_condition_detector.py b/core/strategies/market_condition_detector.py new file mode 100644 index 0000000..56bb634 --- /dev/null +++ b/core/strategies/market_condition_detector.py @@ -0,0 +1,307 @@ +# core/strategies/market_condition_detector.py +""" +๐ Market Condition Detector for Automatic Strategy Switching + +This module detects market conditions (trending vs ranging) for different instruments +and provides market state information for strategy selection. + +Features: +- Trending vs ranging detection using multiple methods +- Volatility regime analysis +- Session-aware trading conditions +- Instrument-specific market characteristics +""" + +import pandas as pd +import numpy as np +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +class MarketConditionDetector: + """ + Detects market conditions for automatic strategy switching + """ + + def __init__(self): + # Market characteristics for different instrument types + self.instrument_configs = { + 'INDICES': { + 'trend_sensitivity': 0.7, + 'volatility_threshold': 1.5, + 'session_hours': {'ny': (13, 22), 'london': (8, 16)}, + 'typical_volatility': 0.8 + }, + 'FOREX': { + 'trend_sensitivity': 0.6, + 'volatility_threshold': 1.2, + 'session_hours': {'ny': (13, 22), 'london': (8, 16), 'asian': (0, 8)}, + 'typical_volatility': 0.5 + }, + 'GOLD': { + 'trend_sensitivity': 0.8, + 'volatility_threshold': 2.0, + 'session_hours': {'ny': (13, 22), 'london': (8, 16)}, + 'typical_volatility': 1.2 + }, + 'CRYPTO': { + 'trend_sensitivity': 0.5, + 'volatility_threshold': 3.0, + 'session_hours': {}, # 24/7 market + 'typical_volatility': 2.5 + } + } + + def detect_market_condition(self, df: pd.DataFrame, symbol: str = 'EURUSD') -> dict: + """ + Detect current market condition for a given instrument + + Args: + df: DataFrame with OHLCV data + symbol: Trading symbol (e.g., 'EURUSD', 'US500') + + Returns: + dict: Market condition analysis + """ + try: + if df.empty or len(df) < 50: + return self._default_condition() + + # Determine instrument type + instrument_type = self._classify_instrument(symbol) + config = self.instrument_configs.get(instrument_type, self.instrument_configs['FOREX']) + + # Calculate indicators + df_analysis = self._calculate_indicators(df) + + # Detect trend vs range + trend_score = self._calculate_trend_score(df_analysis, config) + volatility_regime = self._analyze_volatility_regime(df_analysis, config) + session_status = self._check_trading_session(config) + + # Determine market condition + if trend_score > config['trend_sensitivity']: + market_condition = 'trending' + confidence = min(1.0, trend_score) + else: + market_condition = 'ranging' + confidence = min(1.0, 1 - trend_score) + + # Price action characteristics + price_action = self._analyze_price_action(df_analysis) + + return { + 'instrument_type': instrument_type, + 'market_condition': market_condition, + 'confidence': confidence, + 'volatility_regime': volatility_regime, + 'session_status': session_status, + 'price_action': price_action, + 'trend_score': trend_score, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"Market condition detection error: {e}") + return self._default_condition() + + def _classify_instrument(self, symbol: str) -> str: + """Classify instrument type based on symbol""" + symbol_upper = symbol.upper() + + # Indices + if any(index in symbol_upper for index in ['US30', 'US100', 'US500', 'DE30', 'UK100', 'JP225', 'NAS100', 'SPX500']): + return 'INDICES' + + # Gold/Metals + if any(gold in symbol_upper for gold in ['XAU', 'XAG', 'GOLD']): + return 'GOLD' + + # Crypto + if any(crypto in symbol_upper for crypto in ['BTC', 'ETH', 'LTC', 'ADA', 'DOT', 'SOL']): + return 'CRYPTO' + + # Default to Forex + return 'FOREX' + + def _calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame: + """Calculate technical indicators for market condition analysis""" + df_calc = df.copy() + + # Price changes + df_calc['returns'] = df_calc['close'].pct_change() + df_calc['price_change'] = df_calc['close'] - df_calc['open'] + + # Moving averages + df_calc['sma_20'] = df_calc['close'].rolling(20).mean() + df_calc['sma_50'] = df_calc['close'].rolling(50).mean() + + # ATR for volatility + df_calc['tr'] = np.maximum( + np.maximum( + df_calc['high'] - df_calc['low'], + abs(df_calc['high'] - df_calc['close'].shift(1)) + ), + abs(df_calc['low'] - df_calc['close'].shift(1)) + ) + df_calc['atr'] = df_calc['tr'].rolling(14).mean() + + # ADX for trend strength + df_calc = self._calculate_adx(df_calc) + + return df_calc + + def _calculate_adx(self, df: pd.DataFrame) -> pd.DataFrame: + """Calculate ADX (Average Directional Index)""" + # This is a simplified ADX calculation + df_calc = df.copy() + + # +DI and -DI calculation (simplified) + up_move = df_calc['high'] - df_calc['high'].shift(1) + down_move = df_calc['low'].shift(1) - df_calc['low'] + + # +DM and -DM + df_calc['+dm'] = np.where((up_move > down_move) & (up_move > 0), up_move, 0) + df_calc['-dm'] = np.where((down_move > up_move) & (down_move > 0), down_move, 0) + + # ATR (already calculated) + atr = df_calc['atr'] + + # +DI and -DI + df_calc['+di'] = 100 * (df_calc['+dm'].rolling(14).mean() / atr) + df_calc['-di'] = 100 * (df_calc['-dm'].rolling(14).mean() / atr) + + # ADX + dx = 100 * (abs(df_calc['+di'] - df_calc['-di']) / (df_calc['+di'] + df_calc['-di'])) + df_calc['adx'] = dx.rolling(14).mean() + + return df_calc + + def _calculate_trend_score(self, df: pd.DataFrame, config: dict) -> float: + """Calculate trend score based on multiple factors""" + if len(df) < 50: + return 0.5 + + # ADX trend strength (0-100, higher = stronger trend) + adx_values = df['adx'].tail(20).dropna() + if len(adx_values) > 0: + adx_trend = np.mean(adx_values) / 100 # Normalize to 0-1 + else: + adx_trend = 0.5 + + # Moving average alignment + recent_data = df.tail(20) + if len(recent_data) > 0: + ma_aligned = (recent_data['sma_20'] > recent_data['sma_50']).mean() + ma_trend = abs(ma_aligned - 0.5) * 2 # Convert to 0-1 scale + else: + ma_trend = 0.5 + + # Price trend consistency + returns = df['returns'].tail(20).dropna() + if len(returns) > 0: + trend_consistency = abs(returns.mean() / (returns.std() + 1e-10)) # Sharpe-like ratio + trend_consistency = min(1.0, trend_consistency) # Cap at 1.0 + else: + trend_consistency = 0.5 + + # Weighted average of trend factors + trend_score = (adx_trend * 0.4 + ma_trend * 0.3 + trend_consistency * 0.3) + + return min(1.0, max(0.0, trend_score)) + + def _analyze_volatility_regime(self, df: pd.DataFrame, config: dict) -> str: + """Analyze current volatility regime""" + if len(df) < 20: + return 'normal' + + # Current ATR vs historical ATR + current_atr = df['atr'].iloc[-1] + avg_atr = df['atr'].tail(50).mean() + + if pd.isna(current_atr) or pd.isna(avg_atr) or avg_atr == 0: + return 'normal' + + volatility_ratio = current_atr / avg_atr + + if volatility_ratio > config['volatility_threshold']: + return 'high' + elif volatility_ratio < (1 / config['volatility_threshold']): + return 'low' + else: + return 'normal' + + def _check_trading_session(self, config: dict) -> str: + """Check current trading session status""" + try: + current_time = datetime.now() + current_hour = current_time.hour + + # Check active sessions + active_sessions = [] + for session_name, (start_hour, end_hour) in config['session_hours'].items(): + if start_hour <= current_hour <= end_hour: + active_sessions.append(session_name) + + if active_sessions: + return f"active_{'_'.join(active_sessions)}" + else: + return "quiet" + + except Exception: + return "unknown" + + def _analyze_price_action(self, df: pd.DataFrame) -> dict: + """Analyze recent price action characteristics""" + if len(df) < 20: + return {'pattern': 'neutral', 'strength': 0.5} + + recent_data = df.tail(20) + + # Calculate price action metrics + body_size = abs(recent_data['close'] - recent_data['open']) + wick_size = (recent_data['high'] - recent_data['low']) - body_size + body_wick_ratio = body_size / (wick_size + 1e-10) + + # Trend direction + price_change = recent_data['close'].iloc[-1] - recent_data['close'].iloc[0] + trend_direction = 'bullish' if price_change > 0 else 'bearish' if price_change < 0 else 'neutral' + + # Volatility + volatility = recent_data['returns'].std() * 100 + + return { + 'pattern': trend_direction, + 'strength': min(1.0, volatility / 2), # Normalize volatility + 'body_wick_ratio': body_wick_ratio.mean() if len(body_wick_ratio) > 0 else 1.0 + } + + def _default_condition(self) -> dict: + """Return default market condition when detection fails""" + return { + 'instrument_type': 'FOREX', + 'market_condition': 'ranging', + 'confidence': 0.5, + 'volatility_regime': 'normal', + 'session_status': 'unknown', + 'price_action': {'pattern': 'neutral', 'strength': 0.5}, + 'trend_score': 0.5, + 'timestamp': datetime.now().isoformat() + } + +# Global instance +market_condition_detector = MarketConditionDetector() + +def get_market_condition(df: pd.DataFrame, symbol: str = 'EURUSD') -> dict: + """ + Convenience function to get market condition + + Args: + df: DataFrame with OHLCV data + symbol: Trading symbol + + Returns: + dict: Market condition analysis + """ + return market_condition_detector.detect_market_condition(df, symbol) \ No newline at end of file diff --git a/core/strategies/performance_scorer.py b/core/strategies/performance_scorer.py new file mode 100644 index 0000000..0b1fed9 --- /dev/null +++ b/core/strategies/performance_scorer.py @@ -0,0 +1,370 @@ +# core/strategies/performance_scorer.py +""" +๐ Performance Scoring System for Strategy Ranking + +This module ranks strategy/instrument combinations based on multiple performance metrics +to enable automatic strategy switching. + +Features: +- Multi-metric performance scoring +- Risk-adjusted returns calculation +- Recent performance weighting +- Strategy/instrument compatibility scoring +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from typing import Dict, List, Any +import logging + +logger = logging.getLogger(__name__) + +class PerformanceScorer: + """ + Scores and ranks strategy/instrument combinations for automatic switching + """ + + def __init__(self): + # Scoring weights for different metrics + self.scoring_weights = { + 'profitability': 0.30, # 30% weight + 'risk_control': 0.25, # 25% weight + 'consistency': 0.20, # 20% weight + 'activity_level': 0.15, # 15% weight + 'market_fit': 0.10 # 10% weight + } + + def calculate_performance_score(self, backtest_results: dict, market_condition: dict, + strategy_id: str, symbol: str) -> dict: + """ + Calculate comprehensive performance score for a strategy/instrument combination + + Args: + backtest_results: Backtest results from enhanced engine + market_condition: Market condition analysis + strategy_id: Strategy identifier + symbol: Trading symbol + + Returns: + dict: Performance score and component metrics + """ + try: + # Extract key metrics + metrics = self._extract_metrics(backtest_results) + + # Calculate component scores + profitability_score = self._calculate_profitability_score(metrics) + risk_score = self._calculate_risk_score(metrics) + consistency_score = self._calculate_consistency_score(metrics) + activity_score = self._calculate_activity_score(metrics) + market_fit_score = self._calculate_market_fit_score(strategy_id, symbol, market_condition) + + # Weighted composite score + composite_score = ( + profitability_score * self.scoring_weights['profitability'] + + risk_score * self.scoring_weights['risk_control'] + + consistency_score * self.scoring_weights['consistency'] + + activity_score * self.scoring_weights['activity_level'] + + market_fit_score * self.scoring_weights['market_fit'] + ) + + return { + 'composite_score': composite_score, + 'components': { + 'profitability': profitability_score, + 'risk_control': risk_score, + 'consistency': consistency_score, + 'activity_level': activity_score, + 'market_fit': market_fit_score + }, + 'metrics': metrics, + 'strategy_id': strategy_id, + 'symbol': symbol, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"Performance scoring error: {e}") + return self._default_score(strategy_id, symbol) + + def _extract_metrics(self, backtest_results: dict) -> dict: + """Extract key performance metrics from backtest results""" + return { + 'total_profit': backtest_results.get('total_profit_usd', 0), + 'net_profit': backtest_results.get('net_profit_after_costs', 0), + 'gross_profit': backtest_results.get('total_profit_usd', 0), + 'total_trades': backtest_results.get('total_trades', 0), + 'wins': backtest_results.get('wins', 0), + 'losses': backtest_results.get('losses', 0), + 'win_rate': backtest_results.get('win_rate_percent', 0), + 'max_drawdown': abs(backtest_results.get('max_drawdown_percent', 0)), + 'avg_win': backtest_results.get('avg_win', 0), + 'avg_loss': backtest_results.get('avg_loss', 0), + 'spread_costs': abs(backtest_results.get('total_spread_costs', 0)), + 'final_capital': backtest_results.get('final_capital', 10000), + 'initial_capital': backtest_results.get('initial_capital', 10000) + } + + def _calculate_profitability_score(self, metrics: dict) -> float: + """Calculate profitability component score (0-1)""" + net_profit = metrics['net_profit'] + total_trades = metrics['total_trades'] + spread_costs = metrics['spread_costs'] + gross_profit = metrics['gross_profit'] + + # Base score on net profit + if total_trades == 0: + return 0.5 # Neutral score for no trades + + # Profit factor (gross profit / absolute losses) + if gross_profit <= 0: + profit_factor = 0 + else: + absolute_losses = abs(gross_profit - net_profit - spread_costs) + profit_factor = gross_profit / (absolute_losses + 1e-10) + + # Normalize profit factor (capped at 5 for practical purposes) + normalized_profit_factor = min(1.0, profit_factor / 5.0) + + # Win rate component (30-70% is good range) + win_rate = metrics['win_rate'] / 100 # Convert to decimal + win_rate_score = 1 - abs(win_rate - 0.5) * 2 # Peak at 50%, but we want higher win rates + # Adjust for win rates above 50% + if win_rate > 0.5: + win_rate_score = min(1.0, win_rate_score + (win_rate - 0.5) * 2) + + # Combine factors + profitability_score = (normalized_profit_factor * 0.6 + win_rate_score * 0.4) + + return min(1.0, max(0.0, profitability_score)) + + def _calculate_risk_score(self, metrics: dict) -> float: + """Calculate risk control component score (0-1)""" + max_drawdown = metrics['max_drawdown'] + total_trades = metrics['total_trades'] + + if total_trades == 0: + return 0.5 # Neutral score + + # Drawdown score (lower is better) + # Drawdown under 10% = full score, 50%+ = 0 score + if max_drawdown <= 10: + drawdown_score = 1.0 + elif max_drawdown >= 50: + drawdown_score = 0.0 + else: + # Linear interpolation between 10% and 50% + drawdown_score = 1.0 - (max_drawdown - 10) / 40 + + # Risk/reward ratio (estimated from avg win/loss) + avg_win = abs(metrics['avg_win']) + avg_loss = abs(metrics['avg_loss']) + + if avg_loss <= 0: + risk_reward_score = 1.0 if avg_win > 0 else 0.5 + else: + risk_reward_ratio = avg_win / avg_loss + # 1:1 = 0.5 score, 1:2 = 0.75 score, 1:3+ = 1.0 score + risk_reward_score = min(1.0, risk_reward_ratio / 3.0) + + # Combine risk metrics + risk_score = (drawdown_score * 0.7 + risk_reward_score * 0.3) + + return min(1.0, max(0.0, risk_score)) + + def _calculate_consistency_score(self, metrics: dict) -> float: + """Calculate consistency component score (0-1)""" + total_trades = metrics['total_trades'] + wins = metrics['wins'] + losses = metrics['losses'] + + if total_trades == 0: + return 0.5 # Neutral score + + # Trade frequency (want reasonable number of trades) + # 20-100 trades is good range + if total_trades >= 100: + frequency_score = 1.0 + elif total_trades >= 20: + # Linear interpolation + frequency_score = 0.5 + (total_trades - 20) / 80 * 0.5 + else: + # Below 20 trades, score decreases + frequency_score = max(0.0, total_trades / 20 * 0.5) + + # Profit consistency (how steady the profits are) + # This is a simplified measure - in reality we'd look at equity curve + profit_per_trade = metrics['net_profit'] / max(1, total_trades) + # Positive average = good, but we also want reasonable consistency + profit_consistency = 1.0 if profit_per_trade > 0 else 0.5 if profit_per_trade >= -10 else 0.0 + + # Win/loss distribution (more consistent wins = better) + if wins + losses == 0: + distribution_score = 0.5 + else: + win_ratio = wins / (wins + losses) + # Score higher for win ratios closer to 0.4-0.7 range (realistic) + distribution_score = 1 - abs(win_ratio - 0.55) * 2 + + # Combine consistency factors + consistency_score = (frequency_score * 0.4 + profit_consistency * 0.4 + distribution_score * 0.2) + + return min(1.0, max(0.0, consistency_score)) + + def _calculate_activity_score(self, metrics: dict) -> float: + """Calculate trading activity component score (0-1)""" + total_trades = metrics['total_trades'] + + if total_trades == 0: + return 0.0 + + # Activity score based on trade count (20-200 trades is ideal) + if total_trades >= 200: + activity_score = 1.0 + elif total_trades >= 20: + # Linear interpolation + activity_score = (total_trades - 20) / 180 + else: + # Below 20 trades, very low score + activity_score = total_trades / 20 * 0.2 + + return min(1.0, max(0.0, activity_score)) + + def _calculate_market_fit_score(self, strategy_id: str, symbol: str, market_condition: dict) -> float: + """Calculate market condition fit score (0-1)""" + try: + instrument_type = market_condition.get('instrument_type', 'FOREX') + market_condition_type = market_condition.get('market_condition', 'ranging') + confidence = market_condition.get('confidence', 0.5) + + # Strategy-to-market compatibility mapping + compatibility_map = { + # Indices + ('INDEX_BREAKOUT_PRO', 'INDICES'): 1.0, + ('INDEX_MOMENTUM', 'INDICES'): 0.9, + ('TURTLE_BREAKOUT', 'INDICES'): 0.7, + + # Forex + ('MA_CROSSOVER', 'FOREX'): 0.9, + ('RSI_CROSSOVER', 'FOREX'): 0.8, + ('BOLLINGER_REVERSION', 'FOREX'): 0.85, + ('TURTLE_BREAKOUT', 'FOREX'): 0.7, + + # Gold + ('QUANTUM_VELOCITY', 'GOLD'): 1.0, + ('BOLLINGER_REVERSION', 'GOLD'): 0.8, + ('MERCY_EDGE', 'GOLD'): 0.9, + + # Crypto + ('QUANTUMBOTX_CRYPTO', 'CRYPTO'): 1.0, + ('DYNAMIC_BREAKOUT', 'CRYPTO'): 0.9, + ('QUANTUMBOTX_HYBRID', 'CRYPTO'): 0.8, + } + + # Base compatibility score + base_score = compatibility_map.get((strategy_id, instrument_type), 0.5) + + # Adjust for market condition + strategy_preference = { + 'MA_CROSSOVER': 'trending', + 'RSI_CROSSOVER': 'ranging', + 'TURTLE_BREAKOUT': 'trending', + 'BOLLINGER_REVERSION': 'ranging', + 'INDEX_BREAKOUT_PRO': 'trending', + 'INDEX_MOMENTUM': 'trending', + 'QUANTUM_VELOCITY': 'trending', + 'DYNAMIC_BREAKOUT': 'trending', + 'QUANTUMBOTX_CRYPTO': 'trending', + 'QUANTUMBOTX_HYBRID': 'both' + } + + preferred_condition = strategy_preference.get(strategy_id, 'both') + + if preferred_condition == 'both': + condition_adjustment = 1.0 # Works in both conditions + elif preferred_condition == market_condition_type: + condition_adjustment = 1.0 # Perfect match + else: + condition_adjustment = 0.7 # Still works but less optimal + + # Combine base score with condition adjustment and confidence + market_fit_score = base_score * condition_adjustment * confidence + + return min(1.0, max(0.0, market_fit_score)) + + except Exception: + return 0.5 # Neutral score on error + + def _default_score(self, strategy_id: str, symbol: str) -> dict: + """Return default score when calculation fails""" + return { + 'composite_score': 0.5, + 'components': { + 'profitability': 0.5, + 'risk_control': 0.5, + 'consistency': 0.5, + 'activity_level': 0.5, + 'market_fit': 0.5 + }, + 'metrics': {}, + 'strategy_id': strategy_id, + 'symbol': symbol, + 'timestamp': datetime.now().isoformat() + } + + def rank_strategy_combinations(self, performance_scores: List[dict]) -> List[dict]: + """ + Rank strategy/instrument combinations by composite score + + Args: + performance_scores: List of performance score dictionaries + + Returns: + List[dict]: Ranked combinations sorted by composite score (highest first) + """ + # Sort by composite score (descending) + ranked_combinations = sorted( + performance_scores, + key=lambda x: x['composite_score'], + reverse=True + ) + + # Add rank numbers + for i, combination in enumerate(ranked_combinations): + combination['rank'] = i + 1 + + return ranked_combinations + +# Global instance +performance_scorer = PerformanceScorer() + +def calculate_strategy_score(backtest_results: dict, market_condition: dict, + strategy_id: str, symbol: str) -> dict: + """ + Convenience function to calculate strategy performance score + + Args: + backtest_results: Backtest results + market_condition: Market condition analysis + strategy_id: Strategy identifier + symbol: Trading symbol + + Returns: + dict: Performance score and ranking + """ + return performance_scorer.calculate_performance_score( + backtest_results, market_condition, strategy_id, symbol + ) + +def rank_strategies(performance_scores: List[dict]) -> List[dict]: + """ + Convenience function to rank strategy combinations + + Args: + performance_scores: List of performance scores + + Returns: + List[dict]: Ranked strategy combinations + """ + return performance_scorer.rank_strategy_combinations(performance_scores) \ No newline at end of file diff --git a/core/strategies/strategy_map.py b/core/strategies/strategy_map.py index f0245a9..5e206f9 100644 --- a/core/strategies/strategy_map.py +++ b/core/strategies/strategy_map.py @@ -15,7 +15,6 @@ from .dynamic_breakout import DynamicBreakoutStrategy from .index_momentum import IndexMomentumStrategy from .index_breakout_pro import IndexBreakoutProStrategy from .beginner_defaults import BEGINNER_DEFAULTS -from .strategy_selector import StrategySelector STRATEGY_MAP = { 'MA_CROSSOVER': MACrossoverStrategy, diff --git a/core/strategies/strategy_switcher.py b/core/strategies/strategy_switcher.py new file mode 100644 index 0000000..039ae4d --- /dev/null +++ b/core/strategies/strategy_switcher.py @@ -0,0 +1,426 @@ +# core/strategies/strategy_switcher.py +""" +๐ Automatic Strategy Switching Logic + +This module implements the core logic for automatically switching between +strategy/instrument combinations based on performance scores and market conditions. + +Features: +- Automatic strategy switching based on performance rankings +- Market condition-aware switching +- Performance threshold monitoring +- Switching cooldown periods +- Historical performance tracking +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple, Any +import logging +import json +import os + +from .market_condition_detector import get_market_condition +from .performance_scorer import calculate_strategy_score, rank_strategies +from ..backtesting.enhanced_engine import run_enhanced_backtest +from .strategy_map import STRATEGY_MAP + +logger = logging.getLogger(__name__) + +class StrategySwitcher: + """ + Automatic strategy switching system + """ + + def __init__(self, config_file: str = 'strategy_switcher_config.json'): + self.config_file = config_file + self.config = self._load_config() + self.performance_history = [] + self.last_switch_time = None + self.current_strategy = None + self.current_symbol = None + self.switch_log = [] + + # Default instruments to monitor + self.monitored_instruments = self.config.get('monitored_instruments', [ + 'US500', 'EURUSD', 'GBPUSD', 'XAUUSD', 'BTCUSD' + ]) + + # Default strategies to test + self.test_strategies = self.config.get('test_strategies', [ + 'INDEX_BREAKOUT_PRO', 'MA_CROSSOVER', 'RSI_CROSSOVER', + 'TURTLE_BREAKOUT', 'QUANTUMBOTX_HYBRID' + ]) + + def _load_config(self) -> dict: + """Load configuration from file or use defaults""" + default_config = { + 'switching_cooldown_hours': 24, + 'performance_evaluation_period': 500, # bars + 'min_performance_score': 0.6, + 'switch_threshold': 0.1, # Minimum score improvement to trigger switch + 'data_directory': 'lab/backtest_data', + 'monitored_instruments': ['US500', 'EURUSD', 'GBPUSD', 'XAUUSD', 'BTCUSD'], + 'test_strategies': [ + 'INDEX_BREAKOUT_PRO', 'MA_CROSSOVER', 'RSI_CROSSOVER', + 'TURTLE_BREAKOUT', 'QUANTUMBOTX_HYBRID' + ] + } + + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r') as f: + config = json.load(f) + # Merge with defaults + default_config.update(config) + except Exception as e: + logger.warning(f"Could not load config file {self.config_file}: {e}") + + return default_config + + def evaluate_and_switch(self, current_data: Dict[str, pd.DataFrame]) -> Optional[Dict[str, Any]]: + """ + Evaluate all strategy/instrument combinations and switch if needed + + Args: + current_data: Dictionary of symbol -> DataFrame with current market data + + Returns: + dict: Switching decision or None if no switch needed + """ + try: + # Check if we're in cooldown period + if self._in_cooldown(): + logger.info("Strategy switcher in cooldown period") + return None + + # Evaluate all combinations + performance_scores = self._evaluate_all_combinations(current_data) + + if not performance_scores: + logger.warning("No performance scores calculated") + return None + + # Rank combinations + ranked_combinations = rank_strategies(performance_scores) + + # Determine if switch is needed + switch_decision = self._determine_switch(ranked_combinations) + + if switch_decision: + # Log the switch + self._log_switch(switch_decision) + + # Update current strategy + self.current_strategy = switch_decision['new_strategy'] + self.current_symbol = switch_decision['new_symbol'] + self.last_switch_time = datetime.now() + + return switch_decision + else: + logger.info("No strategy switch needed at this time") + return None + + except Exception as e: + logger.error(f"Strategy switching evaluation error: {e}") + return None + + def _evaluate_all_combinations(self, current_data: Dict[str, pd.DataFrame]) -> List[dict]: + """Evaluate all strategy/instrument combinations""" + performance_scores = [] + + for symbol in self.monitored_instruments: + if symbol not in current_data: + logger.warning(f"No data available for {symbol}") + continue + + df = current_data[symbol] + if df.empty: + logger.warning(f"Empty data for {symbol}") + continue + + # Get market condition for this symbol + market_condition = get_market_condition(df, symbol) + + # Test each strategy on this symbol + for strategy_id in self.test_strategies: + if strategy_id not in STRATEGY_MAP: + logger.warning(f"Strategy {strategy_id} not found in STRATEGY_MAP") + continue + + try: + # Run backtest with recent data + test_df = df.tail(self.config['performance_evaluation_period']).copy() + + # Get strategy-specific parameters + strategy_params = self._get_strategy_parameters(strategy_id, symbol) + + # Run backtest + backtest_results = run_enhanced_backtest( + strategy_id, + strategy_params, + test_df, + symbol_name=symbol + ) + + if 'error' in backtest_results: + logger.warning(f"Backtest error for {strategy_id}/{symbol}: {backtest_results['error']}") + continue + + # Calculate performance score + score = calculate_strategy_score( + backtest_results, market_condition, strategy_id, symbol + ) + + performance_scores.append(score) + + except Exception as e: + logger.error(f"Error evaluating {strategy_id}/{symbol}: {e}") + continue + + return performance_scores + + def _get_strategy_parameters(self, strategy_id: str, symbol: str) -> dict: + """Get appropriate parameters for strategy and symbol combination""" + # Base parameters for different strategy types + base_params = { + 'INDEX_BREAKOUT_PRO': { + 'breakout_period': 25, + 'volume_surge_multiplier': 1.8, + 'confirmation_candles': 3, + 'atr_multiplier_sl': 2.5, + 'atr_multiplier_tp': 3.5, + 'min_breakout_size': 0.25, + 'risk_percent': 0.5 + }, + 'MA_CROSSOVER': { + 'fast_period': 12, + 'slow_period': 26, + 'risk_percent': 0.8, + 'sl_atr_multiplier': 1.8, + 'tp_atr_multiplier': 3.6 + }, + 'RSI_CROSSOVER': { + 'rsi_period': 14, + 'rsi_ma_period': 7, + 'trend_filter_period': 30, + 'risk_percent': 1.0, + 'sl_atr_multiplier': 2.0, + 'tp_atr_multiplier': 4.0 + }, + 'TURTLE_BREAKOUT': { + 'entry_period': 20, + 'exit_period': 10, + 'risk_percent': 1.0, + 'sl_atr_multiplier': 2.0, + 'tp_atr_multiplier': 4.0 + }, + 'QUANTUMBOTX_HYBRID': { + 'adx_period': 14, + 'adx_threshold': 25, + 'ma_fast_period': 12, + 'ma_slow_period': 26, + 'bb_length': 20, + 'bb_std': 2.0, + 'trend_filter_period': 50, + 'risk_percent': 1.0, + 'sl_atr_multiplier': 2.0, + 'tp_atr_multiplier': 4.0 + } + } + + # Get base parameters + params = base_params.get(strategy_id, {}).copy() + + # Adjust for symbol type + symbol_upper = symbol.upper() + if any(index in symbol_upper for index in ['US30', 'US100', 'US500', 'DE30']): + # Index-specific adjustments + params['risk_percent'] = min(params.get('risk_percent', 1.0), 0.5) + if 'sl_atr_multiplier' in params: + params['sl_atr_multiplier'] = min(params['sl_atr_multiplier'], 2.0) + + elif 'XAU' in symbol_upper: + # Gold-specific adjustments + params['risk_percent'] = min(params.get('risk_percent', 1.0), 0.3) + + elif any(crypto in symbol_upper for crypto in ['BTC', 'ETH']): + # Crypto-specific adjustments + params['risk_percent'] = min(params.get('risk_percent', 1.0), 0.5) + + return params + + def _determine_switch(self, ranked_combinations: List[dict]) -> Optional[dict]: + """Determine if a strategy switch is needed""" + if not ranked_combinations: + return None + + # Get top-ranked combination + top_combination = ranked_combinations[0] + top_score = top_combination['composite_score'] + + # Check if score meets minimum threshold + if top_score < self.config['min_performance_score']: + logger.info(f"Top score {top_score:.3f} below minimum threshold {self.config['min_performance_score']}") + return None + + # If this is the first evaluation, switch to top performer + if self.current_strategy is None or self.current_symbol is None: + return { + 'action': 'INITIAL_SWITCH', + 'new_strategy': top_combination['strategy_id'], + 'new_symbol': top_combination['symbol'], + 'reason': f'Initial setup - top performer: {top_score:.3f}', + 'confidence': top_score, + 'performance_metrics': top_combination['metrics'] + } + + # Check if we're already using the top combination + if (top_combination['strategy_id'] == self.current_strategy and + top_combination['symbol'] == self.current_symbol): + logger.info(f"Already using top performer {self.current_strategy}/{self.current_symbol}") + return None + + # Get current combination score + current_score = self._get_current_combination_score(ranked_combinations) + + if current_score is None: + # Current combination not in evaluation - switch to top + improvement = top_score # Full score as improvement + else: + improvement = top_score - current_score + + # Check if improvement meets threshold + if improvement >= self.config['switch_threshold']: + return { + 'action': 'STRATEGY_SWITCH', + 'new_strategy': top_combination['strategy_id'], + 'new_symbol': top_combination['symbol'], + 'old_strategy': self.current_strategy, + 'old_symbol': self.current_symbol, + 'reason': f'Score improvement: {improvement:.3f} (from {current_score:.3f} to {top_score:.3f})', + 'confidence': top_score, + 'improvement': improvement, + 'performance_metrics': top_combination['metrics'] + } + else: + logger.info(f"Score improvement {improvement:.3f} below threshold {self.config['switch_threshold']}") + return None + + def _get_current_combination_score(self, ranked_combinations: List[dict]) -> Optional[float]: + """Get performance score for current strategy/symbol combination""" + if self.current_strategy is None or self.current_symbol is None: + return None + + for combination in ranked_combinations: + if (combination['strategy_id'] == self.current_strategy and + combination['symbol'] == self.current_symbol): + return combination['composite_score'] + + return None + + def _in_cooldown(self) -> bool: + """Check if switching is in cooldown period""" + if self.last_switch_time is None: + return False + + cooldown_hours = self.config.get('switching_cooldown_hours', 24) + cooldown_end = self.last_switch_time + timedelta(hours=cooldown_hours) + + return datetime.now() < cooldown_end + + def _log_switch(self, switch_decision: dict): + """Log strategy switch decision""" + log_entry = { + 'timestamp': datetime.now().isoformat(), + 'decision': switch_decision + } + + self.switch_log.append(log_entry) + + # Keep only recent logs + if len(self.switch_log) > 100: + self.switch_log = self.switch_log[-50:] + + logger.info(f"Strategy switch logged: {switch_decision}") + + # Create notification for the switch + self._create_switch_notification(switch_decision) + + def _create_switch_notification(self, switch_decision: dict): + """Create a notification for strategy switch events""" + try: + # Import here to avoid circular imports + from core.db.queries import add_history_log + + if switch_decision['action'] == 'INITIAL_SWITCH': + action = "STRATEGY_SWITCHER_INITIALIZED" + details = f"Strategy switcher initialized with {switch_decision['new_strategy']}/{switch_decision['new_symbol']} (Score: {switch_decision['confidence']:.3f})" + else: # STRATEGY_SWITCH + action = "STRATEGY_SWITCHED" + details = f"Switched from {switch_decision['old_strategy']}/{switch_decision['old_symbol']} to {switch_decision['new_strategy']}/{switch_decision['new_symbol']} (Improvement: +{switch_decision['improvement']:.3f})" + + # Create the notification in the trade_history table + # Using bot_id=0 as this is a system-level notification not tied to a specific bot + add_history_log( + bot_id=0, + action=action, + details=details, + is_notification=True + ) + except Exception as e: + logger.error(f"Failed to create switch notification: {e}") + + def get_status(self) -> dict: + """Get current strategy switcher status""" + return { + 'current_strategy': self.current_strategy, + 'current_symbol': self.current_symbol, + 'last_switch_time': self.last_switch_time.isoformat() if self.last_switch_time else None, + 'in_cooldown': self._in_cooldown(), + 'monitored_instruments': self.monitored_instruments, + 'test_strategies': self.test_strategies, + 'performance_history_count': len(self.performance_history), + 'switch_log_count': len(self.switch_log) + } + + def get_recent_switches(self, count: int = 5) -> List[dict]: + """Get recent switch history""" + return self.switch_log[-count:] if self.switch_log else [] + +# Global instance +strategy_switcher = StrategySwitcher() + +def evaluate_strategy_switch(current_data: Dict[str, pd.DataFrame]) -> Optional[Dict[str, Any]]: + """ + Convenience function to evaluate and potentially switch strategies + + Args: + current_data: Dictionary of symbol -> DataFrame with market data + + Returns: + dict: Switch decision or None + """ + return strategy_switcher.evaluate_and_switch(current_data) + +def get_switcher_status() -> dict: + """ + Get strategy switcher status + + Returns: + dict: Current status information + """ + return strategy_switcher.get_status() + +def get_recent_switches(count: int = 5) -> List[dict]: + """ + Get recent strategy switches + + Args: + count: Number of recent switches to return + + Returns: + List[dict]: Recent switch history + """ + return strategy_switcher.get_recent_switches(count) \ No newline at end of file diff --git a/core/utils/symbols.py b/core/utils/symbols.py index 6b8cfb0..55c326b 100644 --- a/core/utils/symbols.py +++ b/core/utils/symbols.py @@ -8,8 +8,8 @@ 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'] +# Contoh path: "Stocks\\USA2\\#AAPL" -> akan cocok dengan 'stocks'. +STOCK_KEYWORDS = ['stocks', 'stock', 'share', 'equity', 'saham'] # Prefix untuk Forex, berdasarkan output Anda: "path": "Forex\\AUDCAD" FOREX_PREFIX = 'forex\\' @@ -19,7 +19,7 @@ def get_all_symbols_from_mt5(): try: # Koneksi sudah diinisialisasi saat aplikasi pertama kali berjalan. # Kita tidak perlu melakukan initialize() di sini lagi. - symbols = mt5.symbols_get() + symbols = mt5.symbols_get() # pyright: ignore if symbols: return symbols logger.warning("mt5.symbols_get() tidak mengembalikan simbol apapun.") @@ -41,8 +41,13 @@ def get_stock_symbols(limit=20): for s in all_symbols: if any(keyword in s.path.lower() for keyword in STOCK_KEYWORDS): + # Pastikan simbol diaktifkan di Market Watch + if not mt5.symbol_select(s.name, True): # pyright: ignore + logger.warning(f"Gagal mengaktifkan simbol {s.name} di Market Watch") + continue + # Ambil info detail untuk mendapatkan volume - info = mt5.symbol_info(s.name) + info = mt5.symbol_info(s.name) # pyright: ignore if info: stock_details.append({ "name": s.name, @@ -71,7 +76,12 @@ def get_forex_symbols(): for s in all_symbols: if s.path.lower().startswith(FOREX_PREFIX): - tick = mt5.symbol_info_tick(s.name) + # Pastikan simbol diaktifkan di Market Watch + if not mt5.symbol_select(s.name, True): # pyright: ignore + logger.warning(f"Gagal mengaktifkan simbol {s.name} di Market Watch") + continue + + tick = mt5.symbol_info_tick(s.name) # pyright: ignore if tick: forex_list.append({ "name": s.name, @@ -82,4 +92,4 @@ def get_forex_symbols(): "digits": s.digits, }) - return forex_list + return forex_list \ No newline at end of file diff --git a/docs/beginner_guide.md b/docs/beginner_guide.md new file mode 100644 index 0000000..bf9934d --- /dev/null +++ b/docs/beginner_guide.md @@ -0,0 +1,233 @@ +# ๐ QuantumBotX Beginner's Guide + +Welcome to QuantumBotX, your personal AI-powered trading assistant! This guide will help you get started with the platform, understand its core concepts, and begin your journey toward automated trading success. + +## ๐ Getting Started + +### What is QuantumBotX? +QuantumBotX is an intelligent, modular trading bot platform built for MetaTrader 5 (MT5). It combines professional trading strategies with beginner-friendly features, educational tools, and cultural awareness to create a comprehensive trading experience. + +### Key Features for Beginners: +- ๐ฏ **Progressive Learning Path**: Structured learning from Week 1 to Month 3 +- ๐ **Strategy Difficulty Ratings**: 2-12 complexity scale with automatic recommendations +- ๐ก๏ธ **Beginner Safeguards**: Parameter validation prevents dangerous settings +- ๐ **Educational Framework**: Every setting explained in plain English +- ๐ค **ATR-Based Risk Management**: Automatic position sizing that adapts to market volatility + +## ๐ ๏ธ Installation & Setup + +### Prerequisites +1. **MetaTrader 5 Terminal**: Must be installed and running on your computer +2. **MT5 Account**: Demo or live account with your broker +3. **Python 3.10+**: Required for running the application + +### Installation Steps +1. Clone the repository: + ```bash + git clone https://github.com/rebarakaz/quantumbotx.git + cd quantumbotx + ``` + +2. Create a Python virtual environment: + ```bash + python -m venv venv + # On Windows + .\venv\Scripts\activate + # On macOS/Linux + source venv/bin/activate + ``` + +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +4. Configure your environment: + ```bash + cp .env.example .env + # Edit the .env file with your MT5 credentials + ``` + +5. Run the application: + ```bash + python run.py + ``` + +## ๐ฎ Navigating the Dashboard + +Once the application is running, open your web browser and go to `http://127.0.0.1:5000` to access the dashboard. + +### Main Navigation +- **Dashboard**: Overview of your trading performance and statistics +- **Trading Bots**: Create, manage, and monitor your trading bots +- **Backtester**: Test strategies with historical data +- **AI Mentor**: Get personalized trading advice and education +- **Strategy Switcher**: Automatic strategy optimization system +- **Portfolio**: Track your overall trading performance +- **Market Data**: View real-time market information + +## ๐ค Creating Your First Trading Bot + +### Step 1: Choose a Strategy +For beginners, we recommend starting with these simple strategies: +- **MA_CROSSOVER**: Simple moving average crossover (Complexity: 2/10) +- **RSI_CROSSOVER**: Relative Strength Index momentum strategy (Complexity: 3/10) +- **TURTLE_BREAKOUT**: Breakout trading strategy (Complexity: 2/10) + +### Step 2: Set Up Your Bot +1. Navigate to the "Trading Bots" page +2. Click "Buat Bot Baru" +3. Fill in the basic information: + - **Bot Name**: Give your bot a descriptive name + - **Market**: Choose your trading pair (e.g., EURUSD, XAUUSD) + - **Risk per Trade**: Start with 1.0% (never exceed 2%) + - **SL/TP Multipliers**: Use the default ATR-based values (2.0 for SL, 4.0 for TP) + - **Timeframe**: H1 (1 hour) is recommended for beginners + - **Check Interval**: 60 seconds is fine for most strategies + +### Step 3: Configure Strategy Parameters +For your first bot, use the beginner-friendly defaults: +- **MA_CROSSOVER**: Fast period 10, Slow period 30 +- **RSI_CROSSOVER**: RSI period 14, MA period 7 +- **TURTLE_BREAKOUT**: Entry period 15, Exit period 8 + +### Step 4: Start Trading +1. Save your bot configuration +2. Click the "Start" button to begin trading +3. Monitor your bot's performance on the dashboard + +## ๐ Understanding Risk Management + +### ATR-Based Position Sizing +QuantumBotX uses Average True Range (ATR) to automatically adjust your position size based on market volatility: + +- **Low Volatility**: Larger positions (safe markets) +- **High Volatility**: Smaller positions (protects your capital) + +### Special Protections +- **Gold (XAUUSD) Protection**: Ultra-conservative settings automatically applied +- **Crypto Protection**: Weekend mode and volatility filters for cryptocurrencies +- **Emergency Brake**: System automatically skips dangerous trades + +## ๐ Learning Path for Beginners + +### Week 1-2: Foundation +- **Strategy**: MA_CROSSOVER +- **Focus**: Learn basic trend following +- **Practice**: Demo trading with 0.01 lots +- **Goal**: Understand moving averages and crossovers + +### Week 3-4: Momentum +- **Strategy**: RSI_CROSSOVER +- **Focus**: Learn momentum analysis +- **Practice**: Combine with moving averages +- **Goal**: Understand RSI and momentum concepts + +### Week 5-6: Breakouts +- **Strategy**: TURTLE_BREAKOUT +- **Focus**: Learn breakout trading +- **Practice**: Practice entry/exit timing +- **Goal**: Identify support/resistance levels + +## ๐งช Backtesting Your Strategies + +Before trading live, always backtest your strategies. Backtesting allows you to test your strategies on historical data to see how they would have performed. + +### How Backtesting Works +Backtesting in QuantumBotX works completely offline - you don't need an internet connection or MT5 terminal running. All you need is historical data in CSV format. + +### Getting Historical Data +QuantumBotX provides a convenient script to download historical data from your MT5 account: + +1. Ensure MT5 terminal is running and you're logged in +2. Configure your MT5 credentials in the `.env` file +3. Run the download script: + ```bash + python lab/download_data.py + ``` + +This script will automatically download data for popular symbols and save CSV files to the `lab/backtest_data/` directory. + +### Manual Data Download +If you prefer to download data manually or have data from other sources: + +1. Navigate to the `lab/backtest_data` directory +2. Place your CSV files there with the naming convention: `{SYMBOL}_{TIMEFRAME}_data.csv` (e.g., `EURUSD_H1_data.csv`) +3. Ensure your CSV files have the required columns: time, open, high, low, close, volume + +### Running a Backtest +1. Go to the "Backtester" page in the dashboard +2. Select your strategy and market +3. Choose a historical data period +4. Run the backtest +5. Review the results: + - Profit factor (aim for >1.5) + - Win rate (aim for >55%) + - Maximum drawdown (<30% is acceptable) + - Total trades executed + +### Interpreting Backtest Results +- **Profit Factor**: The ratio of gross profits to gross losses (>1.5 is good) +- **Win Rate**: Percentage of winning trades (>55% is acceptable) +- **Maximum Drawdown**: Largest peak-to-trough decline (<30% is acceptable) +- **Sharpe Ratio**: Risk-adjusted return (higher is better) + +## ๐ฏ Best Practices for Success + +### For Demo Trading +1. **Treat Demo Like Real Money**: Make emotional decisions as if you were risking real capital +2. **Keep a Trading Journal**: Record every trade and your reasoning +3. **Focus on One Strategy**: Master it completely before trying others +4. **Review Every Trade**: Analyze both wins and losses + +### Risk Management Rules +1. **Never risk more than 2%** of your account per trade +2. **Always use stop losses** - no exceptions +3. **Maintain a 1:2 risk-reward ratio** minimum +4. **Diversify across markets** but not strategies + +### Moving to Live Trading +1. **Demo for at least 30 days** with consistent profitability +2. **Start with micro lots** (0.01) even with a live account +3. **Monitor emotions** - fear and greed are your biggest enemies +4. **Gradually increase position size** only after consistent profits + +## ๐ Cultural Features + +QuantumBotX automatically adapts to religious holidays: + +### Ramadan Trading Mode +- Automatically activates during Islamic holy month +- Respects prayer times with trading pauses +- Includes Zakat calculator and charity tracker +- Features patience reminders and optimal trading hours + +### Christmas Trading Mode +- Activates during Christmas period +- Reduces risk during holiday season +- Applies special UI themes and decorations + +## ๐ Getting Help + +### AI Mentor +The AI Mentor provides personalized trading advice: +- Click the AI button on the dashboard +- Ask questions about strategies or market conditions +- Get educational explanations for trading concepts + +### Error Handling +If you encounter issues: +1. Check the application logs in the `logs/` directory +2. Ensure MT5 terminal is running +3. Verify your `.env` configuration +4. Restart the application if needed + +## ๐ Next Steps + +After mastering the basics: +1. **Explore Advanced Strategies**: Try BOLLINGER_REVERSION and PULSE_SYNC +2. **Use Strategy Switcher**: Enable automatic strategy optimization +3. **Customize Parameters**: Fine-tune strategies for specific markets +4. **Expand to Multiple Markets**: Trade Forex, Gold, and Crypto + +Remember: Trading involves significant risk. Always start with demo accounts and never risk money you cannot afford to lose. QuantumBotX is designed to help you learn and trade more effectively, but success depends on your discipline and risk management. \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..fdfb92d --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,233 @@ +# โ QuantumBotX FAQ + +Frequently asked questions about using QuantumBotX. + +## ๐ General Questions + +### What is QuantumBotX? +QuantumBotX is an AI-powered, modular trading bot platform designed for MetaTrader 5 (MT5). It provides professional trading strategies with beginner-friendly features, educational tools, and cultural awareness. + +### Do I need programming knowledge to use QuantumBotX? +No programming knowledge is required! QuantumBotX is designed to be user-friendly with a web-based interface. However, basic computer skills are helpful. + +### Which brokers are supported? +QuantumBotX works with any broker that supports MetaTrader 5, including: +- XM Global +- Exness +- Alpari +- MetaQuotes (demo) +- And many others + +The system automatically migrates symbols between brokers. + +### Can I use QuantumBotX with a demo account? +Yes! In fact, we strongly recommend starting with a demo account to learn the system without risking real money. + +## ๐ฐ Trading Questions + +### How much money do I need to start? +You can start with as little as $100, but we recommend at least $1,000 for more flexibility. For demo accounts, no real money is required. + +### What markets can I trade? +QuantumBotX supports multiple markets: +- **Forex**: EURUSD, GBPUSD, USDJPY, etc. +- **Gold**: XAUUSD +- **Indices**: US30, US500, DE30, etc. +- **Cryptocurrencies**: BTCUSD, ETHUSD (with special weekend mode) + +### How does the risk management work? +QuantumBotX uses ATR (Average True Range) based position sizing: +- Automatically calculates lot sizes based on market volatility +- Caps risk at 1-2% per trade +- Provides special protection for volatile instruments like Gold +- Includes emergency brake system to skip dangerous trades + +### What are the recommended settings for beginners? +For beginners, we recommend: +- Risk per trade: 1.0% +- Stop Loss: 2.0 x ATR +- Take Profit: 4.0 x ATR +- Timeframe: H1 (1 hour) +- Starting strategy: MA_CROSSOVER +- Lot size: 0.01 (micro lots) + +## ๐ค Bot Management + +### How many bots can I run simultaneously? +You can run up to 4 bots simultaneously, which is optimal for most retail traders. + +### Can I modify bot settings while it's running? +Yes, you can modify most settings while the bot is running. However, for safety reasons, some core parameters require the bot to be stopped first. + +### How do I know if my bot is working correctly? +Monitor these indicators: +- Check the dashboard for active bot status +- Review trade history in the "History" section +- Monitor notifications for important events +- Use the AI Mentor for performance analysis + +### What happens if my computer shuts down? +If your computer shuts down, all active bots will stop. When you restart the application, you'll need to manually restart your bots. We recommend using a reliable computer or VPS for live trading. + +## ๐ Strategy Questions + +### Which strategy should I start with? +Beginners should start with: +1. **MA_CROSSOVER** (Week 1-2): Simple moving average strategy +2. **RSI_CROSSOVER** (Week 3-4): Momentum-based strategy +3. **TURTLE_BREAKOUT** (Week 5-6): Breakout trading strategy + +### How do I know which strategy works best for a market? +The system provides automatic recommendations: +- **EURUSD/GBPUSD**: Trend-following strategies like MA_CROSSOVER +- **XAUUSD**: Breakout strategies like TURTLE_BREAKOUT or QUANTUM_VELOCITY +- **BTCUSD/ETHUSD**: Crypto-optimized strategies like QUANTUMBOTX_CRYPTO +- **Indices**: INDEX_BREAKOUT_PRO for professional index trading + +### Can I create my own strategies? +Yes, experienced users can create custom strategies by extending the strategy classes. However, this requires Python programming knowledge. + +### How do I backtest a strategy? +1. Go to the "Backtester" page +2. Select your strategy and market +3. Choose a historical data period +4. Run the backtest +5. Review performance metrics like profit factor and drawdown + +## ๐งช Backtesting & Data + +### Do I need an internet connection for backtesting? +No! Backtesting works completely offline and does not require an internet connection or MT5 terminal. All you need is historical data in CSV format stored in the `lab/backtest_data` directory. + +### How do I get historical data for backtesting? +QuantumBotX provides a data download script in the `lab/download_data.py` directory that can automatically download historical data from your MT5 account: + +1. Ensure MT5 terminal is running and you're logged in +2. Configure your MT5 credentials in the `.env` file +3. Run the download script: + ```bash + python lab/download_data.py + ``` +4. The script will download data for popular symbols and save CSV files to `lab/backtest_data/` + +### What format should backtesting data be in? +Backtesting data should be in CSV format with the following columns: +- `time`: Timestamp in any standard format +- `open`: Opening price +- `high`: Highest price +- `low`: Lowest price +- `close`: Closing price +- `volume`: Trading volume + +Files should be named using the pattern: `{SYMBOL}_{TIMEFRAME}_data.csv` (e.g., `EURUSD_H1_data.csv`) + +### Can I use my own data for backtesting? +Yes! You can use your own historical data by placing CSV files in the `lab/backtest_data` directory with the proper naming convention. This is useful if you have data from other sources or want to test specific market conditions. + +### How much historical data do I need? +For meaningful backtesting results, we recommend at least 500 bars (about 2 years of H1 data) for most strategies. More data generally leads to more reliable results, especially for strategies that use longer timeframes or lookback periods. + +## ๐ก๏ธ Safety & Risk Management + +### Is QuantumBotX safe to use? +QuantumBotX includes multiple safety features: +- ATR-based position sizing automatically adapts to market volatility +- Special protection for Gold (XAUUSD) prevents account blowouts +- Emergency brake system skips dangerous trades +- Parameter validation prevents unsafe configurations + +### What happens if I lose connection to MT5? +If connection to MT5 is lost, the system will: +1. Log the error in the application logs +2. Stop all active bots +3. Attempt to reconnect automatically +4. Require manual restart of bots after reconnection + +### How do I protect my account from large losses? +Follow these risk management principles: +- Never risk more than 2% of your account per trade +- Always use stop losses +- Start with micro lots (0.01) +- Demo trade for at least 30 days before going live +- Monitor your bots regularly + +## ๐ Learning & Education + +### Do I need prior trading experience? +No prior experience is required. QuantumBotX includes a progressive learning path that takes you from absolute beginner to advanced trader over several months. + +### What educational resources are available? +QuantumBotX provides: +- **AI Mentor**: Personalized trading advice and education +- **Strategy explanations**: Plain English descriptions of all parameters +- **Progressive learning path**: Structured curriculum from beginner to expert +- **ATR education**: Understanding volatility-based risk management +- **Beginner defaults**: Safe parameter settings for all strategies + +### How long does it take to learn? +The structured learning path is designed to take 3 months: +- Month 1: Basic strategies and concepts +- Month 2: Intermediate strategies and market analysis +- Month 3: Advanced strategies and portfolio management + +## ๐ Cultural Features + +### What holidays are supported? +QuantumBotX automatically detects and adapts to: +- **Christmas**: December 20 - January 6 +- **Ramadan**: Islamic holy month (dates vary yearly) +- **Eid al-Fitr**: Celebration at the end of Ramadan +- **New Year**: December 30 - January 3 + +### How do holiday modes affect trading? +Holiday modes automatically: +- Reduce risk during distracted periods +- Pause trading during prayer times (Ramadan) +- Apply culturally-appropriate UI themes +- Provide relevant greetings and reminders + +### Can I disable holiday modes? +Holiday modes are automatically activated based on calendar dates and cannot be manually disabled. This ensures cultural sensitivity and appropriate risk management during holiday periods. + +## ๐ง Technical Questions + +### What are the system requirements? +- **Operating System**: Windows 10 or later (MT5 requirement) +- **Python**: 3.10 or later +- **RAM**: 4GB minimum, 8GB recommended +- **Storage**: 500MB free space +- **Internet**: Stable connection required + +### How do I update QuantumBotX? +1. Pull the latest changes from the repository: + ```bash + git pull origin main + ``` +2. Update dependencies: + ```bash + pip install -r requirements.txt + ``` +3. Restart the application + +### Where are logs stored? +Application logs are stored in the `logs/` directory: +- `app.log`: Main application logs +- Historical logs are automatically rotated + +### How do I report bugs or issues? +Please report issues through the GitHub repository's issue tracker with: +- Detailed description of the problem +- Steps to reproduce +- Screenshots if applicable +- Log files if relevant + +## ๐ฑ Mobile & Remote Access + +### Can I access QuantumBotX from my phone? +The web interface is mobile-responsive and can be accessed from any device with a web browser. However, the application must be running on a computer with MT5. + +### Is there a mobile app? +Currently, there is no dedicated mobile app. The web interface works well on mobile devices. + +### Can I run QuantumBotX on a VPS? +Yes, QuantumBotX can run on a Virtual Private Server (VPS), which is recommended for live trading to ensure 24/7 operation. \ No newline at end of file diff --git a/docs/quick_start.md b/docs/quick_start.md new file mode 100644 index 0000000..4147166 --- /dev/null +++ b/docs/quick_start.md @@ -0,0 +1,170 @@ +# ๐ QuantumBotX Quick Start Guide + +Get up and running with QuantumBotX in under 10 minutes! + +## ๐ Before You Begin + +**Requirements:** +- Windows computer (MT5 requirement) +- MetaTrader 5 terminal installed and running +- Python 3.10 or later +- MT5 account (demo is fine to start) + +## โฑ๏ธ 5-Minute Setup + +### Step 1: Download QuantumBotX +```bash +git clone https://github.com/rebarakaz/quantumbotx.git +cd quantumbotx +``` + +### Step 2: Set Up Python Environment +```bash +python -m venv venv +# Windows: +.\venv\Scripts\activate +# Mac/Linux: +source venv/bin/activate +``` + +### Step 3: Install Dependencies +```bash +pip install -r requirements.txt +``` + +### Step 4: Configure MT5 Connection +```bash +cp .env.example .env +``` + +Edit `.env` with your MT5 credentials: +``` +MT5_LOGIN=your_account_number +MT5_PASSWORD=your_password +MT5_SERVER=your_broker_server +``` + +### Step 5: Start QuantumBotX +```bash +python run.py +``` + +## ๐ Access the Dashboard + +Open your web browser and go to: +``` +http://127.0.0.1:5000 +``` + +## ๐ค Create Your First Bot (3 Minutes) + +### 1. Navigate to Trading Bots +Click "Trading Bots" in the left sidebar + +### 2. Click "Buat Bot Baru" +Fill in these beginner settings: + +**Basic Info:** +- **Nama Bot**: My First Bot +- **Pasar**: EURUSD (or any pair you want to trade) +- **Risk per Trade**: 1.0 (%) +- **SL (ATR Multiplier)**: 2.0 +- **TP (ATR Multiplier)**: 4.0 +- **Timeframe**: H1 +- **Interval Cek**: 60 + +**Strategy Settings:** +- **Strategi**: MA_CROSSOVER +- **Fast Period**: 10 +- **Slow Period**: 30 + +### 3. Click "Buat Bot" + +### 4. Start Your Bot +Click the "โถ๏ธ Start" button next to your new bot + +## ๐ Monitor Your Bot + +### Dashboard View +- Check your bot status on the main dashboard +- View real-time performance metrics +- Monitor active trades + +### Trade History +- Go to "History" to see all executed trades +- Review profit/loss for each trade + +## ๐งช Quick Backtesting (Optional) + +Before going live, you can test your strategy with historical data: + +### Get Historical Data +1. Ensure MT5 terminal is running and you're logged in +2. Run the data download script: + ```bash + python lab/download_data.py + ``` + +This will download data for popular symbols to `lab/backtest_data/` + +### Run a Backtest +1. Go to "Backtester" in the dashboard +2. Select "MA_CROSSOVER" strategy +3. Choose "EURUSD" market +4. Click "Run Backtest" +5. Review results before live trading + +## ๐ฏ Success Tips + +### First Week Goals +1. **Observe Only**: Let your bot run for a week without interference +2. **Learn the Interface**: Familiarize yourself with all dashboard features +3. **Check Daily**: Review performance each day +4. **Ask AI Mentor**: Use the AI Mentor for questions and explanations + +### Common Beginner Mistakes +- โ Changing settings too frequently +- โ Running too many bots at once +- โ Not using stop losses +- โ Trading live without demo experience + +### Best Practices +- โ Start with demo account +- โ Use micro lots (0.01) +- โ Monitor bots daily +- โ Keep a simple trading journal +- โ Follow the progressive learning path +- โ Test strategies with backtesting first + +## ๐ Need Help? + +### Quick Troubleshooting +1. **MT5 Connection Issues**: Ensure MT5 terminal is running +2. **Login Problems**: Verify credentials in `.env` file +3. **Bot Not Trading**: Check if market is open and bot is started +4. **Performance Concerns**: Use backtester before going live + +### Getting More Help +- **AI Mentor**: Click the AI button on dashboard for instant help +- **Documentation**: Check `docs/` folder for detailed guides +- **Strategy Explanations**: Every parameter has plain English descriptions + +## ๐ Next Steps + +After your first week: +1. **Run Backtest**: Test your strategy with historical data +2. **Try Different Markets**: Experiment with GBPUSD or XAUUSD +3. **Explore Other Strategies**: Try RSI_CROSSOVER next +4. **Join Learning Path**: Follow the structured 3-month curriculum + +## ๐ก๏ธ Important Reminders + +- **Demo First**: Always demo trade for at least 30 days +- **Small Risk**: Never risk more than 2% per trade +- **Stop Losses**: Always use stop losses +- **Emotional Control**: Treat demo like real money +- **Continuous Learning**: Use AI Mentor regularly + +--- + +**Congratulations!** You've successfully set up and started your first trading bot. Remember, successful trading is about consistency and risk management, not huge profits. Take your time to learn the system and build good habits. \ No newline at end of file diff --git a/init_db.py b/init_db.py index c7d7a99..f16c113 100644 --- a/init_db.py +++ b/init_db.py @@ -60,7 +60,8 @@ def main(): timeframe TEXT NOT NULL DEFAULT 'H1', check_interval_seconds INTEGER NOT NULL DEFAULT 60, strategy TEXT NOT NULL, - strategy_params TEXT + strategy_params TEXT, + enable_strategy_switching INTEGER NOT NULL DEFAULT 0 ); """ diff --git a/lab/download_data.py b/lab/download_data.py index 87d68ee..cb6dae0 100644 --- a/lab/download_data.py +++ b/lab/download_data.py @@ -3,7 +3,7 @@ import MetaTrader5 as mt5 import pandas as pd import os import sys -from datetime import datetime, timedelta +from datetime import datetime from dotenv import load_dotenv # Load environment variables from .env file @@ -25,36 +25,119 @@ if not all([ACCOUNT, PASSWORD, SERVER]): print("\n๐ก Tip: Check the .env file in the project root directory") sys.exit(1) -# --- Popular Trading Symbols for Indonesian Market --- +# --- Popular Trading Symbols for Indonesian Market + Index Trading --- POPULAR_SYMBOLS = { - # Forex Major Pairs + # Forex Major Pairs (Standard) 'EURUSD': mt5.TIMEFRAME_H1, 'GBPUSD': mt5.TIMEFRAME_H1, 'USDJPY': mt5.TIMEFRAME_H1, 'AUDUSD': mt5.TIMEFRAME_H1, 'USDCAD': mt5.TIMEFRAME_H1, 'USDCHF': mt5.TIMEFRAME_H1, + 'NZDUSD': mt5.TIMEFRAME_H1, + 'EURGBP': mt5.TIMEFRAME_H1, + 'EURJPY': mt5.TIMEFRAME_H1, + 'GBPJPY': mt5.TIMEFRAME_H1, + + # Forex Major Pairs (FBS Demo with 'w' suffix) + 'EURUSDw': mt5.TIMEFRAME_H1, + 'GBPUSDw': mt5.TIMEFRAME_H1, + 'USDJPYw': mt5.TIMEFRAME_H1, + 'AUDUSDw': mt5.TIMEFRAME_H1, + 'USDCADw': mt5.TIMEFRAME_H1, + 'USDCHFw': mt5.TIMEFRAME_H1, + 'NZDUSDw': mt5.TIMEFRAME_H1, + 'EURGBPw': mt5.TIMEFRAME_H1, + 'EURJPYw': mt5.TIMEFRAME_H1, + 'GBPJPYw': mt5.TIMEFRAME_H1, # Indonesian Focus 'USDIDR': mt5.TIMEFRAME_H1, # Important for Indonesian traders + 'USDIDRw': mt5.TIMEFRAME_H1, # FBS variant + + # Stock Indices (US Markets) + 'US30': mt5.TIMEFRAME_H1, # Dow Jones Industrial Average + 'US100': mt5.TIMEFRAME_H1, # NASDAQ 100 + 'US500': mt5.TIMEFRAME_H1, # S&P 500 + 'NAS100': mt5.TIMEFRAME_H1, # Alternative NASDAQ name + 'SPX500': mt5.TIMEFRAME_H1, # Alternative S&P 500 name + 'DJ30': mt5.TIMEFRAME_H1, # Alternative Dow Jones name + + # European Indices + 'DE30': mt5.TIMEFRAME_H1, # DAX (Germany) + 'DAX30': mt5.TIMEFRAME_H1, # Alternative DAX name + 'UK100': mt5.TIMEFRAME_H1, # FTSE 100 (UK) + 'FTSE100': mt5.TIMEFRAME_H1, # Alternative FTSE name + 'FR40': mt5.TIMEFRAME_H1, # CAC 40 (France) + 'CAC40': mt5.TIMEFRAME_H1, # Alternative CAC name + 'ES35': mt5.TIMEFRAME_H1, # IBEX 35 (Spain) + 'IT40': mt5.TIMEFRAME_H1, # MIB 40 (Italy) + + # Asian Indices + 'JP225': mt5.TIMEFRAME_H1, # Nikkei 225 (Japan) + 'N225': mt5.TIMEFRAME_H1, # Alternative Nikkei name + 'HK50': mt5.TIMEFRAME_H1, # Hang Seng (Hong Kong) + 'AUS200': mt5.TIMEFRAME_H1, # ASX 200 (Australia) # Precious Metals (High volatility - needs special handling) 'XAUUSD': mt5.TIMEFRAME_H1, # Gold - very popular 'XAGUSD': mt5.TIMEFRAME_H1, # Silver + 'XAUUSDw': mt5.TIMEFRAME_H1, # FBS Gold variant + 'XAGUSDw': mt5.TIMEFRAME_H1, # FBS Silver variant + 'GOLD': mt5.TIMEFRAME_H1, # Alternative Gold symbol + 'SILVER': mt5.TIMEFRAME_H1, # Alternative Silver symbol - # Crypto (if available) + # Energy Commodities + 'USOIL': mt5.TIMEFRAME_H1, # Crude Oil (US) + 'UKOIL': mt5.TIMEFRAME_H1, # Brent Oil (UK) + 'WTI': mt5.TIMEFRAME_H1, # West Texas Intermediate + 'BRENT': mt5.TIMEFRAME_H1, # Brent Crude + 'NGAS': mt5.TIMEFRAME_H1, # Natural Gas + + # Crypto (if available on broker) 'BTCUSD': mt5.TIMEFRAME_H1, 'ETHUSD': mt5.TIMEFRAME_H1, - - # Oil - 'USOIL': mt5.TIMEFRAME_H1, - 'UKOIL': mt5.TIMEFRAME_H1, + 'LTCUSD': mt5.TIMEFRAME_H1, + 'ADAUSD': mt5.TIMEFRAME_H1, + 'DOTUSD': mt5.TIMEFRAME_H1, } +# --- Custom Symbols (Add your broker-specific symbols here) --- +CUSTOM_SYMBOLS = { + # Add any additional symbols your broker offers + # Format: 'SYMBOL_NAME': mt5.TIMEFRAME_H1, + # Examples: + # 'EURAUD': mt5.TIMEFRAME_H1, + # 'GBPCAD': mt5.TIMEFRAME_H1, + # 'CADJPY': mt5.TIMEFRAME_H1, + # 'CHFJPY': mt5.TIMEFRAME_H1, +} + +def add_custom_symbol(symbol_name, timeframe=mt5.TIMEFRAME_H1): + """ + Add a custom symbol to download list + Usage: add_custom_symbol('EURAUD', mt5.TIMEFRAME_H1) + """ + CUSTOM_SYMBOLS[symbol_name] = timeframe + print(f"โ Added {symbol_name} to custom download list") + +def download_custom_symbol(symbol_name, timeframe=mt5.TIMEFRAME_H1, start_date=None, end_date=None): + """ + Download a single custom symbol immediately + """ + if start_date is None: + start_date = datetime(2020, 1, 1) + if end_date is None: + end_date = datetime.now() + + print(f"\n๐ฏ Manual Download: {symbol_name}") + return download_symbol_data(symbol_name, timeframe, start_date, end_date) + def download_symbol_data(symbol, timeframe, start_date, end_date, data_dir="backtest_data"): """ Download historical data for a specific symbol Compatible with QuantumBotX backtesting engine + Enhanced for index trading and broker-specific symbol variants """ # Ensure data directory exists os.makedirs(data_dir, exist_ok=True) @@ -62,22 +145,55 @@ def download_symbol_data(symbol, timeframe, start_date, end_date, data_dir="back print(f"\n๐ Downloading {symbol} data...") # Get symbol info first - symbol_info = mt5.symbol_info(symbol) + symbol_info = mt5.symbol_info(symbol) # pyright: ignore if symbol_info is None: print(f"โ Symbol {symbol} not found on this broker") + + # Suggest alternative symbol names for common cases + suggestions = [] + if symbol.endswith('w'): + base_symbol = symbol[:-1] + suggestions.append(base_symbol) + elif not symbol.endswith('w') and symbol in ['EURUSD', 'GBPUSD', 'USDJPY', 'AUDUSD', 'USDCAD', 'USDCHF']: + suggestions.append(symbol + 'w') + + if symbol == 'US30': + suggestions.extend(['DJ30', 'DOW30', 'DJIA']) + elif symbol == 'US100': + suggestions.extend(['NAS100', 'NASDAQ100', 'NDX']) + elif symbol == 'US500': + suggestions.extend(['SPX500', 'SP500', 'SPX']) + elif symbol == 'DE30': + suggestions.extend(['DAX30', 'GER30', 'DAX']) + elif symbol == 'XAUUSD': + suggestions.extend(['GOLD', 'XAUUSDw']) + + if suggestions: + print(f"๐ก Try these alternatives: {', '.join(suggestions)}") + return None if not symbol_info.visible: # Try to enable the symbol - if not mt5.symbol_select(symbol, True): + if not mt5.symbol_select(symbol, True): # pyright: ignore print(f"โ Failed to enable symbol {symbol}") return None + print(f"โ Enabled symbol {symbol} in Market Watch") + + # Show symbol details for indices and special instruments + if any(idx in symbol.upper() for idx in ['US30', 'US100', 'US500', 'DE30', 'UK100', 'JP225']): + print(f"๐ Index detected: {symbol} (Point value: {symbol_info.point}, Contract size: {symbol_info.trade_contract_size})") + elif 'XAU' in symbol.upper() or 'GOLD' in symbol.upper(): + print(f"๐ฅ Gold detected: {symbol} (Spread typically higher, use conservative settings)") + elif symbol.endswith('w'): + print(f"๐ง FBS variant detected: {symbol} (Micro lot broker format)") # Download the data - rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) + rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) # pyright: ignore if rates is None or len(rates) == 0: print(f"โ No data available for {symbol}") + print(f"๐ก Check if {symbol} was available during the requested date range") return None # Convert to DataFrame @@ -113,7 +229,9 @@ def download_symbol_data(symbol, timeframe, start_date, end_date, data_dir="back timeframe_str = timeframe_map.get(timeframe, 'H1') # Create filename compatible with backtesting engine - filename = f"{symbol}_{timeframe_str}_data.csv" + # Clean symbol name for filename (remove 'w' suffix for file naming consistency) + clean_symbol = symbol.replace('w', '') if symbol.endswith('w') else symbol + filename = f"{clean_symbol}_{timeframe_str}_data.csv" file_path = os.path.join(data_dir, filename) # Save to CSV @@ -122,9 +240,13 @@ def download_symbol_data(symbol, timeframe, start_date, end_date, data_dir="back print(f"โ {symbol}: {len(df)} bars saved to {file_path}") print(f" ๐ Date range: {df['time'].min()} to {df['time'].max()}") - # Special note for XAUUSD (Gold) - if 'XAU' in symbol.upper(): - print(f" โ ๏ธ WARNING: {symbol} is a volatile instrument - QuantumBotX will apply conservative risk settings") + # Special notes for different instrument types + if any(idx in symbol.upper() for idx in ['US30', 'US100', 'US500', 'DE30']): + print(" ๐ INDEX: Suitable for INDEX_MOMENTUM and INDEX_BREAKOUT_PRO strategies") + elif 'XAU' in symbol.upper() or 'GOLD' in symbol.upper(): + print(" โ ๏ธ GOLD: Volatile instrument - QuantumBotX will apply conservative risk settings") + elif symbol.endswith('w'): + print(f" ๐ง FBS Format: Saved as {clean_symbol} for consistency") return file_path @@ -132,24 +254,40 @@ def main(): """Main function to download data for multiple symbols""" # --- Initialize MT5 --- - if not mt5.initialize(login=ACCOUNT, password=PASSWORD, server=SERVER): - error = mt5.last_error() + if not mt5.initialize(login=ACCOUNT, password=PASSWORD, server=SERVER): # pyright: ignore + error = mt5.last_error() # pyright: ignore print(f"โ Failed to initialize MT5! Error: {error}") print("\n๐ง Troubleshooting:") print(" 1. Check if MT5 terminal is running") print(" 2. Verify credentials in .env file") print(" 3. Ensure the account is not already logged in elsewhere") print(" 4. Check internet connection") - mt5.shutdown() + mt5.shutdown() # pyright: ignore return - account_info = mt5.account_info() - print(f"โ Successfully connected to MT5") + account_info = mt5.account_info() # pyright: ignore + print("โ Successfully connected to MT5") print(f"๐ก Server: {account_info.server}") print(f"๐ค Account: {account_info.login}") print(f"๐ฐ Balance: ${account_info.balance:,.2f}") print(f"๐ข Company: {account_info.company}") + # Detect broker type for better symbol selection + server_name = account_info.server.upper() + broker_type = "Unknown" + + if 'FBS' in server_name or 'DEMO' in server_name: + broker_type = "FBS Demo" + print(f"๐ง Detected: {broker_type} - Will prioritize 'w' suffix symbols") + elif 'XM' in server_name: + broker_type = "XM Global" + print(f"๐ง Detected: {broker_type} - Will use standard symbol names") + elif 'EXNESS' in server_name: + broker_type = "Exness" + print(f"๐ง Detected: {broker_type} - Will use 'm' suffix for some symbols") + else: + print(f"๐ง Broker: {server_name} - Will try all symbol variants") + # --- Download Parameters --- start_date = datetime(2020, 1, 1) # 4+ years of data end_date = datetime.now() @@ -158,13 +296,29 @@ def main(): downloaded_files = [] failed_symbols = [] + index_files = [] + forex_files = [] + commodity_files = [] # Download data for all popular symbols - for symbol, timeframe in POPULAR_SYMBOLS.items(): + all_symbols = {**POPULAR_SYMBOLS, **CUSTOM_SYMBOLS} # Merge popular and custom symbols + + if CUSTOM_SYMBOLS: + print(f"\n๐ฏ Custom symbols added: {list(CUSTOM_SYMBOLS.keys())}") + + for symbol, timeframe in all_symbols.items(): try: file_path = download_symbol_data(symbol, timeframe, start_date, end_date) if file_path: downloaded_files.append(file_path) + + # Categorize files for better organization + if any(idx in symbol.upper() for idx in ['US30', 'US100', 'US500', 'DE30', 'UK100', 'JP225']): + index_files.append(file_path) + elif symbol.upper() in ['EURUSD', 'GBPUSD', 'USDJPY', 'AUDUSD'] or symbol.upper().replace('W', '') in ['EURUSD', 'GBPUSD', 'USDJPY', 'AUDUSD']: + forex_files.append(file_path) + elif 'XAU' in symbol.upper() or 'OIL' in symbol.upper(): + commodity_files.append(file_path) else: failed_symbols.append(symbol) except Exception as e: @@ -172,29 +326,78 @@ def main(): failed_symbols.append(symbol) # Cleanup - mt5.shutdown() + mt5.shutdown() # pyright: ignore - # Summary - print(f"\n๐ Download Complete!") + # Enhanced Summary + print("\n๐ Download Complete!") print(f"โ Successfully downloaded: {len(downloaded_files)} files") print(f"โ Failed downloads: {len(failed_symbols)} symbols") - if downloaded_files: - print("\n๐ Downloaded files:") - for file_path in downloaded_files: - print(f" โข {file_path}") + if index_files: + print(f"\n๐ Index Data ({len(index_files)} files):") + for file_path in index_files: + filename = os.path.basename(file_path) + print(f" โข {filename} - Use with INDEX_MOMENTUM or INDEX_BREAKOUT_PRO") + + if forex_files: + print(f"\n๐ฑ Forex Data ({len(forex_files)} files):") + for file_path in forex_files[:5]: # Show first 5 + filename = os.path.basename(file_path) + print(f" โข {filename}") + if len(forex_files) > 5: + print(f" โข ... and {len(forex_files) - 5} more forex pairs") + + if commodity_files: + print(f"\n๐ฅ Commodity Data ({len(commodity_files)} files):") + for file_path in commodity_files: + filename = os.path.basename(file_path) + print(f" โข {filename} - Use conservative settings") if failed_symbols: - print("\nโ ๏ธ Failed symbols:") - for symbol in failed_symbols: + print(f"\nโ ๏ธ Failed symbols ({len(failed_symbols)}):") + for symbol in failed_symbols[:10]: # Show first 10 print(f" โข {symbol}") + if len(failed_symbols) > 10: + print(f" โข ... and {len(failed_symbols) - 10} more symbols") - print("\n๐ก Tips for QuantumBotX Backtesting:") - print(" 1. Upload CSV files via the web interface") - print(" 2. XAUUSD will automatically use conservative settings") - print(" 3. Start with simple strategies like MA_CROSSOVER") - print(" 4. Use USDIDR data for Indonesian market focus") - print(f"\n๐ง Connected to: {SERVER} (Account: {ACCOUNT})") + print("\n๐ก QuantumBotX Strategy Recommendations:") + if index_files: + print(" ๐ For INDEX trading:") + print(" โข Beginners: Try INDEX_MOMENTUM strategy first") + print(" โข Advanced: Use INDEX_BREAKOUT_PRO for institutional patterns") + print(" โข Best symbols: US30, US100, US500, DE30") + + if forex_files: + print(" ๐ฑ For FOREX trading:") + print(" โข Beginners: Start with MA_CROSSOVER on EURUSD") + print(" โข Intermediate: Try RSI_CROSSOVER strategy") + print(" โข Advanced: Use PULSE_SYNC for multi-indicator analysis") + + if commodity_files: + print(" ๐ฅ For COMMODITIES:") + print(" โข XAUUSD: Use TURTLE_BREAKOUT with conservative settings") + print(" โข Oil: Apply trend-following strategies during trending markets") + + print("\n๐ง Setup Instructions:") + print(" 1. Upload CSV files via QuantumBotX web interface") + print(" 2. Start with demo accounts before live trading") + print(" 3. Use lot size 0.01 for initial testing") + print(" 4. Index strategies work best during market hours") + + print("\n๐จโ๐ป Manual Symbol Download:") + print(" # To download additional symbols, modify the CUSTOM_SYMBOLS dictionary") + print(" # Or use the helper functions:") + print(" # add_custom_symbol('EURAUD')") + print(" # download_custom_symbol('EURAUD')") + + print(f"\n๐ Connected to: {SERVER} (Account: {ACCOUNT})") + print(f"๐ Broker Type: {broker_type}") + + # Show sample usage for manual downloads + print("\n๐ Sample Manual Usage:") + print(" from download_data import download_custom_symbol, add_custom_symbol") + print(" add_custom_symbol('EURAUD') # Add to list") + print(" download_custom_symbol('GBPCAD') # Download immediately") if __name__ == "__main__": main() \ No newline at end of file diff --git a/last_broker.json b/last_broker.json index aae68f0..6bb095a 100644 --- a/last_broker.json +++ b/last_broker.json @@ -1,5 +1,5 @@ { - "broker": "XMGlobal-MT5 7", - "company": "XM Global Limited", - "last_check": "2025-08-28T10:33:53.918235" + "broker": "FBS-Demo", + "company": "FBS Markets Inc.", + "last_check": "2025-08-30T16:10:17.445029" } \ No newline at end of file diff --git a/migrate_db.py b/migrate_db.py new file mode 100644 index 0000000..f30f2d8 --- /dev/null +++ b/migrate_db.py @@ -0,0 +1,49 @@ +import sqlite3 +import os + +# Nama file database +DB_FILE = "bots.db" + +def migrate_database(): + """Migrate the database to add the enable_strategy_switching column""" + try: + # Check if database exists + if not os.path.exists(DB_FILE): + print(f"Database file '{DB_FILE}' not found. Run init_db.py first.") + return False + + # Connect to database + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + + # Check if the column already exists + cursor.execute("PRAGMA table_info(bots)") + columns = [column[1] for column in cursor.fetchall()] + + if 'enable_strategy_switching' in columns: + print("Column 'enable_strategy_switching' already exists in bots table.") + conn.close() + return True + + # Add the new column + cursor.execute("ALTER TABLE bots ADD COLUMN enable_strategy_switching INTEGER NOT NULL DEFAULT 0") + conn.commit() + print("Successfully added 'enable_strategy_switching' column to bots table.") + + conn.close() + return True + + except sqlite3.Error as e: + print(f"Database error: {e}") + return False + except Exception as e: + print(f"Error: {e}") + return False + +if __name__ == '__main__': + print("Migrating database to add strategy switching support...") + success = migrate_database() + if success: + print("Database migration completed successfully!") + else: + print("Database migration failed!") \ No newline at end of file diff --git a/run.py b/run.py index f2265ec..ddb77e5 100644 --- a/run.py +++ b/run.py @@ -20,7 +20,7 @@ def shutdown_app(): """Fungsi shutdown terpusat.""" logging.info("Memulai proses shutdown aplikasi...") shutdown_all_bots() - mt5.shutdown() + mt5.shutdown() # pyright: ignore[reportAttributeAccessIssue] logging.info("Koneksi MetaTrader 5 ditutup. Aplikasi berhenti.") # Panggil pabrik untuk membuat aplikasi kita diff --git a/static/js/forex.js b/static/js/forex.js index 73a07c0..7f1f4b2 100644 --- a/static/js/forex.js +++ b/static/js/forex.js @@ -61,14 +61,22 @@ document.addEventListener('click', function(e) { const symbol = e.target.dataset.symbol; fetchSymbolProfile(symbol); } + + // Add handler for Trade buttons + if (e.target.classList.contains('bg-blue-600') && e.target.textContent === 'Trade') { + const row = e.target.closest('tr'); + const symbol = row.querySelector('.details-btn').dataset.symbol; + // Redirect to trading_bots.html with symbol parameter + window.location.href = `/trading-bots?symbol=${encodeURIComponent(symbol)}`; + } }); document.getElementById('close-modal').addEventListener('click', function() { - document.getElementById('stock-modal').classList.add('hidden'); // Menggunakan modal yang sama dengan stocks + document.getElementById('forex-modal').classList.add('hidden'); // Fixed: was referencing 'stock-modal' }); async function fetchSymbolProfile(symbol) { - const modal = document.getElementById('stock-modal'); // Menggunakan modal yang sama dengan stocks + const modal = document.getElementById('forex-modal'); // Fixed: was referencing 'stock-modal' const modalTitle = document.getElementById('modal-title'); const modalContent = document.getElementById('modal-content'); diff --git a/static/js/stocks.js b/static/js/stocks.js index 7875817..2d40fc0 100644 --- a/static/js/stocks.js +++ b/static/js/stocks.js @@ -61,6 +61,14 @@ document.addEventListener('click', function(e) { const symbol = e.target.dataset.symbol; fetchCompanyProfile(symbol); } + + // Add handler for Trade buttons + if (e.target.classList.contains('bg-blue-600') && e.target.textContent === 'Trade') { + const row = e.target.closest('tr'); + const symbol = row.querySelector('.details-btn').dataset.symbol; + // Redirect to trading_bots.html with symbol parameter + window.location.href = `/trading-bots?symbol=${encodeURIComponent(symbol)}`; + } }); document.getElementById('close-modal').addEventListener('click', function() { diff --git a/static/js/trading_bots.js b/static/js/trading_bots.js index 907a796..2c69771 100644 --- a/static/js/trading_bots.js +++ b/static/js/trading_bots.js @@ -107,6 +107,10 @@ document.addEventListener('DOMContentLoaded', function() { // --- Event Listeners --- + // Check for symbol parameter in URL when page loads + const urlParams = new URLSearchParams(window.location.search); + const symbolFromUrl = urlParams.get('symbol'); + // Buka modal untuk membuat bot baru createBotBtn.addEventListener("click", () => { currentBotId = null; @@ -120,6 +124,12 @@ document.addEventListener('DOMContentLoaded', function() { form.elements.sl_atr_multiplier.value = 2.0; form.elements.tp_atr_multiplier.value = 4.0; form.elements.check_interval_seconds.value = 60; + + // If there's a symbol from URL, pre-fill the market field + if (symbolFromUrl) { + form.elements.market.value = symbolFromUrl; + } + modal.classList.remove('hidden'); }); @@ -205,6 +215,9 @@ document.addEventListener('DOMContentLoaded', function() { params[input.name] = parseFloat(input.value) || input.value; }); data.params = params; + + // Tambahkan pengaturan strategy switching + data.enable_strategy_switching = document.getElementById('enable_strategy_switching').checked; const url = currentBotId ? `/api/bots/${currentBotId}` : '/api/bots'; const method = currentBotId ? 'PUT' : 'POST'; @@ -263,6 +276,11 @@ document.addEventListener('DOMContentLoaded', function() { } } + // Set the strategy switching checkbox + if (form.elements.enable_strategy_switching) { + form.elements.enable_strategy_switching.checked = bot.enable_strategy_switching == 1; + } + // 3. Trigger event untuk memuat parameter strategi strategySelect.dispatchEvent(new Event('change', { 'bubbles': true })); diff --git a/templates/ai_mentor/dashboard.html b/templates/ai_mentor/dashboard.html index 0f7b9fd..f7265b1 100644 --- a/templates/ai_mentor/dashboard.html +++ b/templates/ai_mentor/dashboard.html @@ -3,10 +3,10 @@ {% block title %}๐ง AI Mentor Trading - QuantumBotX{% endblock %} -{% block head %} +{% block head_extra %} {% endblock %} @@ -141,7 +145,7 @@
{{ holiday_greeting }}
{% if holiday_config.adjustments.risk_reduction %} @@ -364,26 +368,43 @@ document.getElementById('quickFeedbackModal').addEventListener('click', function // Holiday Effects document.addEventListener('DOMContentLoaded', function() { - {% if holiday_config.active_holiday %} - // Apply holiday theme class - const holidayName = '{{ holiday_config.active_holiday }}'.toLowerCase().replace(' ', '-'); - document.body.classList.add('holiday-' + holidayName.split('-')[0]); + // Apply holiday theme and effects + var holidayConfig = { + activeHoliday: "{{ holiday_config.active_holiday if holiday_config and holiday_config.active_holiday else '' }}", + christmas: "{{ 'christmas' in holiday_config.active_holiday.lower() if holiday_config and holiday_config.active_holiday else 'false' }}", + ramadan: "{{ 'ramadan' in holiday_config.active_holiday.lower() if holiday_config and holiday_config.active_holiday else 'false' }}", + newYear: "{{ 'new year' in holiday_config.active_holiday.lower() if holiday_config and holiday_config.active_holiday else 'false' }}" + }; + + if (holidayConfig.activeHoliday) { + // Apply holiday theme class based on holiday type + if (holidayConfig.christmas === 'true') { + document.body.classList.add('holiday-christmas'); + } else if (holidayConfig.ramadan === 'true') { + document.body.classList.add('holiday-ramadan'); + } else if (holidayConfig.newYear === 'true') { + document.body.classList.add('holiday-new-year'); + } else { + // Fallback: use the first word of the holiday name + var holidayName = holidayConfig.activeHoliday.toLowerCase().replace(' ', '-'); + document.body.classList.add('holiday-' + holidayName.split('-')[0]); + } // Christmas snow effect - {% if 'christmas' in holiday_config.active_holiday.lower() %} + if (holidayConfig.christmas === 'true') { createSnowEffect(); - {% endif %} + } // Ramadan star effect - {% if 'ramadan' in holiday_config.active_holiday.lower() %} + if (holidayConfig.ramadan === 'true') { createStarEffect(); - {% endif %} + } // New Year fireworks effect - {% if 'new year' in holiday_config.active_holiday.lower() %} + if (holidayConfig.newYear === 'true') { // Could add fireworks effect here - {% endif %} - {% endif %} + } + } }); function createSnowEffect() { diff --git a/templates/ai_mentor/history.html b/templates/ai_mentor/history.html index b9902d6..7201fc9 100644 --- a/templates/ai_mentor/history.html +++ b/templates/ai_mentor/history.html @@ -168,7 +168,7 @@Jika diaktifkan, bot akan secara otomatis beralih ke strategi terbaik berdasarkan kinerja terkini.
+