Timeframe
5m
Direction
Long & Short
Stoploss
-2.5%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
50
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisGoldV6 — Aggressive BB Mean-Reversion (BTC 5m)
=====================================================
V5 FAILED because:
- RSI<30 filter too strict → only 0.7 trades/day
- Timeout 30min too short → kills trades before reversion
- SL 0.8% < natural TP → risk:reward inverted
V6 FIXES:
- NO RSI filter on entry (BB touch IS the signal)
- Candle confirmation only (green after touching lower = bounce)
- Timeout 12 candles (1h) — reversion needs time
- SL = 1.5× distance to BB mid (asymmetric in our favor)
- TP at BB mid (or 50% of distance for partial)
MATH:
BB(20,2) lower touch = ~2-3% of candles = ~6-8/day
With green candle + volume = ~3-5/day
BB width on BTC 5m avg = ~0.6% ($500)
TP = half width = ~0.3% ($250)
SL = -0.9% (1.5× TP)
Need WR > 60% for positive EV
"""
import logging
import numpy as np
from pandas import DataFrame
from freqtrade.persistence import Trade
from freqtrade.strategy import (
IStrategy,
DecimalParameter,
IntParameter,
)
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisGoldV6(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "5m"
stoploss = -0.025 # Emergency hard stop 2.5%
minimal_roi = {"0": 100} # Handled by custom_exit
trailing_stop = False
use_custom_stoploss = False
startup_candle_count = 50
process_only_new_candles = True
# ── Entry params ─────────────────────────────────────────────────────────
bb_period = IntParameter(14, 30, default=20, space="buy", optimize=True)
bb_std = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space="buy", optimize=True)
require_volume = DecimalParameter(0.8, 2.0, default=1.0, decimals=1, space="buy", optimize=True)
# ── Exit params ──────────────────────────────────────────────────────────
tp_bb_frac = DecimalParameter(0.3, 1.0, default=0.6, decimals=1, space="sell", optimize=True)
sl_mult = DecimalParameter(1.0, 3.0, default=1.8, decimals=1, space="sell", optimize=True)
timeout_candles = IntParameter(6, 24, default=12, space="sell", optimize=True)
_last_entry: dict = {}
_entry_bb_mid: dict = {}
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Pre-compute BB for all hyperopt combinations
for period in range(14, 31, 2):
for std_x10 in range(15, 31, 5):
std = std_x10 / 10.0
bb = ta.BBANDS(dataframe, timeperiod=period, nbdevup=std, nbdevdn=std)
c = f"_{period}_{std_x10}"
dataframe[f"bbu{c}"] = bb["upperband"]
dataframe[f"bbl{c}"] = bb["lowerband"]
dataframe[f"bbm{c}"] = bb["middleband"]
# Volume
dataframe["vol_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
dataframe["rvol"] = dataframe["volume"] / dataframe["vol_sma"].replace(0, np.nan)
# Candle
dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)
# ATR for reference
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
p = int(self.bb_period.value)
s = int(float(self.bb_std.value) * 10)
vol_m = float(self.require_volume.value)
c = f"_{p}_{s}"
bbl = dataframe[f"bbl{c}"]
bbu = dataframe[f"bbu{c}"]
bbm = dataframe[f"bbm{c}"]
has_data = dataframe["vol_sma"].notna() & (dataframe["volume"] > 0)
# LONG: Wick below lower BB + close above + green + volume
long_sig = (
has_data
& (dataframe["low"] <= bbl)
& (dataframe["close"] > bbl)
& (dataframe["is_green"] == 1)
& (dataframe["rvol"] >= vol_m)
)
# SHORT: Wick above upper BB + close below + red + volume
short_sig = (
has_data
& (dataframe["high"] >= bbu)
& (dataframe["close"] < bbu)
& (dataframe["is_red"] == 1)
& (dataframe["rvol"] >= vol_m)
)
dataframe.loc[long_sig, "enter_long"] = 1
dataframe.loc[long_sig, "enter_tag"] = "bb_bounce"
dataframe.loc[short_sig, "enter_short"] = 1
dataframe.loc[short_sig, "enter_tag"] = "bb_reject"
# Store BB mid for TP target
dataframe["_bb_mid"] = bbm
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
def confirm_trade_entry(
self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs,
) -> bool:
# 2 candle cooldown
last = self._last_entry.get(pair)
if last is not None and (current_time - last).total_seconds() < 600:
return False
self._last_entry[pair] = current_time
# Store BB mid at entry for TP target
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if not dataframe.empty:
self._entry_bb_mid[pair] = dataframe.iloc[-1].get("_bb_mid", rate)
return True
def custom_exit(
self, pair, trade, current_time, current_rate, current_profit, **kwargs,
):
is_short = getattr(trade, "is_short", False)
entry_rate = trade.open_rate
# Get current BB mid
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty:
return None
bb_mid = dataframe.iloc[-1].get("_bb_mid", entry_rate)
# Calculate distances
if not is_short:
dist_to_mid = (bb_mid - entry_rate) / entry_rate
else:
dist_to_mid = (entry_rate - bb_mid) / entry_rate
tp_frac = float(self.tp_bb_frac.value)
sl_m = float(self.sl_mult.value)
# TP: Fraction of distance to BB mid
tp_target = abs(dist_to_mid) * tp_frac
if tp_target < 0.001:
tp_target = 0.001 # Min 0.1%
if current_profit >= tp_target:
return "mr_tp"
# SL: Multiple of TP distance (ensures favorable RR)
sl_limit = tp_target * sl_m
if current_profit <= -sl_limit:
return "mr_sl"
# Timeout
if trade.open_date_utc:
elapsed_candles = (current_time - trade.open_date_utc).total_seconds() / 300
if elapsed_candles >= self.timeout_candles.value:
return "mr_timeout"
return None
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0