Financial market price charts can often appear overwhelmingly chaotic, a dense tapestry of fluctuating highs and lows. Within this perceived randomness, a common desire for traders and analysts is to simplify the noise, to identify the “true” significant swings that define market structure. This is the domain of the ZigZag indicator.
The ZigZag indicator is not a predictive tool in itself. Instead, it serves as a powerful retrospective filter, designed to highlight only those price movements that exceed a certain percentage threshold, effectively creating a simplified skeleton of the price action. It connects significant swing highs and swing lows, ignoring smaller fluctuations. The immediate benefit is clarity: the ZigZag can make it easier to visually identify trends, reversals, and—crucially for our exploration—classic chart patterns like Double Tops, Double Bottoms, and Triangles.
However, the very nature of the ZigZag indicator presents an interesting challenge for algorithmic trading: it is inherently repainting. A ZigZag point is only confirmed when the price moves a specified percentage in the opposite direction. This means the last segment of a ZigZag line can change as new price data comes in. How might one translate such a lagging, repainting indicator into a functional, non-repainting algorithmic strategy? This article delves into an algorithmic exploration of a ZigZag-based strategy, attempting to leverage its pattern recognition capabilities while navigating its unique characteristics.
The strategy idea revolves around using the ZigZag indicator to map market structure and then applying classical technical analysis principles to this simplified view.
zigzag_pct (e.g., 5%). If the price
moves 5% down from a high, that high is confirmed as a ZigZag high. If
it moves 5% up from a low, that low is confirmed as a ZigZag low.The overarching strategy idea is to explore if the clarity offered by the ZigZag in identifying price patterns and S/R levels, when combined with volume confirmation, can provide actionable trading signals, while acknowledging the inherent lagging nature of the ZigZag.
backtrader StrategyThe following backtrader code provides a concrete
implementation for exploring a ZigZag-based strategy. Each snippet will
be presented and analyzed to understand how the theoretical ideas are
translated into executable code.
Every quantitative exploration begins with data. This section prepares the environment and fetches historical price data.
import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib qt
plt.rcParams['figure.figsize'] = (10, 6)
# Download data and run backtest
data = yf.download('ETH-USD', '2021-01-01', '2024-01-01')
data.columns = data.columns.droplevel(1)
data_feed = bt.feeds.PandasData(dataname=data)Analysis of this Snippet:
matplotlib.pyplot
is configured with %matplotlib qt for an interactive plot
window, and plt.rcParams['figure.figsize'] sets the default
plot size.yfinance.download('ETH-USD', ...) fetches historical data
for Ethereum. The data.columns.droplevel(1) call ensures
column headers (e.g., ‘Close’, ‘Volume’) are in a single-level format,
which backtrader expects.bt.feeds.PandasData(dataname=data) converts the prepared
pandas DataFrame into a data feed for
backtrader, enabling the backtesting engine to process it
bar by bar.ZigZagStrategy
InitializationThis section outlines the ZigZagStrategy class,
including its parameters, and the initialization of indicators and
internal state variables for tracking ZigZag points and patterns.
class ZigZagStrategy(bt.Strategy):
params = (
('zigzag_pct', 0.05), # ZigZag reversal percentage (5%)
('pattern_lookback', 5), # Number of ZigZag points for pattern analysis
('support_resistance_strength', 2), # Minimum touches for S/R level confirmation
('breakout_volume_multiplier', 1.2), # Volume confirmation multiplier for breakouts
('volume_period', 7), # Volume average period for volume confirmation
('trailing_stop_pct', 0.02), # 2% trailing stop percentage
)
def __init__(self):
# Price and volume data references
self.high = self.data.high
self.low = self.data.low
self.close = self.data.close
self.volume = self.data.volume
# Volume indicator for confirmation
self.volume_sma = bt.indicators.SMA(self.volume, period=self.params.volume_period)
# ZigZag tracking variables
self.zigzag_points = [] # Stores (bar_index, price, type='high'/'low') tuples
self.current_direction = 0 # 1 for up, -1 for down, 0 for initial
self.current_extreme = None # (bar_index, high_price, low_price) of current extreme bar
self.last_pivot = None # The last confirmed ZigZag pivot point
# Pattern recognition and S/R tracking
self.support_levels = [] # Stores (price_level, count_of_touches)
self.resistance_levels = [] # Stores (price_level, count_of_touches)
self.current_pattern = None # Stores identified chart pattern details
# Trailing stop tracking variables
self.entry_price = 0
self.trailing_stop_price = 0
self.highest_price_since_entry = 0 # For long positions
self.lowest_price_since_entry = 0 # For short positions
# backtrader's order tracking variables
self.order = None
self.stop_order = NoneAnalysis of the __init__ Method:
params: These parameters are the
adjustable controls for the strategy. zigzag_pct defines
the sensitivity of the ZigZag (how large a reversal is needed).
pattern_lookback controls how many recent ZigZag points are
considered for pattern identification.
support_resistance_strength sets the minimum number of
touches required to confirm an S/R level.
breakout_volume_multiplier and volume_period
define the volume confirmation filter. trailing_stop_pct
sets the percentage for the trailing stop-loss.bt.indicators.SMA on
volume is set up for volume confirmation.zigzag_points, current_direction,
current_extreme, last_pivot) are initialized
to manually implement and track the ZigZag indicator’s logic. This
manual implementation is necessary because backtrader does
not have a built-in repainting ZigZag indicator.support_levels, resistance_levels) to store
identified S/R zones and their “strength” (number of touches).
current_pattern will store details of recognized chart
patterns.entry_price,
trailing_stop_price,
highest_price_since_entry,
lowest_price_since_entry).backtrader
variables (order, stop_order) are set up for
managing trade orders.This section contains the core logic for calculating ZigZag points and updating support/resistance levels based on these points.
def calculate_zigzag(self):
"""Calculate ZigZag points based on percentage threshold"""
current_bar = len(self.data) - 1 # Current bar index
current_high = self.high[0]
current_low = self.low[0]
current_close = self.close[0] # Not directly used for extreme, but available
# Initialize ZigZag if it's the first bar
if self.current_extreme is None:
self.current_extreme = (current_bar, current_high, current_low)
return
prev_bar, prev_high, prev_low = self.current_extreme # Previous confirmed extreme
threshold = self.params.zigzag_pct # The percentage reversal needed
# Determine initial direction if not set yet
if self.current_direction == 0:
if current_high > prev_high * (1 + threshold):
self.current_direction = 1 # Upward trend starts
self.add_zigzag_point(prev_bar, prev_low, 'low') # Add previous low as first pivot
self.current_extreme = (current_bar, current_high, current_low)
elif current_low < prev_low * (1 - threshold):
self.current_direction = -1 # Downward trend starts
self.add_zigzag_point(prev_bar, prev_high, 'high') # Add previous high as first pivot
self.current_extreme = (current_bar, current_high, current_low)
# Logic when already in an upward trend
elif self.current_direction == 1:
if current_high > prev_high: # New higher high, extend current upward extreme
self.current_extreme = (current_bar, current_high, current_low)
elif current_low < prev_high * (1 - threshold): # Price reversed significantly downwards
self.add_zigzag_point(prev_bar, prev_high, 'high') # Confirm previous high as a pivot
self.current_direction = -1 # Change direction to down
self.current_extreme = (current_bar, current_high, current_low) # Start new extreme
# Logic when already in a downward trend
elif self.current_direction == -1:
if current_low < prev_low: # New lower low, extend current downward extreme
self.current_extreme = (current_bar, current_high, current_low)
elif current_high > prev_low * (1 + threshold): # Price reversed significantly upwards
self.add_zigzag_point(prev_bar, prev_low, 'low') # Confirm previous low as a pivot
self.current_direction = 1 # Change direction to up
self.current_extreme = (current_bar, current_high, current_low)
def add_zigzag_point(self, bar, price, point_type):
"""Adds a new confirmed ZigZag point to the list and updates S/R levels."""
self.zigzag_points.append((bar, price, point_type))
# Keep only a limited number of recent ZigZag points
if len(self.zigzag_points) > 20: # Keep latest 20 points
self.zigzag_points = self.zigzag_points[-20:]
# Update support/resistance levels based on this new pivot
self.update_support_resistance(price, point_type)
def update_support_resistance(self, price, point_type):
"""Updates support and resistance levels based on new ZigZag pivots."""
tolerance = self.params.zigzag_pct / 2 # A small percentage tolerance for S/R levels
if point_type == 'low': # If a new ZigZag low is confirmed
support_confirmed = False
for i, (level, count) in enumerate(self.support_levels):
# Check if new low is close to an existing support level
if abs(price - level) / level < tolerance:
self.support_levels[i] = (level, count + 1) # Increment touch count
support_confirmed = True
break
if not support_confirmed: # If not near existing level, add as a new potential support
self.support_levels.append((price, 1))
elif point_type == 'high': # If a new ZigZag high is confirmed
resistance_confirmed = False
for i, (level, count) in enumerate(self.resistance_levels):
# Check if new high is close to an existing resistance level
if abs(price - level) / level < tolerance:
self.resistance_levels[i] = (level, count + 1) # Increment touch count
resistance_confirmed = True
break
if not resistance_confirmed: # If not near existing level, add as a new potential resistance
self.resistance_levels.append((price, 1))
# Keep only strong S/R levels (minimum touches) and only the most recent ones
self.support_levels = [(level, count) for level, count in self.support_levels if count >= self.params.support_resistance_strength][-10:]
self.resistance_levels = [(level, count) for level, count in self.resistance_levels if count >= self.params.support_resistance_strength][-10:]Analysis of ZigZag and S/R Management:
calculate_zigzag(): This function
contains the manual implementation of the ZigZag indicator. It tracks
the current_extreme (the highest/lowest price since the
last confirmed pivot) and current_direction. When price
reverses by zigzag_pct from the
current_extreme, a new ZigZag point is confirmed and added
via add_zigzag_point(). This manual approach to ZigZag
allows for precise control but also means handling the lagging nature
and potential repainting (though the add_zigzag_point logic implies
pivots are added at past prev_bar indices when
confirmed).add_zigzag_point(): This helper
function appends confirmed ZigZag pivots (bar index, price, type) to
self.zigzag_points. It also calls
update_support_resistance() to dynamically manage S/R
levels.update_support_resistance(): This
function attempts to identify and track significant S/R levels. When a
new ZigZag low (point_type == 'low') is added, it checks if
it’s close to an existing support level (within a
tolerance). If so, it increments the “touch count” for that
level; otherwise, it adds a new potential support. Similar logic applies
to resistance levels. Only levels with a count greater than
or equal to support_resistance_strength are kept, aiming to
identify more robust S/R zones.This section focuses on identifying classic chart patterns from the sequence of ZigZag points and on querying the nearest support and resistance levels.
def identify_patterns(self):
"""Identifies common ZigZag patterns (Double Top/Bottom, Triangles)."""
if len(self.zigzag_points) < 4: # Need at least 4 points for most basic patterns
return None
# Consider only the most recent ZigZag points for pattern detection
recent_points = self.zigzag_points[-self.params.pattern_lookback:]
if len(recent_points) >= 4:
# Double top pattern: High - Low - High - Low (last point is low)
# Pattern: H - L - H - L (where H2 approx H1, L below H1)
if (recent_points[-1][2] == 'low' and recent_points[-2][2] == 'high' and
recent_points[-3][2] == 'low' and recent_points[-4][2] == 'high'):
high1 = recent_points[-4][1]
high2 = recent_points[-2][1]
low_between = recent_points[-3][1]
# Check if the two highs are approximately equal (within zigzag_pct tolerance)
if abs(high1 - high2) / max(high1, high2) < self.params.zigzag_pct:
# Return pattern details: type, resistance level (max of highs), support level (low between highs)
return {'type': 'double_top', 'resistance': max(high1, high2), 'support': low_between}
# Double bottom pattern: Low - High - Low - High (last point is high)
# Pattern: L - H - L - H (where L2 approx L1, H above L1)
if (len(recent_points) >= 4 and
recent_points[-1][2] == 'high' and recent_points[-2][2] == 'low' and
recent_points[-3][2] == 'high' and recent_points[-4][2] == 'low'):
low1 = recent_points[-4][1]
low2 = recent_points[-2][1]
high_between = recent_points[-3][1]
# Check if the two lows are approximately equal
if abs(low1 - low2) / max(low1, low2) < self.params.zigzag_pct:
# Return pattern details: type, support level (min of lows), resistance level (high between lows)
return {'type': 'double_bottom', 'support': min(low1, low2), 'resistance': high_between}
# Triangle patterns (need at least 5 ZigZag points for converging lines)
if len(recent_points) >= 5:
highs = [p[1] for p in recent_points if p[2] == 'high']
lows = [p[1] for p in recent_points if p[2] == 'low']
if len(highs) >= 2 and len(lows) >= 2:
# Ascending triangle: rising lows, flat resistance
# (Low 1 < Low 2), and (High 1 approx High 2)
if (lows[-1] > lows[-2] and
abs(highs[-1] - highs[-2]) / max(highs[-1], highs[-2]) < self.params.zigzag_pct):
return {'type': 'ascending_triangle', 'resistance': max(highs[-2:]), 'support_trend': 'rising'}
# Descending triangle: falling highs, flat support
# (High 1 > High 2), and (Low 1 approx Low 2)
if (highs[-1] < highs[-2] and
abs(lows[-1] - lows[-2]) / max(lows[-1], lows[-2]) < self.params.zigzag_pct):
return {'type': 'descending_triangle', 'support': min(lows[-2:]), 'resistance_trend': 'falling'}
return None # No recognizable pattern
def get_nearest_support_resistance(self):
"""Finds the nearest strong support and resistance levels to the current price."""
current_price = self.close[0]
nearest_support = None
nearest_resistance = None
# Find nearest support below current price
for level, count in self.support_levels:
if level < current_price: # Check for levels below current price
if nearest_support is None or level > nearest_support: # Find the closest one (highest of supports below price)
nearest_support = level
# Find nearest resistance above current price
for level, count in self.resistance_levels:
if level > current_price: # Check for levels above current price
if nearest_resistance is None or level < nearest_resistance: # Find the closest one (lowest of resistances above price)
nearest_resistance = level
return nearest_support, nearest_resistanceAnalysis of Pattern Recognition and S/R Queries:
identify_patterns(): This function
attempts to recognize classic chart patterns like Double Tops, Double
Bottoms, Ascending Triangles, and Descending Triangles from the sequence
of zigzag_points. It uses relative price comparisons and
the zigzag_pct as a tolerance for “approximately equal”
levels.
pattern_lookback parameter is crucial for this
function.get_nearest_support_resistance(): This
function iterates through the support_levels and
resistance_levels (maintained by
update_support_resistance()) to find the closest S/R levels
to the current price. This provides immediate context for potential
breakout or reversal opportunities.This section contains the notify_order method and a
helper update_trailing_stop method, which together manage
order status updates and implement a dynamic trailing stop-loss
mechanism.
def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy() and self.position.size > 0:
# Long position opened - set initial trailing stop price
self.entry_price = order.executed.price
self.highest_price_since_entry = order.executed.price # Initial highest price
self.trailing_stop_price = order.executed.price * (1 - self.params.trailing_stop_pct)
# Place a simple Stop order for the trailing stop
self.stop_order = self.sell(exectype=bt.Order.Stop, price=self.trailing_stop_price)
elif order.issell() and self.position.size < 0:
# Short position opened - set initial trailing stop price
self.entry_price = order.executed.price
self.lowest_price_since_entry = order.executed.price # Initial lowest price
self.trailing_stop_price = order.executed.price * (1 + self.params.trailing_stop_pct)
# Place a simple Stop order for the trailing stop
self.stop_order = self.buy(exectype=bt.Order.Stop, price=self.trailing_stop_price)
if order.status in [order.Completed, order.Canceled, order.Rejected]:
# Reset main order reference unless it's the stop order itself
if self.stop_order is None or order != self.stop_order:
self.order = None
# If the completed order was the stop order, reset trailing stop tracking
if self.stop_order is not None and order == self.stop_order:
self.stop_order = None
# Reset tracking variables when position is closed by stop-loss
self.entry_price = 0
self.trailing_stop_price = 0
self.highest_price_since_entry = 0
self.lowest_price_since_entry = 0
def update_trailing_stop(self):
"""Updates the trailing stop based on current price movement and pre-defined percentage."""
if not self.position or self.stop_order is None:
return # No open position or no stop order to update
current_price = self.close[0]
if self.position.size > 0: # Long position
# Update the highest price seen since entry (for trailing stop reference)
if current_price > self.highest_price_since_entry:
self.highest_price_since_entry = current_price
# Calculate the new trailing stop price
new_stop_price = self.highest_price_since_entry * (1 - self.params.trailing_stop_pct)
# Only update the stop if it moves in our favor (higher for long)
if new_stop_price > self.trailing_stop_price:
self.trailing_stop_price = new_stop_price
# Cancel the old stop order and place a new one at the updated price
self.cancel(self.stop_order)
self.stop_order = self.sell(exectype=bt.Order.Stop, price=self.trailing_stop_price)
elif self.position.size < 0: # Short position
# Update the lowest price seen since entry (for trailing stop reference)
if current_price < self.lowest_price_since_entry:
self.lowest_price_since_entry = current_price
# Calculate the new trailing stop price
new_stop_price = self.lowest_price_since_entry * (1 + self.params.trailing_stop_pct)
# Only update the stop if it moves in our favor (lower for short)
if new_stop_price < self.trailing_stop_price:
self.trailing_stop_price = new_stop_price
# Cancel the old stop order and place a new one at the updated price
self.cancel(self.stop_order)
self.stop_order = self.buy(exectype=bt.Order.Stop, price=self.trailing_stop_price)Analysis of Risk Management:
notify_order(): This
backtrader method is triggered by order status changes.
Upon a successful buy or sell (order.Completed), it places
an initial trailing stop-loss order
(bt.Order.Stop). For long positions, this is a sell stop
below the entry price, and for short positions, a buy stop above. It
also initializes highest_price_since_entry or
lowest_price_since_entry for the trailing logic. The logic
meticulously manages order references to avoid confusion.update_trailing_stop(): This function
contains the manual logic for updating the trailing stop. For a long
position, it constantly checks if the current price is a new high since
entry. If so, it recalculates the stop price (moving it up) and, if the
new stop is higher than the previous one, it cancels the old stop order
and places a new one at the updated, more favorable price. A similar,
inverse logic applies to short positions. This manual approach to
trailing stops ensures fine-grained control over the stop
placement.This is the main loop that executes for each new bar of data, orchestrating ZigZag calculation, pattern detection, S/R analysis, and trading decisions.
def next(self):
# Update trailing stop first to ensure immediate risk management
self.update_trailing_stop()
if self.order is not None:
return # Skip if an order is pending
# Calculate ZigZag points for the current bar
self.calculate_zigzag()
# Skip if not enough ZigZag points have been accumulated for pattern analysis
if len(self.zigzag_points) < 3:
return
# Identify patterns based on recent ZigZag points
pattern = self.identify_patterns()
current_price = self.close[0]
# Check for volume confirmation for potential breakouts/reversals
volume_confirmation = self.volume[0] > self.volume_sma[0] * self.params.breakout_volume_multiplier
# Get nearest support and resistance levels from confirmed pivots
nearest_support, nearest_resistance = self.get_nearest_support_resistance()
# --- Trading signals based on ZigZag patterns and S/R breakouts ---
# Pattern-based signals
if pattern:
if pattern['type'] == 'double_bottom' and volume_confirmation:
# Buy on breakout above resistance after double bottom
# Hypothesis: Price breaking above the "neckline" confirms reversal
if current_price > pattern['resistance'] and not self.position:
self.order = self.buy()
elif pattern['type'] == 'double_top' and volume_confirmation:
# Sell on breakdown below support after double top
# Hypothesis: Price breaking below the "neckline" confirms reversal
if current_price < pattern['support'] and not self.position:
self.order = self.sell()
elif pattern['type'] == 'ascending_triangle' and volume_confirmation:
# Buy on resistance breakout in ascending triangle
# Hypothesis: Price breaking above flat resistance confirms bullish move
if current_price > pattern['resistance'] and not self.position:
self.order = self.buy()
elif pattern['type'] == 'descending_triangle' and volume_confirmation:
# Sell on support breakdown in descending triangle
# Hypothesis: Price breaking below flat support confirms bearish move
if current_price < pattern['support'] and not self.position:
self.order = self.sell()
# Support/Resistance breakout signals (as a fallback if no pattern is found)
# This part of the logic executes only if no specific pattern was identified this bar
else:
if nearest_resistance and volume_confirmation:
# Buy on resistance breakout (with a small buffer to confirm clear break)
# Hypothesis: Strong break above resistance signals continuation
if (current_price > nearest_resistance * 1.001 and
not self.position):
self.order = self.buy()
if nearest_support and volume_confirmation:
# Sell on support breakdown (with a small buffer to confirm clear break)
# Hypothesis: Strong break below support signals continuation
if (current_price < nearest_support * 0.999 and
not self.position):
self.order = self.sell()
# ZigZag trend reversal signals (can override or complement pattern/S/R breaks)
# These aim to catch reversals immediately after a new ZigZag pivot is confirmed
if len(self.zigzag_points) >= 2: # Ensure at least two pivots to check for reversal
last_point = self.zigzag_points[-1] # The most recently confirmed ZigZag pivot
# New ZigZag low formed recently - potential reversal up
if (last_point[2] == 'low' and
len(self.data) - last_point[0] <= 3 and # Check if this pivot is very recent (within 3 bars)
volume_confirmation):
if self.position.size < 0: # Close short position
if self.stop_order is not None: self.cancel(self.stop_order)
self.order = self.close()
elif not self.position: # Go long if no position
self.order = self.buy()
# New ZigZag high formed recently - potential reversal down
elif (last_point[2] == 'high' and
len(self.data) - last_point[0] <= 3 and # Check if this pivot is very recent
volume_confirmation):
if self.position.size > 0: # Close long position
if self.stop_order is not None: self.cancel(self.stop_order)
self.order = self.close()
elif not self.position: # Go short if no position
self.order = self.sell()Analysis of next() (The Trade
Orchestrator):
self.update_trailing_stop() is called first to ensure that
risk management for any open position is handled immediately.self.calculate_zigzag() is executed on every bar to
continuously update the ZigZag points, which is the foundation for
subsequent analysis.if len(self.zigzag_points) < 3: return ensures that
enough ZigZag points have been identified before attempting pattern
recognition.pattern = self.identify_patterns(),
volume_confirmation, and
nearest_support, nearest_resistance = self.get_nearest_support_resistance()
collect all necessary components for trading decisions.pattern (Double Bottom/Top, Triangles) is identified, the
strategy attempts to trade on a breakout from its implied support or
resistance level, confirmed by volume_confirmation. These
are prioritized.else block), the strategy then checks for
simpler breakouts of the nearest_support or
nearest_resistance levels, also confirmed by volume. A
small buffer (0.1% of price) is added to ensure a “clean” breakout.len(self.data) - last_point[0] <= 3) ZigZag
low or high points, hypothesizing that these signal immediate reversals.
These signals can trigger entries or opposing position closures.if self.order is not None: return prevents multiple main
orders. The self.buy(), self.sell(), and
self.close() calls, combined with
self.cancel(self.stop_order), manage the actual trade
executions and stop-loss orders.This section sets up backtrader’s core engine, adds the
strategy and data, configures the broker, and executes the
simulation.
cerebro = bt.Cerebro()
cerebro.addstrategy(ZigZagStrategy)
cerebro.adddata(data_feed)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.001)
print(f'Start: ${cerebro.broker.getvalue():,.2f}')
results = cerebro.run()
print(f'End: ${cerebro.broker.getvalue():,.2f}')
print(f'Return: {((cerebro.broker.getvalue() / 100000) - 1) * 100:.2f}%')
# Fix matplotlib plotting issues
plt.rcParams['figure.max_open_warning'] = 0
plt.rcParams['agg.path.chunksize'] = 10000
try:
cerebro.plot(iplot=False, style='line', volume=True)
plt.show()
except Exception as e:
print(f"Plotting error: {e}")
print("Strategy completed successfully - plotting skipped")Analysis of the Backtest Execution:
cerebro = bt.Cerebro(): This line
initializes the central backtrader engine responsible for
running the simulation.cerebro.addstrategy(...) registers the defined ZigZag
strategy, and cerebro.adddata(...) feeds it the historical
data for the simulation.cerebro.addsizer(...),
cerebro.broker.setcash(...), and
cerebro.broker.setcommission(...) configure the initial
capital, the percentage of capital to use per trade, and simulate
trading commissions, contributing to a more realistic backtest
environment.cerebro.run(): This command initiates
the entire backtest simulation, allowing the strategy to execute its
logic sequentially through the historical data bars. The
results variable stores the outcome of the backtest.print
statements provide a straightforward summary of the simulation,
displaying the starting and ending portfolio values, along with the
overall percentage return achieved by the strategy over the backtest
period.plt.rcParams
lines configure matplotlib for plotting, potentially
preventing warnings with large datasets. The cerebro.plot()
call generates a visual representation of the backtest. It is configured
with style='line' for prices and crucially,
volume=True to display volume bars. This is essential for
understanding how volume confirms the ZigZag-derived signals.This backtrader strategy offers a fascinating dive into
automating price pattern recognition and support/resistance dynamics
using the ZigZag indicator. It highlights the potential for structuring
trading decisions around simplified market geometry.
next() method has a hierarchical structure for signals
(patterns first, then S/R, then direct ZigZag reversals). How do these
different signal types interact? Do they complement each other, or do
they generate conflicting signals that need further resolution?zigzag_pct, pattern_lookback,
support_resistance_strength,
breakout_volume_multiplier). Their optimal tuning would be
complex and highly dependent on the asset and timeframe.This strategy provides a rich ground for further research into geometry-based trading. The journey of translating the visual art of chart pattern recognition into precise, testable algorithms is a continuous and intriguing challenge in quantitative trading.