Timeframe
1h
Direction
Long & Short
Stoploss
-1.5%
Trailing Stop
No
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 SWING v1.0 — 1H Pullback-to-Trend Strategy
====================================================
Designed for high WR + high RR by catching pullbacks in established trends.
Logic:
1H timeframe entry — bigger candles = bigger moves = easier 3:1 RR
Trend: EMA(9) > EMA(21) > EMA(50) on 1H (bull)
Entry: RSI(14) pulls back below 45 (in uptrend) then recovers above 50
Confirmation: MACD histogram positive and growing
ADX > 25 (trend present)
Exit: Trailing after 1R profit, no fixed TP.
Initial SL = -1.5%
After +1.5%: trail at 1.0%
After +3.0%: trail at 1.5%
Time exit: 48h max
"""
import logging
import numpy as np
from pandas import DataFrame
from datetime import datetime
from freqtrade.strategy import IStrategy
from freqtrade.persistence import Trade
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisSwingV1(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "1h"
stoploss = -0.015 # 1.5% initial SL
minimal_roi = {} # No fixed TP
trailing_stop = False
trailing_stop_positive = 0.0
trailing_stop_positive_offset = 0.0
trailing_only_offset_is_reached = False
use_custom_stoploss = True
use_exit_signal = True
startup_candle_count = 200
process_only_new_candles = True
def custom_stoploss(self, pair: str, trade: Trade,
current_time: datetime, current_rate: float,
current_profit: float, after_fill: bool,
**kwargs) -> float:
# Phase 3: deep into profit, trail tightly
if current_profit >= 0.03:
return -0.015 # Trail at 1.5%
# Phase 2: trail once at 1.5% profit
if current_profit >= 0.015:
return -0.01 # Trail at 1.0%, locking ~0.5% min
# Phase 1: after reaching 0.8%, move near breakeven
if current_profit >= 0.008:
return -0.005 # ~0.3% profit locked
# Phase 0: initial
return -0.015
def custom_exit(self, pair: str, trade: Trade,
current_time: datetime, current_rate: float,
current_profit: float, **kwargs):
elapsed = (current_time - trade.open_date_utc).total_seconds()
# Not working after 12 hours → cut
if elapsed > 12 * 3600 and current_profit < 0.003:
return "time_exit_12h"
# Max hold 48h
if elapsed > 48 * 3600:
return "time_exit_48h"
return None
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["rsi14"] = ta.RSI(dataframe, timeperiod=14)
dataframe["adx14"] = ta.ADX(dataframe, timeperiod=14)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd_hist"] = macd["macdhist"]
# Pullback detection: RSI was below 45 recently (within 3 bars)
dataframe["rsi_below_45_recently"] = (
(dataframe["rsi14"].shift(1) < 45) |
(dataframe["rsi14"].shift(2) < 45) |
(dataframe["rsi14"].shift(3) < 45)
)
dataframe["rsi_above_55_recently"] = (
(dataframe["rsi14"].shift(1) > 55) |
(dataframe["rsi14"].shift(2) > 55) |
(dataframe["rsi14"].shift(3) > 55)
)
# EMA alignment
dataframe["trend_bull"] = (
(dataframe["ema9"] > dataframe["ema21"]) &
(dataframe["ema21"] > dataframe["ema50"])
)
dataframe["trend_bear"] = (
(dataframe["ema9"] < dataframe["ema21"]) &
(dataframe["ema21"] < dataframe["ema50"])
)
# Volume
dataframe["vol_sma20"] = ta.SMA(dataframe["volume"], timeperiod=20)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Pullback-to-trend entry:
- Strong trend (EMA aligned + ADX>25)
- RSI pulled back below 45 recently, now recovering above 50
- MACD histogram aligned
"""
# Trend filters
adx_ok = dataframe["adx14"] > 25
# LONG: Bull trend + RSI pullback recovery
long_cond = (
dataframe["trend_bull"] &
adx_ok &
dataframe["rsi_below_45_recently"] &
(dataframe["rsi14"] > 50) &
(dataframe["macd_hist"] > 0) &
(dataframe["close"] > dataframe["ema21"]) &
(dataframe["volume"] > 0)
)
# SHORT: Bear trend + RSI rally recovery
short_cond = (
dataframe["trend_bear"] &
adx_ok &
dataframe["rsi_above_55_recently"] &
(dataframe["rsi14"] < 50) &
(dataframe["macd_hist"] < 0) &
(dataframe["close"] < dataframe["ema21"]) &
(dataframe["volume"] > 0)
)
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe