OSIRIS PHANTOM - Ultra-Precision Scalper with 10 Proprietary Algorithms.
Timeframe
5m
Direction
Long Only
Stoploss
-2.5%
Trailing Stop
Yes
ROI
0m: 5.0%, 20m: 3.0%, 60m: 2.0%, 120m: 1.0%
Interface Version
3
Startup Candles
200
Indicators
12
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OSIRIS PHANTOM STRATEGY v1.0 - Ultra-Precision Scalping System
================================================================
10 Proprietary Algorithms | Strict 3:1 R:R | 10 Trades/Day Target
Whale Detection | Entropy Analysis | Fractal Breakouts | Smart Money
PROPRIETARY ALGORITHMS (100% original):
1. Whale Shadow Index (WSI) - Institutional activity detection via volume/price anomalies
2. Entropy Flow Oscillator (EFO) - Market order/disorder measurement
3. Quantum Momentum Fusion (QMF) - Multi-TF adaptive-weighted momentum
4. Adaptive Pressure Matrix (APM) - Candle anatomy buy/sell pressure decomposition
5. Fractal Breakout Detector (FBD) - Williams fractal breakout with volume confirmation
6. Volume DNA Helix (VDH) - Dual volume stream divergence/convergence analysis
7. Micro Regime Classifier (MRC) - Dynamic 5-state market regime detection
8. Liquidity Void Scanner (LVS) - Price gap/void identification and proximity
9. Smart Money Divergence (SMD) - Institutional vs retail flow divergence
10. Temporal Cascade Index (TCI) - Multi-timeframe momentum cascade scoring
RISK MANAGEMENT:
- Strict 3:1 Risk:Reward ratio enforced on EVERY trade (custom_exit)
- Progressive trailing: Break-even at 1R, lock profit at trail_rr, tight trail at 2.5R
- Time-based exit to prevent capital lockup
- All managed via buy_risk_pct parameter (hyperopt tunable)
100% proprietario. Desenvolvido exclusivamente para OSIRIS.
"""
import logging
import numpy as np
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__)
class OsirisPhantomStrategy(IStrategy):
"""
OSIRIS PHANTOM - Ultra-Precision Scalper with 10 Proprietary Algorithms.
Combines 10 never-before-seen indicators + standard TA in a score-based
confluence system. Enforces strict 3:1 Risk:Reward on every trade.
Designed for high-frequency scalping: 10+ trades per day across 5 pairs.
Hardware target: i9 + 128GB RAM + RTX 4090
"""
INTERFACE_VERSION = 3
can_short = False # Spot trading only
timeframe = "5m"
# ROI — optimized by Hyperopt
minimal_roi = {
"0": 0.05,
"20": 0.03,
"60": 0.02,
"120": 0.01,
}
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.01
trailing_stop_positive_offset = 0.02
trailing_only_offset_is_reached = True
startup_candle_count = 200
process_only_new_candles = True
# ===================================================================
# HYPEROPT PARAMETERS - BUY (14 parameters)
# ===================================================================
# Core: minimum confluence score to enter (max possible ~22)
buy_score_min = IntParameter(4, 18, default=8, space="buy", optimize=True)
# Whale Shadow Index threshold
buy_wsi_min = DecimalParameter(
0.5, 3.0, default=1.5, decimals=1, space="buy", optimize=True
)
# Entropy Flow — max normalized entropy for entry (lower = more trending)
buy_efo_max = DecimalParameter(
0.3, 2.0, default=1.0, decimals=1, space="buy", optimize=True
)
# Adaptive Pressure Matrix minimum ratio
buy_apm_min = DecimalParameter(
1.0, 2.5, default=1.2, decimals=1, space="buy", optimize=True
)
# Volume DNA Helix minimum divergence
buy_vdh_min = DecimalParameter(
-0.1, 0.5, default=0.1, decimals=2, space="buy", optimize=True
)
# Smart Money Divergence minimum
buy_smd_min = DecimalParameter(
-2.0, 5.0, default=0.5, decimals=1, space="buy", optimize=True
)
# RSI zone boundaries
buy_rsi_min = IntParameter(20, 50, default=30, space="buy", optimize=True)
buy_rsi_max = IntParameter(55, 80, default=70, space="buy", optimize=True)
# ADX trend strength minimum
buy_adx_min = IntParameter(10, 35, default=20, space="buy", optimize=True)
# Temporal Cascade minimum alignment score
buy_tci_min = IntParameter(1, 3, default=2, space="buy", optimize=True)
# EMA trend alignment required
buy_ema_aligned = CategoricalParameter(
[True, False], default=True, space="buy", optimize=True
)
# Allow trading in volatile regimes
buy_allow_volatile = CategoricalParameter(
[True, False], default=False, space="buy", optimize=True
)
# Allow trading in quiet regimes
buy_allow_quiet = CategoricalParameter(
[True, False], default=False, space="buy", optimize=True
)
# Max trade duration in hours before time exit
buy_max_hours = IntParameter(4, 24, default=12, space="buy", optimize=True)
# ===================================================================
# HYPEROPT PARAMETERS - SELL (2 parameters)
# ===================================================================
# Exit signal score threshold (max 12)
sell_score_min = IntParameter(3, 10, default=6, space="sell", optimize=True)
# RSI overbought level for exit signal
sell_rsi_max = IntParameter(60, 90, default=75, space="sell", optimize=True)
# ===================================================================
# INFORMATIVE PAIRS
# ===================================================================
def informative_pairs(self):
pairs = self.dp.current_whitelist()
informative = []
for pair in pairs:
informative.append((pair, "15m"))
informative.append((pair, "1h"))
return informative
# ===================================================================
# ALGORITHM 1: WHALE SHADOW INDEX (WSI)
# Detection of institutional/whale activity via volume-price anomalies
# ===================================================================
def _calc_whale_shadow(self, df: DataFrame) -> DataFrame:
vol_mean = df["volume"].rolling(50).mean()
vol_std = df["volume"].rolling(50).std().replace(0, 1)
vol_zscore = (df["volume"] - vol_mean) / vol_std
pct_change = df["close"].pct_change()
pct_mean = pct_change.rolling(50).mean()
pct_std = pct_change.rolling(50).std().replace(0, 0.0001)
price_zscore = (pct_change - pct_mean) / pct_std
# WSI: volume anomaly relative to price anomaly
df["wsi"] = vol_zscore / np.maximum(np.abs(price_zscore), 0.1)
df["wsi_smooth"] = ta.EMA(df["wsi"], timeperiod=5)
# Whale absorption on dip (buying the dip silently)
df["whale_buy"] = (
(df["wsi_smooth"] > 1.5) & (df["close"] < df["open"])
).astype(int)
# Whale distribution on rally
df["whale_sell"] = (
(df["wsi_smooth"] > 1.5) & (df["close"] > df["open"])
).astype(int)
return df
# ===================================================================
# ALGORITHM 2: ENTROPY FLOW OSCILLATOR (EFO)
# Market order/disorder measurement via statistical entropy proxies
# ===================================================================
def _calc_entropy_flow(self, df: DataFrame) -> DataFrame:
abs_returns = df["close"].pct_change().abs()
rolling_mean = abs_returns.rolling(20).mean().replace(0, 0.0001)
rolling_std = abs_returns.rolling(20).std()
df["efo"] = rolling_std / rolling_mean
efo_long_mean = df["efo"].rolling(100).mean().replace(0, 1)
df["efo_norm"] = df["efo"] / efo_long_mean
# Direction clarity: how unidirectional are recent moves
signed_returns = df["close"].pct_change()
pos_count = (signed_returns > 0).rolling(20).sum()
neg_count = (signed_returns < 0).rolling(20).sum()
df["direction_clarity"] = (pos_count - neg_count).abs() / 20
return df
# ===================================================================
# ALGORITHM 3: QUANTUM MOMENTUM FUSION (QMF)
# Multi-timeframe momentum with adaptive signal-strength weighting
# ===================================================================
def _calc_quantum_momentum(self, df: DataFrame) -> DataFrame:
mom_5m = df["rsi"].fillna(50).values - 50
if "rsi_15m" in df.columns:
mom_15m = df["rsi_15m"].fillna(50).values - 50
else:
mom_15m = np.zeros(len(df))
if "rsi_1h" in df.columns:
mom_1h = df["rsi_1h"].fillna(50).values - 50
else:
mom_1h = np.zeros(len(df))
# Weighted fusion: stronger signals contribute more
total_weight = np.abs(mom_5m) + np.abs(mom_15m) + np.abs(mom_1h)
total_weight = np.where(total_weight == 0, 1, total_weight)
qmf = (
mom_5m * np.abs(mom_5m)
+ mom_15m * np.abs(mom_15m)
+ mom_1h * np.abs(mom_1h)
) / total_weight
df["qmf"] = qmf
df["qmf_smooth"] = ta.EMA(df["qmf"], timeperiod=5)
return df
# ===================================================================
# ALGORITHM 4: ADAPTIVE PRESSURE MATRIX (APM)
# Candle anatomy decomposition into buy/sell pressure with volume
# ===================================================================
def _calc_pressure_matrix(self, df: DataFrame) -> DataFrame:
hl_range = (df["high"] - df["low"]).replace(0, 0.0001)
body = df["close"] - df["open"]
lower_wick = df[["open", "close"]].min(axis=1) - df["low"]
upper_wick = df["high"] - df[["open", "close"]].max(axis=1)
# Buy pressure: lower wick support + bullish body demand
buy_raw = (lower_wick + np.maximum(body, 0)) / hl_range
# Sell pressure: upper wick resistance + bearish body supply
sell_raw = (upper_wick + np.maximum(-body, 0)) / hl_range
buy_pressure_vol = buy_raw * df["volume"]
sell_pressure_vol = sell_raw * df["volume"]
buy_ema = ta.EMA(buy_pressure_vol, timeperiod=10)
sell_ema = ta.EMA(sell_pressure_vol, timeperiod=10)
df["apm"] = buy_ema / np.maximum(sell_ema, 0.0001)
df["apm_momentum"] = df["apm"] - df["apm"].shift(5)
return df
# ===================================================================
# ALGORITHM 5: FRACTAL BREAKOUT DETECTOR (FBD)
# Williams Fractal levels with volume-confirmed breakout signals
# ===================================================================
def _calc_fractal_breakout(self, df: DataFrame) -> DataFrame:
# Fractal highs/lows (2-bar delay to avoid look-ahead)
fractal_high = (
(df["high"].shift(2) > df["high"].shift(3))
& (df["high"].shift(2) > df["high"].shift(4))
& (df["high"].shift(2) > df["high"].shift(1))
& (df["high"].shift(2) > df["high"])
)
fractal_low = (
(df["low"].shift(2) < df["low"].shift(3))
& (df["low"].shift(2) < df["low"].shift(4))
& (df["low"].shift(2) < df["low"].shift(1))
& (df["low"].shift(2) < df["low"])
)
df["fractal_high"] = df["high"].shift(2).where(fractal_high).ffill()
df["fractal_low"] = df["low"].shift(2).where(fractal_low).ffill()
# Breakout: close crosses above fractal high
df["fbd_bull"] = (
(df["close"] > df["fractal_high"])
& (df["close"].shift(1) <= df["fractal_high"].shift(1))
).astype(int)
df["fbd_bear"] = (
(df["close"] < df["fractal_low"])
& (df["close"].shift(1) >= df["fractal_low"].shift(1))
).astype(int)
return df
# ===================================================================
# ALGORITHM 6: VOLUME DNA HELIX (VDH)
# Dual up/down volume streams — divergence = tension, convergence = breakout
# ===================================================================
def _calc_volume_dna(self, df: DataFrame) -> DataFrame:
up_volume = df["volume"].where(df["close"] > df["open"], 0)
down_volume = df["volume"].where(df["close"] <= df["open"], 0)
up_strand = ta.EMA(up_volume, timeperiod=14)
down_strand = ta.EMA(down_volume, timeperiod=14)
total = np.maximum(up_strand + down_strand, 0.0001)
df["vdh"] = (up_strand - down_strand) / total
# Acceleration of divergence
df["vdh_accel"] = df["vdh"] - df["vdh"].shift(3)
return df
# ===================================================================
# ALGORITHM 7: MICRO REGIME CLASSIFIER (MRC)
# 5-state market regime: Quiet, Trending Up/Down, Volatile, Ranging
# ===================================================================
def _calc_regime(self, df: DataFrame) -> DataFrame:
ema_fast = ta.EMA(df["close"], timeperiod=8)
ema_slow = ta.EMA(df["close"], timeperiod=21)
bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
bb_width = (bb["upperband"] - bb["lowerband"]) / bb["middleband"].replace(0, 1)
bb_width_pctile = bb_width.rolling(100).rank(pct=True)
atr_pct = df["atr"] / df["close"]
atr_pctile = atr_pct.rolling(100).rank(pct=True)
# 0=quiet, 1=trending_up, 2=trending_down, 3=volatile, 4=ranging
conditions = [
(df["adx"] > 25) & (ema_fast > ema_slow),
(df["adx"] > 25) & (ema_fast < ema_slow),
bb_width_pctile > 0.80,
(df["adx"] < 18) & (atr_pctile < 0.30),
]
choices = [1, 2, 3, 0]
df["regime"] = np.select(conditions, choices, default=4)
return df
# ===================================================================
# ALGORITHM 8: LIQUIDITY VOID SCANNER (LVS)
# Detects price gaps from fast moves — these act as S/R levels
# ===================================================================
def _calc_liquidity_voids(self, df: DataFrame) -> DataFrame:
body_abs = (df["close"] - df["open"]).abs()
avg_body = body_abs.rolling(50).mean()
large_bull = (df["close"] > df["open"]) & (body_abs > 2 * avg_body)
large_bear = (df["close"] < df["open"]) & (body_abs > 2 * avg_body)
df["void_support"] = df["open"].where(large_bull).ffill()
df["void_resistance"] = df["open"].where(large_bear).ffill()
df["dist_void_support"] = (
(df["close"] - df["void_support"]) / df["close"] * 100
)
# Near void support = potential bounce zone
df["near_void_support"] = (
(df["dist_void_support"] > 0) & (df["dist_void_support"] < 0.5)
).astype(int)
return df
# ===================================================================
# ALGORITHM 9: SMART MONEY DIVERGENCE (SMD)
# Compares institutional flow (OBV) vs retail indicators (RSI)
# ===================================================================
def _calc_smart_money_div(self, df: DataFrame) -> DataFrame:
rsi_slope = df["rsi"] - df["rsi"].shift(10)
df["_obv"] = (np.sign(df["close"].diff()) * df["volume"]).fillna(0).cumsum()
df["_obv_ema"] = ta.EMA(df["_obv"], timeperiod=14)
obv_slope = (
(df["_obv_ema"] - df["_obv_ema"].shift(10))
/ df["_obv_ema"].shift(10).replace(0, 1)
* 100
)
# Positive SMD = institutions accumulating (bullish)
# Negative SMD = institutions distributing (bearish)
df["smd"] = obv_slope - rsi_slope
df["smd_smooth"] = ta.EMA(df["smd"], timeperiod=5)
df["smart_accumulation"] = (
(df["smd_smooth"] > 2) & (df["rsi"] < 50)
).astype(int)
df["smart_distribution"] = (
(df["smd_smooth"] < -2) & (df["rsi"] > 50)
).astype(int)
df.drop(columns=["_obv", "_obv_ema"], inplace=True, errors="ignore")
return df
# ===================================================================
# ALGORITHM 10: TEMPORAL CASCADE INDEX (TCI)
# Multi-timeframe momentum cascade (-3 to +3 alignment score)
# ===================================================================
def _calc_temporal_cascade(self, df: DataFrame) -> DataFrame:
mom_5m = ta.MOM(df["close"], timeperiod=10)
sign_5m = np.sign(mom_5m)
if "close_15m" in df.columns:
# shift(3) on 5m-resampled 15m data = 1 period on 15m
mom_15m_raw = df["close_15m"] - df["close_15m"].shift(3)
sign_15m = np.sign(mom_15m_raw)
else:
sign_15m = np.zeros(len(df))
if "close_1h" in df.columns:
# shift(12) on 5m-resampled 1h data = 1 period on 1h
mom_1h_raw = df["close_1h"] - df["close_1h"].shift(12)
sign_1h = np.sign(mom_1h_raw)
else:
sign_1h = np.zeros(len(df))
# Cascade score: -3 (all bearish) to +3 (all bullish)
df["tci"] = sign_5m + sign_15m + sign_1h
return df
# ===================================================================
# STANDARD TECHNICAL INDICATORS
# ===================================================================
def _calc_standard(self, df: DataFrame) -> DataFrame:
df["rsi"] = ta.RSI(df, timeperiod=14)
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df["macd"] = macd["macd"]
df["macd_signal"] = macd["macdsignal"]
df["macd_hist"] = macd["macdhist"]
df["ema_8"] = ta.EMA(df, timeperiod=8)
df["ema_21"] = ta.EMA(df, timeperiod=21)
df["ema_50"] = ta.EMA(df, timeperiod=50)
df["ema_200"] = ta.EMA(df, timeperiod=200)
df["ema_aligned_bull"] = (
(df["ema_8"] > df["ema_21"]) & (df["ema_21"] > df["ema_50"])
).astype(int)
df["adx"] = ta.ADX(df, timeperiod=14)
df["plus_di"] = ta.PLUS_DI(df, timeperiod=14)
df["minus_di"] = ta.MINUS_DI(df, timeperiod=14)
df["atr"] = ta.ATR(df, timeperiod=14)
stoch = ta.STOCHRSI(df, timeperiod=14, fastk_period=3, fastd_period=3)
df["stochrsi_k"] = stoch["fastk"]
df["mfi"] = ta.MFI(df, timeperiod=14)
# Rolling VWAP
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
return df
# ===================================================================
# POPULATE INDICATORS (computed once, cached by Freqtrade)
# ===================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Standard TA first (RSI, ADX, ATR needed by proprietary algos)
dataframe = self._calc_standard(dataframe)
# Multi-timeframe informative data
if self.dp:
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)
inf_15m["ema_21"] = ta.EMA(inf_15m, timeperiod=21)
inf_15m["ema_50"] = ta.EMA(inf_15m, timeperiod=50)
inf_15m["adx"] = ta.ADX(inf_15m, timeperiod=14)
dataframe = merge_informative_pair(
dataframe, inf_15m, self.timeframe, "15m", ffill=True
)
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_21"] = ta.EMA(inf_1h, timeperiod=21)
inf_1h["ema_50"] = ta.EMA(inf_1h, timeperiod=50)
inf_1h["adx"] = ta.ADX(inf_1h, timeperiod=14)
dataframe = merge_informative_pair(
dataframe, inf_1h, self.timeframe, "1h", ffill=True
)
# 10 Proprietary Algorithms
dataframe = self._calc_whale_shadow(dataframe)
dataframe = self._calc_entropy_flow(dataframe)
dataframe = self._calc_pressure_matrix(dataframe)
dataframe = self._calc_fractal_breakout(dataframe)
dataframe = self._calc_volume_dna(dataframe)
dataframe = self._calc_regime(dataframe)
dataframe = self._calc_liquidity_voids(dataframe)
dataframe = self._calc_smart_money_div(dataframe)
dataframe = self._calc_temporal_cascade(dataframe)
dataframe = self._calc_quantum_momentum(dataframe) # Needs merged MTF data
return dataframe
# ===================================================================
# ENTRY SCORING SYSTEM (max ~22 pts)
# ===================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
score = np.zeros(len(dataframe))
# 1. WHALE SHADOW: absorption on bearish candle (2 pts)
score += np.where(
(dataframe["wsi_smooth"] > self.buy_wsi_min.value)
& (dataframe["close"] < dataframe["open"]),
2, 0,
)
# 2. ENTROPY: low entropy = trending market (1 pt)
score += np.where(dataframe["efo_norm"] < self.buy_efo_max.value, 1, 0)
# 3. QUANTUM MOMENTUM: positive multi-TF fusion (2 pts)
score += np.where(dataframe["qmf_smooth"] > 0, 2, 0)
# 4. PRESSURE MATRIX: buy pressure dominating (2 pts)
score += np.where(dataframe["apm"] > self.buy_apm_min.value, 2, 0)
# 5. FRACTAL BREAKOUT: bullish fractal break (3 pts — strong signal)
score += np.where(dataframe["fbd_bull"] == 1, 3, 0)
# 6. VOLUME DNA: up-strand dominating (1 pt)
score += np.where(dataframe["vdh"] > self.buy_vdh_min.value, 1, 0)
# 7. REGIME: favorable market state (2 pts)
regime_ok = dataframe["regime"] == 1 # trending up always ok
if self.buy_allow_volatile.value:
regime_ok = regime_ok | (dataframe["regime"] == 3)
if self.buy_allow_quiet.value:
regime_ok = regime_ok | (dataframe["regime"] == 0)
score += np.where(regime_ok, 2, 0)
# 8. LIQUIDITY VOID: near support void (1 pt)
score += np.where(dataframe["near_void_support"] == 1, 1, 0)
# 9. SMART MONEY: institutional accumulation (2 pts)
score += np.where(
dataframe["smd_smooth"] > self.buy_smd_min.value, 2, 0
)
# 10. TEMPORAL CASCADE: multi-TF aligned bullish (2 pts)
score += np.where(dataframe["tci"] >= self.buy_tci_min.value, 2, 0)
# BONUS: Standard TA confluence
# RSI in buy zone (1 pt)
score += np.where(
(dataframe["rsi"] > self.buy_rsi_min.value)
& (dataframe["rsi"] < self.buy_rsi_max.value),
1, 0,
)
# ADX showing trend (1 pt)
score += np.where(dataframe["adx"] > self.buy_adx_min.value, 1, 0)
# EMA alignment (conditional, 1 pt)
if self.buy_ema_aligned.value:
score += np.where(dataframe["ema_aligned_bull"] == 1, 1, 0)
# Price above VWAP (1 pt)
score += np.where(dataframe["close"] > dataframe["vwap"], 1, 0)
# === ENTRY SIGNAL ===
dataframe.loc[
(score >= self.buy_score_min.value) & (dataframe["volume"] > 0),
"enter_long",
] = 1
return dataframe
# ===================================================================
# EXIT SCORING SYSTEM (max 12 pts) — supplementary to 3:1 R:R
# ===================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
score = np.zeros(len(dataframe))
# 1. Whale distribution detected (2 pts)
score += np.where(dataframe["whale_sell"] == 1, 2, 0)
# 2. High entropy — market becoming chaotic (1 pt)
score += np.where(dataframe["efo_norm"] > 1.5, 1, 0)
# 3. Sell pressure dominating (2 pts)
score += np.where(dataframe["apm"] < 0.8, 2, 0)
# 4. RSI overbought (1 pt)
score += np.where(dataframe["rsi"] > self.sell_rsi_max.value, 1, 0)
# 5. Bearish fractal breakout (2 pts)
score += np.where(dataframe["fbd_bear"] == 1, 2, 0)
# 6. Volume DNA bearish (1 pt)
score += np.where(dataframe["vdh"] < -0.2, 1, 0)
# 7. Smart money distributing (2 pts)
score += np.where(dataframe["smart_distribution"] == 1, 2, 0)
# 8. Temporal cascade bearish (1 pt)
score += np.where(dataframe["tci"] <= -2, 1, 0)
# === EXIT SIGNAL ===
dataframe.loc[
(score >= self.sell_score_min.value) & (dataframe["volume"] > 0),
"exit_long",
] = 1
return dataframe
# ===================================================================
# CUSTOM EXIT — Time-Based Management
# ===================================================================
def custom_exit(
self,
pair: str,
trade: Trade,
current_time,
current_rate: float,
current_profit: float,
**kwargs,
) -> Optional[str]:
trade_duration_hours = (
(current_time - trade.open_date_utc).total_seconds() / 3600
)
max_hours = self.buy_max_hours.value
# After max_hours: exit if in profit
if trade_duration_hours > max_hours and current_profit > 0.001:
return "time_exit_profit"
# Hard limit: 2× max_hours — close regardless
if trade_duration_hours > max_hours * 2:
return "time_exit_max"
return None