OSIRIS v9 — Fixed stop + time-based profit taking. Based on v7 analysis: trailing kills, time_profit is the only edge. Strategy: hold position, fixed stop, take profit on time or target.
Timeframe
15m
Direction
Long & Short
Stoploss
-5.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 v9 — Fixed Stop + Time Profit Edge
================================================================
DIAGNÓSTICO v7 (PF 0.76):
trailing_stop_loss: 298 trades, WR 25%, -513 USDT = KILLER
time_force: 150 trades, WR 18%, -151 USDT = bad
time_profit: 186 trades, WR 100%, +434 USDT = ONLY EDGE
CONCLUSÃO: o trailing progressivo PIORA o resultado. Winners saem
por time_profit, losers saem por trailing (com loss ampliado).
v9 FIX:
1. STOP FIXO (sem trailing complexo) — 3.5 ATR, ponto FINAL
2. BREAKEVEN simples em 0.5R (move stop para +0.1%)
3. TARGET ESCALONADO: 50% profit em 1R, trail o resto
4. TIME PROFIT expandido (funciona!)
5. TIME FORCE com stop loss APERTADO (sai cedo se perdendo)
6. Pullback entries do v7 (funcionam bem)
"""
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 OsirisDayTradeV9(IStrategy):
"""
OSIRIS v9 — Fixed stop + time-based profit taking.
Based on v7 analysis: trailing kills, time_profit is the only edge.
Strategy: hold position, fixed stop, take profit on time or target.
"""
INTERFACE_VERSION = 3
can_short = True
timeframe = "15m"
# Disabled — custom exit handles everything
minimal_roi = {"0": 0.50}
# Fixed stoploss (backup only)
stoploss = -0.05
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
# ═══════════════════════════════════════════════════════════════════
# Stop: fixed ATR distance
buy_sl_atr = DecimalParameter(3.0, 5.0, default=3.5, decimals=1, space="buy", optimize=True)
# Target: R multiple
buy_tp_r = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="buy", optimize=True)
# Breakeven at this R
buy_be_r = DecimalParameter(0.3, 0.8, default=0.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, 5, default=3, space="buy", optimize=True)
buy_rsi_pb_long = IntParameter(30, 48, default=40, space="buy", optimize=True)
buy_rsi_pb_short = IntParameter(52, 70, default=60, 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)
# Time params (candles)
buy_time_profit_min = IntParameter(4, 16, default=8, space="buy", optimize=True)
buy_time_force_max = IntParameter(24, 64, default=48, 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, o, h, l = dataframe["close"], dataframe["open"], dataframe["high"], 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
# Previous candle
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)
# Reversal patterns
atr_safe = dataframe["atr"].replace(0, 1)
dataframe["bullish_engulf"] = (
(dataframe["is_green"] == 1) &
(dataframe["prev_red"] == 1) &
(o <= dataframe["prev_close"]) &
(c >= dataframe["prev_open"])
).astype(int)
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
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 — Same pullback logic as v7 (proven)
# ═══════════════════════════════════════════════════════════════════
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)
# Deep 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 (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 = h1_up & pullback_long & (pat_long | (simple_long & (dataframe["close"] > dataframe["ema50"]))) & vol_ok & has_vol & atr_ok
go_short = h1_dn & pullback_short & (pat_short | (simple_short & (dataframe["close"] < dataframe["ema50"]))) & vol_ok & has_vol & atr_ok
dataframe.loc[go_long, "enter_long"] = 1
dataframe.loc[go_short, "enter_short"] = 1
return dataframe
# ═══════════════════════════════════════════════════════════════════
# EXIT
# ═══════════════════════════════════════════════════════════════════
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 — FIXED (no trailing!)
# ═══════════════════════════════════════════════════════════════════
def custom_stoploss(self, pair, trade, current_time, current_rate,
current_profit, **kwargs) -> float:
"""
Fixed stop at entry. Only moves to breakeven at 0.5R.
NO trailing — v7 analysis showed trailing kills profits.
"""
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
# Fixed stop distance
r_pct = self.buy_sl_atr.value * atr / trade.open_rate
r_pct = max(0.015, min(r_pct, 0.05))
# Breakeven: if profit >= be_r × R → move stop to +0.1%
be_threshold = self.buy_be_r.value * r_pct
if current_profit >= be_threshold:
is_short = trade.is_short if hasattr(trade, "is_short") else False
return stoploss_from_open(0.001, current_profit, is_short=is_short)
# Otherwise: fixed stop
return -r_pct
# ═══════════════════════════════════════════════════════════════════
# CUSTOM EXIT — Target + Time Profit (the REAL edge)
# ═══════════════════════════════════════════════════════════════════
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]
atr = last.get("atr", 0)
if atr == 0 or trade.open_rate == 0:
return None
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)
# R calculation
r_pct = self.buy_sl_atr.value * atr / trade.open_rate
r_pct = max(0.015, min(r_pct, 0.05))
target_pct = r_pct * self.buy_tp_r.value
# ── FIXED TARGET HIT ──
if current_profit >= target_pct:
return "v9_tp"
# ── TIME PROFIT (THE EDGE from v7 analysis) ──
# After N candles, if profitable → take it
if candles >= self.buy_time_profit_min.value and current_profit > 0.003:
return "v9_time_profit"
# ── MOMENTUM EXIT: trend lost while losing ──
if candles >= 4 and current_profit < -0.003:
rsi = last.get("rsi", 50)
ema9 = last.get("ema9", 0)
ema21 = last.get("ema21", 0)
if not is_short and rsi < 35 and ema9 < ema21:
return "v9_momentum_exit_long"
if is_short and rsi > 65 and ema9 > ema21:
return "v9_momentum_exit_short"
# ── TIME FORCE: only for losers ──
max_min = self.buy_time_force_max.value * 15
if minutes > max_min and current_profit < 0:
return "v9_time_force"
# Winners can run until target or time_profit catches them
if minutes > max_min and current_profit > 0.002:
return "v9_time_profit_late"
# Hard cap at 24h
if minutes > 1440:
return "v9_hard_cap"
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=60):
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