Timeframe
5m
Direction
Long & Short
Stoploss
-5.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
200
Indicators
13
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisGoldV7 — Research Scoring + V9 Exit Logic (BTC 5m)
==========================================================
EVIDENCE-BASED DESIGN:
V4-V6 proved that simple indicators (RSI/BB/EMA) have ZERO edge on 5m.
OsirisResearch proved that 17-point scoring with microstructure+volume
flow achieves 100% WR at threshold=14 (but only 0.18 trades/day).
STRATEGY:
- ENTRY: Research's 17-point scoring (volume flow + microstructure +
innovation + smart money + multi-TF) at LOWER threshold (10-12)
→ ~2-5 trades/day instead of 0.18
- EXIT: V9's proven time-profit system:
1. Fixed TP at R×1.5
2. TIME PROFIT (the real edge): after 8 candles, take any profit >0.3%
3. Momentum exit on trend loss
4. Hard timeout at 48 candles
- PROTECTION: XRSI's loss-streak cooldown (5 losses → pause 32min)
- STOP: Fixed 3.5×ATR (no trailing — V7 diagnosis: trailing kills)
- BREAKEVEN: At 0.5×R → lock +0.1%
LONG + SHORT (both directions, scored independently)
"""
import logging
import numpy as np
from datetime import timedelta
from pandas import DataFrame
from typing import Optional
from freqtrade.strategy import (
IStrategy, DecimalParameter, IntParameter,
merge_informative_pair,
)
from freqtrade.persistence import Trade
import talib.abstract as ta
logger = logging.getLogger(__name__)
class OsirisGoldV7(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "5m"
stoploss = -0.05 # Emergency hard stop 5% (custom takes over)
minimal_roi = {"0": 100} # Disabled — custom_exit handles all
trailing_stop = False
use_custom_stoploss = True
startup_candle_count = 200
process_only_new_candles = True
# ── ENTRY: Score thresholds ──────────────────────────────────────────
buy_score_min = IntParameter(8, 15, default=11, space="buy", optimize=True)
sell_score_min = IntParameter(8, 15, default=11, space="buy", optimize=True)
# ── ENTRY: Volume Flow params ────────────────────────────────────────
buy_pressure = DecimalParameter(0.45, 0.65, default=0.50, decimals=2, space="buy", optimize=True)
buy_rvol = DecimalParameter(0.7, 2.0, default=0.9, decimals=1, space="buy", optimize=True)
buy_cvd_bars = IntParameter(2, 6, default=3, space="buy", optimize=True)
buy_mfi = IntParameter(15, 40, default=22, decimals=0, space="buy", optimize=True)
# ── ENTRY: Momentum params ───────────────────────────────────────────
buy_rsi_min = IntParameter(25, 40, default=29, space="buy", optimize=True)
buy_rsi_max = IntParameter(60, 75, default=69, space="buy", optimize=True)
buy_stochrsi = IntParameter(25, 55, default=44, space="buy", optimize=True)
buy_cci = IntParameter(-150, -40, default=-75, space="buy", optimize=True)
# ── ENTRY: Trend params ──────────────────────────────────────────────
buy_adx = IntParameter(12, 30, default=15, space="buy", optimize=True)
buy_ribbon = IntParameter(1, 4, default=1, space="buy", optimize=True)
# ── ENTRY: Innovation params ─────────────────────────────────────────
buy_efficiency = DecimalParameter(0.05, 0.25, default=0.11, decimals=2, space="buy", optimize=True)
buy_vol_ratio = DecimalParameter(1.0, 2.0, default=1.6, decimals=1, space="buy", optimize=True)
# ── ENTRY: Multi-TF ──────────────────────────────────────────────────
buy_rsi_15m = IntParameter(30, 50, default=36, space="buy", optimize=True)
# ── EXIT: V9-style time-profit ───────────────────────────────────────
atr_sl_mult = DecimalParameter(2.0, 5.0, default=3.5, decimals=1, space="sell", optimize=True)
breakeven_r = DecimalParameter(0.3, 0.8, default=0.5, decimals=1, space="sell", optimize=True)
tp_r_mult = DecimalParameter(1.0, 2.5, default=1.5, decimals=1, space="sell", optimize=True)
time_profit_candles = IntParameter(6, 16, default=8, space="sell", optimize=True)
time_profit_min = DecimalParameter(0.1, 0.5, default=0.3, decimals=1, space="sell", optimize=True)
timeout_candles = IntParameter(30, 60, default=48, space="sell", optimize=True)
# ── State ────────────────────────────────────────────────────────────
_last_entry: dict = {}
_consec_losses: dict = {}
_trade_R: dict = {} # Store R value per trade
def informative_pairs(self):
pairs = self.dp.current_whitelist() if self.dp else []
return [(p, "15m") for p in pairs] + [(p, "1h") for p in pairs]
# =====================================================================
# VOLUME FLOW (from Research)
# =====================================================================
def _calc_volume_flow(self, df: DataFrame) -> DataFrame:
hl_range = (df["high"] - df["low"]).replace(0, np.nan)
df["buy_vol"] = (df["volume"] * (df["close"] - df["low"]) / hl_range).fillna(df["volume"] * 0.5)
df["sell_vol"] = (df["volume"] * (df["high"] - df["close"]) / hl_range).fillna(df["volume"] * 0.5)
total = (df["buy_vol"] + df["sell_vol"]).replace(0, 1)
df["pressure_ratio"] = (df["buy_vol"] / total).fillna(0.5)
df["volume_delta"] = df["buy_vol"] - df["sell_vol"]
df["cvd"] = df["volume_delta"].cumsum()
df["cvd_rising"] = (df["cvd"] > df["cvd"].shift(1)).astype(int)
df["vol_sma20"] = df["volume"].rolling(20).mean()
df["rvol"] = (df["volume"] / df["vol_sma20"].replace(0, 1)).fillna(1)
df["mfi"] = ta.MFI(df, timeperiod=14)
# CMF
mf_mult = ((df["close"] - df["low"]) - (df["high"] - df["close"])) / hl_range
mf_mult = mf_mult.fillna(0)
df["cmf"] = (mf_mult * df["volume"]).rolling(20).sum() / df["volume"].rolling(20).sum().replace(0, 1)
return df
# =====================================================================
# MICROSTRUCTURE (from Research)
# =====================================================================
def _calc_microstructure(self, df: DataFrame) -> DataFrame:
hl_range = (df["high"] - df["low"]).replace(0, np.nan).fillna(0.0001)
body = (df["close"] - df["open"]).abs()
df["body_ratio"] = body / hl_range
df["upper_wick_ratio"] = (df["high"] - df[["open", "close"]].max(axis=1)) / hl_range
df["lower_wick_ratio"] = (df[["open", "close"]].min(axis=1) - df["low"]) / hl_range
df["is_absorption"] = ((df["body_ratio"] < 0.30) & (df["rvol"] > 1.5)).astype(int)
df["is_pin_bar_bull"] = ((df["lower_wick_ratio"] > 0.60) & (df["body_ratio"] < 0.25) & (df["upper_wick_ratio"] < 0.15)).astype(int)
df["is_pin_bar_bear"] = ((df["upper_wick_ratio"] > 0.60) & (df["body_ratio"] < 0.25) & (df["lower_wick_ratio"] < 0.15)).astype(int)
recent_high = df["high"].rolling(20).max().shift(1)
recent_low = df["low"].rolling(20).min().shift(1)
df["liq_sweep_low"] = ((df["low"] < recent_low) & (df["close"] > recent_low)).astype(int)
df["liq_sweep_high"] = ((df["high"] > recent_high) & (df["close"] < recent_high)).astype(int)
return df
# =====================================================================
# INNOVATION (from Research)
# =====================================================================
def _calc_innovation(self, df: DataFrame) -> DataFrame:
period = 20
direction = (df["close"] - df["close"].shift(period)).abs()
volatility = df["close"].diff().abs().rolling(period).sum()
df["efficiency_ratio"] = (direction / volatility.replace(0, 1)).fillna(0)
atr14 = ta.ATR(df, timeperiod=14)
atr50 = ta.ATR(df, timeperiod=50)
df["volatility_ratio"] = (atr14 / atr50.replace(0, 1)).fillna(1)
# Squeeze: BB inside Keltner
kelt_ema = ta.EMA(df, timeperiod=20)
kelt_atr = ta.ATR(df, timeperiod=10)
bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
squeeze_on = ((bb["lowerband"] > kelt_ema - kelt_atr * 1.5) & (bb["upperband"] < kelt_ema + kelt_atr * 1.5)).astype(int)
df["squeeze_release_bull"] = ((squeeze_on.shift(1) == 1) & (squeeze_on == 0) & (df["close"] > df["close"].shift(1))).astype(int)
df["squeeze_release_bear"] = ((squeeze_on.shift(1) == 1) & (squeeze_on == 0) & (df["close"] < df["close"].shift(1))).astype(int)
return df
# =====================================================================
# INDICATORS
# =====================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata["pair"]
# Standard
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd_hist"] = macd["macdhist"]
dataframe["ema8"] = ta.EMA(dataframe, timeperiod=8)
dataframe["ema13"] = ta.EMA(dataframe, timeperiod=13)
dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["ema_ribbon"] = (
(dataframe["ema8"] > dataframe["ema13"]).astype(int)
+ (dataframe["ema13"] > dataframe["ema21"]).astype(int)
+ (dataframe["ema21"] > dataframe["ema50"]).astype(int)
+ (dataframe["ema50"] > dataframe["ema200"]).astype(int)
)
dataframe["ema_ribbon_bear"] = (
(dataframe["ema8"] < dataframe["ema13"]).astype(int)
+ (dataframe["ema13"] < dataframe["ema21"]).astype(int)
+ (dataframe["ema21"] < dataframe["ema50"]).astype(int)
+ (dataframe["ema50"] < dataframe["ema200"]).astype(int)
)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
stoch = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
dataframe["stochrsi_k"] = stoch["fastk"]
dataframe["cci"] = ta.CCI(dataframe, timeperiod=14)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
# VWAP rolling 50
tp = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
vwap_vol = dataframe["volume"].rolling(50).sum().replace(0, 1)
dataframe["vwap"] = (tp * dataframe["volume"]).rolling(50).sum() / vwap_vol
# Proprietary
dataframe = self._calc_volume_flow(dataframe)
dataframe = self._calc_microstructure(dataframe)
dataframe = self._calc_innovation(dataframe)
# 15m
inf_15m = self.dp.get_pair_dataframe(pair=pair, timeframe="15m") if self.dp else DataFrame()
if not inf_15m.empty:
inf_15m["rsi_15m"] = ta.RSI(inf_15m, timeperiod=14)
inf_15m["ema50_15m"] = ta.EMA(inf_15m, timeperiod=50)
m15 = merge_informative_pair(
dataframe, inf_15m[["date", "rsi_15m", "ema50_15m"]],
self.timeframe, "15m", ffill=True,
)
dataframe["rsi_15m"] = m15["rsi_15m_15m"].values
dataframe["ema50_15m"] = m15["ema50_15m_15m"].values
else:
dataframe["rsi_15m"] = 50
dataframe["ema50_15m"] = dataframe["close"]
# 1h
inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h") if self.dp else DataFrame()
if not inf_1h.empty:
inf_1h["ema9_1h"] = ta.EMA(inf_1h, timeperiod=9)
inf_1h["ema21_1h"] = ta.EMA(inf_1h, timeperiod=21)
inf_1h["adx_1h"] = ta.ADX(inf_1h, timeperiod=14)
m1h = merge_informative_pair(
dataframe, inf_1h[["date", "ema9_1h", "ema21_1h", "adx_1h"]],
self.timeframe, "1h", ffill=True,
)
dataframe["ema9_1h"] = m1h["ema9_1h_1h"].values
dataframe["ema21_1h"] = m1h["ema21_1h_1h"].values
dataframe["adx_1h"] = m1h["adx_1h_1h"].values
else:
dataframe["ema9_1h"] = dataframe["close"]
dataframe["ema21_1h"] = dataframe["close"]
dataframe["adx_1h"] = 25
return dataframe
# =====================================================================
# SCORING SYSTEM — LONG (17 points max, mirrors Research)
# =====================================================================
def _score_long(self, df: DataFrame) -> np.ndarray:
s = np.zeros(len(df))
# Volume Flow (4 pts)
s += (df["pressure_ratio"] > self.buy_pressure.value).astype(int) # 1
s += (df["rvol"] > self.buy_rvol.value).astype(int) # 2
cvd_ct = df["cvd_rising"].rolling(int(self.buy_cvd_bars.value)).sum()
s += (cvd_ct >= self.buy_cvd_bars.value).astype(int) # 3
s += (df["mfi"] > self.buy_mfi.value).astype(int) # 4
# Momentum (4 pts)
s += ((df["rsi"] > self.buy_rsi_min.value) & (df["rsi"] < self.buy_rsi_max.value)).astype(int) # 5
s += ((df["macd_hist"] > 0) | (df["macd_hist"] > df["macd_hist"].shift(1))).astype(int) # 6
s += (df["stochrsi_k"] < self.buy_stochrsi.value).astype(int) # 7
s += ((df["cci"] > self.buy_cci.value) & (df["cci"].shift(1) <= self.buy_cci.value)).astype(int) # 8
# Trend (3 pts)
s += (df["adx"] > self.buy_adx.value).astype(int) # 9
s += (df["ema_ribbon"] >= self.buy_ribbon.value).astype(int) # 10
s += (df["close"] > df["vwap"]).astype(int) # 11
# Microstructure (3 pts)
s += (df["is_absorption"].rolling(3).sum() > 0).astype(int) # 12
s += ((df["is_pin_bar_bull"] == 1) | (df["liq_sweep_low"] == 1)).astype(int) # 13
s += (df["squeeze_release_bull"] == 1).astype(int) # 14
# Innovation (2 pts)
s += (df["efficiency_ratio"] > self.buy_efficiency.value).astype(int) # 15
s += (df["volatility_ratio"] < self.buy_vol_ratio.value).astype(int) # 16
# Multi-TF (1 pt)
s += (df["rsi_15m"] > self.buy_rsi_15m.value).astype(int) # 17
return s
# =====================================================================
# SCORING SYSTEM — SHORT (17 points max, inverted)
# =====================================================================
def _score_short(self, df: DataFrame) -> np.ndarray:
s = np.zeros(len(df))
# Volume Flow (4 pts) — INVERTED
s += (df["pressure_ratio"] < (1 - self.buy_pressure.value)).astype(int) # 1
s += (df["rvol"] > self.buy_rvol.value).astype(int) # 2
cvd_fall = (1 - df["cvd_rising"]).rolling(int(self.buy_cvd_bars.value)).sum()
s += (cvd_fall >= self.buy_cvd_bars.value).astype(int) # 3
s += (df["mfi"] < (100 - self.buy_mfi.value)).astype(int) # 4
# Momentum (4 pts) — INVERTED
rsi_low = 100 - self.buy_rsi_max.value
rsi_high = 100 - self.buy_rsi_min.value
s += ((df["rsi"] > rsi_low) & (df["rsi"] < rsi_high)).astype(int) # 5
s += ((df["macd_hist"] < 0) | (df["macd_hist"] < df["macd_hist"].shift(1))).astype(int) # 6
s += (df["stochrsi_k"] > (100 - self.buy_stochrsi.value)).astype(int) # 7
cci_inv = -self.buy_cci.value
s += ((df["cci"] < cci_inv) & (df["cci"].shift(1) >= cci_inv)).astype(int) # 8
# Trend (3 pts) — INVERTED
s += (df["adx"] > self.buy_adx.value).astype(int) # 9
s += (df["ema_ribbon_bear"] >= self.buy_ribbon.value).astype(int) # 10
s += (df["close"] < df["vwap"]).astype(int) # 11
# Microstructure (3 pts) — INVERTED
s += (df["is_absorption"].rolling(3).sum() > 0).astype(int) # 12
s += ((df["is_pin_bar_bear"] == 1) | (df["liq_sweep_high"] == 1)).astype(int) # 13
s += (df["squeeze_release_bear"] == 1).astype(int) # 14
# Innovation (2 pts) — same
s += (df["efficiency_ratio"] > self.buy_efficiency.value).astype(int) # 15
s += (df["volatility_ratio"] < self.buy_vol_ratio.value).astype(int) # 16
# Multi-TF (1 pt) — INVERTED
s += (df["rsi_15m"] < (100 - self.buy_rsi_15m.value)).astype(int) # 17
return s
# =====================================================================
# ENTRY
# =====================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
has_data = dataframe["rsi"].notna() & (dataframe["volume"] > 0)
sl = self._score_long(dataframe)
ss = self._score_short(dataframe)
thr_l = int(self.buy_score_min.value)
thr_s = int(self.sell_score_min.value)
long_sig = has_data & (sl >= thr_l)
short_sig = has_data & (ss >= thr_s)
dataframe.loc[long_sig, "enter_long"] = 1
dataframe.loc[long_sig, "enter_tag"] = "L" + sl[long_sig].astype(int).astype(str)
dataframe.loc[short_sig, "enter_short"] = 1
dataframe.loc[short_sig, "enter_tag"] = "S" + ss[short_sig].astype(int).astype(str)
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# =====================================================================
# TRADE MANAGEMENT — V9 style
# =====================================================================
def confirm_trade_entry(
self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs,
) -> bool:
# XRSI loss-streak cooldown
losses = self._consec_losses.get(pair, 0)
if losses >= 5:
self._consec_losses[pair] = 0 # Reset, try again after 1 skip
return False
# 2 candle cooldown
last = self._last_entry.get(pair)
if last is not None and (current_time - last).total_seconds() < 600:
return False
self._last_entry[pair] = current_time
return True
def custom_stoploss(
self, pair, trade, current_time, current_rate, current_profit,
after_fill, **kwargs,
) -> Optional[float]:
"""Fixed ATR stop + breakeven. NO trailing."""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty or len(dataframe) < 2:
return None
last = dataframe.iloc[-1]
atr = last.get("atr", 0)
if atr <= 0 or trade.open_rate <= 0:
return None
# R = risk in percent
R = (atr * float(self.atr_sl_mult.value)) / trade.open_rate
R = max(0.015, min(R, 0.05)) # Clamp 1.5% — 5%
# Store R for TP calculation
trade_id = f"{pair}_{trade.open_date_utc}"
self._trade_R[trade_id] = R
# Breakeven: once profit >= breakeven_r × R → lock +0.1%
be_trigger = R * float(self.breakeven_r.value)
if current_profit >= be_trigger:
return -0.001 # Lock at +0.1%
return -R
def custom_exit(
self, pair, trade, current_time, current_rate, current_profit, **kwargs,
):
"""V9-style time-profit exit system."""
trade_id = f"{pair}_{trade.open_date_utc}"
R = self._trade_R.get(trade_id, 0.02)
elapsed = 0
if trade.open_date_utc:
elapsed = (current_time - trade.open_date_utc).total_seconds() / 300
# 1. Fixed TP at R × mult
tp = R * float(self.tp_r_mult.value)
if current_profit >= tp:
self._update_streak(pair, True)
return "v7_tp"
# 2. TIME PROFIT (THE EDGE): after N candles, take modest profit
tp_candles = int(self.time_profit_candles.value)
tp_min = float(self.time_profit_min.value) / 100
if elapsed >= tp_candles and current_profit >= tp_min:
self._update_streak(pair, True)
return "v7_time_profit"
# 3. Momentum exit: after 4 candles, if losing + RSI contra
if elapsed >= 4 and current_profit < 0:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if not dataframe.empty:
last = dataframe.iloc[-1]
rsi = last.get("rsi", 50)
is_short = getattr(trade, "is_short", False)
# Long losing + RSI > 65 (overbought = missed move)
if not is_short and rsi > 65:
self._update_streak(pair, False)
return "v7_momentum_exit"
# Short losing + RSI < 35 (oversold = missed move)
if is_short and rsi < 35:
self._update_streak(pair, False)
return "v7_momentum_exit"
# 4. Hard timeout
timeout = int(self.timeout_candles.value)
if elapsed >= timeout:
is_win = current_profit > 0
self._update_streak(pair, is_win)
return "v7_timeout"
# 5. Late profit (after timeout/2, take any small profit)
if elapsed >= timeout / 2 and current_profit >= 0.002:
self._update_streak(pair, True)
return "v7_late_profit"
return None
def _update_streak(self, pair, is_win):
if is_win:
self._consec_losses[pair] = 0
else:
self._consec_losses[pair] = self._consec_losses.get(pair, 0) + 1
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0