
Pandas Ta
- 305 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
pandas-ta is a Claude Code skill that computes 130+ technical analysis indicators on crypto OHLCV data using the pandas-ta library.
About
pandas-ta is a Claude Code skill for computing technical analysis indicators on crypto market data. It uses the pandas-ta library to add 130+ trend, momentum, volatility, and volume indicators to an OHLCV DataFrame, and provides Strategy definitions for scalping, trend-following, mean-reversion, and momentum. A developer uses it to generate trading signals from candle data.
- Adds 130+ technical indicators (RSI, MACD, Bollinger, ATR, VWAP, SuperTrend) to OHLCV data
- Named Strategy presets for scalping, trend, mean-reversion, and momentum
- Ships compute_indicators.py and multi_indicator_scan.py with Birdeye and demo data
Pandas Ta by the numbers
- 305 all-time installs (skills.sh)
- Ranked #333 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
pandas-ta capabilities & compatibility
Free skill; optional Birdeye API key only for live data, demo mode needs none.
- Capabilities
- technical analysis · indicator computation · signal generation · strategy scanning
- Use cases
- data analysis
- Pricing
- Bring your own API key
What pandas-ta says it does
pandas-ta is a Python library that extends pandas DataFrames with 130+ technical analysis indicators accessible via `df.ta`.
It covers trend, momentum, volatility, volume, and overlap indicator categories — all callable with a single method on any OHLCV DataFrame.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill pandas-taAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 305 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Compute technical analysis indicators on crypto OHLCV data using the pandas-ta library.
Who is it for?
Adding technical indicators and generating signals from crypto candle data.
Skip if: Data cleaning (use ohlcv-processing) or order execution.
When should I use this skill?
You have clean OHLCV data and need indicators like RSI, MACD, or Bollinger Bands computed.
What you get
An OHLCV DataFrame enriched with indicator columns and scored strategy signals.
- Indicator columns on the OHLCV DataFrame
- Strategy signal summary
- Multi-strategy alignment score
By the numbers
- 130+ technical indicators
- 3 named strategy patterns (trend, mean reversion, momentum)
Files
pandas-ta — Technical Analysis for Crypto Markets
pandas-ta is a Python library that extends pandas DataFrames with 130+ technical analysis indicators accessible via df.ta. It covers trend, momentum, volatility, volume, and overlap indicator categories — all callable with a single method on any OHLCV DataFrame.
Installation
uv pip install pandas-ta pandas httpxQuick Start
import pandas as pd
import pandas_ta as ta
# Assume df is a DataFrame with columns: open, high, low, close, volume
# All lowercase column names required
# Single indicator
df["rsi"] = df.ta.rsi(length=14)
df["atr"] = df.ta.atr(length=14)
# Multiple indicators via strategy
df.ta.strategy(ta.Strategy(
name="Quick Check",
ta=[
{"kind": "rsi", "length": 14},
{"kind": "macd", "fast": 12, "slow": 26, "signal": 9},
{"kind": "bbands", "length": 20, "std": 2.0},
]
))OHLCV DataFrame Format
pandas-ta expects a DataFrame with lowercase column names:
import pandas as pd
df = pd.DataFrame({
"open": [...],
"high": [...],
"low": [...],
"close": [...],
"volume": [...]
}, index=pd.DatetimeIndex([...]))Important: Set the index to a DatetimeIndex for time-aware indicators like VWAP. Column names must be lowercase (close, not Close).
Handling Missing Data
# Drop rows with NaN in OHLCV columns
df = df.dropna(subset=["open", "high", "low", "close", "volume"])
# Forward-fill small gaps (1-2 bars max)
df = df.ffill(limit=2)
# Verify no zero-volume bars for volume indicators
df = df[df["volume"] > 0]Core Indicator Categories
Trend Indicators
Identify market direction and trend strength.
| Indicator | Call | Key Signal |
|---|---|---|
| SMA | df.ta.sma(length=20) | Price above = bullish |
| EMA | df.ta.ema(length=20) | Faster than SMA, less lag |
| SuperTrend | df.ta.supertrend(length=10, multiplier=3) | Direction column: 1=bull, -1=bear |
| Ichimoku | df.ta.ichimoku() | Returns tuple of (span, lines) DataFrames |
| VWMA | df.ta.vwma(length=20) | Volume-weighted price trend |
| HMA | df.ta.hma(length=20) | Minimal lag, smooth trend |
| ADX | df.ta.adx(length=14) | >25 = trending, <20 = ranging |
Momentum Indicators
Measure speed and magnitude of price changes.
| Indicator | Call | Key Signal |
|---|---|---|
| RSI | df.ta.rsi(length=14) | >70 overbought, <30 oversold |
| MACD | df.ta.macd(fast=12, slow=26, signal=9) | Histogram crossover = entry |
| Stochastic | df.ta.stoch(k=14, d=3, smooth_k=3) | >80 overbought, <20 oversold |
| CCI | df.ta.cci(length=20) | >100 overbought, <-100 oversold |
| Williams %R | df.ta.willr(length=14) | >-20 overbought, <-80 oversold |
| ROC | df.ta.roc(length=10) | Positive = upward momentum |
| MFI | df.ta.mfi(length=14) | Money flow version of RSI |
Volatility Indicators
Measure price dispersion and expected range.
| Indicator | Call | Key Signal |
|---|---|---|
| Bollinger Bands | df.ta.bbands(length=20, std=2) | Squeeze = breakout pending |
| ATR | df.ta.atr(length=14) | Position sizing, stop placement |
| Keltner Channels | df.ta.kc(length=20, scalar=1.5) | BB inside KC = squeeze |
| Donchian Channels | df.ta.donchian(lower_length=20, upper_length=20) | Breakout detection |
Volume Indicators
Confirm price moves with volume analysis.
| Indicator | Call | Key Signal |
|---|---|---|
| OBV | df.ta.obv() | Divergence from price = reversal |
| VWAP | df.ta.vwap() | Intraday fair value (needs DatetimeIndex) |
| CMF | df.ta.cmf(length=20) | >0 accumulation, <0 distribution |
| AD | df.ta.ad() | Accumulation/Distribution line |
Strategy Class
Run multiple indicators in a single call using ta.Strategy:
import pandas_ta as ta
# Built-in "All" strategy runs every indicator
df.ta.strategy(ta.AllStrategy)
# Custom strategy
my_strategy = ta.Strategy(
name="Crypto Scalp",
description="Fast indicators for crypto scalping",
ta=[
{"kind": "ema", "length": 9},
{"kind": "ema", "length": 21},
{"kind": "rsi", "length": 7},
{"kind": "stoch", "k": 5, "d": 3, "smooth_k": 3},
{"kind": "atr", "length": 7},
{"kind": "bbands", "length": 10, "std": 2.0},
{"kind": "obv"},
]
)
df.ta.strategy(my_strategy)Named Strategy Patterns
# Trend following
trend_strategy = ta.Strategy(
name="Trend",
ta=[
{"kind": "ema", "length": 20},
{"kind": "ema", "length": 50},
{"kind": "adx", "length": 14},
{"kind": "supertrend", "length": 10, "multiplier": 3},
{"kind": "atr", "length": 14},
]
)
# Mean reversion
reversion_strategy = ta.Strategy(
name="Mean Reversion",
ta=[
{"kind": "rsi", "length": 14},
{"kind": "bbands", "length": 20, "std": 2.0},
{"kind": "stoch", "k": 14, "d": 3, "smooth_k": 3},
{"kind": "cci", "length": 20},
]
)
# Momentum
momentum_strategy = ta.Strategy(
name="Momentum",
ta=[
{"kind": "macd", "fast": 12, "slow": 26, "signal": 9},
{"kind": "rsi", "length": 14},
{"kind": "obv"},
{"kind": "roc", "length": 10},
{"kind": "mfi", "length": 14},
]
)Crypto-Specific Considerations
24/7 Markets
- No session gaps — indicators that rely on open/close of sessions behave differently
- VWAP resets at midnight UTC by default; consider anchored VWAP for custom periods
- Weekend data is continuous — no Monday gap effects
High Volatility Adjustments
- Bollinger Bands: Use 2.5-3x standard deviation instead of the default 2x
- RSI periods: Shorter periods (7-10) capture faster crypto cycles
- ATR: Use for dynamic stop-losses; crypto ATR is typically 2-5x equity ATR
- SuperTrend multiplier: 3-4x for crypto vs 2-3x for equities
Low-Cap Token Considerations
- Volume indicators (OBV, CMF, MFI) are unreliable with thin order books
- Prefer price-based indicators (RSI, BBands, SuperTrend) for low-liquidity tokens
- ATR-based position sizing is critical — wide spreads amplify losses
- Wash trading inflates volume; cross-reference with on-chain data
Timeframe Selection
| Timeframe | Use Case | Recommended Indicators |
|---|---|---|
| 1m-5m | Scalping, PumpFun | RSI(5-7), EMA(5,13), ATR(5) |
| 15m-1h | Day trading | MACD, RSI(14), BBands, EMA(20,50) |
| 4h-1d | Swing trading | SuperTrend, ADX, EMA(50,200) |
| 1w | Position trading | SMA(20,50), RSI(14), monthly VWAP |
Common Indicator Combinations
Trend Following
# EMA crossover + ADX confirmation + SuperTrend direction
ema_fast = df.ta.ema(length=20)
ema_slow = df.ta.ema(length=50)
adx_df = df.ta.adx(length=14)
st_df = df.ta.supertrend(length=10, multiplier=3)
bullish = (
(ema_fast > ema_slow) &
(adx_df["ADX_14"] > 25) &
(st_df["SUPERTd_10_3.0"] == 1)
)Mean Reversion
# RSI oversold + price at lower BB + Stochastic oversold
rsi = df.ta.rsi(length=14)
bb = df.ta.bbands(length=20, std=2.5)
stoch = df.ta.stoch(k=14, d=3, smooth_k=3)
buy_signal = (
(rsi < 30) &
(df["close"] <= bb["BBL_20_2.5"]) &
(stoch["STOCHk_14_3_3"] < 20)
)Momentum Confirmation
# MACD histogram positive + RSI above 50 + OBV rising
macd = df.ta.macd(fast=12, slow=26, signal=9)
rsi = df.ta.rsi(length=14)
obv = df.ta.obv()
momentum_bull = (
(macd["MACDh_12_26_9"] > 0) &
(rsi > 50) &
(obv > obv.shift(1))
)Volatility Breakout (BB Squeeze)
# Bollinger Band width contracting + volume spike
bb = df.ta.bbands(length=20, std=2.0)
atr = df.ta.atr(length=14)
vol_sma = df["volume"].rolling(20).mean()
bb_width = (bb["BBU_20_2.0"] - bb["BBL_20_2.0"]) / bb["BBM_20_2.0"]
squeeze = bb_width < bb_width.rolling(120).quantile(0.1)
vol_spike = df["volume"] > (vol_sma * 2.0)
breakout_setup = squeeze & vol_spikeIntegration with Other Skills
- birdeye-api: Fetch OHLCV data → feed into pandas-ta for indicator computation
- vectorbt: Use pandas-ta indicators as signal inputs for backtesting
- trading-visualization: Plot indicator overlays on price charts
- slippage-modeling: Combine ATR with slippage estimates for realistic execution modeling
- position-sizing: Use ATR-based sizing from pandas-ta output
Files
References
references/indicator_guide.md— Top 20 crypto indicators with syntax, parameters, and interpretationreferences/strategy_patterns.md— Pre-built strategy combinations for scalping, day trading, and swing tradingreferences/common_pitfalls.md— Common mistakes with technical indicators in crypto markets
Scripts
scripts/compute_indicators.py— Fetch OHLCV data and compute standard indicator set with signal summaryscripts/multi_indicator_scan.py— Run multiple strategy profiles and score current signal alignment
pandas-ta — Common Pitfalls in Crypto Technical Analysis
Mistakes that cause false signals, incorrect backtests, and real losses when using technical indicators on crypto market data.
1. NaN Values at Start of Series
Problem: Every indicator produces NaN for its warmup period. RSI(14) has 14+ NaN bars. MACD(12,26,9) has 33+ NaN bars. Treating NaN as 0 or ignoring it generates false signals.
Fix:
# Always check for sufficient data
min_bars = 200 # Enough warmup for most indicators
if len(df) < min_bars:
raise ValueError(f"Need {min_bars} bars, got {len(df)}")
# Drop NaN rows before signal evaluation
signals = signals.dropna()
# Or check explicitly
rsi = df.ta.rsi(length=14)
valid_rsi = rsi.iloc[14:] # Skip warmup periodRule of thumb: Need at least 3x the longest indicator period in your dataset.
2. Lookahead Bias
Problem: Using future data to make current decisions. Common in backtesting when indicators or signals reference data not yet available at decision time.
Examples:
- Using
df["close"]for the current bar before the bar closes - Centering a moving average (pandas default for some rolling operations)
- Using
BBP(Bollinger Band percent) calculated on the full dataset
Fix:
# Shift signals by 1 bar — act on NEXT bar after signal
df["signal"] = (df["RSI_14"] < 30).astype(int)
df["entry"] = df["signal"].shift(1) # Enter on next bar
# In backtesting, always use .shift(1) for signal-to-execution
# You see the signal at bar N close, you act at bar N+1 open3. Overfitting with Too Many Indicators
Problem: Combining 10+ indicators to find "perfect" entry conditions produces a system that worked historically but fails live. More indicators = more curve fitting.
Symptoms:
- Backtest win rate > 80% but live trading loses money
- Strategy only works on one specific token in one specific time period
- Adding or removing one indicator drastically changes results
Fix:
- Use 3-5 indicators maximum per strategy
- Each indicator should measure something different (trend + momentum + volatility, not RSI + Stochastic + Williams %R which all measure momentum)
- Validate on out-of-sample data from a different time period
- Use walk-forward analysis, not single backtest optimization
4. Parameter Optimization Trap
Problem: Optimizing indicator parameters on historical data finds the best past parameters, not the best future parameters. RSI(13) beating RSI(14) on historical data is noise, not signal.
Fix:
# Test a small range of standard parameters, not a wide sweep
# Standard RSI: 7, 9, 14, 21 — not RSI(1) through RSI(50)
standard_params = {
"rsi": [7, 9, 14, 21],
"ema_fast": [9, 12, 20],
"ema_slow": [21, 26, 50],
"bb_std": [2.0, 2.5, 3.0],
}
# If results are very sensitive to parameter choice, the edge is fragile
# Robust strategies work across a range of parameters5. Volume Indicator Reliability on Low-Cap Tokens
Problem: OBV, CMF, MFI, and VWAP assume volume data is accurate. On Solana memecoins and low-cap tokens, volume is often unreliable due to:
- Wash trading (bots trading with themselves)
- Concentrated liquidity (one market maker = most volume)
- Cross-DEX arbitrage inflating volume
- Different DEX APIs reporting different volume
Fix:
- Use price-based indicators (RSI, BBands, SuperTrend, EMAs) as primary
- Use volume indicators only for confirmation, not as primary signals
- Cross-reference volume with on-chain transaction count
- For tokens with < $100K daily volume, ignore volume indicators entirely
6. Repainting Indicators
Problem: Some indicators change their historical values as new data arrives. The signal that appeared on bar N may look different when viewed from bar N+10.
Common repainting indicators:
- Zigzag (by definition)
- Pivot points (require future data to confirm)
- Some implementations of SuperTrend (confirm pandas-ta uses non-repainting version)
Fix:
- Test by comparing indicator values computed on data ending at bar N vs. data ending at bar N+100
- If historical values change, the indicator repaints
- pandas-ta's standard indicators (RSI, MACD, BBands, EMA) do NOT repaint
# Verification: compute indicator twice, compare
rsi_full = df.ta.rsi(length=14)
rsi_partial = df.iloc[:100].ta.rsi(length=14)
# These should be identical for overlapping bars
assert rsi_full.iloc[:100].equals(rsi_partial), "Indicator repaints!"7. Timeframe Mismatch
Problem: Indicator parameters calibrated for daily charts produce garbage on 1-minute charts. RSI(14) on 1d = 14 trading days. RSI(14) on 1m = 14 minutes. They measure completely different things.
Fix:
- Scale parameters to match the timeframe's noise level
- 1m charts: shorter periods (5-9), wider bands (3x std)
- 1d charts: standard periods (14-21), standard bands (2-2.5x std)
- See the parameter table in
indicator_guide.mdfor recommended values
8. Missing Data and Gaps
Problem: Gaps in OHLCV data (missing bars) cause indicator distortion. A 1h chart missing 3 bars has a gap that makes EMAs jump.
Common causes in crypto:
- API rate limits causing missed fetches
- DEX downtime or low liquidity periods with no trades
- Data provider outages
Fix:
# Detect gaps
expected_freq = pd.Timedelta("1h") # Adjust for your timeframe
time_diffs = df.index.to_series().diff()
gaps = time_diffs[time_diffs > expected_freq * 1.5]
if len(gaps) > 0:
print(f"Warning: {len(gaps)} gaps detected")
print(gaps)
# Option 1: Forward-fill small gaps (1-2 bars)
df = df.asfreq(expected_freq, method="ffill")
# Option 2: Split into continuous segments
segments = []
gap_indices = gaps.index.tolist()
# Process each segment separately
# Option 3: Recompute indicators after gap
# Drop all indicator columns, recompute from scratch9. VWAP Misuse in 24/7 Markets
Problem: VWAP is designed to reset at market open. Crypto has no market open. pandas-ta resets VWAP at midnight UTC, which is arbitrary for most traders.
Fix:
- Use VWAP for intraday reference only, not as a multi-day indicator
- Consider anchored VWAP from significant swing highs/lows instead
- VWMA (volume-weighted moving average) is often more useful than VWAP for crypto
# VWAP requires DatetimeIndex
df.index = pd.DatetimeIndex(df.index)
vwap = df.ta.vwap() # Resets daily at midnight UTC
# For multi-day analysis, use VWMA instead
vwma = df.ta.vwma(length=20)10. Treating Indicators as Absolute Truth
Problem: RSI < 30 does not mean "buy". It means price has dropped significantly relative to recent history. In a crash, RSI can stay below 30 for weeks while price drops another 80%.
Key principles:
- Indicators are probability modifiers, not binary signals
- Combine indicator signals with market context (regime, trend, volume)
- No single indicator works in all market conditions
- Mean reversion indicators fail in trending markets; trend indicators fail in ranges
- Always have a stop-loss independent of indicator readings
11. Column Name Confusion
Problem: pandas-ta uses specific column naming conventions. Getting the name wrong returns KeyError or operates on the wrong column.
Fix:
# After computing, inspect column names
df.ta.macd(append=True)
print([c for c in df.columns if "MACD" in c])
# Output: ['MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9']
# Common pattern: {INDICATOR}_{param1}_{param2}
# BBands: BBL_20_2.0, BBM_20_2.0, BBU_20_2.0, BBB_20_2.0, BBP_20_2.0
# Stoch: STOCHk_14_3_3, STOCHd_14_3_3
# SuperTrend: SUPERT_10_3.0, SUPERTd_10_3.0, SUPERTl_10_3.0, SUPERTs_10_3.012. Ignoring Indicator Divergence
Problem: Price making new highs while RSI makes lower highs (bearish divergence) is one of the strongest signals in TA — but many traders ignore it because the trend "looks strong."
Fix:
# Simple divergence detection
def detect_divergence(price: pd.Series, indicator: pd.Series, lookback: int = 20) -> str:
"""Detect bullish or bearish divergence."""
price_higher = price.iloc[-1] > price.iloc[-lookback:].max() * 0.98
ind_lower = indicator.iloc[-1] < indicator.iloc[-lookback:].max() * 0.95
if price_higher and ind_lower:
return "bearish_divergence"
price_lower = price.iloc[-1] < price.iloc[-lookback:].min() * 1.02
ind_higher = indicator.iloc[-1] > indicator.iloc[-lookback:].min() * 1.05
if price_lower and ind_higher:
return "bullish_divergence"
return "none"Summary Checklist
Before using any indicator-based strategy:
- [ ] Dataset has 3x the longest indicator period in bars
- [ ] NaN values from warmup are excluded from signals
- [ ] Signals are shifted by 1 bar (no lookahead)
- [ ] Using 3-5 indicators max, each measuring different things
- [ ] Parameters are standard values, not over-optimized
- [ ] Volume indicators only used on tokens with reliable volume
- [ ] Tested on out-of-sample data from a different time period
- [ ] Gap detection and handling is implemented
- [ ] Stop-losses are defined independently of indicators
- [ ] Column names are verified after computation
pandas-ta — Top 20 Crypto Indicators Guide
The most useful pandas-ta indicators for crypto trading, organized by category with syntax, recommended parameters, and signal interpretation.
Trend Indicators
1. EMA (Exponential Moving Average)
- Call:
df.ta.ema(length=20) - Returns:
pd.SeriesnamedEMA_20 - Default:
length=10 - Crypto params: 9/21 for scalping, 20/50 for day trading, 50/200 for swing
- Signal: Price above EMA = bullish; EMA crossovers (fast > slow) = buy signal
- Note: Responds faster than SMA to recent price changes
2. SMA (Simple Moving Average)
- Call:
df.ta.sma(length=50) - Returns:
pd.SeriesnamedSMA_50 - Default:
length=10 - Crypto params: 20/50/200 are standard levels
- Signal: Golden cross (50 > 200) = bullish; death cross (50 < 200) = bearish
3. SuperTrend
- Call:
df.ta.supertrend(length=10, multiplier=3.0) - Returns: DataFrame with columns
SUPERT_10_3.0,SUPERTd_10_3.0,SUPERTl_10_3.0,SUPERTs_10_3.0 - Default:
length=7, multiplier=3.0 - Crypto params:
length=10, multiplier=3.0-4.0(wider for volatile tokens) - Signal:
SUPERTd= 1 (bullish) or -1 (bearish); direction flip = trend change - Note: Excellent standalone trend filter for crypto
4. ADX (Average Directional Index)
- Call:
df.ta.adx(length=14) - Returns: DataFrame with
ADX_14,DMP_14,DMN_14 - Default:
length=14 - Signal: ADX > 25 = trending, ADX < 20 = ranging; DMP > DMN = uptrend
5. HMA (Hull Moving Average)
- Call:
df.ta.hma(length=20) - Returns:
pd.SeriesnamedHMA_20 - Default:
length=10 - Signal: Minimal lag; direction change = early trend signal
- Note: Best low-lag moving average for fast crypto markets
6. VWMA (Volume Weighted Moving Average)
- Call:
df.ta.vwma(length=20) - Returns:
pd.SeriesnamedVWMA_20 - Default:
length=10 - Signal: Price above VWMA = volume-confirmed bullish; divergence = weakening trend
- Note: Less reliable on low-cap tokens with thin volume
Momentum Indicators
7. RSI (Relative Strength Index)
- Call:
df.ta.rsi(length=14) - Returns:
pd.SeriesnamedRSI_14 - Default:
length=14 - Crypto params: 7-10 for scalping, 14 for standard, 21 for swing
- Signal: > 70 overbought, < 30 oversold; divergence from price = reversal warning
- Note: In strong crypto trends, RSI can stay >70 for extended periods
8. MACD (Moving Average Convergence Divergence)
- Call:
df.ta.macd(fast=12, slow=26, signal=9) - Returns: DataFrame with
MACD_12_26_9,MACDh_12_26_9,MACDs_12_26_9 - Default:
fast=12, slow=26, signal=9 - Signal: Histogram > 0 = bullish momentum; zero-line crossover = trend change
- Note: Histogram is the most actionable component for timing
9. Stochastic Oscillator
- Call:
df.ta.stoch(k=14, d=3, smooth_k=3) - Returns: DataFrame with
STOCHk_14_3_3,STOCHd_14_3_3 - Default:
k=14, d=3, smooth_k=3 - Crypto params:
k=5, d=3, smooth_k=3for scalping - Signal: > 80 overbought, < 20 oversold; %K crossing above %D = buy
10. CCI (Commodity Channel Index)
- Call:
df.ta.cci(length=20) - Returns:
pd.SeriesnamedCCI_20_0.015 - Default:
length=14 - Signal: > 100 overbought, < -100 oversold; zero-line cross = trend direction
11. Williams %R
- Call:
df.ta.willr(length=14) - Returns:
pd.SeriesnamedWILLR_14 - Default:
length=14 - Signal: > -20 overbought, < -80 oversold; faster than RSI
12. ROC (Rate of Change)
- Call:
df.ta.roc(length=10) - Returns:
pd.SeriesnamedROC_10 - Default:
length=10 - Signal: Positive = upward momentum; crossing zero = trend shift
13. MFI (Money Flow Index)
- Call:
df.ta.mfi(length=14) - Returns:
pd.SeriesnamedMFI_14 - Default:
length=14 - Signal: > 80 overbought, < 20 oversold; volume-weighted RSI equivalent
- Note: Requires reliable volume data; unreliable on low-cap tokens
Volatility Indicators
14. Bollinger Bands
- Call:
df.ta.bbands(length=20, std=2.0) - Returns: DataFrame with
BBL_20_2.0,BBM_20_2.0,BBU_20_2.0,BBB_20_2.0,BBP_20_2.0 - Default:
length=5, std=2.0 - Crypto params:
length=20, std=2.5(wider for crypto volatility) - Signal: BBP (percent B) < 0 = below lower band; BBB (bandwidth) contracting = squeeze
- Note: Use BBB for squeeze detection; narrow BBB = imminent breakout
15. ATR (Average True Range)
- Call:
df.ta.atr(length=14) - Returns:
pd.SeriesnamedATRr_14 - Default:
length=14 - Crypto params: 7 for scalping, 14 for standard
- Signal: Rising ATR = increasing volatility; use for stop-loss distance (1.5-2x ATR)
- Note: Essential for position sizing — normalize by price for cross-asset comparison
16. Keltner Channels
- Call:
df.ta.kc(length=20, scalar=1.5) - Returns: DataFrame with
KCLe_20_1.5,KCBe_20_1.5,KCUe_20_1.5 - Default:
length=20, scalar=2 - Signal: BBands inside KC = TTM Squeeze (low volatility, breakout pending)
17. Donchian Channels
- Call:
df.ta.donchian(lower_length=20, upper_length=20) - Returns: DataFrame with
DCL_20_20,DCM_20_20,DCU_20_20 - Default:
lower_length=20, upper_length=20 - Signal: Price at upper channel = breakout; price at lower = breakdown
Volume Indicators
18. OBV (On-Balance Volume)
- Call:
df.ta.obv() - Returns:
pd.SeriesnamedOBV - Signal: OBV rising while price flat = accumulation; OBV falling while price rising = distribution
- Note: Trend of OBV matters more than absolute value
19. VWAP (Volume Weighted Average Price)
- Call:
df.ta.vwap() - Returns:
pd.SeriesnamedVWAP_D - Signal: Price above VWAP = bullish intraday bias; price below = bearish
- Note: Requires DatetimeIndex; resets daily at midnight UTC for crypto
- Crypto consideration: Anchored VWAP from key swing points is more useful than daily reset
20. CMF (Chaikin Money Flow)
- Call:
df.ta.cmf(length=20) - Returns:
pd.SeriesnamedCMF_20 - Default:
length=20 - Signal: > 0 = buying pressure (accumulation); < 0 = selling pressure (distribution)
- Note: Combines price and volume; confirm with OBV for stronger signals
Parameter Adjustment by Timeframe
| Timeframe | RSI | EMA fast/slow | BB length/std | ATR | Stoch k |
|---|---|---|---|---|---|
| 1m-5m | 5-7 | 5/13 | 10/2.0 | 5 | 5 |
| 15m | 9-14 | 9/21 | 15/2.0 | 10 | 9 |
| 1h | 14 | 20/50 | 20/2.0 | 14 | 14 |
| 4h | 14 | 20/50 | 20/2.5 | 14 | 14 |
| 1d | 14-21 | 50/200 | 20/2.5 | 14 | 14 |
General rule: Shorter timeframes need shorter indicator periods to remain responsive. Longer timeframes benefit from wider volatility bands (higher BB std, higher SuperTrend multiplier).
pandas-ta — Strategy Patterns for Crypto Trading
Pre-built indicator combinations for common crypto trading styles, with ta.Strategy definitions and signal generation logic.
Scalping Strategy (1m-5m Timeframes)
Fast indicators tuned for rapid entries and exits on volatile crypto pairs.
Indicator Set
import pandas_ta as ta
scalp_strategy = ta.Strategy(
name="Crypto Scalp",
ta=[
{"kind": "ema", "length": 9},
{"kind": "ema", "length": 21},
{"kind": "rsi", "length": 7},
{"kind": "stoch", "k": 5, "d": 3, "smooth_k": 3},
{"kind": "atr", "length": 7},
{"kind": "obv"},
]
)
df.ta.strategy(scalp_strategy)Signal Logic
# Buy: EMA9 > EMA21, RSI recovering from <30, Stoch %K crossing above %D
buy = (
(df["EMA_9"] > df["EMA_21"]) &
(df["RSI_7"] > 30) & (df["RSI_7"].shift(1) <= 30) &
(df["STOCHk_5_3_3"] > df["STOCHd_5_3_3"])
)
# Stop loss: 1.5x ATR below entry
stop_distance = df["ATRr_7"] * 1.5
# Exit: EMA9 crosses below EMA21 or RSI > 75
exit_signal = (df["EMA_9"] < df["EMA_21"]) | (df["RSI_7"] > 75)Notes
- Hold time: seconds to minutes
- Best for: high-volume pairs (SOL/USDC, major memecoins with volume)
- Risk: high false signal rate — combine with order flow or L2 data
Day Trading Strategy (15m-1h Timeframes)
Balanced indicator set for intraday position management.
Indicator Set
day_strategy = ta.Strategy(
name="Crypto Day Trade",
ta=[
{"kind": "ema", "length": 20},
{"kind": "ema", "length": 50},
{"kind": "macd", "fast": 12, "slow": 26, "signal": 9},
{"kind": "rsi", "length": 14},
{"kind": "bbands", "length": 20, "std": 2.0},
{"kind": "atr", "length": 14},
{"kind": "vwap"},
{"kind": "obv"},
]
)
df.ta.strategy(day_strategy)Signal Logic
# Buy: Price above VWAP, EMA20 > EMA50, MACD histogram positive, RSI 40-65
buy = (
(df["close"] > df["VWAP_D"]) &
(df["EMA_20"] > df["EMA_50"]) &
(df["MACDh_12_26_9"] > 0) &
(df["RSI_14"].between(40, 65))
)
# Take profit: Price touches upper Bollinger Band or RSI > 70
take_profit = (df["close"] >= df["BBU_20_2.0"]) | (df["RSI_14"] > 70)
# Stop loss: 2x ATR below entry or price below lower BB
stop_loss = (df["close"] < df["BBL_20_2.0"])Notes
- Hold time: minutes to hours
- Best for: established tokens with consistent volume
- Confirm direction with higher timeframe (4h trend)
Swing Trading Strategy (4h-1d Timeframes)
Slower indicators for multi-day positions with trend confirmation.
Indicator Set
swing_strategy = ta.Strategy(
name="Crypto Swing",
ta=[
{"kind": "ema", "length": 50},
{"kind": "ema", "length": 200},
{"kind": "supertrend", "length": 10, "multiplier": 3.0},
{"kind": "rsi", "length": 14},
{"kind": "adx", "length": 14},
{"kind": "atr", "length": 14},
{"kind": "bbands", "length": 20, "std": 2.5},
{"kind": "macd", "fast": 12, "slow": 26, "signal": 9},
]
)
df.ta.strategy(swing_strategy)Signal Logic
# Buy: EMA50 > EMA200, SuperTrend bullish, ADX > 25, RSI 40-60 (pullback entry)
buy = (
(df["EMA_50"] > df["EMA_200"]) &
(df["SUPERTd_10_3.0"] == 1) &
(df["ADX_14"] > 25) &
(df["RSI_14"].between(40, 60))
)
# Stop loss: SuperTrend flip or 2.5x ATR
stop_loss = df["SUPERTd_10_3.0"] == -1
# Take profit: RSI > 75 and MACD histogram declining
take_profit = (df["RSI_14"] > 75) & (df["MACDh_12_26_9"] < df["MACDh_12_26_9"].shift(1))Notes
- Hold time: days to weeks
- Best for: large-cap crypto (SOL, ETH, BTC)
- Use weekly chart for overall trend direction
PumpFun / Memecoin Scalping (1m-5m)
Ultra-fast indicators for new token launches with extreme volatility.
Indicator Set
pump_strategy = ta.Strategy(
name="PumpFun Scalp",
ta=[
{"kind": "ema", "length": 5},
{"kind": "ema", "length": 13},
{"kind": "rsi", "length": 5},
{"kind": "atr", "length": 5},
{"kind": "bbands", "length": 10, "std": 3.0},
]
)
df.ta.strategy(pump_strategy)Signal Logic
# Volume ratio (not a pandas-ta indicator, compute manually)
vol_ratio = df["volume"] / df["volume"].rolling(20).mean()
# Buy: EMA5 > EMA13, RSI recovering (was <25, now >35), volume 3x+ average
buy = (
(df["EMA_5"] > df["EMA_13"]) &
(df["RSI_5"] > 35) & (df["RSI_5"].shift(2) < 25) &
(vol_ratio > 3.0)
)
# Exit: EMA5 < EMA13 or RSI > 85 (take profit) or 2x ATR stop
exit_signal = (df["EMA_5"] < df["EMA_13"]) | (df["RSI_5"] > 85)Notes
- Hold time: seconds to minutes
- Extremely high risk — most PumpFun tokens go to zero
- Volume data is often unreliable — use as secondary confirmation only
- Always set hard stop-losses; never average down on memecoins
Multi-Timeframe Analysis Pattern
Combine signals from multiple timeframes for higher confidence entries.
import pandas as pd
import pandas_ta as ta
def analyze_multi_timeframe(
df_5m: pd.DataFrame,
df_1h: pd.DataFrame,
df_4h: pd.DataFrame,
) -> dict:
"""Score alignment across timeframes.
Args:
df_5m: 5-minute OHLCV DataFrame
df_1h: 1-hour OHLCV DataFrame
df_4h: 4-hour OHLCV DataFrame
Returns:
Dictionary with alignment score and per-timeframe signals.
"""
signals = {}
# 4h: overall trend direction
df_4h.ta.supertrend(length=10, multiplier=3.0, append=True)
df_4h.ta.adx(length=14, append=True)
trend_bull = (df_4h["SUPERTd_10_3.0"].iloc[-1] == 1)
trending = (df_4h["ADX_14"].iloc[-1] > 25)
signals["4h"] = "bullish" if trend_bull and trending else "bearish" if not trend_bull and trending else "neutral"
# 1h: momentum confirmation
df_1h.ta.macd(append=True)
df_1h.ta.rsi(append=True)
macd_bull = df_1h["MACDh_12_26_9"].iloc[-1] > 0
rsi_ok = 40 < df_1h["RSI_14"].iloc[-1] < 70
signals["1h"] = "bullish" if macd_bull and rsi_ok else "bearish" if not macd_bull else "neutral"
# 5m: entry timing
df_5m.ta.ema(length=9, append=True)
df_5m.ta.ema(length=21, append=True)
df_5m.ta.rsi(length=7, append=True)
ema_cross = df_5m["EMA_9"].iloc[-1] > df_5m["EMA_21"].iloc[-1]
rsi_entry = df_5m["RSI_7"].iloc[-1] < 60
signals["5m"] = "bullish" if ema_cross and rsi_entry else "neutral"
# Alignment score
bull_count = sum(1 for v in signals.values() if v == "bullish")
signals["alignment"] = bull_count / len(signals)
signals["recommendation"] = "strong" if bull_count == 3 else "moderate" if bull_count == 2 else "weak"
return signalsSignal Scoring Framework
Convert raw indicator values into a normalized score for comparison.
def score_indicators(df: pd.DataFrame) -> dict:
"""Score current indicator readings on a -100 to +100 scale.
Positive = bullish bias, negative = bearish bias.
"""
scores = {}
last = df.iloc[-1]
# RSI: 0-30 = bullish (oversold), 70-100 = bearish (overbought)
if "RSI_14" in df.columns:
rsi = last["RSI_14"]
scores["rsi"] = (50 - rsi) * 2 # 30→+40, 50→0, 70→-40
# MACD histogram: positive = bullish
if "MACDh_12_26_9" in df.columns:
macd_h = last["MACDh_12_26_9"]
scores["macd"] = min(max(macd_h * 100, -100), 100)
# BB position: below mid = bullish, above = bearish
if "BBP_20_2.0" in df.columns:
bbp = last["BBP_20_2.0"]
scores["bbands"] = (0.5 - bbp) * 200 # 0→+100, 0.5→0, 1→-100
# SuperTrend direction
st_col = [c for c in df.columns if c.startswith("SUPERTd")]
if st_col:
scores["supertrend"] = last[st_col[0]] * 50 # +50 or -50
# Composite
if scores:
scores["composite"] = sum(scores.values()) / len(scores)
return scoresRunning Strategies with ta.Strategy
# Run and inspect results
df.ta.strategy(my_strategy)
# All new columns added to df
new_cols = [c for c in df.columns if c not in ["open", "high", "low", "close", "volume"]]
print(f"Added {len(new_cols)} indicator columns: {new_cols}")
# Get the last row summary
summary = df[new_cols].iloc[-1].to_dict()
for name, value in summary.items():
print(f" {name}: {value:.4f}" if isinstance(value, float) else f" {name}: {value}")#!/usr/bin/env python3
"""Compute standard technical indicators on OHLCV data and generate signal summary.
Fetches OHLCV data from the Birdeye API (or generates synthetic demo data) and
computes a standard set of indicators: RSI, MACD, Bollinger Bands, EMA(20,50),
ATR, OBV, and SuperTrend. Outputs a signal summary and the last 10 bars with
indicator values.
Usage:
python scripts/compute_indicators.py # Demo mode with synthetic data
python scripts/compute_indicators.py --demo # Explicit demo mode
python scripts/compute_indicators.py --live # Fetch from Birdeye API
Dependencies:
uv pip install pandas pandas-ta httpx numpy
Environment Variables:
BIRDEYE_API_KEY: Your Birdeye API key (required for --live mode)
TOKEN_MINT: Solana token mint address (default: SOL)
"""
import argparse
import os
import sys
from typing import Optional
import numpy as np
import pandas as pd
import pandas_ta as ta
# ── Configuration ───────────────────────────────────────────────────
BIRDEYE_API_KEY = os.getenv("BIRDEYE_API_KEY", "")
TOKEN_MINT = os.getenv("TOKEN_MINT", "So11111111111111111111111111111111111111112") # SOL
BIRDEYE_BASE_URL = "https://public-api.birdeye.so"
# Indicator parameters
RSI_LENGTH = 14
MACD_FAST = 12
MACD_SLOW = 26
MACD_SIGNAL = 9
BB_LENGTH = 20
BB_STD = 2.0
EMA_FAST = 20
EMA_SLOW = 50
ATR_LENGTH = 14
SUPERTREND_LENGTH = 10
SUPERTREND_MULTIPLIER = 3.0
# ── Demo Data Generation ───────────────────────────────────────────
def generate_demo_ohlcv(bars: int = 200, seed: int = 42) -> pd.DataFrame:
"""Generate synthetic OHLCV data with realistic price action.
Creates a random walk with trend, mean reversion, and volume patterns
that approximate real crypto price behavior on a 1h timeframe.
Args:
bars: Number of OHLCV bars to generate.
seed: Random seed for reproducibility.
Returns:
DataFrame with columns: open, high, low, close, volume and DatetimeIndex.
"""
rng = np.random.default_rng(seed)
# Start price around $150 (SOL-like)
price = 150.0
prices = []
for _ in range(bars):
# Random returns with slight upward drift and mean reversion
ret = rng.normal(0.0002, 0.015) # ~1.5% hourly vol
price *= (1 + ret)
price = max(price, 1.0) # Floor at $1
# Generate OHLC from close
intra_vol = abs(rng.normal(0, 0.008))
high = price * (1 + intra_vol)
low = price * (1 - intra_vol)
open_price = price * (1 + rng.normal(0, 0.003))
# Ensure OHLC consistency
high = max(high, open_price, price)
low = min(low, open_price, price)
# Volume with some spikes
base_vol = rng.lognormal(mean=12, sigma=0.8)
if rng.random() < 0.05: # 5% chance of volume spike
base_vol *= rng.uniform(3, 8)
prices.append({
"open": round(open_price, 4),
"high": round(high, 4),
"low": round(low, 4),
"close": round(price, 4),
"volume": round(base_vol, 2),
})
# Create DatetimeIndex (1h bars)
end_time = pd.Timestamp.now(tz="UTC").floor("h")
index = pd.date_range(end=end_time, periods=bars, freq="1h")
df = pd.DataFrame(prices, index=index)
df.index.name = "datetime"
return df
# ── Birdeye API Fetch ──────────────────────────────────────────────
def fetch_ohlcv_birdeye(
token_mint: str,
interval: str = "1H",
limit: int = 200,
) -> Optional[pd.DataFrame]:
"""Fetch OHLCV data from the Birdeye API.
Args:
token_mint: Solana token mint address.
interval: Candle interval (1m, 5m, 15m, 30m, 1H, 4H, 1D).
limit: Number of candles to fetch (max 1000).
Returns:
DataFrame with OHLCV data, or None if the request fails.
Raises:
SystemExit: If BIRDEYE_API_KEY is not set.
"""
if not BIRDEYE_API_KEY:
print("Error: BIRDEYE_API_KEY environment variable not set.")
print("Set it with: export BIRDEYE_API_KEY=your_key_here")
sys.exit(1)
try:
import httpx
except ImportError:
print("Error: httpx not installed. Run: uv pip install httpx")
sys.exit(1)
import time
url = f"{BIRDEYE_BASE_URL}/defi/ohlcv"
time_to = int(time.time())
# Map interval to seconds for time_from calculation
interval_seconds = {
"1m": 60, "5m": 300, "15m": 900, "30m": 1800,
"1H": 3600, "4H": 14400, "1D": 86400,
}
secs = interval_seconds.get(interval, 3600)
time_from = time_to - (limit * secs)
params = {
"address": token_mint,
"type": interval,
"time_from": time_from,
"time_to": time_to,
}
headers = {
"X-API-KEY": BIRDEYE_API_KEY,
"Accept": "application/json",
}
try:
with httpx.Client(timeout=30.0) as client:
resp = client.get(url, params=params, headers=headers)
resp.raise_for_status()
data = resp.json()
except httpx.HTTPStatusError as e:
print(f"API error: {e.response.status_code} — {e.response.text[:200]}")
return None
except httpx.RequestError as e:
print(f"Request error: {e}")
return None
items = data.get("data", {}).get("items", [])
if not items:
print("No OHLCV data returned from Birdeye.")
return None
df = pd.DataFrame(items)
df["datetime"] = pd.to_datetime(df["unixTime"], unit="s", utc=True)
df = df.set_index("datetime")
df = df.rename(columns={
"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume",
})
df = df[["open", "high", "low", "close", "volume"]].astype(float)
df = df.sort_index()
return df
# ── Indicator Computation ──────────────────────────────────────────
def compute_indicators(df: pd.DataFrame) -> pd.DataFrame:
"""Compute standard technical indicators on an OHLCV DataFrame.
Adds the following indicators as new columns:
- RSI(14), MACD(12,26,9), Bollinger Bands(20,2), EMA(20), EMA(50),
ATR(14), OBV, SuperTrend(10,3)
Args:
df: OHLCV DataFrame with columns: open, high, low, close, volume.
Returns:
DataFrame with indicator columns appended.
"""
df = df.copy()
# Trend
df[f"EMA_{EMA_FAST}"] = df.ta.ema(length=EMA_FAST)
df[f"EMA_{EMA_SLOW}"] = df.ta.ema(length=EMA_SLOW)
st = df.ta.supertrend(length=SUPERTREND_LENGTH, multiplier=SUPERTREND_MULTIPLIER)
if st is not None:
df = pd.concat([df, st], axis=1)
# Momentum
df[f"RSI_{RSI_LENGTH}"] = df.ta.rsi(length=RSI_LENGTH)
macd = df.ta.macd(fast=MACD_FAST, slow=MACD_SLOW, signal=MACD_SIGNAL)
if macd is not None:
df = pd.concat([df, macd], axis=1)
# Volatility
bb = df.ta.bbands(length=BB_LENGTH, std=BB_STD)
if bb is not None:
df = pd.concat([df, bb], axis=1)
df[f"ATRr_{ATR_LENGTH}"] = df.ta.atr(length=ATR_LENGTH)
# Volume
df["OBV"] = df.ta.obv()
return df
# ── Signal Generation ──────────────────────────────────────────────
def generate_signals(df: pd.DataFrame) -> dict:
"""Generate trading signals from computed indicators.
Evaluates the last bar's indicator values and classifies each
as bullish, bearish, or neutral.
Args:
df: DataFrame with indicator columns (from compute_indicators).
Returns:
Dictionary with signal assessments per indicator and overall bias.
"""
last = df.iloc[-1]
prev = df.iloc[-2]
signals: dict = {}
# RSI
rsi_col = f"RSI_{RSI_LENGTH}"
if rsi_col in df.columns and pd.notna(last[rsi_col]):
rsi_val = last[rsi_col]
if rsi_val < 30:
signals["RSI"] = {"value": round(rsi_val, 2), "signal": "oversold (bullish reversal zone)"}
elif rsi_val > 70:
signals["RSI"] = {"value": round(rsi_val, 2), "signal": "overbought (bearish reversal zone)"}
elif rsi_val > 50:
signals["RSI"] = {"value": round(rsi_val, 2), "signal": "bullish momentum"}
else:
signals["RSI"] = {"value": round(rsi_val, 2), "signal": "bearish momentum"}
# MACD
macd_h_col = f"MACDh_{MACD_FAST}_{MACD_SLOW}_{MACD_SIGNAL}"
if macd_h_col in df.columns and pd.notna(last[macd_h_col]):
macd_val = last[macd_h_col]
macd_prev = prev[macd_h_col] if pd.notna(prev[macd_h_col]) else 0
if macd_val > 0 and macd_val > macd_prev:
signals["MACD"] = {"value": round(macd_val, 4), "signal": "bullish (histogram rising)"}
elif macd_val > 0:
signals["MACD"] = {"value": round(macd_val, 4), "signal": "bullish (histogram falling)"}
elif macd_val < 0 and macd_val < macd_prev:
signals["MACD"] = {"value": round(macd_val, 4), "signal": "bearish (histogram falling)"}
else:
signals["MACD"] = {"value": round(macd_val, 4), "signal": "bearish (histogram rising)"}
# EMA crossover
ema_f_col = f"EMA_{EMA_FAST}"
ema_s_col = f"EMA_{EMA_SLOW}"
if ema_f_col in df.columns and ema_s_col in df.columns:
if pd.notna(last[ema_f_col]) and pd.notna(last[ema_s_col]):
if last[ema_f_col] > last[ema_s_col]:
signals["EMA_Cross"] = {
"value": f"{last[ema_f_col]:.4f} / {last[ema_s_col]:.4f}",
"signal": "bullish (fast above slow)",
}
else:
signals["EMA_Cross"] = {
"value": f"{last[ema_f_col]:.4f} / {last[ema_s_col]:.4f}",
"signal": "bearish (fast below slow)",
}
# Bollinger Bands position
bbp_col = f"BBP_{BB_LENGTH}_{BB_STD}"
if bbp_col in df.columns and pd.notna(last[bbp_col]):
bbp = last[bbp_col]
if bbp < 0:
signals["BBands"] = {"value": round(bbp, 4), "signal": "below lower band (oversold)"}
elif bbp > 1:
signals["BBands"] = {"value": round(bbp, 4), "signal": "above upper band (overbought)"}
elif bbp < 0.3:
signals["BBands"] = {"value": round(bbp, 4), "signal": "near lower band (bullish zone)"}
elif bbp > 0.7:
signals["BBands"] = {"value": round(bbp, 4), "signal": "near upper band (bearish zone)"}
else:
signals["BBands"] = {"value": round(bbp, 4), "signal": "mid-band (neutral)"}
# SuperTrend
st_d_col = f"SUPERTd_{SUPERTREND_LENGTH}_{SUPERTREND_MULTIPLIER}"
if st_d_col in df.columns and pd.notna(last[st_d_col]):
st_dir = int(last[st_d_col])
signals["SuperTrend"] = {
"value": st_dir,
"signal": "bullish" if st_dir == 1 else "bearish",
}
# ATR (informational)
atr_col = f"ATRr_{ATR_LENGTH}"
if atr_col in df.columns and pd.notna(last[atr_col]):
atr_val = last[atr_col]
atr_pct = (atr_val / last["close"]) * 100
signals["ATR"] = {
"value": round(atr_val, 4),
"signal": f"{atr_pct:.2f}% of price (volatility gauge)",
}
# OBV trend
if "OBV" in df.columns and pd.notna(last["OBV"]):
obv_sma = df["OBV"].rolling(20).mean().iloc[-1]
if pd.notna(obv_sma):
if last["OBV"] > obv_sma:
signals["OBV"] = {"value": int(last["OBV"]), "signal": "above 20-bar average (accumulation)"}
else:
signals["OBV"] = {"value": int(last["OBV"]), "signal": "below 20-bar average (distribution)"}
# Overall bias
bullish_count = sum(
1 for s in signals.values()
if "bullish" in str(s.get("signal", "")).lower()
or "oversold" in str(s.get("signal", "")).lower()
or "accumulation" in str(s.get("signal", "")).lower()
)
bearish_count = sum(
1 for s in signals.values()
if "bearish" in str(s.get("signal", "")).lower()
or "overbought" in str(s.get("signal", "")).lower()
or "distribution" in str(s.get("signal", "")).lower()
)
total = len(signals)
if total > 0:
if bullish_count > bearish_count + 1:
signals["_overall"] = "BULLISH"
elif bearish_count > bullish_count + 1:
signals["_overall"] = "BEARISH"
else:
signals["_overall"] = "NEUTRAL"
signals["_score"] = f"{bullish_count} bullish / {bearish_count} bearish / {total - bullish_count - bearish_count} neutral"
return signals
# ── Display ─────────────────────────────────────────────────────────
def print_signal_summary(signals: dict) -> None:
"""Print a formatted signal summary.
Args:
signals: Dictionary from generate_signals().
"""
print("\n" + "=" * 60)
print(" SIGNAL SUMMARY")
print("=" * 60)
overall = signals.pop("_overall", "N/A")
score = signals.pop("_score", "")
for name, info in signals.items():
if isinstance(info, dict):
print(f" {name:<14} {str(info['value']):>12} → {info['signal']}")
print("-" * 60)
print(f" Overall Bias: {overall}")
if score:
print(f" Score: {score}")
print("=" * 60)
def print_last_bars(df: pd.DataFrame, n: int = 10) -> None:
"""Print the last N bars with key indicator values.
Args:
df: DataFrame with OHLCV and indicator columns.
n: Number of bars to display.
"""
display_cols = ["close", f"RSI_{RSI_LENGTH}", f"EMA_{EMA_FAST}", f"EMA_{EMA_SLOW}"]
macd_h_col = f"MACDh_{MACD_FAST}_{MACD_SLOW}_{MACD_SIGNAL}"
if macd_h_col in df.columns:
display_cols.append(macd_h_col)
bbp_col = f"BBP_{BB_LENGTH}_{BB_STD}"
if bbp_col in df.columns:
display_cols.append(bbp_col)
atr_col = f"ATRr_{ATR_LENGTH}"
if atr_col in df.columns:
display_cols.append(atr_col)
available_cols = [c for c in display_cols if c in df.columns]
print(f"\n Last {n} bars:")
print("-" * 100)
tail = df[available_cols].tail(n)
# Format for readable output
with pd.option_context("display.float_format", "{:.4f}".format, "display.width", 120):
print(tail.to_string())
print()
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Main entry point: fetch data, compute indicators, display signals."""
parser = argparse.ArgumentParser(description="Compute technical indicators on OHLCV data")
parser.add_argument("--live", action="store_true", help="Fetch live data from Birdeye API")
parser.add_argument("--demo", action="store_true", help="Use synthetic demo data (default)")
parser.add_argument("--bars", type=int, default=200, help="Number of bars (default: 200)")
args = parser.parse_args()
# Default to demo mode
use_live = args.live and not args.demo
if use_live:
print(f"Fetching OHLCV data from Birdeye for {TOKEN_MINT[:8]}...")
df = fetch_ohlcv_birdeye(TOKEN_MINT, interval="1H", limit=args.bars)
if df is None:
print("Failed to fetch live data. Falling back to demo mode.")
df = generate_demo_ohlcv(bars=args.bars)
else:
print("Using synthetic demo data (1H bars, SOL-like price action)")
df = generate_demo_ohlcv(bars=args.bars)
print(f"Data: {len(df)} bars from {df.index[0]} to {df.index[-1]}")
print(f"Price range: {df['close'].min():.2f} — {df['close'].max():.2f}")
# Compute indicators
print("\nComputing indicators...")
df = compute_indicators(df)
indicator_cols = [c for c in df.columns if c not in ["open", "high", "low", "close", "volume"]]
print(f"Added {len(indicator_cols)} indicator columns")
# Generate and display signals
signals = generate_signals(df)
print_signal_summary(signals)
# Show last N bars
print_last_bars(df, n=10)
# Information notice
print("Note: This output is for informational and educational purposes only.")
print("It does not constitute financial advice or a recommendation to trade.\n")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Run multiple indicator strategy profiles and score signal alignment.
Defines three strategy profiles (trend following, mean reversion, momentum),
runs all three on the same OHLCV data, scores each strategy's current signal
strength, and reports which strategy is most aligned with current conditions.
Usage:
python scripts/multi_indicator_scan.py # Demo mode
python scripts/multi_indicator_scan.py --demo # Explicit demo mode
python scripts/multi_indicator_scan.py --live # Fetch from Birdeye API
Dependencies:
uv pip install pandas pandas-ta httpx numpy
Environment Variables:
BIRDEYE_API_KEY: Your Birdeye API key (required for --live mode)
TOKEN_MINT: Solana token mint address (default: SOL)
"""
import argparse
import os
import sys
from typing import Optional
import numpy as np
import pandas as pd
import pandas_ta as ta
# ── Configuration ───────────────────────────────────────────────────
BIRDEYE_API_KEY = os.getenv("BIRDEYE_API_KEY", "")
TOKEN_MINT = os.getenv("TOKEN_MINT", "So11111111111111111111111111111111111111112")
BIRDEYE_BASE_URL = "https://public-api.birdeye.so"
# ── Strategy Definitions ───────────────────────────────────────────
TREND_STRATEGY = ta.Strategy(
name="Trend Following",
description="EMA crossover with ADX filter and SuperTrend confirmation",
ta=[
{"kind": "ema", "length": 20},
{"kind": "ema", "length": 50},
{"kind": "adx", "length": 14},
{"kind": "supertrend", "length": 10, "multiplier": 3.0},
{"kind": "atr", "length": 14},
{"kind": "sma", "length": 200},
],
)
REVERSION_STRATEGY = ta.Strategy(
name="Mean Reversion",
description="Oversold/overbought detection with BB and oscillators",
ta=[
{"kind": "rsi", "length": 14},
{"kind": "bbands", "length": 20, "std": 2.0},
{"kind": "stoch", "k": 14, "d": 3, "smooth_k": 3},
{"kind": "cci", "length": 20},
{"kind": "willr", "length": 14},
],
)
MOMENTUM_STRATEGY = ta.Strategy(
name="Momentum",
description="MACD + RSI + volume confirmation for momentum trades",
ta=[
{"kind": "macd", "fast": 12, "slow": 26, "signal": 9},
{"kind": "rsi", "length": 14},
{"kind": "obv"},
{"kind": "roc", "length": 10},
{"kind": "mfi", "length": 14},
],
)
# ── Data Generation / Fetching ─────────────────────────────────────
def generate_demo_ohlcv(bars: int = 200, seed: int = 42) -> pd.DataFrame:
"""Generate synthetic OHLCV data with realistic crypto price action.
Args:
bars: Number of bars to generate.
seed: Random seed for reproducibility.
Returns:
OHLCV DataFrame with DatetimeIndex.
"""
rng = np.random.default_rng(seed)
price = 150.0
prices = []
for i in range(bars):
# Add regime changes: trending and mean-reverting phases
phase = (i // 50) % 3
if phase == 0:
drift = 0.001 # Uptrend
elif phase == 1:
drift = -0.0005 # Mild downtrend
else:
drift = 0.0 # Range
ret = rng.normal(drift, 0.015)
price *= (1 + ret)
price = max(price, 1.0)
intra_vol = abs(rng.normal(0, 0.008))
high = price * (1 + intra_vol)
low = price * (1 - intra_vol)
open_price = price * (1 + rng.normal(0, 0.003))
high = max(high, open_price, price)
low = min(low, open_price, price)
base_vol = rng.lognormal(mean=12, sigma=0.8)
if rng.random() < 0.05:
base_vol *= rng.uniform(3, 8)
prices.append({
"open": round(open_price, 4),
"high": round(high, 4),
"low": round(low, 4),
"close": round(price, 4),
"volume": round(base_vol, 2),
})
end_time = pd.Timestamp.now(tz="UTC").floor("h")
index = pd.date_range(end=end_time, periods=bars, freq="1h")
df = pd.DataFrame(prices, index=index)
df.index.name = "datetime"
return df
def fetch_ohlcv_birdeye(
token_mint: str,
interval: str = "1H",
limit: int = 200,
) -> Optional[pd.DataFrame]:
"""Fetch OHLCV data from the Birdeye API.
Args:
token_mint: Solana token mint address.
interval: Candle interval.
limit: Number of candles to fetch.
Returns:
OHLCV DataFrame or None on failure.
"""
if not BIRDEYE_API_KEY:
print("Error: BIRDEYE_API_KEY not set.")
sys.exit(1)
try:
import httpx
except ImportError:
print("Error: httpx not installed. Run: uv pip install httpx")
sys.exit(1)
import time
url = f"{BIRDEYE_BASE_URL}/defi/ohlcv"
interval_seconds = {
"1m": 60, "5m": 300, "15m": 900, "30m": 1800,
"1H": 3600, "4H": 14400, "1D": 86400,
}
secs = interval_seconds.get(interval, 3600)
time_to = int(time.time())
time_from = time_to - (limit * secs)
headers = {"X-API-KEY": BIRDEYE_API_KEY, "Accept": "application/json"}
params = {
"address": token_mint,
"type": interval,
"time_from": time_from,
"time_to": time_to,
}
try:
with httpx.Client(timeout=30.0) as client:
resp = client.get(url, params=params, headers=headers)
resp.raise_for_status()
data = resp.json()
except httpx.HTTPStatusError as e:
print(f"API error: {e.response.status_code}")
return None
except httpx.RequestError as e:
print(f"Request error: {e}")
return None
items = data.get("data", {}).get("items", [])
if not items:
print("No data returned from Birdeye.")
return None
df = pd.DataFrame(items)
df["datetime"] = pd.to_datetime(df["unixTime"], unit="s", utc=True)
df = df.set_index("datetime").rename(columns={
"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume",
})
df = df[["open", "high", "low", "close", "volume"]].astype(float).sort_index()
return df
# ── Strategy Scoring ───────────────────────────────────────────────
def score_trend(df: pd.DataFrame) -> dict:
"""Score trend following strategy signals.
Evaluates EMA crossover, ADX strength, SuperTrend direction, and
price position relative to SMA200.
Args:
df: DataFrame with trend strategy indicators computed.
Returns:
Dictionary with individual scores and total.
"""
last = df.iloc[-1]
scores: dict = {}
# EMA crossover: EMA20 vs EMA50
if "EMA_20" in df.columns and "EMA_50" in df.columns:
if pd.notna(last["EMA_20"]) and pd.notna(last["EMA_50"]):
diff_pct = (last["EMA_20"] - last["EMA_50"]) / last["EMA_50"] * 100
scores["ema_cross"] = {
"score": min(max(diff_pct * 10, -100), 100),
"detail": f"EMA20/50 diff: {diff_pct:+.2f}%",
}
# ADX trend strength
if "ADX_14" in df.columns and pd.notna(last.get("ADX_14")):
adx = last["ADX_14"]
if adx > 40:
strength = 100
elif adx > 25:
strength = 60
elif adx > 20:
strength = 30
else:
strength = -20 # No trend = bad for trend strategy
scores["adx"] = {"score": strength, "detail": f"ADX: {adx:.1f}"}
# SuperTrend direction
st_col = f"SUPERTd_10_3.0"
if st_col in df.columns and pd.notna(last.get(st_col)):
direction = int(last[st_col])
scores["supertrend"] = {
"score": direction * 80,
"detail": f"SuperTrend: {'bullish' if direction == 1 else 'bearish'}",
}
# Price vs SMA200
if "SMA_200" in df.columns and pd.notna(last.get("SMA_200")):
above = last["close"] > last["SMA_200"]
pct_diff = (last["close"] - last["SMA_200"]) / last["SMA_200"] * 100
scores["sma200"] = {
"score": min(max(pct_diff * 5, -100), 100),
"detail": f"Price {'above' if above else 'below'} SMA200 by {abs(pct_diff):.1f}%",
}
total = sum(s["score"] for s in scores.values()) / max(len(scores), 1)
return {"scores": scores, "total": round(total, 1)}
def score_reversion(df: pd.DataFrame) -> dict:
"""Score mean reversion strategy signals.
Evaluates RSI, Bollinger Band position, Stochastic, CCI, and Williams %R
for oversold/overbought conditions.
Args:
df: DataFrame with reversion strategy indicators computed.
Returns:
Dictionary with individual scores and total.
"""
last = df.iloc[-1]
scores: dict = {}
# RSI: oversold = bullish for reversion buy
if "RSI_14" in df.columns and pd.notna(last.get("RSI_14")):
rsi = last["RSI_14"]
# Map: 30→+100 (oversold=buy), 50→0, 70→-100 (overbought=sell)
score = (50 - rsi) * 5
scores["rsi"] = {
"score": min(max(score, -100), 100),
"detail": f"RSI: {rsi:.1f}",
}
# Bollinger Band %B
if "BBP_20_2.0" in df.columns and pd.notna(last.get("BBP_20_2.0")):
bbp = last["BBP_20_2.0"]
# 0→+100 (at lower band=buy), 0.5→0, 1→-100 (at upper band=sell)
score = (0.5 - bbp) * 200
scores["bbands"] = {
"score": min(max(score, -100), 100),
"detail": f"BB %B: {bbp:.3f}",
}
# Stochastic %K
stoch_col = "STOCHk_14_3_3"
if stoch_col in df.columns and pd.notna(last.get(stoch_col)):
stoch_k = last[stoch_col]
score = (50 - stoch_k) * 2.5
scores["stoch"] = {
"score": min(max(score, -100), 100),
"detail": f"Stoch %K: {stoch_k:.1f}",
}
# CCI
cci_col = [c for c in df.columns if c.startswith("CCI_")]
if cci_col and pd.notna(last.get(cci_col[0])):
cci = last[cci_col[0]]
score = -cci * 0.5 # CCI -200→+100, CCI +200→-100
scores["cci"] = {
"score": min(max(score, -100), 100),
"detail": f"CCI: {cci:.1f}",
}
# Williams %R
if "WILLR_14" in df.columns and pd.notna(last.get("WILLR_14")):
willr = last["WILLR_14"]
# -100→+100 (oversold=buy), -50→0, 0→-100 (overbought=sell)
score = (-50 - willr) * 2
scores["willr"] = {
"score": min(max(score, -100), 100),
"detail": f"Williams %R: {willr:.1f}",
}
total = sum(s["score"] for s in scores.values()) / max(len(scores), 1)
return {"scores": scores, "total": round(total, 1)}
def score_momentum(df: pd.DataFrame) -> dict:
"""Score momentum strategy signals.
Evaluates MACD histogram, RSI direction, OBV trend, ROC, and MFI
for momentum strength.
Args:
df: DataFrame with momentum strategy indicators computed.
Returns:
Dictionary with individual scores and total.
"""
last = df.iloc[-1]
prev = df.iloc[-2] if len(df) > 1 else last
scores: dict = {}
# MACD histogram
macd_h = "MACDh_12_26_9"
if macd_h in df.columns and pd.notna(last.get(macd_h)):
val = last[macd_h]
rising = val > (prev.get(macd_h, 0) if pd.notna(prev.get(macd_h)) else 0)
score = 80 if val > 0 and rising else 40 if val > 0 else -40 if val < 0 and not rising else -80
scores["macd"] = {"score": score, "detail": f"MACD-H: {val:.4f} ({'rising' if rising else 'falling'})"}
# RSI momentum (above/below 50)
if "RSI_14" in df.columns and pd.notna(last.get("RSI_14")):
rsi = last["RSI_14"]
score = (rsi - 50) * 2 # 60→+20, 40→-20
scores["rsi"] = {
"score": min(max(score, -100), 100),
"detail": f"RSI: {rsi:.1f}",
}
# OBV trend (above/below 20-period SMA)
if "OBV" in df.columns and pd.notna(last.get("OBV")):
obv_sma = df["OBV"].rolling(20).mean().iloc[-1]
if pd.notna(obv_sma) and obv_sma != 0:
pct_diff = (last["OBV"] - obv_sma) / abs(obv_sma) * 100
score = min(max(pct_diff * 2, -100), 100)
scores["obv"] = {"score": score, "detail": f"OBV vs SMA20: {pct_diff:+.1f}%"}
# ROC
if "ROC_10" in df.columns and pd.notna(last.get("ROC_10")):
roc = last["ROC_10"]
score = min(max(roc * 10, -100), 100)
scores["roc"] = {"score": score, "detail": f"ROC(10): {roc:+.2f}%"}
# MFI
if "MFI_14" in df.columns and pd.notna(last.get("MFI_14")):
mfi = last["MFI_14"]
score = (mfi - 50) * 2
scores["mfi"] = {
"score": min(max(score, -100), 100),
"detail": f"MFI: {mfi:.1f}",
}
total = sum(s["score"] for s in scores.values()) / max(len(scores), 1)
return {"scores": scores, "total": round(total, 1)}
# ── Display ─────────────────────────────────────────────────────────
def print_strategy_report(name: str, result: dict) -> None:
"""Print a formatted strategy score report.
Args:
name: Strategy name.
result: Score dictionary from a score_* function.
"""
total = result["total"]
bar_len = 20
filled = int(abs(total) / 100 * bar_len)
if total >= 0:
bar = "[" + "#" * filled + "." * (bar_len - filled) + "]"
direction = "BULLISH"
else:
bar = "[" + "." * (bar_len - filled) + "#" * filled + "]"
direction = "BEARISH"
print(f"\n {name}")
print(f" {'─' * 50}")
for indicator_name, info in result["scores"].items():
score = info["score"]
detail = info["detail"]
arrow = "▲" if score > 0 else "▼" if score < 0 else "─"
print(f" {arrow} {indicator_name:<14} {score:>+6.0f} {detail}")
print(f" {'─' * 50}")
print(f" Total: {total:>+6.1f} / 100 {bar} {direction}")
def print_recommendation(
trend_result: dict,
reversion_result: dict,
momentum_result: dict,
) -> None:
"""Print which strategy best matches current conditions.
Args:
trend_result: Trend strategy scores.
reversion_result: Mean reversion strategy scores.
momentum_result: Momentum strategy scores.
"""
strategies = {
"Trend Following": trend_result["total"],
"Mean Reversion": reversion_result["total"],
"Momentum": momentum_result["total"],
}
# Find most aligned (highest absolute score with positive = bullish bias)
best_name = max(strategies, key=lambda k: abs(strategies[k]))
best_score = strategies[best_name]
print("\n" + "=" * 60)
print(" STRATEGY ALIGNMENT SUMMARY")
print("=" * 60)
for name, score in sorted(strategies.items(), key=lambda x: abs(x[1]), reverse=True):
marker = " ◀ BEST FIT" if name == best_name else ""
direction = "BULL" if score > 0 else "BEAR" if score < 0 else "FLAT"
print(f" {name:<20} {score:>+6.1f} [{direction}]{marker}")
print("-" * 60)
if abs(best_score) < 20:
print(" Condition: MIXED — no clear strategy alignment.")
print(" Consider: Reducing position size or waiting for clearer signals.")
elif best_score > 0:
print(f" Condition: {best_name} signals are bullish.")
print(f" Strength: {'Strong' if abs(best_score) > 60 else 'Moderate' if abs(best_score) > 35 else 'Weak'}")
else:
print(f" Condition: {best_name} signals are bearish.")
print(f" Strength: {'Strong' if abs(best_score) > 60 else 'Moderate' if abs(best_score) > 35 else 'Weak'}")
print("=" * 60)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run multi-strategy indicator scan and display results."""
parser = argparse.ArgumentParser(description="Multi-indicator strategy scan")
parser.add_argument("--live", action="store_true", help="Fetch live data from Birdeye API")
parser.add_argument("--demo", action="store_true", help="Use synthetic demo data (default)")
parser.add_argument("--bars", type=int, default=200, help="Number of bars (default: 200)")
args = parser.parse_args()
use_live = args.live and not args.demo
if use_live:
print(f"Fetching data from Birdeye for {TOKEN_MINT[:8]}...")
df = fetch_ohlcv_birdeye(TOKEN_MINT, interval="1H", limit=args.bars)
if df is None:
print("Falling back to demo mode.")
df = generate_demo_ohlcv(bars=args.bars)
else:
print("Using synthetic demo data (1H bars, SOL-like price action)")
df = generate_demo_ohlcv(bars=args.bars)
print(f"Data: {len(df)} bars | Price: {df['close'].iloc[-1]:.2f}")
print(f"Range: {df.index[0]} to {df.index[-1]}")
# Run each strategy on a copy to avoid column collisions
print("\nRunning strategy scans...")
# Trend
df_trend = df.copy()
df_trend.ta.strategy(TREND_STRATEGY)
trend_result = score_trend(df_trend)
print_strategy_report("Trend Following", trend_result)
# Mean Reversion
df_rev = df.copy()
df_rev.ta.strategy(REVERSION_STRATEGY)
reversion_result = score_reversion(df_rev)
print_strategy_report("Mean Reversion", reversion_result)
# Momentum
df_mom = df.copy()
df_mom.ta.strategy(MOMENTUM_STRATEGY)
momentum_result = score_momentum(df_mom)
print_strategy_report("Momentum", momentum_result)
# Recommendation
print_recommendation(trend_result, reversion_result, momentum_result)
print("\nNote: This analysis is for informational and educational purposes only.")
print("It does not constitute financial advice or a recommendation to trade.\n")
if __name__ == "__main__":
main()
Related skills
FAQ
How many indicators are available?
pandas-ta exposes 130+ indicators across trend, momentum, volatility, volume, and overlap categories via df.ta.
What DataFrame format is required?
Lowercase column names (close, not Close) and a DatetimeIndex for time-aware indicators like VWAP.