Riy777 commited on
Commit
93cfa3e
·
verified ·
1 Parent(s): bc01627

Update governance_engine.py

Browse files
Files changed (1) hide show
  1. governance_engine.py +63 -159
governance_engine.py CHANGED
@@ -1,10 +1,11 @@
1
  # ============================================================
2
- # 🏛️ governance_engine.py (V38.1 - GEM-Architect: Full Integrity Fix)
3
  # ============================================================
4
  # Description:
5
  # Evaluates trade quality using 156 INDICATORS.
6
- # - Full Logic: Trend, Momentum, Volatility, Volume, Math, Structure.
7
- # - Fixed: OrderBook unpacking error (OKX/Binance compatible).
 
8
  # ============================================================
9
 
10
  import numpy as np
@@ -17,7 +18,7 @@ from typing import Dict, Any, List
17
 
18
  class GovernanceEngine:
19
  def __init__(self):
20
- # ⚖️ Default Strategic Weights (For Normal/Range Operations)
21
  self.DEFAULT_WEIGHTS = {
22
  "order_book": 0.25, # 25%
23
  "market_structure": 0.20, # 20%
@@ -27,7 +28,7 @@ class GovernanceEngine:
27
  "volatility": 0.05, # 5%
28
  "cycle_math": 0.10 # 10%
29
  }
30
- print("🏛️ [Governance Engine V38.1] Context-Aware Protocols Active (Full Integrity).")
31
 
32
  async def evaluate_trade(
33
  self,
@@ -41,7 +42,7 @@ class GovernanceEngine:
41
  ) -> Dict[str, Any]:
42
  """
43
  Main Execution Entry.
44
- Now adapts weights based on 'strategy_type' (SAFE_BOTTOM vs MOMENTUM_LAUNCH).
45
  """
46
  try:
47
  if ta is None:
@@ -63,7 +64,6 @@ class GovernanceEngine:
63
  if df15 is None:
64
  return self._create_rejection("Insufficient Data Length (<60)")
65
 
66
- # optional timeframes (only used when enabled)
67
  df_map: Dict[str, pd.DataFrame] = {'15m': df15}
68
  if use_multi_timeframes:
69
  for tf in ('1h', '4h', '1d'):
@@ -76,7 +76,7 @@ class GovernanceEngine:
76
  print("-" * 80)
77
 
78
  # 2) Calculate Domains
79
- details_pack = {} # only filled when include_details=True
80
 
81
  if not use_multi_timeframes:
82
  s_trend = self._calc_trend_domain(df15, verbose, include_details, details_pack)
@@ -86,7 +86,6 @@ class GovernanceEngine:
86
  s_cycle = self._calc_cycle_math_domain(df15, verbose, include_details, details_pack)
87
  s_struct = self._calc_structure_domain(df15, verbose, include_details, details_pack)
88
  else:
89
- # Weighted by timeframe importance; only timeframes available are used
90
  tfw = {'15m': 0.50, '1h': 0.30, '4h': 0.20, '1d': 0.10}
91
 
92
  def _agg(fn, name: str) -> float:
@@ -95,7 +94,7 @@ class GovernanceEngine:
95
  per_tf = {}
96
  for tf, df_tf in df_map.items():
97
  w = tfw.get(tf, 0.1)
98
- s = fn(df_tf, False, include_details, details_pack) # per-tf verbose off to avoid noise
99
  per_tf[tf] = float(s)
100
  acc += w * float(s)
101
  total_w += w
@@ -124,56 +123,31 @@ class GovernanceEngine:
124
  current_weights = self.DEFAULT_WEIGHTS.copy()
125
 
126
  if strategy_type == 'SAFE_BOTTOM':
127
- # للقاع: نغفر ضعف الترند، ونركز على الرياضيات (الانحراف) والتقلبات والبنية
128
  current_weights = {
129
  "order_book": 0.20,
130
- "market_structure": 0.20, # Hammer/Support important
131
- "trend": 0.05, # Trend is likely negative, ignore it mostly
132
- "momentum": 0.15, # Divergence matters
133
  "volume": 0.10,
134
- "volatility": 0.15, # Exhaustion/BB Squeeze
135
- "cycle_math": 0.15 # Mean Reversion / Z-Score
136
  }
137
  elif strategy_type == 'MOMENTUM_LAUNCH':
138
- # للانطلاق: الترند والزخم ودفتر الطلبات هم الملوك
139
  current_weights = {
140
- "order_book": 0.25, # Walls needed to push
141
  "market_structure": 0.15,
142
- "trend": 0.25, # MUST be uptrending
143
- "momentum": 0.20, # High RSI is good here
144
- "volume": 0.10, # Volume backing the move
145
  "volatility": 0.05,
146
- "cycle_math": 0.00 # Less relevant for breakout
147
  }
148
 
149
  # ============================================================
150
- # 🛑 1. STRICT CONSENSUS CHECK (Veto Power)
151
- # All domains must be non-negative (>= 0).
152
- # Exception: For SAFE_BOTTOM, we tolerate negative Trend if other metrics are strong.
153
  # ============================================================
154
- domain_scores = {
155
- "Trend": s_trend,
156
- "Momentum": s_mom,
157
- "Volatility": s_vol,
158
- "Volume": s_volu,
159
- "Math": s_cycle,
160
- "Structure": s_struct,
161
- "OrderBook": s_ob
162
- }
163
-
164
- veto_domains = []
165
- for name, score in domain_scores.items():
166
- if score < 0:
167
- # Special Exemption for Bottom Fishing
168
- if strategy_type == 'SAFE_BOTTOM' and name == 'Trend':
169
- continue
170
- veto_domains.append(name)
171
-
172
- if veto_domains:
173
- reason = f"Vetoed by negative domains: {', '.join(veto_domains)}"
174
- if verbose:
175
- print(f"⛔ [Governance VETO] {reason}")
176
- return self._create_rejection(reason)
177
 
178
  # 3) Weighted Aggregation using DYNAMIC weights
179
  raw_weighted_score = (
@@ -187,10 +161,11 @@ class GovernanceEngine:
187
  )
188
 
189
  # 4) Final Scoring & Grading
 
190
  final_score = max(0.0, min(100.0, ((raw_weighted_score + 1) / 2) * 100))
191
 
192
  # ============================================================
193
- # 🛑 2. SCORE THRESHOLD CHECK (> 50%)
194
  # ============================================================
195
  if final_score <= 50.0:
196
  if verbose:
@@ -199,6 +174,9 @@ class GovernanceEngine:
199
 
200
  grade = self._get_grade(final_score)
201
 
 
 
 
202
  result = {
203
  "governance_score": round(final_score, 2),
204
  "grade": grade,
@@ -254,7 +232,6 @@ class GovernanceEngine:
254
  # 4. Supertrend
255
  st = ta.supertrend(df['high'], df['low'], c, length=10, multiplier=3)
256
  if self._valid(st):
257
- # Supertrend returns [trend, direction, long, short], usually col 0 is trend line
258
  st_line = st.iloc[:, 0]
259
  if c.iloc[-1] > st_line.iloc[-1]: points += 1; details.append("ST:Bull")
260
  else: points -= 1
@@ -262,10 +239,8 @@ class GovernanceEngine:
262
  # 5. Parabolic SAR
263
  psar = ta.psar(df['high'], df['low'], c)
264
  if self._valid(psar):
265
- # Handle both single series or dataframe return
266
  val = psar.iloc[-1]
267
  if isinstance(val, pd.Series): val = val.dropna().iloc[0] if not val.dropna().empty else 0
268
-
269
  if val != 0:
270
  if val < c.iloc[-1]: points += 1; details.append("PSAR:Bull")
271
  else: points -= 1
@@ -283,7 +258,6 @@ class GovernanceEngine:
283
 
284
  # 7. Ichimoku
285
  ichi = ta.ichimoku(df['high'], df['low'], c)
286
- # Ichimoku returns a tuple of (DataFrame, DataFrame)
287
  if ichi is not None and isinstance(ichi, tuple) and self._valid(ichi[0]):
288
  span_a = ichi[0][ichi[0].columns[0]].iloc[-1]
289
  span_b = ichi[0][ichi[0].columns[1]].iloc[-1]
@@ -449,7 +423,7 @@ class GovernanceEngine:
449
  if self._valid(uo) and uo.iloc[-1] > 50:
450
  points += 0.5; details.append("UO:>50")
451
 
452
- # 16. KDJ (kdj returns df)
453
  kdj = ta.kdj(df['high'], df['low'], c)
454
  if self._valid(kdj) and kdj[kdj.columns[0]].iloc[-1] > kdj[kdj.columns[1]].iloc[-1]:
455
  points += 0.5; details.append("KDJ:Bull")
@@ -491,29 +465,21 @@ class GovernanceEngine:
491
  points = 0.0
492
  details = []
493
  try:
494
- # 1. Bollinger Bands (Bandwidth + %B)
495
  bb = ta.bbands(df['close'], length=20)
496
  if self._valid(bb):
497
- # pandas_ta names usually: BBL_, BBM_, BBU_, BBB_ (bandwidth), BBP_ (%B)
498
  bw_col = self._find_col(bb, ["bbb_", "bandwidth", "bbw"])
499
  pb_col = self._find_col(bb, ["bbp_", "%b", "percentb", "pb"])
500
  width = self._safe_last(bb, col=bw_col) if bw_col else np.nan
501
  pct_b = self._safe_last(bb, col=pb_col) if pb_col else np.nan
502
 
503
- # Bandwidth: smaller -> squeeze, larger -> expansion
504
- # Typical BBB values ~ 0.02 - 0.25 in many markets (depends on volatility)
505
  if np.isfinite(width):
506
- if width < 0.05:
507
- points -= 1; details.append("BBW:Squeeze")
508
- elif width > 0.18:
509
- points += 1; details.append("BBW:Expand")
510
 
511
- # %B: location within bands (0..1 typically)
512
  if np.isfinite(pct_b):
513
- if pct_b > 0.90:
514
- points += 0.5; details.append("BB%B:High")
515
- elif pct_b < 0.10:
516
- points -= 0.5; details.append("BB%B:Low")
517
 
518
  # 3. ATR
519
  atr = ta.atr(df['high'], df['low'], df['close'], length=14)
@@ -566,7 +532,7 @@ class GovernanceEngine:
566
  vr = atr.iloc[-1] / atr.iloc[-20]
567
  if vr > 1.2: points += 1; details.append("VolRatio:High")
568
 
569
- # 12. RVI (Proxy)
570
  if self._valid(c_vol):
571
  std_rsi = ta.rsi(c_vol, length=14)
572
  if self._valid(std_rsi) and std_rsi.iloc[-1] > 50: points += 0.5
@@ -857,7 +823,7 @@ class GovernanceEngine:
857
  return norm_score
858
 
859
  # ==============================================================================
860
- # 📖 DOMAIN 7: ORDER BOOK (FIXED & SAFE)
861
  # ==============================================================================
862
  def _calc_orderbook_domain(self, ob: Dict[str, Any], verbose: bool, include_details: bool = False, details_pack: Any = None) -> float:
863
  points = 0.0
@@ -865,13 +831,12 @@ class GovernanceEngine:
865
  if not ob or 'bids' not in ob or 'asks' not in ob: return 0.0
866
 
867
  try:
868
- # ✅ Safe Numpy Conversion (Fixes OKX Unpacking)
869
  bids = np.array(ob['bids'], dtype=float)
870
  asks = np.array(ob['asks'], dtype=float)
871
 
872
  if len(bids) < 20 or len(asks) < 20: return 0.0
873
 
874
- # Safe Slicing (Ignores extra columns if present)
875
  bid_vol = np.sum(bids[:20, 1])
876
  ask_vol = np.sum(asks[:20, 1])
877
  imbal = (bid_vol - ask_vol) / (bid_vol + ask_vol)
@@ -887,12 +852,11 @@ class GovernanceEngine:
887
 
888
  if bid_vol > ask_vol * 1.5: points += 2; details.append("Depth:Bull")
889
 
890
- # Slippage / depth-to-move
891
  mid = (asks[0, 0] + bids[0, 0]) / 2.0
892
- target_p = mid * 1.005 # ~0.5% up move
893
  vol_needed = 0.0
894
 
895
- # ✅ Safe Iteration (Fixes Unpacking Error)
896
  for row in asks:
897
  p = row[0]
898
  s = row[1]
@@ -931,109 +895,49 @@ class GovernanceEngine:
931
  # 🔧 Utilities
932
  # ==============================================================================
933
  def _valid(self, item, col: Any = None) -> bool:
934
- """Return True if item has a finite last value (Series) or at least one finite last-row value (DataFrame).
935
- If col is provided and item is a DataFrame, checks that column's last value.
936
- """
937
- if item is None:
938
- return False
939
-
940
- # pandas_ta sometimes returns tuples (e.g., ichimoku)
941
- if isinstance(item, tuple):
942
- # consider valid if any element is valid
943
- return any(self._valid(x, col=col) for x in item)
944
-
945
  try:
946
- if isinstance(item, pd.Series):
947
- if item.empty:
948
- return False
949
- v = item.iloc[-1]
950
- return pd.notna(v) and np.isfinite(v)
951
-
952
  if isinstance(item, pd.DataFrame):
953
- if item.empty:
954
- return False
955
- if col is not None:
956
- c = self._find_col(item, [col]) or (col if col in item.columns else None)
957
- if c is None:
958
- return False
959
- v = item[c].iloc[-1]
960
- return pd.notna(v) and np.isfinite(v)
961
- # any finite in last row
962
- last = item.iloc[-1]
963
- if isinstance(last, pd.Series):
964
- vals = last.values.astype(float, copy=False)
965
- return np.isfinite(vals).any()
966
- return False
967
-
968
- # scalars
969
- if isinstance(item, (int, float, np.number)):
970
- return np.isfinite(item)
971
  return True
 
972
 
973
- except Exception:
974
- return False
975
-
976
- def _find_col(self, df: pd.DataFrame, contains_any: List[str]) -> Any:
977
- """Find first column whose name contains any of the provided substrings (case-insensitive)."""
978
- if df is None or getattr(df, "empty", True):
979
- return None
980
- cols = list(df.columns)
981
- lowered = [str(c).lower() for c in cols]
982
- needles = [s.lower() for s in contains_any]
983
- for n in needles:
984
- for c, lc in zip(cols, lowered):
985
- if n in lc:
986
- return c
987
  return None
988
 
989
- def _safe_last(self, item, default=np.nan, col: Any = None) -> float:
990
- """Safely get last finite value from Series/DataFrame (optionally from matched column)."""
991
- if not self._valid(item, col=col):
992
- return float(default)
993
  try:
994
- if isinstance(item, pd.Series):
995
- return float(item.iloc[-1])
996
  if isinstance(item, pd.DataFrame):
997
- if col is None:
998
- # pick first finite value in last row
999
- last = item.iloc[-1]
1000
- for v in last.values:
1001
- if pd.notna(v) and np.isfinite(v):
1002
- return float(v)
1003
- return float(default)
1004
- c = self._find_col(item, [col]) or (col if col in item.columns else None)
1005
- if c is None:
1006
- return float(default)
1007
- return float(item[c].iloc[-1])
1008
- if isinstance(item, (int, float, np.number)):
1009
- return float(item)
1010
- return float(default)
1011
- except Exception:
1012
- return float(default)
1013
-
1014
- def _normalize(self, value: float, max_possible: float) -> float:
1015
  if max_possible == 0: return 0.0
1016
  return max(-1.0, min(1.0, value / max_possible))
1017
 
1018
- def _prepare_dataframe(self, ohlcv: List) -> pd.DataFrame:
1019
  df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
1020
  df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
1021
  df.set_index('timestamp', inplace=True)
1022
- cols = ['open', 'high', 'low', 'close', 'volume']
1023
- df[cols] = df[cols].astype(float)
1024
  return df
1025
 
1026
- def _get_grade(self, score: float) -> str:
1027
  if score >= 85: return "ULTRA"
1028
  if score >= 70: return "STRONG"
1029
  if score > 50: return "NORMAL"
1030
  return "REJECT"
1031
 
1032
- def _create_rejection(self, reason: str):
1033
- return {
1034
- "governance_score": 0.0,
1035
- "grade": "REJECT",
1036
- "status": "REJECTED",
1037
- "reason": reason,
1038
- "components": {}
1039
- }
 
1
  # ============================================================
2
+ # 🏛️ governance_engine.py (V39.0 - GEM-Architect: Score-Based Logic)
3
  # ============================================================
4
  # Description:
5
  # Evaluates trade quality using 156 INDICATORS.
6
+ # - UPDATE V39.0: REMOVED Strict Domain Veto.
7
+ # - Logic: Approval is now purely based on Weighted Score > 50%.
8
+ # - Safety: OKX/Binance Multi-Exchange Safe (Fixed Unpacking).
9
  # ============================================================
10
 
11
  import numpy as np
 
18
 
19
  class GovernanceEngine:
20
  def __init__(self):
21
+ # ⚖️ Default Strategic Weights
22
  self.DEFAULT_WEIGHTS = {
23
  "order_book": 0.25, # 25%
24
  "market_structure": 0.20, # 20%
 
28
  "volatility": 0.05, # 5%
29
  "cycle_math": 0.10 # 10%
30
  }
31
+ print("🏛️ [Governance Engine V39.0] Score-Based Approval Protocol Active (No Veto).")
32
 
33
  async def evaluate_trade(
34
  self,
 
42
  ) -> Dict[str, Any]:
43
  """
44
  Main Execution Entry.
45
+ Approval is now purely score-based (>50%).
46
  """
47
  try:
48
  if ta is None:
 
64
  if df15 is None:
65
  return self._create_rejection("Insufficient Data Length (<60)")
66
 
 
67
  df_map: Dict[str, pd.DataFrame] = {'15m': df15}
68
  if use_multi_timeframes:
69
  for tf in ('1h', '4h', '1d'):
 
76
  print("-" * 80)
77
 
78
  # 2) Calculate Domains
79
+ details_pack = {}
80
 
81
  if not use_multi_timeframes:
82
  s_trend = self._calc_trend_domain(df15, verbose, include_details, details_pack)
 
86
  s_cycle = self._calc_cycle_math_domain(df15, verbose, include_details, details_pack)
87
  s_struct = self._calc_structure_domain(df15, verbose, include_details, details_pack)
88
  else:
 
89
  tfw = {'15m': 0.50, '1h': 0.30, '4h': 0.20, '1d': 0.10}
90
 
91
  def _agg(fn, name: str) -> float:
 
94
  per_tf = {}
95
  for tf, df_tf in df_map.items():
96
  w = tfw.get(tf, 0.1)
97
+ s = fn(df_tf, False, include_details, details_pack)
98
  per_tf[tf] = float(s)
99
  acc += w * float(s)
100
  total_w += w
 
123
  current_weights = self.DEFAULT_WEIGHTS.copy()
124
 
125
  if strategy_type == 'SAFE_BOTTOM':
 
126
  current_weights = {
127
  "order_book": 0.20,
128
+ "market_structure": 0.20,
129
+ "trend": 0.05,
130
+ "momentum": 0.15,
131
  "volume": 0.10,
132
+ "volatility": 0.15,
133
+ "cycle_math": 0.15
134
  }
135
  elif strategy_type == 'MOMENTUM_LAUNCH':
 
136
  current_weights = {
137
+ "order_book": 0.25,
138
  "market_structure": 0.15,
139
+ "trend": 0.25,
140
+ "momentum": 0.20,
141
+ "volume": 0.10,
142
  "volatility": 0.05,
143
+ "cycle_math": 0.00
144
  }
145
 
146
  # ============================================================
147
+ # 🗑️ DELETED: STRICT CONSENSUS CHECK (Veto Power)
148
+ # The system now ignores negative individual domains.
149
+ # It relies solely on the Weighted Score being high enough.
150
  # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  # 3) Weighted Aggregation using DYNAMIC weights
153
  raw_weighted_score = (
 
161
  )
162
 
163
  # 4) Final Scoring & Grading
164
+ # Normalize -1 to 1 range into 0 to 100
165
  final_score = max(0.0, min(100.0, ((raw_weighted_score + 1) / 2) * 100))
166
 
167
  # ============================================================
168
+ # 🛑 SCORE THRESHOLD CHECK (> 50%)
169
  # ============================================================
170
  if final_score <= 50.0:
171
  if verbose:
 
174
 
175
  grade = self._get_grade(final_score)
176
 
177
+ if verbose:
178
+ print(f"✅ [Governance PASS] Score: {final_score:.2f}% (>50%) | Grade: {grade}")
179
+
180
  result = {
181
  "governance_score": round(final_score, 2),
182
  "grade": grade,
 
232
  # 4. Supertrend
233
  st = ta.supertrend(df['high'], df['low'], c, length=10, multiplier=3)
234
  if self._valid(st):
 
235
  st_line = st.iloc[:, 0]
236
  if c.iloc[-1] > st_line.iloc[-1]: points += 1; details.append("ST:Bull")
237
  else: points -= 1
 
239
  # 5. Parabolic SAR
240
  psar = ta.psar(df['high'], df['low'], c)
241
  if self._valid(psar):
 
242
  val = psar.iloc[-1]
243
  if isinstance(val, pd.Series): val = val.dropna().iloc[0] if not val.dropna().empty else 0
 
244
  if val != 0:
245
  if val < c.iloc[-1]: points += 1; details.append("PSAR:Bull")
246
  else: points -= 1
 
258
 
259
  # 7. Ichimoku
260
  ichi = ta.ichimoku(df['high'], df['low'], c)
 
261
  if ichi is not None and isinstance(ichi, tuple) and self._valid(ichi[0]):
262
  span_a = ichi[0][ichi[0].columns[0]].iloc[-1]
263
  span_b = ichi[0][ichi[0].columns[1]].iloc[-1]
 
423
  if self._valid(uo) and uo.iloc[-1] > 50:
424
  points += 0.5; details.append("UO:>50")
425
 
426
+ # 16. KDJ
427
  kdj = ta.kdj(df['high'], df['low'], c)
428
  if self._valid(kdj) and kdj[kdj.columns[0]].iloc[-1] > kdj[kdj.columns[1]].iloc[-1]:
429
  points += 0.5; details.append("KDJ:Bull")
 
465
  points = 0.0
466
  details = []
467
  try:
468
+ # 1. Bollinger Bands
469
  bb = ta.bbands(df['close'], length=20)
470
  if self._valid(bb):
 
471
  bw_col = self._find_col(bb, ["bbb_", "bandwidth", "bbw"])
472
  pb_col = self._find_col(bb, ["bbp_", "%b", "percentb", "pb"])
473
  width = self._safe_last(bb, col=bw_col) if bw_col else np.nan
474
  pct_b = self._safe_last(bb, col=pb_col) if pb_col else np.nan
475
 
 
 
476
  if np.isfinite(width):
477
+ if width < 0.05: points -= 1; details.append("BBW:Squeeze")
478
+ elif width > 0.18: points += 1; details.append("BBW:Expand")
 
 
479
 
 
480
  if np.isfinite(pct_b):
481
+ if pct_b > 0.90: points += 0.5; details.append("BB%B:High")
482
+ elif pct_b < 0.10: points -= 0.5; details.append("BB%B:Low")
 
 
483
 
484
  # 3. ATR
485
  atr = ta.atr(df['high'], df['low'], df['close'], length=14)
 
532
  vr = atr.iloc[-1] / atr.iloc[-20]
533
  if vr > 1.2: points += 1; details.append("VolRatio:High")
534
 
535
+ # 12. RVI
536
  if self._valid(c_vol):
537
  std_rsi = ta.rsi(c_vol, length=14)
538
  if self._valid(std_rsi) and std_rsi.iloc[-1] > 50: points += 0.5
 
823
  return norm_score
824
 
825
  # ==============================================================================
826
+ # 📖 DOMAIN 7: ORDER BOOK (Safe Unpacking)
827
  # ==============================================================================
828
  def _calc_orderbook_domain(self, ob: Dict[str, Any], verbose: bool, include_details: bool = False, details_pack: Any = None) -> float:
829
  points = 0.0
 
831
  if not ob or 'bids' not in ob or 'asks' not in ob: return 0.0
832
 
833
  try:
834
+ # ✅ Safe Numpy Conversion
835
  bids = np.array(ob['bids'], dtype=float)
836
  asks = np.array(ob['asks'], dtype=float)
837
 
838
  if len(bids) < 20 or len(asks) < 20: return 0.0
839
 
 
840
  bid_vol = np.sum(bids[:20, 1])
841
  ask_vol = np.sum(asks[:20, 1])
842
  imbal = (bid_vol - ask_vol) / (bid_vol + ask_vol)
 
852
 
853
  if bid_vol > ask_vol * 1.5: points += 2; details.append("Depth:Bull")
854
 
 
855
  mid = (asks[0, 0] + bids[0, 0]) / 2.0
856
+ target_p = mid * 1.005
857
  vol_needed = 0.0
858
 
859
+ # ✅ Safe Loop Fix
860
  for row in asks:
861
  p = row[0]
862
  s = row[1]
 
895
  # 🔧 Utilities
896
  # ==============================================================================
897
  def _valid(self, item, col: Any = None) -> bool:
898
+ if item is None: return False
899
+ if isinstance(item, tuple): return any(self._valid(x, col=col) for x in item)
 
 
 
 
 
 
 
 
 
900
  try:
901
+ if isinstance(item, pd.Series): return pd.notna(item.iloc[-1]) and np.isfinite(item.iloc[-1])
 
 
 
 
 
902
  if isinstance(item, pd.DataFrame):
903
+ if col: return self._valid(item[col]) if col in item.columns else False
904
+ return True
905
+ if isinstance(item, (int, float)): return np.isfinite(item)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
906
  return True
907
+ except: return False
908
 
909
+ def _find_col(self, df, contains_any):
910
+ if df is None: return None
911
+ for c in df.columns:
912
+ for n in contains_any:
913
+ if n.lower() in str(c).lower(): return c
 
 
 
 
 
 
 
 
 
914
  return None
915
 
916
+ def _safe_last(self, item, default=np.nan, col=None):
917
+ if not self._valid(item, col=col): return float(default)
 
 
918
  try:
919
+ if isinstance(item, pd.Series): return float(item.iloc[-1])
 
920
  if isinstance(item, pd.DataFrame):
921
+ c = self._find_col(item, [col]) if col else item.columns[0]
922
+ return float(item[c].iloc[-1]) if c else float(default)
923
+ return float(item)
924
+ except: return float(default)
925
+
926
+ def _normalize(self, value, max_possible):
 
 
 
 
 
 
 
 
 
 
 
 
927
  if max_possible == 0: return 0.0
928
  return max(-1.0, min(1.0, value / max_possible))
929
 
930
+ def _prepare_dataframe(self, ohlcv):
931
  df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
932
  df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
933
  df.set_index('timestamp', inplace=True)
 
 
934
  return df
935
 
936
+ def _get_grade(self, score):
937
  if score >= 85: return "ULTRA"
938
  if score >= 70: return "STRONG"
939
  if score > 50: return "NORMAL"
940
  return "REJECT"
941
 
942
+ def _create_rejection(self, reason):
943
+ return {"governance_score": 0.0, "status": "REJECTED", "reason": reason, "components": {}}