Notebooks/Dynamic Stop + Volatility Strategy
Signals·TA Strategies·Intermediate

Dynamic Stop + Volatility Strategy

Set stop-loss distances dynamically based on ATR — wider stops in high-volatility regimes, tighter in compressed markets.

ATRstopsvolatility

Dynamic Stop Volatility Strategy

1. Strategy Overview

Static stop-loss mechanisms, characterized by fixed pip or percentage distances, are inherently miscalibrated. During periods of elevated market volatility, these stops tend to be excessively restrictive, leading to premature activation by normal price fluctuations. Conversely, during periods of low volatility, they may be too wide, exposing capital to undue risk.

This strategy employs Average True Range (ATR)-based dynamic stops, which adapt automatically to prevailing market conditions. The calculation for stop and target levels for long positions is defined as follows:

  • Stop (long) = current close − stop_mult × ATR
  • Target (long) = current close + target_mult × ATR

This strategy is designed to compute appropriate stop-loss and profit-target levels for each candle, serving as a complementary component to external entry signals. It does not generate entry signals independently. The stop_mult : target_mult ratio establishes the risk/reward profile. A common practice is a 2:3 ratio, implying a risk of 2 times the ATR to achieve a profit of 3 times the ATR per trade.

2. Environment Setup

2.1 Install Dependencies

Install the necessary Python libraries for data manipulation, numerical operations, and interactive plotting.

[1]
pip install pandas numpy plotly
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4)
Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

2.2 Import Libraries

Import the required modules, including pandas for data structures, numpy for numerical operations, and plotly.graph_objects for visualization. Warnings are suppressed for cleaner output.

[2]
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go

3. Data Generation

3.1 generate_data Function

The generate_data function creates a synthetic OHLCV (Open, High, Low, Close, Volume) dataset. This function simulates price movements and volume over a specified number of periods, suitable for strategy backtesting and demonstration purposes.

[3]
def generate_data(periods):
    start_date = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
    price_data = []; last_close = 42000
    for i in range(periods):
        open_price  = last_close + np.random.normal(0, last_close * 0.0005)
        close_price = open_price + np.random.normal(0, last_close * 0.005)
        body_high   = max(open_price, close_price)
        body_low    = min(open_price, close_price)
        high_price  = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        low_price   = min(body_low  - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        if high_price < low_price: high_price, low_price = low_price, high_price
        price_data.append({"open": max(1,int(open_price)), "high": max(1,int(high_price)),
                            "low":  max(1,int(low_price)),  "close": max(1,int(close_price))})
        last_close = close_price
    df = pd.DataFrame(price_data, index=datetime_index)
    df.index.name = "datetime"
    df["volume"] = np.random.uniform(100.0, 500.0, periods)
    df["datetime"] = df.index.to_series()
    return df.reset_index(drop=True)

3.2 Generate Sample OHLCV Data

Generate a DataFrame containing 500 periods of synthetic OHLCV data for demonstration purposes.

[4]
df = generate_data(500)

4. Strategy Implementation

4.1 dynamic_stop_volatility_strategy Function

The dynamic_stop_volatility_strategy function computes dynamic stop-loss and profit-target levels based on the Average True Range (ATR). It takes a DataFrame of OHLCV data and parameters for ATR window, stop multiplier, and target multiplier as input.

  • atr_window: Specifies the lookback period for calculating the ATR. A standard setting is 14 periods, aligning with the original Wilder ATR methodology.
  • stop_mult: A multiplier applied to the ATR to determine the stop-loss distance. A value of 2.0 positions the stop outside two standard ATR deviations, aiming to prevent premature activation by typical market noise.
  • target_mult: A multiplier applied to the ATR to determine the profit-target distance. A value of 3.0, when combined with a stop_mult of 2.0, results in a 1.5:1 reward-to-risk ratio. This ratio suggests the strategy can be profitable with a win rate exceeding 40%.
[9]
def dynamic_stop_volatility_strategy(
    df: pd.DataFrame,
    atr_window: int = 14,
    stop_mult: float = 2.0,
    target_mult: float = 3.0,
) -> pd.DataFrame:
    """
    Computes dynamic stop-loss and profit-target levels based on the Average True Range (ATR).

    Parameters:
    - df (pd.DataFrame): DataFrame containing OHLCV data with 'high', 'low', and 'close' columns.
    - atr_window (int): The lookback period for calculating the ATR. Default is 14.
    - stop_mult (float): Multiplier for ATR to determine the stop-loss distance. Default is 2.0.
    - target_mult (float): Multiplier for ATR to determine the profit-target distance. Default is 3.0.

    Returns:
    - pd.DataFrame: The input DataFrame with added columns for 'atr', 'stop_long', 'target_long',
      'stop_short', 'target_short', and 'rr_ratio'.
    """
    df = df.copy().sort_values("datetime", ignore_index=True)

    # Calculate True Range (TR) as the greatest of the following:
    # 1. Current High minus Current Low
    # 2. Absolute value of Current High minus Previous Close
    # 3. Absolute value of Current Low minus Previous Close
    tr = pd.concat([
        df["high"] - df["low"],
        (df["high"] - df["close"].shift(1)).abs(),
        (df["low"]  - df["close"].shift(1)).abs(),
    ], axis=1).max(axis=1)

    # Calculate Average True Range (ATR) by taking a rolling mean of TR
    df["atr"] = tr.rolling(atr_window).mean()

    # Calculate dynamic stop-loss and profit-target levels for long positions
    df["stop_long"] = df["close"] - stop_mult * df["atr"]
    df["target_long"] = df["close"] + target_mult * df["atr"]

    # Calculate dynamic stop-loss and profit-target levels for short positions
    df["stop_short"] = df["close"] + stop_mult * df["atr"]
    df["target_short"] = df["close"] - target_mult * df["atr"]

    # Calculate the reward-to-risk ratio
    df["rr_ratio"] = target_mult / stop_mult

    return df

4.2 Apply Strategy to Data

Apply the dynamic_stop_volatility_strategy function to the generated DataFrame df to compute dynamic stop-loss and profit-target levels. The atr_window is set to 14, stop_mult to 2.0, and target_mult to 3.0.

[6]
df_levels = dynamic_stop_volatility_strategy(df, atr_window=14, stop_mult=2.0, target_mult=3.0)

5. Results and Visualization

5.1 Display Dynamic Stop/Target Levels

Display the datetime, close price, atr, stop_long, target_long, and rr_ratio for the last 10 candles, rounded to two decimal places, to inspect the computed levels.

[7]
print("--- Dynamic Stop/Target Levels (last 10 candles) ---")
display(df_levels[["datetime","close","atr","stop_long","target_long","rr_ratio"]].tail(10).round(2))
--- Dynamic Stop/Target Levels (last 10 candles) ---
datetime close atr stop_long target_long rr_ratio
490 2024-01-01 08:10:00+00:00 39316 288.93 38738.14 40182.79 1.5
491 2024-01-01 08:11:00+00:00 39117 274.00 38569.00 39939.00 1.5
492 2024-01-01 08:12:00+00:00 39239 266.29 38706.43 40037.86 1.5
493 2024-01-01 08:13:00+00:00 39483 266.50 38950.00 40282.50 1.5
494 2024-01-01 08:14:00+00:00 39349 281.79 38785.43 40194.36 1.5
495 2024-01-01 08:15:00+00:00 39424 280.36 38863.29 40265.07 1.5
496 2024-01-01 08:16:00+00:00 39272 288.14 38695.71 40136.43 1.5
497 2024-01-01 08:17:00+00:00 39454 272.00 38910.00 40270.00 1.5
498 2024-01-01 08:18:00+00:00 39614 265.93 39082.14 40411.79 1.5
499 2024-01-01 08:19:00+00:00 39317 279.07 38758.86 40154.21 1.5

5.2 Visualize Dynamic ATR-Based Stop and Target Levels

Generate an interactive candlestick chart using Plotly, overlaying the calculated dynamic stop-loss and profit-target levels for long positions, as well as the ATR. This visualization aids in understanding how these levels adapt to price movements and volatility.

[8]
fig = go.FigureWidget(data=[go.Candlestick(
    x=df_levels["datetime"], open=df_levels["open"], high=df_levels["high"],
    low=df_levels["low"],    close=df_levels["close"], name="Price")])
fig.add_trace(go.Scatter(x=df_levels["datetime"], y=df_levels["stop_long"],
    mode="lines", name="Stop (Long)",   line=dict(color="red",   width=1, dash="dash")))
fig.add_trace(go.Scatter(x=df_levels["datetime"], y=df_levels["target_long"],
    mode="lines", name="Target (Long)", line=dict(color="green", width=1, dash="dash")))
fig.add_trace(go.Scatter(x=df_levels["datetime"], y=df_levels["atr"],
    mode="lines", name="ATR", line=dict(color="purple", width=1), yaxis="y2"))
fig.update_layout(title_text="Dynamic ATR-Based Stop and Target Levels",
    xaxis_rangeslider_visible=False, height=600, yaxis=dict(autorange=True),
    yaxis2=dict(title="ATR", overlaying="y", side="right"))
fig.show()
[ ]