← Back to Home
Breakout Strategy with OBV and ATR Confirmation

Breakout Strategy with OBV and ATR Confirmation

Breakout trading is a popular strategy aiming to capitalize on significant price movements when an asset’s price breaks through a defined support or resistance level. However, not all breakouts lead to sustained trends; many turn out to be “false breakouts” or “fakes.” To improve the reliability of breakout signals, traders often incorporate secondary indicators related to volume and volatility.

This article explores a specific breakout strategy applied to Ethereum (ETH-USD) daily data from January 1, 2020, to April 30, 2025. The strategy identifies breakouts using recent price highs/lows and confirms them with two key indicators:

  1. On-Balance Volume (OBV): A momentum indicator relating price and volume, suggesting accumulation (buying pressure) or distribution (selling pressure).
  2. Average True Range (ATR): A measure of market volatility.

The goal is to generate buy (UpBreak) or sell/short (DownBreak) signals only when the price breakout is confirmed by corresponding OBV momentum and occurs during a period of elevated volatility (ATR significantly above its recent average). We’ll walk through the Python implementation using yfinance, pandas, talib, and matplotlib, and analyze the results.

The Strategy Logic

The core idea is to filter potential price breakouts for higher probability trades:

Implementation in Python

1. Data Acquisition and Preparation

First, we download the historical daily price data for ETH-USD using the yfinance library and handle potential multi-level columns.

Python

import numpy as np
import pandas as pd
import yfinance as yf
import talib
import matplotlib.pyplot as plt

# --- Configuration ---
ticker = 'ETH-USD'
start_date = '2020-01-01'
end_date = '2025-04-30' # Data up to Apr 30, 2025 used in the results
atr_period = 14
lookback   = 20      # Lookback window for highs/lows
atr_thresh = 1.2     # ATR multiplier threshold
horizon    = 30      # Forward return horizon (days)

# 1. Download and Prep Data
print(f"Downloading data for {ticker}...")
data = yf.download(ticker, start=start_date, end=end_date, progress=False)

if isinstance(data.columns, pd.MultiIndex):
    data.columns = data.columns.droplevel(1)

print(f"Data downloaded. Shape: {data.shape}")
# Basic cleaning (ensure essential columns exist, drop NaNs)
required_cols = ['High', 'Low', 'Close', 'Volume']
if not all(col in data.columns for col in required_cols):
     raise ValueError("Missing required columns")
data.dropna(subset=required_cols, inplace=True)

The execution yielded:

Downloading data for ETH-USD…

Data downloaded. Shape: (1946, 5)

2. Indicator Calculation

We use the popular TA-Lib library to compute OBV and ATR.

Python

# 2. Compute Indicators
data['OBV'] = talib.OBV(data['Close'], data['Volume'])
data['ATR'] = talib.ATR(data['High'], data['Low'], data['Close'], timeperiod=atr_period)

# Drop initial rows where indicators couldn't be calculated
data.dropna(inplace=True)

3. Defining Breakout Conditions

We calculate the rolling maximum/minimum price and OBV over the lookback window. We use .shift(1) to compare the current bar’s values against the highs/lows established by the preceding lookback period. We also calculate the rolling mean of ATR.

Python

# 3. Compute Rolling Highs/Lows/Means for Breakout Conditions
data['PriceHigh'] = data['Close'].shift(1).rolling(lookback).max()
data['OBVHigh']   = data['OBV'].shift(1).rolling(lookback).max()
data['PriceLow']  = data['Close'].shift(1).rolling(lookback).min()
data['OBVLow']    = data['OBV'].shift(1).rolling(lookback).min()
data['ATRmean']   = data['ATR'].shift(1).rolling(lookback).mean()

# Drop rows with NaNs from rolling calculations
data.dropna(inplace=True)

4. Generating Signals

We combine the price, OBV, and ATR conditions using boolean logic to create the UpBreak and DownBreak signals (1 if conditions met, 0 otherwise).

Python

# 4. Generate Breakout Signals
# Up Breakout: Close > Price High, OBV > OBV High, ATR > ATR Mean * Threshold
data['UpBreak']   = (
    (data['Close'] > data['PriceHigh']) &
    (data['OBV']   > data['OBVHigh'])   &
    (data['ATR']   > data['ATRmean'] * atr_thresh)
).astype(int)

# Down Breakout: Close < Price Low, OBV < OBV Low, ATR > ATR Mean * Threshold
data['DownBreak'] = (
    (data['Close'] < data['PriceLow'])  &
    (data['OBV']   < data['OBVLow'])    &
    (data['ATR']   > data['ATRmean'] * atr_thresh)
).astype(int)

5. Visualization

Plotting the price, OBV, and ATR along with the generated signals is crucial for visually inspecting the strategy’s behavior. The code generates three stacked plots:

Pasted image 20250501021458.png ### Performance Evaluation

To assess the predictive power of these signals, we calculate the forward return over a fixed horizon (30 days in this analysis). This tells us the percentage change in price 30 days after a signal occurred.

Python

# 6. Compute Forward Returns for Performance Evaluation
data['FwdRet'] = data['Close'].shift(-horizon) / data['Close'] - 1

# 7. Evaluate Signal Performance (Win Rate & Expectancy)
up_signals   = data[data['UpBreak'] == 1].dropna(subset=['FwdRet'])
down_signals = data[data['DownBreak'] == 1].dropna(subset=['FwdRet'])
# Calculate return for short trades (profit if price goes down)
down_signals = down_signals.assign(ShortRet = -down_signals['FwdRet'])

# ... [Calculations for wins, win rates, average win/loss, expectancy] ...

Results: Win Rate & Expectancy

The analysis over the specified period yielded the following performance metrics based on a 30-day forward return:

Calculating signal performance...
Up-break signals:     56  →  Winners:    32  (57.1%)
Up-break expectancy per trade (30-day horizon): 6.38%
Down-break signals:   12  →  Winners:     9  (75.0%)
Down-break expectancy per trade (30-day horizon): 2.83%

These results suggest that, historically over this period and using this specific fixed horizon, both signal types had a positive expectancy, although the win rate and average outcome differed.

Overall Strategy Returns (Simplified)

To get a sense of the overall potential, we combined all trades and calculated aggregate returns. Important Caveat: These calculations are highly simplified. They sum the fixed 30-day forward returns (total_simple) or calculate the compounded product (total_compound) assuming each trade is independent and held for exactly 30 days. This does not represent a realistic portfolio simulation (which would need to handle overlapping trades, position sizing, compounding within trades, transaction costs, etc.).

Python

# 8. Calculate Overall Strategy Returns (Simplified)
up_trades   = up_signals.assign(Return=up_signals['FwdRet'], Type='Long')
down_trades = down_signals.assign(Return=down_signals['ShortRet'], Type='Short')
all_trades = pd.concat([up_trades, down_trades]).sort_index()

# ... [Calculations for total_simple and total_compound] ...

Results: Simplified Overall Performance

Calculating overall strategy return (simplified)...

--- Simplified Strategy Performance (30-day horizon) ---
Number of evaluated trades: 68
Simple sum total return:    391.46%
Compound total return:      179.57%
Average return per trade:    5.76%

--- Performance by Trade Type ---
        count      mean       sum       std
Type                                     
Long       56  0.063841  3.575118  0.306096
Short      12  0.028293  0.339521  0.171162

Analysis complete.

Conclusion

This analysis demonstrated how to implement and test a breakout trading strategy for ETH-USD using Python, confirming price breaks with OBV and filtering by ATR. The historical results from Jan 2020 to Apr 2025, based on a 30-day fixed forward return, showed positive expectancy for both long and short signals generated by this specific set of rules. Long signals were more frequent and had higher average returns, while short signals were less common but had a higher win rate.

It is crucial to remember that:

This framework provides a starting point for exploring quantitative breakout strategies. Further research could involve testing different parameters, applying the strategy to other assets or timeframes, incorporating risk management rules, and building a more robust event-driven backtesting system.