# --- Do not remove these libs ---
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from pathlib import Path
import logging
import time
current_time = time.time()
import datetime
from datetime import datetime, timedelta, timezone
import numpy as np
import pandas as pd
pd.options.mode.chained_assignment = None
from pandas import DataFrame, Series
from technical.util import resample_to_interval, resampled_merge
from freqtrade.strategy import (IStrategy, BooleanParameter, CategoricalParameter, DecimalParameter,
IntParameter, RealParameter, merge_informative_pair, stoploss_from_open,
stoploss_from_absolute, merge_informative_pair)
from freqtrade.persistence import Trade
from typing import List, Tuple, Optional, Dict, Any
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
import json
import os
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from collections import deque
import optuna
from optuna.samplers import TPESampler
from optuna.exceptions import OptunaError
import warnings
warnings.simplefilter(action="ignore", category=pd.errors.PerformanceWarning)
logger = logging.getLogger(__name__)
ASCII_ART = '''
╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ██████╗ ██╗ ███████╗██╗ ██╗ ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ ██████╗ ██╗ ██╗██╗███╗ ██╗ ██████╗ ║
║ ██╔══██╗██║ ██╔════╝╚██╗██╔╝██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝██╔═══██╗██║ ██╔╝██║████╗ ██║██╔════╝ ║
║ ███████║██║ █████╗ ╚███╔╝ ██║ ██████╔╝ ╚████╔╝ ██████╔╝ ██║ ██║ ██║█████╔╝ ██║██╔██╗ ██║██║ ███╗ ║
║ ██╔══██║██║ ██╔══╝ ██╔██╗ ██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ ██║ ██║██╔═██╗ ██║██║╚██╗██║██║ ██║ ║
║ ██║ ██║███████╗███████╗██╔╝ ██╗╚██████╗██║ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██╗██║██║ ╚████║╚██████╔╝ ║
║ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ║
║ ║
║ PURE OPTUNA OPTIMIZING STRATEGY ║
║ Enhanced Multi-Feature Optimazion ║
║ Machine Learning Powered ║
║ ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
'''
# FIX: Remove the problematic relative import and create a simple inline replacement
# from .optuna_manager import OptunaManager
class RealOptunaManager:
"""Real Optuna-based optimization manager"""
def __init__(self, strategy_name: str):
self.strategy_name = strategy_name
self.studies = {} # Store studies per pair
self.best_params_cache = {}
self.performance_history = {}
self.optimization_trigger_count = {}
self.pretraining_enabled = True
self.pretraining_days = 14
self.pretraining_done = {}
logger.info(f"📊 [OPTUNA] Initializing RealOptunaManager for strategy: {strategy_name}")
logger.info(f"🎯 [OPTUNA] Real Optuna optimization enabled")
def get_best_params(self, pair: str) -> Dict[str, Any]:
"""Get best parameters for a pair"""
if pair in self.best_params_cache:
return self.best_params_cache[pair]
# Try to load from existing study
try:
study_name = f"{self.strategy_name}_{pair.replace('/', '_').replace(':', '_')}"
study = optuna.load_study(
study_name=study_name,
storage=f"sqlite:///user_data/strategies/optuna_studies/optuna_{study_name}.db"
)
if study.trials:
self.studies[pair] = study
self.best_params_cache[pair] = study.best_params
return study.best_params
except:
pass
return None
def should_optimize(self, pair: str) -> bool:
"""Check if optimization should be triggered"""
if pair not in self.best_params_cache:
return True
if pair in self.performance_history:
recent_performance = self.performance_history[pair][-10:]
if len(recent_performance) >= 5:
avg_performance = sum(recent_performance) / len(recent_performance)
if avg_performance < -0.02:
return True
return False
def should_optimize_based_on_performance(self, pair: str) -> bool:
"""Check if performance-based optimization is needed"""
if pair not in self.performance_history:
return False
recent_trades = self.performance_history[pair][-10:]
if len(recent_trades) >= 5:
avg_performance = sum(recent_trades) / len(recent_trades)
return avg_performance < -0.015
return False
def update_performance(self, pair: str, profit_ratio: float):
"""Update performance tracking"""
if pair not in self.performance_history:
self.performance_history[pair] = []
self.performance_history[pair].append(profit_ratio)
if len(self.performance_history[pair]) > 50:
self.performance_history[pair] = self.performance_history[pair][-50:]
logger.debug(f"📈 [OPTUNA] Updated performance for {pair}: {profit_ratio:.4f}")
def optimize_coin(self, pair: str, objective_func, n_trials: int = 10):
"""Optimize parameters for a specific coin using real Optuna"""
logger.info(f"🚀 [OPTUNA] Starting REAL optimization for {pair} with {n_trials} trials")
try:
optuna.logging.set_verbosity(optuna.logging.WARNING)
from pathlib import Path
study_name = f"{self.strategy_name}_{pair.replace('/', '_').replace(':', '_')}"
Path("user_data/strategies/optuna_studies").mkdir(parents=True, exist_ok=True)
study = optuna.create_study(
study_name=study_name,
direction='maximize',
sampler=TPESampler(),
storage=f"sqlite:///user_data/strategies/optuna_studies/optuna_{study_name}.db",
load_if_exists=True
)
study.optimize(objective_func, n_trials=n_trials)
self.studies[pair] = study
self.best_params_cache[pair] = study.best_params
logger.info(f"✅ [OPTUNA] REAL optimization completed for {pair}")
logger.info(f"🏆 [OPTUNA] Best value: {study.best_value:.4f}")
self._log_formatted_parameters(pair, study.best_params)
return True
except Exception as e:
logger.error(f"❌ [OPTUNA] Real optimization failed for {pair}: {e}")
return False
def _log_formatted_parameters(self, pair: str, params: Dict[str, Any]):
"""Log parameters in a nicely formatted way - Enhanced with all optimized parameters"""
logger.info(f"📈 [OPTUNA] Optimized parameters for {pair}:")
# === INDICATOR PARAMETERS ===
logger.info(f"┌─ 📊 TECHNICAL INDICATOR PARAMETERS")
logger.info(f"│ • rsi_period: {params.get('rsi_period', 'N/A')}")
logger.info(f"│ • willr_period: {params.get('willr_period', 'N/A')}")
logger.info(f"│ • cci_period: {params.get('cci_period', 'N/A')}")
logger.info(f"│ • bb_period: {params.get('bb_period', 'N/A')}")
logger.info(f"│ • bb_std: {params.get('bb_std', 'N/A')}")
logger.info(f"│ • macd_fast/slow/signal: {params.get('macd_fast', 'N/A')}/{params.get('macd_slow', 'N/A')}/{params.get('macd_signal', 'N/A')}")
logger.info(f"│ • ema_short/long: {params.get('ema_short_period', 'N/A')}/{params.get('ema_long_period', 'N/A')}")
logger.info(f"│ • volume_sma_period: {params.get('volume_sma_period', 'N/A')}")
logger.info(f"│ • atr_period: {params.get('atr_period', 'N/A')}")
logger.info(f"│ • pivot_window: {params.get('pivot_window', 'N/A')}")
logger.info(f"│ • swing_period: {params.get('swing_period', 'N/A')}")
# === CORE ENTRY PARAMETERS ===
logger.info(f"├─ 🎯 CORE ENTRY PARAMETERS")
logger.info(f"│ • min_divergence_count: {params.get('min_divergence_count', 'N/A')}")
logger.info(f"│ • min_signal_strength: {params.get('min_signal_strength', 'N/A')}")
logger.info(f"│ • volume_threshold: {params.get('volume_threshold', 'N/A')}")
logger.info(f"│ • volume_factor: {params.get('volume_factor', 'N/A')}")
logger.info(f"│ • atr_multiplier: {params.get('atr_multiplier', 'N/A')}")
logger.info(f"│ • adx_threshold: {params.get('adx_threshold', 'N/A')}")
logger.info(f"│ • max_volatility: {params.get('max_volatility', 'N/A')}")
logger.info(f"│ • min_volatility: {params.get('min_volatility', 'N/A')}")
# === RSI PARAMETERS ===
logger.info(f"├─ 📈 RSI TIMING PARAMETERS")
logger.info(f"│ • rsi_overbought: {params.get('rsi_overbought', 'N/A')}")
logger.info(f"│ • rsi_oversold: {params.get('rsi_oversold', 'N/A')}")
logger.info(f"│ • rsi_long_upper_tight: {params.get('rsi_long_upper_tight', 'N/A')}")
logger.info(f"│ • rsi_short_lower_tight: {params.get('rsi_short_lower_tight', 'N/A')}")
logger.info(f"│ • rsi_recovery_periods: {params.get('rsi_recovery_periods', 'N/A')}")
# === VOLUME PARAMETERS ===
logger.info(f"├─ 📊 VOLUME PARAMETERS")
logger.info(f"│ • volume_ratio_min: {params.get('volume_ratio_min', 'N/A')}")
logger.info(f"│ • volume_trend_multiplier: {params.get('volume_trend_multiplier', 'N/A')}")
logger.info(f"│ • volume_momentum_multiplier: {params.get('volume_momentum_multiplier', 'N/A')}")
logger.info(f"│ • volume_breakout_multiplier: {params.get('volume_breakout_multiplier', 'N/A')}")
logger.info(f"│ • volume_breakout_rsi_min/max: {params.get('volume_breakout_rsi_min', 'N/A')}/{params.get('volume_breakout_rsi_max', 'N/A')}")
# === POSITION TIMING ===
logger.info(f"├─ ⏰ POSITION TIMING")
logger.info(f"│ • bb_percent_long_max: {params.get('bb_percent_long_max', 'N/A')}")
logger.info(f"│ • bb_percent_short_min: {params.get('bb_percent_short_min', 'N/A')}")
logger.info(f"│ • price_position_long_max: {params.get('price_position_long_max', 'N/A')}")
logger.info(f"│ • price_position_short_min: {params.get('price_position_short_min', 'N/A')}")
# === MARKET STRUCTURE ===
logger.info(f"├─ 🏗️ MARKET STRUCTURE")
logger.info(f"│ • adx_trending_min: {params.get('adx_trending_min', 'N/A')}")
logger.info(f"│ • chop_threshold: {params.get('chop_threshold', 'N/A')}")
logger.info(f"│ • min_oversold_conditions: {params.get('min_oversold_conditions', 'N/A')}")
logger.info(f"│ • min_overbought_conditions: {params.get('min_overbought_conditions', 'N/A')}")
logger.info(f"│ • min_reversal_signals: {params.get('min_reversal_signals', 'N/A')}")
logger.info(f"│ • min_trend_filters: {params.get('min_trend_filters', 'N/A')}")
# === MEAN REVERSION PARAMETERS ===
logger.info(f"├─ 🔄 MEAN REVERSION PARAMETERS")
logger.info(f"│ • bb_oversold_threshold: {params.get('bb_oversold_threshold', 'N/A')}")
logger.info(f"│ • bb_overbought_threshold: {params.get('bb_overbought_threshold', 'N/A')}")
logger.info(f"│ • mean_reversion_rsi_oversold: {params.get('mean_reversion_rsi_oversold', 'N/A')}")
logger.info(f"│ • mean_reversion_rsi_overbought: {params.get('mean_reversion_rsi_overbought', 'N/A')}")
# === MOMENTUM CONTINUATION PARAMETERS ===
logger.info(f"├─ 🚀 MOMENTUM CONTINUATION PARAMETERS")
logger.info(f"│ • momentum_ema_periods: {params.get('momentum_ema_periods', 'N/A')}")
logger.info(f"│ • momentum_strength_min: {params.get('momentum_strength_min', 'N/A')}")
logger.info(f"│ • momentum_pullback_max: {params.get('momentum_pullback_max', 'N/A')}")
logger.info(f"│ • momentum_pullback_min: {params.get('momentum_pullback_min', 'N/A')}")
# === EXIT PARAMETERS ===
logger.info(f"├─ 🎯 GLOBAL EXIT SETTINGS")
logger.info(f"│ • session_multiplier_overlap: {params.get('session_multiplier_overlap', 'N/A')}")
logger.info(f"│ • session_multiplier_major: {params.get('session_multiplier_major', 'N/A')}")
logger.info(f"│ • volatility_sensitivity: {params.get('volatility_sensitivity', 'N/A')}")
logger.info(f"│ • high_signal_multiplier: {params.get('high_signal_multiplier', 'N/A')}")
logger.info(f"│ • low_signal_multiplier: {params.get('low_signal_multiplier', 'N/A')}")
logger.info(f"├─ 🔄 MEAN REVERSION (MR1) EXITS")
logger.info(f"│ • mr1_max_hold_minutes: {params.get('mr1_max_hold_minutes', 'N/A')} min")
logger.info(f"│ • mr1_rsi_exit_long/short: {params.get('mr1_rsi_exit_long', 'N/A')}/{params.get('mr1_rsi_exit_short', 'N/A')}")
logger.info(f"│ • mr1_quick_profit_target: {params.get('mr1_quick_profit_target', 'N/A')}")
logger.info(f"│ • mr1_timeout_min_profit: {params.get('mr1_timeout_min_profit', 'N/A')}")
logger.info(f"├─ 🚀 MOMENTUM CONTINUATION (MC1) EXITS")
logger.info(f"│ • mc1_profit_target: {params.get('mc1_profit_target', 'N/A')}")
logger.info(f"│ • mc1_max_hold_minutes: {params.get('mc1_max_hold_minutes', 'N/A')} min")
logger.info(f"│ • mc1_adx_exit_threshold: {params.get('mc1_adx_exit_threshold', 'N/A')}")
logger.info(f"│ • mc1_timeout_min_profit: {params.get('mc1_timeout_min_profit', 'N/A')}")
logger.info(f"├─ 📊 VOLUME BREAKOUT (VB1) EXITS")
logger.info(f"│ • vb1_volume_fade_threshold: {params.get('vb1_volume_fade_threshold', 'N/A')}")
logger.info(f"│ • vb1_profit_target: {params.get('vb1_profit_target', 'N/A')}")
logger.info(f"│ • vb1_quick_profit: {params.get('vb1_quick_profit', 'N/A')}")
logger.info(f"├─ 🔄 REVERSAL (RSV1) EXITS")
logger.info(f"│ • rsv1_rsi_recovery_threshold: {params.get('rsv1_rsi_recovery_threshold', 'N/A')}")
logger.info(f"│ • rsv1_profit_target: {params.get('rsv1_profit_target', 'N/A')}")
logger.info(f"│ • rsv1_max_hold_minutes: {params.get('rsv1_max_hold_minutes', 'N/A')} min")
logger.info(f"│ • rsv1_min_timeout_profit: {params.get('rsv1_min_timeout_profit', 'N/A')}")
logger.info(f"├─ 📈 TREND FOLLOWING EXITS")
logger.info(f"│ • trend_profit_target: {params.get('trend_profit_target', 'N/A')}")
logger.info(f"│ • trend_ema_break_periods: {params.get('trend_ema_break_periods', 'N/A')}")
logger.info(f"├─ ⏰ UNIVERSAL EXIT PARAMETERS")
logger.info(f"│ • medium_term_base_target: {params.get('medium_term_base_target', 'N/A')}")
logger.info(f"│ • rsi_exit_overbought/oversold: {params.get('rsi_exit_overbought', 'N/A')}/{params.get('rsi_exit_oversold', 'N/A')}")
logger.info(f"│ • momentum_fade_threshold: {params.get('momentum_fade_threshold', 'N/A')}")
logger.info(f"├─ 🕐 EXTENDED DURATION MANAGEMENT")
logger.info(f"│ • extended_2hr_target: {params.get('extended_2hr_target', 'N/A')}")
logger.info(f"│ • extended_3hr_target: {params.get('extended_3hr_target', 'N/A')}")
logger.info(f"│ • extended_5hr_target: {params.get('extended_5hr_target', 'N/A')}")
logger.info(f"├─ 🛡️ MARKET PROTECTION")
logger.info(f"│ • friday_close_min_profit: {params.get('friday_close_min_profit', 'N/A')}")
logger.info(f"│ • overnight_min_profit: {params.get('overnight_min_profit', 'N/A')}")
logger.info(f"│ • low_liquidity_min_profit: {params.get('low_liquidity_min_profit', 'N/A')}")
# === RSV1 SPECIFIC PARAMETERS ===
logger.info(f"├─ 🔄 REVERSAL (RSV1) ENTRY PARAMETERS")
logger.info(f"│ • rsv1_rsi_oversold/overbought: {params.get('rsv1_rsi_oversold', 'N/A')}/{params.get('rsv1_rsi_overbought', 'N/A')}")
logger.info(f"│ • rsv1_willr_oversold/overbought: {params.get('rsv1_willr_oversold', 'N/A')}/{params.get('rsv1_willr_overbought', 'N/A')}")
logger.info(f"│ • rsv1_cci_oversold/overbought: {params.get('rsv1_cci_oversold', 'N/A')}/{params.get('rsv1_cci_overbought', 'N/A')}")
logger.info(f"│ • rsv1_volume_factor: {params.get('rsv1_volume_factor', 'N/A')}")
logger.info(f"│ • rsv1_min_conditions: {params.get('rsv1_min_oversold_conditions', 'N/A')}/{params.get('rsv1_min_overbought_conditions', 'N/A')}")
# === ADVANCED ENTRY PARAMETERS ===
logger.info(f"├─ 🎯 E1 PRIMARY ENTRY PARAMETERS")
logger.info(f"│ • e1_rsi_thresholds: {params.get('e1_rsi_oversold_threshold', 'N/A')}-{params.get('e1_rsi_overbought_threshold', 'N/A')}")
logger.info(f"│ • e1_rsi_buffers: {params.get('e1_rsi_oversold_buffer', 'N/A')}/{params.get('e1_rsi_overbought_buffer', 'N/A')}")
logger.info(f"│ • volatility_multipliers: {params.get('volatility_min_multiplier', 'N/A')}-{params.get('volatility_max_multiplier', 'N/A')}")
logger.info(f"├─ 📈 TREND BREAKOUT PARAMETERS")
logger.info(f"│ • trend_volume_window/multiplier: {params.get('trend_volume_window', 'N/A')}/{params.get('trend_volume_multiplier', 'N/A')}")
logger.info(f"│ • trend_rsi_range: {params.get('trend_rsi_min', 'N/A')}-{params.get('trend_rsi_max', 'N/A')}")
logger.info(f"│ • trend_adx_min: {params.get('trend_adx_min', 'N/A')}")
logger.info(f"├─ 🚀 MOMENTUM ENTRY PARAMETERS")
logger.info(f"│ • momentum_rsi_pivot/trigger: {params.get('momentum_rsi_pivot', 'N/A')}/{params.get('momentum_rsi_trigger', 'N/A')}")
logger.info(f"│ • momentum_volume_multiplier: {params.get('momentum_volume_multiplier', 'N/A')}")
logger.info(f"│ • momentum_adx_min: {params.get('momentum_adx_min', 'N/A')}")
# === SUPPORT/RESISTANCE & BREAKDOWN ===
logger.info(f"├─ 📉 BREAKDOWN/BREAKOUT PARAMETERS")
logger.info(f"│ • breakdown_volume_threshold: {params.get('breakdown_volume_threshold', 'N/A')}")
logger.info(f"│ • breakdown_adx_threshold: {params.get('breakdown_adx_threshold', 'N/A')}")
logger.info(f"│ • breakdown_rsi_range: {params.get('breakdown_rsi_lower', 'N/A')}-{params.get('breakdown_rsi_upper', 'N/A')}")
logger.info(f"│ • support_tolerance: {params.get('support_tolerance', 'N/A')}")
logger.info(f"│ • resistance_tolerance: {params.get('resistance_tolerance', 'N/A')}")
# === SECONDARY/TERTIARY ENTRIES ===
logger.info(f"├─ 📊 SECONDARY/TERTIARY ENTRIES")
logger.info(f"│ • secondary_rsi_range: {params.get('secondary_rsi_min', 'N/A')}-{params.get('secondary_rsi_max', 'N/A')}")
logger.info(f"│ • tertiary_volume_multiplier: {params.get('tertiary_volume_multiplier', 'N/A')}")
logger.info(f"│ • sixth_min_div_count: {params.get('sixth_min_div_count', 'N/A')}")
logger.info(f"│ • seventh_atr_multiplier: {params.get('seventh_atr_multiplier', 'N/A')}")
# === SUMMARY ===
logger.info(f"├─ 📋 OPTIMIZATION SUMMARY")
entry_params = len([k for k in params.keys() if any(x in k for x in ['e1_', 'trend_', 'momentum_', 'secondary_', 'tertiary_', 'rsv1_rsi_', 'breakdown_', 'div_'])])
exit_params = len([k for k in params.keys() if any(x in k for x in ['mr1_', 'mc1_', 'vb1_', 'rsv1_profit', 'extended_', 'session_'])])
indicator_params = len([k for k in params.keys() if any(x in k for x in ['rsi_period', 'bb_period', 'macd_', 'ema_', 'volume_sma', 'atr_period'])])
logger.info(f"│ • Entry parameters optimized: {entry_params}")
logger.info(f"│ • Exit parameters optimized: {exit_params}")
logger.info(f"│ • Indicator parameters optimized: {indicator_params}")
logger.info(f"│ • Total parameters in study: {len(params)}")
logger.info(f"└─ 🎯 Strategy fully optimized with {len(params)} parameters")
logger.info(f"📊 [OPTUNA] Comprehensive optimization completed for {pair}")
def pretrain_with_historical_data(self, pair: str, strategy_instance) -> bool:
"""Stub for compatibility - real Optuna doesn't need pre-training"""
return False
class PlotConfig():
def __init__(self):
self.config = {
'main_plot': {
# Try direct column names first to test
'bollinger_upperband': {'color': 'rgba(4,137,122,0.7)'},
'kc_upperband': {'color': 'rgba(4,146,250,0.7)'},
'kc_middleband': {'color': 'rgba(4,146,250,0.7)'},
'kc_lowerband': {'color': 'rgba(4,146,250,0.7)'},
'bollinger_lowerband': {
'color': 'rgba(4,137,122,0.7)',
'fill_to': 'bollinger_upperband',
'fill_color': 'rgba(4,137,122,0.07)'
},
'ema9': {'color': 'purple'},
'ema20': {'color': 'yellow'},
'ema50': {'color': 'red'},
'ema200': {'color': 'white'},
'trend_1h_1h': {'color': 'orange'},
},
'subplots': {
"RSI": {
'rsi': {'color': 'green'}
},
"ATR": {
'atr': {'color': 'firebrick'}
},
"Signal Strength": {
'signal_strength': {'color': 'blue'}
}
}
}
def add_total_divergences_in_config(self, dataframe):
# Test if columns exist before adding them
if 'total_bullish_divergences' in dataframe.columns:
self.config['main_plot']['total_bullish_divergences'] = {
"plotly": {
'mode': 'markers',
'marker': {
'symbol': 'diamond',
'size': 11,
'color': 'green'
}
}
}
if 'total_bearish_divergences' in dataframe.columns:
self.config['main_plot']['total_bearish_divergences'] = {
"plotly": {
'mode': 'markers',
'marker': {
'symbol': 'diamond',
'size': 11,
'color': 'crimson'
}
}
}
return self
class AlexBandSniperV100CO(IStrategy):
"""
╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ██████╗ ██╗ ███████╗██╗ ██╗ ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ ██████╗ ██╗ ██╗██╗███╗ ██╗ ██████╗ ║
║ ██╔══██╗██║ ██╔════╝╚██╗██╔╝██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝██╔═══██╗██║ ██╔╝██║████╗ ██║██╔════╝ ║
║ ███████║██║ █████╗ ╚███╔╝ ██║ ██████╔╝ ╚████╔╝ ██████╔╝ ██║ ██║ ██║█████╔╝ ██║██╔██╗ ██║██║ ███╗ ║
║ ██╔══██║██║ ██╔══╝ ██╔██╗ ██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ ██║ ██║██╔═██╗ ██║██║╚██╗██║██║ ██║ ║
║ ██║ ██║███████╗███████╗██╔╝ ██╗╚██████╗██║ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██╗██║██║ ╚████║╚██████╔╝ ║
║ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ║
║ ║
║ Advanced RL + Optuna Trading Strategy ║
║ Enhanced Multi-Pair Optimization ║
║ Machine Learning Powered ║
║ ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
Alex BandSniper on 15m Timeframe - OPTIMIZED VERSION WITH OPTUNA INTEGRATION
Version 106CO-Optuna - Claude optimized Entry & Exit with Optuna Management # CHANGED: Versionshinweis angepasst
Key improvements:
- Fixed Entry Tags Assignment
- Removed - Bull Pullbak Momentum & Bear Momentum Breakout
- Bull Pullbak Momentum & Bear Momentum Breakout
- Removed RSV1 Signals because of worse results
- Added better Custom_Exit
- Added Signals from V52C
- Enabled ROI and TRAILING
- Dynamic Trailing & New Custom Exits
- Integrated OPTUNA Fully
- Opimizing Daily Optimizing
- Enhanced Parameters for Optuna
- Optuna integration
- Fixed ROI and Trailing adjusted Custom Exits
- Fixed Entry Signals
- Included 1h Informative Timeframe
- Multi-timeframe analysis (1h trend confirmation)
- Enhanced signal filtering with minimum divergence counts
- Volume and volatility filters
- Adaptive position sizing based on signal strength
- Improved risk management
- ADDED: Optuna-based parameter optimization per coin # ADDED: Neuer Kommentar
- ADDED: Historical pre-training for better startup performance # ADDED: Neuer Kommentar
- ADDED: Dynamic parameter adjustment based on performance # ADDED: Neuer Kommentar
"""
INTERFACE_VERSION = 3
def version(self) -> str:
return "v5-Optuna"
class HyperOpt:
# Define a custom stoploss space.
def stoploss_space():
return [SKDecimal(-0.15, -0.03, decimals=2, name='stoploss')]
# Define a custom max_open_trades space
def max_open_trades_space() -> List[Dimension]:
return [
Integer(3, 8, name='max_open_trades'),
]
def trailing_space() -> List[Dimension]:
return [
Categorical([True], name='trailing_stop'),
SKDecimal(0.02, 0.3, decimals=2, name='trailing_stop_positive'),
SKDecimal(0.03, 0.1, decimals=2, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
# Minimal ROI designed for the strategy.
minimal_roi = {
"0": 0.15, # 15% initial target (let winners run!)
"20": 0.08, # After 20 min, 8%
"45": 0.05, # After 45 min, 5%
"90": 0.03, # After 1.5 hours, 3%
"180": 0.02, # After 3 hours, 2%
"360": 0.01 # After 6 hours, 1%
}
# Optimal stoploss designed for the strategy.
stoploss = -0.20
can_short = True
use_custom_stoploss = False
leverage_value = 10.0 # Reduced leverage for better risk management
#trailing_stop = True
#trailing_stop_positive = 0.025 # Start trailing at 2.5% profit
#trailing_stop_positive_offset = 0.035 # Trail 0.8% behind peak
#trailing_stop = False
#trailing_stop_positive = 0.40 # Only trail after 40% profit (very high)
#trailing_stop_positive_offset = 0.45 # Start trailing at 45% profit
#trailing_only_offset_is_reached = True
# Optimal timeframe for the strategy.
timeframe = '15m'
timeframe_minutes = timeframe_to_minutes(timeframe)
# Run "populate_indicators()" only for new candle.
process_only_new_candles = True
# These values can be overridden in the "exit_pricing" section in the config.
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
use_custom_exits = True
use_entry_signal = True
# In your hyperopt parameters, consider these more permissive defaults:
min_divergence_count = IntParameter(1, 3, default=1, space='buy', optimize=True, load=True) # Reduced from 2-5
min_signal_strength = IntParameter(1, 5, default=1, space='buy', optimize=True, load=True) # Reduced from 3-10
volume_threshold = DecimalParameter(1.0, 1.5, default=1.0, decimals=1, space='buy', optimize=True, load=True) # Reduced from 1.1-2.5
# Make ADX less restrictive
adx_threshold = IntParameter(15, 30, default=15, space='buy', optimize=True, load=True) # Reduced from 25-45
# Market Condition Filters
rsi_overbought = DecimalParameter(65.0, 85.0, default=80.0, decimals=1, space='buy', optimize=True, load=True)
rsi_oversold = DecimalParameter(15.0, 35.0, default=15.0, decimals=1, space='buy', optimize=True, load=True)
# Volatility Filters
max_volatility = DecimalParameter(0.015, 0.035, default=0.025, decimals=3, space='buy', optimize=True, load=True)
min_volatility = DecimalParameter(0.003, 0.008, default=0.005, decimals=3, space='buy', optimize=True, load=True)
# Exit Parameters
rsi_exit_overbought = DecimalParameter(70.0, 90.0, default=80.0, decimals=1, space='sell', optimize=True, load=True)
rsi_exit_oversold = DecimalParameter(10.0, 30.0, default=20.0, decimals=1, space='sell', optimize=True, load=True)
adx_exit_threshold = IntParameter(15, 30, default=20, space='sell', optimize=True, load=True)
# Trend Confirmation Parameters
trend_strength_threshold = IntParameter(20, 40, default=25, space='buy', optimize=True, load=True)
# Technical Parameters
window = IntParameter(3, 6, default=4, space="buy", optimize=True, load=True)
index_range = IntParameter(20, 50, default=30, space='buy', optimize=True, load=True)
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 10
# Protection parameters
cooldown_lookback = IntParameter(2, 48, default=1, space="protection", optimize=True)
stop_duration = IntParameter(12, 200, default=20, space="protection", optimize=True)
use_stop_protection = BooleanParameter(default=False, space="protection", optimize=True)
use_cooldown_protection = BooleanParameter(default=False, space="protection", optimize=True)
# Enhanced protection parameters
use_max_drawdown_protection = BooleanParameter(default=False, space="protection", optimize=True)
max_drawdown_lookback = IntParameter(100, 300, default=200, space="protection", optimize=True)
max_drawdown_trade_limit = IntParameter(5, 15, default=10, space="protection", optimize=True)
max_drawdown_stop_duration = IntParameter(1, 5, default=1, space="protection", optimize=True)
max_allowed_drawdown = DecimalParameter(0.08, 0.25, default=0.15, decimals=2, space="protection", optimize=True)
stoploss_guard_lookback = IntParameter(30, 80, default=50, space="protection", optimize=True)
stoploss_guard_trade_limit = IntParameter(2, 6, default=3, space="protection", optimize=True)
stoploss_guard_only_per_pair = BooleanParameter(default=True, space="protection", optimize=True)
# Optional order type mapping.
order_types = {
'entry': 'market',
'exit': 'market',
'stoploss': 'limit',
'stoploss_on_exchange': True
}
# Optional order time in force.
order_time_in_force = {
'entry': 'gtc',
'exit': 'gtc'
}
plot_config = None
def __init__(self, config: dict):
# Signal Performance Tracking
self.signal_performance_file = Path("user_data/strategies/AlexBandSniper_Signal_Performance.json")
self.signal_performance = self.load_signal_performance()
self.signal_locks = {}
# Configurable parameters - add these to your get_default_params()
self.performance_window = 15 # Trades to evaluate over
self.lock_duration_hours = 12 # Hours to lock signal
self.min_win_rate = 0.35 # Minimum win rate (35%)
self.min_avg_profit = -0.015 # Minimum avg profit (-1.5%)
self.min_trades_for_eval = 8 # Minimum trades before evaluation
super().__init__(config)
print(ASCII_ART, flush=True)
logger.info(f"🚀 [STRATEGY] Initializing {self.__class__.__name__}") # AUTO-UPDATED: Klassenname wird automatisch angepasst
# Initialize simple Optuna manager replacement
try:
self.optuna_manager = RealOptunaManager("AlexBandSniperV96CO") # CHANGED: Strategy name angepasst
logger.info(f"✅ [OPTUNA] Successfully initialized RealOptunaManager")
except Exception as e:
logger.error(f"❌ [OPTUNA] Failed to initialize RealOptunaManager: {e}")
self.optuna_manager = None
# Per-coin optimized parameters
self.coin_params: Dict[str, Dict] = {} # ADDED: Dictionary für coin-spezifische Parameter
# Track performance for optimization
self.coin_performance: Dict[str, float] = {} # ADDED: Performance-Tracking pro Coin
# Optimization settings - CHANGED: Enable proactive optimization
self.enable_dynamic_optimization = True # ADDED: Dynamische Optimierung aktiviert (deaktivieren für Backtest)
self.optimization_min_trades = 0 # CHANGED: No minimum trades required for startup optimization
logger.info(f"⚙️ [STRATEGY] Dynamic optimization: {'ENABLED' if self.enable_dynamic_optimization else 'DISABLED'}")
logger.info(f"⚙️ [STRATEGY] Minimum trades for optimization: {self.optimization_min_trades}")
logger.info(f"✅ [STRATEGY] {self.__class__.__name__} initialization completed")
# ...existing code...
def get_coin_params(self, pair: str) -> Dict[str, Any]:
"""Get optimized parameters for specific coin using real Optuna"""
if pair not in self.coin_params:
logger.debug(f"🔍 [OPTUNA] Loading parameters for new pair: {pair}")
# Check if optimization is disabled first (for backtesting)
if not self.enable_dynamic_optimization:
logger.info(f"⏸️ [OPTUNA] Dynamic optimization DISABLED for {pair} - using default parameters")
self.coin_params[pair] = self.get_default_params()
return self.coin_params[pair]
# Real Optuna optimization - only if enabled
if self.enable_dynamic_optimization and self.optuna_manager:
logger.info(f"🚀 [OPTUNA] Real Optuna will optimize {pair} based on actual trade results")
if self.optuna_manager:
best_params = self.optuna_manager.get_best_params(pair)
if best_params:
self.coin_params[pair] = best_params
logger.info(f"✅ [OPTUNA] Loaded optimized parameters for {pair}: {len(best_params)} params")
logger.debug(f"📊 [OPTUNA] Parameters from study with {len(self.optuna_manager.studies.get(pair, {}).trials) if pair in self.optuna_manager.studies else 0} trials")
else:
self.coin_params[pair] = self.get_default_params()
logger.info(f"🔧 [OPTUNA] Using default parameters for {pair} (no study found - will optimize after trades)")
# ADD THIS SECTION: Force startup optimization for new pairs
if self.enable_dynamic_optimization and self.optuna_manager:
logger.info(f"🎯 [OPTUNA] Triggering STARTUP OPTIMIZATION for new pair: {pair}")
try:
# Call maybe_optimize_coin with force_startup=True
self.maybe_optimize_coin(pair, force_startup=True)
# After optimization, try to reload the optimized parameters
optimized_params = self.optuna_manager.get_best_params(pair)
if optimized_params:
self.coin_params[pair] = optimized_params
logger.info(f"🎉 [OPTUNA] Successfully loaded STARTUP optimized parameters for {pair}")
else:
logger.warning(f"⚠️ [OPTUNA] Startup optimization completed but no parameters returned for {pair}")
except Exception as e:
logger.error(f"❌ [OPTUNA] Startup optimization failed for {pair}: {e}")
# Keep using default parameters if optimization fails
else:
self.coin_params[pair] = self.get_default_params()
logger.warning(f"⚠️ [OPTUNA] RealOptunaManager not available, using default parameters for {pair}")
# ENSURE ALL PARAMETERS ARE PRESENT
default_params = self.get_default_params()
current_params = self.coin_params.get(pair, {})
# Merge defaults with current params (current params override defaults)
complete_params = {**default_params, **current_params}
self.coin_params[pair] = complete_params
logger.debug(f"♻️ [OPTUNA] Using complete parameter set for {pair}")
return self.coin_params[pair]
def get_default_params(self) -> Dict[str, Any]:
"""Get enhanced default parameters including exit optimization"""
return {
# === EXISTING ENTRY PARAMETERS ===
'min_divergence_count': 1, # Default fallback only
'min_signal_strength': 1, # Default fallback only
'volume_threshold': 1.0, # Default fallback only
'adx_threshold': 15, # Default fallback only
'rsi_overbought': 80.0, # Default fallback only
'rsi_oversold': 15.0, # Default fallback only
'max_volatility': 0.025, # Default fallback only
'min_volatility': 0.005, # Default fallback only
# === ADD THESE MISSING INDICATOR PARAMETERS ===
# Technical Indicator Periods
'volume_sma_period': 20, # Volume SMA calculation
'atr_period': 14, # ATR calculation
'rsi_period': 14, # RSI period (if not already defined)
'willr_period': 14, # Williams %R period
'cci_period': 20, # CCI period
'mom_period': 10, # Momentum period
# Bollinger Bands Parameters
'bb_period': 20, # Bollinger Bands period
'bb_std': 2.0, # Bollinger Bands standard deviation
# EMA Parameters
'ema_short_period': 8, # Short EMA period
'ema_long_period': 21, # Long EMA period
# MACD Parameters
'macd_fast': 12, # MACD fast period
'macd_slow': 26, # MACD slow period
'macd_signal': 9, # MACD signal period
# Stochastic Parameters
'stoch_k': 14, # Stochastic K period
'stoch_d': 3, # Stochastic D period
# Market Structure Parameters
'pivot_window': 10, # Pivot points window
'swing_period': 50, # Support/Resistance swing period
'recent_high_low_period': 5, # Recent highs/lows period
'rolling_20_period': 20, # 20-period rolling calculations
'rolling_10_period': 10, # 10-period rolling calculations
# Market Analysis Parameters
'chop_period': 14, # Choppiness index period
'natr_period': 14, # NATR period
'cmf_period': 20, # Chaikin Money Flow period
# === EXISTING ENTRY TIMING PARAMETERS ===
'rsi_recovery_periods': 3,
'rsi_long_upper_tight': 65.0,
'rsi_short_lower_tight': 35.0,
'volume_ratio_min': 1.1,
'volume_trend_multiplier': 1.5,
'volume_momentum_multiplier': 1.2,
'bb_percent_long_max': 0.25,
'bb_percent_short_min': 0.75,
'price_position_long_max': 0.3,
'price_position_short_min': 0.7,
'divergence_freshness_periods': 5,
'min_strong_divergence_count': 2,
'min_oversold_conditions': 3,
'min_overbought_conditions': 3,
'min_reversal_signals': 2,
'min_trend_filters': 1,
'chop_threshold': 61.8,
'adx_trending_min': 25,
'volume_breakout_multiplier': 2.5,
'volume_breakout_rsi_min': 45.0,
'volume_breakout_rsi_max': 60.0,
'bb_oversold_threshold': 0.15,
'bb_overbought_threshold': 0.85,
'mean_reversion_rsi_oversold': 28.0,
'mean_reversion_rsi_overbought': 72.0,
'momentum_ema_periods': 9,
'momentum_strength_min': 0.025,
'momentum_pullback_max': 0.35,
'momentum_pullback_min': 0.65,
# === NEW EXIT PARAMETERS ===
# Global Exit Settings
'emergency_profit_limit': 0.25,
'session_multiplier_overlap': 1.3,
'session_multiplier_major': 1.15,
'session_multiplier_quiet': 0.85,
'volatility_sensitivity': 40,
'high_signal_multiplier': 1.2,
'low_signal_multiplier': 0.8,
# Mean Reversion (MR1) Exit Parameters
'mr1_max_hold_minutes': 35,
'mr1_rsi_exit_long': 68.0,
'mr1_rsi_exit_short': 32.0,
'mr1_quick_profit_target': 0.025,
'mr1_timeout_min_profit': 0.008,
'mr1_bb_exit_long': 0.7,
'mr1_bb_exit_short': 0.3,
# Momentum Continuation (MC1) Exit Parameters
'mc1_profit_target': 0.04,
'mc1_momentum_break_threshold': 0.015,
'mc1_max_hold_minutes': 45,
'mc1_adx_exit_threshold': 22,
'mc1_timeout_min_profit': 0.01,
# Volume Breakout (VB1) Exit Parameters
'vb1_volume_fade_threshold': 1.15,
'vb1_profit_target': 0.03,
'vb1_quick_profit': 0.02,
'vb1_min_volatility': 0.012,
# Reversal (RSV1) Exit Parameters
'rsv1_rsi_recovery_threshold': 58.0,
'rsv1_profit_target': 0.035,
'rsv1_max_hold_minutes': 60,
'rsv1_min_timeout_profit': 0.01,
# Trend Following Exit Parameters
'trend_profit_target': 0.045,
'trend_ema_break_periods': 3,
# Universal Exit Parameters
'medium_term_base_target': 0.05,
'reversal_min_profit_long': 0.008,
'reversal_min_profit_short': 0.008,
'rsi_exit_overbought': 78,
'rsi_exit_oversold': 22,
'rsi_extreme_min_profit': 0.015,
'momentum_fade_min_profit': 0.012,
'momentum_fade_threshold': 0.02,
# Extended Duration Management
'extended_2hr_target': 0.025,
'extended_3hr_target': 0.015,
'extended_5hr_target': 0.008,
# Market Protection
'friday_close_min_profit': 0.012,
'overnight_min_profit': 0.008,
'low_liquidity_min_profit': 0.01,
'rsv1_rsi_oversold': 15,
'rsv1_rsi_overbought': 80,
'rsv1_willr_oversold': -80,
'rsv1_willr_overbought': -20,
'rsv1_cci_oversold': -100,
'rsv1_cci_overbought': 100,
'rsv1_bb_oversold': 0.1,
'rsv1_bb_overbought': 0.9,
'rsv1_volume_factor': 1.5,
'rsv1_price_position_low': 0.2,
'rsv1_price_position_high': 0.8,
'rsv1_min_oversold_conditions': 3,
'rsv1_min_overbought_conditions': 3,
'rsv1_min_reversal_signals': 2,
'rsv1_min_trend_filters': 1,
# Add these to the existing get_default_params() method:
# E1 (Primary) Parameters
'e1_rsi_overbought_threshold': 75,
'e1_rsi_overbought_buffer': 5,
'e1_rsi_oversold_min': 25,
'e1_rsi_oversold_threshold': 25,
'e1_rsi_oversold_buffer': 5,
'e1_rsi_overbought_max': 75,
'volatility_min_multiplier': 0.5,
'volatility_max_multiplier': 2.0,
# Trend Parameters
'trend_volume_window': 20,
'trend_volume_multiplier': 1.5,
'trend_rsi_min': 40,
'trend_rsi_max': 60,
'trend_close_lookback': 2,
'trend_atr_window': 20,
'trend_atr_multiplier': 1.0,
'trend_adx_min': 25,
# Momentum Parameters
'momentum_rsi_lookback': 3,
'momentum_rsi_pivot': 50,
'momentum_rsi_trigger': 55,
'momentum_rsi_trigger_short': 45,
'momentum_high_lookback': 2,
'momentum_low_lookback': 2,
'momentum_volume_window': 10,
'momentum_volume_multiplier': 1.2,
'momentum_kc_upper_factor': 0.995,
'momentum_kc_lower_factor': 1.005,
'momentum_adx_min': 25,
# Secondary Parameters
'secondary_close_lookback': 2,
'secondary_rsi_min': 35,
'secondary_rsi_max': 70,
'secondary_rsi_min_short': 30,
'secondary_rsi_max_short': 80,
# Tertiary Parameters
'tertiary_rsi_min': 40,
'tertiary_rsi_max': 65,
'tertiary_rsi_min_short': 35,
'tertiary_rsi_max_short': 75,
'tertiary_volume_window': 10,
'tertiary_volume_multiplier': 1.0,
# Quaternary Parameters
'quaternary_rsi_min': 45,
'quaternary_rsi_max': 55,
'quaternary_volume_window': 5,
'quaternary_volume_multiplier': 1.2,
# Fifth Parameters
'fifth_rsi_min': 35,
'fifth_rsi_max': 70,
'fifth_rsi_min_short': 30,
'fifth_rsi_max_short': 65,
# Sixth Parameters
'sixth_min_div_count': 2,
'sixth_rsi_min': 30,
'sixth_rsi_max': 75,
'sixth_rsi_min_short': 25,
'sixth_rsi_max_short': 70,
# Seventh Parameters
'seventh_volume_window': 20,
'seventh_volume_multiplier': 1.2,
'seventh_rsi_min': 30,
'seventh_rsi_max': 70,
'seventh_rsi_min_short': 30,
'seventh_rsi_max_short': 70,
'seventh_close_lookback': 2,
'seventh_atr_window': 20,
'seventh_atr_multiplier': 0.5,
'seventh_adx_min': 15,
# Divergence Parameters
'div_rsi_min': 25,
'div_rsi_max': 80,
'div_rsi_min_short': 20,
'div_rsi_max_short': 75,
#SUPPORT
'support_tolerance': 0.005, # 0.5% tolerance for support touch
'support_bounce_min': 0.002, # 0.2% minimum bounce
'resistance_tolerance': 0.005, # 0.5% tolerance for resistance touch
'resistance_bounce_min': 0.002, # 0.2% minimum rejection
'sr_volume_window': 20, # Volume average window
'sr_volume_multiplier': 1.3, # Volume surge multiplier
'sr_rsi_min_long': 25, # RSI minimum for long S/R
'sr_rsi_max_long': 70, # RSI maximum for long S/R
'sr_rsi_min_short': 30, # RSI minimum for short S/R
'sr_rsi_max_short': 75, # RSI maximum for short S/R
'sr_price_position_max_long': 0.6, # Max price position for long
'sr_price_position_min_short': 0.4, # Min price position for short
'sr_bb_max_long': 0.7, # Max BB% for long entries
'sr_bb_min_short': 0.3, # Min BB% for short entries
'sr_momentum_lookback': 2, # Momentum confirmation lookback
'sr_adx_min': 15, # Minimum ADX for S/R
# Strong S/R Parameters
'bb_support_tolerance': 0.01, # BB support tolerance
'bb_resistance_tolerance': 0.01, # BB resistance tolerance
'kc_support_tolerance': 0.01, # KC support tolerance
'kc_resistance_tolerance': 0.01, # KC resistance tolerance
'ema_support_tolerance': 0.008, # EMA support tolerance
'ema_resistance_tolerance': 0.008, # EMA resistance tolerance
'strong_sr_volume_multiplier': 1.8, # Strong S/R volume multiplier
'strong_sr_rsi_min_long': 20, # Strong S/R RSI min long
'strong_sr_rsi_max_long': 50, # Strong S/R RSI max long
'strong_sr_rsi_min_short': 50, # Strong S/R RSI min short
'strong_sr_rsi_max_short': 80, # Strong S/R RSI max short
'min_distance_to_resistance': 0.02, # Minimum distance to resistance
'min_distance_to_support': 0.02, # Minimum distance to support
'strong_sr_adx_min': 18, # Minimum ADX for strong S/R
'breakdown_volume_threshold': 1.5,
'breakdown_adx_threshold': 25,
'breakdown_rsi_upper': 60,
'breakdown_rsi_lower': 40,
'breakdown_price_deviation': 1.0,
'breakdown_volume_confirmation': 1.2,
# Add to get_default_params():
'breakdown_volume_multiplier': 1.3,
'breakout_volume_multiplier': 1.3,
'breakdown_rsi_min': 35,
'breakdown_rsi_max': 60,
'breakout_rsi_min': 40,
'breakout_rsi_max': 75,
# Signal Locking Parameters
'signal_lock_enabled': True, # Enable/disable signal locking
'signal_performance_window': 15, # Trades to evaluate over
'signal_lock_duration_hours': 12, # Hours to lock signal
'signal_min_win_rate': 0.35, # Minimum win rate (35%)
'signal_min_avg_profit': -0.015, # Minimum avg profit (-1.5%)
'signal_min_trades_for_eval': 3, # Minimum trades before evaluation
'ema_support_tolerance': 0.008, # 0.8% tolerance for EMA support touch
'ema_resistance_tolerance': 0.008, # 0.8% tolerance for EMA resistance touch
'bb_support_tolerance': 0.01, # 1.0% tolerance for BB support touch
'bb_resistance_tolerance': 0.01, # 1.0% tolerance for BB resistance touch
'kc_support_tolerance': 0.01, # 1.0% tolerance for KC support touch
'kc_resistance_tolerance': 0.01, # 1.0% tolerance for KC resistance touch
}
def create_objective_function(self, pair: str):
"""Create enhanced objective function with exit parameter optimization"""
def objective(trial):
params = {
# === EXISTING ENTRY PARAMETERS ===
'min_divergence_count': trial.suggest_int('min_divergence_count', 1, 3),
'min_signal_strength': trial.suggest_int('min_signal_strength', 1, 5),
'volume_threshold': trial.suggest_float('volume_threshold', 1.0, 1.5),
'adx_threshold': trial.suggest_int('adx_threshold', 15, 30),
'rsi_overbought': trial.suggest_float('rsi_overbought', 65.0, 85.0),
'rsi_oversold': trial.suggest_float('rsi_oversold', 15.0, 35.0),
'max_volatility': trial.suggest_float('max_volatility', 0.015, 0.035),
'min_volatility': trial.suggest_float('min_volatility', 0.003, 0.008),
'volume_factor': trial.suggest_float('volume_factor', 1.2, 2.0),
'atr_multiplier': trial.suggest_float('atr_multiplier', 2.0, 4.0),
# === MISSING INDICATOR PARAMETERS ===
'volume_sma_period': trial.suggest_int('volume_sma_period', 15, 25),
'atr_period': trial.suggest_int('atr_period', 10, 20),
'ema_short_period': trial.suggest_int('ema_short_period', 6, 12),
'ema_long_period': trial.suggest_int('ema_long_period', 18, 25),
'macd_fast': trial.suggest_int('macd_fast', 8, 16),
'macd_slow': trial.suggest_int('macd_slow', 22, 30),
'macd_signal': trial.suggest_int('macd_signal', 6, 12),
'stoch_k': trial.suggest_int('stoch_k', 10, 18),
'stoch_d': trial.suggest_int('stoch_d', 2, 5),
'mom_period': trial.suggest_int('mom_period', 8, 15),
'pivot_window': trial.suggest_int('pivot_window', 8, 15),
'swing_period': trial.suggest_int('swing_period', 40, 60),
'recent_high_low_period': trial.suggest_int('recent_high_low_period', 3, 8),
'rolling_20_period': trial.suggest_int('rolling_20_period', 15, 25),
'rolling_10_period': trial.suggest_int('rolling_10_period', 8, 15),
'chop_period': trial.suggest_int('chop_period', 10, 18),
'natr_period': trial.suggest_int('natr_period', 10, 18),
'cmf_period': trial.suggest_int('cmf_period', 15, 25),
'rsi_period': trial.suggest_int('rsi_period', 10, 20),
'willr_period': trial.suggest_int('willr_period', 10, 20),
'cci_period': trial.suggest_int('cci_period', 15, 25),
'bb_period': trial.suggest_int('bb_period', 15, 25),
'bb_std': trial.suggest_float('bb_std', 1.8, 2.5),
# === EXISTING ENTRY TIMING PARAMETERS ===
'rsi_recovery_periods': trial.suggest_int('rsi_recovery_periods', 2, 5),
'rsi_long_upper_tight': trial.suggest_float('rsi_long_upper_tight', 60.0, 70.0),
'rsi_short_lower_tight': trial.suggest_float('rsi_short_lower_tight', 30.0, 40.0),
'volume_ratio_min': trial.suggest_float('volume_ratio_min', 1.0, 1.4),
'volume_trend_multiplier': trial.suggest_float('volume_trend_multiplier', 1.2, 2.0),
'volume_momentum_multiplier': trial.suggest_float('volume_momentum_multiplier', 1.1, 1.5),
'bb_percent_long_max': trial.suggest_float('bb_percent_long_max', 0.15, 0.35),
'bb_percent_short_min': trial.suggest_float('bb_percent_short_min', 0.65, 0.85),
'price_position_long_max': trial.suggest_float('price_position_long_max', 0.2, 0.4),
'price_position_short_min': trial.suggest_float('price_position_short_min', 0.6, 0.8),
'divergence_freshness_periods': trial.suggest_int('divergence_freshness_periods', 3, 8),
'min_strong_divergence_count': trial.suggest_int('min_strong_divergence_count', 1, 3),
'min_oversold_conditions': trial.suggest_int('min_oversold_conditions', 2, 4),
'min_overbought_conditions': trial.suggest_int('min_overbought_conditions', 2, 4),
'min_reversal_signals': trial.suggest_int('min_reversal_signals', 1, 3),
'min_trend_filters': trial.suggest_int('min_trend_filters', 1, 2),
'chop_threshold': trial.suggest_float('chop_threshold', 55.0, 65.0),
'adx_trending_min': trial.suggest_int('adx_trending_min', 20, 30),
'volume_breakout_multiplier': trial.suggest_float('volume_breakout_multiplier', 2.0, 4.0),
'volume_breakout_rsi_min': trial.suggest_float('volume_breakout_rsi_min', 35.0, 55.0),
'volume_breakout_rsi_max': trial.suggest_float('volume_breakout_rsi_max', 45.0, 65.0),
'bb_oversold_threshold': trial.suggest_float('bb_oversold_threshold', 0.05, 0.25),
'bb_overbought_threshold': trial.suggest_float('bb_overbought_threshold', 0.75, 0.95),
'mean_reversion_rsi_oversold': trial.suggest_float('mean_reversion_rsi_oversold', 20.0, 35.0),
'mean_reversion_rsi_overbought': trial.suggest_float('mean_reversion_rsi_overbought', 65.0, 80.0),
'momentum_ema_periods': trial.suggest_int('momentum_ema_periods', 8, 21),
'momentum_strength_min': trial.suggest_float('momentum_strength_min', 0.015, 0.035),
'momentum_pullback_max': trial.suggest_float('momentum_pullback_max', 0.25, 0.45),
'momentum_pullback_min': trial.suggest_float('momentum_pullback_min', 0.55, 0.75),
# === NEW EXIT PARAMETER OPTIMIZATION ===
# Global Exit Settings
'session_multiplier_overlap': trial.suggest_float('session_multiplier_overlap', 1.1, 1.5),
'session_multiplier_major': trial.suggest_float('session_multiplier_major', 1.0, 1.3),
'volatility_sensitivity': trial.suggest_int('volatility_sensitivity', 25, 60),
'high_signal_multiplier': trial.suggest_float('high_signal_multiplier', 1.0, 1.4),
'low_signal_multiplier': trial.suggest_float('low_signal_multiplier', 0.6, 1.0),
# Mean Reversion (MR1) Exit Optimization
'mr1_max_hold_minutes': trial.suggest_int('mr1_max_hold_minutes', 25, 50),
'mr1_rsi_exit_long': trial.suggest_float('mr1_rsi_exit_long', 62.0, 75.0),
'mr1_rsi_exit_short': trial.suggest_float('mr1_rsi_exit_short', 25.0, 38.0),
'mr1_quick_profit_target': trial.suggest_float('mr1_quick_profit_target', 0.015, 0.035),
'mr1_timeout_min_profit': trial.suggest_float('mr1_timeout_min_profit', 0.005, 0.015),
'mr1_bb_exit_long': trial.suggest_float('mr1_bb_exit_long', 0.6, 0.8),
'mr1_bb_exit_short': trial.suggest_float('mr1_bb_exit_short', 0.2, 0.4),
# Momentum Continuation (MC1) Exit Optimization
'mc1_profit_target': trial.suggest_float('mc1_profit_target', 0.025, 0.055),
'mc1_momentum_break_threshold': trial.suggest_float('mc1_momentum_break_threshold', 0.01, 0.025),
'mc1_max_hold_minutes': trial.suggest_int('mc1_max_hold_minutes', 30, 60),
'mc1_adx_exit_threshold': trial.suggest_int('mc1_adx_exit_threshold', 18, 28),
'mc1_timeout_min_profit': trial.suggest_float('mc1_timeout_min_profit', 0.005, 0.02),
# Volume Breakout (VB1) Exit Optimization
'vb1_volume_fade_threshold': trial.suggest_float('vb1_volume_fade_threshold', 1.0, 1.3),
'vb1_profit_target': trial.suggest_float('vb1_profit_target', 0.02, 0.045),
'vb1_quick_profit': trial.suggest_float('vb1_quick_profit', 0.012, 0.03),
'vb1_min_volatility': trial.suggest_float('vb1_min_volatility', 0.008, 0.018),
# Reversal (RSV1) Exit Optimization
'rsv1_rsi_recovery_threshold': trial.suggest_float('rsv1_rsi_recovery_threshold', 52.0, 65.0),
'rsv1_profit_target': trial.suggest_float('rsv1_profit_target', 0.025, 0.05),
'rsv1_max_hold_minutes': trial.suggest_int('rsv1_max_hold_minutes', 40, 80),
'rsv1_min_timeout_profit': trial.suggest_float('rsv1_min_timeout_profit', 0.005, 0.02),
# Trend Following Exit Optimization
'trend_profit_target': trial.suggest_float('trend_profit_target', 0.03, 0.06),
'trend_ema_break_periods': trial.suggest_int('trend_ema_break_periods', 2, 5),
# Universal Exit Optimization
'medium_term_base_target': trial.suggest_float('medium_term_base_target', 0.035, 0.065),
'reversal_min_profit_long': trial.suggest_float('reversal_min_profit_long', 0.005, 0.015),
'reversal_min_profit_short': trial.suggest_float('reversal_min_profit_short', 0.005, 0.015),
'rsi_exit_overbought': trial.suggest_float('rsi_exit_overbought', 72, 85),
'rsi_exit_oversold': trial.suggest_float('rsi_exit_oversold', 15, 28),
'rsi_extreme_min_profit': trial.suggest_float('rsi_extreme_min_profit', 0.01, 0.025),
'momentum_fade_min_profit': trial.suggest_float('momentum_fade_min_profit', 0.008, 0.02),
'momentum_fade_threshold': trial.suggest_float('momentum_fade_threshold', 0.015, 0.03),
# Extended Duration Management
'extended_2hr_target': trial.suggest_float('extended_2hr_target', 0.015, 0.035),
'extended_3hr_target': trial.suggest_float('extended_3hr_target', 0.008, 0.025),
'extended_5hr_target': trial.suggest_float('extended_5hr_target', 0.005, 0.015),
# Market Protection Optimization
'friday_close_min_profit': trial.suggest_float('friday_close_min_profit', 0.008, 0.02),
'overnight_min_profit': trial.suggest_float('overnight_min_profit', 0.005, 0.015),
'low_liquidity_min_profit': trial.suggest_float('low_liquidity_min_profit', 0.006, 0.018),
# === RSV1 REVERSAL OPTIMIZATION ===
# RSI thresholds
'rsv1_rsi_oversold': trial.suggest_float('rsv1_rsi_oversold', 20.0, 35.0),
'rsv1_rsi_overbought': trial.suggest_float('rsv1_rsi_overbought', 65.0, 80.0),
# Williams %R thresholds
'rsv1_willr_oversold': trial.suggest_float('rsv1_willr_oversold', -90.0, -70.0),
'rsv1_willr_overbought': trial.suggest_float('rsv1_willr_overbought', -30.0, -10.0),
# CCI thresholds
'rsv1_cci_oversold': trial.suggest_float('rsv1_cci_oversold', -120.0, -80.0),
'rsv1_cci_overbought': trial.suggest_float('rsv1_cci_overbought', 80.0, 120.0),
# Bollinger Band position
'rsv1_bb_oversold': trial.suggest_float('rsv1_bb_oversold', 0.05, 0.15),
'rsv1_bb_overbought': trial.suggest_float('rsv1_bb_overbought', 0.85, 0.95),
# Volume and position filters
'rsv1_volume_factor': trial.suggest_float('rsv1_volume_factor', 1.2, 2.0),
'rsv1_price_position_low': trial.suggest_float('rsv1_price_position_low', 0.1, 0.3),
'rsv1_price_position_high': trial.suggest_float('rsv1_price_position_high', 0.7, 0.9),
# Condition count requirements
'rsv1_min_oversold_conditions': trial.suggest_int('rsv1_min_oversold_conditions', 2, 4),
'rsv1_min_overbought_conditions': trial.suggest_int('rsv1_min_overbought_conditions', 2, 4),
'rsv1_min_reversal_signals': trial.suggest_int('rsv1_min_reversal_signals', 1, 3),
'rsv1_min_trend_filters': trial.suggest_int('rsv1_min_trend_filters', 1, 2),
# === E1 PARAMETERS ===
'e1_rsi_overbought_threshold': trial.suggest_float('e1_rsi_overbought_threshold', 70.0, 80.0),
'e1_rsi_overbought_buffer': trial.suggest_int('e1_rsi_overbought_buffer', 3, 8),
'e1_rsi_oversold_min': trial.suggest_float('e1_rsi_oversold_min', 20.0, 30.0),
'e1_rsi_oversold_threshold': trial.suggest_float('e1_rsi_oversold_threshold', 20.0, 30.0),
'e1_rsi_oversold_buffer': trial.suggest_int('e1_rsi_oversold_buffer', 3, 8),
'e1_rsi_overbought_max': trial.suggest_float('e1_rsi_overbought_max', 70.0, 80.0),
'volatility_min_multiplier': trial.suggest_float('volatility_min_multiplier', 0.3, 1.0),
'volatility_max_multiplier': trial.suggest_float('volatility_max_multiplier', 1.5, 3.0),
# === TREND PARAMETERS ===
'trend_volume_window': trial.suggest_int('trend_volume_window', 15, 30),
'trend_volume_multiplier': trial.suggest_float('trend_volume_multiplier', 1.2, 2.0),
'trend_rsi_min': trial.suggest_float('trend_rsi_min', 35.0, 45.0),
'trend_rsi_max': trial.suggest_float('trend_rsi_max', 55.0, 65.0),
'trend_close_lookback': trial.suggest_int('trend_close_lookback', 1, 4),
'trend_atr_window': trial.suggest_int('trend_atr_window', 15, 25),
'trend_atr_multiplier': trial.suggest_float('trend_atr_multiplier', 0.8, 1.3),
'trend_adx_min': trial.suggest_int('trend_adx_min', 20, 30),
# === MOMENTUM PARAMETERS ===
'momentum_rsi_lookback': trial.suggest_int('momentum_rsi_lookback', 2, 5),
'momentum_rsi_pivot': trial.suggest_float('momentum_rsi_pivot', 45.0, 55.0),
'momentum_rsi_trigger': trial.suggest_float('momentum_rsi_trigger', 50.0, 60.0),
'momentum_rsi_trigger_short': trial.suggest_float('momentum_rsi_trigger_short', 40.0, 50.0),
'momentum_high_lookback': trial.suggest_int('momentum_high_lookback', 1, 4),
'momentum_low_lookback': trial.suggest_int('momentum_low_lookback', 1, 4),
'momentum_volume_window': trial.suggest_int('momentum_volume_window', 8, 15),
'momentum_volume_multiplier': trial.suggest_float('momentum_volume_multiplier', 1.0, 1.5),
'momentum_kc_upper_factor': trial.suggest_float('momentum_kc_upper_factor', 0.99, 1.0),
'momentum_kc_lower_factor': trial.suggest_float('momentum_kc_lower_factor', 1.0, 1.01),
'momentum_adx_min': trial.suggest_int('momentum_adx_min', 20, 30),
# === SECONDARY PARAMETERS ===
'secondary_close_lookback': trial.suggest_int('secondary_close_lookback', 1, 4),
'secondary_rsi_min': trial.suggest_float('secondary_rsi_min', 30.0, 40.0),
'secondary_rsi_max': trial.suggest_float('secondary_rsi_max', 65.0, 75.0),
'secondary_rsi_min_short': trial.suggest_float('secondary_rsi_min_short', 25.0, 35.0),
'secondary_rsi_max_short': trial.suggest_float('secondary_rsi_max_short', 75.0, 85.0),
# === TERTIARY PARAMETERS ===
'tertiary_rsi_min': trial.suggest_float('tertiary_rsi_min', 35.0, 45.0),
'tertiary_rsi_max': trial.suggest_float('tertiary_rsi_max', 60.0, 70.0),
'tertiary_rsi_min_short': trial.suggest_float('tertiary_rsi_min_short', 30.0, 40.0),
'tertiary_rsi_max_short': trial.suggest_float('tertiary_rsi_max_short', 70.0, 80.0),
'tertiary_volume_window': trial.suggest_int('tertiary_volume_window', 8, 15),
'tertiary_volume_multiplier': trial.suggest_float('tertiary_volume_multiplier', 0.8, 1.3),
# === QUATERNARY PARAMETERS ===
'quaternary_rsi_min': trial.suggest_float('quaternary_rsi_min', 40.0, 50.0),
'quaternary_rsi_max': trial.suggest_float('quaternary_rsi_max', 50.0, 60.0),
'quaternary_volume_window': trial.suggest_int('quaternary_volume_window', 3, 8),
'quaternary_volume_multiplier': trial.suggest_float('quaternary_volume_multiplier', 1.0, 1.5),
# === FIFTH PARAMETERS ===
'fifth_rsi_min': trial.suggest_float('fifth_rsi_min', 30.0, 40.0),
'fifth_rsi_max': trial.suggest_float('fifth_rsi_max', 65.0, 75.0),
'fifth_rsi_min_short': trial.suggest_float('fifth_rsi_min_short', 25.0, 35.0),
'fifth_rsi_max_short': trial.suggest_float('fifth_rsi_max_short', 60.0, 70.0),
# === SIXTH PARAMETERS ===
'sixth_min_div_count': trial.suggest_int('sixth_min_div_count', 1, 3),
'sixth_rsi_min': trial.suggest_float('sixth_rsi_min', 25.0, 35.0),
'sixth_rsi_max': trial.suggest_float('sixth_rsi_max', 70.0, 80.0),
'sixth_rsi_min_short': trial.suggest_float('sixth_rsi_min_short', 20.0, 30.0),
'sixth_rsi_max_short': trial.suggest_float('sixth_rsi_max_short', 65.0, 75.0),
# === SEVENTH PARAMETERS ===
'seventh_volume_window': trial.suggest_int('seventh_volume_window', 15, 25),
'seventh_volume_multiplier': trial.suggest_float('seventh_volume_multiplier', 1.0, 1.5),
'seventh_rsi_min': trial.suggest_float('seventh_rsi_min', 25.0, 35.0),
'seventh_rsi_max': trial.suggest_float('seventh_rsi_max', 65.0, 75.0),
'seventh_rsi_min_short': trial.suggest_float('seventh_rsi_min_short', 25.0, 35.0),
'seventh_rsi_max_short': trial.suggest_float('seventh_rsi_max_short', 65.0, 75.0),
'seventh_close_lookback': trial.suggest_int('seventh_close_lookback', 1, 4),
'seventh_atr_window': trial.suggest_int('seventh_atr_window', 15, 25),
'seventh_atr_multiplier': trial.suggest_float('seventh_atr_multiplier', 0.3, 0.8),
'seventh_adx_min': trial.suggest_int('seventh_adx_min', 10, 20),
# === DIVERGENCE PARAMETERS ===
'div_rsi_min': trial.suggest_float('div_rsi_min', 20.0, 30.0),
'div_rsi_max': trial.suggest_float('div_rsi_max', 75.0, 85.0),
'div_rsi_min_short': trial.suggest_float('div_rsi_min_short', 15.0, 25.0),
'div_rsi_max_short': trial.suggest_float('div_rsi_max_short', 70.0, 80.0),
# === SUPPORT/RESISTANCE PARAMETERS ===
'support_tolerance': trial.suggest_float('support_tolerance', 0.003, 0.008),
'support_bounce_min': trial.suggest_float('support_bounce_min', 0.001, 0.004),
'resistance_tolerance': trial.suggest_float('resistance_tolerance', 0.003, 0.008),
'resistance_bounce_min': trial.suggest_float('resistance_bounce_min', 0.001, 0.004),
'sr_volume_multiplier': trial.suggest_float('sr_volume_multiplier', 1.1, 1.8),
'sr_rsi_min_long': trial.suggest_float('sr_rsi_min_long', 20.0, 35.0),
'sr_rsi_max_long': trial.suggest_float('sr_rsi_max_long', 60.0, 75.0),
'sr_rsi_min_short': trial.suggest_float('sr_rsi_min_short', 25.0, 40.0),
'sr_rsi_max_short': trial.suggest_float('sr_rsi_max_short', 65.0, 80.0),
'strong_sr_volume_multiplier': trial.suggest_float('strong_sr_volume_multiplier', 1.5, 2.5),
# === BREAKDOWN/BREAKOUT PARAMETERS ===
'breakdown_volume_threshold': trial.suggest_float('breakdown_volume_threshold', 1.1, 3.0),
'breakdown_adx_threshold': trial.suggest_int('breakdown_adx_threshold', 15, 45),
'breakdown_rsi_upper': trial.suggest_int('breakdown_rsi_upper', 55, 70),
'breakdown_rsi_lower': trial.suggest_int('breakdown_rsi_lower', 30, 45),
'breakdown_price_deviation': trial.suggest_float('breakdown_price_deviation', 0.5, 3.0),
'breakdown_volume_confirmation': trial.suggest_float('breakdown_volume_confirmation', 1.0, 2.5),
'breakdown_volume_multiplier': trial.suggest_float('breakdown_volume_multiplier', 1.1, 2.0),
'breakout_volume_multiplier': trial.suggest_float('breakout_volume_multiplier', 1.1, 2.0),
'breakdown_rsi_min': trial.suggest_float('breakdown_rsi_min', 30.0, 45.0),
'breakdown_rsi_max': trial.suggest_float('breakdown_rsi_max', 55.0, 70.0),
'breakout_rsi_min': trial.suggest_float('breakout_rsi_min', 35.0, 50.0),
'breakout_rsi_max': trial.suggest_float('breakout_rsi_max', 65.0, 80.0),
# === ADDITIONAL TECHNICAL PARAMETERS ===
'willr_oversold': trial.suggest_float('willr_oversold', -90, -70),
'willr_overbought': trial.suggest_float('willr_overbought', -30, -10),
'cci_oversold': trial.suggest_float('cci_oversold', -120, -80),
'cci_overbought': trial.suggest_float('cci_overbought', 80, 120),
'ema_support_tolerance': trial.suggest_float('ema_support_tolerance', 0.005, 0.015),
'ema_resistance_tolerance': trial.suggest_float('ema_resistance_tolerance', 0.005, 0.015),
'bb_support_tolerance': trial.suggest_float('bb_support_tolerance', 0.008, 0.020),
'bb_resistance_tolerance': trial.suggest_float('bb_resistance_tolerance', 0.008, 0.020),
'kc_support_tolerance': trial.suggest_float('kc_support_tolerance', 0.008, 0.020),
'kc_resistance_tolerance': trial.suggest_float('kc_resistance_tolerance', 0.008, 0.020),
}
# Return performance for this coin
recent_trades = self.get_recent_trade_performance(pair)
if len(recent_trades) >= 1: # Need at least 3 trades
return sum(recent_trades) / len(recent_trades)
else:
return 0.0 # No optimization until we have trade data
return objective
def get_recent_trade_performance(self, pair: str) -> List[float]:
"""Get recent trade performance for optimization"""
try:
from freqtrade.persistence import Trade
trades = Trade.get_trades_proxy(pair=pair)
if not trades:
return []
# Get last 5 trades for this pair
recent_trades = trades[-5:] if len(trades) >= 5 else trades
# Calculate profit ratios
performance = []
for trade in recent_trades:
if trade.close_date: # Only closed trades
# FIX: Use the close_rate for calc_profit_ratio
performance.append(trade.calc_profit_ratio(trade.close_rate))
logger.debug(f"📊 [OPTUNA] Found {len(performance)} completed trades for {pair}")
return performance
except Exception as e:
logger.error(f"❌ [OPTUNA] Failed to get trade performance for {pair}: {e}")
return []
def daily_optimization_check(self):
"""Enhanced optimization check with smarter scheduling"""
try:
if not self.optuna_manager:
return
current_time = time.time()
all_pairs = list(self.coin_params.keys())
if not all_pairs:
logger.info("No pairs available for optimization yet")
return
optimized_count = 0
for pair in all_pairs:
try:
if self.should_retrain_pair(pair, current_time):
logger.info(f"🔄 Optimizing {pair}")
self.optuna_manager.optimize_coin(pair, self.create_objective_function(pair), n_trials=15)
# Track last optimization time
setattr(self, f'last_optimization_{pair.replace("/", "_").replace(":", "_")}', current_time)
optimized_count += 1
except Exception as e:
logger.error(f"❌ Optimization failed for {pair}: {e}")
logger.info(f"✅ Optimization check completed: {optimized_count}/{len(all_pairs)} pairs optimized")
except Exception as e:
logger.error(f"❌ Daily optimization check failed: {e}")
def should_retrain_pair(self, pair: str, current_time: float) -> bool:
"""Simplified retraining logic"""
last_opt_time = getattr(self, f'last_optimization_{pair.replace("/", "_").replace(":", "_")}', 0)
trades_since_last = self.get_trades_since_optimization(pair, last_opt_time)
# Simplified: Just check if enough time passed OR enough trades
time_threshold = 21600 # 6 hours (reduced from complex scheduling)
trade_threshold = 2 # Only 2 trades needed (reduced from 5-10)
return (current_time - last_opt_time > time_threshold) or (trades_since_last >= trade_threshold)
def get_trades_since_optimization(self, pair: str, last_opt_time: float) -> int:
"""Count trades since last optimization"""
try:
from freqtrade.persistence import Trade
from datetime import datetime
last_opt_datetime = datetime.fromtimestamp(last_opt_time) if last_opt_time > 0 else datetime.min
trades = Trade.get_trades_proxy(pair=pair)
if not trades:
return 0
recent_trades = [t for t in trades if t.open_date_utc > last_opt_datetime]
return len(recent_trades)
except Exception as e:
logger.debug(f"Failed to count recent trades for {pair}: {e}")
return 0
def load_signal_performance(self):
"""Load signal performance history from file"""
if os.path.exists(self.signal_performance_file):
try:
with open(self.signal_performance_file, 'r') as f:
return json.load(f)
except:
return {}
return {}
def save_signal_performance(self):
"""Save signal performance to file - both formats"""
try:
os.makedirs(os.path.dirname(self.signal_performance_file), exist_ok=True)
# Save both formats in one file
data_to_save = {
'trades': getattr(self, 'all_signal_trades', []), # Excel-friendly flat format
'signal_locks': self.signal_performance # Nested format for locking
}
with open(self.signal_performance_file, 'w') as f:
json.dump(data_to_save, f, indent=2)
except Exception as e:
print(f"Failed to save signal performance: {e}")
def is_signal_locked(self, entry_tag, pair):
"""Check if a specific signal is locked for this pair"""
lock_key = f"{pair}_{entry_tag}"
if lock_key in self.signal_locks:
if self.signal_locks[lock_key] > datetime.now():
return True
else:
# Remove expired lock
del self.signal_locks[lock_key]
print(f"Signal lock expired for {entry_tag} on {pair}")
return False
def update_signal_performance(self, entry_tag, pair, profit_ratio):
"""Update performance tracking for a signal - Excel-friendly format + locking support"""
# Create trade record with all info in one line (Excel-friendly)
trade_record = {
'pair': pair,
'entry_tag': entry_tag,
'profit': profit_ratio,
'timestamp': datetime.now().isoformat(),
'win': profit_ratio > 0
}
# Store in flat list for Excel analysis
if not hasattr(self, 'all_signal_trades'):
self.all_signal_trades = []
self.all_signal_trades.append(trade_record)
# Keep only recent window
if len(self.all_signal_trades) > 500:
self.all_signal_trades = self.all_signal_trades[-500:]
# Keep the nested format for the locking logic to work
perf_key = f"{pair}_{entry_tag}"
if perf_key not in self.signal_performance:
self.signal_performance[perf_key] = []
self.signal_performance[perf_key].append({
'profit': profit_ratio,
'timestamp': datetime.now().isoformat(),
'win': profit_ratio > 0
})
if len(self.signal_performance[perf_key]) > self.performance_window:
self.signal_performance[perf_key] = self.signal_performance[perf_key][-self.performance_window:]
# Evaluate if signal should be locked
self.evaluate_signal_lock(entry_tag, pair)
# Save to file
self.save_signal_performance()
def evaluate_signal_lock(self, entry_tag, pair):
"""Evaluate if signal should be locked based on recent performance"""
perf_key = f"{pair}_{entry_tag}"
recent_trades = self.signal_performance.get(perf_key, [])
# Need minimum trades for evaluation
if len(recent_trades) < self.min_trades_for_eval:
return
# Calculate performance metrics
profits = [trade['profit'] for trade in recent_trades]
wins = sum(1 for profit in profits if profit > 0)
win_rate = wins / len(profits)
avg_profit = sum(profits) / len(profits)
# Check if signal should be locked
should_lock = (
win_rate < self.min_win_rate or
avg_profit < self.min_avg_profit
)
if should_lock:
lock_until = datetime.now() + timedelta(hours=self.lock_duration_hours)
lock_key = f"{pair}_{entry_tag}"
self.signal_locks[lock_key] = lock_until
print(f"LOCKED SIGNAL: {entry_tag} for {pair}")
print(f" Win Rate: {win_rate:.1%} (min: {self.min_win_rate:.1%})")
print(f" Avg Profit: {avg_profit:.2%} (min: {self.min_avg_profit:.2%})")
print(f" Locked until: {lock_until.strftime('%Y-%m-%d %H:%M')}")
def get_signal_status(self, pair):
"""Get status of all signals for a pair - for debugging"""
status = {}
# Complete list of entry tags from your trading script
entry_tags = [
# Primary conditions
'Bull_E1', 'Bear_E1',
# Support/Resistance bounce
'Bull_SR_Bounce', 'Bear_SR_Bounce',
# Strong Support/Resistance
'Bull_Strong_Support', 'Bear_Strong_Resistance',
# Trend breakout
'Bull_Trend', 'Bear_Trend',
# Momentum
'Bull_Momentum', 'Bear_Momentum',
# Momentum breakout
'Bull_Momentum_Breakout', 'Bear_Momentum_Breakout',
# Pullback momentum
'Bull_Pullback_Momentum', 'Bear_Pullback_Momentum',
# Secondary conditions
'Bull_E2', 'Bear_E2',
# Breakdown conditions
'Bull_Breakdown', 'Bear_Breakdown',
# Pure divergence
'Bull_Div', 'Bear_Div',
# High quality divergence
'Bull_Div_HQ', 'Bear_Div_HQ',
# Reversal (RSV1)
'Bull_RSV1', 'Bear_RSV1',
# Mean reversion (MR1)
'Bull_MR1', 'Bear_MR1',
# Momentum continuation (MC1)
'Bull_MC1', 'Bear_MC1',
# Seventh condition
'Bull_E7', 'Bear_E7',
# Simple breakout/breakdown (NEW)
'Bull_Simple_Breakout', 'Bear_Simple_Breakdown'
]
for entry_tag in entry_tags:
perf_key = f"{pair}_{entry_tag}"
recent_trades = self.signal_performance.get(perf_key, [])
is_locked = self.is_signal_locked(entry_tag, pair)
if recent_trades:
profits = [trade['profit'] for trade in recent_trades]
wins = sum(1 for profit in profits if profit > 0)
win_rate = wins / len(profits) if profits else 0
avg_profit = sum(profits) / len(profits) if profits else 0
status[entry_tag] = {
'locked': is_locked,
'trades_count': len(recent_trades),
'win_rate': win_rate,
'avg_profit': avg_profit,
'last_profit': profits[-1] if profits else None
}
else:
status[entry_tag] = {
'locked': is_locked,
'trades_count': 0,
'win_rate': 0,
'avg_profit': 0,
'last_profit': None
}
return status
def informative_pairs(self):
"""Define additional timeframes to download"""
pairs = self.dp.current_whitelist()
return [(pair, '1h') for pair in pairs]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Enhanced indicator population with multi-timeframe analysis and Optuna optimization
ENHANCED: Now includes proactive Optuna optimization with parameterized indicators
"""
pair = metadata['pair']
logger.debug(f"📊 [INDICATORS] Processing {pair} with {len(dataframe)} candles")
# ===== PROACTIVE OPTUNA OPTIMIZATION FIRST =====
# ENHANCED: Get optimized parameters EARLY (triggers proactive optimization)
try:
coin_params = self.get_coin_params(pair) # Get coin-specific parameters
logger.debug(f"📈 [OPTUNA] Applied parameters for {pair}: min_div={coin_params.get('min_divergence_count')}, min_signal={coin_params.get('min_signal_strength')}")
except Exception as e:
logger.error(f"❌ [OPTUNA] Failed to get coin parameters for {pair}: {e}")
coin_params = self.get_default_params()
# ===== ENHANCED PARAMETER EXTRACTION =====
# Extract all coin-specific parameters at once for better performance
# Volume analysis parameters
volume_sma_period = coin_params.get('volume_sma_period', 20)
volume_threshold = coin_params.get('volume_threshold', 1.0)
# ATR and volatility parameters
atr_period = coin_params.get('atr_period', 14)
atr_multiplier = coin_params.get('atr_multiplier', 3.0)
# Bollinger Bands parameters
bb_period = coin_params.get('bb_period', getattr(self, 'bb_period', type('obj', (), {'value': 20})).value)
bb_std = coin_params.get('bb_std', getattr(self, 'bb_std', type('obj', (), {'value': 2.0})).value)
# EMA parameters
ema_short_period = coin_params.get('ema_short_period', 8)
ema_long_period = coin_params.get('ema_long_period', 21)
# MACD parameters
macd_fast = coin_params.get('macd_fast', 12)
macd_slow = coin_params.get('macd_slow', 26)
macd_signal = coin_params.get('macd_signal', 9)
# RSI and momentum parameters
rsi_period = coin_params.get('rsi_period', 14)
willr_period = coin_params.get('willr_period', 14)
cci_period = coin_params.get('cci_period', 20)
mom_period = coin_params.get('mom_period', 10)
stoch_k = coin_params.get('stoch_k', 14)
stoch_d = coin_params.get('stoch_d', 3)
# Pivot points window
pivot_window = coin_params.get('pivot_window', getattr(self, 'window', type('obj', (), {'value': 10})).value)
# Support/Resistance parameters
swing_period = coin_params.get('swing_period', 50)
recent_high_low_period = coin_params.get('recent_high_low_period', 5)
# Rolling window parameters
rolling_20_period = coin_params.get('rolling_20_period', 20)
rolling_10_period = coin_params.get('rolling_10_period', 10)
# Choppiness and other analysis parameters
chop_period = coin_params.get('chop_period', 14)
natr_period = coin_params.get('natr_period', 14)
cmf_period = coin_params.get('cmf_period', 20)
# Validate parameters to ensure they're within reasonable ranges
rsi_period = max(2, min(50, rsi_period))
willr_period = max(2, min(50, willr_period))
cci_period = max(2, min(50, cci_period))
# === MULTI-TIMEFRAME ANALYSIS ===
# Get 1h timeframe for trend confirmation with improved error handling
try:
# Check if we're in backtesting mode or if pair supports 1h data
if hasattr(self.dp, 'runmode') and self.dp.runmode.value in ['backtest', 'hyperopt']:
# In backtesting, try to get 1h data but don't fail if unavailable
informative_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1h')
else:
# In live/dry run, be more cautious about data availability
try:
informative_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1h')
except Exception:
informative_1h = None
# Enhanced data validation
if (informative_1h is not None and
len(informative_1h) > 50 and # Reduced minimum requirement
not informative_1h.empty and
'close' in informative_1h.columns):
try:
# 1h Trend indicators with additional error checking
informative_1h['ema50_1h'] = ta.EMA(informative_1h, timeperiod=50)
informative_1h['ema200_1h'] = ta.EMA(informative_1h, timeperiod=200)
informative_1h['trend_1h'] = ta.EMA(informative_1h, timeperiod=21)
informative_1h['trend_strength_1h'] = ta.ADX(informative_1h)
informative_1h['rsi_1h'] = ta.RSI(informative_1h)
# Fill NaN values before merging
informative_1h = informative_1h.bfill().ffill()
# Safe merge with additional error handling
dataframe = merge_informative_pair(dataframe, informative_1h, self.timeframe, '1h', ffill=True)
except Exception as merge_error:
logger.warning(f"Failed to merge 1h data for {metadata['pair']}: {merge_error}")
self._add_dummy_1h_columns(dataframe)
else:
logger.info(f"Using fallback 1h indicators for {metadata['pair']} (insufficient data)")
self._add_dummy_1h_columns(dataframe)
except Exception as e:
logger.warning(f"Error accessing 1h data for {metadata['pair']}: {e}")
self._add_dummy_1h_columns(dataframe)
# === 15M TIMEFRAME INDICATORS ===
informative = dataframe.copy()
# === VOLUME ANALYSIS WITH PARAMETERIZED VALUES ===
try:
informative['volume_sma'] = ta.SMA(informative['volume'], timeperiod=volume_sma_period)
informative['volume_ratio'] = informative['volume'] / informative['volume_sma']
informative['volume_ratio'] = informative['volume_ratio'].fillna(1.0)
except:
informative['volume_sma'] = informative['volume']
informative['volume_ratio'] = 1.0
# === VOLATILITY ANALYSIS WITH PARAMETERIZED VALUES ===
try:
informative['atr'] = ta.ATR(informative, timeperiod=atr_period)
informative['volatility'] = informative['atr'] / informative['close']
informative['volatility'] = informative['volatility'].fillna(0.01)
except:
informative['atr'] = informative['close'] * 0.02
informative['volatility'] = 0.01
# === MOMENTUM INDICATORS WITH PARAMETERIZED VALUES ===
try:
# Parameterized momentum indicators
informative['rsi'] = ta.RSI(informative, timeperiod=rsi_period)
informative['willr'] = ta.WILLR(informative, timeperiod=willr_period)
informative['cci'] = ta.CCI(informative, timeperiod=cci_period)
informative['mom'] = ta.MOM(informative, timeperiod=mom_period)
# MACD with parameterized values
macd = ta.MACD(informative, fastperiod=macd_fast, slowperiod=macd_slow, signalperiod=macd_signal)
informative['macd'] = macd['macd']
informative['macdsignal'] = macd['macdsignal']
informative['macdhist'] = macd['macdhist']
# Additional momentum indicators with parameters
stoch = ta.STOCH(informative, fastk_period=stoch_k, slowk_period=stoch_d, slowd_period=stoch_d)
informative['stoch'] = stoch['slowk']
informative['roc'] = ta.ROC(informative)
informative['uo'] = ta.ULTOSC(informative)
informative['ao'] = qtpylib.awesome_oscillator(informative)
informative['cmf'] = chaikin_money_flow(informative, cmf_period)
informative['obv'] = ta.OBV(informative)
informative['mfi'] = ta.MFI(informative)
informative['adx'] = ta.ADX(informative)
# Fill NaN values for all indicators
indicator_columns = ['rsi', 'stoch', 'roc', 'uo', 'ao', 'macd', 'macdsignal', 'macdhist', 'cci', 'cmf', 'obv', 'mfi', 'adx', 'willr', 'mom']
for col in indicator_columns:
if col in informative.columns:
informative[col] = informative[col].bfill().fillna(50 if col in ['rsi', 'mfi'] else 0)
except Exception as e:
logger.warning(f"Error calculating momentum indicators: {e}")
# Provide fallback values
informative['rsi'] = 50
informative['stoch'] = 50
informative['roc'] = 0
informative['uo'] = 50
informative['ao'] = 0
informative['macd'] = 0
informative['macdsignal'] = 0
informative['macdhist'] = 0
informative['cci'] = 0
informative['cmf'] = 0
informative['obv'] = informative['volume'].cumsum()
informative['mfi'] = 50
informative['adx'] = 25
informative['willr'] = -50
informative['mom'] = 0
# === KELTNER CHANNEL ===
try:
keltner = emaKeltner(informative)
informative["kc_upperband"] = keltner["upper"]
informative["kc_middleband"] = keltner["mid"]
informative["kc_lowerband"] = keltner["lower"]
except:
informative["kc_upperband"] = informative['close'] * 1.02
informative["kc_middleband"] = informative['close']
informative["kc_lowerband"] = informative['close'] * 0.98
# === BOLLINGER BANDS WITH PARAMETERIZED VALUES ===
try:
# Parameterized Bollinger Bands
bollinger = qtpylib.bollinger_bands(informative['close'], window=bb_period, stds=bb_std)
informative['bb_lower'] = bollinger['lower']
informative['bb_middle'] = bollinger['mid']
informative['bb_upper'] = bollinger['upper']
informative['bb_percent'] = (informative['close'] - informative['bb_lower']) / (informative['bb_upper'] - informative['bb_lower'])
# Keep the original naming for compatibility
informative['bollinger_upperband'] = bollinger['upper']
informative['bollinger_lowerband'] = bollinger['lower']
except:
informative['bb_lower'] = informative['close'] * 0.98
informative['bb_middle'] = informative['close']
informative['bb_upper'] = informative['close'] * 1.02
informative['bb_percent'] = 0.5
informative['bollinger_upperband'] = informative['close'] * 1.02
informative['bollinger_lowerband'] = informative['close'] * 0.98
# === EMA WITH PARAMETERIZED VALUES ===
try:
# Parameterized EMAs
informative['ema_short'] = ta.EMA(informative, timeperiod=ema_short_period)
informative['ema_long'] = ta.EMA(informative, timeperiod=ema_long_period)
# Standard EMAs
informative['ema9'] = ta.EMA(informative, timeperiod=9)
informative['ema20'] = ta.EMA(informative, timeperiod=20)
informative['ema50'] = ta.EMA(informative, timeperiod=50)
informative['ema200'] = ta.EMA(informative, timeperiod=200)
informative['ema21'] = ta.EMA(informative, timeperiod=21) # If not already exists
informative['price_vs_ema21'] = (informative['close'] - informative['ema21']) / informative['ema21'] * 100
informative['price_vs_ema50'] = (informative['close'] - informative['ema50']) / informative['ema50'] * 100
informative['price_vs_ema200'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
# Fill NaN values for EMAs
ema_columns = ['ema_short', 'ema_long', 'ema9', 'ema20', 'ema21', 'ema50', 'ema200'] # Added ema21
for col in ema_columns:
if col in informative.columns:
informative[col] = informative[col].bfill().fillna(informative['close'])
# ADD: Fill NaN values for price position indicators
price_pos_columns = ['price_vs_ema21', 'price_vs_ema50', 'price_vs_ema200']
for col in price_pos_columns:
if col in informative.columns:
informative[col] = informative[col].bfill().fillna(0)
except:
informative['ema_short'] = informative['close']
informative['ema_long'] = informative['close']
informative['ema9'] = informative['close']
informative['ema20'] = informative['close']
informative['ema21'] = informative['close'] # ADD THIS
informative['ema50'] = informative['close']
informative['ema200'] = informative['close']
# ADD THESE FALLBACK VALUES:
informative['price_vs_ema21'] = 0
informative['price_vs_ema50'] = 0
informative['price_vs_ema200'] = 0
# === PRICE POSITION ANALYSIS WITH PARAMETERIZED VALUES ===
try:
informative['high_20'] = informative['high'].rolling(window=rolling_20_period).max()
informative['low_20'] = informative['low'].rolling(window=rolling_20_period).min()
informative['price_position'] = (informative['close'] - informative['low_20']) / (informative['high_20'] - informative['low_20'])
except:
informative['high_20'] = informative['high']
informative['low_20'] = informative['low']
informative['price_position'] = 0.5
# === PIVOT POINTS WITH PARAMETERIZED VALUES ===
try:
pivots = pivot_points(informative, pivot_window)
informative['pivot_lows'] = pivots['pivot_lows']
informative['pivot_highs'] = pivots['pivot_highs']
except Exception as e:
logger.warning(f"Error calculating pivot points: {e}")
informative['pivot_lows'] = np.nan
informative['pivot_highs'] = np.nan
# === DIVERGENCE ANALYSIS ===
try:
self.initialize_divergences_lists(informative)
(high_iterator, low_iterator) = self.get_iterators(informative)
# Add divergences for multiple indicators (expanded list from first file)
indicators = ['rsi', 'stoch', 'roc', 'uo', 'ao', 'macd', 'cci', 'cmf', 'obv', 'mfi']
for indicator in indicators:
try:
if indicator in informative.columns:
self.add_divergences(informative, indicator, high_iterator, low_iterator)
except Exception as e:
logger.warning(f"Error adding divergences for {indicator}: {e}")
continue
except Exception as e:
logger.warning(f"Error in divergence analysis: {e}")
# Initialize with empty divergence data
informative["total_bullish_divergences"] = 0
informative["total_bullish_divergences_count"] = 0
informative["total_bullish_divergences_names"] = ''
informative["total_bearish_divergences"] = 0
informative["total_bearish_divergences_count"] = 0
informative["total_bearish_divergences_names"] = ''
# === SIGNAL STRENGTH CALCULATION ===
try:
informative['signal_strength'] = self.calculate_signal_strength(informative)
except:
informative['signal_strength'] = 0
# === MERGE BACK TO DATAFRAME ===
for col in informative.columns:
if col not in dataframe.columns:
dataframe[col] = informative[col]
else:
dataframe[col] = informative[col]
# ===== APPLY COIN-SPECIFIC OPTUNA ADJUSTMENTS =====
# Apply coin-specific adjustments AFTER all indicators are calculated
dataframe['coin_min_divergence'] = coin_params.get('min_divergence_count', self.min_divergence_count.value)
dataframe['coin_min_signal'] = coin_params.get('min_signal_strength', self.min_signal_strength.value)
dataframe['coin_volume_threshold'] = coin_params.get('volume_threshold', self.volume_threshold.value)
dataframe['coin_adx_threshold'] = coin_params.get('adx_threshold', self.adx_threshold.value)
# === ADDITIONAL MARKET STRUCTURE ANALYSIS WITH PARAMETERIZED VALUES ===
try:
dataframe['chop'] = choppiness_index(dataframe['high'], dataframe['low'], dataframe['close'], window=chop_period)
dataframe['natr'] = ta.NATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=natr_period)
dataframe['natr_diff'] = dataframe['natr'] - dataframe['natr'].shift(1)
dataframe['natr_direction_change'] = (dataframe['natr_diff'] * dataframe['natr_diff'].shift(1) < 0)
except:
dataframe['chop'] = 50
dataframe['natr'] = 0.02
dataframe['natr_diff'] = 0
dataframe['natr_direction_change'] = False
# === SUPPORT/RESISTANCE LEVELS WITH PARAMETERIZED VALUES ===
try:
dataframe['swing_high'] = dataframe['high'].rolling(window=swing_period, min_periods=1).max()
dataframe['swing_low'] = dataframe['low'].rolling(window=swing_period, min_periods=1).min()
dataframe['distance_to_resistance'] = (dataframe['swing_high'] - dataframe['close']) / dataframe['close']
dataframe['distance_to_support'] = (dataframe['close'] - dataframe['swing_low']) / dataframe['close']
except:
dataframe['swing_high'] = dataframe['high']
dataframe['swing_low'] = dataframe['low']
dataframe['distance_to_resistance'] = 0.02
dataframe['distance_to_support'] = 0.02
try:
# Calculate recent highs/lows for pattern recognition WITH PARAMETERIZED VALUES
dataframe['recent_high_5'] = dataframe['high'].rolling(window=recent_high_low_period).max()
dataframe['recent_low_5'] = dataframe['low'].rolling(window=recent_high_low_period).min()
dataframe['recent_high_10'] = dataframe['high'].rolling(window=rolling_10_period).max()
dataframe['recent_low_10'] = dataframe['low'].rolling(window=rolling_10_period).min()
# MA alignment indicators for trend confirmation
dataframe['ma_alignment_bull'] = (
(dataframe['ema21'] > dataframe['ema50']) &
(dataframe['ema50'] > dataframe['ema200'])
)
dataframe['ma_alignment_bear'] = (
(dataframe['ema21'] < dataframe['ema50']) &
(dataframe['ema50'] < dataframe['ema200'])
)
# Rejection pattern detection
dataframe['rejected_at_ema21'] = (
(dataframe['high'].shift(1) > dataframe['ema21'].shift(1)) &
(dataframe['close'].shift(1) < dataframe['ema21'].shift(1))
)
dataframe['rejected_at_ema50'] = (
(dataframe['high'].shift(1) > dataframe['ema50'].shift(1)) &
(dataframe['close'].shift(1) < dataframe['ema50'].shift(1))
)
dataframe['bounced_from_ema21'] = (
(dataframe['low'].shift(1) < dataframe['ema21'].shift(1)) &
(dataframe['close'].shift(1) > dataframe['ema21'].shift(1))
)
dataframe['bounced_from_ema50'] = (
(dataframe['low'].shift(1) < dataframe['ema50'].shift(1)) &
(dataframe['close'].shift(1) > dataframe['ema50'].shift(1))
)
# Candle pattern indicators
dataframe['red_candle'] = dataframe['close'] < dataframe['close'].shift(1)
dataframe['green_candle'] = dataframe['close'] > dataframe['close'].shift(1)
dataframe['strong_red'] = (dataframe['close'] < dataframe['close'].shift(1)) & (dataframe['volume_ratio'] > 1.2)
dataframe['strong_green'] = (dataframe['close'] > dataframe['close'].shift(1)) & (dataframe['volume_ratio'] > 1.2)
# Fill NaN values
pattern_columns = [
'recent_high_5', 'recent_low_5', 'recent_high_10', 'recent_low_10',
'ma_alignment_bull', 'ma_alignment_bear', 'rejected_at_ema21', 'rejected_at_ema50',
'bounced_from_ema21', 'bounced_from_ema50', 'red_candle', 'green_candle',
'strong_red', 'strong_green'
]
for col in pattern_columns:
if col in dataframe.columns:
if col.startswith('recent_'):
dataframe[col] = dataframe[col].bfill().fillna(dataframe['high'] if 'high' in col else dataframe['low'])
else:
dataframe[col] = dataframe[col].fillna(False)
except Exception as e:
logger.warning(f"Error calculating breakdown patterns: {e}")
# Fallback values
dataframe['recent_high_5'] = dataframe['high']
dataframe['recent_low_5'] = dataframe['low']
dataframe['recent_high_10'] = dataframe['high']
dataframe['recent_low_10'] = dataframe['low']
dataframe['ma_alignment_bull'] = False
dataframe['ma_alignment_bear'] = False
dataframe['rejected_at_ema21'] = False
dataframe['rejected_at_ema50'] = False
dataframe['bounced_from_ema21'] = False
dataframe['bounced_from_ema50'] = False
dataframe['red_candle'] = False
dataframe['green_candle'] = False
dataframe['strong_red'] = False
dataframe['strong_green'] = False
# === PLOT CONFIGURATION ===
try:
self.plot_config = (
PlotConfig()
.add_total_divergences_in_config(dataframe)
.config)
except:
self.plot_config = None
logger.debug(f"✅ [INDICATORS] Completed processing for {pair}")
# Run daily optimization check (only once per day)
if not hasattr(self, 'last_daily_check'):
self.last_daily_check = 0
current_time = time.time()
if current_time - self.last_daily_check > 3600: # Check every hour instead of daily
self.daily_optimization_check()
self.last_daily_check = current_time
return dataframe
def _add_dummy_1h_columns(self, dataframe):
"""Add dummy 1h columns when higher timeframe data is unavailable"""
# Use current 15m data to simulate 1h trend
try:
dataframe['ema50_1h_1h'] = ta.EMA(dataframe, timeperiod=200) # Use longer period on 15m
dataframe['ema200_1h_1h'] = ta.EMA(dataframe, timeperiod=800) # Use much longer period
dataframe['trend_1h_1h'] = ta.EMA(dataframe, timeperiod=84) # 21 * 4 (4x 15m = 1h)
dataframe['trend_strength_1h_1h'] = ta.ADX(dataframe)
dataframe['rsi_1h_1h'] = ta.RSI(dataframe, timeperiod=56) # Adjusted for timeframe
# Fill NaN values
columns_1h = ['ema50_1h_1h', 'ema200_1h_1h', 'trend_1h_1h', 'trend_strength_1h_1h', 'rsi_1h_1h']
for col in columns_1h:
if col in dataframe.columns:
dataframe[col] = dataframe[col].bfill().fillna(
dataframe['close'] if 'ema' in col or 'trend' in col else
25 if 'strength' in col else 50
)
except Exception as e:
logger.warning(f"Error creating dummy 1h columns: {e}")
# Absolute fallback
dataframe['ema50_1h_1h'] = dataframe['close']
dataframe['ema200_1h_1h'] = dataframe['close']
dataframe['trend_1h_1h'] = dataframe['close']
dataframe['trend_strength_1h_1h'] = 25
dataframe['rsi_1h_1h'] = 50
def calculate_signal_strength(self, dataframe: DataFrame) -> Series:
"""
Calculate overall signal strength based on multiple factors
"""
strength = pd.Series(0, index=dataframe.index)
# Divergence strength
strength += dataframe['total_bullish_divergences_count'] * 2
strength += dataframe['total_bearish_divergences_count'] * 2
# Volume strength
volume_strength = np.where(dataframe['volume_ratio'] > 1.5, 2,
np.where(dataframe['volume_ratio'] > 1.2, 1, 0))
strength += volume_strength
# Trend alignment strength
ema_bullish = (dataframe['ema20'] > dataframe['ema50']) & (dataframe['ema50'] > dataframe['ema200'])
ema_bearish = (dataframe['ema20'] < dataframe['ema50']) & (dataframe['ema50'] < dataframe['ema200'])
strength += np.where(ema_bullish | ema_bearish, 1, 0)
# ADX strength
strength += np.where(dataframe['adx'] > 30, 1, 0)
return strength
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
print(f"Processing entry signals for {metadata['pair']}") # Debug line
"""
Enhanced entry strategy with RealOptuna optimization
Based on better-performing original logic with dynamic parameter tuning
"""
if len(dataframe) > 0:
last_row = dataframe.iloc[-1]
print(f" RSI: {last_row.get('rsi', 0):.1f}")
print(f" Volume: {last_row.get('volume', 0)}")
print(f" Bull Div: {last_row.get('total_bullish_divergences', 0)}")
print(f" Bear Div: {last_row.get('total_bearish_divergences', 0)}")
# Initialize
dataframe['enter_long'] = 0
dataframe['enter_short'] = 0
dataframe["enter_tag"] = ""
dataframe['entry_type'] = 0
# Get optimized parameters for this pair
coin_params = self.get_coin_params(metadata['pair'])
pair = metadata['pair']
signal_lock_enabled = coin_params.get('signal_lock_enabled', True)
# === BASIC FILTERS ===
has_volume = dataframe['volume'] > 0
# === PRIMARY DIVERGENCE CONDITIONS (E1) - HIGHEST PRIORITY ===
bullish_divergence = (
(dataframe['total_bullish_divergences'].shift(1) > 0)
)
bearish_divergence = (
(dataframe['total_bearish_divergences'].shift(1) > 0)
)
# Volatility filter with Optuna optimization
volatility_ok = (
(dataframe['volatility'].shift(1) >= coin_params.get('min_volatility', 0.003) * coin_params.get('volatility_min_multiplier', 0.5)) &
(dataframe['volatility'].shift(1) <= coin_params.get('max_volatility', 0.035) * coin_params.get('volatility_max_multiplier', 2.0))
)
# KC Bands filter with optimization
bands_long = (
(dataframe['low'] <= dataframe['kc_lowerband']) |
(dataframe['close'] <= dataframe['kc_lowerband'])
)
bands_short = (
(dataframe['high'] >= dataframe['kc_upperband']) |
(dataframe['close'] >= dataframe['kc_upperband'])
)
# RSI filters with optimization
rsi_long_ok = (
(dataframe['rsi'].shift(1) < coin_params.get('e1_rsi_overbought_threshold', 75) + coin_params.get('e1_rsi_overbought_buffer', 5)) &
(dataframe['rsi'].shift(1) > coin_params.get('e1_rsi_oversold_min', 25))
)
rsi_short_ok = (
(dataframe['rsi'].shift(1) > coin_params.get('e1_rsi_oversold_threshold', 25) - coin_params.get('e1_rsi_oversold_buffer', 5)) &
(dataframe['rsi'].shift(1) < coin_params.get('e1_rsi_overbought_max', 75))
)
long_condition_primary = (
bullish_divergence &
volatility_ok &
bands_long &
rsi_long_ok &
has_volume
)
short_condition_primary = (
bearish_divergence &
volatility_ok &
bands_short &
rsi_short_ok &
has_volume
)
# Enhanced Support Bounce (Long Entry)
long_condition_support_bounce = (
# Multiple support level confluence - stronger than single swing level
(
# Primary: Recent low near swing support (existing)
(dataframe['low'] <= dataframe['swing_low'] * (1 + coin_params.get('support_tolerance', 0.005))) |
# Secondary: EMA20/50 support confluence
(
(dataframe['low'] <= dataframe['ema_short'] * (1 - coin_params.get('ema_support_tolerance', 0.008))) &
(dataframe['ema_short'] > dataframe['ema_long']) # Bullish EMA alignment
) |
# Tertiary: BB lower band support
(dataframe['low'] <= dataframe['bb_lower'] * (1 + coin_params.get('bb_support_tolerance', 0.01)))
) &
# Stronger bounce confirmation
(dataframe['close'] > dataframe['swing_low'] * (1 + coin_params.get('support_bounce_min', 0.002))) &
(dataframe['close'] > dataframe['open']) & # Green candle
(dataframe['close'] > dataframe['close'].shift(1)) & # Higher close than previous
# Enhanced volume confirmation
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('sr_volume_window', 20)).mean() *
coin_params.get('strong_sr_volume_multiplier', 1.8)) & # Higher threshold
# Better trend context
(dataframe['adx'] > coin_params.get('strong_sr_adx_min', 18)) & # Stronger momentum requirement
(dataframe['ema20'] >= dataframe['ema50']) & # Bullish bias
# Improved RSI positioning
(dataframe['rsi'] > coin_params.get('strong_sr_rsi_min_long', 20)) &
(dataframe['rsi'] < coin_params.get('strong_sr_rsi_max_long', 50)) &
(dataframe['rsi'] > dataframe['rsi'].shift(coin_params.get('sr_momentum_lookback', 2))) & # RSI improving
# Enhanced position and structure filters
(dataframe['price_position'] < coin_params.get('sr_price_position_max_long', 0.6)) &
(dataframe['bb_percent'] < coin_params.get('sr_bb_max_long', 0.7)) &
# Distance filter - ensure room to move up
(dataframe['close'] < dataframe['swing_high'] * (1 - coin_params.get('min_distance_to_resistance', 0.02))) &
# Volatility filters
(dataframe['volatility'] >= coin_params.get('min_volatility', 0.005)) &
(dataframe['volatility'] <= coin_params.get('max_volatility', 0.025)) &
# Momentum confirmation over multiple periods
(dataframe['close'] > dataframe['close'].shift(coin_params.get('sr_momentum_lookback', 2))) &
has_volume
)
# Enhanced Resistance Rejection (Short Entry)
short_condition_resistance_bounce = (
# Multiple resistance level confluence - stronger than single swing level
(
# Primary: Recent high near swing resistance (existing)
(dataframe['high'] >= dataframe['swing_high'] * (1 - coin_params.get('resistance_tolerance', 0.005))) |
# Secondary: EMA20/50 resistance confluence
(
(dataframe['high'] >= dataframe['ema_short'] * (1 + coin_params.get('ema_resistance_tolerance', 0.008))) &
(dataframe['ema_short'] < dataframe['ema_long']) # Bearish EMA alignment
) |
# Tertiary: BB upper band resistance
(dataframe['high'] >= dataframe['bb_upper'] * (1 - coin_params.get('bb_resistance_tolerance', 0.01)))
) &
# Stronger rejection confirmation
(dataframe['close'] < dataframe['swing_high'] * (1 - coin_params.get('resistance_bounce_min', 0.002))) &
(dataframe['close'] < dataframe['open']) & # Red candle
(dataframe['close'] < dataframe['close'].shift(1)) & # Lower close than previous
# Enhanced volume confirmation
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('sr_volume_window', 20)).mean() *
coin_params.get('strong_sr_volume_multiplier', 1.8)) & # Higher threshold
# Better trend context
(dataframe['adx'] > coin_params.get('strong_sr_adx_min', 18)) & # Stronger momentum requirement
(dataframe['ema_short'] <= dataframe['ema_long']) & # Bearish bias
# Improved RSI positioning
(dataframe['rsi'] > coin_params.get('strong_sr_rsi_min_short', 50)) &
(dataframe['rsi'] < coin_params.get('strong_sr_rsi_max_short', 80)) &
(dataframe['rsi'] < dataframe['rsi'].shift(coin_params.get('sr_momentum_lookback', 2))) & # RSI declining
# Enhanced position and structure filters
(dataframe['price_position'] > coin_params.get('sr_price_position_min_short', 0.4)) &
(dataframe['bb_percent'] > coin_params.get('sr_bb_min_short', 0.3)) &
# Distance filter - ensure room to move down
(dataframe['close'] > dataframe['swing_low'] * (1 + coin_params.get('min_distance_to_support', 0.02))) &
# Volatility filters
(dataframe['volatility'] >= coin_params.get('min_volatility', 0.005)) &
(dataframe['volatility'] <= coin_params.get('max_volatility', 0.025)) &
# Momentum confirmation over multiple periods
(dataframe['close'] < dataframe['close'].shift(coin_params.get('sr_momentum_lookback', 2))) &
has_volume
)
# === ENHANCED S/R BOUNCE WITH CONFLUENCE ===
# Stronger version that requires multiple confirmations
# Strong Support Bounce (Higher Quality)
long_condition_strong_support = (
# Multiple support level confluence - at least one must be touched
(
# EMA50 support (existing)
(dataframe['low'] <= dataframe['ema_long'] * (1 - coin_params.get('support_tolerance', 0.005))) |
# BB lower band support
(dataframe['low'] <= dataframe['bb_lower'] * (1 + coin_params.get('bb_support_tolerance', 0.01))) |
# KC lower band support
(dataframe['low'] <= dataframe['kc_lowerband'] * (1 + coin_params.get('kc_support_tolerance', 0.01))) |
# EMA20 support for shorter-term bounces
(dataframe['low'] <= dataframe['ema_short'] * (1 - coin_params.get('ema_support_tolerance', 0.008)))
) &
# Stronger bounce confirmation
(dataframe['close'] > dataframe['low'] * (1 + coin_params.get('support_bounce_min', 0.002))) &
(dataframe['close'] > dataframe['open']) & # Green candle confirmation
# Trend and momentum context
(dataframe['adx'] > coin_params.get('strong_sr_adx_min', 18)) & # Momentum strength
# Enhanced volume requirement - more selective
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('sr_volume_window', 20)).mean() *
coin_params.get('strong_sr_volume_multiplier', 1.8)) &
# Tighter RSI positioning for quality entries
(dataframe['rsi'] > coin_params.get('strong_sr_rsi_min_long', 20)) &
(dataframe['rsi'] < coin_params.get('strong_sr_rsi_max_long', 50)) &
# Distance filter - ensure room to move up (avoid resistance squeeze)
(dataframe['close'] < dataframe['swing_high'] * (1 - coin_params.get('min_distance_to_resistance', 0.02))) &
# Basic volatility filter
(dataframe['volatility'] >= coin_params.get('min_volatility', 0.005)) &
(dataframe['volatility'] <= coin_params.get('max_volatility', 0.025)) &
has_volume
)
# Enhanced Strong Resistance (Short Entry)
short_condition_strong_resistance = (
# Multiple resistance level confluence - at least one must be touched
(
# EMA50 resistance (existing)
(dataframe['high'] >= dataframe['ema_long'] * (1 + coin_params.get('resistance_tolerance', 0.005))) |
# BB upper band resistance
(dataframe['high'] >= dataframe['bb_upper'] * (1 - coin_params.get('bb_resistance_tolerance', 0.01))) |
# KC upper band resistance
(dataframe['high'] >= dataframe['kc_upperband'] * (1 - coin_params.get('kc_resistance_tolerance', 0.01))) |
# EMA20 resistance for shorter-term rejections
(dataframe['high'] >= dataframe['ema_short'] * (1 + coin_params.get('ema_resistance_tolerance', 0.008)))
) &
# Stronger rejection confirmation
(dataframe['close'] < dataframe['high'] * (1 - coin_params.get('resistance_bounce_min', 0.002))) &
(dataframe['close'] < dataframe['open']) & # Red candle confirmation
# Trend and momentum context
(dataframe['adx'] > coin_params.get('strong_sr_adx_min', 18)) & # Momentum strength
# Enhanced volume requirement - more selective
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('sr_volume_window', 20)).mean() *
coin_params.get('strong_sr_volume_multiplier', 1.8)) &
# Tighter RSI positioning for quality entries
(dataframe['rsi'] > coin_params.get('strong_sr_rsi_min_short', 50)) &
(dataframe['rsi'] < coin_params.get('strong_sr_rsi_max_short', 80)) &
# Distance filter - ensure room to move down (avoid support squeeze)
(dataframe['close'] > dataframe['swing_low'] * (1 + coin_params.get('min_distance_to_support', 0.02))) &
# Basic volatility filter
(dataframe['volatility'] >= coin_params.get('min_volatility', 0.005)) &
(dataframe['volatility'] <= coin_params.get('max_volatility', 0.025)) &
has_volume
)
# === TREND BREAKOUT CONDITIONS ===
long_condition_trend = (
# Core trend detection (more flexible)
(
# Primary trend: EMA20 above EMA50 (main trend signal)
(dataframe['ema_short'] > dataframe['ema_long']) &
# Secondary: Recent bullish momentum
(dataframe['close'] > dataframe['ema20'].shift(1)) &
# Price action: Break above previous resistance or pullback entry
(
# Breakout scenario
(dataframe['close'] > dataframe['high'].shift(coin_params.get('trend_close_lookback', 2))) |
# Pullback scenario - price pulling back to EMA20 after uptrend
(
(dataframe['close'].shift(2) > dataframe['ema20'].shift(2)) &
(dataframe['low'] <= dataframe['ema20']) &
(dataframe['close'] > dataframe['ema_short'])
)
)
) &
# Volume confirmation (less restrictive)
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('trend_volume_window', 20)).mean() * coin_params.get('trend_volume_multiplier', 1.2)) &
# RSI not oversold (broader range)
(dataframe['rsi'].shift(1) > coin_params.get('trend_rsi_min', 35)) &
(dataframe['rsi'].shift(1) < coin_params.get('trend_rsi_max', 70)) &
# Volatility filter
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('trend_atr_window', 20)).mean() * coin_params.get('trend_atr_multiplier', 0.8)) &
# Momentum confirmation
(dataframe['close'] > dataframe['close'].shift(3)) &
# Optional: Weak ADX requirement (trend forming)
(dataframe['adx'] > coin_params.get('trend_adx_min', 15)) &
has_volume
)
short_condition_trend = (
# Core trend detection (more flexible)
(
# Primary trend: EMA20 below EMA50 (main trend signal)
(dataframe['ema_short'] < dataframe['ema_long']) &
# Secondary: Recent bearish momentum
(dataframe['close'] < dataframe['ema20'].shift(1)) &
# Price action: Break below previous support or pullback entry
(
# Breakdown scenario
(dataframe['close'] < dataframe['low'].shift(coin_params.get('trend_close_lookback', 2))) |
# Pullback scenario - price pulling back to EMA20 after downtrend
(
(dataframe['close'].shift(2) < dataframe['ema20'].shift(2)) &
(dataframe['high'] >= dataframe['ema20']) &
(dataframe['close'] < dataframe['ema_short'])
)
)
) &
# Volume confirmation (less restrictive)
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('trend_volume_window', 20)).mean() * coin_params.get('trend_volume_multiplier', 1.2)) &
# RSI not overbought (broader range)
(dataframe['rsi'].shift(1) > coin_params.get('trend_rsi_min', 30)) &
(dataframe['rsi'].shift(1) < coin_params.get('trend_rsi_max', 65)) &
# Volatility filter
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('trend_atr_window', 20)).mean() * coin_params.get('trend_atr_multiplier', 0.8)) &
# Momentum confirmation
(dataframe['close'] < dataframe['close'].shift(3)) &
# Optional: Weak ADX requirement (trend forming)
(dataframe['adx'] > coin_params.get('trend_adx_min', 15)) &
has_volume
)
# === ENHANCED MOMENTUM CONTINUATION CONDITIONS ===
long_condition_momentum = (
# Momentum detection (multiple scenarios)
(
# Scenario 1: Strong momentum continuation
(
(dataframe['close'] > dataframe['ema_short']) &
(dataframe['rsi'] > dataframe['rsi'].shift(1)) &
(dataframe['rsi'].shift(1) > coin_params.get('momentum_rsi_trigger', 50)) &
(dataframe['close'] > dataframe['high'].shift(coin_params.get('momentum_high_lookback', 2)))
) |
# Scenario 2: Momentum recovery after pullback
(
(dataframe['rsi'].shift(3) < coin_params.get('momentum_rsi_pivot', 45)) &
(dataframe['rsi'] > coin_params.get('momentum_rsi_trigger', 52)) &
(dataframe['close'] > dataframe['ema_short']) &
(dataframe['volume'] > dataframe['volume'].shift(1))
) |
# Scenario 3: Breaking out of consolidation with momentum
(
(dataframe['close'] > dataframe['high'].rolling(5).max().shift(1)) &
(dataframe['rsi'] > 50) &
(dataframe['volume'] > dataframe['volume'].rolling(10).mean() * 1.5)
)
) &
# Basic trend filter (less restrictive)
(dataframe['ema20'] >= dataframe['ema50']) &
# Volume confirmation (flexible)
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('momentum_volume_window', 10)).mean() * coin_params.get('momentum_volume_multiplier', 1.0)) &
# Not in extreme overbought
(dataframe['close'] < dataframe['kc_upperband'] * coin_params.get('momentum_kc_upper_factor', 0.998)) &
# Momentum strength
(dataframe['adx'] > coin_params.get('momentum_adx_min', 20)) &
has_volume
)
short_condition_momentum = (
# Momentum detection (multiple scenarios)
(
# Scenario 1: Strong momentum continuation
(
(dataframe['close'] < dataframe['ema_short']) &
(dataframe['rsi'] < dataframe['rsi'].shift(1)) &
(dataframe['rsi'].shift(1) < coin_params.get('momentum_rsi_trigger_short', 50)) &
(dataframe['close'] < dataframe['low'].shift(coin_params.get('momentum_low_lookback', 2)))
) |
# Scenario 2: Momentum recovery after pullback (bearish)
(
(dataframe['rsi'].shift(3) > coin_params.get('momentum_rsi_pivot', 55)) &
(dataframe['rsi'] < coin_params.get('momentum_rsi_trigger_short', 48)) &
(dataframe['close'] < dataframe['ema_short']) &
(dataframe['volume'] > dataframe['volume'].shift(1))
) |
# Scenario 3: Breaking down from consolidation with momentum
(
(dataframe['close'] < dataframe['low'].rolling(5).min().shift(1)) &
(dataframe['rsi'] < 50) &
(dataframe['volume'] > dataframe['volume'].rolling(10).mean() * 1.5)
)
) &
# Basic trend filter (less restrictive)
(dataframe['ema_short'] <= dataframe['ema_long']) &
# Volume confirmation (flexible)
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('momentum_volume_window', 10)).mean() * coin_params.get('momentum_volume_multiplier', 1.0)) &
# Not in extreme oversold
(dataframe['close'] > dataframe['kc_lowerband'] * coin_params.get('momentum_kc_lower_factor', 1.002)) &
# Momentum strength
(dataframe['adx'] > coin_params.get('momentum_adx_min', 20)) &
has_volume
)
# === ADD NEW MOMENTUM BREAKOUT CONDITIONS ===
# Momentum Breakout Long
long_momentum_breakout = (
# Trend context using existing trend parameters
(dataframe['ema_short'] > dataframe['ema_long']) & # Overall bullish trend
(dataframe['adx'] > coin_params.get('momentum_adx_min', 25)) & # Strong momentum
# Enhanced breakout detection
(dataframe['close'] > dataframe['high'].rolling(coin_params.get('trend_close_lookback', 2) * 10).max().shift(1)) & # Longer period breakout
# Volume confirmation using existing parameters
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('momentum_volume_window', 10)).mean() *
coin_params.get('volume_breakout_multiplier', 2.5)) & # Your existing volume breakout param
# RSI positioning using existing parameters
(dataframe['rsi'] > coin_params.get('volume_breakout_rsi_min', 45.0)) &
(dataframe['rsi'] < coin_params.get('volume_breakout_rsi_max', 60.0)) &
# Momentum strength filter
(dataframe['close'] > dataframe['close'].shift(coin_params.get('momentum_high_lookback', 2))) &
# ATR expansion for volatility
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('trend_atr_window', 20)).mean() *
coin_params.get('trend_atr_multiplier', 1.0)) &
has_volume
)
# Momentum Breakout Short
short_momentum_breakout = (
# Trend context using existing trend parameters
(dataframe['ema_short'] < dataframe['ema_long']) & # Overall bearish trend
(dataframe['adx'] > coin_params.get('momentum_adx_min', 25)) & # Strong momentum
# Enhanced breakdown detection
(dataframe['close'] < dataframe['low'].rolling(coin_params.get('trend_close_lookback', 2) * 10).min().shift(1)) & # Longer period breakdown
# Volume confirmation using existing parameters
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('momentum_volume_window', 10)).mean() *
coin_params.get('volume_breakout_multiplier', 2.5)) & # Your existing volume breakout param
# RSI positioning - inverted for short
(dataframe['rsi'] < coin_params.get('breakdown_rsi_upper', 60)) & # Using your breakdown params
(dataframe['rsi'] > coin_params.get('breakdown_rsi_lower', 40)) & # Avoid oversold bounces
# Momentum strength filter
(dataframe['close'] < dataframe['close'].shift(coin_params.get('momentum_low_lookback', 2))) &
# ATR expansion for volatility
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('trend_atr_window', 20)).mean() *
coin_params.get('trend_atr_multiplier', 1.0)) &
has_volume
)
# === ADD PULLBACK MOMENTUM CONDITIONS ===
# Using existing optimized parameters for better confluence and filtering
# Enhanced Long Pullback Momentum
long_pullback_momentum = (
# Stronger trend context - multiple confirmations
(dataframe['ema_short'] > dataframe['ema_long']) &
(dataframe['ema50'] > dataframe['ema200']) & # Longer-term bullish context
(dataframe['adx'] > coin_params.get('momentum_adx_min', 25)) & # Strong trend momentum
# Better pullback detection - more substantial pullback required
(
# Touched EMA20 support (existing)
(dataframe['low'] <= dataframe['ema20']) &
# Must have pulled back meaningfully from recent high
(dataframe['low'] < dataframe['high'].rolling(coin_params.get('momentum_high_lookback', 2) * 3).max().shift(1) * 0.98) &
# Price was above EMA20 recently (confirms this is a pullback, not breakdown)
(dataframe['close'].shift(coin_params.get('momentum_rsi_lookback', 3)) > dataframe['ema20'].shift(coin_params.get('momentum_rsi_lookback', 3)))
) &
# Stronger bounce confirmation
(dataframe['close'] > dataframe['ema_short']) & # Back above support
(dataframe['close'] > dataframe['open']) & # Green candle
(dataframe['close'] > dataframe['close'].shift(1)) & # Higher close than previous
# Enhanced volume and momentum confirmation
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('momentum_volume_window', 10)).mean() *
coin_params.get('momentum_volume_multiplier', 1.2) * 1.5) & # 1.8x total volume requirement
# Better RSI positioning and momentum
(dataframe['rsi'] > coin_params.get('momentum_rsi_trigger', 55)) & # Above midpoint
(dataframe['rsi'] < coin_params.get('volume_breakout_rsi_max', 60.0)) & # Not overbought
(dataframe['rsi'] > dataframe['rsi'].rolling(coin_params.get('momentum_rsi_lookback', 3)).mean()) & # RSI improving over period
# Price structure confirmation
(dataframe['close'] > dataframe['kc_middleband']) & # Above middle of range
(dataframe['close'] < dataframe['kc_upperband'] * coin_params.get('momentum_kc_upper_factor', 0.995)) & # Not at extreme top
# Volatility and market conditions
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('trend_atr_window', 20)).mean() *
coin_params.get('trend_atr_multiplier', 1.0)) & # Sufficient volatility
has_volume
)
# Enhanced Short Pullback Momentum
short_pullback_momentum = (
# Stronger trend context - multiple confirmations
(dataframe['ema_short'] < dataframe['ema_long']) &
(dataframe['ema50'] < dataframe['ema200']) & # Longer-term bearish context
(dataframe['adx'] > coin_params.get('momentum_adx_min', 25)) & # Strong trend momentum
# Better pullback detection - more substantial pullback required
(
# Touched EMA20 resistance (existing)
(dataframe['high'] >= dataframe['ema20']) &
# Must have pulled back meaningfully from recent low
(dataframe['high'] > dataframe['low'].rolling(coin_params.get('momentum_low_lookback', 2) * 3).min().shift(1) * 1.02) &
# Price was below EMA20 recently (confirms this is a pullback, not breakout)
(dataframe['close'].shift(coin_params.get('momentum_rsi_lookback', 3)) < dataframe['ema20'].shift(coin_params.get('momentum_rsi_lookback', 3)))
) &
# Stronger rejection confirmation
(dataframe['close'] < dataframe['ema_short']) & # Back below resistance
(dataframe['close'] < dataframe['open']) & # Red candle
(dataframe['close'] < dataframe['close'].shift(1)) & # Lower close than previous
# Enhanced volume and momentum confirmation
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('momentum_volume_window', 10)).mean() *
coin_params.get('momentum_volume_multiplier', 1.2) * 1.5) & # 1.8x total volume requirement
# Better RSI positioning and momentum
(dataframe['rsi'] < coin_params.get('momentum_rsi_trigger_short', 45)) & # Below midpoint
(dataframe['rsi'] > coin_params.get('volume_breakout_rsi_min', 45.0) - 15) & # Not oversold (30+)
(dataframe['rsi'] < dataframe['rsi'].rolling(coin_params.get('momentum_rsi_lookback', 3)).mean()) & # RSI declining over period
# Price structure confirmation
(dataframe['close'] < dataframe['kc_middleband']) & # Below middle of range
(dataframe['close'] > dataframe['kc_lowerband'] * coin_params.get('momentum_kc_lower_factor', 1.005)) & # Not at extreme bottom
# Volatility and market conditions
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('trend_atr_window', 20)).mean() *
coin_params.get('trend_atr_multiplier', 1.0)) & # Sufficient volatility
has_volume
)
# === SECONDARY CONDITIONS ===
long_condition_secondary = (
bullish_divergence &
(dataframe['close'] > dataframe['close'].shift(coin_params.get('secondary_close_lookback', 2))) &
(dataframe['rsi'].shift(1) > coin_params.get('secondary_rsi_min', 35)) &
(dataframe['rsi'].shift(1) < coin_params.get('secondary_rsi_max', 70)) &
has_volume
)
short_condition_secondary = (
bearish_divergence &
(dataframe['close'] < dataframe['close'].shift(coin_params.get('secondary_close_lookback', 2))) &
(dataframe['rsi'].shift(1) > coin_params.get('secondary_rsi_min_short', 30)) &
(dataframe['rsi'].shift(1) < coin_params.get('secondary_rsi_max_short', 80)) &
has_volume
)
# === TERTIARY CONDITIONS ===
long_condition_tertiary = (
bullish_divergence &
(dataframe['close'] > dataframe['ema_short']) &
(dataframe['rsi'].shift(1) > coin_params.get('tertiary_rsi_min', 40)) &
(dataframe['rsi'].shift(1) < coin_params.get('tertiary_rsi_max', 65)) &
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('tertiary_volume_window', 10)).mean() * coin_params.get('tertiary_volume_multiplier', 1.0)) &
has_volume
)
short_condition_tertiary = (
bearish_divergence &
(dataframe['close'] < dataframe['ema_short']) &
(dataframe['rsi'].shift(1) > coin_params.get('tertiary_rsi_min_short', 35)) &
(dataframe['rsi'].shift(1) < coin_params.get('tertiary_rsi_max_short', 75)) &
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('tertiary_volume_window', 10)).mean() * coin_params.get('tertiary_volume_multiplier', 1.0)) &
has_volume
)
# === QUATERNARY CONDITIONS (EMA CROSSOVER) ===
long_condition_quaternary = (
bullish_divergence &
(dataframe['close'].shift(1) < dataframe['ema20'].shift(1)) &
(dataframe['close'] > dataframe['ema_short']) &
(dataframe['rsi'] > coin_params.get('quaternary_rsi_min', 45)) &
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('quaternary_volume_window', 5)).mean() * coin_params.get('quaternary_volume_multiplier', 1.2)) &
has_volume
)
short_condition_quaternary = (
bearish_divergence &
(dataframe['close'].shift(1) > dataframe['ema20'].shift(1)) &
(dataframe['close'] < dataframe['ema_short']) &
(dataframe['rsi'] < coin_params.get('quaternary_rsi_max', 55)) &
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('quaternary_volume_window', 5)).mean() * coin_params.get('quaternary_volume_multiplier', 1.2)) &
has_volume
)
# === FIFTH CONDITIONS (KC MIDDLE BAND) ===
long_condition_fifth = (
bullish_divergence &
(dataframe['close'] > dataframe['kc_middleband']) &
(dataframe['close'] < dataframe['kc_upperband']) &
(dataframe['close'] > dataframe['close'].shift(1)) &
(dataframe['rsi'].shift(1) > coin_params.get('fifth_rsi_min', 35)) &
(dataframe['rsi'].shift(1) < coin_params.get('fifth_rsi_max', 70)) &
has_volume
)
short_condition_fifth = (
bearish_divergence &
(dataframe['close'] < dataframe['kc_middleband']) &
(dataframe['close'] > dataframe['kc_lowerband']) &
(dataframe['close'] < dataframe['close'].shift(1)) &
(dataframe['rsi'].shift(1) > coin_params.get('fifth_rsi_min_short', 30)) &
(dataframe['rsi'].shift(1) < coin_params.get('fifth_rsi_max_short', 65)) &
has_volume
)
# === SIXTH CONDITIONS - STRONG DIVERGENCE COUNT ===
long_condition_sixth = (
(dataframe['total_bullish_divergences_count'] >= coin_params.get('sixth_min_div_count', 2)) &
bullish_divergence &
(dataframe['rsi'].shift(1) > coin_params.get('sixth_rsi_min', 30)) &
(dataframe['rsi'].shift(1) < coin_params.get('sixth_rsi_max', 75)) &
has_volume
)
short_condition_sixth = (
(dataframe['total_bearish_divergences_count'] >= coin_params.get('sixth_min_div_count', 2)) &
bearish_divergence &
(dataframe['rsi'].shift(1) > coin_params.get('sixth_rsi_min_short', 25)) &
(dataframe['rsi'].shift(1) < coin_params.get('sixth_rsi_max_short', 70)) &
has_volume
)
# Enhanced Seventh Condition - Uses Optuna Parameters
long_condition_seventh = (
# 1. Trend Context - Bullish bias
(dataframe['ema_short'] > dataframe['ema_long']) &
(dataframe['ema50'] > dataframe['ema200']) & # Add stronger trend filter
# 2. Pullback and Recovery Pattern
(
# Was below EMA20 recently (pullback happened)
(dataframe['close'].shift(coin_params.get('seventh_close_lookback', 2)) < dataframe['ema20'].shift(coin_params.get('seventh_close_lookback', 2))) &
# Now recovering above EMA20 (breakout from pullback)
(dataframe['close'] > dataframe['ema_short']) &
# Clear break above with momentum
(dataframe['close'] > dataframe['ema20'] * 1.002) # 0.2% above EMA20
) &
# 3. Price Action Confirmation
(
# Current candle is green (bullish momentum)
(dataframe['close'] > dataframe['open']) &
# Recent momentum building
(dataframe['close'] > dataframe['close'].shift(1)) &
# Not too extended above EMA20
(dataframe['close'] < dataframe['ema20'] * 1.02) # Within 2% of EMA20
) &
# 4. Volume and Strength Filters
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('seventh_volume_window', 20)).mean() * coin_params.get('seventh_volume_multiplier', 1.2)) &
(dataframe['rsi'].shift(1) > coin_params.get('seventh_rsi_min', 35)) & # Not oversold
(dataframe['rsi'].shift(1) < coin_params.get('seventh_rsi_max', 65)) & # Not overbought
(dataframe['rsi'] > dataframe['rsi'].shift(1)) & # RSI improving
# 5. Market Structure
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('seventh_atr_window', 20)).mean() * coin_params.get('seventh_atr_multiplier', 0.7)) &
(dataframe['close'] > dataframe['kc_middleband']) & # Above middle band
(dataframe['adx'] > coin_params.get('seventh_adx_min', 18)) &
# 6. Recent Context Validation
(dataframe['low'] <= dataframe['ema20'].shift(1)) & # Recently touched EMA20 support
(dataframe['close'] > dataframe['low'] * 1.005) & # Clear bounce from low
has_volume
)
# Bear E7 - Improved Short Condition
short_condition_seventh = (
# 1. Trend Context - Bearish bias
(dataframe['ema_short'] < dataframe['ema_long']) &
(dataframe['ema50'] < dataframe['ema200']) & # Strong bearish trend
# 2. Pullback and Rejection Pattern
(
# Was above EMA20 recently (pullback happened)
(dataframe['close'].shift(coin_params.get('seventh_close_lookback', 2)) > dataframe['ema20'].shift(coin_params.get('seventh_close_lookback', 2))) &
# Now rejected below EMA20 (breakdown from pullback)
(dataframe['close'] < dataframe['ema_short']) &
# Clear break below with momentum
(dataframe['close'] < dataframe['ema20'] * 0.998) # 0.2% below EMA20
) &
# 3. Price Action Confirmation
(
# Current candle is red (bearish momentum)
(dataframe['close'] < dataframe['open']) &
# Recent momentum declining
(dataframe['close'] < dataframe['close'].shift(1)) &
# Actually touched resistance (not just any EMA20 cross)
(dataframe['high'] >= dataframe['ema_short'] * 0.999) & # Very close touch
# Strong rejection with upper wick
(dataframe['high'] - dataframe['close']) > (dataframe['close'] - dataframe['low'])
) &
# 4. Volume and Strength Filters
(dataframe['volume'] > dataframe['volume'].rolling(coin_params.get('seventh_volume_window', 20)).mean() * coin_params.get('seventh_volume_multiplier', 1.2)) &
(dataframe['rsi'].shift(1) > coin_params.get('seventh_rsi_min_short', 35)) & # Not oversold
(dataframe['rsi'].shift(1) < coin_params.get('seventh_rsi_max_short', 65)) & # Not overbought
(dataframe['rsi'] < dataframe['rsi'].shift(1)) & # RSI declining
# 5. Market Structure
(dataframe['atr'] > dataframe['atr'].rolling(coin_params.get('seventh_atr_window', 20)).mean() * coin_params.get('seventh_atr_multiplier', 0.7)) &
(dataframe['close'] < dataframe['kc_middleband']) & # Below middle band
(dataframe['adx'] > coin_params.get('seventh_adx_min', 18)) &
# 6. Recent Context Validation
(dataframe['high'] >= dataframe['ema20'].shift(1)) & # Recently touched EMA20 resistance
(dataframe['close'] < dataframe['high'] * 0.995) & # Clear rejection from high
# Was showing recent strength before rejection
(dataframe['rsi'].shift(3) > 40) &
has_volume
)
# === PURE DIVERGENCE CONDITIONS - FALLBACK ===
long_condition_div = (
bullish_divergence &
(dataframe['rsi'].shift(1) > coin_params.get('div_rsi_min', 25)) &
(dataframe['rsi'].shift(1) < coin_params.get('div_rsi_max', 80)) &
has_volume
)
short_condition_div = (
bearish_divergence &
(dataframe['rsi'].shift(1) > coin_params.get('div_rsi_min_short', 20)) &
(dataframe['rsi'].shift(1) < coin_params.get('div_rsi_max_short', 75)) &
has_volume
)
strong_bear_breakdown = (
(dataframe['close'] < dataframe['ema21']) &
(dataframe['ma_alignment_bear']) & # EMAs aligned bearish
(dataframe['volume_ratio'] > coin_params.get('breakdown_volume_threshold', 1.5)) & # Strong volume
(dataframe['rsi'] < coin_params.get('breakdown_rsi_upper', 60)) & # Not overbought
(dataframe['adx'] > coin_params.get('breakdown_adx_threshold', 25)) & # Strong trend
(dataframe['strong_red']) & # Volume-confirmed red candle
(dataframe['price_vs_ema21'] < -coin_params.get('breakdown_price_deviation', 1.0)) & # Below EMA21
(dataframe['rejected_at_ema21']) & # Previous rejection at EMA21
(dataframe['close'] < dataframe['ema50']) # Below intermediate MA
)
# Strong Bull Breakdown (Reversal) Pattern
strong_bull_breakdown = (
(dataframe['close'] > dataframe['ema21']) &
(dataframe['ma_alignment_bull']) & # EMAs aligned bullish
(dataframe['volume_ratio'] > coin_params.get('breakdown_volume_threshold', 1.5)) & # Strong volume
(dataframe['rsi'] > coin_params.get('breakdown_rsi_lower', 40)) & # Not oversold
(dataframe['adx'] > coin_params.get('breakdown_adx_threshold', 25)) & # Strong trend
(dataframe['strong_green']) & # Volume-confirmed green candle
(dataframe['price_vs_ema21'] > coin_params.get('breakdown_price_deviation', 1.0)) & # Above EMA21
(dataframe['bounced_from_ema21']) & # Previous bounce from EMA21
(dataframe['close'] > dataframe['ema50']) # Above intermediate MA
)
# Resistance Rejection Pattern (Bears)
resistance_rejection_bear = (
(dataframe['rejected_at_ema50']) & # Rejected at EMA50 resistance
(dataframe['red_candle']) & # Follow-through down
(dataframe['volume_ratio'] > coin_params.get('breakdown_volume_confirmation', 1.2)) & # Volume confirmation
(dataframe['rsi'] < 65) & # Not extremely overbought
(dataframe['close'] < dataframe['ema21']) & # Below shorter MA
(dataframe['ma_alignment_bear']) # Overall bearish alignment
)
# Support Rejection Pattern (Bulls)
support_rejection_bull = (
(dataframe['bounced_from_ema50']) & # Bounced from EMA50 support
(dataframe['green_candle']) & # Follow-through up
(dataframe['volume_ratio'] > coin_params.get('breakdown_volume_confirmation', 1.2)) & # Volume confirmation
(dataframe['rsi'] > 35) & # Not extremely oversold
(dataframe['close'] > dataframe['ema21']) & # Above shorter MA
(dataframe['ma_alignment_bull']) # Overall bullish alignment
)
# Higher Timeframe Bias (using EMA200 as proxy)
higher_tf_bearish = (
(dataframe['close'] < dataframe['ema200']) & # Below long-term trend
(dataframe['ema50'] < dataframe['ema200']) & # MAs aligned bearish
(dataframe['ema21'] < dataframe['ema50']) # Short-term also bearish
)
higher_tf_bullish = (
(dataframe['close'] > dataframe['ema200']) & # Above long-term trend
(dataframe['ema50'] > dataframe['ema200']) & # MAs aligned bullish
(dataframe['ema21'] > dataframe['ema50']) # Short-term also bullish
)
# Enhanced breakdown with multiple confirmation
enhanced_bear_breakdown = (
(strong_bear_breakdown | resistance_rejection_bear) &
higher_tf_bearish &
(dataframe['price_vs_ema200'] < 0) # Below long-term trend
)
enhanced_bull_breakdown = (
(strong_bull_breakdown | support_rejection_bull) &
higher_tf_bullish &
(dataframe['price_vs_ema200'] > 0)
)
simple_breakout_long = (
(dataframe['close'] > dataframe['high'].rolling(10).max().shift(1)) &
(dataframe['volume'] > dataframe['volume'].rolling(15).mean() * coin_params.get('breakout_volume_multiplier', 1.3)) &
(dataframe['rsi'] > 40) & (dataframe['rsi'] < 75) &
(dataframe['ema_short'] > dataframe['ema_long'])
)
simple_breakdown_short = (
(dataframe['close'] < dataframe['low'].rolling(10).min().shift(1)) &
(dataframe['volume'] > dataframe['volume'].rolling(15).mean() * coin_params.get('breakdown_volume_multiplier', 1.3)) &
(dataframe['rsi'] > 35) & (dataframe['rsi'] < 60) &
(dataframe['ema_short'] < dataframe['ema_long'])
)
# === OPTIMIZED RSV1 REVERSAL CONDITIONS ===
# Replace your current reversal conditions with these optimized versions
# Long reversal conditions - using coin-specific parameters
oversold_conditions = [
(dataframe['rsi'] < coin_params.get('rsv1_rsi_oversold', 25.0)),
(dataframe['willr'] < coin_params.get('rsv1_willr_oversold', -80.0)),
(dataframe['cci'] < coin_params.get('rsv1_cci_oversold', -100.0)),
(dataframe['close'] <= dataframe['bb_lower']),
(dataframe['bb_percent'] < coin_params.get('rsv1_bb_oversold', 0.1)),
]
long_reversal_signals = [
(dataframe['volume_ratio'] > coin_params.get('rsv1_volume_factor', 1.5)),
(dataframe['close'] > dataframe['open']),
(dataframe['macdhist'] > dataframe['macdhist'].shift(1)),
(dataframe['price_position'] < coin_params.get('rsv1_price_position_low', 0.2)),
]
long_trend_filter = [
(dataframe['ema_short'] > dataframe['ema_short'].shift(coin_params.get('trend_close_lookback', 3))),
(dataframe['close'] > dataframe['close'].shift(coin_params.get('secondary_close_lookback', 2))),
]
long_condition_reversal = (
(sum(oversold_conditions) >= coin_params.get('rsv1_min_oversold_conditions', 3)) &
(sum(long_reversal_signals) >= coin_params.get('rsv1_min_reversal_signals', 2)) &
(sum(long_trend_filter) >= coin_params.get('rsv1_min_trend_filters', 1)) &
(dataframe['volume'] > 0)
)
# Short reversal conditions - using coin-specific parameters
overbought_conditions = [
(dataframe['rsi'] > coin_params.get('rsv1_rsi_overbought', 75.0)),
(dataframe['willr'] > coin_params.get('rsv1_willr_overbought', -20.0)),
(dataframe['cci'] > coin_params.get('rsv1_cci_overbought', 100.0)),
(dataframe['close'] >= dataframe['bb_upper']),
(dataframe['bb_percent'] > coin_params.get('rsv1_bb_overbought', 0.9)),
]
short_reversal_signals = [
(dataframe['volume_ratio'] > coin_params.get('rsv1_volume_factor', 1.5)),
(dataframe['close'] < dataframe['open']),
(dataframe['macdhist'] < dataframe['macdhist'].shift(1)),
(dataframe['price_position'] > coin_params.get('rsv1_price_position_high', 0.8)),
]
short_trend_filter = [
(dataframe['ema_short'] < dataframe['ema_short'].shift(coin_params.get('trend_close_lookback', 3))),
(dataframe['close'] < dataframe['close'].shift(coin_params.get('secondary_close_lookback', 2))),
]
short_condition_reversal = (
(sum(overbought_conditions) >= coin_params.get('rsv1_min_overbought_conditions', 3)) &
(sum(short_reversal_signals) >= coin_params.get('rsv1_min_reversal_signals', 2)) &
(sum(short_trend_filter) >= coin_params.get('rsv1_min_trend_filters', 1)) &
(dataframe['volume'] > 0)
)
# === MEAN REVERSION CONDITIONS ===
mean_reversion_long = (
(dataframe['bb_percent'].shift(1) < coin_params.get('bb_oversold_threshold', 0.15)) &
(dataframe['rsi'].shift(1) < coin_params.get('mean_reversion_rsi_oversold', 28)) &
(dataframe['close'] > dataframe['low'].shift(1)) & # Not continuing down
(dataframe['volume_ratio'].shift(1) > coin_params.get('volume_ratio_min', 1.1)) &
(dataframe['close'] > dataframe['open']) & # Recovery candle
has_volume
)
mean_reversion_short = (
(dataframe['bb_percent'].shift(1) > coin_params.get('bb_overbought_threshold', 0.85)) &
(dataframe['rsi'].shift(1) > coin_params.get('mean_reversion_rsi_overbought', 72)) &
(dataframe['close'] < dataframe['high'].shift(1)) & # Not continuing up
(dataframe['volume_ratio'].shift(1) > coin_params.get('volume_ratio_min', 1.1)) &
(dataframe['close'] < dataframe['open']) & # Rejection candle
has_volume
)
# === MOMENTUM CONTINUATION CONDITIONS ===
momentum_continuation_long = (
(dataframe['ema9'] > dataframe['ema20']) &
(dataframe['ema_short'] > dataframe['ema_long']) &
(dataframe['close'].shift(1) < dataframe['ema9'].shift(1)) & # Pullback
(dataframe['close'] > dataframe['ema9']) & # Back above
(dataframe['price_position'].shift(1) > coin_params.get('momentum_pullback_max', 0.35)) &
(dataframe['adx'].shift(1) > coin_params.get('adx_trending_min', 25)) &
(dataframe['volume_ratio'].shift(1) > coin_params.get('volume_ratio_min', 1.1)) &
has_volume
)
momentum_continuation_short = (
(dataframe['ema9'] < dataframe['ema20']) &
(dataframe['ema_short'] < dataframe['ema_long']) &
(dataframe['close'].shift(1) > dataframe['ema9'].shift(1)) & # Pullback
(dataframe['close'] < dataframe['ema9']) & # Back below
(dataframe['price_position'].shift(1) < coin_params.get('momentum_pullback_min', 0.65)) &
(dataframe['adx'].shift(1) > coin_params.get('adx_trending_min', 25)) &
(dataframe['volume_ratio'].shift(1) > coin_params.get('volume_ratio_min', 1.1)) &
has_volume
)
# Combine breakdown patterns
long_condition_breakdown = enhanced_bull_breakdown
short_condition_breakdown = enhanced_bear_breakdown
long_condition_hqdiv = bullish_divergence
short_condition_hqdiv = bearish_divergence
# 1. PRIMARY CONDITIONS (E1) - Check lock before assignment
if not signal_lock_enabled or not self.is_signal_locked('Bull_E1', pair):
dataframe.loc[long_condition_primary, 'enter_long'] = 1
dataframe.loc[long_condition_primary, 'enter_tag'] = 'Bull_E1'
else:
print(f"Signal LOCKED: Bull_E1 for {pair}")
if not signal_lock_enabled or not self.is_signal_locked('Bear_E1', pair):
dataframe.loc[short_condition_primary, 'enter_short'] = 1
dataframe.loc[short_condition_primary, 'enter_tag'] = 'Bear_E1'
else:
print(f"Signal LOCKED: Bear_E1 for {pair}")
# 2. SUPPORT/RESISTANCE BOUNCE CONDITIONS
if not signal_lock_enabled or not self.is_signal_locked('Bull_SR_Bounce', pair):
sr_bounce_long = long_condition_support_bounce & (dataframe['enter_tag'] == "")
dataframe.loc[sr_bounce_long, 'enter_long'] = 1
dataframe.loc[sr_bounce_long, 'enter_tag'] = 'Bull_SR_Bounce'
if not signal_lock_enabled or not self.is_signal_locked('Bear_SR_Bounce', pair):
sr_bounce_short = short_condition_resistance_bounce & (dataframe['enter_tag'] == "")
dataframe.loc[sr_bounce_short, 'enter_short'] = 1
dataframe.loc[sr_bounce_short, 'enter_tag'] = 'Bear_SR_Bounce'
# 3. STRONG SUPPORT/RESISTANCE CONDITIONS
if not signal_lock_enabled or not self.is_signal_locked('Bull_Strong_Support', pair):
strong_sr_long = long_condition_strong_support & (dataframe['enter_tag'] == "")
dataframe.loc[strong_sr_long, 'enter_long'] = 1
dataframe.loc[strong_sr_long, 'enter_tag'] = 'Bull_Strong_Support'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Strong_Resistance', pair):
strong_sr_short = short_condition_strong_resistance & (dataframe['enter_tag'] == "")
dataframe.loc[strong_sr_short, 'enter_short'] = 1
dataframe.loc[strong_sr_short, 'enter_tag'] = 'Bear_Strong_Resistance'
# 4. TREND BREAKOUT CONDITIONS
if not signal_lock_enabled or not self.is_signal_locked('Bull_Trend', pair):
trend_long = long_condition_trend & (dataframe['enter_tag'] == "")
dataframe.loc[trend_long, 'enter_long'] = 1
dataframe.loc[trend_long, 'enter_tag'] = 'Bull_Trend'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Trend', pair):
trend_short = short_condition_trend & (dataframe['enter_tag'] == "")
dataframe.loc[trend_short, 'enter_short'] = 1
dataframe.loc[trend_short, 'enter_tag'] = 'Bear_Trend'
# 5. MOMENTUM CONDITIONS
if not signal_lock_enabled or not self.is_signal_locked('Bull_Momentum', pair):
momentum_long = long_condition_momentum & (dataframe['enter_tag'] == "")
dataframe.loc[momentum_long, 'enter_long'] = 1
dataframe.loc[momentum_long, 'enter_tag'] = 'Bull_Momentum'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Momentum', pair):
momentum_short = short_condition_momentum & (dataframe['enter_tag'] == "")
dataframe.loc[momentum_short, 'enter_short'] = 1
dataframe.loc[momentum_short, 'enter_tag'] = 'Bear_Momentum'
# 6. MOMENTUM BREAKOUT CONDITIONS (Enhanced)
if not signal_lock_enabled or not self.is_signal_locked('Bull_Momentum_Breakout', pair):
breakout_long = long_momentum_breakout & (dataframe['enter_tag'] == "")
dataframe.loc[breakout_long, 'enter_long'] = 1
dataframe.loc[breakout_long, 'enter_tag'] = 'Bull_Momentum_Breakout'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Momentum_Breakout', pair):
breakout_short = short_momentum_breakout & (dataframe['enter_tag'] == "")
dataframe.loc[breakout_short, 'enter_short'] = 1
dataframe.loc[breakout_short, 'enter_tag'] = 'Bear_Momentum_Breakout'
# 7. PULLBACK MOMENTUM CONDITIONS (Enhanced)
if not signal_lock_enabled or not self.is_signal_locked('Bull_Pullback_Momentum', pair):
pullback_long = long_pullback_momentum & (dataframe['enter_tag'] == "")
dataframe.loc[pullback_long, 'enter_long'] = 1
dataframe.loc[pullback_long, 'enter_tag'] = 'Bull_Pullback_Momentum'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Pullback_Momentum', pair):
pullback_short = short_pullback_momentum & (dataframe['enter_tag'] == "")
dataframe.loc[pullback_short, 'enter_short'] = 1
dataframe.loc[pullback_short, 'enter_tag'] = 'Bear_Pullback_Momentum'
# 8. SECONDARY CONDITIONS (E2)
if not signal_lock_enabled or not self.is_signal_locked('Bull_E2', pair):
secondary_long = long_condition_secondary & (dataframe['enter_tag'] == "")
dataframe.loc[secondary_long, 'enter_long'] = 1
dataframe.loc[secondary_long, 'enter_tag'] = 'Bull_E2'
if not signal_lock_enabled or not self.is_signal_locked('Bear_E2', pair):
secondary_short = short_condition_secondary & (dataframe['enter_tag'] == "")
dataframe.loc[secondary_short, 'enter_short'] = 1
dataframe.loc[secondary_short, 'enter_tag'] = 'Bear_E2'
# 9. BREAKDOWN CONDITIONS
if not signal_lock_enabled or not self.is_signal_locked('Bull_Breakdown', pair):
breakdown_long = long_condition_breakdown & (dataframe['enter_tag'] == "")
dataframe.loc[breakdown_long, 'enter_long'] = 1
dataframe.loc[breakdown_long, 'enter_tag'] = 'Bull_Breakdown'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Breakdown', pair):
breakdown_short = short_condition_breakdown & (dataframe['enter_tag'] == "")
dataframe.loc[breakdown_short, 'enter_short'] = 1
dataframe.loc[breakdown_short, 'enter_tag'] = 'Bear_Breakdown'
# 10. PURE DIVERGENCE CONDITIONS (Lower Priority)
if not signal_lock_enabled or not self.is_signal_locked('Bull_Div', pair):
div_long = long_condition_div & (dataframe['enter_tag'] == "")
dataframe.loc[div_long, 'enter_long'] = 1
dataframe.loc[div_long, 'enter_tag'] = 'Bull_Div'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Div', pair):
div_short = short_condition_div & (dataframe['enter_tag'] == "")
dataframe.loc[div_short, 'enter_short'] = 1
dataframe.loc[div_short, 'enter_tag'] = 'Bear_Div'
# 11. HIGH QUALITY DIVERGENCE CONDITIONS (Lowest Priority)
if not signal_lock_enabled or not self.is_signal_locked('Bull_Div_HQ', pair):
hqdiv_long = long_condition_hqdiv & (dataframe['enter_tag'] == "")
dataframe.loc[hqdiv_long, 'enter_long'] = 1
dataframe.loc[hqdiv_long, 'enter_tag'] = 'Bull_Div_HQ'
if not signal_lock_enabled or not self.is_signal_locked('Bear_Div_HQ', pair):
hqdiv_short = short_condition_hqdiv & (dataframe['enter_tag'] == "")
dataframe.loc[hqdiv_short, 'enter_short'] = 1
dataframe.loc[hqdiv_short, 'enter_tag'] = 'Bear_Div_HQ'
# 12. RSV1
if not signal_lock_enabled or not self.is_signal_locked('Bull_RSV1', pair):
rsv1_long = long_condition_reversal & (dataframe['enter_tag'] == "")
dataframe.loc[rsv1_long, 'enter_long'] = 1
dataframe.loc[rsv1_long, 'enter_tag'] = 'Bull_RSV1'
if not signal_lock_enabled or not self.is_signal_locked('Bear_RSV1', pair):
rsv1_short = short_condition_hqdiv & (dataframe['enter_tag'] == "")
dataframe.loc[rsv1_short, 'enter_short'] = 1
dataframe.loc[rsv1_short, 'enter_tag'] = 'Bear_RSV1'
# 12. MR1
if not signal_lock_enabled or not self.is_signal_locked('Bull_MR1', pair):
mr1_long = mean_reversion_long & (dataframe['enter_tag'] == "")
dataframe.loc[mr1_long, 'enter_long'] = 1
dataframe.loc[mr1_long, 'enter_tag'] = 'Bull_MR1'
if not signal_lock_enabled or not self.is_signal_locked('Bear_MR1', pair):
mr1_short = mean_reversion_short & (dataframe['enter_tag'] == "")
dataframe.loc[mr1_short, 'enter_short'] = 1
dataframe.loc[mr1_short, 'enter_tag'] = 'Bear_MR1'
# 12. MC1
if not signal_lock_enabled or not self.is_signal_locked('Bull_MC1', pair):
mc1_long = momentum_continuation_long & (dataframe['enter_tag'] == "")
dataframe.loc[mc1_long, 'enter_long'] = 1
dataframe.loc[mc1_long, 'enter_tag'] = 'Bull_MC1'
if not signal_lock_enabled or not self.is_signal_locked('Bear_MC1', pair):
mc1_short = momentum_continuation_short & (dataframe['enter_tag'] == "")
dataframe.loc[mc1_short, 'enter_short'] = 1
dataframe.loc[mc1_short, 'enter_tag'] = 'Bear_MC1'
if not signal_lock_enabled or not self.is_signal_locked('Bull_E7', pair):
seventh_long = long_condition_seventh & (dataframe['enter_tag'] == "")
dataframe.loc[seventh_long, 'enter_long'] = 1
dataframe.loc[seventh_long, 'enter_tag'] = 'Bull_E7'
else:
print(f"Signal LOCKED: Bull_E7 for {pair}")
if not signal_lock_enabled or not self.is_signal_locked('Bear_E7', pair):
seventh_short = short_condition_seventh & (dataframe['enter_tag'] == "")
dataframe.loc[seventh_short, 'enter_short'] = 1
dataframe.loc[seventh_short, 'enter_tag'] = 'Bear_E7'
else:
print(f"Signal LOCKED: Bear_E7 for {pair}")
# 12. SIMPLE BREAKOUT CONDITIONS (New)
if not signal_lock_enabled or not self.is_signal_locked('Bull_Simple_Breakout', pair):
simple_breakout = simple_breakout_long & (dataframe['enter_tag'] == "")
dataframe.loc[simple_breakout, 'enter_long'] = 1
dataframe.loc[simple_breakout, 'enter_tag'] = 'Bull_Simple_Breakout'
else:
print(f"Signal LOCKED: Bull_Simple_Breakout for {pair}")
if not signal_lock_enabled or not self.is_signal_locked('Bear_Simple_Breakdown', pair):
simple_breakdown = simple_breakdown_short & (dataframe['enter_tag'] == "")
dataframe.loc[simple_breakdown, 'enter_short'] = 1
dataframe.loc[simple_breakdown, 'enter_tag'] = 'Bear_Simple_Breakdown'
else:
print(f"Signal LOCKED: Bear_Simple_Breakdown for {pair}")
# === LOGGING ===
if len(dataframe) > 0:
last_row = dataframe.iloc[-1]
if last_row.get('enter_long', 0) == 1 or last_row.get('enter_short', 0) == 1:
logger.info(f"🚀 {metadata['pair']} ENTRY DETECTED!")
logger.info(f" 🏷️ Tag: {last_row.get('enter_tag', '')}")
logger.info(f" 📊 RSI: {last_row.get('rsi', 0):.1f}")
logger.info(f" 💧 Volume Ratio: {last_row.get('volume_ratio', 0):.2f}")
logger.info(f" 🎯 Bull Div Count: {last_row.get('total_bullish_divergences_count', 0)}")
logger.info(f" 🎯 Bear Div Count: {last_row.get('total_bearish_divergences_count', 0)}")
logger.info(f" 📈 Signal Strength: {last_row.get('signal_strength', 0)}")
logger.info(f" 🎯 ADX: {last_row.get('adx', 0):.1f}")
long_signals = (dataframe['enter_long'] == 1).sum()
short_signals = (dataframe['enter_short'] == 1).sum()
print(f" Generated {long_signals} long signals, {short_signals} short signals")
return dataframe
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:
"""
Adaptive leverage based on signal strength
"""
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) > 0:
current_signal_strength = dataframe['signal_strength'].iloc[-1]
# Reduce leverage for weaker signals
if current_signal_strength >= 8:
return self.leverage_value # Full leverage for strong signals
elif current_signal_strength >= 6:
return self.leverage_value * 0.8 # 80% leverage
elif current_signal_strength >= 4:
return self.leverage_value * 0.6 # 60% leverage
else:
return self.leverage_value * 0.4 # 40% leverage for weak signals
except:
pass
return self.leverage_value * 0.5 # Conservative fallback
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, after_fill: bool, **kwargs) -> float:
"""
Enhanced dynamic stoploss with breakeven and trailing functionality
Works seamlessly with your existing custom_exit
"""
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) < 1:
return self.stoploss
# Get market data
last_row = dataframe.iloc[-1]
atr = last_row.get('atr', 0)
current_price = last_row['close']
volatility = last_row.get('volatility', 0.02)
signal_strength = last_row.get('signal_strength', 5)
entry_tag = trade.enter_tag or ""
trade_duration_minutes = (current_time - trade.open_date_utc).total_seconds() / 60
# === BREAKEVEN LOGIC ===
# Move to breakeven once we hit decent profit
breakeven_trigger = 0.025 # 2.5% profit
if any(tag in entry_tag for tag in ['MR1', 'E1', 'E2']):
breakeven_trigger = 0.02 # Earlier breakeven for mean reversion
elif any(tag in entry_tag for tag in ['Trend', 'SR_Bounce']):
breakeven_trigger = 0.035 # Later breakeven for trend trades
# Move to breakeven + small buffer
if current_profit >= breakeven_trigger:
breakeven_stop = 0.003 # 0.3% positive (covers fees)
return stoploss_from_open(breakeven_stop, current_profit, is_short=trade.is_short)
# === DYNAMIC TRAILING LOGIC ===
# Start trailing after higher profit threshold
trailing_trigger = 0.045 # 4.5% profit
if current_profit >= trailing_trigger:
# Entry-specific trailing distances
if any(tag in entry_tag for tag in ['MR1', 'E1', 'E2']):
trailing_distance = 0.015 # Tight trailing for mean reversion
elif any(tag in entry_tag for tag in ['MC1', 'Momentum', 'Breakout']):
trailing_distance = 0.025 # Normal trailing for momentum
elif any(tag in entry_tag for tag in ['Trend', 'SR_Bounce', 'Strong']):
trailing_distance = 0.035 # Wide trailing for trend
else:
trailing_distance = 0.02 # Default trailing
# Adjust for volatility
volatility_adj = min(1.5, max(0.7, volatility / 0.02))
trailing_distance *= volatility_adj
# Adjust for signal strength
if signal_strength >= 8:
trailing_distance *= 1.2 # Wider trailing for strong signals
elif signal_strength <= 4:
trailing_distance *= 0.8 # Tighter trailing for weak signals
return stoploss_from_open(trailing_distance, current_profit, is_short=trade.is_short)
# === INITIAL DYNAMIC STOPLOSS (same as before) ===
if signal_strength >= 8:
atr_multiplier = 3.0
elif signal_strength >= 6:
atr_multiplier = 2.5
elif signal_strength >= 4:
atr_multiplier = 2.0
else:
atr_multiplier = 1.5
# Adjust for volatility
volatility_adjustment = min(1.5, max(0.7, volatility / 0.02))
atr_multiplier *= volatility_adjustment
# Calculate stop distance
if atr > 0:
stop_distance = (atr * atr_multiplier) / current_price
else:
stop_distance = 0.08 + (volatility * 2)
# Entry type adjustments
if any(tag in entry_tag for tag in ['MR1', 'E1', 'E2']):
stop_distance *= 0.8
elif any(tag in entry_tag for tag in ['MC1', 'Momentum', 'Breakout']):
stop_distance *= 1.0
elif any(tag in entry_tag for tag in ['Trend', 'SR_Bounce', 'Strong']):
stop_distance *= 1.2
elif any(tag in entry_tag for tag in ['Div', 'RSV1']):
stop_distance *= 0.9
# Safety limits
min_stop = 0.04
max_stop = 0.15
stop_distance = max(min_stop, min(max_stop, stop_distance))
dynamic_stoploss = -stop_distance
# Time-based tightening for initial stops
if trade_duration_minutes > 60:
time_factor = min(0.2, (trade_duration_minutes - 60) / 300)
dynamic_stoploss *= (1 - time_factor)
# Never worse than original
dynamic_stoploss = max(dynamic_stoploss, self.stoploss)
# Minimal logging
if hasattr(self, '_stoploss_log_counter'):
self._stoploss_log_counter = getattr(self, '_stoploss_log_counter', 0) + 1
else:
self._stoploss_log_counter = 1
if self._stoploss_log_counter % 30 == 0:
if current_profit >= trailing_trigger:
logger.info(f"Trailing {pair}: Profit={current_profit:.3f}, Stop={dynamic_stoploss:.3f}, Tag={entry_tag}")
elif current_profit >= breakeven_trigger:
logger.info(f"Breakeven {pair}: Profit={current_profit:.3f}, Stop={dynamic_stoploss:.3f}, Tag={entry_tag}")
else:
logger.info(f"Dynamic SL {pair}: Signal={signal_strength}, Stop={dynamic_stoploss:.3f}, Tag={entry_tag}")
return dynamic_stoploss
except Exception as e:
logger.error(f"Error in custom_stoploss for {pair}: {e}")
return self.stoploss
def initialize_divergences_lists(self, dataframe: DataFrame):
"""Initialize divergence tracking columns"""
# Bullish Divergences
dataframe["total_bullish_divergences"] = np.nan
dataframe["total_bullish_divergences_count"] = 0
dataframe["total_bullish_divergences_names"] = ''
# Bearish Divergences
dataframe["total_bearish_divergences"] = np.nan
dataframe["total_bearish_divergences_count"] = 0
dataframe["total_bearish_divergences_names"] = ''
def get_iterators(self, dataframe):
"""Get pivot point iterators for divergence detection"""
low_iterator = []
high_iterator = []
for index, row in enumerate(dataframe.itertuples(index=True, name='Pandas')):
if np.isnan(row.pivot_lows):
low_iterator.append(0 if len(low_iterator) == 0 else low_iterator[-1])
else:
low_iterator.append(index)
if np.isnan(row.pivot_highs):
high_iterator.append(0 if len(high_iterator) == 0 else high_iterator[-1])
else:
high_iterator.append(index)
return high_iterator, low_iterator
def add_divergences(self, dataframe: DataFrame, indicator: str, high_iterator, low_iterator):
"""Add divergence detection for a specific indicator"""
(bearish_divergences, bearish_lines, bullish_divergences, bullish_lines) = self.divergence_finder_dataframe(
dataframe, indicator, high_iterator, low_iterator)
dataframe['bearish_divergence_' + indicator + '_occurence'] = bearish_divergences
dataframe['bullish_divergence_' + indicator + '_occurence'] = bullish_divergences
def divergence_finder_dataframe(self, dataframe: DataFrame, indicator_source: str, high_iterator, low_iterator) -> Tuple[pd.Series, pd.Series]:
"""Enhanced divergence finder with improved logic"""
bearish_lines = [np.empty(len(dataframe['close'])) * np.nan]
bearish_divergences = np.empty(len(dataframe['close'])) * np.nan
bullish_lines = [np.empty(len(dataframe['close'])) * np.nan]
bullish_divergences = np.empty(len(dataframe['close'])) * np.nan
for index, row in enumerate(dataframe.itertuples(index=True, name='Pandas')):
# Bearish divergence detection
bearish_occurence = self.bearish_divergence_finder(
dataframe, dataframe[indicator_source], high_iterator, index)
if bearish_occurence is not None:
(prev_pivot, current_pivot) = bearish_occurence
bearish_prev_pivot = dataframe['close'][prev_pivot]
bearish_current_pivot = dataframe['close'][current_pivot]
bearish_ind_prev_pivot = dataframe[indicator_source][prev_pivot]
bearish_ind_current_pivot = dataframe[indicator_source][current_pivot]
# Enhanced validation for bearish divergence
price_diff = abs(bearish_current_pivot - bearish_prev_pivot)
indicator_diff = abs(bearish_ind_current_pivot - bearish_ind_prev_pivot)
time_diff = current_pivot - prev_pivot
# Only accept divergences with sufficient magnitude and time separation
if (price_diff > dataframe['atr'][current_pivot] * 0.5 and
indicator_diff > 5 and
time_diff >= 5):
bearish_divergences[index] = row.close
dataframe.loc[index, "total_bearish_divergences"] = row.close
dataframe.loc[index, "total_bearish_divergences_count"] += 1
dataframe.loc[index, "total_bearish_divergences_names"] += indicator_source.upper() + '
'
# Bullish divergence detection
bullish_occurence = self.bullish_divergence_finder(
dataframe, dataframe[indicator_source], low_iterator, index)
if bullish_occurence is not None:
(prev_pivot, current_pivot) = bullish_occurence
bullish_prev_pivot = dataframe['close'][prev_pivot]
bullish_current_pivot = dataframe['close'][current_pivot]
bullish_ind_prev_pivot = dataframe[indicator_source][prev_pivot]
bullish_ind_current_pivot = dataframe[indicator_source][current_pivot]
# Enhanced validation for bullish divergence
price_diff = abs(bullish_current_pivot - bullish_prev_pivot)
indicator_diff = abs(bullish_ind_current_pivot - bullish_ind_prev_pivot)
time_diff = current_pivot - prev_pivot
# Only accept divergences with sufficient magnitude and time separation
if (price_diff > dataframe['atr'][current_pivot] * 0.5 and
indicator_diff > 5 and
time_diff >= 5):
bullish_divergences[index] = row.close
dataframe.loc[index, "total_bullish_divergences"] = row.close
# CORRECT - increment BULLISH counters for bullish divergence:
dataframe.loc[index, "total_bullish_divergences_count"] += 1
dataframe.loc[index, "total_bullish_divergences_names"] += indicator_source.upper() + '
'
return (bearish_divergences, bearish_lines, bullish_divergences, bullish_lines)
def bearish_divergence_finder(self, dataframe, indicator, high_iterator, index):
"""Enhanced bearish divergence detection"""
try:
if high_iterator[index] == index:
current_pivot = high_iterator[index]
occurences = list(dict.fromkeys(high_iterator))
current_index = occurences.index(high_iterator[index])
for i in range(current_index-1, current_index - self.window.value - 1, -1):
if i < 0 or i >= len(occurences):
continue
prev_pivot = occurences[i]
if np.isnan(prev_pivot):
continue
# Enhanced divergence validation
price_higher = dataframe['pivot_highs'][current_pivot] > dataframe['pivot_highs'][prev_pivot]
indicator_lower = indicator[current_pivot] < indicator[prev_pivot]
price_lower = dataframe['pivot_highs'][current_pivot] < dataframe['pivot_highs'][prev_pivot]
indicator_higher = indicator[current_pivot] > indicator[prev_pivot]
# Check for classic or hidden divergence
if (price_higher and indicator_lower) or (price_lower and indicator_higher):
# Additional validation: check trend consistency
if self.validate_divergence_trend(dataframe, prev_pivot, current_pivot, 'bearish'):
return (prev_pivot, current_pivot)
except:
pass
return None
def bullish_divergence_finder(self, dataframe, indicator, low_iterator, index):
"""Enhanced bullish divergence detection"""
try:
if low_iterator[index] == index:
current_pivot = low_iterator[index]
occurences = list(dict.fromkeys(low_iterator))
current_index = occurences.index(low_iterator[index])
for i in range(current_index-1, current_index - self.window.value - 1, -1):
if i < 0 or i >= len(occurences):
continue
prev_pivot = occurences[i]
if np.isnan(prev_pivot):
continue
# Enhanced divergence validation
price_lower = dataframe['pivot_lows'][current_pivot] < dataframe['pivot_lows'][prev_pivot]
indicator_higher = indicator[current_pivot] > indicator[prev_pivot]
price_higher = dataframe['pivot_lows'][current_pivot] > dataframe['pivot_lows'][prev_pivot]
indicator_lower = indicator[current_pivot] < indicator[prev_pivot]
# Check for classic or hidden divergence
if (price_lower and indicator_higher) or (price_higher and indicator_lower):
# Additional validation: check trend consistency
if self.validate_divergence_trend(dataframe, prev_pivot, current_pivot, 'bullish'):
return (prev_pivot, current_pivot)
except:
pass
return None
def validate_divergence_trend(self, dataframe, prev_pivot, current_pivot, divergence_type):
"""Validate divergence by checking intermediate trend"""
try:
# Check if there's a clear trend between pivots
mid_point = (prev_pivot + current_pivot) // 2
if divergence_type == 'bearish':
# For bearish divergence, expect uptrend in between
return dataframe['ema20'][mid_point] > dataframe['ema20'][prev_pivot]
else:
# For bullish divergence, expect downtrend in between
return dataframe['ema20'][mid_point] < dataframe['ema20'][prev_pivot]
except:
return True # Default to accepting divergence if validation fails
@property
def protections(self):
"""Enhanced protection configuration"""
prot = []
if self.use_cooldown_protection.value:
prot.append({
"method": "CooldownPeriod",
"stop_duration_candles": self.cooldown_lookback.value
})
if self.use_max_drawdown_protection.value:
prot.append({
"method": "MaxDrawdown",
"lookback_period_candles": self.max_drawdown_lookback.value,
"trade_limit": self.max_drawdown_trade_limit.value,
"stop_duration_candles": self.max_drawdown_stop_duration.value,
"max_allowed_drawdown": self.max_allowed_drawdown.value
})
if self.use_stop_protection.value:
prot.append({
"method": "StoplossGuard",
"lookback_period_candles": self.stoploss_guard_lookback.value,
"trade_limit": self.stoploss_guard_trade_limit.value,
"stop_duration_candles": self.stop_duration.value,
"only_per_pair": self.stoploss_guard_only_per_pair.value,
})
return prot
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Initialize
dataframe['exit_long'] = 0
dataframe['exit_short'] = 0
dataframe['exit_tag'] = ''
# === YOUR OTHER EXIT CONDITIONS FIRST ===
# (Add any other exit conditions you have here)
# any_long_exit = (some_other_condition)
# any_short_exit = (some_other_condition)
# === EXIT ON OPPOSITE SIGNALS (your previous approach) ===
if 'enter_long' in dataframe.columns and 'enter_short' in dataframe.columns:
# Exit longs on short signals
reversal_long_exit = (dataframe['enter_short'] == 1)
dataframe.loc[reversal_long_exit, 'exit_long'] = 1
dataframe.loc[reversal_long_exit, 'exit_tag'] = 'Reversal_Short_Signal'
# Exit shorts on long signals
if self.can_short:
reversal_short_exit = (dataframe['enter_long'] == 1)
dataframe.loc[reversal_short_exit, 'exit_short'] = 1
dataframe.loc[reversal_short_exit, 'exit_tag'] = 'Reversal_Long_Signal'
return dataframe
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, **kwargs):
"""
Simple and effective custom exit - just take profits!
15m timeframe optimized - triggers exits reliably
"""
from logging import getLogger
logger = getLogger(__name__)
# Get entry tag and trade duration
entry_tag = getattr(trade, 'enter_tag', '')
trade_duration_minutes = (current_time - trade.open_date_utc).total_seconds() / 60
# === GET BASIC MARKET DATA FOR SAFETY EXITS ===
rsi = 50
momentum_score = 0
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if not dataframe.empty:
last_row = dataframe.iloc[-1]
if 'rsi' in dataframe.columns:
rsi = last_row['rsi']
# Simple momentum calculation
if len(dataframe) >= 5:
momentum_score = (last_row['close'] - dataframe.iloc[-5]['close']) / dataframe.iloc[-5]['close']
except Exception:
pass
# === ENHANCED LOG AND EXIT WITH SIGNAL TRACKING ===
def log_and_exit(reason: str):
# UPDATE SIGNAL PERFORMANCE TRACKING
if hasattr(trade, 'enter_tag') and trade.enter_tag:
try:
profit_ratio = trade.calc_profit_ratio(current_rate)
self.update_signal_performance(trade.enter_tag, pair, profit_ratio)
logger.debug(f"Updated signal performance: {trade.enter_tag} -> {profit_ratio:.4f}")
except Exception as e:
logger.error(f"Failed to update signal performance: {e}")
# LOG EXIT DETAILS
logger.info(f"🔤 {pair} EXIT: {reason}")
logger.info(f" 🕐 Time Held: {int(trade_duration_minutes)} min")
logger.info(f" 💰 Profit: {current_profit * 100:.2f}%")
logger.info(f" 📊 RSI: {rsi:.1f}")
logger.info(f" 📈 Momentum: {momentum_score * 100:.2f}%")
logger.info(f" 🏷️ Entry Tag: {entry_tag}")
logger.info(f" 🔄 Exit Trigger: {reason}")
return reason
# === SIMPLE PROFIT TAKING - NO COMPLEX LOGIC ===
# === SIMPLE PROFIT TAKING - NO COMPLEX LOGIC ===
# Immediate exits for big profits
if current_profit >= 0.12: # 12%
return log_and_exit("profit_12pct")
elif current_profit >= 0.10: # 10%
return log_and_exit("profit_10pct")
elif current_profit >= 0.08: # 8%
return log_and_exit("profit_8pct")
elif current_profit >= 0.06: # 6%
return log_and_exit("profit_6pct")
elif current_profit >= 0.05: # 5%
return log_and_exit("profit_5pct")
elif current_profit >= 0.04: # 4%
return log_and_exit("profit_4pct")
# Time-based exits - COVERS ALL TRADES UNDER 4% TOO
elif current_profit >= 0.035 and trade_duration_minutes >= 15: # 3.5% after 15min
return log_and_exit("profit_35pct_15min")
elif current_profit >= 0.03 and trade_duration_minutes >= 30: # 3% after 30min
return log_and_exit("profit_3pct_30min")
elif current_profit >= 0.025 and trade_duration_minutes >= 45: # 2.5% after 45min
return log_and_exit("profit_25pct_45min")
elif current_profit >= 0.02 and trade_duration_minutes >= 60: # 2% after 1hr
return log_and_exit("profit_2pct_1hr")
elif current_profit >= 0.015 and trade_duration_minutes >= 90: # 1.5% after 1.5hr
return log_and_exit("profit_15pct_90min")
elif current_profit >= 0.01 and trade_duration_minutes >= 120: # 1% after 2hr
return log_and_exit("profit_1pct_2hr")
elif current_profit >= 0.008 and trade_duration_minutes >= 180: # 0.8% after 3hr
return log_and_exit("profit_08pct_3hr")
elif current_profit >= 0.005 and trade_duration_minutes >= 240: # 0.5% after 4hr
return log_and_exit("force_exit_4hr")
# === GET BASIC MARKET DATA FOR SAFETY EXITS ===
rsi = 50
momentum_score = 0
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if not dataframe.empty:
last_row = dataframe.iloc[-1]
if 'rsi' in dataframe.columns:
rsi = last_row['rsi']
# Simple momentum calculation
if len(dataframe) >= 5:
momentum_score = (last_row['close'] - dataframe.iloc[-5]['close']) / dataframe.iloc[-5]['close']
except Exception:
pass
# === ENHANCED LOG AND EXIT WITH SIGNAL TRACKING ===
def log_and_exit(reason: str):
# UPDATE SIGNAL PERFORMANCE TRACKING
if hasattr(trade, 'enter_tag') and trade.enter_tag:
try:
profit_ratio = trade.calc_profit_ratio(current_rate)
self.update_signal_performance(trade.enter_tag, pair, profit_ratio)
logger.debug(f"Updated signal performance: {trade.enter_tag} -> {profit_ratio:.4f}")
except Exception as e:
logger.error(f"Failed to update signal performance: {e}")
# LOG EXIT DETAILS
logger.info(f"🔤 {pair} EXIT: {reason}")
logger.info(f" 🕐 Time Held: {int(trade_duration_minutes)} min")
logger.info(f" 💰 Profit: {current_profit * 100:.2f}%")
logger.info(f" 📊 RSI: {rsi:.1f}")
logger.info(f" 📈 Momentum: {momentum_score * 100:.2f}%")
logger.info(f" 🏷️ Entry Tag: {entry_tag}")
logger.info(f" 🔄 Exit Trigger: {reason}")
return reason
# === SIMPLE RSI EXTREME EXITS ===
if current_profit >= 0.02: # Only if we have some profit
if not trade.is_short and rsi >= 80: # Very overbought
return log_and_exit("rsi_extreme_80")
elif trade.is_short and rsi <= 20: # Very oversold
return log_and_exit("rsi_extreme_20")
# === SIMPLE RSI WARNING EXITS ===
if current_profit >= 0.015: # Only if we have decent profit
if not trade.is_short and rsi >= 75: # Overbought
return log_and_exit("rsi_overbought_75")
elif trade.is_short and rsi <= 25: # Oversold
return log_and_exit("rsi_oversold_25")
# === EMERGENCY EXITS FOR HUGE PROFITS ===
if current_profit >= 0.30: # 30% - definitely exit!
return log_and_exit("emergency_30pct")
elif current_profit >= 0.25: # 25% - definitely exit!
return log_and_exit("emergency_25pct")
elif current_profit >= 0.20: # 20% - definitely exit!
return log_and_exit("emergency_20pct")
elif current_profit >= 0.15: # 15% - definitely exit!
return log_and_exit("emergency_15pct")
# === REVERSAL SIGNAL EXITS ===
try:
if dataframe is not None and not dataframe.empty:
last_row = dataframe.iloc[-1]
# Exit on opposite signals if we have any profit
if current_profit >= 0.005: # 0.5% minimum
if not trade.is_short and last_row.get('enter_short', 0) == 1:
return log_and_exit("reversal_short_signal")
elif trade.is_short and last_row.get('enter_long', 0) == 1:
return log_and_exit("reversal_long_signal")
except Exception:
pass
# === WEEKEND SAFETY EXIT ===
if current_time.weekday() == 4 and current_time.hour >= 20: # Friday evening
if current_profit >= 0.01: # 1% minimum
return log_and_exit("friday_close")
# === FINAL SAFETY - EXIT ANYTHING AFTER 6 HOURS ===
if trade_duration_minutes >= 360: # 6 hours
if current_profit >= 0.003: # Even 0.3% profit
return log_and_exit("final_safety_6hr")
return None
def maybe_optimize_coin(self, pair: str, force_startup: bool = False):
"""ENHANCED optimization trigger with better permanent optimization"""
# ADDED: Komplette neue Methode für Optimierungslogik
if not self.optuna_manager:
logger.debug(f"🚫 [OPTUNA] OptunaManager not available for {pair}")
return
# ENHANCED: Force startup optimization or intelligent check
if force_startup:
logger.info(f"🚀 [OPTUNA] FORCED STARTUP OPTIMIZATION for {pair}")
should_optimize = True
else:
should_optimize = self.optuna_manager.should_optimize(pair)
if not should_optimize:
return
logger.info(f"🔄 [OPTUNA] Checking optimization conditions for {pair}")
try:
trades_count = self.get_coin_trades_count(pair)
optimization_count = self.optuna_manager.optimization_trigger_count.get(pair, 0)
logger.info(f"📊 [OPTUNA] {pair} has {trades_count} trades, {optimization_count} optimizations done")
# ENHANCED: More flexible trade requirements
min_trades_required = max(0, optimization_count * 5) # Require more trades for subsequent optimizations
if not force_startup and trades_count < min_trades_required:
logger.info(f"⏳ [OPTUNA] Not enough trades for {pair} optimization ({trades_count}/{min_trades_required})")
return
# Track optimization type
if force_startup:
opt_type = "STARTUP"
elif self.optuna_manager.should_optimize_based_on_performance(pair):
opt_type = "PERFORMANCE-TRIGGERED"
else:
opt_type = "PERIODIC"
logger.info(f"✨ [OPTUNA] {opt_type} OPTIMIZATION for {pair} (trades: {trades_count})")
objective_func = self.create_objective_function(pair)
# ENHANCED: More trials for performance-triggered optimizations
n_trials = 15 if opt_type == "PERFORMANCE-TRIGGERED" else 10
# Start optimization
self.optuna_manager.optimize_coin(pair, objective_func, n_trials=n_trials)
# Track optimization count
self.optuna_manager.optimization_trigger_count[pair] = optimization_count + 1
logger.info(f"✅ [OPTUNA] Completed {opt_type} optimization for {pair}")
except Exception as e:
logger.error(f"❌ [OPTUNA] Optimization failed for {pair}: {e}")
import traceback
logger.debug(f"🔍 [OPTUNA] Full traceback: {traceback.format_exc()}")
def get_coin_trades_count(self, pair: str) -> int:
"""Get number of trades for specific coin"""
# ADDED: Hilfsmethode für Trade-Anzahl pro Coin
try:
from freqtrade.persistence import Trade
trades = Trade.get_trades_proxy(pair=pair)
count = len(trades) if trades else 0
logger.debug(f"📊 [OPTUNA] Trade count for {pair}: {count}")
return count
except Exception as e:
logger.error(f"❌ [OPTUNA] Failed to get trade count for {pair}: {e}")
return 0
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:
"""Track trade exits and update performance - works in backtesting"""
try:
profit_ratio = trade.calc_profit_ratio(rate)
# Update Optuna performance if available
if self.optuna_manager:
self.optuna_manager.update_performance(pair, profit_ratio)
logger.debug(f"📈 [OPTUNA] Updated performance for {pair}: {profit_ratio:.4f}")
if pair not in self.coin_performance:
self.coin_performance[pair] = 0.0
self.coin_performance[pair] += profit_ratio
# ADD THIS: Update signal performance tracking for backtesting
enter_tag = getattr(trade, 'enter_tag', None)
if enter_tag:
self.update_signal_performance(enter_tag, pair, profit_ratio)
# Debug output
result_type = "WIN" if profit_ratio > 0 else "LOSS"
print(f"📊 Signal Performance Tracked: {pair} {enter_tag} - {result_type}")
print(f" Profit: {profit_ratio:.4f} ({profit_ratio*100:.2f}%)")
print(f" Exit reason: {exit_reason}")
# Log significant performance updates
if abs(profit_ratio) > 0.02: # More than 2% gain/loss
logger.info(f"💰 [OPTUNA] Significant trade result for {pair}: {profit_ratio:.2%} (cumulative: {self.coin_performance[pair]:.2%})")
except Exception as e:
logger.error(f"⛔ [OPTUNA] Failed to update performance for {pair}: {e}")
return True
def on_trade_close(self, trade: Trade, **kwargs) -> None:
"""Called when a trade is closed - captures ALL exits including stoploss"""
try:
entry_tag = getattr(trade, 'enter_tag', '')
if entry_tag: # Only track if we have an entry tag
pair = trade.pair
profit_ratio = trade.calc_profit_ratio()
# Update signal performance for ALL exits (wins and losses)
self.update_signal_performance(entry_tag, pair, profit_ratio)
# Log the trade result
result_type = "WIN" if profit_ratio > 0 else "LOSS"
exit_reason = getattr(trade, 'exit_reason', 'unknown')
print(f"Trade closed: {pair} {entry_tag} - {result_type}")
print(f" Profit: {profit_ratio:.4f} ({profit_ratio*100:.2f}%)")
print(f" Exit reason: {exit_reason}")
except Exception as e:
print(f"Error in on_trade_close: {e}")
def choppiness_index(high, low, close, window=14):
"""Calculate Choppiness Index"""
natr = pd.Series(ta.NATR(high, low, close, window=window))
high_max = high.rolling(window=window).max()
low_min = low.rolling(window=window).min()
choppiness = 100 * np.log10((natr.rolling(window=window).sum()) / (high_max - low_min)) / np.log10(window)
return choppiness
def resample(indicator):
"""Resample function for compatibility"""
return indicator
def two_bands_check_long(dataframe):
"""Allow long when price is near/at lower band (oversold area)"""
return (
(dataframe['low'] <= dataframe['kc_lowerband']) |
(dataframe['close'] <= dataframe['kc_lowerband'])
)
def two_bands_check_short(dataframe):
"""Allow short when price is near/at upper band (overbought area)"""
return (
(dataframe['high'] >= dataframe['kc_upperband']) |
(dataframe['close'] >= dataframe['kc_upperband'])
)
def green_candle(dataframe):
"""Check for green candle"""
return dataframe[resample('open')] < dataframe[resample('close')]
def red_candle(dataframe):
"""Check for red candle"""
return dataframe[resample('open')] > dataframe[resample('close')]
def pivot_points(dataframe: DataFrame, window: int = 5, pivot_source=None) -> DataFrame:
"""Enhanced pivot point detection"""
from enum import Enum
class PivotSource(Enum):
HighLow = 0
Close = 1
if pivot_source is None:
pivot_source = PivotSource.Close
high_source = 'close' if pivot_source == PivotSource.Close else 'high'
low_source = 'close' if pivot_source == PivotSource.Close else 'low'
pivot_points_lows = np.empty(len(dataframe['close'])) * np.nan
pivot_points_highs = np.empty(len(dataframe['close'])) * np.nan
last_values = deque()
# Find pivot points with enhanced validation
for index, row in enumerate(dataframe.itertuples(index=True, name='Pandas')):
last_values.append(row)
if len(last_values) >= window * 2 + 1:
current_value = last_values[window]
is_greater = True
is_less = True
for window_index in range(0, window):
left = last_values[window_index]
right = last_values[2 * window - window_index]
local_is_greater, local_is_less = check_if_pivot_is_greater_or_less(
current_value, high_source, low_source, left, right)
is_greater &= local_is_greater
is_less &= local_is_less
# Additional validation: ensure pivot is significant
if is_greater:
current_high = getattr(current_value, high_source)
# Check if high is significant enough (above ATR threshold)
if hasattr(current_value, 'atr') and current_high > 0:
pivot_points_highs[index - window] = current_high
if is_less:
current_low = getattr(current_value, low_source)
# Check if low is significant enough
if hasattr(current_value, 'atr') and current_low > 0:
pivot_points_lows[index - window] = current_low
last_values.popleft()
return pd.DataFrame(index=dataframe.index, data={
'pivot_lows': pivot_points_lows,
'pivot_highs': pivot_points_highs
})
def check_if_pivot_is_greater_or_less(current_value, high_source: str, low_source: str, left, right) -> Tuple[bool, bool]:
"""Helper function for pivot point validation"""
is_greater = True
is_less = True
if (getattr(current_value, high_source) <= getattr(left, high_source) or
getattr(current_value, high_source) <= getattr(right, high_source)):
is_greater = False
if (getattr(current_value, low_source) >= getattr(left, low_source) or
getattr(current_value, low_source) >= getattr(right, low_source)):
is_less = False
return (is_greater, is_less)
def emaKeltner(dataframe):
"""Calculate EMA-based Keltner Channels"""
keltner = {}
atr = qtpylib.atr(dataframe, window=10)
ema20 = ta.EMA(dataframe, timeperiod=20)
keltner['upper'] = ema20 + atr
keltner['mid'] = ema20
keltner['lower'] = ema20 - atr
return keltner
def chaikin_money_flow(dataframe, n=20, fillna=False) -> Series:
"""Calculate Chaikin Money Flow indicator"""
df = dataframe.copy()
mfv = ((df['close'] - df['low']) - (df['high'] - df['close'])) / (df['high'] - df['low'])
mfv = mfv.fillna(0.0)
mfv *= df['volume']
cmf = (mfv.rolling(n, min_periods=0).sum() / df['volume'].rolling(n, min_periods=0).sum())
if fillna:
cmf = cmf.replace([np.inf, -np.inf], np.nan).fillna(0)
return Series(cmf, name='cmf')