Riy777 commited on
Commit
c180eec
·
verified ·
1 Parent(s): fdf1b31

Delete governance_engine.py

Browse files
Files changed (1) hide show
  1. governance_engine.py +0 -773
governance_engine.py DELETED
@@ -1,773 +0,0 @@
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
11
- import pandas as pd
12
- import pandas_ta as ta
13
- from typing import Dict, Any, List
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%
21
- "trend": 0.15, # 15%
22
- "momentum": 0.15, # 15%
23
- "volume": 0.10, # 10%
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
35
- if '15m' not in ohlcv_data:
36
- return self._create_rejection("No 15m Data")
37
-
38
- df = self._prepare_dataframe(ohlcv_data['15m'])
39
- if len(df) < 60: return self._create_rejection("Insufficient Data Length (<60)")
40
-
41
- if verbose:
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)
49
- s_volu = self._calc_volume_domain(df, verbose)
50
- s_cycle = self._calc_cycle_math_domain(df, verbose)
51
- s_struct = self._calc_structure_domain(df, verbose)
52
- s_ob = self._calc_orderbook_domain(order_book, verbose)
53
-
54
- if verbose: print("-" * 80)
55
-
56
- # 3. Weighted Aggregation
57
- raw_weighted_score = (
58
- (s_trend * self.WEIGHTS['trend']) +
59
- (s_mom * self.WEIGHTS['momentum']) +
60
- (s_vol * self.WEIGHTS['volatility']) +
61
- (s_volu * self.WEIGHTS['volume']) +
62
- (s_cycle * self.WEIGHTS['cycle_math']) +
63
- (s_struct * self.WEIGHTS['market_structure']) +
64
- (s_ob * self.WEIGHTS['order_book'])
65
- )
66
-
67
- # 4. Final Scoring & Grading
68
- final_score = max(0.0, min(100.0, ((raw_weighted_score + 1) / 2) * 100))
69
- grade = self._get_grade(final_score)
70
-
71
- return {
72
- "governance_score": round(final_score, 2),
73
- "grade": grade,
74
- "components": {
75
- "trend": round(s_trend, 2),
76
- "momentum": round(s_mom, 2),
77
- "volatility": round(s_vol, 2),
78
- "volume": round(s_volu, 2),
79
- "cycle_math": round(s_cycle, 2),
80
- "structure": round(s_struct, 2),
81
- "order_book": round(s_ob, 2)
82
- },
83
- "status": "APPROVED" if grade != "REJECT" else "REJECTED"
84
- }
85
-
86
- except Exception as e:
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
95
- details = []
96
- try:
97
- c = df['close']
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
224
- details = []
225
- try:
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
348
- details = []
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
558
- details = []
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
628
- details = []
629
- try:
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
695
- details = []
696
- if not ob or 'bids' not in ob or 'asks' not in ob: return 0.0
697
-
698
- try:
699
- bids = np.array(ob['bids'], dtype=float)
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:
723
- if p > target_p: break
724
- vol_needed += s
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)
734
- if verbose: print(f" 📖 [ORDERBOOK] Score: {norm_score:.2f} | {', '.join(details)}")
735
- return norm_score
736
-
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))
750
-
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)
757
- return df
758
-
759
- def _get_grade(self, score: float) -> str:
760
- if score >= 85: return "ULTRA"
761
- if score >= 70: return "STRONG"
762
- if score >= 50: return "NORMAL"
763
- if score >= 35: return "WEAK"
764
- return "REJECT"
765
-
766
- def _create_rejection(self, reason: str):
767
- return {
768
- "governance_score": 0.0,
769
- "grade": "REJECT",
770
- "status": "REJECTED",
771
- "reason": reason,
772
- "components": {}
773
- }