Supertrend in Python: Build Crypto Trading Signals
Learn to code, backtest, and automate the Supertrend indicator in Python for crypto markets. Practical examples using Binance data with full working code.
Learn to code, backtest, and automate the Supertrend indicator in Python for crypto markets. Practical examples using Binance data with full working code.
The Supertrend indicator flips a lot of traders from losing to consistently profitable — not because it's magic, but because it cuts through market noise and gives you a clear, unambiguous signal: buy or sell. When you code it in Python and run it against real crypto data from Binance or Bybit, something clicks. You stop second-guessing candles and start trusting a system. This guide walks you through building that system from scratch.
Supertrend is a trend-following indicator built on top of ATR — Average True Range. ATR measures how much an asset moves on average, accounting for gaps. Supertrend uses that volatility measurement to draw a dynamic band around price action. When price closes above the band, you get a green signal (bullish). When it closes below, you get red (bearish). The band flips sides when the trend reverses.
Think of it like a moving floor and ceiling. During a bull run, the Supertrend line acts as a floor beneath price — as long as price stays above it, you're in the trend. The moment price crashes through that floor, the line snaps to the ceiling and the signal flips to bearish. It's essentially an adaptive trailing stop that also tells you direction.
Key Takeaway: Supertrend doesn't predict where price is going — it tells you which side of the trend you're currently on. It's a confirmation tool, not a crystal ball.
You'll need three libraries: ccxt for fetching OHLCV data from exchanges like Binance or OKX, pandas for data manipulation, and numpy for math. If you're running backtests, pandas-ta gives you a ready-made Supertrend function so you don't have to reinvent the wheel.
pip install ccxt pandas numpy pandas-ta
Once installed, pull historical data from Binance. CCXT makes this one function call regardless of which exchange you target — switching from Binance to Bybit is literally changing one word in your code.
import ccxt
import pandas as pd
def fetch_ohlcv(symbol='BTC/USDT', timeframe='1h', limit=500, exchange_id='binance'):
exchange = getattr(ccxt, exchange_id)()
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)
return df
# Fetch 500 hourly candles for BTC/USDT from Binance
df = fetch_ohlcv('BTC/USDT', '1h', 500, 'binance')
print(df.tail())
You have two options: use pandas-ta for a one-liner, or implement it manually for full control. Both are valid — the manual version is better if you want to understand what's happening under the hood or need to tune the calculation for speed in a live bot.
The pandas-ta approach is the fastest way to get running:
import pandas_ta as ta
# period=10, multiplier=3.0 are the classic defaults
df.ta.supertrend(length=10, multiplier=3.0, append=True)
# pandas-ta adds these columns:
# SUPERT_10_3.0 — the actual band value
# SUPERTd_10_3.0 — direction: 1 = bullish, -1 = bearish
# SUPERTl_10_3.0 — long band
# SUPERTs_10_3.0 — short band
df['signal'] = df['SUPERTd_10_3.0'].map({1: 'BUY', -1: 'SELL'})
print(df[['close', 'SUPERT_10_3.0', 'signal']].tail(10))
If you prefer to build it manually — useful when you're deploying to a server without pandas-ta or need exact control over ATR smoothing:
import numpy as np
def supertrend(df, period=10, multiplier=3.0):
hl2 = (df['high'] + df['low']) / 2
# True Range
prev_close = df['close'].shift(1)
tr = pd.concat([
df['high'] - df['low'],
(df['high'] - prev_close).abs(),
(df['low'] - prev_close).abs()
], axis=1).max(axis=1)
# Wilder's ATR (RMA)
atr = tr.ewm(alpha=1/period, adjust=False).mean()
upper = hl2 + multiplier * atr
lower = hl2 - multiplier * atr
supertrend_vals = pd.Series(index=df.index, dtype=float)
direction = pd.Series(index=df.index, dtype=int)
for i in range(1, len(df)):
# Adjust upper band
if upper.iloc[i] < upper.iloc[i-1] or df['close'].iloc[i-1] > upper.iloc[i-1]:
final_upper = upper.iloc[i]
else:
final_upper = upper.iloc[i-1]
# Adjust lower band
if lower.iloc[i] > lower.iloc[i-1] or df['close'].iloc[i-1] < lower.iloc[i-1]:
final_lower = lower.iloc[i]
else:
final_lower = lower.iloc[i-1]
upper.iloc[i] = final_upper
lower.iloc[i] = final_lower
# Direction
if direction.iloc[i-1] == -1: # Was bearish
direction.iloc[i] = 1 if df['close'].iloc[i] > final_upper else -1
else: # Was bullish
direction.iloc[i] = -1 if df['close'].iloc[i] < final_lower else 1
supertrend_vals.iloc[i] = final_lower if direction.iloc[i] == 1 else final_upper
df['supertrend'] = supertrend_vals
df['direction'] = direction
df['signal'] = direction.map({1: 'BUY', -1: 'SELL'})
return df
df = supertrend(df)
Key Takeaway: The manual implementation uses Wilder's smoothing (EWM with alpha=1/period) for ATR — this matches TradingView's Supertrend exactly. If your signals don't match TradingView, ATR smoothing is usually the culprit.
Before trusting any indicator with real money, run it through history. A basic backtest loops through your signal column, enters on BUY, exits on SELL, and tracks the result. This isn't meant to be production-grade — it's meant to tell you whether the indicator has any edge on your chosen pair and timeframe.
def backtest(df, initial_capital=10000):
capital = initial_capital
position = 0
entry_price = 0
trades = []
for i in range(1, len(df)):
signal = df['signal'].iloc[i]
price = df['close'].iloc[i]
prev_signal = df['signal'].iloc[i-1]
# Enter long on BUY signal
if signal == 'BUY' and prev_signal == 'SELL' and position == 0:
position = capital / price
entry_price = price
# Exit on SELL signal
elif signal == 'SELL' and prev_signal == 'BUY' and position > 0:
exit_value = position * price
pnl = exit_value - capital
trades.append({
'entry': entry_price,
'exit': price,
'pnl': pnl,
'pnl_pct': (price / entry_price - 1) * 100
})
capital = exit_value
position = 0
results = pd.DataFrame(trades)
if not results.empty:
print(f"Total trades: {len(results)}")
print(f"Win rate: {(results['pnl'] > 0).mean():.1%}")
print(f"Total return: {(capital / initial_capital - 1):.1%}")
print(f"Avg trade: {results['pnl_pct'].mean():.2f}%")
return results, capital
trades, final_capital = backtest(df)
When you run this on BTC/USDT 1h data from Binance, typical results show 45-60% win rate with a positive expectancy — Supertrend catches the big moves even if it whipsaws on choppy days. ETH/USDT tends to behave similarly. Altcoins with thinner liquidity (available on Gate.io and KuCoin) often need a higher multiplier to reduce noise.
Once you've backtested and found settings you're comfortable with, the live version is straightforward: fetch the latest candles every hour (or whatever your timeframe is), recalculate Supertrend, and check if the last signal changed. If it flipped from SELL to BUY — that's your entry trigger.
import time
def run_live(symbol='ETH/USDT', timeframe='1h', exchange_id='bybit'):
print(f"Monitoring {symbol} on {exchange_id}...")
last_signal = None
while True:
df = fetch_ohlcv(symbol, timeframe, 200, exchange_id)
df = supertrend(df)
current_signal = df['signal'].iloc[-1]
if current_signal != last_signal:
print(f"[SIGNAL CHANGE] {df.index[-1]} — {last_signal} → {current_signal}")
print(f"Price: {df['close'].iloc[-1]:.2f}")
last_signal = current_signal
# Sleep until next candle close (3600s for 1h)
time.sleep(3600)
run_live('ETH/USDT', '1h', 'bybit')
On Binance you can access most major pairs with tight spreads and reliable API uptime, making it the default choice for BTC and ETH bots. Bybit is strong for perpetual futures if you want to trade long and short signals directly. OKX is worth considering for its unified margin account which simplifies capital allocation across multiple Supertrend bots running simultaneously. If you want pre-built signals without running your own Python stack, VoiceOfChain provides real-time crypto trading signals including trend-based alerts — useful for confirming your bot's signals against broader market data.
Warning: Never run a live bot without a stop-loss or position size limit. Supertrend can stay wrong for multiple candles during choppy, sideways markets — size accordingly.
The default period=10 and multiplier=3.0 are a starting point, not gospel. Crypto moves differently than traditional assets — higher volatility means you often need wider bands to avoid being whipsawed out of legitimate trends. Here's a practical reference for different use cases:
| Style | Period | Multiplier | Timeframe | Best For |
|---|---|---|---|---|
| Scalping | 7 | 2.0 | 5m / 15m | BTC, ETH on Binance |
| Swing Trading | 10 | 3.0 | 1h / 4h | Most major pairs |
| Position Trading | 14 | 4.0 | 1D | BTC, ETH long-term |
| High Volatility Alts | 10 | 4.0–5.0 | 1h | Altcoins on KuCoin, Gate.io |
The fastest way to find optimal parameters is a grid search — loop through combinations, run your backtest on each, and pick the highest Sharpe ratio rather than raw return. Higher return often comes with higher drawdown; Sharpe balances both.
Supertrend in Python is one of the most accessible ways to build a systematic crypto trading approach. You fetch data with CCXT from exchanges like Binance or Bybit, calculate the indicator with pandas-ta or a manual implementation, backtest it against real history, and then run it live with a simple polling loop. The indicator won't make you rich by itself — no indicator does. But it gives you a clear framework: a rule-based system that removes emotion and forces consistency. Start with BTC/USDT on the 1h timeframe, validate your backtest results, keep position sizes small, and iterate. That's the actual edge — not the indicator, but the discipline of following it.