Timeframe
5m
Direction
Long & Short
Stoploss
-2.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
100
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisGoldV2 — High-Frequency Mean-Reversion Scalper (BTC/USDT 5m)
====================================================================
TARGET: 10+ trades/day | 65%+ Win Rate | 2:1+ R:R
MATH:
288 candles/day on 5m. Need ~10 entries = trigger ~3.5% of candles.
RSI(7) crosses above 40 from below = ~5% of candles
+ 1h trend alignment = ~2.5%
+ candle color confirmation = ~1.8%
Multiple patterns combined = ~3-5% trigger rate = 10-14 trades/day
PATTERNS:
A) RSI_BOUNCE: RSI(7) crosses 40 from below + green candle + 1h up
B) EMA_TOUCH: Price touches EMA(21) from above in uptrend + bounce
C) BB_MEAN: Close below BB middle, bounces back + volume
D) RSI_SHORT: RSI(7) crosses 60 from above + red candle + 1h down
E) EMA_REJ: Price touches EMA(21) from below in downtrend + rejected
RISK:
SL = 0.35% (ATR-calibrated micro stop)
TP = 0.70% (2:1 ratio)
BE at 1x SL
Max hold: 12 candles (1h)
"""
import logging
import numpy as np
import pandas as pd
from datetime import datetime
from pandas import DataFrame
from freqtrade.persistence import Trade
from freqtrade.strategy import (
IStrategy,
DecimalParameter,
IntParameter,
merge_informative_pair,
)
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 OsirisGoldV2(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "5m"
stoploss = -0.02
minimal_roi = {"0": 100}
trailing_stop = False
use_custom_stoploss = True
startup_candle_count = 100
process_only_new_candles = True
# ── Risk params ──────────────────────────────────────────────────────────
sl_pct = DecimalParameter(
0.15, 0.60, default=0.35, decimals=2, space="sell", optimize=True
)
rr_ratio = DecimalParameter(
1.5, 3.0, default=2.0, decimals=1, space="sell", optimize=True
)
max_hold = IntParameter(6, 24, default=12, space="sell", optimize=True)
# ── Entry params ─────────────────────────────────────────────────────────
rsi_bounce = IntParameter(30, 45, default=40, space="buy", optimize=True)
rsi_reject = IntParameter(55, 70, default=60, space="buy", optimize=True)
ema_period = IntParameter(15, 30, default=21, space="buy", optimize=True)
ema_tol = DecimalParameter(
0.05, 0.30, default=0.15, decimals=2, space="buy", optimize=True
)
cooldown = IntParameter(1, 4, default=2, space="buy", optimize=True)
_last_entry_time: dict = {}
# ── Informative pairs ────────────────────────────────────────────────────
def informative_pairs(self):
pairs = self.dp.current_whitelist() if self.dp else []
return [(p, "1h") for p in pairs]
# ── Indicators ───────────────────────────────────────────────────────────
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata["pair"]
ema_p = int(self.ema_period.value)
# ── 5m ───────────────────────────────────────────────────────────────
dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=7)
dataframe["rsi7_prev"] = dataframe["rsi7"].shift(1)
dataframe["ema"] = ta.EMA(dataframe, timeperiod=ema_p)
dataframe["ema_prev"] = dataframe["ema"].shift(1)
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_mid"] = bb["middleband"]
dataframe["bb_lower"] = bb["lowerband"]
dataframe["bb_upper"] = bb["upperband"]
dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)
# Price relative to EMA (%)
dataframe["ema_dist"] = (dataframe["close"] - dataframe["ema"]) / dataframe["ema"] * 100
# ── 1h trend ─────────────────────────────────────────────────────────
inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h")
if not inf_1h.empty:
inf_1h["ema50_1h"] = ta.EMA(inf_1h, timeperiod=50)
inf_1h["ema20_1h"] = ta.EMA(inf_1h, timeperiod=20)
m1h = merge_informative_pair(
dataframe,
inf_1h[["date", "ema50_1h", "ema20_1h", "close"]],
self.timeframe, "1h", ffill=True,
)
dataframe["ema50_1h"] = m1h["ema50_1h_1h"].values
dataframe["ema20_1h"] = m1h["ema20_1h_1h"].values
dataframe["close_1h"] = m1h["close_1h"].values
else:
dataframe["ema50_1h"] = dataframe["close"]
dataframe["ema20_1h"] = dataframe["close"]
dataframe["close_1h"] = dataframe["close"]
# 1h trend signals
dataframe["trend_up"] = (
(dataframe["close_1h"] > dataframe["ema50_1h"]) &
(dataframe["ema20_1h"] > dataframe["ema50_1h"])
).astype(int)
dataframe["trend_dn"] = (
(dataframe["close_1h"] < dataframe["ema50_1h"]) &
(dataframe["ema20_1h"] < dataframe["ema50_1h"])
).astype(int)
return dataframe
# ── Entry signals ────────────────────────────────────────────────────────
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
bounce = int(self.rsi_bounce.value)
reject = int(self.rsi_reject.value)
tol = float(self.ema_tol.value)
has_data = dataframe["rsi7"].notna() & (dataframe["volume"] > 0)
# ═════════════════════════════════════════════════════════════════════
# LONG A: RSI(7) bounce from oversold + green + uptrend
# RSI was below threshold, now crossing back above
# ═════════════════════════════════════════════════════════════════════
long_rsi = (
has_data
& (dataframe["trend_up"] == 1)
& (dataframe["rsi7_prev"] < bounce)
& (dataframe["rsi7"] >= bounce)
& (dataframe["is_green"] == 1)
)
# ═════════════════════════════════════════════════════════════════════
# LONG B: EMA touch (pullback to EMA in uptrend)
# Price dipped close to EMA and bounced
# ═════════════════════════════════════════════════════════════════════
long_ema = (
has_data
& (dataframe["trend_up"] == 1)
& (dataframe["low"] <= dataframe["ema"] * (1 + tol / 100))
& (dataframe["close"] > dataframe["ema"])
& (dataframe["is_green"] == 1)
& (dataframe["rsi7"] > 35)
& (dataframe["rsi7"] < 55)
)
# ═════════════════════════════════════════════════════════════════════
# LONG C: BB middle bounce (mean reversion in uptrend)
# Price was below BB middle, closing above
# ═════════════════════════════════════════════════════════════════════
long_bb = (
has_data
& (dataframe["trend_up"] == 1)
& (dataframe["low"] <= dataframe["bb_mid"])
& (dataframe["close"] > dataframe["bb_mid"])
& (dataframe["is_green"] == 1)
& (dataframe["volume"] > dataframe["volume_sma"] * 0.8)
)
# ═════════════════════════════════════════════════════════════════════
# SHORT D: RSI(7) rejection from overbought + red + downtrend
# ═════════════════════════════════════════════════════════════════════
short_rsi = (
has_data
& (dataframe["trend_dn"] == 1)
& (dataframe["rsi7_prev"] > reject)
& (dataframe["rsi7"] <= reject)
& (dataframe["is_red"] == 1)
)
# ═════════════════════════════════════════════════════════════════════
# SHORT E: EMA rejection (bounce off EMA from below in downtrend)
# ═════════════════════════════════════════════════════════════════════
short_ema = (
has_data
& (dataframe["trend_dn"] == 1)
& (dataframe["high"] >= dataframe["ema"] * (1 - tol / 100))
& (dataframe["close"] < dataframe["ema"])
& (dataframe["is_red"] == 1)
& (dataframe["rsi7"] > 45)
& (dataframe["rsi7"] < 65)
)
# ═════════════════════════════════════════════════════════════════════
# COMBINE with tags
# ═════════════════════════════════════════════════════════════════════
dataframe.loc[long_rsi, ["enter_long", "enter_tag"]] = [1, "rsi_bounce"]
dataframe.loc[long_ema & ~long_rsi, ["enter_long", "enter_tag"]] = [1, "ema_touch"]
dataframe.loc[long_bb & ~long_rsi & ~long_ema, ["enter_long", "enter_tag"]] = [1, "bb_mean"]
dataframe.loc[short_rsi, ["enter_short", "enter_tag"]] = [1, "rsi_reject"]
dataframe.loc[short_ema & ~short_rsi, ["enter_short", "enter_tag"]] = [1, "ema_reject"]
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# ── Trade management ─────────────────────────────────────────────────────
def confirm_trade_entry(
self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs,
) -> bool:
last = self._last_entry_time.get(pair)
cd_seconds = int(self.cooldown.value) * 300
if last is not None:
if (current_time - last).total_seconds() < cd_seconds:
return False
self._last_entry_time[pair] = current_time
return True
def custom_stoploss(
self, pair, trade: Trade, current_time,
current_rate: float, current_profit: float, **kwargs,
) -> float:
sl = float(self.sl_pct.value) / 100
# Breakeven at 1x SL profit
if current_profit >= sl:
is_short = getattr(trade, "is_short", False)
return stoploss_from_open(0.001, current_profit, is_short=is_short)
return -sl
def custom_exit(
self, pair, trade, current_time, current_rate, current_profit, **kwargs,
):
sl = float(self.sl_pct.value) / 100
tp = sl * float(self.rr_ratio.value)
if current_profit >= tp:
return "tp_hit"
if trade.open_date_utc:
elapsed = (current_time - trade.open_date_utc).total_seconds() / 300
if elapsed >= self.max_hold.value:
return "timeout"
return None
def leverage(
self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs,
) -> float:
return 1.0