From dc668b0ba7cf75d4044fc00024c626ea1a576613 Mon Sep 17 00:00:00 2001 From: chrisnov-it Date: Tue, 19 May 2026 12:27:21 +0800 Subject: [PATCH] Fix Python 3.12 dependency and strategy imports --- core/backtesting/engine.py | 4 +- core/backtesting/enhanced_engine.py | 6 +- core/bots/controller.py | 4 +- core/bots/trading_bot.py | 4 +- core/routes/api_bots.py | 8 +- core/strategies/strategy_map.py | 25 ++- pandas_ta/__init__.py | 260 ++++++++++++++++++++++++++++ pyproject.toml | 5 +- requirements.txt | 3 +- testing/validate_all_strategies.py | 11 +- 10 files changed, 304 insertions(+), 26 deletions(-) create mode 100644 pandas_ta/__init__.py diff --git a/core/backtesting/engine.py b/core/backtesting/engine.py index 3b77fc0..b3b09d8 100644 --- a/core/backtesting/engine.py +++ b/core/backtesting/engine.py @@ -2,7 +2,7 @@ import math # Import modul math import logging # Import modul logging -from core.strategies.strategy_map import STRATEGY_MAP +from core.strategies.strategy_map import resolve_strategy_class logger = logging.getLogger(__name__) # Completely disable backtesting logs for silent operation @@ -20,7 +20,7 @@ def run_backtest(strategy_id, params, historical_data_df, symbol_name=None): historical_data_df: DataFrame dengan data historis symbol_name: Nama simbol (opsional, untuk deteksi XAUUSD yang akurat) """ - strategy_class = STRATEGY_MAP.get(strategy_id) + strategy_class = resolve_strategy_class(strategy_id) if not strategy_class: return {"error": "Strategi tidak ditemukan"} diff --git a/core/backtesting/enhanced_engine.py b/core/backtesting/enhanced_engine.py index ac0c2b4..ae70bde 100644 --- a/core/backtesting/enhanced_engine.py +++ b/core/backtesting/enhanced_engine.py @@ -4,7 +4,7 @@ import math import logging import os -from core.strategies.strategy_map import STRATEGY_MAP +from core.strategies.strategy_map import resolve_strategy_class logger = logging.getLogger(__name__) # Set appropriate logging level @@ -263,7 +263,7 @@ def run_enhanced_backtest(strategy_id, params, historical_data_df, symbol_name=N ) # Get strategy - strategy_class = STRATEGY_MAP.get(strategy_id) + strategy_class = resolve_strategy_class(strategy_id) if not strategy_class: return {"error": "Strategy not found"} @@ -486,4 +486,4 @@ def run_enhanced_backtest(strategy_id, params, historical_data_df, symbol_name=N # Wrapper function for backward compatibility def run_backtest(strategy_id, params, historical_data_df, symbol_name=None): """Backward compatible wrapper for enhanced backtesting""" - return run_enhanced_backtest(strategy_id, params, historical_data_df, symbol_name) \ No newline at end of file + return run_enhanced_backtest(strategy_id, params, historical_data_df, symbol_name) diff --git a/core/bots/controller.py b/core/bots/controller.py index f1c228d..7da56ff 100644 --- a/core/bots/controller.py +++ b/core/bots/controller.py @@ -4,7 +4,7 @@ import json import logging from core.db import queries from .trading_bot import TradingBot -from core.strategies.strategy_map import STRATEGY_MAP +from core.strategies.strategy_map import STRATEGY_MAP, resolve_strategy_class logger = logging.getLogger(__name__) @@ -280,7 +280,7 @@ def get_bot_analysis_data(bot_id: int): return {"signal": "ERROR", "explanation": "Unable to fetch market data"} # Instantiate strategy - strategy_class = STRATEGY_MAP.get(bot_data['strategy']) + strategy_class = resolve_strategy_class(bot_data['strategy']) if not strategy_class: return {"signal": "ERROR", "explanation": f"Strategy '{bot_data['strategy']}' not found"} diff --git a/core/bots/trading_bot.py b/core/bots/trading_bot.py index d4aeda1..52eec5c 100644 --- a/core/bots/trading_bot.py +++ b/core/bots/trading_bot.py @@ -5,7 +5,7 @@ import time import logging from datetime import datetime import MetaTrader5 as mt5 -from core.strategies.strategy_map import STRATEGY_MAP +from core.strategies.strategy_map import resolve_strategy_class from core.mt5.trade import place_trade, close_trade from core.utils.mt5 import TIMEFRAME_MAP # <-- Impor dari lokasi terpusat # AI Mentor Integration @@ -57,7 +57,7 @@ class TradingBot(threading.Thread): return # Hentikan eksekusi jika simbol tidak valid try: - strategy_class = STRATEGY_MAP.get(self.strategy_name) + strategy_class = resolve_strategy_class(self.strategy_name) if not strategy_class: raise ValueError(f"Strategi '{self.strategy_name}' tidak ditemukan.") diff --git a/core/routes/api_bots.py b/core/routes/api_bots.py index 1e2de73..1fa48e8 100644 --- a/core/routes/api_bots.py +++ b/core/routes/api_bots.py @@ -8,7 +8,7 @@ from core.bots import controller from core.db import queries from core.utils.mt5 import get_rates_mt5 from core.utils.mt5 import TIMEFRAME_MAP -from core.strategies.strategy_map import STRATEGY_MAP +from core.strategies.strategy_map import STRATEGY_MAP, resolve_strategy_class api_bots = Blueprint('api_bots', __name__) logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ def get_strategies_route(): @api_bots.route('/api/strategies//params', methods=['GET']) def get_strategy_params_route(strategy_id): """Mengembalikan parameter yang bisa diatur untuk sebuah strategi.""" - strategy_class = STRATEGY_MAP.get(strategy_id) + strategy_class = resolve_strategy_class(strategy_id) if not strategy_class: return jsonify({"error": "Strategi tidak ditemukan"}), 404 @@ -63,7 +63,7 @@ def get_bots_route(): # Perkaya data bot dengan nama strategi yang mudah dibaca for bot in bots: strategy_key = bot.get('strategy') - strategy_class = STRATEGY_MAP.get(strategy_key) # pyright: ignore[reportArgumentType] + strategy_class = resolve_strategy_class(strategy_key) # pyright: ignore[reportArgumentType] if strategy_class: bot['strategy_name'] = getattr(strategy_class, 'name', strategy_key) else: @@ -203,4 +203,4 @@ def get_rsi_data_route(): 'timestamps': [ts.strftime('%H:%M') for ts in last_50_rows.index], 'rsi_values': list(last_50_rows.iloc[:, -1]) } - return jsonify(chart_data) \ No newline at end of file + return jsonify(chart_data) diff --git a/core/strategies/strategy_map.py b/core/strategies/strategy_map.py index 5e206f9..aeafcf3 100644 --- a/core/strategies/strategy_map.py +++ b/core/strategies/strategy_map.py @@ -24,7 +24,7 @@ STRATEGY_MAP = { 'BOLLINGER_REVERSION': BollingerBandsStrategy, 'BOLLINGER_SQUEEZE': BollingerSqueezeStrategy, 'MERCY_EDGE': MercyEdgeStrategy, - 'quantum_velocity': QuantumVelocityStrategy, + 'QUANTUM_VELOCITY': QuantumVelocityStrategy, 'PULSE_SYNC': PulseSyncStrategy, 'TURTLE_BREAKOUT': TurtleBreakoutStrategy, 'ICHIMOKU_CLOUD': IchimokuCloudStrategy, @@ -33,6 +33,22 @@ STRATEGY_MAP = { 'INDEX_BREAKOUT_PRO': IndexBreakoutProStrategy, } + +def normalize_strategy_id(strategy_id): + """Return the canonical strategy ID used by STRATEGY_MAP.""" + if not strategy_id: + return strategy_id + aliases = { + "quantum_velocity": "QUANTUM_VELOCITY", + } + raw = str(strategy_id).strip() + return aliases.get(raw, raw.upper()) + + +def resolve_strategy_class(strategy_id): + """Resolve a strategy class while accepting legacy lowercase IDs.""" + return STRATEGY_MAP.get(normalize_strategy_id(strategy_id)) + # Beginner-friendly strategy metadata STRATEGY_METADATA = { # ✅ BEGINNER FRIENDLY @@ -181,11 +197,12 @@ def get_strategies_for_market(market_type): def get_strategy_info(strategy_name): """Get complete strategy information""" - metadata = STRATEGY_METADATA.get(strategy_name, {}) - beginner_info = BEGINNER_DEFAULTS.get(strategy_name, {}) + strategy_id = normalize_strategy_id(strategy_name) + metadata = STRATEGY_METADATA.get(strategy_id, {}) + beginner_info = BEGINNER_DEFAULTS.get(strategy_id, {}) return { - 'strategy_class': STRATEGY_MAP.get(strategy_name), + 'strategy_class': resolve_strategy_class(strategy_id), 'metadata': metadata, 'beginner_info': beginner_info } diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py new file mode 100644 index 0000000..38e303b --- /dev/null +++ b/pandas_ta/__init__.py @@ -0,0 +1,260 @@ +"""Local pandas_ta compatibility layer for QuantumBotX. + +The upstream pandas_ta package has had availability issues on modern Python +versions. This module implements the indicator subset used by the app so the +runtime stays installable on Python 3.12. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd + + +def _series(values, index) -> pd.Series: + return pd.Series(values, index=index, dtype="float64") + + +def _na_series(close: pd.Series) -> pd.Series: + return _series(np.nan, close.index) + + +def sma(close: pd.Series, length: int = 14, **kwargs) -> pd.Series: + return close.rolling(length, min_periods=1).mean() + + +def ema(close: pd.Series, length: int = 14, **kwargs) -> pd.Series: + return close.ewm(span=length, adjust=False).mean() + + +def rsi(close: pd.Series, length: int = 14, **kwargs) -> pd.Series: + delta = close.diff() + gain = delta.clip(lower=0) + loss = -delta.clip(upper=0) + avg_gain = gain.ewm(alpha=1 / length, adjust=False).mean() + avg_loss = loss.ewm(alpha=1 / length, adjust=False).mean() + rs = avg_gain / avg_loss.replace(0, np.nan) + return 100 - (100 / (1 + rs)) + + +def atr(high: pd.Series, low: pd.Series, close: pd.Series, length: int = 14, **kwargs) -> pd.Series: + prev_close = close.shift(1) + tr = pd.concat( + [ + (high - low).abs(), + (high - prev_close).abs(), + (low - prev_close).abs(), + ], + axis=1, + ).max(axis=1) + return tr.ewm(alpha=1 / length, adjust=False).mean() + + +def bbands(close: pd.Series, length: int = 20, std: float = 2.0, **kwargs) -> pd.DataFrame: + middle = close.rolling(length, min_periods=1).mean() + deviation = close.rolling(length, min_periods=1).std() + lower = middle - (std * deviation) + upper = middle + (std * deviation) + width = (upper - lower) / middle.replace(0, np.nan) + percent = (close - lower) / (upper - lower).replace(0, np.nan) + std_label = f"{float(std):.1f}" + return pd.DataFrame( + { + f"BBL_{length}_{std_label}": lower, + f"BBM_{length}_{std_label}": middle, + f"BBU_{length}_{std_label}": upper, + f"BBB_{length}_{std_label}": width, + f"BBP_{length}_{std_label}": percent, + }, + index=close.index, + ) + + +def adx(high: pd.Series, low: pd.Series, close: pd.Series, length: int = 14, **kwargs) -> pd.DataFrame: + up_move = high.diff() + down_move = -low.diff() + plus_dm = up_move.where((up_move > down_move) & (up_move > 0), 0.0) + minus_dm = down_move.where((down_move > up_move) & (down_move > 0), 0.0) + atr_value = atr(high, low, close, length=length) + plus_di = 100 * plus_dm.ewm(alpha=1 / length, adjust=False).mean() / atr_value.replace(0, np.nan) + minus_di = 100 * minus_dm.ewm(alpha=1 / length, adjust=False).mean() / atr_value.replace(0, np.nan) + dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan) + return pd.DataFrame( + { + f"ADX_{length}": dx.ewm(alpha=1 / length, adjust=False).mean(), + f"DMP_{length}": plus_di, + f"DMN_{length}": minus_di, + }, + index=close.index, + ) + + +def macd(close: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9, **kwargs) -> pd.DataFrame: + macd_line = ema(close, fast) - ema(close, slow) + signal_line = ema(macd_line, signal) + hist = macd_line - signal_line + return pd.DataFrame( + { + f"MACD_{fast}_{slow}_{signal}": macd_line, + f"MACDh_{fast}_{slow}_{signal}": hist, + f"MACDs_{fast}_{slow}_{signal}": signal_line, + }, + index=close.index, + ) + + +def stoch( + high: pd.Series, + low: pd.Series, + close: pd.Series, + k: int = 14, + d: int = 3, + smooth_k: int = 3, + **kwargs, +) -> pd.DataFrame: + lowest_low = low.rolling(k, min_periods=1).min() + highest_high = high.rolling(k, min_periods=1).max() + raw_k = 100 * (close - lowest_low) / (highest_high - lowest_low).replace(0, np.nan) + k_line = raw_k.rolling(smooth_k, min_periods=1).mean() + d_line = k_line.rolling(d, min_periods=1).mean() + return pd.DataFrame( + { + f"STOCHk_{k}_{d}_{smooth_k}": k_line, + f"STOCHd_{k}_{d}_{smooth_k}": d_line, + }, + index=close.index, + ) + + +def donchian( + high: pd.Series, + low: pd.Series, + lower_length: int = 20, + upper_length: int = 20, + **kwargs, +) -> pd.DataFrame: + lower = low.rolling(lower_length, min_periods=1).min() + upper = high.rolling(upper_length, min_periods=1).max() + middle = (lower + upper) / 2 + return pd.DataFrame( + { + f"DCL_{lower_length}_{upper_length}": lower, + f"DCM_{lower_length}_{upper_length}": middle, + f"DCU_{lower_length}_{upper_length}": upper, + }, + index=high.index, + ) + + +def ichimoku( + high: pd.Series, + low: pd.Series, + close: pd.Series | None = None, + tenkan: int = 9, + kijun: int = 26, + senkou: int = 52, + **kwargs, +): + tenkan_line = (high.rolling(tenkan, min_periods=1).max() + low.rolling(tenkan, min_periods=1).min()) / 2 + kijun_line = (high.rolling(kijun, min_periods=1).max() + low.rolling(kijun, min_periods=1).min()) / 2 + span_a = ((tenkan_line + kijun_line) / 2).shift(kijun) + span_b = ((high.rolling(senkou, min_periods=1).max() + low.rolling(senkou, min_periods=1).min()) / 2).shift(kijun) + frame = pd.DataFrame( + { + f"ITS_{tenkan}": tenkan_line, + f"IKS_{kijun}": kijun_line, + f"ISA_{tenkan}": span_a, + f"ISB_{kijun}": span_b, + }, + index=high.index, + ) + return frame, None + + +def roc(close: pd.Series, length: int = 10, **kwargs) -> pd.Series: + return close.pct_change(periods=length) * 100 + + +class _TAAccessor: + def __init__(self, df: pd.DataFrame): + self._df = df + + def _col(self, name: str) -> pd.Series: + return self._df[name] + + def _append_frame(self, frame: pd.DataFrame, append: bool): + if append: + for col in frame.columns: + self._df[col] = frame[col] + return frame + + def sma(self, close: str = "close", length: int = 14, append: bool = False, **kwargs): + out = sma(self._col(close), length=length, **kwargs) + if append: + self._df[f"SMA_{length}"] = out + return out + + def ema(self, close: str = "close", length: int = 14, append: bool = False, **kwargs): + out = ema(self._col(close), length=length, **kwargs) + if append: + self._df[f"EMA_{length}"] = out + return out + + def rsi(self, close: str = "close", length: int = 14, append: bool = False, **kwargs): + out = rsi(self._col(close), length=length, **kwargs) + if append: + self._df[f"RSI_{length}"] = out + return out + + def atr(self, high: str = "high", low: str = "low", close: str = "close", length: int = 14, append: bool = False, **kwargs): + out = atr(self._col(high), self._col(low), self._col(close), length=length, **kwargs) + if append: + self._df[f"ATRr_{length}"] = out + self._df[f"ATR_{length}"] = out + return out + + def bbands(self, close: str = "close", length: int = 20, std: float = 2.0, append: bool = False, **kwargs): + return self._append_frame(bbands(self._col(close), length=length, std=std, **kwargs), append) + + def adx(self, high: str = "high", low: str = "low", close: str = "close", length: int = 14, append: bool = False, **kwargs): + return self._append_frame(adx(self._col(high), self._col(low), self._col(close), length=length, **kwargs), append) + + def macd(self, close: str = "close", fast: int = 12, slow: int = 26, signal: int = 9, append: bool = False, **kwargs): + return self._append_frame(macd(self._col(close), fast=fast, slow=slow, signal=signal, **kwargs), append) + + def stoch( + self, + high: str = "high", + low: str = "low", + close: str = "close", + k: int = 14, + d: int = 3, + smooth_k: int = 3, + append: bool = False, + **kwargs, + ): + return self._append_frame(stoch(self._col(high), self._col(low), self._col(close), k=k, d=d, smooth_k=smooth_k, **kwargs), append) + + def donchian(self, high: str = "high", low: str = "low", lower_length: int = 20, upper_length: int = 20, append: bool = False, **kwargs): + return self._append_frame(donchian(self._col(high), self._col(low), lower_length=lower_length, upper_length=upper_length, **kwargs), append) + + def ichimoku(self, high: str = "high", low: str = "low", close: str = "close", append: bool = False, **kwargs): + frame, extra = ichimoku(self._col(high), self._col(low), self._df[close] if close in self._df else None, **kwargs) + if append: + for col in frame.columns: + self._df[col] = frame[col] + return frame, extra + + +@pd.api.extensions.register_dataframe_accessor("ta") +class _PandasTAAccessor(_TAAccessor): + pass + + +def __getattr__(name): + def _fallback(*args, **kwargs): + if args and isinstance(args[0], pd.Series): + return _na_series(args[0]) + return pd.Series(dtype="float64") + + return _fallback diff --git a/pyproject.toml b/pyproject.toml index 3a4fb3e..ab4a56a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,8 @@ dependencies = [ "Jinja2==3.1.6", "MarkupSafe==3.0.2", "MetaTrader5==5.0.5120", - "numpy==1.26.0", + "numpy>=1.26.4,<3", "pandas==2.3.1", - "pandas_ta==0.3.14b0", "python-dateutil==2.9.0.post0", "python-dotenv==1.1.1", "pytz==2025.2", @@ -32,4 +31,4 @@ dependencies = [ "tzdata==2025.2", "urllib3==2.5.0", "Werkzeug==3.1.3" -] \ No newline at end of file +] diff --git a/requirements.txt b/requirements.txt index 3b4e637..eeca533 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,9 +9,8 @@ itsdangerous==2.2.0 Jinja2==3.1.6 MarkupSafe==3.0.2 MetaTrader5==5.0.5120 -numpy==1.23.5 +numpy>=1.26.4,<3 pandas==2.3.1 -pandas_ta==0.3.14b0 python-dateutil==2.9.0.post0 python-dotenv==1.1.1 pytz==2025.2 diff --git a/testing/validate_all_strategies.py b/testing/validate_all_strategies.py index 56aaf61..aef4ec9 100644 --- a/testing/validate_all_strategies.py +++ b/testing/validate_all_strategies.py @@ -6,7 +6,11 @@ Ensures all strategies work properly with the fixed backtesting engine import sys import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) +repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if repo_root not in sys.path: + sys.path.insert(0, repo_root) +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") def validate_strategy_registration(): """Validate strategy registration and imports""" @@ -142,8 +146,7 @@ def check_strategy_id_consistency(): for strategy_id in strategy_ids: if strategy_id != strategy_id.upper() and '_' in strategy_id: # Most strategies use UPPER_CASE, but some don't - if strategy_id not in ['quantum_velocity']: # Known exception - inconsistent_naming.append(strategy_id) + inconsistent_naming.append(strategy_id) if inconsistent_naming: print("⚠️ Naming convention inconsistencies:") @@ -290,4 +293,4 @@ def main(): print(f"All {len(registered)} strategies should work properly with the fixed backtesting engine.") if __name__ == '__main__': - main() \ No newline at end of file + main()