"""Frozen LITUSDT 15m LuxAlgo Long-Only Champion Book #001 (30 rules / 6 slots). Ported bar-for-bar from the search that produced it: ``E:\\EC\\Binance-AggTrades\\Test-08-19-Sept`` — ``features.py`` (primitive maths), ``multibook.py`` (multi-slot walk), and ``optimized_strategies.json`` (champion pick). Thirty rules are distributed across SIX concurrent sleeves (slots 0..5). On each closed 15m bar, each flat slot evaluates its assigned rules in priority order. The winning rule owns that slot's barrier geometry (TP, SL, max_hold). All slots run independently at 1/6th equity each, so total exposure never exceeds 100%. """ from __future__ import annotations import numpy as np import pandas as pd from freqtrade.strategy import IStrategy # --------------------------------------------------------------------------- # Primitive maths — pure numpy / pandas implementations # --------------------------------------------------------------------------- def _rma(v: np.ndarray, period: int) -> np.ndarray: n = len(v) out = np.full(n, np.nan, dtype=np.float64) if n < period: return out cur = float(np.mean(v[:period])) out[period - 1] = cur alpha = 1.0 / period for i in range(period, n): cur = cur * (1.0 - alpha) + v[i] * alpha out[i] = cur return out def _ema(v: np.ndarray, period: int) -> np.ndarray: return pd.Series(v).ewm(span=period, adjust=False).mean().to_numpy(dtype=np.float64) def _true_range(h: np.ndarray, l: np.ndarray, c: np.ndarray) -> np.ndarray: n = len(c) tr = np.empty(n, dtype=np.float64) tr[0] = h[0] - l[0] tr[1:] = np.maximum.reduce([h[1:] - l[1:], np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1])]) return tr def _atr(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int = 14) -> np.ndarray: return _rma(_true_range(h, l, c), period) def _supertrend(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int, mult: float): """Returns (line, direction, atr) with direction: +1 up / -1 down.""" n = len(c) atr = _rma(_true_range(h, l, c), period) hl2 = (h + l) * 0.5 lb, ub = hl2 - mult * atr, hl2 + mult * atr lower = np.full(n, np.nan, dtype=np.float64) upper = np.full(n, np.nan, dtype=np.float64) line = np.full(n, np.nan, dtype=np.float64) direction = np.zeros(n, dtype=np.int8) if n < period: return line, direction, atr s = period - 1 lower[s], upper[s] = lb[s], ub[s] direction[s] = 1 if c[s] > ub[s] else (-1 if c[s] < lb[s] else 1) line[s] = lower[s] if direction[s] == 1 else upper[s] for i in range(period, n): lower[i] = max(lb[i], lower[i - 1]) if c[i - 1] > lower[i - 1] else lb[i] upper[i] = min(ub[i], upper[i - 1]) if c[i - 1] < upper[i - 1] else ub[i] p = direction[i - 1] if p == -1 and c[i] > upper[i - 1]: direction[i] = 1 elif p == 1 and c[i] < lower[i - 1]: direction[i] = -1 else: direction[i] = p line[i] = lower[i] if direction[i] == 1 else upper[i] return line, direction, atr def _evasive_supertrend(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int, mult: float): """LuxAlgo Evasive SuperTrend with its ATR noise-zone band.""" n = len(c) atr = _rma(_true_range(h, l, c), period) hl2 = (h + l) * 0.5 upper, lower = hl2 + mult * atr, hl2 - mult * atr band = np.full(n, np.nan, dtype=np.float64) direction = np.zeros(n, dtype=np.int8) if n < period: return band, direction, atr start = period - 1 direction[start] = 1 if c[start] >= hl2[start] else -1 band[start] = lower[start] if direction[start] == 1 else upper[start] for i in range(period, n): noisy = abs(c[i] - band[i - 1]) < atr[i] if direction[i - 1] == 1: candidate = band[i - 1] - atr[i] * 0.5 if noisy else max(lower[i], band[i - 1]) direction[i] = -1 if c[i] < candidate else 1 band[i] = upper[i] if direction[i] == -1 else candidate else: candidate = band[i - 1] + atr[i] * 0.5 if noisy else min(upper[i], band[i - 1]) direction[i] = 1 if c[i] > candidate else -1 band[i] = lower[i] if direction[i] == 1 else candidate return band, direction, atr def _supertrend_oscillator(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int, mult: float): line, _, atr = _supertrend(h, l, c, period, mult) osc = np.clip((c - line) / (2.0 * mult * np.maximum(atr, 1e-12)), -1.0, 1.0) hist = osc - _ema(osc, 9) return osc, hist def _rsi(c: np.ndarray, period: int) -> np.ndarray: diff = np.diff(c, prepend=c[0]) gain = np.where(diff > 0, diff, 0.0) loss = np.where(diff < 0, -diff, 0.0) rs = _rma(gain, period) / np.maximum(_rma(loss, period), 1e-12) return 100.0 - (100.0 / (1.0 + rs)) def _stoch_rsi(c: np.ndarray, rsi_len: int, stoch_len: int, k_smooth: int, d_smooth: int): r = _rsi(c, rsi_len) s = pd.Series(r) ll = s.rolling(stoch_len).min().to_numpy() hh = s.rolling(stoch_len).max().to_numpy() stoch = 100.0 * (r - ll) / np.maximum(hh - ll, 1e-12) k = pd.Series(np.nan_to_num(stoch, nan=50.0)).rolling(k_smooth).mean().to_numpy(dtype=np.float64) d = pd.Series(k).rolling(d_smooth).mean().to_numpy(dtype=np.float64) return k, d def _wavetrend(h: np.ndarray, l: np.ndarray, c: np.ndarray, ch_len: int, avg_len: int): hlc3 = (h + l + c) / 3.0 esa = _ema(hlc3, ch_len) d = _ema(np.abs(hlc3 - esa), ch_len) ci = (hlc3 - esa) / np.maximum(0.015 * d, 1e-12) wt1 = _ema(ci, avg_len) wt2 = pd.Series(wt1).rolling(4).mean().to_numpy(dtype=np.float64) return wt1, wt2 def _adx(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int): n = len(c) tr = _true_range(h, l, c) up_move = np.zeros(n) dn_move = np.zeros(n) up_move[1:] = h[1:] - h[:-1] dn_move[1:] = l[:-1] - l[1:] plus_dm = np.where((up_move > dn_move) & (up_move > 0), up_move, 0.0) minus_dm = np.where((dn_move > up_move) & (dn_move > 0), dn_move, 0.0) tr_smooth = _rma(tr, period) plus_di = 100.0 * _rma(plus_dm, period) / np.maximum(tr_smooth, 1e-12) minus_di = 100.0 * _rma(minus_dm, period) / np.maximum(tr_smooth, 1e-12) di_sum = plus_di + minus_di dx = 100.0 * np.abs(plus_di - minus_di) / np.maximum(di_sum, 1e-12) adx = np.full(n, np.nan, dtype=np.float64) offset = period - 1 if n > offset: adx[offset:] = _rma(dx[offset:], period) return adx, plus_di, minus_di def _tsi(c: np.ndarray, r: int, s: int, g: int): diff = np.diff(c, prepend=c[0]) abs_diff = np.abs(diff) smooth_diff = _ema(_ema(diff, r), s) smooth_abs = _ema(_ema(abs_diff, r), s) tsi = 100.0 * smooth_diff / np.maximum(smooth_abs, 1e-12) signal = _ema(tsi, g) return tsi, signal def _cmo(c: np.ndarray, period: int) -> np.ndarray: diff = np.diff(c, prepend=c[0]) cz = pd.Series(np.where(diff > 0, diff, 0.0)).rolling(period).sum().to_numpy() cf = pd.Series(np.where(diff < 0, -diff, 0.0)).rolling(period).sum().to_numpy() denom = cz + cf return np.where(denom > 1e-12, 100.0 * (cz - cf) / denom, 0.0) def _cci(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int) -> np.ndarray: tp = (h + l + c) / 3.0 series = pd.Series(tp) sma = series.rolling(period).mean().to_numpy() mad = series.rolling(period).apply(lambda x: np.mean(np.abs(x - np.mean(x))), raw=True).to_numpy() return (tp - sma) / np.maximum(0.015 * mad, 1e-12) def _williams_r(h: np.ndarray, l: np.ndarray, c: np.ndarray, period: int) -> np.ndarray: hh = pd.Series(h).rolling(period).max().to_numpy() ll = pd.Series(l).rolling(period).min().to_numpy() rng = hh - ll return np.where(rng > 1e-12, -100.0 * (hh - c) / rng, -50.0) def _bollinger_bands(c: np.ndarray, period: int, mult: float): s = pd.Series(c) mid = s.rolling(period).mean().to_numpy() std = s.rolling(period).std().to_numpy() return mid + mult * std, mid, mid - mult * std def _donchian_channels(h: np.ndarray, l: np.ndarray, period: int): du = pd.Series(h).shift(1).rolling(period).max().to_numpy() dl = pd.Series(l).shift(1).rolling(period).min().to_numpy() return du, dl def _xup(a: np.ndarray, b: np.ndarray) -> np.ndarray: out = np.zeros(len(a), bool) out[1:] = (a[1:] > b[1:]) & (a[:-1] <= b[:-1]) return out def _rising(a: np.ndarray, k: int = 1) -> np.ndarray: out = np.zeros(len(a), bool) out[k:] = a[k:] > a[:-k] return out def _pctrank(a: np.ndarray, win: int) -> np.ndarray: return pd.Series(a).rolling(win).rank(pct=True).to_numpy() # --------------------------------------------------------------------------- # Strategy Class # --------------------------------------------------------------------------- class LITLuxBook001_15M(IStrategy): INTERFACE_VERSION = 3 timeframe = "15m" atr_period = 14 can_short = False stoploss = -0.99 minimal_roi = {"0": 100.0} process_only_new_candles = True use_exit_signal = False startup_candle_count = 1000 trailing_stop = False slots = 6 leverage = 1 # Pre-calculated barrier parameters: # (global_rule_id, slot, tp_pct, sl_pct, tp_mult, sl_mult, max_hold) # tp_pct/sl_pct: fraction of price (e.g. 0.012 for 1.2%) or None if ATR multiple # tp_mult/sl_mult: ATR multiplier or None if fixed percentage RULES_GEOMETRY = [ # Rule 1: Slot 0 | TP atr2.0 | SL atr6.0 | Hold 32 {"id": 1, "slot": 0, "tp_pct": None, "sl_pct": None, "tp_mult": 2.0, "sl_mult": 6.0, "hold": 32}, # Rule 2: Slot 1 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 2, "slot": 1, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 3: Slot 2 | TP pct0.012 | SL atr4.0 | Hold 16 {"id": 3, "slot": 2, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 16}, # Rule 4: Slot 3 | TP pct0.015 | SL atr4.0 | Hold 16 {"id": 4, "slot": 3, "tp_pct": 0.015, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 16}, # Rule 5: Slot 4 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 5, "slot": 4, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 6: Slot 5 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 6, "slot": 5, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 7: Slot 1 | TP pct0.012 | SL atr4.0 | Hold 32 {"id": 7, "slot": 1, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 32}, # Rule 8: Slot 4 | TP atr2.0 | SL atr6.0 | Hold 32 {"id": 8, "slot": 4, "tp_pct": None, "sl_pct": None, "tp_mult": 2.0, "sl_mult": 6.0, "hold": 32}, # Rule 9: Slot 5 | TP pct0.012 | SL atr4.0 | Hold 32 {"id": 9, "slot": 5, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 32}, # Rule 10: Slot 0 | TP pct0.015 | SL atr5.0 | Hold 16 {"id": 10, "slot": 0, "tp_pct": 0.015, "sl_pct": None, "tp_mult": None, "sl_mult": 5.0, "hold": 16}, # Rule 11: Slot 3 | TP pct0.012 | SL atr3.0 | Hold 32 {"id": 11, "slot": 3, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 3.0, "hold": 32}, # Rule 12: Slot 2 | TP pct0.02 | SL pct0.05 | Hold 32 {"id": 12, "slot": 2, "tp_pct": 0.020, "sl_pct": 0.050, "tp_mult": None, "sl_mult": None, "hold": 32}, # Rule 13: Slot 5 | TP atr1.5 | SL atr5.0 | Hold 16 {"id": 13, "slot": 5, "tp_pct": None, "sl_pct": None, "tp_mult": 1.5, "sl_mult": 5.0, "hold": 16}, # Rule 14: Slot 0 | TP atr2.5 | SL atr6.0 | Hold 16 {"id": 14, "slot": 0, "tp_pct": None, "sl_pct": None, "tp_mult": 2.5, "sl_mult": 6.0, "hold": 16}, # Rule 15: Slot 1 | TP pct0.015 | SL atr5.0 | Hold 16 {"id": 15, "slot": 1, "tp_pct": 0.015, "sl_pct": None, "tp_mult": None, "sl_mult": 5.0, "hold": 16}, # Rule 16: Slot 0 | TP atr2.5 | SL atr6.0 | Hold 16 {"id": 16, "slot": 0, "tp_pct": None, "sl_pct": None, "tp_mult": 2.5, "sl_mult": 6.0, "hold": 16}, # Rule 17: Slot 3 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 17, "slot": 3, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 18: Slot 2 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 18, "slot": 2, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 19: Slot 5 | TP pct0.012 | SL atr4.0 | Hold 32 {"id": 19, "slot": 5, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 32}, # Rule 20: Slot 4 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 20, "slot": 4, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 21: Slot 1 | TP atr1.0 | SL atr2.5 | Hold 96 {"id": 21, "slot": 1, "tp_pct": None, "sl_pct": None, "tp_mult": 1.0, "sl_mult": 2.5, "hold": 96}, # Rule 22: Slot 3 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 22, "slot": 3, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 23: Slot 2 | TP atr1.0 | SL atr3.0 | Hold 8 {"id": 23, "slot": 2, "tp_pct": None, "sl_pct": None, "tp_mult": 1.0, "sl_mult": 3.0, "hold": 8}, # Rule 24: Slot 1 | TP pct0.015 | SL atr4.0 | Hold 32 {"id": 24, "slot": 1, "tp_pct": 0.015, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 32}, # Rule 25: Slot 5 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 25, "slot": 5, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 26: Slot 4 | TP pct0.08 | SL pct0.08 | Hold 16 {"id": 26, "slot": 4, "tp_pct": 0.080, "sl_pct": 0.080, "tp_mult": None, "sl_mult": None, "hold": 16}, # Rule 27: Slot 2 | TP pct0.025 | SL pct0.06 | Hold 16 {"id": 27, "slot": 2, "tp_pct": 0.025, "sl_pct": 0.060, "tp_mult": None, "sl_mult": None, "hold": 16}, # Rule 28: Slot 0 | TP pct0.012 | SL atr4.0 | Hold 96 {"id": 28, "slot": 0, "tp_pct": 0.012, "sl_pct": None, "tp_mult": None, "sl_mult": 4.0, "hold": 96}, # Rule 29: Slot 5 | TP pct0.008 | SL atr2.5 | Hold 96 {"id": 29, "slot": 5, "tp_pct": 0.008, "sl_pct": None, "tp_mult": None, "sl_mult": 2.5, "hold": 96}, # Rule 30: Slot 4 | TP pct0.02 | SL pct0.06 | Hold 96 {"id": 30, "slot": 4, "tp_pct": 0.020, "sl_pct": 0.060, "tp_mult": None, "sl_mult": None, "hold": 96}, ] def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: n = len(dataframe) o = dataframe["open"].to_numpy(dtype=np.float64) h = dataframe["high"].to_numpy(dtype=np.float64) l = dataframe["low"].to_numpy(dtype=np.float64) c = dataframe["close"].to_numpy(dtype=np.float64) v = dataframe["volume"].to_numpy(dtype=np.float64) ot = dataframe["open_time"].to_numpy() p = {} # 1. Supertrend family line7_1, d7_1, atr7_1 = _supertrend(h, l, c, 7, 1.0) p["ST[7,1.0]bear"] = d7_1 == -1 fd7_1 = np.zeros(n, bool); fd7_1[1:] = (d7_1[1:] == -1) & (d7_1[:-1] == 1) p["ST[7,1.0]flipDn"] = fd7_1 line7_15, d7_15, atr7_15 = _supertrend(h, l, c, 7, 1.5) fd7_15 = np.zeros(n, bool); fd7_15[1:] = (d7_15[1:] == -1) & (d7_15[:-1] == 1) p["ST[7,1.5]flipDn"] = fd7_15 with np.errstate(invalid="ignore"): dist7_15 = (c - line7_15) / np.maximum(atr7_15, 1e-12) p["ST[7,1.5]nearBand"] = (d7_15 == 1) & (dist7_15 < 0.5) _, d10_1, _ = _supertrend(h, l, c, 10, 1.0) fd10_1 = np.zeros(n, bool); fd10_1[1:] = (d10_1[1:] == -1) & (d10_1[:-1] == 1) p["ST[10,1.0]flipDn"] = fd10_1 line10_15, d10_15, atr10_15 = _supertrend(h, l, c, 10, 1.5) with np.errstate(invalid="ignore"): dist10_15 = (c - line10_15) / np.maximum(atr10_15, 1e-12) p["ST[10,1.5]nearBand"] = (d10_15 == 1) & (dist10_15 < 0.5) _, d14_4, _ = _supertrend(h, l, c, 14, 4.0) p["ST[14,4.0]bull"] = d14_4 == 1 _, d30_15, _ = _supertrend(h, l, c, 30, 1.5) fd30_15 = np.zeros(n, bool); fd30_15[1:] = (d30_15[1:] == -1) & (d30_15[:-1] == 1) p["ST[30,1.5]flipDn"] = fd30_15 _, hist14_2 = _supertrend_oscillator(h, l, c, 14, 2.0) p["SThist[14,2.0]>0"] = hist14_2 > 0 _, ed20_3, _ = _evasive_supertrend(h, l, c, 20, 3.0) p["EvaST[20,3.0]bull"] = ed20_3 == 1 # 2. Oscillators p["RSI2<20"] = _rsi(c, 2) < 20 p["RSI2<40"] = _rsi(c, 2) < 40 p["RSI3<20"] = _rsi(c, 3) < 20 rsi7 = _rsi(c, 7) p["RSI7>25"] = rsi7 > 25 p["RSI7>50"] = rsi7 > 50 p["RSI7rise3"] = _rising(rsi7, 3) rsi9 = _rsi(c, 9) p["RSI9<30"] = rsi9 < 30 p["RSI9<35"] = rsi9 < 35 p["RSI9>45"] = rsi9 > 45 p["RSI9<65"] = rsi9 < 65 p["RSI14>50"] = _rsi(c, 14) > 50 p["RSI21>50"] = _rsi(c, 21) > 50 kk14, _ = _stoch_rsi(c, 14, 14, 3, 3) p["StochRSI[14]k<20"] = kk14 < 20 wt14_1, wt14_2 = _wavetrend(h, l, c, 14, 28) p["WT[14,28]x"] = _xup(wt14_1, wt14_2) adx14, _, _ = _adx(h, l, c, 14) p["ADX14<25"] = adx14 < 25 adx21, dip21, dim21 = _adx(h, l, c, 21) p["ADX21<30"] = adx21 < 30 p["DI21x"] = _xup(dip21, dim21) tsi25, tsig25 = _tsi(c, 25, 13, 7) p["TSI[25,13]x"] = _xup(tsi25, tsig25) cmo20 = _cmo(c, 20) p["CMO20<-40"] = cmo20 < -40 p["CMO20<-20"] = cmo20 < -20 p["CCI14<-150"] = _cci(h, l, c, 14) < -150 p["CCI20xup-100"] = _xup(_cci(h, l, c, 20), np.full(n, -100.0)) p["WR9<-90"] = _williams_r(h, l, c, 9) < -90 p["WR9<-80"] = _williams_r(h, l, c, 9) < -80 p["WR14<-90"] = _williams_r(h, l, c, 14) < -90 p["WR14>-80"] = _williams_r(h, l, c, 14) > -80 # 3. Channels & Structure up50, mid50, lo50 = _bollinger_bands(c, 50, 2.0) w50 = (up50 - lo50) / np.maximum(mid50, 1e-12) p["BB[50,2.0]wide"] = _pctrank(w50, 200) > 0.75 up10, mid10, lo10 = _bollinger_bands(c, 10, 1.5) w10 = (up10 - lo10) / np.maximum(mid10, 1e-12) p["BB[10,1.5]squeeze"] = _pctrank(w10, 200) < 0.25 pctb10 = (c - lo10) / np.maximum(up10 - lo10, 1e-12) p["BB[10,1.5]pb<0.3"] = pctb10 < 0.3 du20, dl20 = _donchian_channels(h, l, 20) pos20 = (c - dl20) / np.maximum(du20 - dl20, 1e-12) p["DC20pos<0.15"] = pos20 < 0.15 # 4. Moving Averages e21 = _ema(c, 21) p["dEMA21<2"] = (c - e21) / np.maximum(e21, 1e-12) * 100 < 2 e100 = _ema(c, 100) p["dEMA100>-2"] = (c - e100) / np.maximum(e100, 1e-12) * 100 > -2 p["dEMA100<2"] = (c - e100) / np.maximum(e100, 1e-12) * 100 < 2 e200 = _ema(c, 200) p["dEMA200>-4"] = (c - e200) / np.maximum(e200, 1e-12) * 100 > -4 e400 = _ema(c, 400) p["EMA400rise5"] = _rising(e400, 5) p["dEMA400>-4"] = (c - e400) / np.maximum(e400, 1e-12) * 100 > -4 # 5. Momentum & Returns mom4 = np.zeros(n); mom4[4:] = c[4:] / np.maximum(c[:-4], 1e-12) - 1 p["mom1h>-0.015"] = mom4 > -0.015 mom16 = np.zeros(n); mom16[16:] = c[16:] / np.maximum(c[:-16], 1e-12) - 1 p["mom4h<-0.03"] = mom16 < -0.03 r1 = np.zeros(n); r1[1:] = c[1:] / np.maximum(c[:-1], 1e-12) - 1 p["ret1<-0.01"] = r1 < -0.01 r5 = np.zeros(n); r5[5:] = c[5:] / np.maximum(c[:-5], 1e-12) - 1 p["ret5<-0.02"] = r5 < -0.02 p["ret5<0.02"] = r5 < 0.02 r8 = np.zeros(n); r8[8:] = c[8:] / np.maximum(c[:-8], 1e-12) - 1 p["ret8<-0.03"] = r8 < -0.03 # 6. Order Flow Features (using DataFrame fields with fallback to deltaProxy) if "bnot" in dataframe and "snot" in dataframe and dataframe["bnot"].sum() > 0: bnot = dataframe["bnot"].to_numpy(float) snot = dataframe["snot"].to_numpy(float) bbig = dataframe["bbig"].to_numpy(float) if "bbig" in dataframe else np.zeros(n) sbig = dataframe["sbig"].to_numpy(float) if "sbig" in dataframe else np.zeros(n) lbnot = dataframe["lbnot"].to_numpy(float) if "lbnot" in dataframe else np.zeros(n) lsnot = dataframe["lsnot"].to_numpy(float) if "lsnot" in dataframe else np.zeros(n) tot = np.maximum(bnot + snot, 1e-9) ofi = (bnot - snot) / tot bigtot = np.maximum(bbig + sbig, 1e-9) bofi = (bbig - sbig) / bigtot ltot = np.maximum(lbnot + lsnot, 1e-9) lofi = (lbnot - lsnot) / ltot else: bar_rng = h - l d_proxy = np.where(bar_rng > 0, (c - o) / bar_rng * v, 0.0) tot = np.maximum(v, 1e-9) ofi = d_proxy / tot bofi = pd.Series(d_proxy).rolling(5).sum().to_numpy() / np.maximum(pd.Series(v).rolling(5).sum().to_numpy(), 1e-9) lofi = pd.Series(d_proxy).rolling(3).sum().to_numpy() / np.maximum(pd.Series(v).rolling(3).sum().to_numpy(), 1e-9) p["OFI<0.05"] = ofi < 0.05 p["bigOFI<-0.2"] = bofi < -0.2 p["bigOFI>-0.1"] = bofi > -0.1 p["bigOFI<-0.05"] = bofi < -0.05 p["bigOFI>0.05"] = bofi > 0.05 bofima12 = pd.Series(bofi).rolling(12).mean().to_numpy() p["bigOFIma12<0"] = bofima12 < 0 p["lateOFI<0.1"] = lofi < 0.1 # 7. Volume Z-Score & Session vz = (v - pd.Series(v).rolling(96).mean().to_numpy()) / np.maximum(pd.Series(v).rolling(96).std().to_numpy(), 1e-9) p["volZ<-0.5"] = vz < -0.5 p["volZ<0.5"] = vz < 0.5 p["volZ>1.0"] = vz > 1.0 hour = pd.to_datetime(ot, unit="ms", utc=True).hour.to_numpy() p["sess_eu"] = (hour >= 7) & (hour < 13) # 8. Evaluate 30 rules rules = [ # Rule 1 [Slot 0] p["CMO20<-40"] & p["WR14<-90"], # Rule 2 [Slot 1] p["ST[30,1.5]flipDn"] & p["RSI21>50"] & p["BB[50,2.0]wide"], # Rule 3 [Slot 2] p["ST[7,1.5]flipDn"] & p["sess_eu"], # Rule 4 [Slot 3] p["ST[30,1.5]flipDn"] & p["sess_eu"], # Rule 5 [Slot 4] p["ST[30,1.5]flipDn"] & p["RSI21>50"] & p["BB[50,2.0]wide"] & p["ret5<0.02"], # Rule 6 [Slot 5] p["RSI2<40"] & p["RSI7rise3"] & p["mom4h<-0.03"], # Rule 7 [Slot 1] p["CCI20xup-100"] & p["mom4h<-0.03"], # Rule 8 [Slot 4] p["CMO20<-40"] & p["WR9<-90"], # Rule 9 [Slot 5] p["ST[7,1.0]flipDn"] & p["WR14>-80"] & p["volZ>1.0"], # Rule 10 [Slot 0] p["ST[30,1.5]flipDn"] & p["ADX21<30"] & p["sess_eu"], # Rule 11 [Slot 3] p["RSI7rise3"] & p["DC20pos<0.15"] & p["EMA400rise5"], # Rule 12 [Slot 2] p["CMO20<-40"] & p["WR14<-90"] & p["volZ<0.5"], # Rule 13 [Slot 5] p["ST[10,1.0]flipDn"] & p["ADX14<25"] & p["dEMA200>-4"] & p["sess_eu"], # Rule 14 [Slot 0] p["ST[7,1.5]flipDn"] & p["dEMA100<2"] & p["sess_eu"], # Rule 15 [Slot 1] p["ST[30,1.5]flipDn"] & p["RSI7>25"] & p["ADX21<30"] & p["sess_eu"], # Rule 16 [Slot 0] p["ST[7,1.0]bear"] & p["RSI2<20"] & p["RSI7>50"] & p["dEMA100>-2"], # Rule 17 [Slot 3] p["WR9<-90"] & p["BB[50,2.0]wide"] & p["bigOFI>0.05"], # Rule 18 [Slot 2] p["ST[7,1.5]nearBand"] & p["BB[50,2.0]wide"] & p["dEMA400>-4"] & p["bigOFI<-0.05"], # Rule 19 [Slot 5] p["RSI3<20"] & p["RSI14>50"], # Rule 20 [Slot 4] p["ST[30,1.5]flipDn"] & p["ret5<-0.02"], # Rule 21 [Slot 1] p["EvaST[20,3.0]bull"] & p["RSI9<35"] & p["BB[10,1.5]squeeze"], # Rule 22 [Slot 3] p["ST[30,1.5]flipDn"] & p["StochRSI[14]k<20"] & p["WR9<-80"] & p["OFI<0.05"], # Rule 23 [Slot 2] p["CCI14<-150"] & p["ret8<-0.03"] & p["bigOFI<-0.2"], # Rule 24 [Slot 1] p["ST[7,1.0]flipDn"] & p["WR14>-80"] & p["dEMA21<2"] & p["volZ>1.0"], # Rule 25 [Slot 5] p["ST[14,4.0]bull"] & p["RSI9>45"] & p["CMO20<-20"], # Rule 26 [Slot 4] p["DI21x"] & p["mom1h>-0.015"] & p["bigOFI>-0.1"] & p["volZ<-0.5"], # Rule 27 [Slot 2] p["RSI9<65"] & p["WT[14,28]x"] & p["DI21x"] & p["dEMA200>-4"], # Rule 28 [Slot 0] p["ST[10,1.5]nearBand"] & p["BB[50,2.0]wide"] & p["BB[10,1.5]pb<0.3"], # Rule 29 [Slot 5] p["RSI9<30"] & p["ret1<-0.01"] & p["bigOFIma12<0"], # Rule 30 [Slot 4] p["SThist[14,2.0]>0"] & p["WT[14,28]x"] & p["TSI[25,13]x"] & p["lateOFI<0.1"], ] atr14 = _atr(h, l, c, 14) dataframe["atr"] = atr14 # 9. Assign winning rule per slot for s in range(self.slots): slot_rules_idx = [i for i, geom in enumerate(self.RULES_GEOMETRY) if geom["slot"] == s] s_rule = np.full(n, -1, dtype=np.int16) s_tp_pct = np.full(n, np.nan, dtype=np.float64) s_sl_pct = np.full(n, np.nan, dtype=np.float64) s_tp_mult = np.full(n, np.nan, dtype=np.float64) s_sl_mult = np.full(n, np.nan, dtype=np.float64) s_hold = np.zeros(n, dtype=np.int16) # Earliest rule in slot wins for rid, global_i in enumerate(slot_rules_idx): rule_mask = rules[global_i] geom = self.RULES_GEOMETRY[global_i] new_bars = rule_mask & (s_rule < 0) if new_bars.any(): s_rule[new_bars] = rid if geom["tp_pct"] is not None: s_tp_pct[new_bars] = geom["tp_pct"] if geom["sl_pct"] is not None: s_sl_pct[new_bars] = geom["sl_pct"] if geom["tp_mult"] is not None: s_tp_mult[new_bars] = geom["tp_mult"] if geom["sl_mult"] is not None: s_sl_mult[new_bars] = geom["sl_mult"] s_hold[new_bars] = geom["hold"] dataframe[f"slot_{s}_rule"] = s_rule dataframe[f"slot_{s}_tp_pct"] = s_tp_pct dataframe[f"slot_{s}_sl_pct"] = s_sl_pct dataframe[f"slot_{s}_tp_mult"] = s_tp_mult dataframe[f"slot_{s}_sl_mult"] = s_sl_mult dataframe[f"slot_{s}_hold"] = s_hold return dataframe def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: armed = dataframe["atr"].notna() & (dataframe["atr"] > 0) any_slot = False for s in range(self.slots): any_slot = any_slot | (dataframe[f"slot_{s}_rule"] >= 0) dataframe["enter_long"] = (any_slot & armed).astype(np.int8) dataframe["enter_short"] = 0 return dataframe def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: dataframe["exit_long"] = 0 dataframe["exit_short"] = 0 dataframe["exit_reason"] = "" dataframe["exit_price"] = np.nan return dataframe