# BOT-OWSKY Strategy V3.0 - Pattern-First ML Trading Strategy # # KEY CHANGES FROM V2: # ═══════════════════════════════════════════════════════════════════════════════ # - Pattern-First approach (35% weight vs 15% in V2) # - 3-Gate Entry System (Pattern → Trend → ML) # - Stricter thresholds for 15m timeframe # - ATR-based dynamic stoploss # - Declining DCA (not exponential) # - Faster Bayesian learning (2-3 trades vs 5+) # - Reduced leverage (2x max vs 5x) # - Simplified ML features (10 vs 14) # """ import numpy as np import pandas as pd from pandas import DataFrame, Series import talib.abstract as ta from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.preprocessing import RobustScaler import warnings import logging import json from pathlib import Path from typing import Any, Dict, List, Tuple, Optional from datetime import datetime, timedelta from freqtrade.strategy import ( IStrategy, IntParameter, DecimalParameter, BooleanParameter, ) from freqtrade.persistence import Trade warnings.filterwarnings("ignore") logger = logging.getLogger(__name__) # ═══════════════════════════════════════════════════════════════════════════════ # COIN CONFIDENCE MANAGER V3 - Bayesian Learning # ═══════════════════════════════════════════════════════════════════════════════ class CoinConfidenceManagerV3: """ Bayesian confidence manager - learns meaningfully after 2-3 trades. """ def __init__(self, storage_path: str = None): if storage_path is None: storage_path = Path("user_data/models/bot_owsky_v3") else: storage_path = Path(storage_path) storage_path.mkdir(parents=True, exist_ok=True) self.storage_path = storage_path self.confidence_file = storage_path / "coin_confidence_v3.json" self.patterns_file = storage_path / "pattern_performance_v3.json" self.regime_file = storage_path / "regime_performance_v3.json" self.coin_data = self._load_json(self.confidence_file) self.pattern_data = self._load_json(self.patterns_file) self.regime_data = self._load_json(self.regime_file) # Bayesian priors self.default_prior = { 'win_rate': 0.50, 'profit_factor': 1.0, 'confidence': 50.0 } logger.info(f"CoinConfidenceManagerV3 initialized with {len(self.coin_data)} coins") def _load_json(self, filepath: Path) -> Dict: if filepath.exists(): try: with open(filepath, 'r') as f: return json.load(f) except Exception as e: logger.warning(f"Could not load {filepath}: {e}") return {} def save(self): try: with open(self.confidence_file, 'w') as f: json.dump(self.coin_data, f, indent=2) with open(self.patterns_file, 'w') as f: json.dump(self.pattern_data, f, indent=2) with open(self.regime_file, 'w') as f: json.dump(self.regime_data, f, indent=2) except Exception as e: logger.error(f"Error saving data: {e}") def get_coin_confidence(self, pair: str) -> float: if pair not in self.coin_data: return 50.0 return self.coin_data[pair].get('confidence', 50.0) def get_regime_performance(self, pair: str, regime: str) -> Dict: key = f"{pair}_{regime}" if key not in self.regime_data: return {'win_rate': 0.5, 'avg_profit': 0.0, 'trades': 0} return self.regime_data[key] def update_trade_result(self, pair: str, profit: float, duration_minutes: int, entry_patterns: list, exit_reason: str, regime: str = 'unknown'): if pair not in self.coin_data: self.coin_data[pair] = self._create_new_coin_entry(pair) data = self.coin_data[pair] data['total_trades'] = data.get('total_trades', 0) + 1 data['total_profit'] = data.get('total_profit', 0.0) + profit is_win = profit > 0 if is_win: data['wins'] = data.get('wins', 0) + 1 data['total_win_profit'] = data.get('total_win_profit', 0.0) + profit else: data['losses'] = data.get('losses', 0) + 1 data['total_loss_profit'] = data.get('total_loss_profit', 0.0) + abs(profit) self._update_pattern_performance(pair, entry_patterns, is_win, profit) self._update_regime_performance(pair, regime, is_win, profit) # Bayesian confidence calculation data['confidence'] = self._calculate_bayesian_confidence(data) self.save() def _create_new_coin_entry(self, pair: str) -> Dict: return { 'pair': pair, 'confidence': 50.0, 'total_trades': 0, 'wins': 0, 'losses': 0, 'total_profit': 0.0, 'total_win_profit': 0.0, 'total_loss_profit': 0.0, } def _calculate_bayesian_confidence(self, data: Dict) -> float: """ Bayesian confidence - meaningful after 2-3 trades instead of 5+ """ total = data.get('total_trades', 0) if total == 0: return 50.0 # Prior weight decreases quickly # After 2 trades: 0.42, After 3: 0.32, After 5: 0.22 prior_weight = 1 / (1 + total * 0.7) # Observed win rate wins = data.get('wins', 0) observed_win_rate = wins / total # Bayesian posterior for win rate posterior_win_rate = ( prior_weight * self.default_prior['win_rate'] + (1 - prior_weight) * observed_win_rate ) # Base confidence from win rate confidence = posterior_win_rate * 100 # Profit factor adjustment (avoid division by zero) gross_profit = data.get('total_win_profit', 0.0) gross_loss = max(data.get('total_loss_profit', 0.0), 0.001) # Min 0.001 to prevent div/0 profit_factor = gross_profit / gross_loss if profit_factor > 1.5: confidence += 10 elif profit_factor > 1.2: confidence += 5 elif profit_factor < 0.8: confidence -= 10 elif profit_factor < 1.0: confidence -= 5 return max(0, min(100, confidence)) def _update_pattern_performance(self, pair: str, patterns: list, is_win: bool, profit: float): for pattern in patterns: if pattern not in self.pattern_data: self.pattern_data[pattern] = {'wins': 0, 'losses': 0, 'total_profit': 0.0} if is_win: self.pattern_data[pattern]['wins'] += 1 else: self.pattern_data[pattern]['losses'] += 1 self.pattern_data[pattern]['total_profit'] += profit def _update_regime_performance(self, pair: str, regime: str, is_win: bool, profit: float): key = f"{pair}_{regime}" if key not in self.regime_data: self.regime_data[key] = {'wins': 0, 'losses': 0, 'total_profit': 0.0, 'trades': 0} self.regime_data[key]['trades'] += 1 self.regime_data[key]['total_profit'] += profit if is_win: self.regime_data[key]['wins'] += 1 else: self.regime_data[key]['losses'] += 1 total = self.regime_data[key]['trades'] self.regime_data[key]['win_rate'] = self.regime_data[key]['wins'] / total self.regime_data[key]['avg_profit'] = self.regime_data[key]['total_profit'] / total # ═══════════════════════════════════════════════════════════════════════════════ # PATTERN ANALYZER V3 # ═══════════════════════════════════════════════════════════════════════════════ class PatternAnalyzerV3: """ Pattern analysis with Bulkowski reliability ratings. Now primary decision maker (35% weight). """ HIGH_RELIABILITY = { 'CDL3WHITESOLDIERS': 0.78, 'CDL3BLACKCROWS': 0.78, 'CDLENGULFING': 0.63, 'CDLMORNINGSTAR': 0.76, 'CDLEVENINGSTAR': 0.72, 'CDLPIERCING': 0.64, 'CDLDARKCLOUDCOVER': 0.60, } MEDIUM_RELIABILITY = { 'CDLHAMMER': 0.60, 'CDLHANGINGMAN': 0.59, 'CDLDOJI': 0.51, 'CDLHARAMI': 0.53, 'CDLINVERTEDHAMMER': 0.55, 'CDLSHOOTINGSTAR': 0.58, } def __init__(self): self.all_patterns = list(self.HIGH_RELIABILITY.keys()) + list(self.MEDIUM_RELIABILITY.keys()) def analyze(self, dataframe: DataFrame) -> DataFrame: df = dataframe.copy() pattern_signals = {} for pattern in self.all_patterns: try: pattern_signals[pattern] = getattr(ta, pattern)(df) except Exception: pattern_signals[pattern] = pd.Series(0, index=df.index) for pattern, sig in pattern_signals.items(): df[f'pattern_{pattern}'] = sig df['pattern_confidence'] = self._calc_confidence(df, pattern_signals) df['bullish_pattern_count'] = sum((s > 0).fillna(False).astype(int) for s in pattern_signals.values()) df['bearish_pattern_count'] = sum((s < 0).fillna(False).astype(int) for s in pattern_signals.values()) df['pattern_strength'] = df['bullish_pattern_count'] - df['bearish_pattern_count'] df['weighted_pattern_strength'] = self._calc_weighted_strength(pattern_signals) # Normalized pattern score (0-1) max_strength = len(self.all_patterns) * 0.78 df['pattern_score'] = (df['weighted_pattern_strength'].clip(-max_strength, max_strength) + max_strength) / (2 * max_strength) return df def _calc_confidence(self, df: DataFrame, signals: Dict) -> Series: confidence = pd.Series(50.0, index=df.index) for i in range(len(df)): active = [(p, s.iloc[i]) for p, s in signals.items() if s.iloc[i] != 0] if active: confs = [] for p, _ in active: if p in self.HIGH_RELIABILITY: confs.append(self.HIGH_RELIABILITY[p] * 100) elif p in self.MEDIUM_RELIABILITY: confs.append(self.MEDIUM_RELIABILITY[p] * 100) if confs: confidence.iloc[i] = np.mean(confs) return confidence def _calc_weighted_strength(self, signals: Dict) -> Series: weighted = pd.Series(0.0, index=list(signals.values())[0].index) for p, s in signals.items(): w = self.HIGH_RELIABILITY.get(p, self.MEDIUM_RELIABILITY.get(p, 0.5)) weighted += s * w return weighted def get_active_patterns(self, row) -> List[str]: """Extract active pattern names from a dataframe row.""" patterns = [] for pattern in self.all_patterns: col = f'pattern_{pattern}' if col in row.index and row[col] != 0: patterns.append(pattern) return patterns # ═══════════════════════════════════════════════════════════════════════════════ # MAIN STRATEGY CLASS # ═══════════════════════════════════════════════════════════════════════════════ class BotOwskyStrategy_V3(IStrategy): """ BOT-OWSKY V3: Pattern-First ML Strategy for 15m Futures Trading 3-Gate Entry System: 1. Pattern Gate: Min 55% confidence 2. Trend Gate: Never trade against strong trends 3. ML Gate: Ensemble prediction > 0.58 """ INTERFACE_VERSION = 3 # ═══════════════════════════════════════════════════════════════════════════ # STRATEGY CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════ timeframe = '15m' startup_candle_count: int = 300 # More for 15m can_short = True # ROI - Faster exits for 15m minimal_roi = { "0": 0.08, "30": 0.04, "60": 0.025, "120": 0.015, "240": 0.008 } # Stoploss - Default, ATR overrides via custom_stoploss stoploss = -0.04 # Trailing stop - Tighter for 15m trailing_stop = True trailing_stop_positive = 0.008 trailing_stop_positive_offset = 0.015 trailing_only_offset_is_reached = True # Order configuration order_types = { 'entry': 'limit', 'exit': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': True } order_time_in_force = { 'entry': 'GTC', 'exit': 'GTC' } # DCA - Declining, not exponential position_adjustment_enable = True max_entry_position_adjustment = 2 # ═══════════════════════════════════════════════════════════════════════════ # HYPEROPTABLE PARAMETERS - STRICTER THAN V2 # ═══════════════════════════════════════════════════════════════════════════ # Indicator periods atr_period = IntParameter(10, 20, default=14, space='buy', optimize=True) rsi_period = IntParameter(10, 21, default=14, space='buy', optimize=True) ema_fast = IntParameter(8, 21, default=12, space='buy', optimize=True) ema_slow = IntParameter(21, 55, default=30, space='buy', optimize=True) # Entry thresholds - STRICTER for 15m composite_threshold = DecimalParameter(0.30, 0.55, default=0.40, decimals=2, space='buy', optimize=True) pattern_min_confidence = IntParameter(50, 70, default=55, space='buy', optimize=True) rf_threshold = DecimalParameter(0.50, 0.70, default=0.58, decimals=2, space='buy', optimize=True) volume_threshold = DecimalParameter(0.8, 1.5, default=1.0, decimals=1, space='buy', optimize=True) # Trend filter adx_strong_threshold = IntParameter(25, 40, default=30, space='buy', optimize=True) adx_weak_threshold = IntParameter(15, 25, default=20, space='buy', optimize=True) # Divergence use_divergence = BooleanParameter(default=True, space='buy', optimize=True) divergence_lookback = IntParameter(10, 30, default=15, space='buy', optimize=True) divergence_boost = DecimalParameter(0.05, 0.15, default=0.10, decimals=2, space='buy', optimize=True) # ATR stoploss atr_stoploss_mult = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space='buy', optimize=True) max_stoploss_pct = DecimalParameter(0.03, 0.06, default=0.04, decimals=2, space='buy', optimize=True) # Exit parameters exit_rsi_high = IntParameter(65, 80, default=70, space='sell', optimize=True) exit_rsi_low = IntParameter(20, 35, default=30, space='sell', optimize=True) # RSI guards for entries - STRICTER rsi_long_max = IntParameter(45, 60, default=55, space='buy', optimize=True) rsi_short_min = IntParameter(40, 55, default=45, space='buy', optimize=True) # ═══════════════════════════════════════════════════════════════════════════ # ML CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════ MIN_CANDLES_FOR_TRAINING = 300 # Down from 500 RETRAIN_INTERVAL = 100 ML_FEATURES = [ 'rsi', 'adx', 'atr_pct', 'bb_pct', 'volume_ratio', 'macd_hist', 'pattern_strength', 'trend_strength', 'momentum_quality', 'squeeze' ] # Component weights ML_WEIGHT = 0.35 PATTERN_WEIGHT = 0.35 TREND_WEIGHT = 0.20 MOMENTUM_WEIGHT = 0.10 # ═══════════════════════════════════════════════════════════════════════════ # INITIALIZATION # ═══════════════════════════════════════════════════════════════════════════ def __init__(self, config: dict) -> None: super().__init__(config) self.model_dir = Path(config.get('user_data_dir', 'user_data')) / 'models' / 'bot_owsky_v3' self.model_dir.mkdir(parents=True, exist_ok=True) # ML models self.rf_long = None self.rf_short = None self.gb_long = None self.gb_short = None self.scaler = RobustScaler() self.models_loaded = False self.last_train_candle = 0 # Managers self.coin_confidence = CoinConfidenceManagerV3(str(self.model_dir)) self.pattern_analyzer = PatternAnalyzerV3() # Current regime tracking self.current_regimes = {} logger.info("BOT-OWSKY V3.0 initialized - Pattern-First ML Strategy") # ═══════════════════════════════════════════════════════════════════════════ # LEVERAGE # ═══════════════════════════════════════════════════════════════════════════ def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: """ Conservative leverage: max 2.0x (down from 5x in VVRP) """ # Base leverage base_leverage = 2.0 # Reduce for low confidence coins confidence = self.coin_confidence.get_coin_confidence(pair) if confidence < 40: base_leverage = 1.5 elif confidence < 30: base_leverage = 1.0 return min(base_leverage, max_leverage) # ═══════════════════════════════════════════════════════════════════════════ # ATR-BASED CUSTOM STOPLOSS # ═══════════════════════════════════════════════════════════════════════════ def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ ATR-based dynamic stoploss instead of fixed percentage. """ try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if len(dataframe) == 0: return -self.max_stoploss_pct.value atr = dataframe['atr'].iloc[-1] entry_price = trade.open_rate # Base: 2x ATR atr_stop = (atr * self.atr_stoploss_mult.value) / entry_price # Tighten as profit increases if current_profit > 0.03: # 3%+ atr_stop *= 0.5 # 1x ATR elif current_profit > 0.015: # 1.5%+ atr_stop *= 0.7 # 1.4x ATR # Cap at max stoploss final_stop = min(atr_stop, self.max_stoploss_pct.value) return -final_stop except Exception as e: logger.warning(f"Error in custom_stoploss: {e}") return -self.max_stoploss_pct.value # ═══════════════════════════════════════════════════════════════════════════ # DECLINING DCA # ═══════════════════════════════════════════════════════════════════════════ def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs) -> Optional[float]: """ Declining DCA: Each addition is 50% of previous (not exponential growth). """ # Only DCA if down 2%+ if current_profit > -0.02: return None filled_entries = trade.nr_of_successful_entries if filled_entries >= 3: # Max 3 entries total return None # Declining stake: 50% of previous # Entry 1: 100%, Entry 2: 50%, Entry 3: 25% dca_multiplier = 0.5 ** filled_entries dca_stake = trade.stake_amount * dca_multiplier # Minimum viable stake if min_stake and dca_stake < min_stake: return None return dca_stake # ═══════════════════════════════════════════════════════════════════════════ # CUSTOM STAKE AMOUNT # ═══════════════════════════════════════════════════════════════════════════ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: """ Confidence-based position sizing. """ # Base: 80% of proposed base_stake = proposed_stake * 0.8 # Confidence multiplier: 0.6x to 1.2x confidence = self.coin_confidence.get_coin_confidence(pair) conf_multiplier = 0.6 + (confidence / 100) * 0.6 final_stake = base_stake * conf_multiplier return max(min_stake or 0, min(final_stake, max_stake)) # ═══════════════════════════════════════════════════════════════════════════ # DIVERGENCE DETECTION # ═══════════════════════════════════════════════════════════════════════════ def detect_divergence(self, dataframe: DataFrame, lookback: int = 15) -> Tuple[Series, Series]: """Detect bullish and bearish RSI divergences.""" bullish_div = pd.Series(0, index=dataframe.index) bearish_div = pd.Series(0, index=dataframe.index) if len(dataframe) < lookback + 5: return bullish_div, bearish_div close = dataframe['close'].values rsi = dataframe['rsi'].values for i in range(lookback, len(dataframe)): price_window = close[i-lookback:i+1] rsi_window = rsi[i-lookback:i+1] price_min_idx = np.argmin(price_window) price_max_idx = np.argmax(price_window) # Bullish: Price lower low, RSI higher low if price_min_idx > lookback // 2: prev_low_idx = np.argmin(price_window[:lookback//2]) if price_window[price_min_idx] < price_window[prev_low_idx]: if rsi_window[price_min_idx] > rsi_window[prev_low_idx]: bullish_div.iloc[i] = 1 # Bearish: Price higher high, RSI lower high if price_max_idx > lookback // 2: prev_high_idx = np.argmax(price_window[:lookback//2]) if price_window[price_max_idx] > price_window[prev_high_idx]: if rsi_window[price_max_idx] < rsi_window[prev_high_idx]: bearish_div.iloc[i] = 1 return bullish_div, bearish_div # ═══════════════════════════════════════════════════════════════════════════ # MARKET REGIME DETECTION (Simplified to 3 states) # ═══════════════════════════════════════════════════════════════════════════ def detect_market_regime(self, dataframe: DataFrame) -> Series: """ Simplified 3-state regime: - trending: ADX > 30 - weak_trend: ADX 20-30 - ranging: ADX < 20 """ regime = pd.Series('ranging', index=dataframe.index) adx = dataframe['adx'] ema_diff = (dataframe['ema_fast'] - dataframe['ema_slow']) / dataframe['ema_slow'] * 100 # Strong trend strong_up = (adx > self.adx_strong_threshold.value) & (ema_diff > 0.5) strong_down = (adx > self.adx_strong_threshold.value) & (ema_diff < -0.5) # Weak trend weak_up = (adx > self.adx_weak_threshold.value) & (adx <= self.adx_strong_threshold.value) & (ema_diff > 0) weak_down = (adx > self.adx_weak_threshold.value) & (adx <= self.adx_strong_threshold.value) & (ema_diff < 0) regime.loc[strong_up] = 'strong_uptrend' regime.loc[strong_down] = 'strong_downtrend' regime.loc[weak_up] = 'weak_uptrend' regime.loc[weak_down] = 'weak_downtrend' return regime # ═══════════════════════════════════════════════════════════════════════════ # ML TRAINING # ═══════════════════════════════════════════════════════════════════════════ def train_ml_models(self, dataframe: DataFrame) -> bool: """Train ML models on historical data.""" if len(dataframe) < self.MIN_CANDLES_FOR_TRAINING: return False try: # Prepare features features = self._prepare_ml_features(dataframe) if features is None or len(features) < self.MIN_CANDLES_FOR_TRAINING: return False # Create labels: +1.5% within 12 candles = success labels_long = self._create_labels(dataframe, direction='long', threshold=0.015, horizon=12) labels_short = self._create_labels(dataframe, direction='short', threshold=0.015, horizon=12) # Align data valid_idx = ~(features.isna().any(axis=1) | labels_long.isna() | labels_short.isna()) X = features.loc[valid_idx] y_long = labels_long.loc[valid_idx] y_short = labels_short.loc[valid_idx] if len(X) < 200: return False # Scale features X_scaled = self.scaler.fit_transform(X) # Train models self.rf_long = RandomForestClassifier(n_estimators=100, max_depth=8, min_samples_leaf=10, n_jobs=-1, random_state=42) self.rf_short = RandomForestClassifier(n_estimators=100, max_depth=8, min_samples_leaf=10, n_jobs=-1, random_state=42) self.gb_long = GradientBoostingClassifier(n_estimators=50, max_depth=5, learning_rate=0.1, random_state=42) self.gb_short = GradientBoostingClassifier(n_estimators=50, max_depth=5, learning_rate=0.1, random_state=42) self.rf_long.fit(X_scaled, y_long) self.rf_short.fit(X_scaled, y_short) self.gb_long.fit(X_scaled, y_long) self.gb_short.fit(X_scaled, y_short) self.models_loaded = True self.last_train_candle = len(dataframe) logger.info(f"V3 ML models trained on {len(X)} samples") return True except Exception as e: logger.error(f"Error training ML models: {e}") return False def _prepare_ml_features(self, dataframe: DataFrame) -> Optional[DataFrame]: """Prepare feature matrix for ML.""" required = ['rsi', 'adx', 'atr', 'bb_pct', 'volume_ratio', 'macd_hist', 'pattern_strength', 'ema_fast', 'ema_slow'] if not all(col in dataframe.columns for col in required): return None features = pd.DataFrame(index=dataframe.index) features['rsi'] = dataframe['rsi'] features['adx'] = dataframe['adx'] features['atr_pct'] = dataframe['atr'] / dataframe['close'] * 100 features['bb_pct'] = dataframe['bb_pct'] features['volume_ratio'] = dataframe['volume_ratio'] features['macd_hist'] = dataframe['macd_hist'] features['pattern_strength'] = dataframe['pattern_strength'] features['trend_strength'] = (dataframe['ema_fast'] - dataframe['ema_slow']) / dataframe['ema_slow'] * 100 features['momentum_quality'] = dataframe.get('momentum_quality', 50) features['squeeze'] = dataframe.get('squeeze', 0).astype(float) return features def _create_labels(self, dataframe: DataFrame, direction: str, threshold: float, horizon: int) -> Series: """Create binary labels for ML training.""" labels = pd.Series(0, index=dataframe.index) close = dataframe['close'].values for i in range(len(close) - horizon): future_max = np.max(close[i+1:i+horizon+1]) future_min = np.min(close[i+1:i+horizon+1]) if direction == 'long': if (future_max - close[i]) / close[i] >= threshold: labels.iloc[i] = 1 else: # short if (close[i] - future_min) / close[i] >= threshold: labels.iloc[i] = 1 return labels def _get_ml_predictions(self, dataframe: DataFrame) -> Tuple[Series, Series]: """Get ML ensemble predictions.""" if not self.models_loaded: return pd.Series(0.5, index=dataframe.index), pd.Series(0.5, index=dataframe.index) try: features = self._prepare_ml_features(dataframe) if features is None: return pd.Series(0.5, index=dataframe.index), pd.Series(0.5, index=dataframe.index) X_scaled = self.scaler.transform(features.fillna(0)) # RF predictions (50% weight) rf_long = self.rf_long.predict_proba(X_scaled)[:, 1] rf_short = self.rf_short.predict_proba(X_scaled)[:, 1] # GB predictions (30% weight) gb_long = self.gb_long.predict_proba(X_scaled)[:, 1] gb_short = self.gb_short.predict_proba(X_scaled)[:, 1] # Ensemble ml_long = rf_long * 0.6 + gb_long * 0.4 ml_short = rf_short * 0.6 + gb_short * 0.4 return pd.Series(ml_long, index=dataframe.index), pd.Series(ml_short, index=dataframe.index) except Exception as e: logger.warning(f"ML prediction error: {e}") return pd.Series(0.5, index=dataframe.index), pd.Series(0.5, index=dataframe.index) # ═══════════════════════════════════════════════════════════════════════════ # POPULATE INDICATORS # ═══════════════════════════════════════════════════════════════════════════ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """Calculate all indicators.""" pair = metadata['pair'] # Basic indicators dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.rsi_period.value) dataframe['adx'] = ta.ADX(dataframe, timeperiod=14) dataframe['atr'] = ta.ATR(dataframe, timeperiod=self.atr_period.value) # EMAs dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=self.ema_fast.value) dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=self.ema_slow.value) # Bollinger Bands bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) dataframe['bb_upper'] = bollinger['upperband'] dataframe['bb_middle'] = bollinger['middleband'] dataframe['bb_lower'] = bollinger['lowerband'] dataframe['bb_width'] = (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle'] dataframe['bb_pct'] = (dataframe['close'] - dataframe['bb_lower']) / (dataframe['bb_upper'] - dataframe['bb_lower']) # MACD macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9) dataframe['macd'] = macd['macd'] dataframe['macd_signal'] = macd['macdsignal'] dataframe['macd_hist'] = macd['macdhist'] # Volume dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean() dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_ma'] # Keltner for squeeze detection dataframe['kc_upper'] = ta.EMA(dataframe, timeperiod=20) + ta.ATR(dataframe, timeperiod=14) * 1.5 dataframe['kc_lower'] = ta.EMA(dataframe, timeperiod=20) - ta.ATR(dataframe, timeperiod=14) * 1.5 dataframe['squeeze'] = (dataframe['bb_lower'] > dataframe['kc_lower']) & (dataframe['bb_upper'] < dataframe['kc_upper']) # Momentum quality roc = ta.ROC(dataframe, timeperiod=10) dataframe['momentum_quality'] = ( dataframe['rsi'].clip(0, 100) * 0.4 + (dataframe['macd_hist'] / dataframe['close'] * 1000).clip(-50, 50) + 50 * 0.3 + (roc.clip(-10, 10) + 10) * 5 * 0.3 ) # Pattern analysis dataframe = self.pattern_analyzer.analyze(dataframe) # Divergence if self.use_divergence.value: bullish_div, bearish_div = self.detect_divergence(dataframe, self.divergence_lookback.value) dataframe['bullish_div'] = bullish_div dataframe['bearish_div'] = bearish_div else: dataframe['bullish_div'] = 0 dataframe['bearish_div'] = 0 # Market regime dataframe['regime'] = self.detect_market_regime(dataframe) self.current_regimes[pair] = dataframe['regime'].iloc[-1] if len(dataframe) > 0 else 'unknown' # Trend strength (normalized 0-1) trend_diff = (dataframe['ema_fast'] - dataframe['ema_slow']) / dataframe['ema_slow'] * 100 dataframe['trend_strength'] = trend_diff dataframe['trend_score'] = (trend_diff.clip(-5, 5) + 5) / 10 # ML predictions need_retrain = ( not self.models_loaded or len(dataframe) - self.last_train_candle > self.RETRAIN_INTERVAL ) if need_retrain and len(dataframe) >= self.MIN_CANDLES_FOR_TRAINING: self.train_ml_models(dataframe) ml_long, ml_short = self._get_ml_predictions(dataframe) dataframe['ml_long'] = ml_long dataframe['ml_short'] = ml_short # Composite scores dataframe['composite_long'] = self._calculate_composite_score(dataframe, 'long') dataframe['composite_short'] = self._calculate_composite_score(dataframe, 'short') return dataframe def _calculate_composite_score(self, dataframe: DataFrame, direction: str) -> Series: """ Calculate composite entry score with V3 weights: - ML: 35% - Pattern: 35% - Trend: 20% - Momentum: 10% """ if direction == 'long': ml_score = dataframe['ml_long'] pattern_score = dataframe['pattern_score'] trend_score = dataframe['trend_score'] else: ml_score = dataframe['ml_short'] pattern_score = 1 - dataframe['pattern_score'] # Invert for shorts trend_score = 1 - dataframe['trend_score'] # Momentum score (normalized 0-1) momentum_score = dataframe['momentum_quality'] / 100 composite = ( ml_score * self.ML_WEIGHT + pattern_score * self.PATTERN_WEIGHT + trend_score * self.TREND_WEIGHT + momentum_score * self.MOMENTUM_WEIGHT ) # Divergence bonus if direction == 'long': composite = composite + dataframe['bullish_div'] * self.divergence_boost.value else: composite = composite + dataframe['bearish_div'] * self.divergence_boost.value return composite.clip(0, 1) # ═══════════════════════════════════════════════════════════════════════════ # 3-GATE ENTRY SYSTEM # ═══════════════════════════════════════════════════════════════════════════ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ 3-Gate Entry System: 1. Pattern Gate: Min 55% confidence 2. Trend Gate: Never against strong trends 3. ML Gate: Ensemble > 0.58 """ dataframe.loc[:, 'enter_long'] = 0 dataframe.loc[:, 'enter_short'] = 0 # Gate 1: Pattern confidence pattern_ok = dataframe['pattern_confidence'] >= self.pattern_min_confidence.value # Gate 2: Trend alignment (never against strong trends) long_trend_ok = ~( (dataframe['regime'] == 'strong_downtrend') | ((dataframe['adx'] > self.adx_strong_threshold.value) & (dataframe['ema_fast'] < dataframe['ema_slow'])) ) short_trend_ok = ~( (dataframe['regime'] == 'strong_uptrend') | ((dataframe['adx'] > self.adx_strong_threshold.value) & (dataframe['ema_fast'] > dataframe['ema_slow'])) ) # Gate 3: ML ensemble (pattern-first fallback when ML not useful) # If ML predictions are near 0 or 1, they're not calibrated properly ml_avg = (dataframe['ml_long'].mean() + dataframe['ml_short'].mean()) / 2 ml_is_useful = self.models_loaded and 0.2 < ml_avg < 0.8 if ml_is_useful: ml_long_ok = dataframe['ml_long'] >= self.rf_threshold.value ml_short_ok = dataframe['ml_short'] >= self.rf_threshold.value else: # Pattern-first mode: ML gate always passes, rely on pattern + trend + composite ml_long_ok = True ml_short_ok = True # Additional filters volume_ok = dataframe['volume_ratio'] >= self.volume_threshold.value rsi_long_ok = dataframe['rsi'] < self.rsi_long_max.value rsi_short_ok = dataframe['rsi'] > self.rsi_short_min.value composite_long_ok = dataframe['composite_long'] >= self.composite_threshold.value composite_short_ok = dataframe['composite_short'] >= self.composite_threshold.value # Long entries long_conditions = ( pattern_ok & long_trend_ok & ml_long_ok & volume_ok & rsi_long_ok & composite_long_ok ) # Short entries short_conditions = ( pattern_ok & short_trend_ok & ml_short_ok & volume_ok & rsi_short_ok & composite_short_ok ) dataframe.loc[long_conditions, 'enter_long'] = 1 dataframe.loc[short_conditions, 'enter_short'] = 1 # Entry tags dataframe.loc[long_conditions, 'enter_tag'] = dataframe.apply( lambda row: self._create_entry_tag(row, 'long'), axis=1 ) dataframe.loc[short_conditions, 'enter_tag'] = dataframe.apply( lambda row: self._create_entry_tag(row, 'short'), axis=1 ) return dataframe def _create_entry_tag(self, row, direction: str) -> str: """Create informative entry tag.""" import math dir_code = 'L' if direction == 'long' else 'S' regime = str(row.get('regime', 'unknown'))[:2].upper() rsi_val = row.get('rsi', 50) adx_val = row.get('adx', 20) conf_val = row.get('pattern_confidence', 50) rsi = int(rsi_val) if not (isinstance(rsi_val, float) and math.isnan(rsi_val)) else 50 adx = int(adx_val) if not (isinstance(adx_val, float) and math.isnan(adx_val)) else 20 conf = int(conf_val) if not (isinstance(conf_val, float) and math.isnan(conf_val)) else 50 patterns = self.pattern_analyzer.get_active_patterns(row) pattern_code = '_'.join(patterns[:2]) if patterns else 'NP' return f"{dir_code}_{regime}_r{rsi}_a{adx}_c{conf}_{pattern_code}_v3" # ═══════════════════════════════════════════════════════════════════════════ # EXIT LOGIC # ═══════════════════════════════════════════════════════════════════════════ def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """Exit based on RSI extremes and trend reversal.""" dataframe.loc[:, 'exit_long'] = 0 dataframe.loc[:, 'exit_short'] = 0 # RSI-based exits dataframe.loc[ (dataframe['rsi'] > self.exit_rsi_high.value) & (dataframe['macd_hist'] < dataframe['macd_hist'].shift(1)), 'exit_long' ] = 1 dataframe.loc[ (dataframe['rsi'] < self.exit_rsi_low.value) & (dataframe['macd_hist'] > dataframe['macd_hist'].shift(1)), 'exit_short' ] = 1 return dataframe def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[str]: """Priority-ordered exit system.""" try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if len(dataframe) == 0: return None last = dataframe.iloc[-1] is_long = not trade.is_short # Priority 1: Regime reversal if is_long and last['regime'] == 'strong_downtrend': return 'regime_reversal' if not is_long and last['regime'] == 'strong_uptrend': return 'regime_reversal' # Priority 2: Pattern reversal with high confidence if is_long and last['pattern_strength'] < -2 and last['pattern_confidence'] > 65: return 'bearish_pattern' if not is_long and last['pattern_strength'] > 2 and last['pattern_confidence'] > 65: return 'bullish_pattern' # Priority 3: Time-based (stagnant trade) trade_hours = (current_time - trade.open_date_utc).total_seconds() / 3600 if trade_hours > 24 and abs(current_profit) < 0.005: return 'time_exit_stagnant' return None except Exception as e: logger.warning(f"Error in custom_exit: {e}") return None # ═══════════════════════════════════════════════════════════════════════════ # TRADE RESULT CALLBACK # ═══════════════════════════════════════════════════════════════════════════ def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: """Update confidence manager on trade exit.""" try: profit = trade.calc_profit_ratio(rate) duration = (current_time - trade.open_date_utc).total_seconds() / 60 # Extract patterns and regime from entry tag patterns = [] regime = 'unknown' if trade.enter_tag: parts = trade.enter_tag.split('_') for part in parts: if part.startswith('CDL'): patterns.append(part) if len(parts) >= 2: regime_code = parts[1] regime_map = {'ST': 'strong_trend', 'WE': 'weak_trend', 'RA': 'ranging'} regime = regime_map.get(regime_code, 'unknown') self.coin_confidence.update_trade_result( pair=pair, profit=profit, duration_minutes=int(duration), entry_patterns=patterns, exit_reason=exit_reason, regime=regime ) except Exception as e: logger.warning(f"Error updating confidence: {e}") return True