Timeframe
4h
Direction
Long & Short
Stoploss
-15.0%
Trailing Stop
No
ROI
0m: 10000.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 V2 — Hyperoptable Swing Strategy
================================================
Based on OsirisSwing4H V1 (+22.65% on 2022-2025).
SL/TP both intra-candle via custom_stoploss + stoploss_from_open.
Hyperopt optimizes entry EMA params, SL%, TP%, and max hold.
"""
import logging
from pandas import DataFrame
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter, informative
import talib.abstract as ta
logger = logging.getLogger(__name__)
def stoploss_from_open(open_relative_stop, current_profit, is_short=False):
if current_profit == 0:
return 1
if is_short:
return -1 + ((1 - open_relative_stop) / (1 - current_profit))
return 1 - ((1 + open_relative_stop) / (1 + current_profit))
class OsirisSwing4HV2(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "4h"
# Safety net stoploss (never hit — custom_stoploss handles real SL)
stoploss = -0.15
use_custom_stoploss = True
trailing_stop = False
minimal_roi = {"0": 100} # Disabled — TP via custom_stoploss
startup_candle_count = 250
process_only_new_candles = True
# ── Hyperopt: Entry ─────────────────────────────────────────────
ema_fast = IntParameter(5, 15, default=9, space="buy", optimize=True)
ema_slow = IntParameter(15, 30, default=21, space="buy", optimize=True)
# ── Hyperopt: Exit ──────────────────────────────────────────────
sl_pct = DecimalParameter(2.0, 6.0, default=3.0, decimals=1, space="sell", optimize=True)
tp_pct = DecimalParameter(3.0, 12.0, default=6.0, decimals=1, space="sell", optimize=True)
max_hold_bars = IntParameter(6, 30, default=18, space="sell", optimize=True)
@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:
for p in range(5, 31):
dataframe[f"ema{p}"] = ta.EMA(dataframe, timeperiod=p)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
fast = int(self.ema_fast.value)
slow = int(self.ema_slow.value)
if fast >= slow:
return dataframe
ema_f = dataframe[f"ema{fast}"]
ema_s = dataframe[f"ema{slow}"]
has_data = (
ema_f.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 = (ema_f > ema_s) & (ema_f.shift(1) <= ema_s.shift(1))
ema_cross_dn = (ema_f < ema_s) & (ema_f.shift(1) >= ema_s.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_stoploss(self, pair, trade, current_time,
current_rate, current_profit, **kwargs) -> float:
"""Intra-candle SL and TP via stoploss_from_open."""
sl = float(self.sl_pct.value) / 100
tp = float(self.tp_pct.value) / 100
is_short = trade.is_short
# TP reached → lock profit with tight trail from open
if current_profit >= tp:
return stoploss_from_open(tp - 0.001, current_profit, is_short=is_short)
# Normal SL from entry
return stoploss_from_open(-sl, current_profit, is_short=is_short)
def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
max_h = int(self.max_hold_bars.value)
duration_hours = (current_time - trade.open_date_utc).total_seconds() / 3600
if duration_hours >= max_h * 4:
return "timeout"
return None
def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs):
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
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0