Timeframe
15m
Direction
Long & Short
Stoploss
-1.0%
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
"""
OsirisMultiScalp v2 — Multi-Pair Mean Reversion (ATR-aware)
============================================================
DATA FINDINGS:
- ATR on 15m = 0.4-0.7% for top 10 pairs
- SL must be >= 1x ATR to survive intra-candle noise
- With SL=1%, TP=2% (2:1), fee is only 2-4% of TP
- RSI mean-reversion has genuine statistical edge
SIGNALS:
- LONG: RSI fresh cross below threshold (oversold → buy)
- SHORT: RSI fresh cross above threshold (overbought → sell)
- Fresh = RSI wasn't extreme 1-2 candles ago (no stacking)
EXIT: Native Freqtrade stoploss + minimal_roi (rock-solid simulation)
"""
import logging
import numpy as np
from pandas import DataFrame
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import (
IStrategy,
DecimalParameter,
IntParameter,
)
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisMultiScalpV2(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "15m"
startup_candle_count = 50
process_only_new_candles = True
# Order type: LIMIT for maker fees
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False,
}
# ── Native SL/TP ─────────────────────────────────────────────────
stoploss = -0.01
minimal_roi = {"0": 100} # disabled — exit via custom_stoploss
trailing_stop = False
use_custom_stoploss = True
# ── TP/SL params (hyperoptable) ──────────────────────────────────
tp_pct = DecimalParameter(0.5, 4.0, default=2.0, decimals=1, space="sell", optimize=True)
sl_pct = DecimalParameter(0.3, 2.0, default=1.0, decimals=1, space="sell", optimize=True)
max_hold = IntParameter(8, 96, default=48, space="sell", optimize=True)
# ── Entry params (hyperoptable) ──────────────────────────────────
long_rsi = IntParameter(20, 40, default=35, space="buy", optimize=True)
short_rsi = IntParameter(60, 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)
use_adx_filter = IntParameter(0, 1, default=0, space="buy", optimize=True)
adx_min = IntParameter(15, 30, default=20, space="buy", optimize=True)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# RSI for multiple periods
for period in range(10, 21):
dataframe[f"rsi_{period}"] = ta.RSI(dataframe, timeperiod=period)
# Bollinger Bands
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upper"] = bb["upperband"]
dataframe["bb_lower"] = bb["lowerband"]
# ADX
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
return dataframe
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
is_short = trade.is_short
tp = self.tp_pct.value / 100
sl = self.sl_pct.value / 100
# TP: close when profit exceeds target
if current_profit >= tp:
return 0.001 # tiny positive = close immediately
# Timeout: close after max_hold candles
duration_min = (current_time - trade.open_date_utc).total_seconds() / 60
max_min = self.max_hold.value * 15 # 15m candle
if duration_min >= max_min:
return 0.001
# SL: fixed from open
return -sl
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 long entry
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)
# Fresh short entry
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
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)
# Optional ADX filter (trend strength)
if self.use_adx_filter.value == 1:
adx_ok = dataframe["adx"] > self.adx_min.value
long_base = long_base & adx_ok
short_base = short_base & adx_ok
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:
# All exits via stoploss + minimal_roi
dataframe["exit_long"] = 0
dataframe["exit_short"] = 0
return dataframe