Timeframe
5m
Direction
Long & Short
Stoploss
-1.5%
Trailing Stop
No
ROI
0m: 1.5%
Interface Version
3
Startup Candles
200
Indicators
10
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisGoldV4 — Fixed SL/TP Edge Test (BTC/USDT 5m)
=====================================================
HYPOTHESIS TEST: Do the GoldV3 entries have ANY directional edge?
TEST 1: Fixed 1:1 RR (SL=1.0%, TP=1.0%)
If WR > 52% → entries have edge → optimize from here
If WR = 50% → entries are noise → need different entries
TEST 2: Fixed 2:1 RR (SL=1.0%, TP=2.0%)
If WR > 34% → positive EV (barely)
TEST 3: Time exit at 6 candles (30min), emergency SL only
If avg profit > 0 → entries capture drift → time exit is the edge
Using GoldV3's scoring with configurable threshold.
"""
import logging
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
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 OsirisGoldV4(IStrategy):
INTERFACE_VERSION = 3
can_short = True
timeframe = "5m"
# Hard SL only — no trailing, no custom
stoploss = -0.015 # 1.5% hard stop
minimal_roi = {"0": 0.015} # 1.5% ROI = 1:1 RR with SL
trailing_stop = False
use_custom_stoploss = False
startup_candle_count = 200
process_only_new_candles = True
# ── Params ───────────────────────────────────────────────────────────────
score_threshold = IntParameter(8, 14, default=10, space="buy", optimize=True)
score_threshold_short = IntParameter(8, 14, default=10, space="buy", optimize=True)
cooldown_candles = IntParameter(1, 6, default=2, space="buy", optimize=True)
sl_pct = DecimalParameter(
0.5, 3.0, default=1.5, decimals=1, space="sell", optimize=True
)
tp_pct = DecimalParameter(
0.5, 3.0, default=1.5, decimals=1, space="sell", optimize=True
)
_last_entry_time: dict = {}
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]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata["pair"]
# 5m indicators
dataframe["rsi14"] = ta.RSI(dataframe, timeperiod=14)
dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=7)
dataframe["mfi"] = ta.MFI(dataframe, timeperiod=14)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd_hist"] = macd["macdhist"]
stoch = ta.STOCH(dataframe, fastk_period=14, slowk_period=3, slowd_period=3)
dataframe["stoch_k"] = stoch["slowk"]
dataframe["cci"] = ta.CCI(dataframe, timeperiod=20)
dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upper"] = bb["upperband"]
dataframe["bb_lower"] = bb["lowerband"]
dataframe["bb_mid"] = bb["middleband"]
dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
dataframe["rvol"] = dataframe["volume"] / dataframe["volume_sma"].replace(0, np.nan)
dataframe["obv"] = ta.OBV(dataframe)
dataframe["obv_sma"] = ta.SMA(dataframe["obv"], timeperiod=20)
dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)
# Candlestick patterns
body = abs(dataframe["close"] - dataframe["open"])
candle_range = (dataframe["high"] - dataframe["low"]).replace(0, np.nan)
dataframe["body_pct"] = body / candle_range
dataframe["lower_wick"] = (
np.minimum(dataframe["close"], dataframe["open"]) - dataframe["low"]
) / candle_range
dataframe["upper_wick"] = (
dataframe["high"] - np.maximum(dataframe["close"], dataframe["open"])
) / candle_range
dataframe["bullish_engulf"] = (
(dataframe["is_green"] == 1)
& (dataframe["is_red"].shift(1) == 1)
& (dataframe["close"] > dataframe["open"].shift(1))
& (dataframe["open"] < dataframe["close"].shift(1))
).astype(int)
dataframe["hammer"] = (
(dataframe["lower_wick"] > 0.6) & (dataframe["body_pct"] > 0.1) & (dataframe["upper_wick"] < 0.15)
).astype(int)
dataframe["bearish_engulf"] = (
(dataframe["is_red"] == 1)
& (dataframe["is_green"].shift(1) == 1)
& (dataframe["close"] < dataframe["open"].shift(1))
& (dataframe["open"] > dataframe["close"].shift(1))
).astype(int)
dataframe["shooting_star"] = (
(dataframe["upper_wick"] > 0.6) & (dataframe["body_pct"] > 0.1) & (dataframe["lower_wick"] < 0.15)
).astype(int)
# 15m
inf_15m = self.dp.get_pair_dataframe(pair=pair, timeframe="15m")
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)
macd_15 = ta.MACD(inf_15m, fastperiod=12, slowperiod=26, signalperiod=9)
inf_15m["macd_hist_15m"] = macd_15["macdhist"]
m = merge_informative_pair(
dataframe, inf_15m[["date", "rsi_15m", "ema50_15m", "macd_hist_15m"]],
self.timeframe, "15m", ffill=True,
)
dataframe["rsi_15m"] = m["rsi_15m_15m"].values
dataframe["ema50_15m"] = m["ema50_15m_15m"].values
dataframe["macd_15m"] = m["macd_hist_15m_15m"].values
else:
dataframe["rsi_15m"] = 50
dataframe["ema50_15m"] = dataframe["close"]
dataframe["macd_15m"] = 0
# 1h
inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h")
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)
inf_1h["rsi_1h"] = ta.RSI(inf_1h, timeperiod=14)
m1h = merge_informative_pair(
dataframe, inf_1h[["date", "ema9_1h", "ema21_1h", "adx_1h", "rsi_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
dataframe["rsi_1h"] = m1h["rsi_1h_1h"].values
else:
dataframe["ema9_1h"] = dataframe["close"]
dataframe["ema21_1h"] = dataframe["close"]
dataframe["adx_1h"] = 25
dataframe["rsi_1h"] = 50
# Derived
dataframe["trend_1h_up"] = (dataframe["ema9_1h"] > dataframe["ema21_1h"]).astype(int)
dataframe["trend_1h_dn"] = (dataframe["ema9_1h"] < dataframe["ema21_1h"]).astype(int)
dataframe["m15_pos"] = (dataframe["macd_15m"] > 0).astype(int)
dataframe["m15_neg"] = (dataframe["macd_15m"] < 0).astype(int)
dataframe["ema_ribbon_up"] = (
(dataframe["ema9"] > dataframe["ema21"]) & (dataframe["ema21"] > dataframe["ema50"])
).astype(int)
dataframe["ema_ribbon_dn"] = (
(dataframe["ema9"] < dataframe["ema21"]) & (dataframe["ema21"] < dataframe["ema50"])
).astype(int)
# ═══ VECTORIZED SCORING ═════════════════════════════════════════════
# Long score
sl = np.zeros(len(dataframe))
sl += (dataframe["trend_1h_up"] == 1) * 2
sl += (dataframe["ema_ribbon_up"] == 1) * 1
sl += (dataframe["adx_1h"] > 20) * 1
sl += (dataframe["rsi14"] < 35) * 2
sl += ((dataframe["rsi14"] >= 35) & (dataframe["rsi14"] < 45)) * 1
sl += (dataframe["mfi"] < 30) * 1
sl += (dataframe["stoch_k"] < 25) * 1
sl += (dataframe["cci"] < -100) * 1
sl += (dataframe["rvol"] > 1.5) * 2
sl += ((dataframe["rvol"] > 1.0) & (dataframe["rvol"] <= 1.5)) * 1
sl += (dataframe["obv"] > dataframe["obv_sma"]) * 1
sl += (dataframe["is_green"] == 1) * 1
sl += (
(dataframe["close"] <= dataframe["bb_mid"])
& (dataframe["low"] <= dataframe["bb_lower"] * 1.005)
) * 1
sl += (dataframe["bullish_engulf"] == 1) * 1
sl += (dataframe["hammer"] == 1) * 1
sl += (dataframe["m15_pos"] == 1) * 1
sl += (dataframe["rsi_15m"] > 40) * 1
sl += (dataframe["close"] > dataframe["ema50_15m"]) * 1
sl += ((dataframe["rsi_1h"] > 35) & (dataframe["rsi_1h"] < 65)) * 1
dataframe["score_long"] = sl
# Short score
ss = np.zeros(len(dataframe))
ss += (dataframe["trend_1h_dn"] == 1) * 2
ss += (dataframe["ema_ribbon_dn"] == 1) * 1
ss += (dataframe["adx_1h"] > 20) * 1
ss += (dataframe["rsi14"] > 65) * 2
ss += ((dataframe["rsi14"] <= 65) & (dataframe["rsi14"] > 55)) * 1
ss += (dataframe["mfi"] > 70) * 1
ss += (dataframe["stoch_k"] > 75) * 1
ss += (dataframe["cci"] > 100) * 1
ss += (dataframe["rvol"] > 1.5) * 2
ss += ((dataframe["rvol"] > 1.0) & (dataframe["rvol"] <= 1.5)) * 1
ss += (dataframe["obv"] < dataframe["obv_sma"]) * 1
ss += (dataframe["is_red"] == 1) * 1
ss += (
(dataframe["close"] >= dataframe["bb_mid"])
& (dataframe["high"] >= dataframe["bb_upper"] * 0.995)
) * 1
ss += (dataframe["bearish_engulf"] == 1) * 1
ss += (dataframe["shooting_star"] == 1) * 1
ss += (dataframe["m15_neg"] == 1) * 1
ss += (dataframe["rsi_15m"] < 60) * 1
ss += (dataframe["close"] < dataframe["ema50_15m"]) * 1
ss += ((dataframe["rsi_1h"] > 35) & (dataframe["rsi_1h"] < 65)) * 1
dataframe["score_short"] = ss
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
thr_l = int(self.score_threshold.value)
thr_s = int(self.score_threshold_short.value)
has_data = dataframe["rsi14"].notna() & (dataframe["volume"] > 0)
long_sig = has_data & (dataframe["score_long"] >= thr_l)
short_sig = has_data & (dataframe["score_short"] >= thr_s)
dataframe.loc[long_sig, "enter_long"] = 1
dataframe.loc[long_sig, "enter_tag"] = "L" + dataframe.loc[long_sig, "score_long"].astype(int).astype(str)
dataframe.loc[short_sig, "enter_short"] = 1
dataframe.loc[short_sig, "enter_tag"] = "S" + dataframe.loc[short_sig, "score_short"].astype(int).astype(str)
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
def confirm_trade_entry(
self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs,
) -> bool:
last = self._last_entry_time.get(pair)
cd = int(self.cooldown_candles.value) * 300
if last is not None and (current_time - last).total_seconds() < cd:
return False
self._last_entry_time[pair] = current_time
return True
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0