Skip to content

Entry Signals

Filter strategy entries using 80+ technical analysis signals powered by pandas-ta-classic. Use signal_dates to compute valid dates from stock price data, then pass them as entry_dates or exit_dates to any strategy.

import optopsy as op

# Load stock and options data separately
stocks = op.load_cached_stocks("SPY")
options = op.load_cached_options("SPY")

# Enter only when RSI(14) is below 30
entry_dates = op.signal_dates(stocks, op.rsi_below(14, 30))
results = op.long_calls(options, entry_dates=entry_dates)

# Require RSI below 30 for 5 consecutive days
entry_dates = op.signal_dates(stocks, op.sustained(op.rsi_below(14, 30), days=5))
results = op.long_calls(options, entry_dates=entry_dates)

# Compose signals with & and |
sig = op.signal(op.rsi_below(14, 30)) & op.signal(op.day_of_week(3))  # Oversold + Thursday
entry_dates = op.signal_dates(stocks, sig)
results = op.long_calls(options, entry_dates=entry_dates)

How It Works

Signals are computed on stock price data (OHLCV), not options data. The flow is:

  1. Load stock data — via load_cached_stocks() or your own DataFrame
  2. Build signals — run signal functions on stock data to get entry/exit dates
  3. Run strategy — pass the dates to a strategy along with options data
# Stock data needs: underlying_symbol, quote_date, close
# Optional: open, high, low, volume (for OHLCV signals)
stocks = op.load_cached_stocks("SPY")

# Or bring your own DataFrame
stocks = pd.DataFrame({
    "underlying_symbol": "SPY",
    "quote_date": my_dates,
    "close": my_prices,
})

Available Signals

Momentum

Signal Description Default Parameters
rsi_below, rsi_above RSI threshold (oversold / overbought) period=14, threshold=30 / 70
macd_cross_above, macd_cross_below MACD / signal line crossover fast=12, slow=26, signal_period=9
stoch_below, stoch_above Stochastic %K threshold k_period=14, d_period=3, threshold=20 / 80
stochrsi_below, stochrsi_above Stochastic RSI %K threshold period=14, rsi_period=14, k_smooth=3, d_smooth=3, threshold=20 / 80
willr_below, willr_above Williams %R threshold period=14, threshold=-80 / -20
cci_below, cci_above Commodity Channel Index threshold period=20, threshold=-100 / 100
roc_above, roc_below Rate of Change threshold period=10, threshold=0
ppo_cross_above, ppo_cross_below Percentage Price Oscillator crossover fast=12, slow=26, signal_period=9
tsi_cross_above, tsi_cross_below True Strength Index crossover long=25, short=13, signal_period=13
cmo_above, cmo_below Chande Momentum Oscillator threshold period=14, threshold=50 / -50
uo_above, uo_below Ultimate Oscillator threshold fast=7, medium=14, slow=28, threshold=70 / 30
squeeze_on, squeeze_off Squeeze (BB inside/outside KC) bb_length=20, bb_std=2.0, kc_length=20, kc_scalar=1.5
ao_above, ao_below Awesome Oscillator threshold fast=5, slow=34, threshold=0
smi_cross_above, smi_cross_below Stochastic Momentum Index crossover fast=5, slow=20, signal_period=5
kst_cross_above, kst_cross_below Know Sure Thing crossover (uses pandas-ta defaults)
fisher_cross_above, fisher_cross_below Fisher Transform crossover period=9

Overlap (Moving Averages)

Signal Description Default Parameters
sma_above, sma_below Price vs. Simple Moving Average period=20
ema_cross_above, ema_cross_below EMA fast/slow crossover fast=10, slow=50
dema_cross_above, dema_cross_below Double EMA crossover fast=10, slow=50
tema_cross_above, tema_cross_below Triple EMA crossover fast=10, slow=50
hma_cross_above, hma_cross_below Hull MA crossover fast=10, slow=50
kama_cross_above, kama_cross_below Kaufman Adaptive MA crossover fast=10, slow=50
wma_cross_above, wma_cross_below Weighted MA crossover fast=10, slow=50
zlma_cross_above, zlma_cross_below Zero-Lag MA crossover fast=10, slow=50
alma_cross_above, alma_cross_below Arnaud Legoux MA crossover fast=10, slow=50

Volatility

Signal Description Default Parameters
atr_above, atr_below ATR vs. median ATR regime period=14, multiplier=1.0
bb_above_upper, bb_below_lower Price vs. Bollinger Bands length=20, std=2.0
kc_above_upper, kc_below_lower Price vs. Keltner Channel length=20, scalar=1.5
donchian_above_upper, donchian_below_lower Price vs. Donchian Channel lower_length=20, upper_length=20
natr_above, natr_below Normalized ATR threshold (% of price) period=14, threshold=2.0 / 1.0
massi_above, massi_below Mass Index threshold (reversal signal) fast=9, slow=25, threshold=27 / 26.5

Trend

Signal Description Default Parameters
adx_above, adx_below Average Directional Index threshold period=14, threshold=25 / 20
aroon_cross_above, aroon_cross_below Aroon Up/Down crossover period=25
supertrend_buy, supertrend_sell Supertrend direction flip period=7, multiplier=3.0
psar_buy, psar_sell Parabolic SAR direction flip af0=0.02, af=0.02, max_af=0.2
chop_above, chop_below Choppiness Index threshold period=14, threshold=61.8 / 38.2
vhf_above, vhf_below Vertical Horizontal Filter threshold period=28, threshold=0.4

Volume

Require OHLCV data with a volume column.

Signal Description Default Parameters
mfi_above, mfi_below Money Flow Index threshold period=14, threshold=80 / 20
obv_cross_above_sma, obv_cross_below_sma OBV vs. its SMA crossover sma_period=20
cmf_above, cmf_below Chaikin Money Flow threshold period=20, threshold=0.05 / -0.05
ad_cross_above_sma, ad_cross_below_sma A/D Line vs. its SMA crossover sma_period=20

IV Rank

Require options data with an implied_volatility column. IV rank signals are the exception — they run on options data, not stock data.

Signal Description Default Parameters
iv_rank_above, iv_rank_below IV rank percentile threshold threshold=0.5, window=252

Calendar

Signal Description Default Parameters
day_of_week Restrict to specific weekdays Days: 0=Mon ... 4=Fri

Custom

Signal Description Default Parameters
custom_signal Boolean flag from a DataFrame column flag_col='signal'

Combinators

Function Description
sustained(signal, days=5) Require signal True for N consecutive bars
and_signals(sig1, sig2, ...) All signals must be True
or_signals(sig1, sig2, ...) At least one signal must be True
Signal class with & / \| Fluent operator chaining

Signal Examples

RSI - enter on oversold, exit on overbought

import optopsy as op

stocks = op.load_cached_stocks("SPY")
options = op.load_cached_options("SPY")

entry_dates = op.signal_dates(stocks, op.rsi_below(period=14, threshold=30))
exit_dates = op.signal_dates(stocks, op.rsi_above(period=14, threshold=70))
results = op.long_calls(options, entry_dates=entry_dates, exit_dates=exit_dates)

SMA - trend filter

Only enter when price is above its 50-day moving average:

entry_dates = op.signal_dates(stocks, op.sma_above(period=50))
results = op.short_puts(options, entry_dates=entry_dates)

MACD - enter on bullish crossover

entry_dates = op.signal_dates(stocks, op.macd_cross_above(fast=12, slow=26, signal_period=9))
results = op.long_call_spread(options, entry_dates=entry_dates)

Stochastic - oversold entry

entry_dates = op.signal_dates(stocks, op.stoch_below(k_period=14, d_period=3, threshold=20))
results = op.long_calls(options, entry_dates=entry_dates)

Bollinger Bands - mean reversion

Enter when price dips below the lower band:

entry_dates = op.signal_dates(stocks, op.bb_below_lower(length=20, std=2.0))
results = op.long_puts(options, entry_dates=entry_dates)

EMA Crossover - golden cross

Fast EMA crosses above slow EMA:

entry_dates = op.signal_dates(stocks, op.ema_cross_above(fast=10, slow=50))
results = op.long_calls(options, entry_dates=entry_dates)

ADX - trend strength filter

Only enter trend-following strategies when ADX confirms a strong trend:

entry = op.signal(op.adx_above(period=14, threshold=25)) & op.signal(op.ema_cross_above(10, 50))
entry_dates = op.signal_dates(stocks, entry)
results = op.long_calls(options, entry_dates=entry_dates)

Supertrend - trend direction

Enter when Supertrend flips bullish:

entry_dates = op.signal_dates(stocks, op.supertrend_buy(period=7, multiplier=3.0))
results = op.long_call_spread(options, entry_dates=entry_dates)

ATR - low-volatility regime filter

Only sell premium in low-volatility regimes:

entry_dates = op.signal_dates(stocks, op.atr_below(period=14, multiplier=0.75))
results = op.iron_condor(options, entry_dates=entry_dates)

Keltner Channel - breakout entry

Enter when price breaks above the upper Keltner Channel:

entry_dates = op.signal_dates(stocks, op.kc_above_upper(length=20, scalar=1.5))
results = op.long_calls(options, entry_dates=entry_dates)

Squeeze - volatility compression

Enter when the squeeze releases (Bollinger Bands expand outside Keltner Channels):

entry_dates = op.signal_dates(stocks, op.squeeze_off())
results = op.long_straddles(options, entry_dates=entry_dates)

Volume - MFI oversold

Enter when Money Flow Index indicates oversold conditions:

entry_dates = op.signal_dates(stocks, op.mfi_below(period=14, threshold=20))
results = op.long_calls(options, entry_dates=entry_dates)

Calendar - restrict entries to specific days

# Enter only on Mondays and Fridays
entry_dates = op.signal_dates(stocks, op.day_of_week(0, 4))
results = op.short_straddles(options, entry_dates=entry_dates)

Combining Multiple Signals

Use the Signal class with & (AND) and | (OR) operators, or the functional and_signals / or_signals helpers:

import optopsy as op

stocks = op.load_cached_stocks("SPY")
options = op.load_cached_options("SPY")

# Fluent API: oversold + uptrend + low volatility
entry = op.signal(op.rsi_below(14, 30)) & op.signal(op.sma_above(50)) & op.signal(op.atr_below(14, 0.75))
entry_dates = op.signal_dates(stocks, entry)
results = op.long_calls(options, entry_dates=entry_dates)

# Functional API: same logic
entry = op.and_signals(op.rsi_below(14, 30), op.sma_above(50), op.atr_below(14, 0.75))
entry_dates = op.signal_dates(stocks, entry)
results = op.long_calls(options, entry_dates=entry_dates)

# OR: enter when EITHER condition fires
entry = op.or_signals(op.macd_cross_above(), op.bb_below_lower())
entry_dates = op.signal_dates(stocks, entry)
results = op.long_call_spread(options, entry_dates=entry_dates)

Sustained Signals

Require a condition to persist for multiple consecutive days before triggering:

stocks = op.load_cached_stocks("SPY")
options = op.load_cached_options("SPY")

# RSI must stay below 30 for 5 straight days
entry_dates = op.signal_dates(stocks, op.sustained(op.rsi_below(14, 30), days=5))
results = op.long_calls(options, entry_dates=entry_dates)

# Bollinger Band breach sustained for 3 days
entry_dates = op.signal_dates(stocks, op.sustained(op.bb_below_lower(20, 2.0), days=3))
results = op.long_puts(options, entry_dates=entry_dates)

Using Your Own Stock Data

You can use any DataFrame with underlying_symbol, quote_date, and close columns. Signals that need high/low/volume will use those columns if present, falling back to close if not.

import pandas as pd
import optopsy as op

# Load your own OHLCV data
stock_df = pd.read_csv("SPY_daily.csv", parse_dates=["quote_date"])

# Use it directly with signal_dates
entry = op.signal(op.adx_above(period=14, threshold=25)) & op.signal(op.supertrend_buy())
entry_dates = op.signal_dates(stock_df, entry)

options = op.load_cached_options("SPY")
results = op.long_straddles(options, entry_dates=entry_dates)

Tip

Signals that use high/low data (Stochastic, Williams %R, CCI, ATR, Keltner, Donchian, ADX, Aroon, Supertrend, PSAR, Choppiness, MFI, OBV, CMF, A/D) will fall back to using close as a proxy if high and low columns are not present. For best accuracy, provide real OHLCV data.

IV Rank - volatility regime filter

IV Rank measures where current implied volatility sits relative to its trailing range. Requires an implied_volatility column in your options data. This is the one signal type that runs on options data, not stock data.

Sell premium when IV is elevated

options = op.load_cached_options("SPY")

# Enter short strategies when IV rank is above 50th percentile (1-year lookback)
entry_dates = op.signal_dates(options, op.iv_rank_above(threshold=0.5, window=252))
results = op.iron_condor(options, entry_dates=entry_dates)

Buy options when IV is cheap

# Enter long strategies when IV rank is in the bottom 30%
entry_dates = op.signal_dates(options, op.iv_rank_below(threshold=0.3, window=252))
results = op.long_straddles(options, entry_dates=entry_dates)

Note

IV rank signals work directly on options chain data (not stock OHLCV). The signal computes ATM implied volatility per quote date and ranks it over the trailing window.

Custom Signal from DataFrame

Use custom_signal() to create a signal from any DataFrame with a boolean flag column. This lets you define arbitrary entry/exit conditions using external data sources, model outputs, or manual annotations.

import pandas as pd
import optopsy as op

# Any DataFrame with dates and a boolean flag works
my_signals = pd.DataFrame({
    "underlying_symbol": ["SPY", "SPY", "SPY"],
    "quote_date": ["2018-01-02", "2018-01-03", "2018-01-04"],
    "buy": [True, False, True],
})

# Create a signal from the DataFrame
sig = op.custom_signal(my_signals, flag_col="buy")
entry_dates = op.signal_dates(my_signals, sig)

# Pass to any strategy
options = op.load_cached_options("SPY")
results = op.long_calls(options, entry_dates=entry_dates, raw=True)

The DataFrame must contain underlying_symbol, quote_date, and the flag column. Integer (0/1), nullable boolean, and NaN values are all handled — NaN is treated as False.

Composing custom signals with built-in signals

import optopsy as op

sig = op.custom_signal(my_signals, flag_col="buy")

# Combine with built-in signals using & and |
combined = op.signal(sig) & op.signal(op.day_of_week(1))
entry_dates = op.signal_dates(my_signals, combined)

options = op.load_cached_options("SPY")
results = op.long_calls(options, entry_dates=entry_dates)

Custom Signal Functions

Any function matching the signature (pd.DataFrame) -> pd.Series[bool] can be used as a signal:

import optopsy as op

stocks = op.load_cached_stocks("SPY")
options = op.load_cached_options("SPY")

# Custom: only enter when close price is above 400
def price_above_400(data):
    return data["close"] > 400

entry_dates = op.signal_dates(stocks, price_above_400)
results = op.iron_condor(options, entry_dates=entry_dates)

# Combine custom signals with built-in ones
entry = op.signal(price_above_400) & op.signal(op.rsi_below(14, 30))
entry_dates = op.signal_dates(stocks, entry)
results = op.long_calls(options, entry_dates=entry_dates)