Timeframe
5m
Direction
Long & Short
Stoploss
-4.0%
Trailing Stop
Yes
ROI
N/A
Interface Version
3
Startup Candles
200
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OSIRIS FUNNEL STRATEGY v2.0 — Grid-Optimized for 1 Year BTC
=============================================================
Entry logic from exhaustive funnel analysis:
Base: EMA9 cross close on 5m
Filter 1: MACD histogram aligned
Filter 2: 1H RSI aligned >50/<50
Filter 3: ADX(14) > 25 (trending market)
Exit: Fixed SL=2.5%, TP=3.0% (RR=1.2:1) via stoploss + ROI.
- No custom_stoploss (avoids ATR re-evaluation drift)
- No trailing stop
- No exit signals
1Y Backtest (2025-03-20 to 2026-03-20, BTC/USDT futures):
174 trades, 50.6% WR, +3.14%, PF=1.15, DD=2.36%
7/12 months profitable, survived -20% market decline
"""
import logging
import numpy as np
from pandas import DataFrame
from typing import Optional
from freqtrade.strategy import IStrategy, merge_informative_pair
from freqtrade.persistence import Trade
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisFunnelStrategy(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "5m"
# TP via ROI: 3.0% — 1Y grid-search winner
# SL=2.5%, TP=3.0% → 50.6% WR, +$31.4/Y, DD=2.4%, PF=1.15 (174 trades)
minimal_roi = {}
# Fixed stoploss: -2.5%
stoploss = -0.0400
# Trailing: COMPLETELY DISABLED
trailing_stop = True
trailing_stop_positive = 0.0100
trailing_stop_positive_offset = 0.0200
trailing_only_offset_is_reached = True
# NO custom stoploss — use fixed stoploss for deterministic behavior
use_custom_stoploss = False
# No exit signal — only ROI (TP) and stoploss (SL)
use_exit_signal = False
startup_candle_count = 200
process_only_new_candles = True
# ADX threshold
adx_threshold = 25
def informative_pairs(self):
pairs = self.dp.current_whitelist()
informative = []
for pair in pairs:
informative.append((pair, "1h"))
return informative
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# === 5m indicators ===
dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
dataframe["adx14"] = ta.ADX(dataframe, timeperiod=14)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd_hist"] = macd["macdhist"]
# EMA9 cross detection
dataframe["prev_close"] = dataframe["close"].shift(1)
dataframe["prev_ema9"] = dataframe["ema9"].shift(1)
# === 1H indicators (RSI for filter) ===
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 = inf_1h[["date", "rsi_1h"]].copy()
dataframe = merge_informative_pair(
dataframe, inf_1h, self.timeframe, "1h", ffill=True
)
# Ensure column exists
if "rsi_1h_1h" not in dataframe.columns:
dataframe["rsi_1h_1h"] = 50.0
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
FUNNEL ENTRY:
Base: Price crosses EMA9 (bullish cross = long, bearish = short)
Filter 1: MACD histogram aligned (>0 long, <0 short)
Filter 2: 1H RSI aligned (>50 long, <50 short)
Filter 3: ADX > threshold (trending market)
"""
# 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
macd_bull = dataframe["macd_hist"] > 0
macd_bear = dataframe["macd_hist"] < 0
# Filter 2: 1H RSI
rsi1h_bull = dataframe["rsi_1h_1h"] > 50
rsi1h_bear = dataframe["rsi_1h_1h"] < 50
# Filter 3: ADX trending
adx_ok = dataframe["adx14"] > self.adx_threshold
# Volume sanity
vol_ok = dataframe["volume"] > 0
# === LONG ===
dataframe.loc[
cross_above & macd_bull & rsi1h_bull & adx_ok & vol_ok,
"enter_long",
] = 1
# === SHORT ===
dataframe.loc[
cross_below & macd_bear & rsi1h_bear & adx_ok & vol_ok,
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# No exit signals — only ROI (TP) and stoploss (SL)
return dataframe