Improved RSI-based strategy with multiple enhancements:
Timeframe
5m
Direction
Long Only
Stoploss
-24.3%
Trailing Stop
Yes
ROI
0m: 22.5%, 39m: 3.7%, 73m: 2.2%, 110m: 0.0%
Interface Version
3
Startup Candles
N/A
Indicators
7
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
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, # @informative decorator
# 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,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
from technical import qtpylib
class ImprovedStrategyV2(IStrategy):
"""
Improved RSI-based strategy with multiple enhancements:
1. Trend Filter: Only trade in direction of 200-period EMA
2. Trailing Stop Loss: Protect profits with dynamic trailing stop
3. MACD Confirmation: Require MACD bullish/bearish alignment
4. Bollinger Bands: Use BB for volatility-based entries/exits
5. Multi-Indicator Confirmation: Combine RSI, MACD, and BB signals
This strategy aims to improve upon the basic RSI strategy by:
- Reducing false signals with trend filter
- Protecting profits with trailing stop
- Adding confirmation from multiple indicators
- Being hyperoptable for parameter optimization
"""
# Strategy interface version
INTERFACE_VERSION = 3
# Can this strategy go short?
can_short: bool = False
# Minimal ROI designed for the strategy.
minimal_roi = {
"0": 0.225, # 22.5% immediate
"39": 0.037, # 3.7% after 39 minutes
"73": 0.022, # 2.2% after 73 minutes
"110": 0, # 0% after 110 minutes
}
# Optimal stoploss designed for the strategy.
stoploss = -0.243 # Hyperopt optimized stop loss (-24.3%) - overridden by custom_stoploss
# Trailing stoploss - ENABLED
trailing_stop = True
trailing_only_offset_is_reached = False
trailing_stop_positive = 0.128 # 12.8% trailing stop distance (hyperopt)
trailing_stop_positive_offset = 0.185 # Activate trailing after 18.5% profit (hyperopt)
# Optimal timeframe for the strategy.
timeframe = "5m"
# Run "populate_indicators()" only for new candle.
process_only_new_candles = True
# These values can be overridden in the config.
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
use_custom_stoploss = True
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 300 # For EMA 251 + buffer calculation
# Hyperoptable parameters
buy_rsi = IntParameter(15, 40, default=30, space="buy", optimize=True)
sell_rsi = IntParameter(60, 90, default=78, space="sell", optimize=True)
# EMA period for trend filter (long-term trend)
ema_period = IntParameter(100, 300, default=168, space="buy", optimize=True)
# Bollinger Bands parameters
bb_period = IntParameter(10, 30, default=21, space="buy", optimize=True)
bb_std = DecimalParameter(1.5, 3.0, default=2.118, space="buy", optimize=True)
# MACD parameters
macd_fast = IntParameter(8, 15, default=12, space="buy", optimize=True)
macd_slow = IntParameter(20, 30, default=20, space="buy", optimize=True)
macd_signal = IntParameter(5, 12, default=11, space="buy", optimize=True)
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached.
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame.
"""
# Trend Indicator: EMA (for trend filter)
dataframe['ema'] = ta.EMA(dataframe, timeperiod=self.ema_period.value)
# Momentum Indicators
dataframe['rsi'] = ta.RSI(dataframe)
# Bollinger Bands
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe),
window=self.bb_period.value,
stds=self.bb_std.value
)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['bb_width'] = (dataframe['bb_upperband'] - dataframe['bb_lowerband']) / dataframe['bb_middleband']
# MACD
macd = ta.MACD(
dataframe,
fastperiod=self.macd_fast.value,
slowperiod=self.macd_slow.value,
signalperiod=self.macd_signal.value
)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Volume indicators
dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=20)
# ATR for volatility measurement
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
# ADX for trend strength
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
# Short EMA for sideways markets
dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=50)
# Dynamic EMA: use long EMA when strong trend (ADX > 25), otherwise short EMA
dataframe['ema_dynamic'] = np.where(
dataframe['adx'] > 25,
dataframe['ema'],
dataframe['ema_short']
)
# Calculate percent above/below EMA
dataframe['ema_distance'] = (dataframe['close'] - dataframe['ema']) / dataframe['ema'] * 100
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe.
Buy when RSI crosses above oversold level (momentum shift) and price is above EMA (uptrend).
Minimal test version.
"""
conditions = []
# Condition 1: Price above dynamic EMA (adaptive trend filter)
conditions.append(dataframe['close'] > dataframe['ema_dynamic'])
# Condition 2: RSI crosses above oversold level (momentum shift)
conditions.append(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value))
# Condition 3: Basic volume check
conditions.append(dataframe['volume'] > 0)
# Apply all conditions
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe.
Sell when RSI crosses above overbought level (entering overbought territory).
"""
conditions = []
# Condition 1: RSI crosses above overbought level
conditions.append(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value))
# Apply all conditions
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'exit_long'] = 1
return dataframe
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
"""
Custom stake amount based on volatility.
Lower position size during high volatility.
"""
# Safety check: data provider may not be available in all modes
if self.dp is None:
return proposed_stake
# Get the dataframe for this pair
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) > 0:
# Use ATR-based position sizing
current_atr = dataframe['atr'].iloc[-1]
current_price = dataframe['close'].iloc[-1]
atr_percent = current_atr / current_price
# Higher volatility = smaller position (risk management)
# Base risk: 1% of portfolio per trade
base_risk = 0.01
volatility_adjustment = base_risk / max(atr_percent, 0.01) # Cap at 1% risk
# Adjust stake (but respect min/max limits)
adjusted_stake = proposed_stake * volatility_adjustment
return min(max(adjusted_stake, min_stake), max_stake)
return proposed_stake
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
"""
Dynamic stop-loss based on ATR volatility.
Stop loss = 1.5 × ATR below entry price, capped between -2% and -8%.
"""
# Safety check: data provider may not be available in all modes
if self.dp is None:
return self.stoploss # Fallback to default
# Get the dataframe for this pair
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) > 0:
current_atr = dataframe['atr'].iloc[-1]
current_price = dataframe['close'].iloc[-1]
atr_percent = current_atr / current_price
# Dynamic stop loss: 1.5 × ATR
atr_stoploss = 1.5 * atr_percent
# Cap between 2% and 8% (allow tighter stops in low volatility)
capped_stoploss = max(min(atr_stoploss, 0.08), 0.02)
# Return negative value (stop loss is negative)
return -capped_stoploss
# Fallback to default stop loss
return self.stoploss
# Required import for reduce function
from functools import reduce