Timeframe
15m
Direction
Long & Short
Stoploss
-5.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
50
Indicators
1
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
this is an example class, implementing a PSAR based trailing stop loss you are supposed to take the `custom_stoploss()` and `populate_indicators()` parts and adapt it to your own strategy
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisMultiScalp — Multi-Pair Mean Reversion Day Trade
========================================================
DATA-DRIVEN strategy built from empirical analysis of 10 pairs x 809 days.
PROVEN EDGE (maker fee 0.02%/side):
- SHORT mean-rev: RSI > 60 → short, TP=0.6% SL=0.2% (3:1 R:R)
~3/day/pair, 34% WR, +711% net across 10 pairs
- LONG mean-rev: RSI < 35 → long, TP=0.7% SL=0.23% (3:1 R:R)
~2/day/pair, 35% WR, +672% net across 10 pairs
- Combined: ~50 ops/day across 10 pairs
KEY REQUIREMENTS:
- MUST use limit orders (maker fee 0.02% vs taker 0.04%)
- Multi-pair mandatory — edge is thin per pair, aggregated over many
- BTC alone is NOT profitable — altcoins (SOL, DOGE, ADA, XRP) carry the edge
PARAMETERS (from fine-grained sweep):
- LONG: RSI < 35, TP=0.7%, SL=0.23%, max_hold=12 candles (3h)
- SHORT: RSI > 60, TP=0.6%, SL=0.20%, max_hold=18 candles (4.5h)
- R:R = 3:1 on both sides
- Avg hold: 1.7-4.4 candles (25min-1h)
"""
import logging
from pandas import DataFrame
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
from freqtrade.strategy import (
IStrategy,
DecimalParameter,
IntParameter,
)
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 OsirisMultiScalp(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "15m"
# Wide stoploss — real exit via custom_stoploss
stoploss = -0.05
minimal_roi = {"0": 100}
trailing_stop = False
use_custom_stoploss = True
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,
}
# ── SHORT params ──────────────────────────────────────────────────
short_rsi_threshold = IntParameter(55, 75, default=60, space="buy", optimize=True)
short_tp = DecimalParameter(0.3, 1.0, default=0.6, decimals=1, space="sell", optimize=True)
short_sl = DecimalParameter(0.1, 0.5, default=0.2, decimals=2, space="sell", optimize=True)
short_max_hold = IntParameter(6, 36, default=18, space="sell", optimize=True)
# ── LONG params ───────────────────────────────────────────────────
long_rsi_threshold = IntParameter(25, 45, default=35, space="buy", optimize=True)
long_tp = DecimalParameter(0.3, 1.2, default=0.7, decimals=1, space="sell", optimize=True)
long_sl = DecimalParameter(0.1, 0.5, default=0.23, decimals=2, space="sell", optimize=True)
long_max_hold = IntParameter(6, 36, default=12, space="sell", optimize=True)
# ── Shared ────────────────────────────────────────────────────────
rsi_period = IntParameter(10, 20, default=14, space="buy", optimize=True)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Precompute RSI for multiple periods
for period in range(10, 21):
dataframe[f"rsi_{period}"] = ta.RSI(dataframe, timeperiod=period)
# Fresh entry filter: RSI wasn't already extreme in last 2 candles
# This prevents stacking entries in the same move
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
rsi_col = f"rsi_{self.rsi_period.value}"
# LONG: RSI drops below threshold (mean reversion from oversold)
# Fresh entry: RSI wasn't below threshold 1-2 candles ago
long_cond = (
(dataframe[rsi_col] < self.long_rsi_threshold.value)
& (
(dataframe[rsi_col].shift(1) >= self.long_rsi_threshold.value)
| (dataframe[rsi_col].shift(2) >= self.long_rsi_threshold.value)
)
& (dataframe["volume"] > 0)
)
# SHORT: RSI rises above threshold (mean reversion from overbought)
short_cond = (
(dataframe[rsi_col] > self.short_rsi_threshold.value)
& (
(dataframe[rsi_col].shift(1) <= self.short_rsi_threshold.value)
| (dataframe[rsi_col].shift(2) <= self.short_rsi_threshold.value)
)
& (dataframe["volume"] > 0)
)
dataframe.loc[long_cond, "enter_long"] = 1
dataframe.loc[short_cond, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Exit via custom_stoploss (TP/SL/timeout)
dataframe["exit_long"] = 0
dataframe["exit_short"] = 0
return dataframe
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
Implements TP, SL, and timeout exit logic.
Returns stoploss value relative to current price.
"""
is_short = trade.is_short
# Determine params based on trade direction
if is_short:
tp_pct = self.short_tp.value / 100
sl_pct = self.short_sl.value / 100
max_hold_candles = self.short_max_hold.value
else:
tp_pct = self.long_tp.value / 100
sl_pct = self.long_sl.value / 100
max_hold_candles = self.long_max_hold.value
# TP hit — close immediately
if current_profit >= tp_pct:
return stoploss_from_open(tp_pct * 0.95, current_profit, is_short=is_short)
# Timeout — close at market
trade_duration = (current_time - trade.open_date_utc).total_seconds()
max_duration = max_hold_candles * 15 * 60 # 15m per candle
if trade_duration >= max_duration:
return stoploss_from_open(-0.001, current_profit, is_short=is_short)
# SL — normal stoploss from open
return stoploss_from_open(-sl_pct, current_profit, is_short=is_short)