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