Volatility 5-Min Donchian Channel Breakout

đŸĻŠ Premium Script

Strategy Overview: The Volatility 5-Min Donchian Channel Breakout is a pure price-action volatility system designed to capture explosive intraday momentum. It completely bypasses lagging moving averages by utilizing a 20-period lookback window to map the absolute highest highs and lowest lows, dynamically plotting precise breakout thresholds. When the Nifty decisively closes above the upper Donchian band, the algorithm fires a long signal to ride the breakout surge. Conversely, a breakdown below the lower channel boundary triggers a short position, capitalizing on downward cascades and support failures . Unlike pure continuous-reversal systems, this algorithm integrates an asymmetric 1:2 risk-reward architecture—enforcing strict 50-point stop losses and 100-point take-profit targets. To ensure statistical integrity, the code utilizes a shift mechanism (shift(1)) to calculate the exact previous highs/lows, eliminating forward-looking data leakage. If price action aggressively fades a breakout, a built-in reversal protocol will seamlessly flip the directional bias. By stripping away oscillating indicators and focusing strictly on raw market structure, it provides an unfiltered lens into true supply and demand dynamics. As with all our intraday models, a hard 15:15 square-off sequence neutralizes all active positions to immunize your capital from unpredictable overnight gap risk.

📈 EXPECTED WIN RATE
40.68%
📉 MAX DRAWDOWN
677 Pts
💰 TOTAL PNL
+6,014 Pts
🔄 TOTAL TRADES
3717
🏆 TOTAL WINS
1512
!
Important This algorithm strictly relies on raw price action by utilizing Pandas rolling().max().shift(1) functions to dynamically map support and resistance channels, preventing forward-looking data leakage during backtesting.
â„šī¸
Disclaimer These results are generated based on automated backtesting performed using Python code and algorithms. Actual results may vary, and manual backtesting outcomes could differ due to varying assumptions, data interpretation, and market conditions. This information is provided for educational and informational purposes only and should not be considered as financial advice. Before making any trading or investment decisions, please consult with a qualified financial advisor or professional.

1. Imports and Configuration

Summary: This section imports required libraries and defines the core backtest parameters. It sets the 5-minute timeframe, establishes a 20-period lookback for the Donchian Channels, enforces a 15:15 intraday exit time, and sets a strict 50-point stop loss alongside a 100-point target.

python script Copy Code
import pandas as pd
import numpy as np
import datetime as dt

# ===================== CONFIG =====================
FILE_PATH = "Index_1_minute.csv"  
DAYFIRST = True                    
EXIT_TIME = dt.time(15, 15)        
SLIPPAGE = 0.5                     

# Strategy params
TIMEFRAME = "5min"   
DONCHIAN_PERIOD = 20               # Lookback period for Highest High / Lowest Low

# Risk Management
STOP_LOSS_POINTS = 50.0            
TARGET_POINTS = 100.0              
# ==================================================

2. Data Loading and Preparation

Summary: This segment safely handles the ingestion of historical CSV data. It converts raw date strings into standard datetime objects, coerces price information to numeric formats, removes nulls, and accurately resamples the minute data into structured 5-minute OHLC candles.

python script Copy Code
# ----------------- LOAD & PREP --------------------
print("Loading and preparing data...")
df = pd.read_csv(FILE_PATH)
df.columns = [c.strip().lower() for c in df.columns]
required = {"date","open","high","low","close"}
missing = required - set(df.columns)
if missing:
    raise ValueError(f"CSV missing columns: {missing}. Need exactly {sorted(required)}")

df["dt"] = pd.to_datetime(df["date"], dayfirst=DAYFIRST)
df = df.sort_values("dt").reset_index(drop=True)

for c in ["open","high","low","close"]:
    df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=["open","high","low","close","dt"]).copy()

# Resampling
df = df.set_index("dt").resample(TIMEFRAME).agg({
    "open": "first",
    "high": "max",
    "low": "min",
    "close": "last"
}).dropna().reset_index()

df["d"] = df["dt"].dt.date
df["t"] = df["dt"].dt.time

3. Donchian Channel Indicator Logic

Summary: This is where the core indicator logic resides. By utilizing Pandas’ rolling functions, it dynamically maps the absolute highest high and lowest low of the previous 20 periods. The .shift(1) method is critical here to prevent forward-looking data leakage during backtesting.

python script Copy Code
# ----------------- INDICATORS --------------------
print("Calculating Donchian Channels (Price Action)...")

# Calculate rolling highest high and lowest low of the PREVIOUS N periods

df["donchian_high"] = df["high"].rolling(window=DONCHIAN_PERIOD).max().shift(1)
df["donchian_low"] = df["low"].rolling(window=DONCHIAN_PERIOD).min().shift(1)

4. Utility Functions

Summary: Provides clean helper logic for backtesting. The stats function aggregates all executed trades into high-level metrics like Win Rate and Average PnL, whilst close_out universally structures point calculation for both long and short positions upon trade exit.

python script Copy Code
# -------------- UTILITIES ----------------
def stats(trades):
    if not trades:
        return {"trades":0,"wins":0,"win_rate":0.0,"avg_pnl":0.0,"total_pnl":0.0}
    pnl = np.array([x["pnl"] for x in trades])
    wins = (pnl > 0).sum()
    return {
        "trades": len(trades),
        "wins": int(wins),
        "win_rate": round(100*wins/len(trades), 2),
        "avg_pnl": round(pnl.mean(), 2),
        "total_pnl": round(pnl.sum(), 2),
    }

def close_out(direction, entry, row_close):
    return (row_close - entry) if direction=="long" else (entry - row_close)

5. The Core Backtest Engine

Summary: The central logic loop. It executes trades sequentially based on Donchian band breakouts. In addition to monitoring hard Stop-Loss/Take-Profit triggers, it facilitates an automatic 15:15 market-close liquidation. Dynamic positional flips occur instantly if price reverses through opposite channel extremes.

python script Copy Code
# -------------- STRATEGY: DONCHIAN BREAKOUT --------------
def backtest_donchian_breakout(df):
    print("Running backtest loop...")
    trades = []
    in_trade = False
    direction = None
    entry = None
    entry_time = None

    for _, r in df.iterrows():
        # 1. INTRADAY TIME EXIT
        if r["t"] >= EXIT_TIME:
            if in_trade:
                pnl = close_out(direction, entry, r["close"]) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"DONCHIAN", "dir":direction, "entry":entry,
                    "exit":r["close"], "pnl":pnl, "outcome":"TIME_EXIT"
                })
                in_trade = False
            continue

        if pd.isna(r["donchian_high"]) or pd.isna(r["donchian_low"]):
            continue

        # 2. SL / TP LOGIC
        if in_trade:
            sl_hit = False
            tp_hit = False
            exit_price = None
            outcome = None

            # Check conservative High/Low hits within the current candle
            if direction == "long":
                if r["low"] <= entry - STOP_LOSS_POINTS:
                    sl_hit = True
                    exit_price = entry - STOP_LOSS_POINTS
                    outcome = "STOP_LOSS"
                elif r["high"] >= entry + TARGET_POINTS:
                    tp_hit = True
                    exit_price = entry + TARGET_POINTS
                    outcome = "TARGET"
            elif direction == "short":
                if r["high"] >= entry + STOP_LOSS_POINTS:
                    sl_hit = True
                    exit_price = entry + STOP_LOSS_POINTS
                    outcome = "STOP_LOSS"
                elif r["low"] <= entry - TARGET_POINTS:
                    tp_hit = True
                    exit_price = entry - TARGET_POINTS
                    outcome = "TARGET"

            # If stopped out or target reached, close the trade
            if sl_hit or tp_hit:
                pnl = close_out(direction, entry, exit_price) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"DONCHIAN", "dir":direction, "entry":entry,
                    "exit":exit_price, "pnl":pnl, "outcome":outcome
                })
                in_trade = False
                direction = None
                entry = None
                entry_time = None
                # Skip entry logic on the same candle we hit SL/TP
                continue

        # Signal Logic
        breakout_up = r["close"] > r["donchian_high"]
        breakout_down = r["close"] < r["donchian_low"]

        # 3. ENTRY LOGIC
        if not in_trade:
            if breakout_up:
                direction = "long"
                entry = r["close"]
                entry_time = r["dt"]
                in_trade = True
            elif breakout_down:
                direction = "short"
                entry = r["close"]
                entry_time = r["dt"]
                in_trade = True

        # 4. REVERSAL LOGIC
        else:
            if direction=="long" and breakout_down:
                exit_price = r["close"]
                pnl = (exit_price - entry) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"DONCHIAN", "dir":"long", "entry":entry,
                    "exit":exit_price, "pnl":pnl, "outcome":"REVERSE"
                })
                # Reverse to short
                direction = "short"
                entry = r["close"]
                entry_time = r["dt"]

            elif direction=="short" and breakout_up:
                exit_price = r["close"]
                pnl = (entry - exit_price) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"DONCHIAN", "dir":"short", "entry":entry,
                    "exit":exit_price, "pnl":pnl, "outcome":"REVERSE"
                })
                # Reverse to long
                direction = "long"
                entry = r["close"]
                entry_time = r["dt"]

    return trades

6. Execution and Reporting

Summary: Acts as the system’s runtime orchestrator. It triggers the backtest engine, displays terminal summaries, then structures the detailed timeline of trades into an Excel workbook complete with dedicated Weekly, Monthly, and Drawdown tracking tabs.

python script Copy Code
# -------------- EXECUTION & REPORTING --------------
donchian_trades = backtest_donchian_breakout(df)
summary = {"DONCHIAN": stats(donchian_trades)}

print(f"✅ Backtest complete ({TIMEFRAME} candles)")
for name, s in summary.items():
    print(f"\n{name} -> Trades: {s['trades']}, Wins: {s['wins']}, WinRate: {s['win_rate']}%, "
          f"AvgPnL: {s['avg_pnl']}, TotalPnL: {s['total_pnl']}")

# Write results to Excel
output_file = "donchian_breakout_results.xlsx"
print(f"\nSaving results to {output_file}...")

with pd.ExcelWriter(output_file, engine="xlsxwriter") as writer:
    if donchian_trades:
        trades_df = pd.DataFrame(donchian_trades)
        trades_df.to_excel(writer, sheet_name="DONCHIAN", index=False)

        trades_df["date_dt"] = pd.to_datetime(trades_df["date"])
        trades_df["week"] = trades_df["date_dt"].dt.to_period("W-FRI")
        weekly = trades_df.groupby("week", as_index=False)["pnl"].sum()
        weekly.rename(columns={"pnl": "total_points"}, inplace=True)
        weekly["week"] = weekly["week"].astype(str)
        weekly.to_excel(writer, sheet_name="Weekly_Report", index=False)

        trades_df["month"] = trades_df["date_dt"].dt.to_period("M")
        monthly = trades_df.groupby("month", as_index=False)["pnl"].sum()
        monthly.rename(columns={"pnl": "total_points"}, inplace=True)
        monthly["month"] = monthly["month"].astype(str)
        monthly.to_excel(writer, sheet_name="Monthly_Report", index=False)

        dd_rows = []
        for m, mdf in trades_df.groupby("month"):
            mdf = mdf.sort_values("exit_time")
            cum = mdf["pnl"].cumsum()
            max_dd = cum.min()
            dd_rows.append({"month": str(m), "max_drawdown": max_dd})
        dd_df = pd.DataFrame(dd_rows)
        dd_df.to_excel(writer, sheet_name="Drawdown_Report", index=False)

    pd.DataFrame([{"strategy": k, **v} for k, v in summary.items()]).to_excel(
        writer, sheet_name="Summary", index=False
    )

print("Process finished successfully.")