OSIRIS v8 — Cut losers early, let winners run forever. Progressive trailing + conditional loser exit + no time cap for winners.
Timeframe
15m
Direction
Long & Short
Stoploss
-6.0%
Trailing Stop
No
ROI
0m: 50.0%
Interface Version
3
Startup Candles
200
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OSIRIS DAY TRADE v8 — Smart Losers, Long Winners
================================================================
v7 → v8: PF 0.76 → target 1.0+
MUDANÇAS CHAVE DO v8:
1. CORTA LOSERS CEDO: se em 4 candles (1h) a trade está perdendo
E condições deterioram (RSI + EMA contra) → SAIR COM LOSS MENOR
2. WINNERS SEM LIMITE DE TEMPO: time_force SÓ para trades perdendo
3. BREAKEVEN mais agressivo: mover stop para BE em 0.5R
4. Manter: deep pullback, patterns, ADX filter, progressive trail
"""
import logging
import numpy as np
import pandas as pd
from pandas import DataFrame
from typing import Optional
from datetime import datetime, timedelta
from freqtrade.strategy import IStrategy, merge_informative_pair
from freqtrade.strategy import DecimalParameter, IntParameter
from freqtrade.persistence import Trade
import talib.abstract as ta
try:
from freqtrade.strategy import stoploss_from_open
except ImportError:
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))
logger = logging.getLogger(__name__)
class OsirisDayTradeV8(IStrategy):
"""
OSIRIS v8 — Cut losers early, let winners run forever.
Progressive trailing + conditional loser exit + no time cap for winners.
"""
INTERFACE_VERSION = 3
can_short = True
timeframe = "15m"
minimal_roi = {"0": 0.50}
stoploss = -0.06
trailing_stop = False
use_custom_stoploss = True
startup_candle_count = 200
process_only_new_candles = True
_daily_trades = {}
_consecutive_losses = 0
_last_loss_time = None
# ═══════════════════════════════════════════════════════════════════
# PARAMETERS
# ═══════════════════════════════════════════════════════════════════
# Trailing
buy_sl_initial = DecimalParameter(3.0, 5.0, default=3.5, decimals=1, space="buy", optimize=True)
buy_sl_trail_1 = DecimalParameter(2.5, 4.0, default=3.0, decimals=1, space="buy", optimize=True)
buy_sl_trail_2 = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space="buy", optimize=True)
buy_sl_trail_3 = DecimalParameter(1.0, 2.0, default=1.5, decimals=1, space="buy", optimize=True)
# Trend
buy_adx_long = IntParameter(15, 28, default=20, space="buy", optimize=True)
buy_adx_short = IntParameter(20, 35, default=28, space="buy", optimize=True)
# Pullback
buy_pb_candles = IntParameter(2, 4, default=2, space="buy", optimize=True)
buy_rsi_pb_long = IntParameter(30, 48, default=42, space="buy", optimize=True)
buy_rsi_pb_short = IntParameter(52, 70, default=58, space="buy", optimize=True)
# Volume
buy_vol_min = DecimalParameter(0.3, 1.0, default=0.6, decimals=1, space="buy", optimize=True)
# Max daily
buy_max_daily = IntParameter(6, 15, default=10, space="buy", optimize=True)
# Early exit thresholds
buy_early_exit_candles = IntParameter(3, 8, default=4, space="buy", optimize=True)
buy_early_exit_rsi_long = IntParameter(28, 40, default=35, space="buy", optimize=True)
buy_early_exit_rsi_short = IntParameter(60, 72, default=65, space="buy", optimize=True)
# Time force (only for losers)
buy_max_hold_loser = IntParameter(16, 48, default=32, space="buy", optimize=True)
# ═══════════════════════════════════════════════════════════════════
# INFORMATIVE
# ═══════════════════════════════════════════════════════════════════
def informative_pairs(self):
return [("BTC/USDT:USDT", "1h")]
# ═══════════════════════════════════════════════════════════════════
# INDICATORS
# ═══════════════════════════════════════════════════════════════════
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["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
dataframe["vol_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
dataframe["vol_ratio"] = dataframe["volume"] / dataframe["vol_sma"].replace(0, 1)
c = dataframe["close"]
o = dataframe["open"]
h = dataframe["high"]
l = dataframe["low"]
dataframe["is_green"] = (c > o).astype(int)
dataframe["is_red"] = (c < o).astype(int)
dataframe["body"] = abs(c - o)
dataframe["lower_wick"] = pd.concat([c, o], axis=1).min(axis=1) - l
dataframe["upper_wick"] = h - pd.concat([c, o], axis=1).max(axis=1)
dataframe["range"] = h - l
dataframe["prev_red"] = dataframe["is_red"].shift(1)
dataframe["prev_green"] = dataframe["is_green"].shift(1)
dataframe["prev_close"] = c.shift(1)
dataframe["prev_open"] = o.shift(1)
# Engulfing
dataframe["bullish_engulf"] = (
(dataframe["is_green"] == 1) &
(dataframe["prev_red"] == 1) &
(o <= dataframe["prev_close"]) &
(c >= dataframe["prev_open"])
).astype(int)
atr_safe = dataframe["atr"].replace(0, 1)
dataframe["hammer"] = (
(dataframe["is_green"] == 1) &
(dataframe["lower_wick"] > 2 * dataframe["body"].replace(0, 0.01)) &
(dataframe["lower_wick"] > 2 * dataframe["upper_wick"].replace(0, 0.01)) &
(dataframe["range"] > 0.3 * atr_safe)
).astype(int)
dataframe["bearish_engulf"] = (
(dataframe["is_red"] == 1) &
(dataframe["prev_green"] == 1) &
(o >= dataframe["prev_close"]) &
(c <= dataframe["prev_open"])
).astype(int)
dataframe["shooting_star"] = (
(dataframe["is_red"] == 1) &
(dataframe["upper_wick"] > 2 * dataframe["body"].replace(0, 0.01)) &
(dataframe["upper_wick"] > 2 * dataframe["lower_wick"].replace(0, 0.01)) &
(dataframe["range"] > 0.3 * atr_safe)
).astype(int)
dataframe["pat_bull"] = ((dataframe["bullish_engulf"] == 1) | (dataframe["hammer"] == 1)).astype(int)
dataframe["pat_bear"] = ((dataframe["bearish_engulf"] == 1) | (dataframe["shooting_star"] == 1)).astype(int)
# Consecutive candles
red = dataframe["is_red"].values
green = dataframe["is_green"].values
n = len(dataframe)
c_red = np.zeros(n, dtype=int)
c_green = np.zeros(n, dtype=int)
for i in range(1, n):
c_red[i] = c_red[i - 1] + 1 if red[i - 1] else 0
c_green[i] = c_green[i - 1] + 1 if green[i - 1] else 0
dataframe["consec_red"] = c_red
dataframe["consec_green"] = c_green
# 1h merge
if self.dp:
pair = metadata["pair"]
inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h")
if not inf_1h.empty:
inf_1h["ema9"] = ta.EMA(inf_1h, timeperiod=9)
inf_1h["ema21"] = ta.EMA(inf_1h, timeperiod=21)
inf_1h["adx"] = ta.ADX(inf_1h, timeperiod=14)
dataframe = merge_informative_pair(
dataframe, inf_1h, self.timeframe, "1h", ffill=True
)
return dataframe
# ═══════════════════════════════════════════════════════════════════
# ENTRIES — Deep pullback + pattern (from v7, slightly relaxed)
# ═══════════════════════════════════════════════════════════════════
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
has_1h = "ema9_1h" in dataframe.columns
pb = self.buy_pb_candles.value
rsi_l = self.buy_rsi_pb_long.value
rsi_s = self.buy_rsi_pb_short.value
vol_min = self.buy_vol_min.value
vol_ok = dataframe["vol_ratio"] > vol_min
has_vol = dataframe["volume"] > 0
atr_ok = dataframe["atr"] > 1
if has_1h:
h1_up = (dataframe["ema9_1h"] > dataframe["ema21_1h"]) & (dataframe["adx_1h"] > self.buy_adx_long.value)
h1_dn = (dataframe["ema9_1h"] < dataframe["ema21_1h"]) & (dataframe["adx_1h"] > self.buy_adx_short.value)
else:
h1_up = pd.Series(True, index=dataframe.index)
h1_dn = pd.Series(True, index=dataframe.index)
# Pullback
pullback_long = (dataframe["consec_red"] >= pb) | (dataframe["rsi"] < rsi_l)
pullback_short = (dataframe["consec_green"] >= pb) | (dataframe["rsi"] > rsi_s)
# Pattern entries
pat_long = dataframe["pat_bull"] == 1
pat_short = dataframe["pat_bear"] == 1
# Simple entries (green/red after pullback, with EMA confirmation)
simple_long = (dataframe["is_green"] == 1) & (dataframe["ema9"] > dataframe["ema21"])
simple_short = (dataframe["is_red"] == 1) & (dataframe["ema9"] < dataframe["ema21"])
go_long_pat = h1_up & pullback_long & pat_long & vol_ok & has_vol & atr_ok
go_long_simple = h1_up & pullback_long & simple_long & vol_ok & has_vol & atr_ok
go_short_pat = h1_dn & pullback_short & pat_short & vol_ok & has_vol & atr_ok
go_short_simple = h1_dn & pullback_short & simple_short & vol_ok & has_vol & atr_ok
dataframe.loc[go_long_pat | go_long_simple, "enter_long"] = 1
dataframe.loc[go_short_pat | go_short_simple, "enter_short"] = 1
return dataframe
# ═══════════════════════════════════════════════════════════════════
# EXIT — Let trailing work
# ═══════════════════════════════════════════════════════════════════
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[dataframe["rsi"] > 90, "exit_long"] = 1
dataframe.loc[dataframe["rsi"] < 10, "exit_short"] = 1
return dataframe
# ═══════════════════════════════════════════════════════════════════
# CUSTOM STOPLOSS — Progressive + breakeven at 0.5R
# ═══════════════════════════════════════════════════════════════════
def custom_stoploss(self, pair, trade, current_time, current_rate,
current_profit, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) == 0:
return -0.035
last = dataframe.iloc[-1]
atr = last.get("atr", 0)
if atr == 0 or trade.open_rate == 0:
return -0.035
r_pct = self.buy_sl_initial.value * atr / trade.open_rate
r_pct = max(0.015, min(r_pct, 0.05))
if current_profit <= 0:
return -r_pct
r_mult = current_profit / r_pct
if r_mult >= 3.0:
trail = self.buy_sl_trail_3.value * atr / trade.open_rate
elif r_mult >= 2.0:
trail = self.buy_sl_trail_2.value * atr / trade.open_rate
elif r_mult >= 1.0:
trail = self.buy_sl_trail_1.value * atr / trade.open_rate
elif r_mult >= 0.5:
# New: breakeven at 0.5R
is_short = trade.is_short if hasattr(trade, "is_short") else False
return stoploss_from_open(0.001, current_profit, is_short=is_short)
else:
return -r_pct
trail = max(0.008, min(trail, 0.04))
return -trail
# ═══════════════════════════════════════════════════════════════════
# CUSTOM EXIT — Early loser cut + time for losers only
# ═══════════════════════════════════════════════════════════════════
def custom_exit(self, pair, trade, current_time, current_rate,
current_profit, **kwargs) -> Optional[str]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) == 0:
return None
last = dataframe.iloc[-1]
is_short = trade.is_short if hasattr(trade, "is_short") else False
minutes = (current_time - trade.open_date_utc).total_seconds() / 60
candles = int(minutes / 15)
# ── EARLY LOSER CUT ──
# After N candles, if trade is losing and conditions deteriorating → exit
if candles >= self.buy_early_exit_candles.value and current_profit < -0.003:
rsi = last.get("rsi", 50)
ema9 = last.get("ema9", 0)
ema21 = last.get("ema21", 0)
if not is_short:
# Long: RSI dropping + EMA broken → cut loss early
if rsi < self.buy_early_exit_rsi_long.value and ema9 < ema21:
return "v8_early_cut_long"
else:
# Short: RSI rising + EMA crossed up → cut loss early
if rsi > self.buy_early_exit_rsi_short.value and ema9 > ema21:
return "v8_early_cut_short"
# ── TIME FORCE: ONLY for losers ──
max_min_loser = self.buy_max_hold_loser.value * 15
if minutes > max_min_loser and current_profit < 0:
return "v8_time_force_loser"
# ── Winners: no time limit! Let trailing work ──
# (but add a very generous time limit as absolute safety)
if minutes > 1440 and current_profit > 0.005: # 24h max
return "v8_time_profit_24h"
if minutes > 1440:
return "v8_time_force_24h"
return None
# ═══════════════════════════════════════════════════════════════════
# CONFIRM
# ═══════════════════════════════════════════════════════════════════
def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs) -> bool:
today = current_time.strftime("%Y-%m-%d")
if today not in self._daily_trades:
self._daily_trades = {today: 0}
if self._daily_trades.get(today, 0) >= self.buy_max_daily.value:
return False
if self._consecutive_losses >= 3 and self._last_loss_time:
if current_time < self._last_loss_time + timedelta(minutes=45):
return False
self._consecutive_losses = 0
self._daily_trades[today] = self._daily_trades.get(today, 0) + 1
return True
def confirm_trade_exit(self, pair, trade, order_type, amount, rate,
time_in_force, exit_reason, current_time, **kwargs) -> bool:
if trade.calc_profit_ratio(rate) < 0:
self._consecutive_losses += 1
self._last_loss_time = current_time
else:
self._consecutive_losses = 0
return True