OsirisGoldDT — Gold scoring (18pt) + Order Flow (3pt) = 21pt system. Day trade exits with faster ROI and trailing.
Timeframe
5m
Direction
Long Only
Stoploss
-15.0%
Trailing Stop
Yes
ROI
0m: 15.0%, 40m: 6.0%, 100m: 2.5%, 200m: 0.5%
Interface Version
N/A
Startup Candles
200
Indicators
16
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OsirisGoldDT — Gold Day Trade Hybrid
=====================================
THESIS: OsirisGold's 50+ indicator scoring system HAS proven edge
(100% WR, +36% over 15 months). The problem is frequency (~1 trade/3 days).
SOLUTION:
- Same 18-point confluence scoring from OsirisGold
- PLUS 3 extra points from REAL order flow data (tick-level)
- Total: 21 possible points
- Lower threshold (8-13 instead of 14) → more trades
- Order flow acts as quality filter — replaces the seletivity lost
- Day trade exits: faster ROI, tighter trailing, reasonable SL
ORDER FLOW FEATURES (from Binance aggTrades, 449 days):
- delta: real buy-sell volume difference (not OHLC proxy)
- buy_ratio: actual percentage of buy trades
- delta_zscore: how extreme the delta is relative to recent history
- absorption: institutional absorption pattern detected in ticks
- big_imbalance: large buy/sell imbalance (>1 BTC trades)
- sweep_up/sweep_down: stop hunt patterns at tick level
- trade_burst: institutional burst activity (>2000 trades in 10s window)
- delta_flips: how many times delta changed sign within candle
TARGET: 2-5 trades/day, >50% WR, positive expectancy
"""
import logging
from pathlib import Path
import numpy as np
import pandas as pd
from pandas import DataFrame
from typing import Optional
from freqtrade.strategy import IStrategy, merge_informative_pair
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
from freqtrade.persistence import Trade
import talib.abstract as ta
logger = logging.getLogger(__name__)
# ── Order flow data path ────────────────────────────────────────────────
OF_PATH = Path(__file__).resolve().parent.parent / "data" / "orderflow" / "BTCUSDT-orderflow-5m.feather"
class OsirisGoldDT(IStrategy):
"""
OsirisGoldDT — Gold scoring (18pt) + Order Flow (3pt) = 21pt system.
Day trade exits with faster ROI and trailing.
"""
timeframe = "5m"
can_short = False # Long-only: shorts destroyed edge in OOS
# ROI: wide like Gold (proven pattern — let winners run)
minimal_roi = {"0": 0.15, "40": 0.06, "100": 0.025, "200": 0.005, "288": 0}
stoploss = -0.15
trailing_stop = True
trailing_stop_positive = 0.015
trailing_stop_positive_offset = 0.025
trailing_only_offset_is_reached = True
use_custom_stoploss = True
startup_candle_count = 200
process_only_new_candles = True
# ═══════════════════════════════════════════════════════════════════
# HYPEROPT PARAMETERS — BUY
# ═══════════════════════════════════════════════════════════════════
# Total score threshold (21 max: 18 Gold + 3 OF)
# 12/21 = 57% — proven sweet spot from first hyperopt (+59.62%)
buy_score_min = IntParameter(10, 18, default=12, space="buy", optimize=True)
short_score_min = IntParameter(10, 18, default=14, space="buy", optimize=False)
# --- Volume Flow (original 4pts) ---
buy_pressure_ratio = DecimalParameter(0.50, 0.70, default=0.55, decimals=2, space="buy", optimize=True)
buy_rvol_min = DecimalParameter(0.8, 3.0, default=1.2, decimals=1, space="buy", optimize=True)
buy_cvd_periods = IntParameter(3, 10, default=5, space="buy", optimize=True)
buy_mfi_min = IntParameter(20, 50, default=30, space="buy", optimize=True)
# --- Momentum (original 4pts) ---
buy_rsi_min = IntParameter(25, 45, default=30, space="buy", optimize=True)
buy_rsi_max = IntParameter(55, 75, default=65, space="buy", optimize=True)
buy_stochrsi_max = IntParameter(20, 50, default=30, space="buy", optimize=True)
buy_cci_min = IntParameter(-200, -30, default=-100, space="buy", optimize=True)
# --- Trend (original 3pts) ---
buy_adx_min = IntParameter(15, 40, default=20, space="buy", optimize=True)
buy_ema_ribbon_min = IntParameter(1, 4, default=2, space="buy", optimize=True)
# --- Innovation (original 2pts) ---
buy_efficiency_min = DecimalParameter(0.05, 0.40, default=0.15, decimals=2, space="buy", optimize=True)
buy_volatility_ratio_max = DecimalParameter(0.8, 2.0, default=1.3, decimals=1, space="buy", optimize=True)
# --- Multi-TF (original 2pts) ---
buy_mtf_rsi_15m_min = IntParameter(30, 55, default=40, space="buy", optimize=True)
buy_mtf_aligned = CategoricalParameter([True, False], default=True, space="buy", optimize=True)
# --- Order Flow REAL (NEW 3pts) ---
of_delta_zscore_min = DecimalParameter(0.5, 3.0, default=1.5, decimals=1, space="buy", optimize=True)
of_buy_ratio_min = DecimalParameter(0.50, 0.65, default=0.55, decimals=2, space="buy", optimize=True)
of_pattern_any = CategoricalParameter([True, False], default=True, space="buy", optimize=True)
# ═══════════════════════════════════════════════════════════════════
# HYPEROPT PARAMETERS — SELL
# ═══════════════════════════════════════════════════════════════════
sell_score_min = IntParameter(3, 12, default=6, space="sell", optimize=True)
sell_rsi_max = IntParameter(60, 90, default=75, space="sell", optimize=True)
sell_pressure_ratio_max = DecimalParameter(0.25, 0.50, default=0.40, decimals=2, space="sell", optimize=True)
sell_stochrsi_min = IntParameter(55, 95, default=80, space="sell", optimize=True)
sell_cci_max = IntParameter(50, 250, default=150, space="sell", optimize=True)
sell_adx_weakening = CategoricalParameter([True, False], default=True, space="sell", optimize=True)
sell_efficiency_min = DecimalParameter(0.05, 0.40, default=0.15, decimals=2, space="sell", optimize=True)
sell_exhaustion_rvol = DecimalParameter(1.5, 4.0, default=2.0, decimals=1, space="sell", optimize=True)
sell_cmf_threshold = DecimalParameter(-0.20, 0.0, default=-0.05, decimals=2, space="sell", optimize=True)
sell_macd_hist_negative = CategoricalParameter([True, False], default=True, space="sell", optimize=True)
# ── Internal state ──────────────────────────────────────────────────
_of_data: pd.DataFrame = None
_of_loaded = False
# ═══════════════════════════════════════════════════════════════════
# ORDER FLOW DATA LOADER
# ═══════════════════════════════════════════════════════════════════
def _load_orderflow(self):
"""Load order flow feather once, indexed by date."""
if self._of_loaded:
return
self._of_loaded = True
if OF_PATH.exists():
try:
of = pd.read_feather(OF_PATH)
of["date"] = pd.to_datetime(of["date"], utc=True)
of = of.set_index("date").sort_index()
# Keep only the columns we need (avoid conflicts)
keep = [
"delta", "cvd", "buy_ratio", "delta_zscore", "vol_spike",
"absorption", "sweep_up", "sweep_down", "big_imbalance",
"trade_burst", "delta_flips", "delta_accel", "price_impact",
"max_run", "aggression", "max_trade_size",
"buy_volume", "sell_volume",
]
available = [c for c in keep if c in of.columns]
self._of_data = of[available]
logger.info(f"OsirisGoldDT: Loaded {len(of)} rows of order flow data")
except Exception as e:
logger.warning(f"OsirisGoldDT: Failed to load OF data: {e}")
self._of_data = None
else:
logger.warning(f"OsirisGoldDT: OF file not found at {OF_PATH}")
self._of_data = None
def _merge_orderflow(self, df: DataFrame) -> DataFrame:
"""Merge order flow features into the 5m dataframe by date."""
self._load_orderflow()
if self._of_data is None:
# Fallback: add NaN columns
for c in ["of_delta", "of_buy_ratio", "of_delta_zscore", "of_absorption",
"of_big_imbalance", "of_sweep_up", "of_sweep_down",
"of_trade_burst", "of_delta_flips"]:
df[c] = 0.0
return df
# Match by date
of = self._of_data.copy()
of = of.rename(columns={
"delta": "of_delta",
"buy_ratio": "of_buy_ratio",
"delta_zscore": "of_delta_zscore",
"absorption": "of_absorption",
"big_imbalance": "of_big_imbalance",
"sweep_up": "of_sweep_up",
"sweep_down": "of_sweep_down",
"trade_burst": "of_trade_burst",
"delta_flips": "of_delta_flips",
"delta_accel": "of_delta_accel",
"aggression": "of_aggression",
})
merge_cols = [c for c in of.columns if c.startswith("of_")]
of_merge = of[merge_cols].copy()
of_merge = of_merge.reset_index()
# Ensure date column is UTC and datetime[ns]
df_date = df["date"].copy()
if df_date.dt.tz is None:
df_date = df_date.dt.tz_localize("UTC")
else:
df_date = df_date.dt.tz_convert("UTC")
of_merge["date"] = pd.to_datetime(of_merge["date"], utc=True)
# Merge on date
df["_merge_date"] = df_date
of_merge = of_merge.rename(columns={"date": "_merge_date"})
merged = pd.merge(df, of_merge, on="_merge_date", how="left")
merged = merged.drop(columns=["_merge_date"])
# Fill NaN with 0
for c in merge_cols:
if c in merged.columns:
merged[c] = merged[c].fillna(0)
return merged
# ═══════════════════════════════════════════════════════════════════
# PROPRIETARY INDICATOR MODULES (from OsirisGold)
# ═══════════════════════════════════════════════════════════════════
def _calc_volume_flow(self, df: DataFrame) -> DataFrame:
hl_range = (df["high"] - df["low"]).replace(0, np.nan)
df["buy_vol_proxy"] = (df["volume"] * (df["close"] - df["low"]) / hl_range).fillna(df["volume"] * 0.5)
df["sell_vol_proxy"] = (df["volume"] * (df["high"] - df["close"]) / hl_range).fillna(df["volume"] * 0.5)
df["volume_delta"] = df["buy_vol_proxy"] - df["sell_vol_proxy"]
df["cvd_proxy"] = df["volume_delta"].cumsum()
df["cvd_ema"] = ta.EMA(df["cvd_proxy"], timeperiod=20)
df["cvd_rising"] = (df["cvd_proxy"] > df["cvd_proxy"].shift(1)).astype(int)
total_vol = (df["buy_vol_proxy"] + df["sell_vol_proxy"]).replace(0, 1)
df["pressure_ratio"] = (df["buy_vol_proxy"] / total_vol).fillna(0.5)
df["volume_sma_20"] = df["volume"].rolling(20).mean()
df["rvol"] = (df["volume"] / df["volume_sma_20"].replace(0, 1)).fillna(1)
df["obv"] = (np.sign(df["close"].diff()) * df["volume"]).fillna(0).cumsum()
df["obv_ema"] = ta.EMA(df["obv"], timeperiod=20)
df["mfi"] = ta.MFI(df, timeperiod=14)
mf_mult = ((df["close"] - df["low"]) - (df["high"] - df["close"])) / hl_range
mf_mult = mf_mult.fillna(0)
mf_volume = mf_mult * df["volume"]
vol_sum = df["volume"].rolling(20).sum().replace(0, 1)
df["cmf"] = mf_volume.rolling(20).sum() / vol_sum
tp = (df["high"] + df["low"] + df["close"]) / 3
vwap_vol = df["volume"].rolling(50).sum().replace(0, 1)
df["vwap"] = (tp * df["volume"]).rolling(50).sum() / vwap_vol
df["vwap_distance"] = ((df["close"] - df["vwap"]) / df["vwap"].replace(0, 1) * 100)
return df
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
upper_wick = df["high"] - df[["open", "close"]].max(axis=1)
lower_wick = df[["open", "close"]].min(axis=1) - df["low"]
df["upper_wick_ratio"] = upper_wick / hl_range
df["lower_wick_ratio"] = lower_wick / hl_range
df["is_absorption"] = ((df["body_ratio"] < 0.30) & (df["rvol"] > 1.5)).astype(int)
df["is_exhaustion_bull"] = ((df["body_ratio"] > 0.70) & (df["rvol"] > 2.0) & (df["close"] > df["open"])).astype(int)
df["is_exhaustion_bear"] = ((df["body_ratio"] > 0.70) & (df["rvol"] > 2.0) & (df["close"] < df["open"])).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)
df["is_displacement_bull"] = ((df["body_ratio"] > 0.80) & (df["close"] > df["open"]) & (df["rvol"] > 1.2)).astype(int)
df["is_displacement_bear"] = ((df["body_ratio"] > 0.80) & (df["close"] < df["open"]) & (df["rvol"] > 1.2)).astype(int)
df["fvg_bull"] = (df["low"] > df["high"].shift(2)).astype(int)
df["fvg_bear"] = (df["high"] < df["low"].shift(2)).astype(int)
recent_high = df["high"].rolling(20).max().shift(1)
recent_low = df["low"].rolling(20).min().shift(1)
df["liquidity_sweep_high"] = ((df["high"] > recent_high) & (df["close"] < recent_high)).astype(int)
df["liquidity_sweep_low"] = ((df["low"] < recent_low) & (df["close"] > recent_low)).astype(int)
df["volume_climax_reversal_bear"] = ((df["rvol"].shift(1) > 3.0) & (df["close"].shift(1) > df["open"].shift(1)) & (df["close"] < df["open"])).astype(int)
df["volume_climax_reversal_bull"] = ((df["rvol"].shift(1) > 3.0) & (df["close"].shift(1) < df["open"].shift(1)) & (df["close"] > df["open"])).astype(int)
return df
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)
df["atr"] = ta.ATR(df, timeperiod=14)
atr_long = ta.ATR(df, timeperiod=50)
df["volatility_ratio"] = (df["atr"] / atr_long.replace(0, 1)).fillna(1)
net_move_10 = (df["close"] - df["close"].shift(10)).abs()
total_move_10 = df["close"].diff().abs().rolling(10).sum()
df["oscillation_index"] = 1 - (net_move_10 / total_move_10.replace(0, 1))
price_velocity = df["close"].diff(5)
df["trend_acceleration"] = price_velocity.diff(5)
df["trend_accel_norm"] = (df["trend_acceleration"] / df["atr"].replace(0, 1)).fillna(0)
price_chg = df["close"].pct_change(10).fillna(0)
vol_chg = df["volume"].pct_change(10).fillna(0)
df["vol_price_divergence"] = price_chg - vol_chg
roc_3 = ta.ROC(df, timeperiod=3).fillna(0)
roc_5 = ta.ROC(df, timeperiod=5).fillna(0)
roc_8 = ta.ROC(df, timeperiod=8).fillna(0)
roc_13 = ta.ROC(df, timeperiod=13).fillna(0)
roc_21 = ta.ROC(df, timeperiod=21).fillna(0)
df["composite_momentum"] = (roc_3 * 5 + roc_5 * 4 + roc_8 * 3 + roc_13 * 2 + roc_21 * 1) / 15
kelt_ema_20 = ta.EMA(df, timeperiod=20)
kelt_atr_10 = ta.ATR(df, timeperiod=10)
kelt_upper = kelt_ema_20 + kelt_atr_10 * 1.5
kelt_lower = kelt_ema_20 - kelt_atr_10 * 1.5
bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
df["bb_upper"] = bb["upperband"]
df["bb_middle"] = bb["middleband"]
df["bb_lower"] = bb["lowerband"]
df["bb_width"] = (bb["upperband"] - bb["lowerband"]) / bb["middleband"].replace(0, 1)
df["bb_percent"] = (df["close"] - bb["lowerband"]) / (bb["upperband"] - bb["lowerband"]).replace(0, 1)
df["squeeze_on"] = ((bb["lowerband"] > kelt_lower) & (bb["upperband"] < kelt_upper)).astype(int)
df["squeeze_release_bull"] = ((df["squeeze_on"].shift(1) == 1) & (df["squeeze_on"] == 0) & (df["close"] > df["close"].shift(1))).astype(int)
df["squeeze_release_bear"] = ((df["squeeze_on"].shift(1) == 1) & (df["squeeze_on"] == 0) & (df["close"] < df["close"].shift(1))).astype(int)
return df
# ═══════════════════════════════════════════════════════════════════
# INDICATOR COMPUTATION — GOLD + ORDER FLOW
# ═══════════════════════════════════════════════════════════════════
def informative_pairs(self):
pairs = self.dp.current_whitelist()
informative = []
for pair in pairs:
informative.append((pair, "15m"))
informative.append((pair, "1h"))
return informative
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ── Conventional indicators ─────────────────────────────────
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["rsi_fast"] = ta.RSI(dataframe, timeperiod=7)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd"] = macd["macd"]
dataframe["macd_signal"] = macd["macdsignal"]
dataframe["macd_hist"] = macd["macdhist"]
dataframe["ema_8"] = ta.EMA(dataframe, timeperiod=8)
dataframe["ema_13"] = ta.EMA(dataframe, timeperiod=13)
dataframe["ema_21"] = ta.EMA(dataframe, timeperiod=21)
dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["ema_ribbon"] = (
(dataframe["ema_8"] > dataframe["ema_13"]).astype(int)
+ (dataframe["ema_13"] > dataframe["ema_21"]).astype(int)
+ (dataframe["ema_21"] > dataframe["ema_50"]).astype(int)
+ (dataframe["ema_50"] > dataframe["ema_200"]).astype(int)
)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["plus_di"] = ta.PLUS_DI(dataframe, timeperiod=14)
dataframe["minus_di"] = ta.MINUS_DI(dataframe, timeperiod=14)
stoch = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
dataframe["stochrsi_k"] = stoch["fastk"]
dataframe["stochrsi_d"] = stoch["fastd"]
dataframe["cci"] = ta.CCI(dataframe, timeperiod=14)
dataframe["willr"] = ta.WILLR(dataframe, timeperiod=14)
dataframe["roc"] = ta.ROC(dataframe, timeperiod=10)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] * 100
ichi_tenkan = (dataframe["high"].rolling(9).max() + dataframe["low"].rolling(9).min()) / 2
ichi_kijun = (dataframe["high"].rolling(26).max() + dataframe["low"].rolling(26).min()) / 2
dataframe["ichi_tenkan"] = ichi_tenkan
dataframe["ichi_kijun"] = ichi_kijun
dataframe["ichi_above"] = (
(dataframe["close"] > ichi_tenkan).astype(int) + (dataframe["close"] > ichi_kijun).astype(int)
)
dataframe["sar"] = ta.SAR(dataframe, acceleration=0.02, maximum=0.2)
dataframe["sar_bullish"] = (dataframe["close"] > dataframe["sar"]).astype(int)
# ── Proprietary modules ─────────────────────────────────────
dataframe = self._calc_volume_flow(dataframe)
dataframe = self._calc_microstructure(dataframe)
dataframe = self._calc_innovation(dataframe)
# ── Order Flow REAL (from tick data) ────────────────────────
dataframe = self._merge_orderflow(dataframe)
# ── Multi-Timeframe ─────────────────────────────────────────
if self.dp:
# 15m
inf_15m = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="15m")
if not inf_15m.empty:
inf_15m["rsi"] = ta.RSI(inf_15m, timeperiod=14)
macd_15m = ta.MACD(inf_15m)
inf_15m["macd_hist"] = macd_15m["macdhist"]
inf_15m["ema_50"] = ta.EMA(inf_15m, timeperiod=50)
inf_15m["ema_200"] = ta.EMA(inf_15m, timeperiod=200)
inf_15m["adx"] = ta.ADX(inf_15m, timeperiod=14)
hl_15m = (inf_15m["high"] - inf_15m["low"]).replace(0, np.nan)
bv_15m = (inf_15m["volume"] * (inf_15m["close"] - inf_15m["low"]) / hl_15m).fillna(inf_15m["volume"] * 0.5)
sv_15m = (inf_15m["volume"] * (inf_15m["high"] - inf_15m["close"]) / hl_15m).fillna(inf_15m["volume"] * 0.5)
inf_15m["pressure"] = (bv_15m / (bv_15m + sv_15m).replace(0, 1)).fillna(0.5)
dataframe = merge_informative_pair(dataframe, inf_15m, self.timeframe, "15m", ffill=True)
# 1h
inf_1h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="1h")
if not inf_1h.empty:
inf_1h["rsi"] = ta.RSI(inf_1h, timeperiod=14)
inf_1h["ema_50"] = ta.EMA(inf_1h, timeperiod=50)
inf_1h["ema_200"] = ta.EMA(inf_1h, timeperiod=200)
inf_1h["adx"] = ta.ADX(inf_1h, timeperiod=14)
dataframe = merge_informative_pair(dataframe, inf_1h, self.timeframe, "1h", ffill=True)
return dataframe
# ═══════════════════════════════════════════════════════════════════
# SCORING — ENTRY (21 points max)
# ═══════════════════════════════════════════════════════════════════
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ── LONG SCORE ──────────────────────────────────────────────
score = np.zeros(len(dataframe))
# --- Volume Flow (4 pts) ---
score += (dataframe["pressure_ratio"] > self.buy_pressure_ratio.value).astype(int)
score += (dataframe["rvol"] > self.buy_rvol_min.value).astype(int)
cvd_rising_count = dataframe["cvd_rising"].rolling(self.buy_cvd_periods.value).sum()
score += (cvd_rising_count >= self.buy_cvd_periods.value).astype(int)
score += (dataframe["mfi"] > self.buy_mfi_min.value).astype(int)
# --- Momentum (4 pts) ---
score += ((dataframe["rsi"] > self.buy_rsi_min.value) & (dataframe["rsi"] < self.buy_rsi_max.value)).astype(int)
score += ((dataframe["macd_hist"] > 0) | (dataframe["macd_hist"] > dataframe["macd_hist"].shift(1))).astype(int)
score += (dataframe["stochrsi_k"] < self.buy_stochrsi_max.value).astype(int)
score += ((dataframe["cci"] > self.buy_cci_min.value) & (dataframe["cci"].shift(1) <= self.buy_cci_min.value)).astype(int)
# --- Trend (3 pts) ---
score += (dataframe["adx"] > self.buy_adx_min.value).astype(int)
score += (dataframe["ema_ribbon"] >= self.buy_ema_ribbon_min.value).astype(int)
score += (dataframe["close"] > dataframe["vwap"]).astype(int)
# --- Microstructure (3 pts) ---
score += (dataframe["is_absorption"].rolling(3).sum() > 0).astype(int)
score += ((dataframe["is_pin_bar_bull"] == 1) | (dataframe["liquidity_sweep_low"] == 1)).astype(int)
score += (dataframe["squeeze_release_bull"] == 1).astype(int)
# --- Innovation (2 pts) ---
score += (dataframe["efficiency_ratio"] > self.buy_efficiency_min.value).astype(int)
score += (dataframe["volatility_ratio"] < self.buy_volatility_ratio_max.value).astype(int)
# --- Multi-TF (2 pts) ---
rsi_15m_col = "rsi_15m"
if rsi_15m_col in dataframe.columns:
score += (dataframe[rsi_15m_col] > self.buy_mtf_rsi_15m_min.value).astype(int)
if self.buy_mtf_aligned.value:
ema50_15m = "ema_50_15m"
ema50_1h = "ema_50_1h"
if ema50_15m in dataframe.columns and ema50_1h in dataframe.columns:
score += ((dataframe["close"] > dataframe[ema50_15m]) & (dataframe["close"] > dataframe[ema50_1h])).astype(int)
# --- ORDER FLOW REAL (3 pts) --- THE NEW EDGE ---
# 19. Real delta z-score confirms buy pressure
score += (dataframe["of_delta_zscore"] > self.of_delta_zscore_min.value).astype(int)
# 20. Real buy ratio confirms buyers dominate
score += (dataframe["of_buy_ratio"] > self.of_buy_ratio_min.value).astype(int)
# 21. Any tick-level pattern (absorption, sweep, big imbalance, burst)
if self.of_pattern_any.value:
of_pattern = (
(dataframe["of_absorption"] > 0)
| (dataframe["of_big_imbalance"] > 0.5)
| (dataframe["of_sweep_down"] > 0) # sweep low = bullish reversal
| (dataframe["of_trade_burst"] > 2000)
)
score += of_pattern.astype(int)
# === LONG ENTRY ===
dataframe.loc[(score >= self.buy_score_min.value) & (dataframe["volume"] > 0), "enter_long"] = 1
# ── SHORT SCORE (inverted) ─────────────────────────────────
ss = np.zeros(len(dataframe))
# Volume Flow inverted
ss += (dataframe["pressure_ratio"] < (1 - self.buy_pressure_ratio.value)).astype(int)
ss += (dataframe["rvol"] > self.buy_rvol_min.value).astype(int)
cvd_falling = dataframe["cvd_rising"].rolling(self.buy_cvd_periods.value).sum()
ss += (cvd_falling == 0).astype(int)
ss += (dataframe["mfi"] < (100 - self.buy_mfi_min.value)).astype(int)
# Momentum inverted
ss += ((dataframe["rsi"] < (100 - self.buy_rsi_min.value)) & (dataframe["rsi"] > (100 - self.buy_rsi_max.value))).astype(int)
ss += ((dataframe["macd_hist"] < 0) | (dataframe["macd_hist"] < dataframe["macd_hist"].shift(1))).astype(int)
ss += (dataframe["stochrsi_k"] > (100 - self.buy_stochrsi_max.value)).astype(int)
ss += ((dataframe["cci"] < -self.buy_cci_min.value) & (dataframe["cci"].shift(1) >= -self.buy_cci_min.value)).astype(int)
# Trend inverted
ss += (dataframe["adx"] > self.buy_adx_min.value).astype(int)
ss += (dataframe["ema_ribbon"] <= (4 - self.buy_ema_ribbon_min.value)).astype(int)
ss += (dataframe["close"] < dataframe["vwap"]).astype(int)
# Microstructure inverted
ss += (dataframe["is_absorption"].rolling(3).sum() > 0).astype(int)
ss += ((dataframe["is_pin_bar_bear"] == 1) | (dataframe["liquidity_sweep_high"] == 1)).astype(int)
ss += (dataframe["squeeze_release_bear"] == 1).astype(int)
# Innovation (same)
ss += (dataframe["efficiency_ratio"] > self.buy_efficiency_min.value).astype(int)
ss += (dataframe["volatility_ratio"] < self.buy_volatility_ratio_max.value).astype(int)
# Multi-TF inverted
if rsi_15m_col in dataframe.columns:
ss += (dataframe[rsi_15m_col] < (100 - self.buy_mtf_rsi_15m_min.value)).astype(int)
if self.buy_mtf_aligned.value:
ema50_15m = "ema_50_15m"
ema50_1h = "ema_50_1h"
if ema50_15m in dataframe.columns and ema50_1h in dataframe.columns:
ss += ((dataframe["close"] < dataframe[ema50_15m]) & (dataframe["close"] < dataframe[ema50_1h])).astype(int)
# Order Flow inverted (sell pressure)
ss += (dataframe["of_delta_zscore"] < -self.of_delta_zscore_min.value).astype(int)
ss += (dataframe["of_buy_ratio"] < (1 - self.of_buy_ratio_min.value)).astype(int)
if self.of_pattern_any.value:
of_pattern_short = (
(dataframe["of_absorption"] > 0)
| (dataframe["of_big_imbalance"] < -0.5)
| (dataframe["of_sweep_up"] > 0) # sweep high = bearish reversal
| (dataframe["of_trade_burst"] > 2000)
)
ss += of_pattern_short.astype(int)
# === SHORT ENTRY ===
dataframe.loc[(ss >= self.short_score_min.value) & (dataframe["volume"] > 0), "enter_short"] = 1
return dataframe
# ═══════════════════════════════════════════════════════════════════
# SCORING — EXIT (12 points max, same as Gold)
# ═══════════════════════════════════════════════════════════════════
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
score = np.zeros(len(dataframe))
# Volume Flow (3 pts)
score += (dataframe["pressure_ratio"] < self.sell_pressure_ratio_max.value).astype(int)
score += (dataframe["cvd_proxy"] < dataframe["cvd_ema"]).astype(int)
score += (dataframe["cmf"] < self.sell_cmf_threshold.value).astype(int)
# Momentum (3 pts)
score += (dataframe["rsi"] > self.sell_rsi_max.value).astype(int)
score += (dataframe["stochrsi_k"] > self.sell_stochrsi_min.value).astype(int)
score += (dataframe["cci"] > self.sell_cci_max.value).astype(int)
# Trend Weakening (2 pts)
if self.sell_macd_hist_negative.value:
score += ((dataframe["macd_hist"] < 0) & (dataframe["macd_hist"] < dataframe["macd_hist"].shift(1))).astype(int)
if self.sell_adx_weakening.value:
score += (dataframe["adx"] < dataframe["adx"].shift(1)).astype(int)
# Microstructure (2 pts)
score += ((dataframe["is_exhaustion_bull"] == 1) & (dataframe["rvol"] > self.sell_exhaustion_rvol.value)).astype(int)
score += ((dataframe["is_displacement_bear"] == 1) | (dataframe["liquidity_sweep_high"] == 1)).astype(int)
# Innovation (2 pts)
score += (dataframe["efficiency_ratio"] < self.sell_efficiency_min.value).astype(int)
score += (dataframe["volume_climax_reversal_bear"] == 1).astype(int)
# Exit Long
dataframe.loc[(score >= self.sell_score_min.value) & (dataframe["volume"] > 0), "exit_long"] = 1
# Exit Short (inverted)
es = np.zeros(len(dataframe))
es += (dataframe["pressure_ratio"] > (1 - self.sell_pressure_ratio_max.value)).astype(int)
es += (dataframe["cvd_proxy"] > dataframe["cvd_ema"]).astype(int)
es += (dataframe["cmf"] > -self.sell_cmf_threshold.value).astype(int)
es += (dataframe["rsi"] < (100 - self.sell_rsi_max.value)).astype(int)
es += (dataframe["stochrsi_k"] < (100 - self.sell_stochrsi_min.value)).astype(int)
es += (dataframe["cci"] < -self.sell_cci_max.value).astype(int)
es += (dataframe["volume_climax_reversal_bull"] == 1).astype(int)
dataframe.loc[(es >= self.sell_score_min.value) & (dataframe["volume"] > 0), "exit_short"] = 1
return dataframe
# ═══════════════════════════════════════════════════════════════════
# LEVERAGE
# ═══════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════
# CUSTOM STOPLOSS — ATR-based, capped at -5%
# ═══════════════════════════════════════════════════════════════════
def custom_stoploss(self, pair, trade: Trade, current_time,
current_rate, current_profit, after_fill, **kwargs) -> float:
# Move to breakeven after +2% profit
if current_profit >= 0.02:
return 1 - ((1 + 0.002) / (1 + current_profit)) # Lock +0.2%
# ATR-based SL, capped at -15%
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is not None and len(dataframe) > 0:
atr_pct = dataframe.iloc[-1].get("atr_pct", 0)
if atr_pct > 0:
sl = -(atr_pct / 100) * 2.5
return max(sl, -0.15)
return -0.08
# ═══════════════════════════════════════════════════════════════════
# CUSTOM EXIT — timeout for day trade
# ═══════════════════════════════════════════════════════════════════
def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
if not trade.open_date_utc:
return None
hours = (current_time - trade.open_date_utc).total_seconds() / 3600
# Max hold: 24 hours (day trade constraint)
if hours >= 24:
return "timeout_24h"
# Lock in profit after 6h if profitable
if hours >= 6 and current_profit > 0.005:
return "profit_lock_6h"
return None
def leverage(self, pair, current_time, current_rate, proposed_leverage,
max_leverage, entry_tag, side, **kwargs) -> float:
return 1.0