Support and Resistance Trading Strategy
Timeframe
12h
Direction
Long Only
Stoploss
-85.0%
Trailing Stop
No
ROI
0m: 500000.0%
Interface Version
N/A
Startup Candles
N/A
Indicators
1
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
This strategy uses custom_stoploss() to enforce a fixed risk/reward ratio by first calculating a dynamic initial stoploss via ATR - last negative peak
import numpy as np
import pandas as pd
import pandas_ta as ta
from datetime import datetime
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter, stoploss_from_absolute, informative, Order)
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
import scipy
import logging
import warnings
# Suppress common warnings that don't affect strategy performance
warnings.filterwarnings(
'ignore', message='The objective has been evaluated at this point before.')
warnings.simplefilter(action="ignore", category=pd.errors.PerformanceWarning)
warnings.filterwarnings("ignore", category=RuntimeWarning)
logger = logging.getLogger(__name__)
# based on original idea from Neurotrader: https://www.youtube.com/watch?v=mNWPSFOVoYA
def find_levels(price: np.array, atr: float,
first_w: float = 0.1,
atr_mult: float = 3.0,
prom_thresh: float = 0.25):
"""
Identify support and resistance levels using weighted kernel density estimation.
This function uses a Gaussian kernel to create a market profile from historical
price data, with more recent prices weighted higher. Significant peaks in the
density represent potential support/resistance levels.
Args:
price (np.array): Array of log closing prices
atr (float): Average True Range (log scale) for bandwidth scaling
first_w (float): Weight for oldest price data (default: 0.1)
atr_mult (float): Multiplier for ATR bandwidth scaling (default: 3.0)
prom_thresh (float): Minimum prominence threshold for peak detection (default: 0.25)
Returns:
tuple: (levels, peaks, props, price_range, pdf, weights)
- levels: List of identified support/resistance levels
- peaks: Peak indices in the density function
- props: Peak properties from scipy.signal.find_peaks
- price_range: Price range used for density calculation
- pdf: Probability density function values
- weights: Weight array used for recent price emphasis
"""
# Create linearly increasing weights (recent prices weighted more heavily)
last_w = 1.0
weight_step = (last_w - first_w) / len(price)
weights = first_w + np.arange(len(price)) * weight_step
weights[weights < 0] = 0.0
# Generate kernel density estimation with ATR-scaled bandwidth
kernel = scipy.stats.gaussian_kde(price, bw_method=atr * atr_mult, weights=weights)
# Create market profile over price range
min_price = np.min(price)
max_price = np.max(price)
price_step = (max_price - min_price) / 200
price_range = np.arange(min_price, max_price, price_step)
pdf = kernel(price_range) # Market profile density
# Identify significant peaks representing support/resistance levels
pdf_max = np.max(pdf)
prominence_minimum = pdf_max * prom_thresh
peaks, peak_properties = scipy.signal.find_peaks(pdf, prominence=prominence_minimum)
# Convert log prices back to actual price levels
levels = []
for peak in peaks:
levels.append(np.exp(price_range[peak]))
return levels, peaks, peak_properties, price_range, pdf, weights
def support_resistance_levels(data_in: pd.DataFrame, lookback: int,
first_w: float = 0.01,
atr_mult: float = 3.0,
prom_thresh: float = 0.25):
"""
Calculate support and resistance levels for each candle in the dataset.
For each candle, this function analyzes the previous 'lookback' candles
to identify current support and resistance levels using kernel density estimation.
Args:
data_in (pd.DataFrame): OHLC price data
lookback (int): Number of previous candles to analyze
first_w (float): Weight for oldest price data
atr_mult (float): ATR multiplier for bandwidth scaling
prom_thresh (float): Prominence threshold for peak detection
Returns:
list: List of support/resistance levels for each candle (None for insufficient data)
"""
data = data_in.copy()
# Calculate Average True Range on log scale for bandwidth estimation
log_high = np.log(data['high'])
log_low = np.log(data['low'])
log_close = np.log(data['close'])
atr = ta.atr(log_high, log_low, log_close, lookback)
# Initialize levels array
all_levels = [None] * len(data)
# Calculate levels for each candle with sufficient lookback data
for i in range(lookback, len(data)):
start_index = i - lookback
# Extract log closing prices for the lookback period
log_closes = np.log(data.iloc[start_index + 1: i + 1]['close'].to_numpy())
levels, _, _, _, _, _ = find_levels(
log_closes, atr.iloc[i], first_w, atr_mult, prom_thresh
)
all_levels[i] = levels
return all_levels
def sr_penetration_signal(data_in: pd.DataFrame, levels: list):
"""
Generate trading signals based on support/resistance level penetrations.
Creates buy signals when price closes above a resistance level and
sell signals when price closes below a support level.
Args:
data_in (pd.DataFrame): OHLC price data
levels (list): Support/resistance levels for each candle
Returns:
pd.DataFrame: Original data with added 'signal' column
- 1.0: Buy signal (resistance breakout)
- -1.0: Sell signal (support breakdown)
- 0.0: No signal
"""
data = data_in.copy()
signal = np.zeros(len(data))
current_signal = 0.0
close_prices = data['close'].to_numpy()
# Analyze each candle for level penetrations
for i in range(1, len(data)):
if levels[i] is None:
continue
previous_close = close_prices[i - 1]
current_close = close_prices[i]
# Check each support/resistance level for penetrations
for level in levels[i]:
# Resistance breakout (bullish signal)
if current_close > level and previous_close <= level:
current_signal = 1.0
# Support breakdown (bearish signal)
elif current_close < level and previous_close >= level:
current_signal = -1.0
signal[i] = current_signal
# Add signal to output dataframe
data_out = data_in.copy()
data_out['signal'] = signal
return data_out
class SUPPORT_RESISTANCE(IStrategy):
"""
Support and Resistance Trading Strategy
This strategy identifies dynamic support and resistance levels using kernel
density estimation and generates trading signals when price penetrates these levels.
Key Features:
- Uses weighted kernel density to identify key price levels
- ATR-based bandwidth scaling for adaptive level detection
- Long-only strategy focusing on resistance breakouts
- Exit signals on support breakdowns
"""
# Strategy configuration
minimal_roi = {"0": 5000} # Hold positions indefinitely (5000% ROI target)
stoploss = -0.85 # very large, never hit
timeframe = '12h' # 12-hour candles
startup_candle_count: int = 370 # Minimum candles needed before trading
can_short: bool = False # Long-only strategy
use_custom_stoploss: bool = False
process_only_new_candles: bool = True
# Optimization parameters
first_w = DecimalParameter(
1.0, 2.0, decimals=1, default=1.6, space="buy", optimize=True,
load=True
)
atr_mult = DecimalParameter(
2.0, 4.0, decimals=1, default=3.2, space="buy", optimize=True,
load=True
)
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
"""
Calculate all technical indicators and support/resistance levels.
Args:
dataframe (pd.DataFrame): Raw OHLC data
metadata (dict): Pair metadata
Returns:
pd.DataFrame: Dataframe with indicators and trading signals
"""
# Calculate support/resistance levels using 365-candle lookback
levels = support_resistance_levels(
data_in=dataframe,
lookback=365,
first_w=self.first_w.value,
atr_mult=self.atr_mult.value
)
# Generate penetration signals based on calculated levels
dataframe = sr_penetration_signal(data_in=dataframe, levels=levels)
return dataframe
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
"""
Define entry conditions for long positions.
Entry signal: Resistance level breakout (signal == 1)
Args:
dataframe (pd.DataFrame): Dataframe with indicators
metadata (dict): Pair metadata
Returns:
pd.DataFrame: Dataframe with entry signals
"""
dataframe.loc[
(dataframe['signal'] == 1),
'enter_long'
] = 1
# Short entries disabled for this long-only strategy
# dataframe.loc[
# (dataframe['signal'] == -1),
# 'enter_short'
# ] = 1
return dataframe
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
"""
Define exit conditions for positions.
Exit signal: Support level breakdown (signal == -1)
Args:
dataframe (pd.DataFrame): Dataframe with indicators
metadata (dict): Pair metadata
Returns:
pd.DataFrame: Dataframe with exit signals
"""
dataframe.loc[
(dataframe['signal'] == -1),
'exit_long'
] = 1
# Short exits disabled for this long-only strategy
# dataframe.loc[
# (dataframe['signal'] == 0),
# 'exit_short'
# ] = 1
return dataframe