Timeframe
15m
Direction
Long & Short
Stoploss
-1.0%
Trailing Stop
No
ROI
0m: 1.0%, 1m: 100.0%
Interface Version
3
Startup Candles
50
Indicators
2
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisMultiScalp v3 — Go simple, go native
============================================
LESSON LEARNED: custom_stoploss is a trap for simulating tight TP/SL.
Native stoploss + minimal_roi is rock-solid and correctly simulates
intra-candle behavior.
MATH FOR 1:1 R:R:
Random WR = ~50%
Fee impact = 0.04% / 1.0% target = 4% overhead
Need WR > 52% to be profitable
RSI extremes show +7pp edge → WR ~57% → PF 1.33
"""
import logging
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 OsirisMultiScalpV3(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "15m"
startup_candle_count = 50
process_only_new_candles = True
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False,
}
# ── Fixed SL & TP ────────────────────────────────────────────────
stoploss = -0.01 # 1% fixed SL from open
minimal_roi = {
"0": 0.01, # 1% TP → 1:1 R:R with 1% SL
}
trailing_stop = False
use_custom_stoploss = False
# ── Entry params (hyperoptable) ──────────────────────────────────
long_rsi = IntParameter(20, 45, default=35, space="buy", optimize=True)
short_rsi = IntParameter(55, 80, default=65, space="buy", optimize=True)
rsi_period = IntParameter(10, 20, default=14, space="buy", optimize=True)
use_bb_filter = IntParameter(0, 1, default=0, space="buy", optimize=True)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
for period in range(10, 21):
dataframe[f"rsi_{period}"] = ta.RSI(dataframe, timeperiod=period)
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upper"] = bb["upperband"]
dataframe["bb_lower"] = bb["lowerband"]
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
rsi = dataframe[f"rsi_{self.rsi_period.value}"]
rsi_1 = rsi.shift(1)
rsi_2 = rsi.shift(2)
# Fresh RSI cross: wasn't extreme recently (prevents stacking)
long_fresh = (rsi_1 >= self.long_rsi.value) | (rsi_2 >= self.long_rsi.value)
long_base = (rsi < self.long_rsi.value) & long_fresh & (dataframe["volume"] > 0)
short_fresh = (rsi_1 <= self.short_rsi.value) | (rsi_2 <= self.short_rsi.value)
short_base = (rsi > self.short_rsi.value) & short_fresh & (dataframe["volume"] > 0)
# Optional BB filter (closer to band = stronger mean-rev)
if self.use_bb_filter.value == 1:
long_base = long_base & (dataframe["close"] <= dataframe["bb_lower"] * 1.005)
short_base = short_base & (dataframe["close"] >= dataframe["bb_upper"] * 0.995)
dataframe.loc[long_base, "enter_long"] = 1
dataframe.loc[short_base, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["exit_long"] = 0
dataframe["exit_short"] = 0
return dataframe