Someone just spent $236,000,000 on a painting. Here’s why it matters for your wallet.

The WSJ just reported the highest price ever paid for modern art at auction.

While equities, gold, bitcoin hover near highs, the art market is showing signs of early recovery after one of the longest downturns since the 1990s.

Here’s where it gets interesting→

Each investing environment is unique, but after the dot com crash, contemporary and post-war art grew ~24% a year for a decade, and after 2008, it grew ~11% annually for 12 years.*

Overall, the segment has outpaced the S&P by 15 percent with near-zero correlation from 1995 to 2025.

Now, Masterworks lets you invest in shares of artworks featuring legends like Banksy, Basquiat, and Picasso. Since 2019, investors have deployed $1.25 billion across 500+ artworks.

Masterworks has sold 25 works with net annualized returns like 14.6%, 17.6%, and 17.8%.

Shares can sell quickly, but my subscribers skip the waitlist:

*Per Masterworks data. Investing involves risk. Past performance not indicative of future returns. Important Reg A disclosures: masterworks.com/cd

🚀 Your Algo Edge Just Leveled Up — Premium Plans Are Here!🚀

A year in, our Starter, Pro, and Elite Quant Plans are crushing it—members are live-trading bots and booking 1-on-1 wins. Now with annual + lifetime deals for max savings.

Every premium member gets: Full code from every article Private GitHub repos + templates 3–5 deep-dive paid articles/mo Early access + live strategy teardowns

Pick your edge:

  • Starter (€20/mo) → 1 paid article + public repos

  • Builder (€30/mo) → Full code + private repos (most popular)

  • Master (€50/mo) → Two 1-on-1 calls + custom bot built for you

Best deals: 📅 Annual: 2 months FREE 🔒 Lifetime: Own it forever + exclusive perks

First 50 annual/lifetime signups get a free 15-min audit. Don’t wait—the market won’t.

— AlgoEdge Insights Team

Financial markets are dynamic, with prices constantly moving. This activity creates a complex picture, often filled with “noise” — minor fluctuations that can obscure the underlying direction and significant turning points. Technical analysts use various tools to filter this noise and gain clarity. The Zig Zag indicator is one such tool, designed to highlight significant price swings by ignoring smaller movements.

This article delves into a Python script that automates the process of applying the Zig Zag indicator. We’ll explore how it uses libraries like yfinance, pandas, numpy, and mplfinance to fetch data, calculate Zig Zag points, and plot them onto a clear candlestick chart. We will break down the code with snippets and explain the Zig Zag indicator conceptually and its practical applications.

The full end-to-end workflow is available in a Google Colab notebook, exclusively for paid subscribers of my newsletter. Paid subscribers also gain access to the complete article, including the full code snippet in the Google Colab notebook, which is accessible below the paywall at the end of the article. Subscribe now to unlock these benefits

1. Understanding the Zig Zag Indicator

What it is: The Zig Zag indicator is primarily a trend filtering tool. It doesn’t predict future prices but rather helps simplify past price action by connecting significant peaks (highs) and troughs (lows) on a chart.

How it works (Conceptually): Imagine drawing lines on a price chart, but only changing direction when the price moves significantly against the current trend. The Zig Zag indicator does this by establishing a minimum price movement threshold (often a percentage, but sometimes fixed points).

  • It identifies potential peaks and troughs as the price moves.

  • A potential peak is only confirmed after the price drops by more than the predefined threshold from that peak.

  • A potential trough is only confirmed after the price rises by more than the predefined threshold from that trough.

  • It only plots these confirmed, significant turning points, effectively filtering out price movements smaller than the threshold.

Key Parameter: The reversal threshold (represented as sigma in our script) is crucial.

  • A small threshold (e.g., 1%) will make the indicator very sensitive, resulting in many zig-zags, capturing smaller swings but potentially including more noise.

  • A large threshold (e.g., 10%) will filter aggressively, showing only major trend reversals and ignoring moderate swings.

Visual Representation: Typically, it’s drawn as a series of straight lines connecting alternating, confirmed peaks and troughs directly on the price chart. Our script focuses on marking the peak/trough points themselves.

2. Why Use the Zig Zag Indicator? (Usefulness)

The Zig Zag indicator is valuable for several analytical purposes:

  • Noise Reduction: Its primary benefit is simplifying complex price action, making it easier to discern the underlying trend by filtering out minor, insignificant fluctuations.

  • Identifying Support & Resistance: The troughs (bottoms) identified often correspond to significant support levels where buying interest previously emerged. The peaks (tops) often indicate resistance levels where selling pressure overcame buying.

  • Clearer Trend Definition: By showing sequences of higher highs and higher lows (uptrend) or lower highs and lower lows (downtrend) based on significant movements, it provides a cleaner picture of the dominant trend.

  • Chart Pattern Identification: The simplified lines or points can make classic chart patterns like Head and Shoulders, Double/Triple Tops and Bottoms, Flags, and Pennants much easier to spot.

  • Measuring Swing Magnitude: Allows analysts to easily measure the price distance or percentage change between significant market turns (the length of the “zig” or “zag”).

  • Aid for Elliott Wave Analysis: Some practitioners use Zig Zag to help identify potential wave counts according to Elliott Wave theory, as it simplifies the wave structure.

Important Note: The Zig Zag indicator is based on past data and requires a certain amount of price movement after a peak or trough to confirm it. Therefore, the most recent line segment or point is subject to change (repaint) until confirmed by a sufficient reversal. It’s an analytical tool, not typically used for generating direct entry/exit signals in isolation.

3. The Python Implementation: A Code Walkthrough

Let’s break down how the provided Python script implements the Zig Zag calculation and visualization.

3.1 Setting Up the Environment

The script starts by importing the necessary libraries and setting up basic logging:

Python

#!/usr/bin/env python3
import logging
from typing import List, Tuple, Optional # For type hinting

import matplotlib.pyplot as plt # Base plotting library
import mplfinance as mpf        # Financial charting
import numpy as np             # Numerical operations (arrays)
import pandas as pd            # Data manipulation (DataFrames)
import yfinance as yf          # Downloading market data
# --- Setup logging ---
logging.basicConfig(
    level=logging.INFO, # Set minimum level of messages to show
    format='[%(asctime)s] %(levelname)s: %(message)s' # Message format
)

This setup ensures all required tools are available and provides informative output during execution.

3.2 Fetching Market Data

The script fetches historical OHLC (Open, High, Low, Close) data using yfinance within the main execution block:

Python

if __name__ == '__main__':
    # --- Configuration for the run ---
    TICKER = 'BTC-USD'  # Example: Bitcoin USD
    PERIOD = '1mo'     # Example: Data for the last 1 month
                         # Other options: '1y', '6mo', '5d', 'max', etc.
    SIGMA = 0.03       # Example: 3% reversal threshold for Zig Zag

# --- Download Data ---
    logging.info(f"Downloading {TICKER} data for period: {PERIOD}")
    try:
        # Download data, progress=False hides the download status bar
        data = yf.download(TICKER, period=PERIOD, progress=False)
        if data.empty:
            raise ValueError(f"No data downloaded for {TICKER}.")
        # yfinance sometimes returns multi-level columns, flatten them
        # data.columns = data.columns.droplevel(1) # Adjusted to be optional/checked
        if isinstance(data.columns, pd.MultiIndex):
             data.columns = data.columns.droplevel(1)
        logging.info(f"Data downloaded successfully. Shape: {data.shape}")
    except Exception as e:
        logging.exception(f"Failed to download data for {TICKER}: {e}")
        exit() # Stop script if data download fails
    # ... (Calculation and Plotting calls follow) ...

Here, TICKER, PERIOD, and the crucial SIGMA are defined. The yf.download function retrieves the data into a pandasDataFrame, with basic error handling included.

3.3 The Core Calculation: find_zigzag_extremes

This function contains the algorithm described conceptually above.

  • Function Signature: Defines the inputs and output type.

  • Python

    def find_zigzag_extremes(
        high: np.ndarray,
        low: np.ndarray,
        close: np.ndarray,
        sigma: float
    ) -> pd.DataFrame:
        """ Docstring explaining function... """
        if len(close) < 2: # Need at least two points
             return pd.DataFrame(columns=['price', 'type', 'confirmation_idx'])
        # ... rest of the function ...
  • Initialization: Determines the initial trend based on the first two data points.

  • Python

    # Inside find_zigzag_extremes:
    # State variables initialization...
    last_extreme_type = 0
    up_trend = None
    # ... other variables ...
    
    # Initialize based on first two points trend
    if close[1] > close[0]:
        up_trend = True
        last_extreme_type = -1 # Implied previous bottom
        candidate_extreme_price = high[0]
        candidate_extreme_idx = 0
        last_confirmed_extreme_price = low[0] # Record initial point data
        last_confirmed_extreme_idx = 0
    else:
        up_trend = False
        last_extreme_type = 1 # Implied previous top
        candidate_extreme_price = low[0]
        candidate_extreme_idx = 0
        last_confirmed_extreme_price = high[0] # Record initial point data
        last_confirmed_extreme_idx = 0
  • Core Reversal Logic: The loop iterates through prices, updating candidates and checking for confirmations using sigma.

  • Python

    # Inside the loop `for i in range(1, len(close)):`
    
    # Example: Checking for Top Confirmation
    if up_trend: # Looking for a top
        if high[i] >= candidate_extreme_price:
            # New higher high found, update candidate top
            candidate_extreme_price = high[i]
            candidate_extreme_idx = i
        elif close[i] < candidate_extreme_price * (1 - sigma): # Price reversed > sigma%
            # Price reversed significantly, confirm the candidate top
            if last_extreme_type != 1: # Avoid confirming consecutive tops
                logging.debug(f"Idx {i}: Top confirmed: Idx={candidate_extreme_idx}, Price={candidate_extreme_price:.2f}...")
                extremes_data.append((candidate_extreme_idx, candidate_extreme_price, 1, i)) # Store confirmed top (type 1)
                last_extreme_type = 1
                last_confirmed_extreme_price = candidate_extreme_price
                last_confirmed_extreme_idx = candidate_extreme_idx
                # Start looking for a bottom
                up_trend = False
                candidate_extreme_price = low[i] # Initial candidate bottom
                candidate_extreme_idx = i
    else: # Looking for a bottom
        # Similar logic for finding lower lows and checking for upward reversal:
        # if low[i] <= candidate_extreme_price: ...
        # elif close[i] > candidate_extreme_price * (1 + sigma): ...
        #     if last_extreme_type != -1: ... # Confirm bottom (type -1)
            # ... update state, switch to looking for top ...
  • This section directly translates the Zig Zag concept: update the potential extreme until price reverses by more than sigma percent, then confirm and switch direction.

  • Output DataFrame: Converts the collected list of extremes into a structured pandas.DataFrame.

  • Python

    # After the loop in find_zigzag_extremes:
    if not extremes_data:
        logging.warning("No significant extremes found...")
        return pd.DataFrame(columns=['price', 'type', 'confirmation_idx'])
    
    # Create DataFrame
    extremes_df = pd.DataFrame(
        extremes_data,
        columns=['extreme_idx', 'price', 'type', 'confirmation_idx']
    )
    extremes_df = extremes_df.set_index('extreme_idx') # Use bar number as index
    extremes_df = extremes_df.sort_index()
    # Remove duplicates if any (unlikely but safe)
    extremes_df = extremes_df[~extremes_df.index.duplicated(keep='last')]
    
    return extremes_df

3.4 Plotting the Results: plot_zigzag

This function visualizes the OHLC data and overlays the calculated Zig Zag points.

  • Preparing Marker Data: It creates lists aligned with the main OHLC data, placing the price (slightly offset) at the index of each extreme and NaN elsewhere.

  • Python

    # Inside plot_zigzag:
    tops_plot = [np.nan] * len(ohlc_data)
    bottoms_plot = [np.nan] * len(ohlc_data)
    marker_offset_factor = 0.01 # Small offset for visibility
    
    for idx, row in extremes.iterrows(): # Loop through confirmed extremes
         if idx < len(ohlc_data): # Check index bounds
             if row['type'] == 1: # Top
                 # Place marker slightly above the high
                 tops_plot[idx] = ohlc_data['High'].iloc[idx] * (1 + marker_offset_factor)
             elif row['type'] == -1: # Bottom
                 # Place marker slightly below the low
                 bottoms_plot[idx] = ohlc_data['Low'].iloc[idx] * (1 - marker_offset_factor)
  • Defining Added Plots: mplfinance.make_addplot defines how to draw the markers.

  • Python

    # Inside plot_zigzag, after preparing tops_plot and bottoms_plot:
    apds = [ # List of addplot dictionaries
        mpf.make_addplot(tops_plot, type='scatter', marker='v', markersize=50, color='green', panel=0), # Tops
        mpf.make_addplot(bottoms_plot, type='scatter', marker='^', markersize=50, color='red', panel=0)   # Bottoms
    ]
  • Creating the Final Chart: mplfinance.plot draws the main chart and overlays the markers.

  • Python

    # End of plot_zigzag function:
    mpf.plot(ohlc_data, # Main OHLC data
             type='candle', # Candlestick chart
             style='yahoo', # Visual style
             title=title,
             ylabel='Price',
             volume='Volume' in ohlc_data.columns, # Show volume panel if data exists
             addplot=apds, # Overlay the scatter markers for extremes
             figsize=(10, 6) # Suggested figure size
            )

3.5 Running the Script

The if __name__ == '__main__': block ties everything together:

Python

if __name__ == '__main__':
    # --- Configuration ---
    TICKER = 'BTC-USD'
    PERIOD = '1mo'
    SIGMA = 0.03

    # --- Download Data ---
    # (Code shown previously)
    data = yf.download(...)

    # --- Calculate Extremes ---
    logging.info(f"Calculating Zigzag extremes with sigma={SIGMA:.2%}")
    extremes_df = find_zigzag_extremes(
        data['High'].to_numpy(), # Pass price arrays
        data['Low'].to_numpy(),
        data['Close'].to_numpy(),
        SIGMA
    )

    # --- Plot Results ---
    if not extremes_df.empty:
        logging.info(f"Found {len(extremes_df)} extremes.")
        logging.info("Generating plot...")
        plot_title = f"{TICKER} Price with Zigzag Extremes (Sigma={SIGMA:.1%})"
        plot_zigzag(data, extremes_df, plot_title) # Call plotting function
        logging.info("Plot generation complete.")
    else:
        logging.warning("No extremes found to plot.")
        # Optionally plot just price data
        mpf.plot(data, type='candle', title=f"{TICKER} Price Data (No Extremes Found)", ...)

This structure makes the script runnable, executing the download, calculation, and plotting steps sequentially.

4. Putting It Together: Example Interpretation

If you run this script with TICKER='BTC-USD', PERIOD='1mo', and SIGMA=0.03, it will…

logo

Subscribe to our premium content to read the rest.

Become a paying subscriber to get access to this post and other subscriber-only content.

Upgrade

Keep Reading