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:
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 core idea is to filter potential price breakouts for higher probability trades:
lookback
period (e.g., 20 days).lookback
period,
indicating volume supports the upward price move.lookback
low, suggesting volume confirms the selling
pressure.lookback
period. This helps filter out
breakouts happening during low-volatility “noise.”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:
UpBreak
signals and red downward arrows marking
DownBreak
signals. ### 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.
(1 + R1) * (1 + R2) * ... - 1
, was +179.57%. The
significant difference between simple and compound returns highlights
the impact of large winners/losers and the simplified nature of this
metric.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:
lookback
, atr_thresh
,
horizon
) were chosen arbitrarily for this example.
Optimization might yield different results.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.