Trading Indicators in Python: Moving Averages, RSI, MACD, Bollinger Bands, and ATR
You don't need twenty indicators in a bot. What actually survives in the code is usually fewer than five. This article covers those five — moving averages, RSI, MACD, Bollinger Bands, and ATR — as one line of meaning plus the Python that computes it.
⚠️ This article is for educational and informational purposes only. No indicator guarantees a profit, and any losses are your own responsibility.
All the code below assumes a pandas DataFrame named df with high, low, and close columns.
1. Moving Average (MA) — Which Way the Trend Points
The average price over the last N periods. Its job is to erase the jitter so you can see whether the flow is currently upward, and nothing more.
df["sma20"] = df["close"].rolling(20).mean() # simple moving average
df["ema20"] = df["close"].ewm(span=20).mean() # exponential, weights recent bars
The simple version (SMA) treats every period equally; the exponential version (EMA) weights recent periods more heavily. Want responsiveness, use EMA. Want stability, use SMA.
Common misreading: a moving average is not a forecasting tool, it's a smoothing tool. It always moves later than price does. The lag isn't a defect — it's the mechanism.
2. RSI — The Ratio of Recent Gains to Recent Losses
RSI (Relative Strength Index) is a 0–100 value expressing the ratio between how much price has risen and how much it has fallen recently. Above 70 is conventionally called overbought, below 30 oversold.
def rsi(close, period=14):
delta = close.diff() # change vs. previous bar
gain = delta.clip(lower=0) # size of up moves
loss = -delta.clip(upper=0) # size of down moves (positive)
# Wilder smoothing — the standard RSI definition
avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, 1e-9) # guard against divide-by-zero
return 100 - (100 / (1 + rs))
df["rsi14"] = rsi(df["close"])
Plenty of examples online compute this with rolling().mean(), which produces values subtly different from the original definition. If you want your numbers to line up with your charting software, use Wilder smoothing as above.
Common misreading: "RSI below 30 = buy signal" is false. In a strong downtrend, RSI will sit in the twenties for weeks. Counter-trend entries need a trend filter alongside them.
3. MACD — The Gap Between Two Moving Averages
A plot of the difference between a fast EMA and a slow EMA. Widening gap reads as a strengthening trend; narrowing gap as one running out of steam.
def macd(close, fast=12, slow=26, signal=9):
ema_fast = close.ewm(span=fast).mean()
ema_slow = close.ewm(span=slow).mean()
macd_line = ema_fast - ema_slow # MACD line
signal_line = macd_line.ewm(span=signal).mean()
hist = macd_line - signal_line # histogram
return macd_line, signal_line, hist
df["macd"], df["signal"], df["hist"] = macd(df["close"])
# Golden cross: below yesterday, above today
buy = (df["macd"] > df["signal"]) & (df["macd"].shift(1) <= df["signal"].shift(1))
Because MACD is built on moving averages, signals explode in a sideways market. In a range with no direction, crossovers fire continuously and all you accumulate is fees. That's exactly why people pair it with a volatility filter.
4. Bollinger Bands — How Far From the Mean Are We
Lines drawn two standard deviations above and below a moving average. They tell you how statistically stretched the current price is.
def bollinger(close, period=20, k=2):
mid = close.rolling(period).mean()
std = close.rolling(period).std()
return mid - k * std, mid, mid + k * std
df["bb_low"], df["bb_mid"], df["bb_up"] = bollinger(df["close"])
# Band width — narrow means volatility contraction (a squeeze)
df["bb_width"] = (df["bb_up"] - df["bb_low"]) / df["bb_mid"]
In practice the more useful reading isn't a touch of the upper or lower band but the band width. Large moves tend to follow extreme contraction, which makes width a solid entry-timing filter for a strategy.
5. ATR — How Much This Instrument Normally Swings
The most underrated indicator on the list. It says nothing about direction and only reports the size of the movement. You use it to size stops and positions.
def atr(df, period=14):
prev_close = df["close"].shift(1)
tr = pd.concat([
df["high"] - df["low"], # today's high-low range
(df["high"] - prev_close).abs(), # accounts for gap up
(df["low"] - prev_close).abs(), # accounts for gap down
], axis=1).max(axis=1)
return tr.ewm(alpha=1/period, adjust=False).mean()
df["atr14"] = atr(df)
# Set the stop in ATR terms (width adapts per instrument automatically)
entry = df["close"].iloc[-1]
stop = entry - 2 * df["atr14"].iloc[-1]
Use a fixed percentage like "3% stop" and an instrument that moves 5% a day will shake you out the moment you enter. An ATR-based stop adjusts its width to the character of the instrument on its own.
Principles for Using Indicators
- Don't stack indicators that do the same job — a MACD crossover and a moving-average crossover are effectively telling you the same thing twice.
- Combine one trend indicator with one volatility indicator — one for direction (MA/MACD), one for magnitude (ATR/band width). That's the practical pairing.
- Don't over-tune parameters — if 14 makes twice the profit that 13 does, that isn't a discovery, it's overfitting.
- Apply signals one bar later — a backtest that generates a signal from today's close and fills at today's close does not exist in reality.
Summary
- MA — direction; lagging is normal
- RSI — ratio of up-force to down-force, computed with Wilder smoothing
- MACD — the gap between two EMAs; fragile in ranges
- Bollinger — band width is more useful than band breaks
- ATR — the basis for stop distance and position size
Knowing many indicators matters far less than being able to explain why a given indicator is in your system. If you can't explain it, you're usually better off removing it.
댓글
댓글 쓰기