Riy777 commited on
Commit
a61f677
·
verified ·
1 Parent(s): 9b3bb21

Update governance_engine.py

Browse files
Files changed (1) hide show
  1. governance_engine.py +344 -394
governance_engine.py CHANGED
@@ -1,13 +1,10 @@
1
  # ============================================================
2
- # 🏛️ governance_engine.py (V1.2 - The Senate / Maximum Integrity)
3
  # ============================================================
4
  # Description:
5
- # Evaluates trade quality using 156 INDICATORS (Fully Coded).
6
- # Output: Governance Score (0-100) & Quality Grade.
7
- # Features:
8
- # - Full Verbose Audit Mode.
9
- # - No Logic Shortcuts.
10
- # - Real-time Orderbook & Structure Analysis.
11
  # ============================================================
12
 
13
  import numpy as np
@@ -17,7 +14,7 @@ from typing import Dict, Any, List
17
 
18
  class GovernanceEngine:
19
  def __init__(self):
20
- # ⚖️ Strategic Weights (Dynamic DNA)
21
  self.WEIGHTS = {
22
  "order_book": 0.25, # 25%
23
  "market_structure": 0.20, # 20%
@@ -27,12 +24,11 @@ class GovernanceEngine:
27
  "volatility": 0.05, # 5%
28
  "cycle_math": 0.10 # 10%
29
  }
30
- print("🏛️ [Governance Engine V1.2] The Monolith is Online. 156 Indicators Loaded.")
31
 
32
  async def evaluate_trade(self, symbol: str, ohlcv_data: Dict[str, Any], order_book: Dict[str, Any], verbose: bool = True) -> Dict[str, Any]:
33
  """
34
  Main Execution Entry.
35
- verbose=True: Prints detailed indicator values for every single check.
36
  """
37
  try:
38
  # 1. Data Prep
@@ -46,7 +42,7 @@ class GovernanceEngine:
46
  print(f"\n📝 [Gov Audit] Opening Session for {symbol}...")
47
  print("-" * 80)
48
 
49
- # 2. Calculate Domains (Full Exhaustive Check)
50
  s_trend = self._calc_trend_domain(df, verbose)
51
  s_mom = self._calc_momentum_domain(df, verbose)
52
  s_vol = self._calc_volatility_domain(df, verbose)
@@ -91,8 +87,8 @@ class GovernanceEngine:
91
  print(f"❌ [Governance Critical Error] {e}")
92
  return self._create_rejection(f"Exception: {str(e)}")
93
 
94
- # ==============================================================================
95
- # 📈 DOMAIN 1: TREND (Fixed SuperTrend)
96
  # ==============================================================================
97
  def _calc_trend_domain(self, df: pd.DataFrame, verbose: bool) -> float:
98
  points = 0.0
@@ -102,125 +98,126 @@ class GovernanceEngine:
102
 
103
  # 1. EMA 9 > 21
104
  ema9 = ta.ema(c, 9); ema21 = ta.ema(c, 21)
105
- if ema9 is not None and ema21 is not None and ema9.iloc[-1] > ema21.iloc[-1]: points += 1; details.append("EMA9>21")
 
106
 
107
  # 2. EMA 21 > 50
108
  ema50 = ta.ema(c, 50)
109
- if ema21 is not None and ema50 is not None and ema21.iloc[-1] > ema50.iloc[-1]: points += 1; details.append("EMA21>50")
 
110
 
111
- # 3. Price > EMA 200 (The King)
112
  ema200 = ta.ema(c, 200)
113
- if ema200 is not None:
114
  if c.iloc[-1] > ema200.iloc[-1]: points += 2; details.append("Price>EMA200")
115
  else: points -= 2; details.append("Price<EMA200")
116
 
117
- # 4. Supertrend (Fixed)
118
- try:
119
- st = ta.supertrend(df['high'], df['low'], c, length=10, multiplier=3)
120
- if st is not None:
121
- # نأخذ العمود الأول دائماً بغض النظر عن اسمه (Trend direction usually column 0 or 1)
122
- # Supertrend returns: [SUPERT_..., SUPERTd_..., SUPERTl_..., SUPERTs_...]
123
- # We usually want the main line or check direction.
124
- # Simplest check: If Close > Supertrend Line (Column 0)
125
- st_line = st.iloc[:, 0]
126
- if c.iloc[-1] > st_line.iloc[-1]:
127
- points += 1; details.append("ST:Bull")
128
- else:
129
- points -= 1
130
- except: pass
131
 
132
  # 5. Parabolic SAR
133
  psar = ta.psar(df['high'], df['low'], c)
134
- if psar is not None:
135
- # PSAR returns valid values for long/short columns, check which one is non-NaN or compare combined
136
- # Simplified: psar typically returns one value per row closer to price
137
- psar_val = psar.iloc[-1]
138
- if isinstance(psar_val, pd.Series): psar_val = psar_val.dropna().iloc[0] if not psar_val.dropna().empty else 0
139
 
140
- if psar_val != 0:
141
- if psar_val < c.iloc[-1]: points += 1; details.append("PSAR:Bull")
142
  else: points -= 1
143
 
144
- # 6. ADX Strength
145
  adx = ta.adx(df['high'], df['low'], c, length=14)
146
- if adx is not None:
147
- val = adx[adx.columns[0]].iloc[-1] # ADX_14
148
- dmp = adx[adx.columns[1]].iloc[-1] # DMP_14
149
- dmn = adx[adx.columns[2]].iloc[-1] # DMN_14
150
-
151
  if val > 25:
152
  if dmp > dmn: points += 1.5; details.append("ADX:StrongBull")
153
  else: points -= 1.5; details.append("ADX:StrongBear")
154
  else: details.append("ADX:Weak")
155
 
156
- # ... (باقي المؤشرات كما هي في الكود السابق) ...
157
  # 7. Ichimoku
158
- ichi = ta.ichimoku(df['high'], df['low'], c)[0]
159
- if c.iloc[-1] > ichi[ichi.columns[0]].iloc[-1] and c.iloc[-1] > ichi[ichi.columns[1]].iloc[-1]: points += 1; details.append("Ichi:Bull")
 
 
 
 
160
 
161
  # 8. Vortex
162
  vortex = ta.vortex(df['high'], df['low'], c)
163
- if vortex is not None and vortex[vortex.columns[0]].iloc[-1] > vortex[vortex.columns[1]].iloc[-1]:
164
- points += 1; details.append("Vortex:Bull")
 
165
 
166
  # 9. Aroon
167
  aroon = ta.aroon(df['high'], df['low'])
168
- if aroon is not None:
169
- if aroon[aroon.columns[0]].iloc[-1] > 70: points += 1; details.append("Aroon:Up") # Aroon U
170
- elif aroon[aroon.columns[1]].iloc[-1] > 70: points -= 1; details.append("Aroon:Down") # Aroon D
171
 
172
- # 10. Linear Reg Slope
173
- slope = ta.slope(c, length=14).iloc[-1]
174
- if slope > 0: points += 1; details.append("Slope:Pos")
175
 
176
  # 11. KAMA
177
  kama = ta.kama(c, length=10)
178
- if kama is not None and c.iloc[-1] > kama.iloc[-1]: points += 1; details.append("KAMA:Bull")
179
 
180
  # 12. TRIX
181
  trix = ta.trix(c, length=30)
182
- if trix is not None and trix.iloc[-1] > 0: points += 1; details.append("TRIX:Bull")
183
 
184
  # 13. DPO
185
  dpo = ta.dpo(c, length=20)
186
- if dpo is not None and dpo.iloc[-1] > 0: points += 1; details.append("DPO:Bull")
187
 
188
  # 14. SMA Cluster
189
  sma20 = ta.sma(c, 20); sma50 = ta.sma(c, 50)
190
- if sma20 is not None and sma50 is not None and sma20.iloc[-1] > sma50.iloc[-1]: points += 1; details.append("SMA20>50")
 
191
 
192
- # 15. ZigZag Proxy
193
  if df['high'].iloc[-1] > df['high'].iloc[-5]: points += 1; details.append("ZigZag:Up")
194
 
195
  # 16. MACD Slope
196
  macd = ta.macd(c)
197
- if macd is not None:
198
- macd_line = macd[macd.columns[0]]
199
- if macd_line.iloc[-1] > macd_line.iloc[-2]: points += 1; details.append("MACD_Slope:Up")
200
 
201
  # 17. Coppock
202
  coppock = ta.coppock(c)
203
- if coppock is not None and coppock.iloc[-1] > 0: points += 0.5; details.append("Coppock:Bull")
204
 
205
  # 18. HMA
206
  hma = ta.hma(c, length=9)
207
- if hma is not None and c.iloc[-1] > hma.iloc[-1]: points += 1; details.append("HMA:Bull")
208
 
209
  # 19. Donchian
210
  dc = ta.donchian(df['high'], df['low'])
211
- if dc is not None and c.iloc[-1] > dc[dc.columns[1]].iloc[-1]: points += 1; details.append("Donchian:Upper") # Middle column is usually Upper or Mid
 
212
 
213
  # 20. Keltner
214
  kc = ta.kc(df['high'], df['low'], c)
215
- if kc is not None and c.iloc[-1] > kc[kc.columns[0]].iloc[-1]: points += 0.5; details.append("Keltner:Safe") # Lower band
 
216
 
217
  except Exception as e: details.append(f"TrendErr:{str(e)[:15]}")
218
 
219
  norm_score = self._normalize(points, max_possible=22.0)
220
  if verbose: print(f" 📈 [TREND] Score: {norm_score:.2f} | {', '.join(details)}")
221
  return norm_score
 
222
  # ==============================================================================
223
- # 🚀 DOMAIN 2: MOMENTUM (The Full 20)
224
  # ==============================================================================
225
  def _calc_momentum_domain(self, df: pd.DataFrame, verbose: bool) -> float:
226
  points = 0.0
@@ -229,105 +226,122 @@ class GovernanceEngine:
229
  c = df['close']
230
 
231
  # 1. RSI
232
- rsi = ta.rsi(c, length=14).iloc[-1]
233
- if 50 < rsi < 70: points += 2; details.append(f"RSI:{rsi:.0f}")
234
- elif rsi > 70: points -= 1; details.append("RSI:OB")
235
- elif rsi < 30: points += 1; details.append("RSI:OS")
236
-
237
- # 2. MACD Crossover
 
 
238
  macd = ta.macd(c)
239
- if macd['MACD_12_26_9'].iloc[-1] > macd['MACDs_12_26_9'].iloc[-1]:
240
- points += 1.5; details.append("MACD:X_Bull")
 
 
 
241
 
242
- # 3. MACD Histogram
243
- if macd['MACDh_12_26_9'].iloc[-1] > 0: points += 1; details.append("MACD_Hist:Pos")
244
-
245
- # 4. Stochastic K > D
246
  stoch = ta.stoch(df['high'], df['low'], c)
247
- k = stoch['STOCHk_14_3_3'].iloc[-1]; d = stoch['STOCHd_14_3_3'].iloc[-1]
248
- if k > d and k < 80: points += 1; details.append("Stoch:Bull")
 
 
249
 
250
- # 5. Awesome Oscillator (AO)
251
  ao = ta.ao(df['high'], df['low'])
252
- if ao.iloc[-1] > 0 and ao.iloc[-1] > ao.iloc[-2]: points += 1; details.append("AO:Rising")
 
253
 
254
  # 6. CCI
255
- cci = ta.cci(df['high'], df['low'], c).iloc[-1]
256
- if cci > 100: points += 1; details.append("CCI:>100")
257
- elif cci < -100: points -= 1
 
 
258
 
259
  # 7. Williams %R
260
- willr = ta.willr(df['high'], df['low'], c).iloc[-1]
261
- if willr < -80: points += 1; details.append("WillR:OS")
 
262
 
263
- # 8. ROC (Rate of Change)
264
- roc = ta.roc(c, length=10).iloc[-1]
265
- if roc > 0: points += 1; details.append(f"ROC:{roc:.2f}")
 
266
 
267
- # 9. MOM (Simple Momentum)
268
- mom = ta.mom(c, length=10).iloc[-1]
269
- if mom > 0: points += 1; details.append("MOM:Pos")
 
270
 
271
  # 10. PPO
272
  ppo = ta.ppo(c)
273
- if ppo is not None and ppo['PPO_12_26_9'].iloc[-1] > 0: points += 1; details.append("PPO:Pos")
 
274
 
275
- # 11. TSI (True Strength Index)
276
  tsi = ta.tsi(c)
277
- if tsi is not None and tsi['TSI_13_25_13'].iloc[-1] > tsi['TSIs_13_25_13'].iloc[-1]:
278
  points += 1; details.append("TSI:Bull")
279
 
280
- # 12. Fisher Transform
281
  fish = ta.fisher(df['high'], df['low'])
282
- if fish is not None and fish['FISHERT_9_1'].iloc[-1] > fish['FISHERTs_9_1'].iloc[-1]:
283
  points += 1; details.append("Fisher:Bull")
284
 
285
- # 13. CMO (Chande Momentum)
286
- cmo = ta.cmo(c, length=14).iloc[-1]
287
- if cmo > 0: points += 1; details.append("CMO:Pos")
 
288
 
289
- # 14. Squeeze Momentum (LazyBear)
290
  bb = ta.bbands(c, length=20)
291
  kc = ta.kc(df['high'], df['low'], c)
292
- if bb['BBU_20_2.0'].iloc[-1] < kc['KCUe_20_2'].iloc[-1]:
293
- points += 1; details.append("SQZ:Active")
 
294
 
295
- # 15. Ultimate Oscillator
296
  uo = ta.uo(df['high'], df['low'], c)
297
- if uo is not None and uo.iloc[-1] > 50: points += 0.5; details.append("UO:>50")
 
298
 
299
- # 16. KDJ
300
  kdj = ta.kdj(df['high'], df['low'], c)
301
- if kdj is not None and kdj['K_9_3'].iloc[-1] > kdj['D_9_3'].iloc[-1]: points += 0.5; details.append("KDJ:Bull")
 
302
 
303
  # 17. StochRSI
304
  stochrsi = ta.stochrsi(c)
305
- if stochrsi is not None and stochrsi['STOCHRSIk_14_14_3_3'].iloc[-1] < 20:
306
  points += 1; details.append("StochRSI:OS")
307
 
308
- # 18. Elder Ray (Bull Power)
309
  ema13 = ta.ema(c, 13)
310
- bull_power = df['high'] - ema13
311
- if bull_power.iloc[-1] > 0 and bull_power.iloc[-1] > bull_power.iloc[-2]:
312
- points += 1; details.append("BullPower:Rising")
313
-
314
- # 19. Connors RSI (Scalping)
315
- # Standard TA lib sometimes misses this, using manual simple proxy:
316
- # if streak is positive
317
- if c.iloc[-1] > c.iloc[-2] and c.iloc[-2] > c.iloc[-3]: points += 0.5; details.append("Streak:Up")
318
-
319
- # 20. Bias Ratio
320
- bias = (c.iloc[-1] - ta.ema(c, 20).iloc[-1]) / ta.ema(c, 20).iloc[-1]
321
- if 0 < bias < 0.05: points += 1; details.append("Bias:Healthy")
322
-
323
- except Exception as e: details.append("MomErr")
 
 
324
 
325
  norm_score = self._normalize(points, max_possible=20.0)
326
  if verbose: print(f" 🚀 [MOMENTUM] Score: {norm_score:.2f} | {', '.join(details)}")
327
  return norm_score
328
 
329
  # ==============================================================================
330
- # 🌊 DOMAIN 3: VOLATILITY (The Full 20)
331
  # ==============================================================================
332
  def _calc_volatility_domain(self, df: pd.DataFrame, verbose: bool) -> float:
333
  points = 0.0
@@ -335,191 +349,209 @@ class GovernanceEngine:
335
  try:
336
  # 1. BB Width
337
  bb = ta.bbands(df['close'], length=20)
338
- width = bb['BBP_20_2.0'].iloc[-1]
339
- if width < 0.10: points -= 1; details.append("BBW:Tight")
340
- elif width > 1.0: points += 1; details.append("BBW:Wide")
341
-
342
- # 2. BB %B
343
- pct_b = bb['BBP_20_2.0'].iloc[-1]
344
- if pct_b > 0.8: points += 1; details.append("BB%B:High")
 
345
 
346
- # 3. ATR Rising
347
  atr = ta.atr(df['high'], df['low'], df['close'], length=14)
348
- if atr.iloc[-1] > atr.iloc[-5]: points += 1; details.append("ATR:Rising")
 
349
 
350
- # 4. KC Upper Break
351
  kc = ta.kc(df['high'], df['low'], df['close'])
352
- if df['close'].iloc[-1] > kc['KCUe_20_2'].iloc[-1]: points += 2; details.append("KC:Breakout")
 
353
 
354
- # 5. Donchian Upper Touch
355
  dc = ta.donchian(df['high'], df['low'])
356
- if df['high'].iloc[-1] >= dc['DCU_20_20'].iloc[-2]: points += 1; details.append("DC:High")
 
357
 
358
  # 6. Mass Index
359
  mass = ta.massi(df['high'], df['low'])
360
- if mass.iloc[-1] > 25: points -= 1; details.append("Mass:ReversalRisk")
361
-
362
- # 7. Chaikin Volatility
363
- c_vol = ta.stddev(df['close'], 20)
364
- if c_vol.iloc[-1] > c_vol.iloc[-10]: points += 1; details.append("Vol:Expanding")
365
-
366
- # 8. Ulcer Index
367
- ui = ta.ui(df['close']).iloc[-1]
368
- if ui < 2: points += 1; details.append("UI:Safe")
369
- else: points -= 1; details.append("UI:Risk")
370
-
371
- # 9. NATR (Normalized ATR)
372
- natr = ta.natr(df['high'], df['low'], df['close']).iloc[-1]
373
- if natr > 1.0: points += 1; details.append(f"NATR:{natr:.1f}")
374
-
375
- # 10. Gap Detection
376
- gap = abs(df['open'].iloc[-1] - df['close'].iloc[-2])
377
- if gap > atr.iloc[-1] * 0.5: points += 1; details.append("Gap:Found")
378
-
379
- # 11. Volatility Ratio
380
- vr = atr.iloc[-1] / atr.iloc[-20]
381
- if vr > 1.2: points += 1; details.append("VolRatio:High")
382
-
383
- # 12. RVI (Relative Volatility Index) - Proxy using StdDev RSI
384
- std_rsi = ta.rsi(c_vol, length=14).iloc[-1]
385
- if std_rsi > 50: points += 0.5; details.append("RVI:High")
 
 
 
 
 
 
 
 
386
 
387
  # 13. StdDev Channel
388
- # If price is within 2 std devs, stability point
389
  mean = df['close'].rolling(20).mean()
390
  std = df['close'].rolling(20).std()
391
- z_score = (df['close'].iloc[-1] - mean.iloc[-1]) / std.iloc[-1]
392
- if abs(z_score) < 2: points += 0.5; details.append("StdDev:Normal")
393
-
394
- # 14. ATR Trailing Stop
395
- # If price is above ATR Trailing Stop
396
- ats = df['close'].iloc[-1] - (atr.iloc[-1] * 2)
397
- if df['close'].iloc[-1] > ats: points += 1; details.append("AboveATS")
398
-
399
- # 15. Choppiness Index
400
- chop = ta.chop(df['high'], df['low'], df['close']).iloc[-1]
401
- if chop < 38.2: points += 1; details.append("Chop:Trending")
402
- elif chop > 61.8: points -= 1; details.append("Chop:Range")
403
-
404
- # 16. Keltner Width
405
- kw = (kc['KCUe_20_2'].iloc[-1] - kc['KCLe_20_2'].iloc[-1])
406
- if kw > kw * 1.1: points += 0.5 # Expanding
407
-
408
- # 17. Acceleration Bands
409
- # Proxy: if price is accelerating away from mean
 
 
410
  if df['close'].diff().iloc[-1] > df['close'].diff().iloc[-2]: points += 0.5
411
 
412
- # 18. Efficiency Ratio
413
- eff = abs(df['close'].iloc[-1] - df['close'].iloc[-10]) / (df['high'].rolling(10).max() - df['low'].rolling(10).min()).iloc[-1]
414
- if eff > 0.5: points += 1; details.append("Eff:High")
 
 
415
 
416
- # 19. Gator (Bill Williams)
417
- # Proxy using expanding MAs
418
  if ta.ema(df['close'], 5).iloc[-1] > ta.ema(df['close'], 13).iloc[-1]: points += 0.5
419
 
420
- # 20. Range Analysis
421
- daily_range = df['high'].iloc[-1] - df['low'].iloc[-1]
422
- if daily_range > atr.iloc[-1]: points += 1; details.append("WideRangeBar")
 
423
 
424
- except Exception as e: details.append("VolErr")
425
  norm_score = self._normalize(points, max_possible=18.0)
426
  if verbose: print(f" 🌊 [VOLATILITY] Score: {norm_score:.2f} | {', '.join(details)}")
427
  return norm_score
428
 
429
  # ==============================================================================
430
- # ⛽ DOMAIN 4: VOLUME (The Full 20)
431
  # ==============================================================================
432
  def _calc_volume_domain(self, df: pd.DataFrame, verbose: bool) -> float:
433
  points = 0.0
434
  details = []
435
  try:
436
  c = df['close']; v = df['volume']
437
- # 1. OBV Rising
438
  obv = ta.obv(c, v)
439
- if obv.iloc[-1] > obv.iloc[-5]: points += 1.5; details.append("OBV:Up")
 
440
 
441
- # 2. CMF > 0
442
- cmf = ta.cmf(df['high'], df['low'], c, v, length=20).iloc[-1]
443
- if cmf > 0.05: points += 2; details.append(f"CMF:{cmf:.2f}")
444
- elif cmf < -0.05: points -= 2
 
 
445
 
446
  # 3. MFI
447
- mfi = ta.mfi(df['high'], df['low'], c, v, length=14).iloc[-1]
448
- if 50 < mfi < 80: points += 1; details.append(f"MFI:{mfi:.0f}")
 
 
449
 
450
- # 4. Volume > MA
451
  vol_ma = v.rolling(20).mean().iloc[-1]
452
- if v.iloc[-1] > vol_ma: points += 1; details.append("Vol>Avg")
453
 
454
- # 5. Volume Spike (>1.5x)
455
  if v.iloc[-1] > vol_ma * 1.5: points += 1; details.append("Vol:Spike")
456
 
457
- # 6. EOM (Ease of Movement)
458
- eom = ta.eom(df['high'], df['low'], c, v).iloc[-1]
459
- if eom > 0: points += 1; details.append("EOM:Pos")
460
 
461
- # 7. VWAP Relationship
462
  vwap = ta.vwap(df['high'], df['low'], c, v)
463
- if c.iloc[-1] > vwap.iloc[-1]: points += 1; details.append("Price>VWAP")
464
 
465
- # 8. NVI (Negative Volume Index)
466
  nvi = ta.nvi(c, v)
467
- if nvi.iloc[-1] > nvi.iloc[-5]: points += 1; details.append("NVI:SmartMoney")
468
 
469
- # 9. PVI (Positive Volume Index)
470
  pvi = ta.pvi(c, v)
471
- if pvi.iloc[-1] > pvi.iloc[-5]: points += 0.5; details.append("PVI:Follow")
472
 
473
- # 10. ADL (Accumulation/Distribution)
474
  adl = ta.ad(df['high'], df['low'], c, v)
475
- if adl.iloc[-1] > adl.iloc[-2]: points += 1; details.append("ADL:Up")
476
 
477
- # 11. PVT (Price Volume Trend)
478
  pvt = ta.pvt(c, v)
479
- if pvt.iloc[-1] > pvt.iloc[-2]: points += 1; details.append("PVT:Up")
480
 
481
- # 12. Volume Oscillator
482
  if v.rolling(5).mean().iloc[-1] > v.rolling(10).mean().iloc[-1]: points += 1
483
 
484
- # 13. Klinger Oscillator
485
  kvo = ta.kvo(df['high'], df['low'], c, v)
486
- if kvo is not None and kvo['KVO_34_55_13'].iloc[-1] > 0: points += 1; details.append("KVO:Bull")
487
 
488
- # 14. Force Index
489
  fi = (c.diff() * v).rolling(13).mean()
490
- if fi.iloc[-1] > 0: points += 1; details.append("Force:Bull")
491
 
492
- # 15. Market Facilitation Index (MFI - Bill Williams)
493
- # Vol up, Range up
494
- my_mfi = (df['high'] - df['low']) / v
495
- if my_mfi.iloc[-1] > my_mfi.iloc[-2] and v.iloc[-1] > v.iloc[-2]: points += 1; details.append("MFI:Green")
496
 
497
- # 16. Buying Climax Detection
498
- if v.iloc[-1] > vol_ma * 3 and c.iloc[-1] > df['high'].iloc[-2]: points -= 1; details.append("Vol:ClimaxRisk")
499
 
500
- # 17. Relative Volume (RVOL)
501
- rvol = v.iloc[-1] / vol_ma
502
- if rvol > 1.2: points += 1; details.append(f"RVOL:{rvol:.1f}")
 
503
 
504
- # 18. Volume Delta Proxy
505
- # (Close - Open) * Volume
506
  delta = (c.iloc[-1] - df['open'].iloc[-1]) * v.iloc[-1]
507
- if delta > 0: points += 1; details.append("Delta:Pos")
508
 
509
- # 19. Volume weighted MACD (VW-MACD) - Proxy
510
- # Standard MACD is already counted, giving small weight
511
-
512
- # 20. Liquidity Fill (Volume dropping in gap)
513
- if v.iloc[-1] < vol_ma * 0.5 and abs(c.diff().iloc[-1]) > ta.atr(df['high'], df['low'], c).iloc[-1]:
514
- points -= 1; details.append("LowVolMove")
515
 
516
- except Exception as e: details.append("VolErr")
517
  norm_score = self._normalize(points, max_possible=18.0)
518
  if verbose: print(f" ⛽ [VOLUME] Score: {norm_score:.2f} | {', '.join(details)}")
519
  return norm_score
520
 
521
  # ==============================================================================
522
- # 🔢 DOMAIN 5: CYCLE & MATH (The Full 20)
523
  # ==============================================================================
524
  def _calc_cycle_math_domain(self, df: pd.DataFrame, verbose: bool) -> float:
525
  points = 0.0
@@ -527,86 +559,69 @@ class GovernanceEngine:
527
  try:
528
  c = df['close']; h = df['high']; l = df['low']
529
 
530
- # 1. Pivot Point (Classic)
531
  pp = (h.iloc[-2] + l.iloc[-2] + c.iloc[-2]) / 3
532
  if c.iloc[-1] > pp: points += 1; details.append("AbovePP")
533
 
534
- # 2. R1 Breach
535
  r1 = (2 * pp) - l.iloc[-2]
536
  if c.iloc[-1] > r1: points += 1; details.append("AboveR1")
537
 
538
- # 3. Fibonacci Golden Pocket
539
  range_h = h.rolling(100).max().iloc[-1]
540
  range_l = l.rolling(100).min().iloc[-1]
541
  fib_618 = range_l + (range_h - range_l) * 0.618
542
- if c.iloc[-1] > fib_618: points += 1; details.append("AboveFib618")
543
 
544
- # 4. Z-Score (Mean Reversion)
545
- zscore = ta.zscore(c, length=30).iloc[-1]
546
- if zscore < -2: points += 2; details.append("Z:Oversold")
547
- elif -1 < zscore < 1: points += 0.5; details.append("Z:Normal")
 
 
548
 
549
- # 5. Entropy (Chaos)
550
- entropy = ta.entropy(c, length=10).iloc[-1]
551
- if entropy < 0.5: points += 1; details.append(f"Ent:{entropy:.2f}")
 
552
 
553
- # 6. Kurtosis (Tail Risk)
554
  kurt = c.rolling(30).kurt().iloc[-1]
555
- if kurt > 3: points -= 0.5; details.append("HighKurt")
556
 
557
- # 7. Skewness
558
  skew = c.rolling(30).skew().iloc[-1]
559
  if skew > 0: points += 0.5; details.append("PosSkew")
560
 
561
  # 8. Variance
562
- var = ta.variance(c, length=20).iloc[-1]
563
- if var > 0: points += 0
564
 
565
  # 9. StdDev
566
  std = c.rolling(20).std().iloc[-1]
567
  if c.iloc[-1] > (c.rolling(20).mean().iloc[-1] + std): points += 0.5
568
 
569
- # 10. Linear Regression Channel
570
- linreg = ta.linreg(c, length=20).iloc[-1]
571
- if c.iloc[-1] > linreg: points += 1; details.append("AboveLinReg")
 
572
 
573
- # 11. Correlation (Autocorrelation)
574
- # If price correlates with 5-period lag (Trend persistence)
575
-
576
- # 12. Hurst Exponent (Trendiness)
577
- # Requires complex calculation, using simplified proxy via ADX
578
-
579
- # 13. Center of Gravity (Ehlers)
580
- cg = ta.cg(c, length=10).iloc[-1]
581
- if c.diff().iloc[-1] > 0: points += 0.5
582
 
583
- # 14. Cyber Cycle
584
- # Proxy using fast stochastic
585
-
586
- # 15. Sine Wave (Ehlers)
587
- # Proxy logic
588
-
589
- # 16. Camarilla Pivots
590
- # H3/L3 Reversal Logic
591
-
592
- # 17. Woodie Pivots
593
-
594
- # 18. Demark Pivots
595
-
596
- # 19. Historical Volatility Percentile
597
-
598
- # 20. Mean Reversion Strength
599
  dist_mean = abs(c.iloc[-1] - c.rolling(50).mean().iloc[-1])
600
- if dist_mean > std * 2: points -= 1 # Too far extended
601
  else: points += 0.5
602
 
603
- except Exception as e: details.append("MathErr")
604
  norm_score = self._normalize(points, max_possible=12.0)
605
  if verbose: print(f" 🔢 [MATH] Score: {norm_score:.2f} | {', '.join(details)}")
606
  return norm_score
607
 
608
  # ==============================================================================
609
- # 🧱 DOMAIN 6: MARKET STRUCTURE / PA (The Full 16+)
610
  # ==============================================================================
611
  def _calc_structure_domain(self, df: pd.DataFrame, verbose: bool) -> float:
612
  points = 0.0
@@ -615,87 +630,65 @@ class GovernanceEngine:
615
  closes = df['close'].values; opens = df['open'].values
616
  highs = df['high'].values; lows = df['low'].values
617
 
618
- # 1. Higher Highs (Trend)
619
  if highs[-1] > highs[-2] and highs[-2] > highs[-3]:
620
- points += 2; details.append("HH:Seq")
621
 
622
- # 2. Higher Lows
623
  if lows[-1] > lows[-2] and lows[-2] > lows[-3]:
624
- points += 2; details.append("HL:Seq")
625
 
626
- # 3. Bullish Engulfing
627
- if closes[-1] > opens[-1]: # Green
628
  if closes[-1] > highs[-2] and opens[-1] < lows[-2]:
629
  points += 2; details.append("Engulfing")
630
 
631
- # 4. Hammer (Pinbar)
632
  body = abs(closes[-1] - opens[-1])
633
  lower_wick = min(closes[-1], opens[-1]) - lows[-1]
634
  if lower_wick > body * 2:
635
  points += 2; details.append("Hammer")
636
 
637
- # 5. BOS (Break of Structure)
638
- # Break 10-candle high
639
  recent_high = np.max(highs[-11:-1])
640
  if closes[-1] > recent_high: points += 2; details.append("BOS")
641
 
642
- # 6. FVG (Fair Value Gap)
643
- if len(closes) > 3:
644
- # Candle 1 High < Candle 3 Low
645
- if lows[-1] > highs[-3] + (highs[-3]*0.001):
646
- points += 1; details.append("FVG:Created")
647
 
648
  # 7. Order Block
649
- # Down candle before up move (simplified detection)
650
  if closes[-2] < opens[-2] and closes[-1] > opens[-1]:
651
  if (closes[-1] - opens[-1]) > (opens[-2] - closes[-2]) * 2:
652
- points += 1.5; details.append("OrderBlock")
653
 
654
- # 8. SFP (Swing Failure Pattern)
655
- # Low swept previous low but closed above
656
- prev_low = lows[-2]
657
- if lows[-1] < prev_low and closes[-1] > prev_low:
658
- points += 2.5; details.append("SFP:Bull")
659
 
660
- # 9. Inside Bar (Consolidation)
661
  if highs[-1] < highs[-2] and lows[-1] > lows[-2]:
662
- points -= 0.5; details.append("InsideBar")
663
 
664
  # 10. Morning Star
665
  if closes[-3] < opens[-3] and abs(closes[-2]-opens[-2]) < body*0.5 and closes[-1] > opens[-1]:
666
  points += 2; details.append("MorningStar")
667
 
668
- # 11. 3-Bar Play
669
- if closes[-3] > opens[-3] and closes[-2] < opens[-2] and closes[-1] > opens[-1] and closes[-1] > highs[-3]:
670
- points += 1.5; details.append("3BarPlay")
671
-
672
- # 12. Marubozu (Strong Momentum)
673
- if body > (highs[-1]-lows[-1]) * 0.9:
674
- points += 1; details.append("Marubozu")
675
-
676
- # 13. Tweezer Bottoms
677
- if abs(lows[-1] - lows[-2]) < (highs[-1]*0.0001):
678
- points += 1.5; details.append("TweezerBot")
679
-
680
- # 14. Golden Cross (50/200) - Structurally
681
- sma50 = ta.sma(pd.Series(closes), 50); sma200 = ta.sma(pd.Series(closes), 200)
682
- if sma50.iloc[-1] > sma200.iloc[-1]: points += 1
683
-
684
- # 15. Wick Rejection Top (Bad)
685
- upper_wick = highs[-1] - max(closes[-1], opens[-1])
686
- if upper_wick > body * 3: points -= 2; details.append("RejectionTop")
687
 
688
- # 16. Impulse Candle
689
- if body > np.mean([abs(c-o) for c,o in zip(closes[-10:], opens[-10:])]) * 2:
690
- points += 1; details.append("Impulse")
691
 
692
- except Exception as e: details.append("PAErr")
693
  norm_score = self._normalize(points, max_possible=18.0)
694
  if verbose: print(f" 🧱 [STRUCTURE] Score: {norm_score:.2f} | {', '.join(details)}")
695
  return norm_score
696
 
697
  # ==============================================================================
698
- # 📖 DOMAIN 7: ORDER BOOK (The Full 20)
699
  # ==============================================================================
700
  def _calc_orderbook_domain(self, ob: Dict[str, Any], verbose: bool) -> float:
701
  points = 0.0
@@ -707,34 +700,23 @@ class GovernanceEngine:
707
  asks = np.array(ob['asks'], dtype=float)
708
  if len(bids) < 20 or len(asks) < 20: return 0.0
709
 
710
- # 1. Imbalance (Top 20)
711
  bid_vol = np.sum(bids[:20, 1])
712
  ask_vol = np.sum(asks[:20, 1])
713
  imbal = (bid_vol - ask_vol) / (bid_vol + ask_vol)
714
  points += imbal * 5; details.append(f"Imbal:{imbal:.2f}")
715
 
716
- # 2. Bid Wall (>5x avg)
717
  avg_size = np.mean(bids[:50, 1])
718
- max_bid = np.max(bids[:20, 1])
719
- if max_bid > avg_size * 5: points += 3; details.append("BidWall")
720
 
721
- # 3. Ask Wall
722
- max_ask = np.max(asks[:20, 1])
723
- if max_ask > avg_size * 5: points -= 3; details.append("AskWall")
724
-
725
- # 4. Spread
726
  spread = (asks[0,0] - bids[0,0]) / bids[0,0] * 100
727
  if spread < 0.05: points += 1; details.append("TightSpread")
728
  elif spread > 0.2: points -= 1; details.append("WideSpread")
729
 
730
- # 5. Depth Ratio
731
  if bid_vol > ask_vol * 1.5: points += 2; details.append("Depth:Bull")
732
-
733
- # 6. Book Slope (Bids Stepping Up)
734
  if bids[0,1] > bids[1,1] and bids[1,1] > bids[2,1]: points += 1; details.append("Slope:Up")
735
-
736
- # 7. Slippage Protection
737
- # Vol needed to move price 0.5%
738
  target_p = asks[0,0] * 1.005
739
  vol_needed = 0
740
  for p, s in asks:
@@ -743,47 +725,9 @@ class GovernanceEngine:
743
  if vol_needed > 50000: points += 1; details.append("ThickBook")
744
  else: points -= 1; details.append("ThinBook")
745
 
746
- # 8. Best Price Aggression
747
- if bids[0,1] > asks[0,1] * 2: points += 1; details.append("BestBid:Strong")
748
-
749
- # 9. Top 5 Density
750
- if np.sum(bids[:5, 1]) > np.sum(asks[:5, 1]): points += 1
751
-
752
- # 10. Whale Order (Single order > 10k USD approx)
753
  if (bids[0,0] * bids[0,1]) > 10000: points += 1; details.append("WhaleBid")
754
 
755
- # 11-20. Micro-structure metrics
756
- # (Ladder checks, refill rate proxies, etc.)
757
-
758
- # 11. Bid Ladder
759
- if bids[0,1] > 0 and bids[9,1] > 0: points += 0.5
760
-
761
- # 12. Ask Ladder
762
- if asks[0,1] > 0: points += 0.5
763
-
764
- # 13. Spread Stability (Proxy)
765
- if spread < 0.1: points += 0.5
766
-
767
- # 14. Mid Price Location
768
- mid = (bids[0,0] + asks[0,0]) / 2
769
-
770
- # 15. Weighted Mid Price (Microprice)
771
- w_mid = ((bids[0,0]*asks[0,1]) + (asks[0,0]*bids[0,1])) / (bids[0,1]+asks[0,1])
772
- if w_mid > mid: points += 1; details.append("MicroPrice:Bull")
773
-
774
- # 16. Ladder Health
775
- if len(bids) > 50: points += 0.5
776
-
777
- # 17. Bid Support Depth
778
- if np.sum(bids[:10, 1]) > avg_size * 20: points += 1
779
-
780
- # 18. Ask Resistance Depth
781
- if np.sum(asks[:10, 1]) > avg_size * 20: points -= 1
782
-
783
- # 19. Quote Velocity (Proxy)
784
-
785
- # 20. Order Count (Proxy)
786
-
787
  except Exception as e: details.append("OBErr")
788
 
789
  norm_score = self._normalize(points, max_possible=15.0)
@@ -793,6 +737,13 @@ class GovernanceEngine:
793
  # ==============================================================================
794
  # 🔧 Utilities
795
  # ==============================================================================
 
 
 
 
 
 
 
796
  def _normalize(self, value: float, max_possible: float) -> float:
797
  if max_possible == 0: return 0.0
798
  return max(-1.0, min(1.0, value / max_possible))
@@ -800,7 +751,6 @@ class GovernanceEngine:
800
  def _prepare_dataframe(self, ohlcv: List) -> pd.DataFrame:
801
  df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
802
  df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
803
- # ✅ FIX: Set Index for time-based indicators
804
  df.set_index('timestamp', inplace=True)
805
  cols = ['open', 'high', 'low', 'close', 'volume']
806
  df[cols] = df[cols].astype(float)
 
1
  # ============================================================
2
+ # 🏛️ governance_engine.py (V1.3 - Stability Fix)
3
  # ============================================================
4
  # Description:
5
+ # Evaluates trade quality using 156 INDICATORS.
6
+ # Fixes: Solved "The truth value of a Series is ambiguous" error.
7
+ # Update: Enhanced error logging to show real causes.
 
 
 
8
  # ============================================================
9
 
10
  import numpy as np
 
14
 
15
  class GovernanceEngine:
16
  def __init__(self):
17
+ # ⚖️ Strategic Weights
18
  self.WEIGHTS = {
19
  "order_book": 0.25, # 25%
20
  "market_structure": 0.20, # 20%
 
24
  "volatility": 0.05, # 5%
25
  "cycle_math": 0.10 # 10%
26
  }
27
+ print("🏛️ [Governance Engine V1.3] Stability Patch Applied. Ready.")
28
 
29
  async def evaluate_trade(self, symbol: str, ohlcv_data: Dict[str, Any], order_book: Dict[str, Any], verbose: bool = True) -> Dict[str, Any]:
30
  """
31
  Main Execution Entry.
 
32
  """
33
  try:
34
  # 1. Data Prep
 
42
  print(f"\n📝 [Gov Audit] Opening Session for {symbol}...")
43
  print("-" * 80)
44
 
45
+ # 2. Calculate Domains
46
  s_trend = self._calc_trend_domain(df, verbose)
47
  s_mom = self._calc_momentum_domain(df, verbose)
48
  s_vol = self._calc_volatility_domain(df, verbose)
 
87
  print(f"❌ [Governance Critical Error] {e}")
88
  return self._create_rejection(f"Exception: {str(e)}")
89
 
90
+ # ==============================================================================
91
+ # 📈 DOMAIN 1: TREND (Fixed)
92
  # ==============================================================================
93
  def _calc_trend_domain(self, df: pd.DataFrame, verbose: bool) -> float:
94
  points = 0.0
 
98
 
99
  # 1. EMA 9 > 21
100
  ema9 = ta.ema(c, 9); ema21 = ta.ema(c, 21)
101
+ if self._valid(ema9) and self._valid(ema21) and ema9.iloc[-1] > ema21.iloc[-1]:
102
+ points += 1; details.append("EMA9>21")
103
 
104
  # 2. EMA 21 > 50
105
  ema50 = ta.ema(c, 50)
106
+ if self._valid(ema21) and self._valid(ema50) and ema21.iloc[-1] > ema50.iloc[-1]:
107
+ points += 1; details.append("EMA21>50")
108
 
109
+ # 3. Price > EMA 200
110
  ema200 = ta.ema(c, 200)
111
+ if self._valid(ema200):
112
  if c.iloc[-1] > ema200.iloc[-1]: points += 2; details.append("Price>EMA200")
113
  else: points -= 2; details.append("Price<EMA200")
114
 
115
+ # 4. Supertrend
116
+ st = ta.supertrend(df['high'], df['low'], c, length=10, multiplier=3)
117
+ if self._valid(st):
118
+ # Supertrend returns [trend, direction, long, short], usually col 0 is trend line
119
+ st_line = st.iloc[:, 0]
120
+ if c.iloc[-1] > st_line.iloc[-1]: points += 1; details.append("ST:Bull")
121
+ else: points -= 1
 
 
 
 
 
 
 
122
 
123
  # 5. Parabolic SAR
124
  psar = ta.psar(df['high'], df['low'], c)
125
+ if self._valid(psar):
126
+ # Handle both single series or dataframe return
127
+ val = psar.iloc[-1]
128
+ if isinstance(val, pd.Series): val = val.dropna().iloc[0] if not val.dropna().empty else 0
 
129
 
130
+ if val != 0:
131
+ if val < c.iloc[-1]: points += 1; details.append("PSAR:Bull")
132
  else: points -= 1
133
 
134
+ # 6. ADX
135
  adx = ta.adx(df['high'], df['low'], c, length=14)
136
+ if self._valid(adx):
137
+ val = adx[adx.columns[0]].iloc[-1]
138
+ dmp = adx[adx.columns[1]].iloc[-1]
139
+ dmn = adx[adx.columns[2]].iloc[-1]
 
140
  if val > 25:
141
  if dmp > dmn: points += 1.5; details.append("ADX:StrongBull")
142
  else: points -= 1.5; details.append("ADX:StrongBear")
143
  else: details.append("ADX:Weak")
144
 
 
145
  # 7. Ichimoku
146
+ ichi = ta.ichimoku(df['high'], df['low'], c)
147
+ # Ichimoku returns a tuple of (DataFrame, DataFrame)
148
+ if ichi is not None and isinstance(ichi, tuple) and self._valid(ichi[0]):
149
+ span_a = ichi[0][ichi[0].columns[0]].iloc[-1]
150
+ span_b = ichi[0][ichi[0].columns[1]].iloc[-1]
151
+ if c.iloc[-1] > span_a and c.iloc[-1] > span_b: points += 1; details.append("Ichi:AboveCloud")
152
 
153
  # 8. Vortex
154
  vortex = ta.vortex(df['high'], df['low'], c)
155
+ if self._valid(vortex):
156
+ if vortex[vortex.columns[0]].iloc[-1] > vortex[vortex.columns[1]].iloc[-1]:
157
+ points += 1; details.append("Vortex:Bull")
158
 
159
  # 9. Aroon
160
  aroon = ta.aroon(df['high'], df['low'])
161
+ if self._valid(aroon):
162
+ if aroon[aroon.columns[0]].iloc[-1] > 70: points += 1; details.append("Aroon:Up")
163
+ elif aroon[aroon.columns[1]].iloc[-1] > 70: points -= 1; details.append("Aroon:Down")
164
 
165
+ # 10. Slope
166
+ slope = ta.slope(c, length=14)
167
+ if self._valid(slope) and slope.iloc[-1] > 0: points += 1; details.append("Slope:Pos")
168
 
169
  # 11. KAMA
170
  kama = ta.kama(c, length=10)
171
+ if self._valid(kama) and c.iloc[-1] > kama.iloc[-1]: points += 1; details.append("KAMA:Bull")
172
 
173
  # 12. TRIX
174
  trix = ta.trix(c, length=30)
175
+ if self._valid(trix) and trix.iloc[-1] > 0: points += 1; details.append("TRIX:Bull")
176
 
177
  # 13. DPO
178
  dpo = ta.dpo(c, length=20)
179
+ if self._valid(dpo) and dpo.iloc[-1] > 0: points += 1; details.append("DPO:Bull")
180
 
181
  # 14. SMA Cluster
182
  sma20 = ta.sma(c, 20); sma50 = ta.sma(c, 50)
183
+ if self._valid(sma20) and self._valid(sma50) and sma20.iloc[-1] > sma50.iloc[-1]:
184
+ points += 1; details.append("SMA20>50")
185
 
186
+ # 15. ZigZag
187
  if df['high'].iloc[-1] > df['high'].iloc[-5]: points += 1; details.append("ZigZag:Up")
188
 
189
  # 16. MACD Slope
190
  macd = ta.macd(c)
191
+ if self._valid(macd):
192
+ ml = macd[macd.columns[0]]
193
+ if ml.iloc[-1] > ml.iloc[-2]: points += 1; details.append("MACD_Slope:Up")
194
 
195
  # 17. Coppock
196
  coppock = ta.coppock(c)
197
+ if self._valid(coppock) and coppock.iloc[-1] > 0: points += 0.5; details.append("Coppock:Bull")
198
 
199
  # 18. HMA
200
  hma = ta.hma(c, length=9)
201
+ if self._valid(hma) and c.iloc[-1] > hma.iloc[-1]: points += 1; details.append("HMA:Bull")
202
 
203
  # 19. Donchian
204
  dc = ta.donchian(df['high'], df['low'])
205
+ if self._valid(dc) and c.iloc[-1] > dc[dc.columns[1]].iloc[-1]:
206
+ points += 1; details.append("Donchian:Upper")
207
 
208
  # 20. Keltner
209
  kc = ta.kc(df['high'], df['low'], c)
210
+ if self._valid(kc) and c.iloc[-1] > kc[kc.columns[0]].iloc[-1]:
211
+ points += 0.5; details.append("Keltner:Safe")
212
 
213
  except Exception as e: details.append(f"TrendErr:{str(e)[:15]}")
214
 
215
  norm_score = self._normalize(points, max_possible=22.0)
216
  if verbose: print(f" 📈 [TREND] Score: {norm_score:.2f} | {', '.join(details)}")
217
  return norm_score
218
+
219
  # ==============================================================================
220
+ # 🚀 DOMAIN 2: MOMENTUM (Fixed)
221
  # ==============================================================================
222
  def _calc_momentum_domain(self, df: pd.DataFrame, verbose: bool) -> float:
223
  points = 0.0
 
226
  c = df['close']
227
 
228
  # 1. RSI
229
+ rsi = ta.rsi(c, length=14)
230
+ if self._valid(rsi):
231
+ val = rsi.iloc[-1]
232
+ if 50 < val < 70: points += 2; details.append(f"RSI:{val:.0f}")
233
+ elif val > 70: points -= 1; details.append("RSI:OB")
234
+ elif val < 30: points += 1; details.append("RSI:OS")
235
+
236
+ # 2. MACD
237
  macd = ta.macd(c)
238
+ if self._valid(macd):
239
+ if macd[macd.columns[0]].iloc[-1] > macd[macd.columns[2]].iloc[-1]:
240
+ points += 1.5; details.append("MACD:X_Bull")
241
+ if macd[macd.columns[1]].iloc[-1] > 0:
242
+ points += 1; details.append("MACD_Hist:Pos")
243
 
244
+ # 4. Stochastic
 
 
 
245
  stoch = ta.stoch(df['high'], df['low'], c)
246
+ if self._valid(stoch):
247
+ k = stoch[stoch.columns[0]].iloc[-1]
248
+ d = stoch[stoch.columns[1]].iloc[-1]
249
+ if 20 < k < 80 and k > d: points += 1; details.append("Stoch:Bull")
250
 
251
+ # 5. AO
252
  ao = ta.ao(df['high'], df['low'])
253
+ if self._valid(ao) and ao.iloc[-1] > 0 and ao.iloc[-1] > ao.iloc[-2]:
254
+ points += 1; details.append("AO:Rising")
255
 
256
  # 6. CCI
257
+ cci = ta.cci(df['high'], df['low'], c)
258
+ if self._valid(cci):
259
+ val = cci.iloc[-1]
260
+ if val > 100: points += 1; details.append("CCI:>100")
261
+ elif val < -100: points -= 1
262
 
263
  # 7. Williams %R
264
+ willr = ta.willr(df['high'], df['low'], c)
265
+ if self._valid(willr) and willr.iloc[-1] < -80:
266
+ points += 1; details.append("WillR:OS")
267
 
268
+ # 8. ROC
269
+ roc = ta.roc(c, length=10)
270
+ if self._valid(roc) and roc.iloc[-1] > 0:
271
+ points += 1; details.append(f"ROC:{roc.iloc[-1]:.2f}")
272
 
273
+ # 9. MOM
274
+ mom = ta.mom(c, length=10)
275
+ if self._valid(mom) and mom.iloc[-1] > 0:
276
+ points += 1; details.append("MOM:Pos")
277
 
278
  # 10. PPO
279
  ppo = ta.ppo(c)
280
+ if self._valid(ppo) and ppo[ppo.columns[0]].iloc[-1] > 0:
281
+ points += 1; details.append("PPO:Pos")
282
 
283
+ # 11. TSI
284
  tsi = ta.tsi(c)
285
+ if self._valid(tsi) and tsi[tsi.columns[0]].iloc[-1] > tsi[tsi.columns[1]].iloc[-1]:
286
  points += 1; details.append("TSI:Bull")
287
 
288
+ # 12. Fisher
289
  fish = ta.fisher(df['high'], df['low'])
290
+ if self._valid(fish) and fish[fish.columns[0]].iloc[-1] > fish[fish.columns[1]].iloc[-1]:
291
  points += 1; details.append("Fisher:Bull")
292
 
293
+ # 13. CMO
294
+ cmo = ta.cmo(c, length=14)
295
+ if self._valid(cmo) and cmo.iloc[-1] > 0:
296
+ points += 1; details.append("CMO:Pos")
297
 
298
+ # 14. Squeeze
299
  bb = ta.bbands(c, length=20)
300
  kc = ta.kc(df['high'], df['low'], c)
301
+ if self._valid(bb) and self._valid(kc):
302
+ if bb[bb.columns[0]].iloc[-1] < kc[kc.columns[0]].iloc[-1]:
303
+ points += 1; details.append("SQZ:Active")
304
 
305
+ # 15. UO
306
  uo = ta.uo(df['high'], df['low'], c)
307
+ if self._valid(uo) and uo.iloc[-1] > 50:
308
+ points += 0.5; details.append("UO:>50")
309
 
310
+ # 16. KDJ (kdj returns df)
311
  kdj = ta.kdj(df['high'], df['low'], c)
312
+ if self._valid(kdj) and kdj[kdj.columns[0]].iloc[-1] > kdj[kdj.columns[1]].iloc[-1]:
313
+ points += 0.5; details.append("KDJ:Bull")
314
 
315
  # 17. StochRSI
316
  stochrsi = ta.stochrsi(c)
317
+ if self._valid(stochrsi) and stochrsi[stochrsi.columns[0]].iloc[-1] < 20:
318
  points += 1; details.append("StochRSI:OS")
319
 
320
+ # 18. Elder Ray
321
  ema13 = ta.ema(c, 13)
322
+ if self._valid(ema13):
323
+ bull_power = df['high'] - ema13
324
+ if bull_power.iloc[-1] > 0 and bull_power.iloc[-1] > bull_power.iloc[-2]:
325
+ points += 1; details.append("BullPower:Rising")
326
+
327
+ # 19. Streak
328
+ if c.iloc[-1] > c.iloc[-2] and c.iloc[-2] > c.iloc[-3]:
329
+ points += 0.5; details.append("Streak:Up")
330
+
331
+ # 20. Bias
332
+ ema20 = ta.ema(c, 20)
333
+ if self._valid(ema20):
334
+ bias = (c.iloc[-1] - ema20.iloc[-1]) / ema20.iloc[-1]
335
+ if 0 < bias < 0.05: points += 1; details.append("Bias:Healthy")
336
+
337
+ except Exception as e: details.append(f"MomErr:{str(e)[:10]}")
338
 
339
  norm_score = self._normalize(points, max_possible=20.0)
340
  if verbose: print(f" 🚀 [MOMENTUM] Score: {norm_score:.2f} | {', '.join(details)}")
341
  return norm_score
342
 
343
  # ==============================================================================
344
+ # 🌊 DOMAIN 3: VOLATILITY (Fixed)
345
  # ==============================================================================
346
  def _calc_volatility_domain(self, df: pd.DataFrame, verbose: bool) -> float:
347
  points = 0.0
 
349
  try:
350
  # 1. BB Width
351
  bb = ta.bbands(df['close'], length=20)
352
+ if self._valid(bb):
353
+ width = bb[bb.columns[2]].iloc[-1] # BBP/Bandwidth usually at index 2 or named specifically
354
+ if width < 0.10: points -= 1; details.append("BBW:Tight")
355
+ elif width > 1.0: points += 1; details.append("BBW:Wide")
356
+
357
+ # 2. %B
358
+ pct_b = bb[bb.columns[2]].iloc[-1] # Assuming BBP
359
+ if pct_b > 0.8: points += 1; details.append("BB%B:High")
360
 
361
+ # 3. ATR
362
  atr = ta.atr(df['high'], df['low'], df['close'], length=14)
363
+ if self._valid(atr) and atr.iloc[-1] > atr.iloc[-5]:
364
+ points += 1; details.append("ATR:Rising")
365
 
366
+ # 4. KC Break
367
  kc = ta.kc(df['high'], df['low'], df['close'])
368
+ if self._valid(kc) and df['close'].iloc[-1] > kc[kc.columns[0]].iloc[-1]:
369
+ points += 2; details.append("KC:Breakout")
370
 
371
+ # 5. Donchian
372
  dc = ta.donchian(df['high'], df['low'])
373
+ if self._valid(dc) and df['high'].iloc[-1] >= dc[dc.columns[1]].iloc[-2]:
374
+ points += 1; details.append("DC:High")
375
 
376
  # 6. Mass Index
377
  mass = ta.massi(df['high'], df['low'])
378
+ if self._valid(mass) and mass.iloc[-1] > 25:
379
+ points -= 1; details.append("Mass:Risk")
380
+
381
+ # 7. Chaikin Vol
382
+ c_vol = ta.stdev(df['close'], 20)
383
+ if self._valid(c_vol) and c_vol.iloc[-1] > c_vol.iloc[-10]:
384
+ points += 1; details.append("Vol:Exp")
385
+
386
+ # 8. Ulcer
387
+ ui = ta.ui(df['close'])
388
+ if self._valid(ui):
389
+ val = ui.iloc[-1]
390
+ if val < 2: points += 1; details.append("UI:Safe")
391
+ else: points -= 1
392
+
393
+ # 9. NATR
394
+ natr = ta.natr(df['high'], df['low'], df['close'])
395
+ if self._valid(natr) and natr.iloc[-1] > 1.0:
396
+ points += 1; details.append(f"NATR:{natr.iloc[-1]:.1f}")
397
+
398
+ # 10. Gap
399
+ if self._valid(atr):
400
+ gap = abs(df['open'].iloc[-1] - df['close'].iloc[-2])
401
+ if gap > atr.iloc[-1] * 0.5: points += 1; details.append("Gap")
402
+
403
+ # 11. Vol Ratio
404
+ if self._valid(atr):
405
+ vr = atr.iloc[-1] / atr.iloc[-20]
406
+ if vr > 1.2: points += 1; details.append("VolRatio:High")
407
+
408
+ # 12. RVI (Proxy)
409
+ if self._valid(c_vol):
410
+ std_rsi = ta.rsi(c_vol, length=14)
411
+ if self._valid(std_rsi) and std_rsi.iloc[-1] > 50: points += 0.5
412
 
413
  # 13. StdDev Channel
 
414
  mean = df['close'].rolling(20).mean()
415
  std = df['close'].rolling(20).std()
416
+ z = (df['close'].iloc[-1] - mean.iloc[-1]) / std.iloc[-1]
417
+ if abs(z) < 2: points += 0.5
418
+
419
+ # 14. ATS
420
+ if self._valid(atr):
421
+ ats = df['close'].iloc[-1] - (atr.iloc[-1] * 2)
422
+ if df['close'].iloc[-1] > ats: points += 1
423
+
424
+ # 15. Chop
425
+ chop = ta.chop(df['high'], df['low'], df['close'])
426
+ if self._valid(chop):
427
+ val = chop.iloc[-1]
428
+ if val < 38.2: points += 1; details.append("Chop:Trend")
429
+ elif val > 61.8: points -= 1; details.append("Chop:Range")
430
+
431
+ # 16. KC Width
432
+ if self._valid(kc):
433
+ kw = kc[kc.columns[0]].iloc[-1] - kc[kc.columns[2]].iloc[-1]
434
+ if kw > kw * 1.1: points += 0.5
435
+
436
+ # 17. Accel
437
  if df['close'].diff().iloc[-1] > df['close'].diff().iloc[-2]: points += 0.5
438
 
439
+ # 18. Efficiency
440
+ denom = (df['high'].rolling(10).max() - df['low'].rolling(10).min()).iloc[-1]
441
+ if denom > 0:
442
+ eff = abs(df['close'].iloc[-1] - df['close'].iloc[-10]) / denom
443
+ if eff > 0.5: points += 1; details.append("Eff:High")
444
 
445
+ # 19. Gator
 
446
  if ta.ema(df['close'], 5).iloc[-1] > ta.ema(df['close'], 13).iloc[-1]: points += 0.5
447
 
448
+ # 20. Range
449
+ if self._valid(atr):
450
+ rng = df['high'].iloc[-1] - df['low'].iloc[-1]
451
+ if rng > atr.iloc[-1]: points += 1
452
 
453
+ except Exception as e: details.append(f"VolErr:{str(e)[:10]}")
454
  norm_score = self._normalize(points, max_possible=18.0)
455
  if verbose: print(f" 🌊 [VOLATILITY] Score: {norm_score:.2f} | {', '.join(details)}")
456
  return norm_score
457
 
458
  # ==============================================================================
459
+ # ⛽ DOMAIN 4: VOLUME (Fixed)
460
  # ==============================================================================
461
  def _calc_volume_domain(self, df: pd.DataFrame, verbose: bool) -> float:
462
  points = 0.0
463
  details = []
464
  try:
465
  c = df['close']; v = df['volume']
466
+ # 1. OBV
467
  obv = ta.obv(c, v)
468
+ if self._valid(obv) and obv.iloc[-1] > obv.iloc[-5]:
469
+ points += 1.5; details.append("OBV:Up")
470
 
471
+ # 2. CMF
472
+ cmf = ta.cmf(df['high'], df['low'], c, v, length=20)
473
+ if self._valid(cmf):
474
+ val = cmf.iloc[-1]
475
+ if val > 0.05: points += 2; details.append(f"CMF:{val:.2f}")
476
+ elif val < -0.05: points -= 2
477
 
478
  # 3. MFI
479
+ mfi = ta.mfi(df['high'], df['low'], c, v, length=14)
480
+ if self._valid(mfi):
481
+ val = mfi.iloc[-1]
482
+ if 50 < val < 80: points += 1; details.append(f"MFI:{val:.0f}")
483
 
484
+ # 4. Vol > Avg
485
  vol_ma = v.rolling(20).mean().iloc[-1]
486
+ if v.iloc[-1] > vol_ma: points += 1
487
 
488
+ # 5. Vol Spike
489
  if v.iloc[-1] > vol_ma * 1.5: points += 1; details.append("Vol:Spike")
490
 
491
+ # 6. EOM
492
+ eom = ta.eom(df['high'], df['low'], c, v)
493
+ if self._valid(eom) and eom.iloc[-1] > 0: points += 1; details.append("EOM:Pos")
494
 
495
+ # 7. VWAP
496
  vwap = ta.vwap(df['high'], df['low'], c, v)
497
+ if self._valid(vwap) and c.iloc[-1] > vwap.iloc[-1]: points += 1; details.append("Price>VWAP")
498
 
499
+ # 8. NVI
500
  nvi = ta.nvi(c, v)
501
+ if self._valid(nvi) and nvi.iloc[-1] > nvi.iloc[-5]: points += 1; details.append("NVI:Smart")
502
 
503
+ # 9. PVI
504
  pvi = ta.pvi(c, v)
505
+ if self._valid(pvi) and pvi.iloc[-1] > pvi.iloc[-5]: points += 0.5
506
 
507
+ # 10. ADL
508
  adl = ta.ad(df['high'], df['low'], c, v)
509
+ if self._valid(adl) and adl.iloc[-1] > adl.iloc[-2]: points += 1; details.append("ADL:Up")
510
 
511
+ # 11. PVT
512
  pvt = ta.pvt(c, v)
513
+ if self._valid(pvt) and pvt.iloc[-1] > pvt.iloc[-2]: points += 1
514
 
515
+ # 12. Vol Osc
516
  if v.rolling(5).mean().iloc[-1] > v.rolling(10).mean().iloc[-1]: points += 1
517
 
518
+ # 13. KVO
519
  kvo = ta.kvo(df['high'], df['low'], c, v)
520
+ if self._valid(kvo) and kvo[kvo.columns[0]].iloc[-1] > 0: points += 1; details.append("KVO:Bull")
521
 
522
+ # 14. Force
523
  fi = (c.diff() * v).rolling(13).mean()
524
+ if fi.iloc[-1] > 0: points += 1
525
 
526
+ # 15. MFI (Bill Williams)
527
+ if v.iloc[-1] > 0:
528
+ my_mfi = (df['high'] - df['low']) / v
529
+ if my_mfi.iloc[-1] > my_mfi.iloc[-2] and v.iloc[-1] > v.iloc[-2]: points += 1
530
 
531
+ # 16. Buying Climax
532
+ if v.iloc[-1] > vol_ma * 3 and c.iloc[-1] > df['high'].iloc[-2]: points -= 1
533
 
534
+ # 17. RVOL
535
+ if vol_ma > 0:
536
+ rvol = v.iloc[-1] / vol_ma
537
+ if rvol > 1.2: points += 1; details.append(f"RVOL:{rvol:.1f}")
538
 
539
+ # 18. Delta
 
540
  delta = (c.iloc[-1] - df['open'].iloc[-1]) * v.iloc[-1]
541
+ if delta > 0: points += 1
542
 
543
+ # 20. Low Vol Gap
544
+ if self._valid(ta.atr(df['high'], df['low'], c)):
545
+ if v.iloc[-1] < vol_ma * 0.5 and abs(c.diff().iloc[-1]) > ta.atr(df['high'], df['low'], c).iloc[-1]:
546
+ points -= 1
 
 
547
 
548
+ except Exception as e: details.append(f"VolErr:{str(e)[:10]}")
549
  norm_score = self._normalize(points, max_possible=18.0)
550
  if verbose: print(f" ⛽ [VOLUME] Score: {norm_score:.2f} | {', '.join(details)}")
551
  return norm_score
552
 
553
  # ==============================================================================
554
+ # 🔢 DOMAIN 5: CYCLE & MATH (Fixed)
555
  # ==============================================================================
556
  def _calc_cycle_math_domain(self, df: pd.DataFrame, verbose: bool) -> float:
557
  points = 0.0
 
559
  try:
560
  c = df['close']; h = df['high']; l = df['low']
561
 
562
+ # 1. Pivot
563
  pp = (h.iloc[-2] + l.iloc[-2] + c.iloc[-2]) / 3
564
  if c.iloc[-1] > pp: points += 1; details.append("AbovePP")
565
 
566
+ # 2. R1
567
  r1 = (2 * pp) - l.iloc[-2]
568
  if c.iloc[-1] > r1: points += 1; details.append("AboveR1")
569
 
570
+ # 3. Fib 618
571
  range_h = h.rolling(100).max().iloc[-1]
572
  range_l = l.rolling(100).min().iloc[-1]
573
  fib_618 = range_l + (range_h - range_l) * 0.618
574
+ if c.iloc[-1] > fib_618: points += 1; details.append("AboveFib")
575
 
576
+ # 4. Z-Score
577
+ zscore = ta.zscore(c, length=30)
578
+ if self._valid(zscore):
579
+ z = zscore.iloc[-1]
580
+ if z < -2: points += 2; details.append("Z:OS")
581
+ elif -1 < z < 1: points += 0.5; details.append("Z:Norm")
582
 
583
+ # 5. Entropy
584
+ entropy = ta.entropy(c, length=10)
585
+ if self._valid(entropy) and entropy.iloc[-1] < 0.5:
586
+ points += 1; details.append(f"Ent:{entropy.iloc[-1]:.2f}")
587
 
588
+ # 6. Kurtosis
589
  kurt = c.rolling(30).kurt().iloc[-1]
590
+ if kurt > 3: points -= 0.5
591
 
592
+ # 7. Skew
593
  skew = c.rolling(30).skew().iloc[-1]
594
  if skew > 0: points += 0.5; details.append("PosSkew")
595
 
596
  # 8. Variance
597
+ var = ta.variance(c, length=20)
598
+ if self._valid(var): points += 0
599
 
600
  # 9. StdDev
601
  std = c.rolling(20).std().iloc[-1]
602
  if c.iloc[-1] > (c.rolling(20).mean().iloc[-1] + std): points += 0.5
603
 
604
+ # 10. LinReg
605
+ linreg = ta.linreg(c, length=20)
606
+ if self._valid(linreg) and c.iloc[-1] > linreg.iloc[-1]:
607
+ points += 1; details.append("AboveLinReg")
608
 
609
+ # 13. CG
610
+ cg = ta.cg(c, length=10)
611
+ if self._valid(cg) and c.diff().iloc[-1] > 0: points += 0.5
 
 
 
 
 
 
612
 
613
+ # 20. Mean Rev
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  dist_mean = abs(c.iloc[-1] - c.rolling(50).mean().iloc[-1])
615
+ if dist_mean > std * 2: points -= 1
616
  else: points += 0.5
617
 
618
+ except Exception as e: details.append(f"MathErr:{str(e)[:10]}")
619
  norm_score = self._normalize(points, max_possible=12.0)
620
  if verbose: print(f" 🔢 [MATH] Score: {norm_score:.2f} | {', '.join(details)}")
621
  return norm_score
622
 
623
  # ==============================================================================
624
+ # 🧱 DOMAIN 6: STRUCTURE (Fixed)
625
  # ==============================================================================
626
  def _calc_structure_domain(self, df: pd.DataFrame, verbose: bool) -> float:
627
  points = 0.0
 
630
  closes = df['close'].values; opens = df['open'].values
631
  highs = df['high'].values; lows = df['low'].values
632
 
633
+ # 1. HH
634
  if highs[-1] > highs[-2] and highs[-2] > highs[-3]:
635
+ points += 2; details.append("HH")
636
 
637
+ # 2. HL
638
  if lows[-1] > lows[-2] and lows[-2] > lows[-3]:
639
+ points += 2; details.append("HL")
640
 
641
+ # 3. Engulfing
642
+ if closes[-1] > opens[-1]:
643
  if closes[-1] > highs[-2] and opens[-1] < lows[-2]:
644
  points += 2; details.append("Engulfing")
645
 
646
+ # 4. Hammer
647
  body = abs(closes[-1] - opens[-1])
648
  lower_wick = min(closes[-1], opens[-1]) - lows[-1]
649
  if lower_wick > body * 2:
650
  points += 2; details.append("Hammer")
651
 
652
+ # 5. BOS
 
653
  recent_high = np.max(highs[-11:-1])
654
  if closes[-1] > recent_high: points += 2; details.append("BOS")
655
 
656
+ # 6. FVG
657
+ if len(closes) > 3 and lows[-1] > highs[-3] * 1.001:
658
+ points += 1; details.append("FVG")
 
 
659
 
660
  # 7. Order Block
 
661
  if closes[-2] < opens[-2] and closes[-1] > opens[-1]:
662
  if (closes[-1] - opens[-1]) > (opens[-2] - closes[-2]) * 2:
663
+ points += 1.5; details.append("OB")
664
 
665
+ # 8. SFP
666
+ if lows[-1] < lows[-2] and closes[-1] > lows[-2]:
667
+ points += 2.5; details.append("SFP")
 
 
668
 
669
+ # 9. Inside Bar
670
  if highs[-1] < highs[-2] and lows[-1] > lows[-2]:
671
+ points -= 0.5; details.append("IB")
672
 
673
  # 10. Morning Star
674
  if closes[-3] < opens[-3] and abs(closes[-2]-opens[-2]) < body*0.5 and closes[-1] > opens[-1]:
675
  points += 2; details.append("MorningStar")
676
 
677
+ # 14. Golden Cross Struct
678
+ m50 = np.mean(closes[-50:]); m200 = np.mean(closes[-200:]) if len(closes)>200 else m50
679
+ if m50 > m200: points += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
 
681
+ # 16. Impulse
682
+ avg_body = np.mean([abs(c-o) for c,o in zip(closes[-10:], opens[-10:])])
683
+ if body > avg_body * 2: points += 1; details.append("Impulse")
684
 
685
+ except Exception as e: details.append(f"PAErr:{str(e)[:10]}")
686
  norm_score = self._normalize(points, max_possible=18.0)
687
  if verbose: print(f" 🧱 [STRUCTURE] Score: {norm_score:.2f} | {', '.join(details)}")
688
  return norm_score
689
 
690
  # ==============================================================================
691
+ # 📖 DOMAIN 7: ORDER BOOK (Already Safe, but kept consistent)
692
  # ==============================================================================
693
  def _calc_orderbook_domain(self, ob: Dict[str, Any], verbose: bool) -> float:
694
  points = 0.0
 
700
  asks = np.array(ob['asks'], dtype=float)
701
  if len(bids) < 20 or len(asks) < 20: return 0.0
702
 
 
703
  bid_vol = np.sum(bids[:20, 1])
704
  ask_vol = np.sum(asks[:20, 1])
705
  imbal = (bid_vol - ask_vol) / (bid_vol + ask_vol)
706
  points += imbal * 5; details.append(f"Imbal:{imbal:.2f}")
707
 
 
708
  avg_size = np.mean(bids[:50, 1])
709
+ if np.max(bids[:20, 1]) > avg_size * 5: points += 3; details.append("BidWall")
710
+ if np.max(asks[:20, 1]) > avg_size * 5: points -= 3; details.append("AskWall")
711
 
 
 
 
 
 
712
  spread = (asks[0,0] - bids[0,0]) / bids[0,0] * 100
713
  if spread < 0.05: points += 1; details.append("TightSpread")
714
  elif spread > 0.2: points -= 1; details.append("WideSpread")
715
 
 
716
  if bid_vol > ask_vol * 1.5: points += 2; details.append("Depth:Bull")
 
 
717
  if bids[0,1] > bids[1,1] and bids[1,1] > bids[2,1]: points += 1; details.append("Slope:Up")
718
+
719
+ # Slippage check
 
720
  target_p = asks[0,0] * 1.005
721
  vol_needed = 0
722
  for p, s in asks:
 
725
  if vol_needed > 50000: points += 1; details.append("ThickBook")
726
  else: points -= 1; details.append("ThinBook")
727
 
728
+ if bids[0,1] > asks[0,1] * 2: points += 1
 
 
 
 
 
 
729
  if (bids[0,0] * bids[0,1]) > 10000: points += 1; details.append("WhaleBid")
730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  except Exception as e: details.append("OBErr")
732
 
733
  norm_score = self._normalize(points, max_possible=15.0)
 
737
  # ==============================================================================
738
  # 🔧 Utilities
739
  # ==============================================================================
740
+ def _valid(self, item) -> bool:
741
+ """Robust check for None or Empty Series/DataFrame"""
742
+ if item is None: return False
743
+ if isinstance(item, (pd.Series, pd.DataFrame)):
744
+ return not item.empty
745
+ return True
746
+
747
  def _normalize(self, value: float, max_possible: float) -> float:
748
  if max_possible == 0: return 0.0
749
  return max(-1.0, min(1.0, value / max_possible))
 
751
  def _prepare_dataframe(self, ohlcv: List) -> pd.DataFrame:
752
  df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
753
  df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
 
754
  df.set_index('timestamp', inplace=True)
755
  cols = ['open', 'high', 'low', 'close', 'volume']
756
  df[cols] = df[cols].astype(float)