ATR and Supertrend

Chapter 72 already introduced ATR for position sizing. This chapter revisits it as a standalone indicator and builds Supertrend — one of the most widely used ATR-based trend/trailing-stop indicators on Indian retail platforms.

def atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
    high, low, close = df["high"], df["low"], df["close"]
    prev_close = close.shift(1)
    tr = pd.concat([high - low, (high - prev_close).abs(), (low - prev_close).abs()], axis=1).max(axis=1)
    return tr.ewm(alpha=1/period, adjust=False).mean()   # Wilder's smoothing, matches chapter 103's RSI convention

Note: chapter 72's version used a plain rolling mean; this Wilder-smoothed version is what most charting platforms (including Kite's own charts) actually use — prefer this version going forward for consistency with what you'll visually cross-check against.

Supertrend

def supertrend(df: pd.DataFrame, period: int = 10, multiplier: float = 3.0) -> pd.DataFrame:
    hl2 = (df["high"] + df["low"]) / 2
    atr_val = atr(df, period)

    upper_band = hl2 + multiplier * atr_val
    lower_band = hl2 - multiplier * atr_val

    supertrend_line = pd.Series(index=df.index, dtype=float)
    direction = pd.Series(index=df.index, dtype=int)

    for i in range(len(df)):
        if i == 0:
            supertrend_line.iloc[i] = upper_band.iloc[i]
            direction.iloc[i] = -1
            continue

        prev_st = supertrend_line.iloc[i-1]
        close = df["close"].iloc[i]

        curr_upper = upper_band.iloc[i] if upper_band.iloc[i] < prev_st or df["close"].iloc[i-1] > prev_st else prev_st
        curr_lower = lower_band.iloc[i] if lower_band.iloc[i] > prev_st or df["close"].iloc[i-1] < prev_st else prev_st

        if direction.iloc[i-1] == -1 and close > curr_upper:
            direction.iloc[i] = 1
        elif direction.iloc[i-1] == 1 and close < curr_lower:
            direction.iloc[i] = -1
        else:
            direction.iloc[i] = direction.iloc[i-1]

        supertrend_line.iloc[i] = curr_lower if direction.iloc[i] == 1 else curr_upper

    return pd.DataFrame({"supertrend": supertrend_line, "direction": direction}, index=df.index)

This is inherently a loop-based (event-driven, chapter 81) calculation — Supertrend's bands depend on their own previous value, which resists pure vectorization. For large universes, consider numba-accelerating this loop if performance matters (a full NIFTY 500 daily scan runs fine without it; a large intraday multi-instrument scan may not).

Using it as both a trend filter and a trailing stop

def supertrend_signal(st_df: pd.DataFrame) -> pd.Series:
    return st_df["direction"]   # already -1/1, directly usable as a trend signal

def supertrend_as_trailing_stop(st_df: pd.DataFrame, side: str) -> pd.Series:
    """Use the supertrend line itself as a dynamic stop-loss level, similar to chapter 76's ATR trail."""
    return st_df["supertrend"]

Supertrend is popular precisely because it does double duty — direction signal and trailing stop level in one indicator — but it lags in choppy markets like any ATR-based trend follower; pair with ADX (chapter 106) to avoid trading its whipsaws during genuinely range-bound stretches.

Next: 108 — VWAP