In the world of crypto trading, high volatility is a double-edged sword. While it offers profit potential, it often precedes violent reversals. This article explores a systematic strategy designed to enter Ethereum (ETH) positions when the price is trending up but the “chaos” (volatility) is starting to settle—a concept known as Volatility Cooling.
Most traders chase breakouts during peak volatility. Our strategy does the opposite. It seeks a “quiet” entry into an existing trend. We look for three specific conditions:
Price Momentum: The current price is higher than it was \(n\) days ago.
Trend Confirmation: The price is above a long-term Moving Average.
Volatility Cooling: Volatility is high but decreasing relative to its recent past.
We use a Volatility Oscillator to measure the rate of change in standard deviation. If price is climbing while this oscillator drops, it suggests a healthy, sustainable trend rather than a blow-off top.
Python
# Volatility Oscillator calculation
vol = ret.rolling(vol_w).std()
vol_osc = (vol / vol.shift(vol_roc) - 1.0) * 100.0
# The "Cooling" Signal
vol_cooling = (vol_osc - vol_osc.shift(price_lb)) < 0
base_bull = price_up & vol_cooling
To protect capital, we don’t use a fixed percentage stop. Instead, we use the Average True Range (ATR). This adjusts the stop-loss distance based on the asset’s current “noise” level. As the price moves in our favor, the stop trails behind, locking in gains.
Python
# Trailing stop logic inside the loop
if pos == 1:
new_stop = price - float(atr.iloc[i]) * atr_m
# Only move the stop UP (never down)
stop = new_stop if np.isnan(stop) else max(stop, new_stop)
if float(l.iloc[i]) <= stop:
pos = 0 # Exit position
ex[i] = True
Using vectorbt, we can test hundreds of parameter
combinations (different window lengths for moving averages and
volatility) simultaneously. We filter for a minimum trade count to avoid
“fluke” results and rank the rest by their Sharpe
Ratio.
Python
# Running the backtest across all parameter combinations
pf = vbt.Portfolio.from_signals(
close=c,
entries=entries,
exits=exits,
fees=0.001,
freq="1D"
)
# Filtering for robustness
trade_count = pf.trades.count()
mask = trade_count >= 10
best_index = sharpe.where(mask).idxmax()
BEST PARAMETERS (by Sharpe, filtered) (LONG-ONLY)
ticker: ETH-USD | period: 1y | fees: 0.001 | min_trades: 10
vol_window: 14
vol_roc_period: 5
price_lb: 10
trend_window: 30
atr_window: 7
atr_mult: 1.0
Trades: 21
Sharpe: 1.831
TotalRet: 74.52%
By shifting the data (df.shift(1)), we ensure there is
no lookahead bias—the code only “sees” information
available at the time of the trade. The combination of trend-following
and volatility-filtering creates a smoother equity curve than simply
“buying and holding” through ETH’s massive drawdowns.