Spaces:
Sleeping
Sleeping
Update governance_engine.py
Browse files- governance_engine.py +63 -159
governance_engine.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# 🏛️ governance_engine.py (
|
| 3 |
# ============================================================
|
| 4 |
# Description:
|
| 5 |
# Evaluates trade quality using 156 INDICATORS.
|
| 6 |
-
# -
|
| 7 |
-
# -
|
|
|
|
| 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
|
| 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
|
| 31 |
|
| 32 |
async def evaluate_trade(
|
| 33 |
self,
|
|
@@ -41,7 +42,7 @@ class GovernanceEngine:
|
|
| 41 |
) -> Dict[str, Any]:
|
| 42 |
"""
|
| 43 |
Main Execution Entry.
|
| 44 |
-
|
| 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 = {}
|
| 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)
|
| 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,
|
| 131 |
-
"trend": 0.05,
|
| 132 |
-
"momentum": 0.15,
|
| 133 |
"volume": 0.10,
|
| 134 |
-
"volatility": 0.15,
|
| 135 |
-
"cycle_math": 0.15
|
| 136 |
}
|
| 137 |
elif strategy_type == 'MOMENTUM_LAUNCH':
|
| 138 |
-
# للانطلاق: الترند والزخم ودفتر الطلبات هم الملوك
|
| 139 |
current_weights = {
|
| 140 |
-
"order_book": 0.25,
|
| 141 |
"market_structure": 0.15,
|
| 142 |
-
"trend": 0.25,
|
| 143 |
-
"momentum": 0.20,
|
| 144 |
-
"volume": 0.10,
|
| 145 |
"volatility": 0.05,
|
| 146 |
-
"cycle_math": 0.00
|
| 147 |
}
|
| 148 |
|
| 149 |
# ============================================================
|
| 150 |
-
#
|
| 151 |
-
#
|
| 152 |
-
#
|
| 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 |
-
# 🛑
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 (
|
| 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
|
| 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
|
| 893 |
vol_needed = 0.0
|
| 894 |
|
| 895 |
-
# ✅ Safe
|
| 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 |
-
|
| 935 |
-
|
| 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.
|
| 954 |
-
|
| 955 |
-
|
| 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 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 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
|
| 990 |
-
|
| 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
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 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
|
| 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
|
| 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
|
| 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": {}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|