Timeframe
1m
Direction
Long & Short
Stoploss
-5.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
300
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisHyperScalp — High-Frequency Precision Scalper
====================================================
TARGET: 10+ trades/day | 70%+ WR | 2:1 R:R
MECHANISM (Multi-TF Reversal Scalp):
Higher TF (5m) defines TREND DIRECTION only.
Entry TF (1m) catches MICRO-REVERSALS within that trend.
The insight: In a confirmed uptrend, 1m dips almost always bounce.
We only trade WITH the trend, catching pullbacks at high-probability
reversal zones (Bollinger lower band + RSI extreme + volume spike).
LONG SETUP (5m uptrend):
1. 5m EMA fast > slow (trend confirmed)
2. 1m RSI drops to extreme oversold
3. 1m price touches/pierces lower Bollinger Band
4. 1m reversal candle (green after red, or hammer)
5. Volume spike confirms institutional interest
SHORT SETUP (5m downtrend):
1. 5m EMA fast < slow
2. 1m RSI spikes to extreme overbought
3. 1m price touches/pierces upper Bollinger Band
4. 1m reversal candle (red after green)
5. Volume spike
EXITS:
TP = 2 × SL (hard 2:1 R:R)
SL = ATR-based (adaptive to volatility)
Timeout = max hold minutes
Breakeven move after 1× SL profit
"""
import logging
from datetime import datetime
from functools import reduce
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.persistence import Trade
from freqtrade.strategy import (
IStrategy,
DecimalParameter,
IntParameter,
merge_informative_pair,
)
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisHyperScalp(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "1m"
stoploss = -0.05 # safety net
minimal_roi = {"0": 100} # disabled — exits via custom logic
trailing_stop = False
use_custom_stoploss = True
startup_candle_count = 300
process_only_new_candles = True
# ═══════════════════════════════════════════════════════════════════
# HYPEROPT PARAMETERS
# ═══════════════════════════════════════════════════════════════════
# ── Risk Management ──────────────────────────────────────────────
# SL as multiplier of ATR (adaptive to volatility)
sl_atr_mult = DecimalParameter(
0.5, 3.0, default=1.5, decimals=1, space="sell", optimize=True
)
# TP multiplier of SL (R:R ratio)
tp_rr = DecimalParameter(
1.5, 3.0, default=2.0, decimals=1, space="sell", optimize=True
)
# Max SL cap in % (prevent huge stops in volatile periods)
sl_max_pct = DecimalParameter(
0.3, 1.5, default=0.8, decimals=1, space="sell", optimize=True
)
# Breakeven trigger (0 = disabled, 1.0 = move SL to entry at 1×SL profit)
be_trigger = DecimalParameter(
0.0, 1.5, default=1.0, decimals=1, space="sell", optimize=True
)
# ── 5m Trend Filter ──────────────────────────────────────────────
trend_ema_fast = IntParameter(5, 20, default=8, space="buy", optimize=True)
trend_ema_slow = IntParameter(15, 50, default=21, space="buy", optimize=True)
# Trend strength: minimum EMA separation in %
trend_min_sep = DecimalParameter(
0.0, 0.5, default=0.05, decimals=2, space="buy", optimize=True
)
# ── 1m RSI Extremes ──────────────────────────────────────────────
rsi_period = IntParameter(4, 14, default=6, space="buy", optimize=True)
rsi_oversold = IntParameter(10, 35, default=20, space="buy", optimize=True)
rsi_overbought = IntParameter(65, 90, default=80, space="buy", optimize=True)
# ── Bollinger Bands ──────────────────────────────────────────────
bb_period = IntParameter(10, 30, default=20, space="buy", optimize=True)
bb_std = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space="buy", optimize=True)
# ── Volume Confirmation ──────────────────────────────────────────
vol_period = IntParameter(10, 40, default=20, space="buy", optimize=True)
vol_mult = DecimalParameter(0.5, 3.0, default=1.2, decimals=1, space="buy", optimize=True)
# ── Candle Pattern Strength ──────────────────────────────────────
# 0 = any reversal candle, 1 = require wick ratio confirmation
require_wick = IntParameter(0, 1, default=1, space="buy", optimize=True)
# ── Time Management ──────────────────────────────────────────────
max_hold_min = IntParameter(5, 60, default=30, space="sell", optimize=True)
cooldown_min = IntParameter(1, 15, default=3, space="buy", optimize=True)
# ── Session Filter (UTC hours) ───────────────────────────────────
# Only trade during high-volume sessions
session_start = IntParameter(0, 12, default=6, space="buy", optimize=True)
session_end = IntParameter(12, 24, default=22, space="buy", optimize=True)
_last_entry_time: dict = {}
# ═══════════════════════════════════════════════════════════════════
# INFORMATIVE PAIRS (5m for trend)
# ═══════════════════════════════════════════════════════════════════
def informative_pairs(self):
pairs = self.dp.current_whitelist() if self.dp else []
return [(p, "5m") for p in pairs]
# ═══════════════════════════════════════════════════════════════════
# INDICATORS
# ═══════════════════════════════════════════════════════════════════
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ── 1m Indicators ────────────────────────────────────────────
# RSI (multiple periods for hyperopt)
for p in range(4, 15):
dataframe[f"rsi_{p}"] = ta.RSI(dataframe, timeperiod=p)
# Bollinger Bands (multiple configs for hyperopt)
for period in [10, 14, 20, 25, 30]:
for std in [1.5, 2.0, 2.5, 3.0]:
bb = ta.BBANDS(
dataframe, timeperiod=period, nbdevup=std, nbdevdn=std
)
k = f"bb_{period}_{str(std).replace('.','')}"
dataframe[f"{k}_upper"] = bb["upperband"]
dataframe[f"{k}_lower"] = bb["lowerband"]
dataframe[f"{k}_mid"] = bb["middleband"]
dataframe[f"{k}_width"] = (
(bb["upperband"] - bb["lowerband"]) / bb["middleband"]
)
# ATR for adaptive stops
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] * 100
# Volume SMA (multiple periods)
for p in [10, 20, 30, 40]:
dataframe[f"vol_sma_{p}"] = ta.SMA(dataframe["volume"], timeperiod=p)
# Candle patterns
dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)
dataframe["prev_green"] = dataframe["is_green"].shift(1)
dataframe["prev_red"] = dataframe["is_red"].shift(1)
# Body and wick ratios
body = abs(dataframe["close"] - dataframe["open"])
candle_range = dataframe["high"] - dataframe["low"]
candle_range = candle_range.replace(0, np.nan)
dataframe["body_ratio"] = body / candle_range
# Lower wick ratio (for bullish reversal = hammer)
dataframe["lower_wick"] = (
dataframe[["close", "open"]].min(axis=1) - dataframe["low"]
) / candle_range
# Upper wick ratio (for bearish reversal = shooting star)
dataframe["upper_wick"] = (
dataframe["high"] - dataframe[["close", "open"]].max(axis=1)
) / candle_range
# Hour of day for session filter
dataframe["hour"] = pd.to_datetime(dataframe["date"]).dt.hour
# ── 5m Trend Indicators (merge from informative) ────────────
inf_5m = self.dp.get_pair_dataframe(
pair=metadata["pair"], timeframe="5m"
)
if not inf_5m.empty:
# Compute all possible EMA periods on 5m
for p in range(5, 51):
inf_5m[f"ema5m_{p}"] = ta.EMA(inf_5m, timeperiod=p)
inf_5m["rsi5m"] = ta.RSI(inf_5m, timeperiod=14)
# Which columns to merge
cols_to_merge = ["date"] + [f"ema5m_{p}" for p in range(5, 51)] + ["rsi5m"]
m5 = merge_informative_pair(
dataframe,
inf_5m[cols_to_merge],
self.timeframe,
"5m",
ffill=True,
)
for p in range(5, 51):
dataframe[f"ema5m_{p}"] = m5[f"ema5m_{p}_5m"].values
dataframe["rsi5m"] = m5["rsi5m_5m"].values
else:
for p in range(5, 51):
dataframe[f"ema5m_{p}"] = ta.EMA(dataframe, timeperiod=p * 5)
dataframe["rsi5m"] = ta.RSI(dataframe, timeperiod=70)
return dataframe
# ═══════════════════════════════════════════════════════════════════
# ENTRY SIGNALS
# ═══════════════════════════════════════════════════════════════════
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Resolve hyperopt params
ef = int(self.trend_ema_fast.value)
es = int(self.trend_ema_slow.value)
rp = int(self.rsi_period.value)
bp = int(self.bb_period.value)
bs = float(self.bb_std.value)
vp = int(self.vol_period.value)
vm = float(self.vol_mult.value)
rw = int(self.require_wick.value)
sep = float(self.trend_min_sep.value) / 100.0
ss = int(self.session_start.value)
se = int(self.session_end.value)
# Map BB std to key
bs_key = str(bs).replace(".", "")
bb_key = f"bb_{bp}_{bs_key}"
# Ensure BB columns exist (pick closest available)
available_periods = [10, 14, 20, 25, 30]
available_stds = [1.5, 2.0, 2.5, 3.0]
bp_actual = min(available_periods, key=lambda x: abs(x - bp))
bs_actual = min(available_stds, key=lambda x: abs(x - bs))
bb_key = f"bb_{bp_actual}_{str(bs_actual).replace('.', '')}"
rsi = dataframe[f"rsi_{rp}"]
rsi_prev = dataframe[f"rsi_{rp}"].shift(1)
bb_lower = dataframe[f"{bb_key}_lower"]
bb_upper = dataframe[f"{bb_key}_upper"]
vol_sma_key = f"vol_sma_{min([10,20,30,40], key=lambda x: abs(x-vp))}"
ema_fast_5m = dataframe[f"ema5m_{ef}"]
ema_slow_5m = dataframe[f"ema5m_{es}"]
# ── Common Filters ───────────────────────────────────────────
has_data = rsi.notna() & dataframe["rsi5m"].notna() & (dataframe["volume"] > 0)
vol_ok = dataframe["volume"] > (dataframe[vol_sma_key] * vm)
# Session filter
if ss < se:
session_ok = (dataframe["hour"] >= ss) & (dataframe["hour"] < se)
else:
session_ok = (dataframe["hour"] >= ss) | (dataframe["hour"] < se)
# ── 5m TREND ─────────────────────────────────────────────────
trend_bull = (
(ema_fast_5m > ema_slow_5m)
& ((ema_fast_5m - ema_slow_5m) / ema_slow_5m > sep)
)
trend_bear = (
(ema_fast_5m < ema_slow_5m)
& ((ema_slow_5m - ema_fast_5m) / ema_slow_5m > sep)
)
# ── 1m REVERSAL SIGNALS ──────────────────────────────────────
# RSI crossback from extreme (was extreme → recovering)
rsi_bounce_up = (rsi_prev < self.rsi_oversold.value) & (rsi >= self.rsi_oversold.value)
rsi_bounce_down = (rsi_prev > self.rsi_overbought.value) & (rsi <= self.rsi_overbought.value)
# Price at/below BB lower (for long) or at/above BB upper (for short)
bb_long = dataframe["low"] <= bb_lower
bb_short = dataframe["high"] >= bb_upper
# Reversal candle
green_reversal = (dataframe["is_green"] == 1) & (dataframe["prev_red"] == 1)
red_reversal = (dataframe["is_red"] == 1) & (dataframe["prev_green"] == 1)
# Wick confirmation (optional)
if rw:
wick_long = dataframe["lower_wick"] > 0.3 # decent lower wick
wick_short = dataframe["upper_wick"] > 0.3
else:
wick_long = True
wick_short = True
# ── LONG ENTRY ───────────────────────────────────────────────
long_cond = (
has_data
& session_ok
& vol_ok
& trend_bull # 5m uptrend confirmed
& bb_long # price touched lower BB
& rsi_bounce_up # RSI recovering from oversold
& green_reversal # reversal candle
& wick_long # wick confirmation
)
# ── SHORT ENTRY ──────────────────────────────────────────────
short_cond = (
has_data
& session_ok
& vol_ok
& trend_bear # 5m downtrend confirmed
& bb_short # price touched upper BB
& rsi_bounce_down # RSI falling from overbought
& red_reversal # reversal candle
& wick_short # wick confirmation
)
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:
return dataframe
# ═══════════════════════════════════════════════════════════════════
# TRADE MANAGEMENT
# ═══════════════════════════════════════════════════════════════════
def confirm_trade_entry(
self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs,
) -> bool:
cd = int(self.cooldown_min.value)
if cd > 0:
last = self._last_entry_time.get(pair)
if last is not None:
diff = (current_time - last).total_seconds()
if diff < cd * 60:
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,
after_fill: bool, **kwargs,
) -> float:
# ATR-based adaptive stop
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty:
return -1
last = dataframe.iloc[-1]
atr_pct = last.get("atr_pct", 0.5)
sl = atr_pct * float(self.sl_atr_mult.value) / 100.0
max_sl = float(self.sl_max_pct.value) / 100.0
sl = min(sl, max_sl)
# Breakeven logic
be = float(self.be_trigger.value)
if be > 0 and current_profit >= sl * be:
return -0.001 # break-even (tiny cushion)
return -sl
def custom_exit(
self, pair, trade: Trade, current_time, current_rate,
current_profit, **kwargs,
):
# Get ATR for TP calculation
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty:
return None
last = dataframe.iloc[-1]
atr_pct = last.get("atr_pct", 0.5)
sl = min(
atr_pct * float(self.sl_atr_mult.value) / 100.0,
float(self.sl_max_pct.value) / 100.0,
)
tp = sl * float(self.tp_rr.value)
# TP hit
if current_profit >= tp:
return "hyperscalp_tp"
# Timeout
if trade.open_date_utc:
elapsed = (current_time - trade.open_date_utc).total_seconds() / 60
if elapsed >= int(self.max_hold_min.value):
return "hyperscalp_timeout"
return None
def leverage(
self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs,
) -> float:
return 1.0