In this tutorial, we implement how to use pandas-ta-classic to build a complete technical analysis and trading strategy workflow. We start by installing the required libraries, downloading historical OHLCV stock data with yfinance, cleaning the returned data structure, and inspecting the available indicator categories inside the library. We then calculate popular indicators such as SMA, EMA, RSI, ATR, MACD, Bollinger Bands, candlestick patterns, and a custom distance-from-EMA feature. Also, we combine daily and weekly signals, create entry and exit logic, backtest the strategy with shifted positions, calculate performance metrics, run a parameter sweep, and visualize price action, RSI behavior, trade signals, and equity curves in a structured way.
import subprocess, sys def _pip(pkgs): subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *pkgs]) _pip(["pandas-ta-classic", "yfinance", "matplotlib"]) import numpy as np import pandas as pd import yfinance as yf import pandas_ta_classic as ta import matplotlib.pyplot as plt from itertools import product pd.set_option("display.max_columns", 80) pd.set_option("display.width", 200) TICKER, START, END = "AAPL", "2018-01-01", "2024-12-31" raw = yf.download(TICKER, start=START, end=END, auto_adjust=True, progress=False) if isinstance(raw.columns, pd.MultiIndex): raw.columns = raw.columns.get_level_values(0) df = (raw.rename(columns=str.lower) [["open", "high", "low", "close", "volume"]] .dropna() .copy()) df.index.name = "date" print(f"[data] {TICKER}: {len(df)} rows " f"{df.index.min().date()} → {df.index.max().date()}") print("[lib] Categories:", list(ta.Category.keys())) for cat in ("momentum", "overlap", "trend", "volatility", "volume"): names = ta.Category.get(cat, []) print(f"[lib] {cat:<11} ({len(names):>3}): " f"{', '.join(names[:8])}{' ...' if len(names) > 8 else ''}")
We install the required packages and import the main libraries needed for technical analysis, data handling, plotting, and parameter combinations. We download Apple’s historical OHLCV data using yfinance, clean the returned DataFrame, and convert column names to lowercase for easier processing. We also review the available pandas-ta-classic indicator categories to understand which technical indicators we can use in the tutorial.
df.ta.sma(length=20, append=True) df.ta.sma(length=50, append=True) df.ta.ema(length=200, append=True) df.ta.rsi(length=14, append=True) df.ta.atr(length=14, append=True) df.ta.macd(append=True) df.ta.bbands(length=20, std=2.0, append=True) my_strategy = ta.Strategy( name="AdvancedDemo", description="Trend + momentum + volume + volatility in one shot", ta=[ {"kind": "hma", "length": 30}, {"kind": "adx", "length": 14}, {"kind": "aroon", "length": 14}, {"kind": "stoch", "k": 14, "d": 3}, {"kind": "obv"}, {"kind": "mfi", "length": 14}, {"kind": "willr", "length": 14}, {"kind": "cci", "length": 20}, {"kind": "kc", "length": 20, "scalar": 2}, ], ) df.ta.strategy(my_strategy) print(f"[strat] DataFrame now has {df.shape[1]} columns") df["dist_ema200_pct"] = (df["close"] / df["EMA_200"] - 1.0) * 100 df.ta.cdl_doji(append=True) df.ta.cdl_inside(append=True) doji_col = next((c for c in df.columns if c.startswith("CDL_DOJI")), None) print(f"[cdl] Doji days detected: {int((df[doji_col] == 100).sum())}")
We apply several commonly used technical indicators directly through the .ta DataFrame extension. We calculate moving averages, RSI, ATR, MACD, Bollinger Bands, and then run a custom multi-indicator strategy using ta.Strategy. We also create a custom EMA-distance feature and detect candlestick patterns such as Doji and Inside candles.
weekly = (df[["open", "high", "low", "close", "volume"]] .resample("W-FRI") .agg({"open":"first","high":"max","low":"min","close":"last","volume":"sum"}) .dropna()) weekly["RSI_W_14"] = ta.rsi(weekly["close"], length=14) df = df.join(weekly[["RSI_W_14"]]) df["RSI_W_14"] = df["RSI_W_14"].ffill().shift(1) trend = df["SMA_20"] > df["SMA_50"] mom_cross = (df["RSI_14"] > 50) & (df["RSI_14"].shift(1) <= 50) mtf_ok = df["RSI_W_14"] > 50 exit_cond = (df["RSI_14"] < 45) | (df["SMA_20"] < df["SMA_50"]) position = np.zeros(len(df), dtype=int) in_pos = False for i in range(len(df)): if not in_pos and trend.iat[i] and mom_cross.iat[i] and bool(mtf_ok.iat[i]): in_pos = True elif in_pos and exit_cond.iat[i]: in_pos = False position[i] = 1 if in_pos else 0 df["pos"] = position df["ret"] = df["close"].pct_change().fillna(0.0) df["strat_ret"] = df["pos"].shift(1).fillna(0) * df["ret"]
We create a weekly version of the daily OHLCV data and calculate weekly RSI for higher-timeframe confirmation. We join the weekly RSI back to the daily DataFrame and shift it to avoid using future information in our trading logic. We then define the trend, momentum, multi-timeframe filter, exit condition, position state, daily returns, and strategy returns.
def perf(returns, ppy=252): r = returns.dropna() if len(r) == 0 or r.std() == 0: return {} cum = (1 + r).cumprod() cagr = cum.iloc[-1] ** (ppy / len(r)) - 1 vol = r.std() * np.sqrt(ppy) sharpe = (r.mean() / r.std()) * np.sqrt(ppy) downside = r[r < 0].std() * np.sqrt(ppy) sortino = (r.mean() * ppy) / downside if downside > 0 else np.nan mdd = (cum / cum.cummax() - 1).min() nz = r[r != 0] win = (nz > 0).mean() if len(nz) else 0.0 return {"CAGR": cagr, "Vol": vol, "Sharpe": sharpe, "Sortino": sortino, "MaxDD": mdd, "WinRate": win, "FinalEquity": cum.iloc[-1]} summary = pd.DataFrame({ "Buy & Hold": perf(df["ret"]), "Strategy": perf(df["strat_ret"]), }).T print("n[perf] ----------------------------------------") print(summary.round(4)) def quick_bt(prices, fast, slow, rsi_thr=50): if fast >= slow: return None d = prices.copy() d["SMAf"] = ta.sma(d["close"], length=fast) d["SMAs"] = ta.sma(d["close"], length=slow) d["RSI"] = ta.rsi(d["close"], length=14) sig = ((d["SMAf"] > d["SMAs"]) & (d["RSI"] > rsi_thr)).astype(int) sret = sig.shift(1).fillna(0) * d["close"].pct_change().fillna(0) return perf(sret) prices = df[["open", "high", "low", "close", "volume"]] rows = [] for fast, slow in product([5, 10, 20, 30], [50, 100, 150, 200]): m = quick_bt(prices, fast, slow) if m: rows.append({"fast": fast, "slow": slow, **m}) sweep = (pd.DataFrame(rows) .sort_values("Sharpe", ascending=False) .reset_index(drop=True)) print("n[sweep] Top 5 (fast SMA, slow SMA) by Sharpe:") print(sweep.head().round(4))
We define a performance function that calculates key metrics, including CAGR, volatility, Sharpe ratio, Sortino ratio, maximum drawdown, win rate, and final equity. We compare the strategy performance against a simple buy-and-hold baseline to see whether our signal logic adds value. We also run a parameter sweep across different fast and slow SMA combinations and rank the results by Sharpe ratio.
entries = df.index[(df["pos"].diff() == 1)] exits = df.index[(df["pos"].diff() == -1)] fig, (ax1, ax2, ax3) = plt.subplots( 3, 1, figsize=(13, 10), sharex=True, gridspec_kw={"height_ratios": [3, 1, 2]}, ) ax1.plot(df.index, df["close"], lw=1.1, color="black", label="Close") ax1.plot(df.index, df["SMA_20"], lw=0.9, label="SMA 20") ax1.plot(df.index, df["SMA_50"], lw=0.9, label="SMA 50") bbu, bbl = "BBU_20_2.0", "BBL_20_2.0" if bbu in df and bbl in df: ax1.fill_between(df.index, df[bbl], df[bbu], alpha=0.12, label="Bollinger 20,2") ax1.scatter(entries, df.loc[entries, "close"], marker="^", s=70, color="green", zorder=5, label="Entry") ax1.scatter(exits, df.loc[exits, "close"], marker="v", s=70, color="red", zorder=5, label="Exit") ax1.set_title(f"{TICKER} — price, MAs, Bollinger, signals") ax1.legend(loc="upper left"); ax1.grid(alpha=0.3) ax2.plot(df.index, df["RSI_14"], lw=0.9, label="RSI 14") ax2.axhline(70, color="red", ls="--", lw=0.6) ax2.axhline(30, color="green", ls="--", lw=0.6) ax2.set_title("RSI 14"); ax2.legend(loc="upper left"); ax2.grid(alpha=0.3) ax3.plot(df.index, (1 + df["ret"]).cumprod(), lw=1.1, label="Buy & Hold") ax3.plot(df.index, (1 + df["strat_ret"]).cumprod(), lw=1.1, label="Strategy") ax3.set_title("Equity curves ($1 start)") ax3.legend(loc="upper left"); ax3.grid(alpha=0.3) plt.tight_layout(); plt.show() print("nTweak TICKER, the Strategy list, or the sweep grid to keep exploring.")
We identify the strategy’s entry and exit points from changes in the position column. We then create a three-panel chart showing price action with moving averages and Bollinger Bands, RSI behavior, and equity curves for both buy-and-hold and the strategy. We use these visuals to understand where trades happen, how momentum behaves, and how the strategy performs over time.
In conclusion, we built an end-to-end technical analysis pipeline that shows how pandas-ta-classic can support both quick indicator generation and more advanced strategy development. We used the library to compute individual indicators and also to create custom strategies, add multi-timeframe confirmation, reduce look-ahead bias, evaluate returns, and compare the strategy against buy-and-hold performance. We also ran a simple parameter sweep to understand how different moving-average combinations affect results and to help us identify stronger configurations. Also, we gained a foundation for experimenting with technical indicators, trading signals, backtesting logic, performance evaluation, and financial data visualization.
Check out the Codes with Notebook. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.
Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us
Sana Hassan
Sana Hassan, a consulting intern at Marktechpost and dual-degree student at IIT Madras, is passionate about applying technology and AI to address real-world challenges. With a keen interest in solving practical problems, he brings a fresh perspective to the intersection of AI and real-life solutions.

