# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
"""
ImprovedSimpleStrategy — Multi-signal entry with adaptive risk management.
Key improvements over SimpleStrategy:
1. THREE entry signals instead of one — catches more opportunities:
- Signal A: RSI oversold bounce (original, for pullbacks)
- Signal B: EMA50 pullback bounce (for dips in uptrends)
- Signal C: Momentum breakout (for trend continuation)
2. Dynamic stoploss based on ATR — adapts to volatility
3. Tighter base stoploss (-4%) — cuts losers faster
4. Earlier partial exits (+3% and +7%) — locks in profits sooner
5. Market regime filter — avoids entries in choppy markets
Why this works better:
- SimpleStrategy was idle 85% of the time (RSI crossover too restrictive)
- Multiple signals increase frequency while maintaining quality
- ATR-based stops prevent getting stopped out by normal volatility
- Earlier partial exits improve the win rate by locking in gains faster
"""
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative,
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
AnnotationType,
)
import talib.abstract as ta
from technical import qtpylib
class ImprovedSimpleStrategy(IStrategy):
"""
Multi-regime strategy with 3 entry signals, dynamic stoploss, and partial exits.
"""
INTERFACE_VERSION = 3
timeframe = "5m"
can_short: bool = False
# No ROI table — use trailing stop + exit signals + partial exits
minimal_roi = {}
# Tighter base stoploss
stoploss = -0.04
# Trailing stop: kicks in at +4%, trails at 2.5%
trailing_stop = True
trailing_only_offset_is_reached = True
trailing_stop_positive = 0.025
trailing_stop_positive_offset = 0.04
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# Enable partial exits
position_adjustment_enable = True
max_entry_position_adjustment = 2
startup_candle_count: int = 200
# ===================== HYPEROPT PARAMETERS =====================
# Signal A: RSI oversold bounce
buy_rsi_max = IntParameter(25, 45, default=35, space="buy", optimize=True)
buy_rsi_recovery = IntParameter(30, 50, default=40, space="buy", optimize=True)
# Signal B: EMA pullback
buy_pullback_rsi = IntParameter(30, 55, default=45, space="buy", optimize=True)
# Signal C: Momentum
buy_momentum_adx = IntParameter(15, 35, default=20, space="buy", optimize=True)
# Exit parameters
sell_rsi_min = IntParameter(65, 85, default=75, space="sell", optimize=True)
# Volume filter
buy_volume_factor = DecimalParameter(1.0, 2.0, default=1.1, space="buy", optimize=True)
# ===================== ORDER TYPES =====================
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "limit",
"stoploss_on_exchange": False,
}
order_time_in_force = {
"entry": "GTC",
"exit": "GTC",
}
# ===================== INDICATORS =====================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# --- Trend ---
dataframe["ema20"] = ta.EMA(dataframe, timeperiod=20)
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
# --- Momentum ---
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["adx"] = ta.ADX(dataframe)
dataframe["mfi"] = ta.MFI(dataframe)
# Stochastic
stoch_fast = ta.STOCHF(dataframe)
dataframe["fastd"] = stoch_fast["fastd"]
dataframe["fastk"] = stoch_fast["fastk"]
# MACD
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
# --- Volatility ---
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
# Bollinger Bands
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=20, stds=2
)
dataframe["bb_lowerband"] = bollinger["lower"]
dataframe["bb_middleband"] = bollinger["mid"]
dataframe["bb_upperband"] = bollinger["upper"]
dataframe["bb_percent"] = (
(dataframe["close"] - dataframe["bb_lowerband"])
/ (dataframe["bb_upperband"] - dataframe["bb_lowerband"])
)
dataframe["bb_width"] = (
(dataframe["bb_upperband"] - dataframe["bb_lowerband"])
/ dataframe["bb_middleband"]
)
# --- Volume ---
dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
dataframe["volume_ratio"] = dataframe["volume"] / dataframe["volume_mean_20"]
# --- Pullback detection ---
# Price pulled back to EMA20 area (within 2%)
dataframe["near_ema20"] = (
(dataframe["close"] - dataframe["ema20"]).abs() / dataframe["ema20"] < 0.02
)
# Price pulled back to EMA50 area (within 3%)
dataframe["near_ema50"] = (
(dataframe["close"] - dataframe["ema50"]).abs() / dataframe["ema50"] < 0.03
)
# --- RSI trend (is RSI recovering?) ---
dataframe["rsi_rising"] = dataframe["rsi"] > dataframe["rsi"].shift(1)
dataframe["rsi_was_oversold"] = dataframe["rsi"].shift(1) < self.buy_rsi_max.value
# --- MACD histogram trend ---
dataframe["macdhist_rising"] = (
dataframe["macdhist"] > dataframe["macdhist"].shift(1)
)
return dataframe
# ===================== ENTRY SIGNALS =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Common filters for all signals
in_uptrend = dataframe["close"] > dataframe["ema200"]
good_volume = (
dataframe["volume"] > dataframe["volume_mean_20"] * self.buy_volume_factor.value
)
not_overbought = dataframe["bb_percent"] < 0.8
not_too_volatile = dataframe["bb_width"] < 0.15 # Avoid extreme volatility
# --- SIGNAL A: RSI Oversold Bounce (original, improved) ---
# Instead of requiring RSI crossover above a threshold,
# we look for RSI recovering from oversold territory.
# This catches MORE signals than the crossover approach.
signal_a = (
# RSI was oversold last candle
dataframe["rsi_was_oversold"]
# RSI is now recovering (rising)
& dataframe["rsi_rising"]
# RSI is above the oversold zone (confirmed bounce)
& (dataframe["rsi"] > self.buy_rsi_recovery.value)
& in_uptrend
& good_volume
& not_overbought
)
# --- SIGNAL B: EMA Pullback Bounce ---
# Price dips to EMA50 in an uptrend, then bounces.
# Much more common than RSI < 30 crossover.
signal_b = (
# Price near EMA50 (pulled back)
dataframe["near_ema50"]
# RSI not overbought (room to run)
& (dataframe["rsi"] < self.buy_pullback_rsi.value)
# RSI starting to recover
& dataframe["rsi_rising"]
# MACD histogram turning positive (momentum shifting up)
& dataframe["macdhist_rising"]
& in_uptrend
& good_volume
& not_overbought
)
# --- SIGNAL C: Momentum Breakout ---
# ADX confirms trend strength, MACD crossover confirms momentum.
signal_c = (
# ADX > threshold (trend is strong)
(dataframe["adx"] > self.buy_momentum_adx.value)
# MACD crossed above signal
& (qtpylib.crossed_above(dataframe["macd"], dataframe["macdsignal"]))
# Price above EMA50 (in uptrend)
& (dataframe["close"] > dataframe["ema50"])
# RSI not overbought
& (dataframe["rsi"] < 70)
& in_uptrend
& good_volume
& not_too_volatile
)
# Combine signals
dataframe.loc[signal_a, "enter_long"] = 1
dataframe.loc[signal_a, "enter_tag"] = "RSI_BOUNCE"
dataframe.loc[signal_b, "enter_long"] = 1
dataframe.loc[signal_b, "enter_tag"] = "EMA50_PULLBACK"
dataframe.loc[signal_c, "enter_long"] = 1
dataframe.loc[signal_c, "enter_tag"] = "MOMENTUM"
return dataframe
# ===================== EXIT SIGNALS =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
# RSI overbought
(dataframe["rsi"] > self.sell_rsi_min.value)
# OR price dropped below EMA20 (trend weakening)
| (dataframe["close"] < dataframe["ema20"])
# With volume confirmation
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1
return dataframe
# ===================== CUSTOM STOPLOSS =====================
def custom_stoploss(self, pair: str, trade: "Trade", current_profit: float,
current_time: "datetime", **kwargs) -> float:
"""
Dynamic stoploss based on ATR and profit level.
- Before breakeven: Use ATR-based stop (tighter in volatile markets)
- After +3% profit: Lock in gains, use trailing
- After +8% profit: Very tight trailing to protect gains
"""
if current_profit >= 0.08:
# Very tight trailing after big gains
return 0.01 # 1% trail
elif current_profit >= 0.04:
# Normal trailing after moderate gains
return 0.02 # 2% trail
elif current_profit >= 0.0:
# Breakeven protection
return 0.0 # Exit at breakeven
else:
# Let the base stoploss handle it
return None
# ===================== PARTIAL EXITS =====================
def adjust_trade_position(self, trade: Trade, current_profit: float, **kwargs) -> Optional[Union[float, Tuple[float, str]]]:
"""
Multi-tier partial exits:
- Tier 1: Sell 30% at +3% profit (lock in gains early)
- Tier 2: Sell 30% at +7% profit (lock more gains)
- Remaining 40% rides the trailing stop
"""
if trade.is_short:
return None
# Tier 1: 30% exit at +3%
if current_profit >= 0.03 and trade.nr_of_successful_exits == 0:
return (-0.30, "TP1: 30% exit at 3%+ profit")
# Tier 2: 30% exit at +7%
if current_profit >= 0.07 and trade.nr_of_successful_exits == 1:
return (-0.30, "TP2: 30% exit at 7%+ profit")
return None
# ===================== TRADE ANNOTATION =====================
def annotate(self, trade: "Trade", df: DataFrame, **kwargs) -> Tuple[AnnotationType, str]:
"""Annotate trades for better debugging/analysis."""
if trade.is_open:
profit_ratio = trade.calc_profit_ratio()
if profit_ratio >= 0.05:
return AnnotationType.HIGHLIGHT, f"🟢 {profit_ratio:.1%} profit"
elif profit_ratio <= -0.02:
return AnnotationType.WARNING, f"🔴 {profit_ratio:.1%} loss"
return AnnotationType.NONE, ""