Pivot points are foundational technical analysis tools that provide
dynamic support and resistance levels based on previous period’s price
action. By incorporating pivot points from multiple timeframes (daily,
weekly, monthly), traders can gain a more comprehensive view of
potential turning points in the market. This article explores a
backtrader
strategy that leverages these multi-timeframe
pivot points, combining them with volume and Relative Strength Index
(RSI) for trade confirmation, and always ensuring disciplined exits with
trailing stops.
The standard pivot point calculation is based on the previous period’s high, low, and close prices. From the central Pivot Point (PP), several support (S1, S2, S3) and resistance (R1, R2, R3) levels are derived:
These levels act as potential areas where price might reverse (bounce) or continue its move (breakout).
PivotPointVolumeRSIConfirmationStrategy
in backtrader
The PivotPointStrategy
(renamed to
PivotPointVolumeRSIConfirmationStrategy
to reflect its
comprehensive logic) is designed to identify and trade these
interactions.
import backtrader as bt
from datetime import timedelta # Needed for weekly/monthly calculations
import numpy as np # For np.isnan check
class PivotPointVolumeRSIConfirmationStrategy(bt.Strategy):
= (
params 'use_daily', True), # Use daily pivot points
('use_weekly', True), # Use weekly pivot points
('use_monthly', False), # Use monthly pivot points
('bounce_threshold', 0.01), # 1% threshold for level interaction (e.g., 1% of level price)
('breakout_threshold', 0.03), # 3% threshold for breakouts
('volume_multiplier', 1.2), # Volume confirmation multiplier (e.g., 20% above average)
('volume_period', 7), # Volume average period
('rsi_period', 14), # RSI for momentum confirmation
('stop_loss_pct', 0.05), # Initial stop loss percentage
('trail_percent', 0.02), # Trailing stop percentage for exits
(
)
def __init__(self):
# Price data
self.high = self.data.high
self.low = self.data.low
self.close = self.data.close
self.open = self.data.open
self.volume = self.data.volume
# Technical indicators for confirmation
self.volume_sma = bt.indicators.SMA(self.volume, period=self.params.volume_period)
self.rsi = bt.indicators.RSI(period=self.params.rsi_period)
# Pivot point levels storage for different timeframes
self.daily_pivots = {}
self.weekly_pivots = {}
self.monthly_pivots = {}
# Current active levels (sorted list of tuples: (value, timeframe, name))
self.current_levels = []
# Track last calculation dates to determine new periods
self.last_daily_calc = None
self.last_weekly_calc = None
self.last_monthly_calc = None
# Store recent OHLC for calculations across periods
self.daily_ohlc = {'high': 0, 'low': 0, 'close': 0}
self.weekly_ohlc = {'high': 0, 'low': 0, 'close': 0}
self.monthly_ohlc = {'high': 0, 'low': 0, 'close': 0}
# Track orders to prevent multiple orders
self.order = None
self.stop_order = None # For fixed stop loss
self.trail_order = None # For trailing stop
def calculate_pivot_levels(self, high, low, close):
"""Calculate pivot point and support/resistance levels based on classic formula."""
= (high + low + close) / 3
pp
# Support levels
= (2 * pp) - high
s1 = pp - (high - low)
s2 = low - 2 * (high - pp)
s3
# Resistance levels
= (2 * pp) - low
r1 = pp + (high - low)
r2 = high + 2 * (pp - low)
r3
return {
'PP': pp,
'S1': s1, 'S2': s2, 'S3': s3,
'R1': r1, 'R2': r2, 'R3': r3
}
def update_ohlc_data(self):
"""
Aggregates OHLC data for daily, weekly, and monthly pivot calculations.
Calculates pivot levels when a new period begins.
"""
= self.data.datetime.date(0)
current_date = self.high[0]
current_high = self.low[0]
current_low = self.close[0]
current_close
# Update daily OHLC and calculate pivots if a new day starts
if self.last_daily_calc != current_date:
if self.last_daily_calc is not None:
# Calculate pivots from previous day's accumulated OHLC
self.daily_pivots[current_date] = self.calculate_pivot_levels(
self.daily_ohlc['high'],
self.daily_ohlc['low'],
self.daily_ohlc['close']
)
# Reset for new day
self.daily_ohlc = {'high': current_high, 'low': current_low, 'close': current_close}
self.last_daily_calc = current_date
else:
# Update current day's high/low
self.daily_ohlc['high'] = max(self.daily_ohlc['high'], current_high)
self.daily_ohlc['low'] = min(self.daily_ohlc['low'], current_low)
self.daily_ohlc['close'] = current_close # Update close as it's the last price of the bar
# Update weekly OHLC (Monday = 0, Sunday = 6) and calculate pivots if a new week starts
= current_date - timedelta(days=current_date.weekday())
week_start if self.last_weekly_calc != week_start:
if self.last_weekly_calc is not None:
self.weekly_pivots[week_start] = self.calculate_pivot_levels(
self.weekly_ohlc['high'],
self.weekly_ohlc['low'],
self.weekly_ohlc['close']
)
self.weekly_ohlc = {'high': current_high, 'low': current_low, 'close': current_close}
self.last_weekly_calc = week_start
else:
self.weekly_ohlc['high'] = max(self.weekly_ohlc['high'], current_high)
self.weekly_ohlc['low'] = min(self.weekly_ohlc['low'], current_low)
self.weekly_ohlc['close'] = current_close
# Update monthly OHLC and calculate pivots if a new month starts
= current_date.replace(day=1)
month_start if self.last_monthly_calc != month_start:
if self.last_monthly_calc is not None:
self.monthly_pivots[month_start] = self.calculate_pivot_levels(
self.monthly_ohlc['high'],
self.monthly_ohlc['low'],
self.monthly_ohlc['close']
)
self.monthly_ohlc = {'high': current_high, 'low': current_low, 'close': current_close}
self.last_monthly_calc = month_start
else:
self.monthly_ohlc['high'] = max(self.monthly_ohlc['high'], current_high)
self.monthly_ohlc['low'] = min(self.monthly_ohlc['low'], current_low)
self.monthly_ohlc['close'] = current_close
def get_current_pivot_levels(self):
"""
Retrieves all currently active pivot levels (daily, weekly, monthly)
and returns them as a sorted list.
"""
= self.data.datetime.date(0)
current_date = []
levels
# Daily pivots
if self.params.use_daily and current_date in self.daily_pivots:
= self.daily_pivots[current_date]
daily for level_name, level_value in daily.items():
'daily', level_name))
levels.append((level_value,
# Weekly pivots
if self.params.use_weekly:
= current_date - timedelta(days=current_date.weekday())
week_start if week_start in self.weekly_pivots:
= self.weekly_pivots[week_start]
weekly for level_name, level_value in weekly.items():
'weekly', level_name))
levels.append((level_value,
# Monthly pivots
if self.params.use_monthly:
= current_date.replace(day=1)
month_start if month_start in self.monthly_pivots:
= self.monthly_pivots[month_start]
monthly for level_name, level_value in monthly.items():
'monthly', level_name))
levels.append((level_value,
return sorted(levels, key=lambda x: x[0]) # Sort by price
def find_nearest_levels(self, price):
"""Find the nearest support and resistance levels to the current price."""
= self.get_current_pivot_levels()
levels
= [(level, timeframe, name) for level, timeframe, name in levels if level > price]
resistance_levels = [(level, timeframe, name) for level, timeframe, name in levels if level < price]
support_levels
= min(resistance_levels, key=lambda x: x[0] - price) if resistance_levels else None
nearest_resistance = max(support_levels, key=lambda x: x[0] - price) if support_levels else None
nearest_support
return nearest_support, nearest_resistance
def check_level_interaction(self, price, high, low):
"""
Checks if the current price is interacting with any pivot levels,
distinguishing between 'touch/near' for bounces and 'breakout'.
"""
= self.get_current_pivot_levels()
levels
for level_price, timeframe, level_name in levels:
# Check if price is near the level for bounce consideration
= abs(price - level_price) / level_price
distance_pct
if distance_pct <= self.params.bounce_threshold:
# Check if we touched or crossed the level within the current bar
if (low <= level_price <= high):
return 'touch', level_price, timeframe, level_name
elif distance_pct <= self.params.bounce_threshold / 2: # Very close proximity
return 'near', level_price, timeframe, level_name
# Check for breakouts - current close *past* the level with significant momentum
elif distance_pct <= self.params.breakout_threshold:
if level_name.startswith('S') and price < level_price: # Price closed below support
# Check if the low of the bar broke below the support
if low <= level_price:
return 'support_break', level_price, timeframe, level_name
elif level_name.startswith('R') and price > level_price: # Price closed above resistance
# Check if the high of the bar broke above the resistance
if high >= level_price:
return 'resistance_break', level_price, timeframe, level_name
return None, None, None, None
def volume_confirmation(self):
"""Checks if current volume is significantly higher than average."""
if np.isnan(self.volume_sma[0]) or self.volume_sma[0] == 0:
return True # Not enough data for SMA, or SMA is zero
return self.volume[0] > self.volume_sma[0] * self.params.volume_multiplier
def momentum_confirmation(self, trade_direction):
"""Checks RSI for momentum confirmation, avoiding overbought/oversold extremes."""
if np.isnan(self.rsi[0]):
return True # Not enough data for RSI
if trade_direction == 'long':
# Looking for a long: RSI should be rising from oversold or in a healthy range
return self.rsi[0] > 30 # Avoid extreme oversold
elif trade_direction == 'short':
# Looking for a short: RSI should be falling from overbought or in a healthy range
return self.rsi[0] < 70 # Avoid extreme overbought
return True # Default to true if no specific direction
def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy(): # If a buy order completed
# Set initial fixed stop loss
= order.executed.price * (1 - self.params.stop_loss_pct)
stop_price_fixed self.stop_order = self.sell(exectype=bt.Order.Stop, price=stop_price_fixed)
# Set trailing stop
self.trail_order = self.sell(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
elif order.issell(): # If a sell (short) order completed
# Set initial fixed stop loss
= order.executed.price * (1 + self.params.stop_loss_pct)
stop_price_fixed self.stop_order = self.buy(exectype=bt.Order.Stop, price=stop_price_fixed)
# Set trailing stop
self.trail_order = self.buy(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
# Reset order tracking variables
if order.status in [order.Completed, order.Canceled, order.Rejected]:
self.order = None
if order == self.stop_order:
self.stop_order = None
if order == self.trail_order:
self.trail_order = None
def next(self):
if self.order is not None: # If an order is already pending, wait
return
# Update OHLC data and calculate pivots for the current bar
self.update_ohlc_data()
# Get current price action
= self.close[0]
current_price = self.high[0]
current_high = self.low[0]
current_low
# Check how the price is interacting with pivot levels
= self.check_level_interaction(
interaction, level_price, timeframe, level_name
current_price, current_high, current_low
)
# If no significant interaction, do nothing
if interaction is None:
return
# Trading logic based on pivot level interactions and confirmations
if interaction in ['touch', 'near']:
# Bounce strategy - expect reversal at key levels
if level_name.startswith('S'): # Support level - expect bounce up
if (self.momentum_confirmation('long') and
self.volume_confirmation()):
if self.position.size < 0: # If currently short, close short position
# Cancel existing stop/trail orders before closing
if self.stop_order: self.cancel(self.stop_order)
if self.trail_order: self.cancel(self.trail_order)
self.order = self.close()
elif not self.position: # If no position, go long
self.order = self.buy()
elif level_name.startswith('R'): # Resistance level - expect bounce down
if (self.momentum_confirmation('short') and
self.volume_confirmation()):
if self.position.size > 0: # If currently long, close long position
# Cancel existing stop/trail orders before closing
if self.stop_order: self.cancel(self.stop_order)
if self.trail_order: self.cancel(self.trail_order)
self.order = self.close()
elif not self.position: # If no position, go short
self.order = self.sell()
elif interaction == 'resistance_break':
# Resistance breakout - go long
if (self.momentum_confirmation('long') and
self.volume_confirmation()):
if self.position.size < 0: # Close short if existing
if self.stop_order: self.cancel(self.stop_order)
if self.trail_order: self.cancel(self.trail_order)
self.order = self.close()
elif not self.position: # Go long
self.order = self.buy()
elif interaction == 'support_break':
# Support breakdown - go short
if (self.momentum_confirmation('short') and
self.volume_confirmation()):
if self.position.size > 0: # Close long if existing
if self.stop_order: self.cancel(self.stop_order)
if self.trail_order: self.cancel(self.trail_order)
self.order = self.close()
elif not self.position: # Go short
self.order = self.sell()
Multi-Timeframe Pivot Calculation:
update_ohlc_data()
method intelligently aggregates
high, low, and close prices for daily, weekly, and monthly periods.calculate_pivot_levels()
function only when a new daily, weekly, or monthly period begins (e.g.,
at the start of a new day, Monday for a new week, or the 1st of the
month for a new month). This ensures that pivot levels are derived from
the previous completed period’s data, which is the standard
practice.get_current_pivot_levels()
method then gathers all
active daily, weekly, and monthly pivot levels and sorts them by price,
making it easy to identify nearby support/resistance.Interaction Detection
(check_level_interaction
):
bounce_threshold
and
breakout_threshold
parameters to define these proximity
zones, allowing for flexible sensitivity.Confirmation Filters:
volume_confirmation
): This checks if the current
trading volume exceeds a multiple (volume_multiplier
) of
the average volume over a volume_period
. High volume often
confirms the strength of a price move.momentum_confirmation
): It uses the RSI indicator
to gauge momentum. For long trades, it seeks RSI above 30 (not
oversold), and for short trades, RSI below 70 (not overbought). This
aims to ensure trades are taken when there’s healthy momentum, avoiding
entries into exhausted moves.Trading Logic (next
):
Risk Management (Stop Loss and Trailing Stop):
notify_order
method immediately places both an initial
fixed stop-loss (stop_loss_pct
) and a trailing stop
(trail_percent
) after an entry order is completed.This PivotPointVolumeRSIConfirmationStrategy
provides a
comprehensive framework for trading pivot points, enhancing signal
reliability through multi-timeframe analysis and additional confirmation
indicators, and implementing robust risk management with trailing stops.
It reflects a nuanced approach to capturing both bounce and breakout
opportunities.