Timeframe
5m
Direction
Long & Short
Stoploss
-1.0%
Trailing Stop
No
ROI
0m: 3.0%
Interface Version
3
Startup Candles
200
Indicators
6
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OSIRIS FUNNEL STRATEGY v3.0 — Ultra-Selective 80% WR Target
=============================================================
Entry: Pullback-to-trend with multi-timeframe alignment.
Base: EMA9 cross close on 5m (entry trigger)
Filter 1: MACD histogram aligned + growing momentum
Filter 2: 1H RSI aligned (>55 long, <45 short) — stronger bias
Filter 3: ADX(14) > 30 (strong trend only, not weak 25-30)
Filter 4: EMA alignment on 5m (9>21>50 bull, 9<21<50 bear)
Filter 5: 1H EMA alignment (trend confirmation)
Filter 6: Volume > 1.0× SMA(20) — above average activity
Filter 7: RSI directional (long: RSI not overbought, short: RSI not oversold)
Filter 8: BB position (long: not top, short: not bottom)
Exit: RR 3:1 — TP = 3 × SL via stoploss + ROI.
"""
import logging
import numpy as np
from pandas import DataFrame
from freqtrade.strategy import IStrategy, merge_informative_pair
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisFunnelV3(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "5m"
# RR 3:1: SL=1.0%, TP=3.0% — grid-searchable
minimal_roi = {"0": 0.03}
stoploss = -0.01
# Trailing: OFF
trailing_stop = False
trailing_stop_positive = 0.0
trailing_stop_positive_offset = 0.0
trailing_only_offset_is_reached = False
use_custom_stoploss = False
use_exit_signal = False
startup_candle_count = 200
process_only_new_candles = True
def informative_pairs(self):
pairs = self.dp.current_whitelist()
return [(pair, "1h") for pair in pairs]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# === 5m indicators ===
dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["adx14"] = ta.ADX(dataframe, timeperiod=14)
dataframe["rsi14"] = ta.RSI(dataframe, timeperiod=14)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd_hist"] = macd["macdhist"]
dataframe["macd_hist_prev"] = dataframe["macd_hist"].shift(1)
# Bollinger Bands
bb = ta.BBANDS(dataframe, timeperiod=20)
dataframe["bb_upper"] = bb["upperband"]
dataframe["bb_lower"] = bb["lowerband"]
dataframe["bb_mid"] = bb["middleband"]
# Volume SMA
dataframe["vol_sma20"] = ta.SMA(dataframe["volume"], timeperiod=20)
# EMA9 cross detection
dataframe["prev_close"] = dataframe["close"].shift(1)
dataframe["prev_ema9"] = dataframe["ema9"].shift(1)
# EMA alignment flags
dataframe["ema_bull"] = (
(dataframe["ema9"] > dataframe["ema21"]) &
(dataframe["ema21"] > dataframe["ema50"])
).astype(int)
dataframe["ema_bear"] = (
(dataframe["ema9"] < dataframe["ema21"]) &
(dataframe["ema21"] < dataframe["ema50"])
).astype(int)
# === 1H indicators ===
if self.dp:
inf_1h = self.dp.get_pair_dataframe(
pair=metadata["pair"], timeframe="1h"
)
if not inf_1h.empty:
inf_1h["rsi_1h"] = ta.RSI(inf_1h, timeperiod=14)
inf_1h["adx_1h"] = ta.ADX(inf_1h, timeperiod=14)
inf_1h["ema9_1h"] = ta.EMA(inf_1h, timeperiod=9)
inf_1h["ema21_1h"] = ta.EMA(inf_1h, timeperiod=21)
inf_1h["ema50_1h"] = ta.EMA(inf_1h, timeperiod=50)
inf_1h["trend_1h"] = (
((inf_1h["ema9_1h"] > inf_1h["ema21_1h"]) &
(inf_1h["ema21_1h"] > inf_1h["ema50_1h"])).astype(int) -
((inf_1h["ema9_1h"] < inf_1h["ema21_1h"]) &
(inf_1h["ema21_1h"] < inf_1h["ema50_1h"])).astype(int)
)
inf_1h = inf_1h[["date", "rsi_1h", "adx_1h", "trend_1h"]].copy()
dataframe = merge_informative_pair(
dataframe, inf_1h, self.timeframe, "1h", ffill=True
)
for col in ["rsi_1h_1h", "adx_1h_1h", "trend_1h_1h"]:
if col not in dataframe.columns:
dataframe[col] = 50.0 if "rsi" in col else (20.0 if "adx" in col else 0.0)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
ULTRA-SELECTIVE ENTRY — 8 filters for maximum quality.
"""
# === BASE SIGNAL: EMA9 cross ===
cross_above = (
(dataframe["prev_close"] < dataframe["prev_ema9"]) &
(dataframe["close"] > dataframe["ema9"])
)
cross_below = (
(dataframe["prev_close"] > dataframe["prev_ema9"]) &
(dataframe["close"] < dataframe["ema9"])
)
# === FILTER 1: MACD histogram aligned + growing ===
macd_bull = (dataframe["macd_hist"] > 0)
macd_bear = (dataframe["macd_hist"] < 0)
# === FILTER 2: 1H RSI strong bias ===
rsi1h_bull = dataframe["rsi_1h_1h"] > 55
rsi1h_bear = dataframe["rsi_1h_1h"] < 45
# === FILTER 3: ADX > 30 (strong trend only) ===
adx_strong = dataframe["adx14"] > 30
# === FILTER 4: 5m EMA alignment ===
ema_aligned_bull = dataframe["ema_bull"] == 1
ema_aligned_bear = dataframe["ema_bear"] == 1
# === FILTER 5: 1H trend alignment ===
trend_1h_bull = dataframe["trend_1h_1h"] == 1
trend_1h_bear = dataframe["trend_1h_1h"] == -1
# === FILTER 6: Volume above average ===
vol_above = dataframe["volume"] > dataframe["vol_sma20"]
# === FILTER 7: RSI directional (not overextended) ===
rsi_long_ok = (dataframe["rsi14"] > 40) & (dataframe["rsi14"] < 65)
rsi_short_ok = (dataframe["rsi14"] > 35) & (dataframe["rsi14"] < 60)
# === FILTER 8: BB position (room to run) ===
bb_range = dataframe["bb_upper"] - dataframe["bb_lower"]
bb_range = bb_range.replace(0, 1)
bb_pos = (dataframe["close"] - dataframe["bb_lower"]) / bb_range
bb_long_ok = bb_pos < 0.7 # Not at top
bb_short_ok = bb_pos > 0.3 # Not at bottom
# === LONG ===
dataframe.loc[
cross_above & macd_bull & rsi1h_bull & adx_strong &
ema_aligned_bull & trend_1h_bull & vol_above &
rsi_long_ok & bb_long_ok,
"enter_long",
] = 1
# === SHORT ===
dataframe.loc[
cross_below & macd_bear & rsi1h_bear & adx_strong &
ema_aligned_bear & trend_1h_bear & vol_above &
rsi_short_ok & bb_short_ok,
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe