170 lines
6.5 KiB
Python
170 lines
6.5 KiB
Python
import json
|
|
import hashlib
|
|
import pandas as pd
|
|
from io import StringIO
|
|
from typing import Dict, Any, Tuple
|
|
from tradingagents.utils.logger import app_logger as logger
|
|
from tradingagents.agents.utils.agent_states import ExecutionResult, FactLedger, FinalDecision
|
|
from tradingagents.agents.data_registrar import LedgerDomain # Assuming this is available, if not falling back to str
|
|
|
|
class ExecutionGatekeeper:
|
|
"""
|
|
The Deterministic Authority.
|
|
Enforces the 'Python Veto'.
|
|
"""
|
|
def __init__(self):
|
|
self.name = "Execution Gatekeeper"
|
|
self.CONFIDENCE_THRESHOLD = 0.70
|
|
self.MAX_DIVERGENCE = 0.4 # Strict divergence limit
|
|
|
|
def _verify_ledger_integrity(self, ledger: FactLedger) -> bool:
|
|
"""Gate 1: Ensure Reality hasn't shifted."""
|
|
if not ledger or "ledger_id" not in ledger:
|
|
return False
|
|
# In Phase 3, we will re-hash payload here.
|
|
# For Phase 2, existence check is sufficient.
|
|
return True
|
|
|
|
def check_compliance(self, ledger: FactLedger) -> bool:
|
|
"""Gate 2: Real Compliance Logic."""
|
|
# Access safely via Enum or string key
|
|
# Use str fallback if LedgerDomain not imported/available yet
|
|
insider_key = "insider_data"
|
|
if 'LedgerDomain' in globals():
|
|
insider_key = LedgerDomain.INSIDER.value
|
|
|
|
insider_data = ledger.get(insider_key, "")
|
|
|
|
# Insider Flow Panic Check
|
|
# If massive insider selling detected in raw data, block BUYs
|
|
if isinstance(insider_data, str) and "Cluster Sale" in insider_data:
|
|
logger.warning("COMPLIANCE: Cluster Sale detected.")
|
|
return False
|
|
|
|
return True
|
|
|
|
def check_divergence(self, debate_state: Dict, confidence: float) -> bool:
|
|
"""Gate 3: Epistemic Uncertainty Check."""
|
|
if not debate_state:
|
|
return True # Pass if no debate data (Sim mode)
|
|
|
|
# Note: Debate manager must populate these. Defaulting to 0.5 prevents crash.
|
|
bull_score = debate_state.get("bull_score", 0.5)
|
|
bear_score = debate_state.get("bear_score", 0.5)
|
|
|
|
# Formula: |Bull - Bear| * Confidence
|
|
divergence = abs(bull_score - bear_score) * confidence
|
|
|
|
if divergence > self.MAX_DIVERGENCE:
|
|
logger.warning(f"DIVERGENCE: {divergence:.2f} > {self.MAX_DIVERGENCE}")
|
|
return False
|
|
|
|
return True
|
|
|
|
def check_trend_override(self, ledger: FactLedger, regime: str, action: str) -> Tuple[bool, str]:
|
|
"""
|
|
Gate 4: Don't Fight The Tape.
|
|
"""
|
|
if action != "SELL":
|
|
return True, ""
|
|
|
|
# Only protect in clear BULL regimes
|
|
if "TRENDING_UP" not in regime and "BULL" not in regime:
|
|
return True, ""
|
|
|
|
try:
|
|
# Access safely
|
|
price_key = "price_data"
|
|
if 'LedgerDomain' in globals():
|
|
price_key = LedgerDomain.PRICE.value
|
|
|
|
price_raw = ledger.get(price_key, "")
|
|
|
|
if isinstance(price_raw, str):
|
|
df = pd.read_csv(StringIO(price_raw), comment='#')
|
|
if 'Close' in df.columns:
|
|
current_price = df['Close'].iloc[-1]
|
|
sma_200 = df['Close'].rolling(window=200).mean().iloc[-1]
|
|
|
|
# LOGIC: Regime says UP AND Price says UP (Structure)
|
|
if current_price > (sma_200 * 1.05):
|
|
return False, f"BLOCKED_TREND: Regime ({regime}) + Price > 1.05*200SMA. Don't fight the tape."
|
|
except Exception as e:
|
|
logger.warning(f"Trend Check Error: {e}")
|
|
|
|
return True, ""
|
|
|
|
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
logger.info("🛡️ GATEKEEPER: Validating Decision...")
|
|
|
|
ledger = state.get("fact_ledger")
|
|
if not ledger:
|
|
return self._abort(ExecutionResult.ABORT_DATA_GAP, "FactLedger Missing")
|
|
|
|
trader_decision = state.get("trader_decision", {"action": "HOLD", "confidence": 0.0})
|
|
|
|
action = trader_decision["action"]
|
|
confidence = trader_decision["confidence"]
|
|
regime = state.get("market_regime", "UNKNOWN")
|
|
|
|
# --- GATE 1: INTEGRITY ---
|
|
if not self._verify_ledger_integrity(ledger):
|
|
return self._abort(ExecutionResult.ABORT_DATA_GAP, "Ledger Integrity Failed")
|
|
|
|
# --- GATE 2: COMPLIANCE ---
|
|
if not self.check_compliance(ledger):
|
|
return self._abort(ExecutionResult.ABORT_COMPLIANCE, "Insider/Restricted Flag")
|
|
|
|
# --- GATE 3: CONFIDENCE ---
|
|
if confidence < self.CONFIDENCE_THRESHOLD and action != "HOLD":
|
|
return self._abort(ExecutionResult.ABORT_LOW_CONFIDENCE, f"Conf {confidence:.2f} < {self.CONFIDENCE_THRESHOLD}")
|
|
|
|
# --- GATE 4: DIVERGENCE ---
|
|
if not self.check_divergence(state.get("investment_debate_state", {}), confidence):
|
|
return self._abort(ExecutionResult.ABORT_DIVERGENCE, "Analyst Divergence Too High")
|
|
|
|
# --- GATE 5: TREND OVERRIDE ---
|
|
allowed, reason = self.check_trend_override(ledger, regime, action)
|
|
if not allowed:
|
|
return self._block(reason, original_action=action)
|
|
|
|
# ✅ APPROVED
|
|
logger.info(f"✅ EXECUTION APPROVED: {action}")
|
|
return {
|
|
"final_trade_decision": {
|
|
"status": ExecutionResult.APPROVED,
|
|
"action": action,
|
|
"confidence": confidence,
|
|
"details": {"rationale": trader_decision.get("rationale")}
|
|
}
|
|
}
|
|
|
|
def _abort(self, status: ExecutionResult, reason: str) -> Dict:
|
|
logger.critical(f"⛔ {status.value}: {reason}")
|
|
return {
|
|
"final_trade_decision": {
|
|
"status": status,
|
|
"action": "NO_OP",
|
|
"confidence": 0.0,
|
|
"details": {"reason": reason}
|
|
}
|
|
}
|
|
|
|
def _block(self, reason: str, original_action: str) -> Dict:
|
|
logger.warning(f"🛡️ BLOCKED: {reason}")
|
|
return {
|
|
"final_trade_decision": {
|
|
"status": ExecutionResult.BLOCKED_TREND,
|
|
"action": "HOLD",
|
|
"confidence": 0.0,
|
|
"details": {
|
|
"reason": reason,
|
|
"counterfactual": f"Intent: {original_action} -> Blocked by Regime"
|
|
}
|
|
}
|
|
}
|
|
|
|
def create_execution_gatekeeper():
|
|
gatekeeper = ExecutionGatekeeper()
|
|
return gatekeeper.run
|