Entry Signals¶
Filter strategy entries using 80+ technical analysis signals powered by pandas-ta-classic. Use apply_signal to compute valid dates, then pass them as entry_dates or exit_dates to any strategy.
from optopsy import long_calls, apply_signal, rsi_below, sustained, signal, day_of_week
# Enter only when RSI(14) is below 30
entry_dates = apply_signal(data, rsi_below(14, 30))
results = long_calls(data, entry_dates=entry_dates)
# Require RSI below 30 for 5 consecutive days
entry_dates = apply_signal(data, sustained(rsi_below(14, 30), days=5))
results = long_calls(data, entry_dates=entry_dates)
# Compose signals with & and |
sig = signal(rsi_below(14, 30)) & signal(day_of_week(3)) # Oversold + Thursday
entry_dates = apply_signal(data, sig)
results = long_calls(data, entry_dates=entry_dates)
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.
| 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
from optopsy import apply_signal, rsi_below, rsi_above
entry_dates = apply_signal(data, rsi_below(period=14, threshold=30))
exit_dates = apply_signal(data, rsi_above(period=14, threshold=70))
results = op.long_calls(data, entry_dates=entry_dates, exit_dates=exit_dates)
SMA - trend filter¶
Only enter when price is above its 50-day moving average:
from optopsy import apply_signal, sma_above
entry_dates = apply_signal(data, sma_above(period=50))
results = op.short_puts(data, entry_dates=entry_dates)
MACD - enter on bullish crossover¶
from optopsy import apply_signal, macd_cross_above
entry_dates = apply_signal(data, macd_cross_above(fast=12, slow=26, signal_period=9))
results = op.long_call_spread(data, entry_dates=entry_dates)
Stochastic - oversold entry¶
from optopsy import apply_signal, stoch_below
entry_dates = apply_signal(data, stoch_below(k_period=14, d_period=3, threshold=20))
results = op.long_calls(data, entry_dates=entry_dates)
Bollinger Bands - mean reversion¶
Enter when price dips below the lower band:
from optopsy import apply_signal, bb_below_lower
entry_dates = apply_signal(data, bb_below_lower(length=20, std=2.0))
results = op.long_puts(data, entry_dates=entry_dates)
EMA Crossover - golden cross¶
Fast EMA crosses above slow EMA:
from optopsy import apply_signal, ema_cross_above
entry_dates = apply_signal(data, ema_cross_above(fast=10, slow=50))
results = op.long_calls(data, entry_dates=entry_dates)
ADX - trend strength filter¶
Only enter trend-following strategies when ADX confirms a strong trend:
from optopsy import apply_signal, adx_above, signal, ema_cross_above
entry = signal(adx_above(period=14, threshold=25)) & signal(ema_cross_above(10, 50))
entry_dates = apply_signal(data, entry)
results = op.long_calls(data, entry_dates=entry_dates)
Supertrend - trend direction¶
Enter when Supertrend flips bullish:
from optopsy import apply_signal, supertrend_buy
entry_dates = apply_signal(data, supertrend_buy(period=7, multiplier=3.0))
results = op.long_call_spread(data, entry_dates=entry_dates)
ATR - low-volatility regime filter¶
Only sell premium in low-volatility regimes:
from optopsy import apply_signal, atr_below
entry_dates = apply_signal(data, atr_below(period=14, multiplier=0.75))
results = op.iron_condor(data, entry_dates=entry_dates)
Keltner Channel - breakout entry¶
Enter when price breaks above the upper Keltner Channel:
from optopsy import apply_signal, kc_above_upper
entry_dates = apply_signal(data, kc_above_upper(length=20, scalar=1.5))
results = op.long_calls(data, entry_dates=entry_dates)
Squeeze - volatility compression¶
Enter when the squeeze releases (Bollinger Bands expand outside Keltner Channels):
from optopsy import apply_signal, squeeze_off
entry_dates = apply_signal(data, squeeze_off())
results = op.long_straddles(data, entry_dates=entry_dates)
Volume - MFI oversold¶
Enter when Money Flow Index indicates oversold conditions:
from optopsy import apply_signal, mfi_below
entry_dates = apply_signal(stock_df, mfi_below(period=14, threshold=20))
results = op.long_calls(data, entry_dates=entry_dates)
Calendar - restrict entries to specific days¶
from optopsy import apply_signal, day_of_week
# Enter only on Mondays and Fridays
entry_dates = apply_signal(data, day_of_week(0, 4))
results = op.short_straddles(data, entry_dates=entry_dates)
Combining Multiple Signals¶
Use the Signal class with & (AND) and | (OR) operators, or the functional and_signals / or_signals helpers:
from optopsy import apply_signal, signal, rsi_below, sma_above, atr_below, day_of_week
from optopsy import and_signals, or_signals
# Fluent API: oversold + uptrend + low volatility
entry = signal(rsi_below(14, 30)) & signal(sma_above(50)) & signal(atr_below(14, 0.75))
entry_dates = apply_signal(data, entry)
results = op.long_calls(data, entry_dates=entry_dates)
# Functional API: same logic
entry = and_signals(rsi_below(14, 30), sma_above(50), atr_below(14, 0.75))
entry_dates = apply_signal(data, entry)
results = op.long_calls(data, entry_dates=entry_dates)
# OR: enter when EITHER condition fires
from optopsy import macd_cross_above, bb_below_lower
entry = or_signals(macd_cross_above(), bb_below_lower())
entry_dates = apply_signal(data, entry)
results = op.long_call_spread(data, entry_dates=entry_dates)
Sustained Signals¶
Require a condition to persist for multiple consecutive days before triggering:
from optopsy import apply_signal, sustained, rsi_below, bb_below_lower
# RSI must stay below 30 for 5 straight days
entry_dates = apply_signal(data, sustained(rsi_below(14, 30), days=5))
results = op.long_calls(data, entry_dates=entry_dates)
# Bollinger Band breach sustained for 3 days
entry_dates = apply_signal(data, sustained(bb_below_lower(20, 2.0), days=3))
results = op.long_puts(data, entry_dates=entry_dates)
Using Stock OHLCV Data¶
By default, signals compute indicators from the option chain's underlying_price column. For more accurate TA signals (especially those needing high/low/volume data), use apply_signal on a separate stock OHLCV DataFrame and pass the result as entry_dates:
import pandas as pd
import optopsy as op
from optopsy import apply_signal, adx_above, supertrend_buy, signal
# Load OHLCV stock data (must have: underlying_symbol, quote_date, close;
# optional: open, high, low, volume)
stock_df = pd.read_csv("SPX_daily_ohlcv.csv", parse_dates=["quote_date"])
# Compute entry dates from stock data using real high/low for trend signals
entry = signal(adx_above(period=14, threshold=25)) & signal(supertrend_buy())
entry_dates = apply_signal(stock_df, entry)
# Pass pre-computed dates to the strategy
results = op.long_straddles(data, 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 underlying_price 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.
Sell premium when IV is elevated¶
from optopsy import apply_signal, iv_rank_above
# Enter short strategies when IV rank is above 50th percentile (1-year lookback)
entry_dates = apply_signal(data, iv_rank_above(threshold=0.5, window=252))
results = op.iron_condor(data, entry_dates=entry_dates)
Buy options when IV is cheap¶
from optopsy import apply_signal, iv_rank_below
# Enter long strategies when IV rank is in the bottom 30%
entry_dates = apply_signal(data, iv_rank_below(threshold=0.3, window=252))
results = op.long_straddles(data, 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.apply_signal(my_signals, sig)
# Pass to any strategy
results = op.long_calls(data, 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.apply_signal(my_signals, combined)
results = op.long_calls(data, 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
from optopsy import apply_signal, signal, rsi_below
# Custom: only enter when underlying price is above 4000
def price_above_4000(data):
return data["underlying_price"] > 4000
entry_dates = apply_signal(data, price_above_4000)
results = op.iron_condor(data, entry_dates=entry_dates)
# Combine custom signals with built-in ones
entry = signal(price_above_4000) & signal(rsi_below(14, 30))
entry_dates = apply_signal(data, entry)
results = op.long_calls(data, entry_dates=entry_dates)