← Back to Home
Trading Volatility Cooling - A Trend-Following Strategy in Python

Trading Volatility Cooling - A Trend-Following Strategy in Python

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.


The Core Thesis

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:

  1. Price Momentum: The current price is higher than it was \(n\) days ago.

  2. Trend Confirmation: The price is above a long-term Moving Average.

  3. Volatility Cooling: Volatility is high but decreasing relative to its recent past.


1. Defining the “Cooling” Logic

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

2. Risk Management with ATR Trailing Stops

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

3. Mass Backtesting & Optimization

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()

4. Results

Pasted image 20260224035207.png
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%

Why This Works

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.