Timeframe
1m
Direction
Long & Short
Stoploss
-3.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
200
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisScalper v6 — 1m Mean-Reversion Scalper
==============================================
Pure mean-reversion on 1m with BB bands.
LOGIC:
Entry: Price crosses below BB lower → long (mean reversion)
Price crosses above BB upper → short (mean reversion)
Filter: RSI confirms extreme (oversold/overbought)
5m EMA alignment as trend sanity check
Exit: Small fixed TP (0.2-0.5%) — quick scalps
Tight SL (0.2-0.5%) — cut losses fast
Breakeven when in profit
Timeout: 15-30 candles max
WHY 1m MEAN REVERSION:
- BB bounces on 1m resolve within 5-15 candles (5-15 min)
- High frequency: 10-30 signals per day on BTC
- Mean reversion has naturally higher WR than momentum
- Small TP captures the reversion reliably
"""
import logging
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 OsirisScalper(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "1m"
stoploss = -0.03
minimal_roi = {"0": 100}
trailing_stop = False
use_custom_stoploss = True
startup_candle_count = 200
process_only_new_candles = True
# ── Risk params (sell) ───────────────────────────────────────────────
sl_pct = DecimalParameter(
0.15, 0.60, default=0.35, decimals=2, space="sell", optimize=True
)
tp_pct = DecimalParameter(
0.15, 0.80, default=0.30, decimals=2, space="sell", optimize=True
)
be_trigger = DecimalParameter(
0.10, 0.40, default=0.15, decimals=2, space="sell", optimize=True
)
max_hold = IntParameter(5, 45, default=20, space="sell", optimize=True)
# ── Entry thresholds (buy) ───────────────────────────────────────────
rsi_os = IntParameter(15, 40, default=30, space="buy", optimize=True)
rsi_ob = IntParameter(60, 85, default=70, space="buy", optimize=True)
bb_std = DecimalParameter(
1.5, 2.8, default=2.0, decimals=1, space="buy", optimize=True
)
_last_entry_time: dict = {}
def informative_pairs(self):
pairs = self.dp.current_whitelist()
return [(pair, "5m") for pair in pairs]
# ── Indicators ───────────────────────────────────────────────────────
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata["pair"]
# 1m indicators
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
# Precompute BB for multiple std dev values
for std in [1.5, 2.0, 2.5]:
s = int(std * 10)
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=std, nbdevdn=std)
dataframe[f"bb_upper_{s}"] = bb["upperband"]
dataframe[f"bb_mid_{s}"] = bb["middleband"]
dataframe[f"bb_lower_{s}"] = bb["lowerband"]
# 5m trend filter (informative)
inf_5m = self.dp.get_pair_dataframe(pair=pair, timeframe="5m")
if not inf_5m.empty:
inf_5m["ema8_5m"] = ta.EMA(inf_5m, timeperiod=8)
inf_5m["ema21_5m"] = ta.EMA(inf_5m, timeperiod=21)
merged = merge_informative_pair(
dataframe,
inf_5m[["date", "ema8_5m", "ema21_5m"]],
self.timeframe,
"5m",
ffill=True,
)
dataframe["ema8_5m"] = merged["ema8_5m_5m"].values
dataframe["ema21_5m"] = merged["ema21_5m_5m"].values
else:
dataframe["ema8_5m"] = dataframe["close"]
dataframe["ema21_5m"] = dataframe["close"]
return dataframe
# ── Entry signals ────────────────────────────────────────────────────
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
os_val = int(self.rsi_os.value)
ob_val = int(self.rsi_ob.value)
# Map bb_std to nearest precomputed column
std_val = float(self.bb_std.value)
std_key = min([15, 20, 25], key=lambda x: abs(x - int(std_val * 10)))
bb_upper = dataframe[f"bb_upper_{std_key}"]
bb_lower = dataframe[f"bb_lower_{std_key}"]
bb_mid = dataframe[f"bb_mid_{std_key}"]
has_data = dataframe["rsi"].notna() & (dataframe["volume"] > 0)
# 5m trend (optional alignment - gives higher WR when active)
trend_bull_5m = dataframe["ema8_5m"] > dataframe["ema21_5m"]
trend_bear_5m = dataframe["ema8_5m"] < dataframe["ema21_5m"]
# ═══════════════════════════════════════════════════════════════════
# LONG: Price at/below BB lower + RSI oversold = mean reversion up
# ═══════════════════════════════════════════════════════════════════
long_bb = (
has_data
& (dataframe["close"] <= bb_lower * 1.001)
& (dataframe["rsi"] < os_val)
)
# LONG with 5m trend alignment (higher quality)
long_trend = (
has_data & trend_bull_5m
& (dataframe["close"] <= bb_mid)
& (dataframe["rsi"] < os_val + 10)
& (dataframe["close"] > dataframe["close"].shift(1)) # bouncing
)
# ═══════════════════════════════════════════════════════════════════
# SHORT: Price at/above BB upper + RSI overbought = mean reversion down
# ═══════════════════════════════════════════════════════════════════
short_bb = (
has_data
& (dataframe["close"] >= bb_upper * 0.999)
& (dataframe["rsi"] > ob_val)
)
# SHORT with 5m trend alignment (higher quality)
short_trend = (
has_data & trend_bear_5m
& (dataframe["close"] >= bb_mid)
& (dataframe["rsi"] > ob_val - 10)
& (dataframe["close"] < dataframe["close"].shift(1)) # falling
)
dataframe.loc[long_bb | long_trend, "enter_long"] = 1
dataframe.loc[short_bb | short_trend, "enter_short"] = 1
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:
# Min 2 min cooldown between trades per pair
last = self._last_entry_time.get(pair)
if last is not None:
if (current_time - last).total_seconds() < 120:
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
be = float(self.be_trigger.value) / 100
is_short = getattr(trade, "is_short", False)
# Breakeven when profit >= be_trigger
if current_profit >= be:
return stoploss_from_open(0.0001, current_profit, is_short=is_short)
return -sl
def custom_exit(
self, pair, trade, current_time, current_rate, current_profit, **kwargs,
):
tp = float(self.tp_pct.value) / 100
# Take profit
if current_profit >= tp:
return "tp_hit"
# Timeout
if trade.open_date_utc:
candles = (current_time - trade.open_date_utc).total_seconds() / 60
if candles >= 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