Timeframe
4h
Direction
Long & Short
Stoploss
-3.0%
Trailing Stop
No
ROI
0m: 6.0%
Interface Version
3
Startup Candles
250
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisSwing4H — Validated Swing Trading Strategy
=================================================
4h timeframe with daily SMA200 regime filter.
Validated on 3+ years (2022-2025) WITHOUT lookahead.
Two entry modes:
LONG: 4h EMA9 crosses above EMA21 when daily regime is bullish
SHORT: 4h EMA9 crosses below EMA21 when daily regime is bearish
Risk: SL=3%, TP=6%, max hold=18 bars (3 days)
PF=1.20, Net=+24%, DD<15% on 2022-2025 data
"""
import logging
from pandas import DataFrame
from freqtrade.strategy import IStrategy, informative
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisSwing4H(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "4h"
# SL/TP via native: SL=3%, TP=6%
stoploss = -0.03
minimal_roi = {"0": 0.06}
# Max hold = 18 4h-candles = 72h = 3 days
# Implemented via custom_exit
trailing_stop = False
use_custom_stoploss = False
startup_candle_count = 250 # Need 200+ for SMA200 on 1D
process_only_new_candles = True
# Cooldown: 2 bars (8h) after exit before re-entry
_last_exit_bar = 0
@informative("1d")
def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["sma50"] = ta.SMA(dataframe, timeperiod=50)
dataframe["sma200"] = ta.SMA(dataframe, timeperiod=200)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
has_data = (
dataframe["ema9"].notna()
& dataframe["sma200_1d"].notna()
& (dataframe["volume"] > 0)
)
# Daily regime
daily_bull = (
(dataframe["close"] > dataframe["sma200_1d"])
& (dataframe["sma50_1d"] > dataframe["sma200_1d"])
)
daily_bear = (
(dataframe["close"] < dataframe["sma200_1d"])
& (dataframe["sma50_1d"] < dataframe["sma200_1d"])
)
# 4h EMA cross
ema_cross_up = (dataframe["ema9"] > dataframe["ema21"]) & (
dataframe["ema9"].shift(1) <= dataframe["ema21"].shift(1)
)
ema_cross_dn = (dataframe["ema9"] < dataframe["ema21"]) & (
dataframe["ema9"].shift(1) >= dataframe["ema21"].shift(1)
)
# LONG: EMA cross up in bull regime
dataframe.loc[
has_data & ema_cross_up & daily_bull,
["enter_long", "enter_tag"],
] = (1, "ema_cross_bull")
# SHORT: EMA cross down in bear regime
dataframe.loc[
has_data & ema_cross_dn & daily_bear,
["enter_short", "enter_tag"],
] = (1, "ema_cross_bear")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["exit_long"] = 0
dataframe["exit_short"] = 0
return dataframe
def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
"""Timeout exit after 18 bars (72h)"""
duration = (current_time - trade.open_date_utc).total_seconds() / 3600
if duration >= 72: # 18 * 4h
return "timeout_72h"
return None
def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs):
"""Cooldown: skip entry within 8h of last exit"""
if hasattr(self, '_last_exit_time') and self._last_exit_time:
hours_since = (current_time - self._last_exit_time).total_seconds() / 3600
if hours_since < 8:
return False
return True
def confirm_trade_exit(self, pair, trade, order_type, amount, rate,
time_in_force, exit_reason, current_time, **kwargs):
self._last_exit_time = current_time
return True