Spaces:
Sleeping
Sleeping
| """ | |
| server_F.py — Cerebro F — The Liquidator v6.0 APEX POLIMÓRFICO | |
| =============================================================== | |
| NUEVAS CAPACIDADES v6.0 (sobre v5.0 OSI+LLM): | |
| 1. COGNITIVE TRAILING STOP ASÍNCRONO | |
| Si profit > +0.50% Y velocity_score cae a 0 durante >10 barras → MARKET_EXIT inmediato. | |
| No espera el OSI ni el LLM. Embolsa el flotante antes de que el mercado se pudra. | |
| Umbral configurable por env: VELOCITY_STALL_BARS (default=10), PROFIT_FLOOR_SCALP=0.50%. | |
| 2. BIDIRECCIONAL NATIVO (LONG/SHORT) | |
| Acepta trade_side = "long" | "short" en el payload. | |
| Para SHORT: la lógica OSI se invierte (delta_cvd positivo = presión alcista = KILL_SHORT). | |
| Output incluye "close_action": "buy" | "sell" para que orders_processor.py sepa | |
| qué orden de cierre mandar a Alpaca. | |
| 3. VWAP DINÁMICO (Herramienta 1) | |
| Calcula VWAP rodante desde las barras disponibles. | |
| Valida dirección del trade vs posición precio/VWAP: | |
| LONG válido → precio < VWAP (compra con descuento institucional) | |
| SHORT válido → precio > VWAP (reversión corta confirmada) | |
| Flag: price_vs_vwap = "ABOVE" | "BELOW" | "AT" | |
| 4. ENTROPY Z-SCORE OBI (Herramienta 2) | |
| Cruza OBI velocity con RSI adaptativo de micro-temporalidad. | |
| Si Z-Score de impulso cae < -1.5σ → MARKET_EXHAUSTION = True → KILL inmediato. | |
| Elimina el "Síndrome del Capital Atrapado" a las 10 barras, no a las 120. | |
| 5. CROSS-ASSET CORRELATION (Herramienta 3) | |
| Circuit breaker: si BTC/ETH rompen VWAP a la baja con volumen, bloquea LONGs. | |
| Recibe anchor_correlation del payload (calculado por data_manager.py). | |
| 6. NEWS IMPACT WEIGHT (Herramienta 4) | |
| Recibe macro_impact_weight del Cerebro E. | |
| Si peso institucional > 2 y bearish → fuerza KILL independientemente del OSI. | |
| 7. STYLE POLYMORPHISM | |
| Acepta c_style = "SCALP" | "MOMENTUM" del Cerebro C. | |
| SCALP: umbrales más agresivos de salida (profit_floor=0.50%, velocity_bars=8) | |
| MOMENTUM: aguanta más (profit_floor=1.00%, velocity_bars=15) | |
| 8. SHORT HARD STOP MATEMÁTICO (Cerebro H integration) | |
| Para shorts: short_stop_price se calcula desde entry_price × (1 + 0.015). | |
| Si current_price >= short_stop_price → KILL_SHORT incondicional, no consultado. | |
| OUTPUT v6.0 (100% compatible con swarm_engine via legacy aliases): | |
| { | |
| "decision": "SELL|HOLD", | |
| "close_action": "sell|buy", # sell=cierra long, buy=cierra short | |
| "trade_side": "long|short", | |
| "confidence": float, | |
| "kill_score": float, | |
| "trigger": str, | |
| "urgency": "critical|high|medium|low", | |
| "osi": float, | |
| "osi_zone": str, | |
| "vwap": float, | |
| "price_vs_vwap": "ABOVE|BELOW|AT", | |
| "market_exhaustion": bool, | |
| "exhaustion_zscore": float, | |
| "anchor_blocked": bool, | |
| "macro_kill": bool, | |
| "c_style": "SCALP|MOMENTUM", | |
| "cognitive_trailing_triggered": bool, | |
| "short_stop_triggered": bool, | |
| "orderflow": dict, | |
| "_math_ms": float, | |
| "_llm_ms": float, | |
| "_total_ms": float, | |
| } | |
| TELEMETRÍA: [F/MATH] | [F/CTS] Cognitive Trailing | [F/LLM] | [F/TOTAL] | |
| """ | |
| import os, json, re, time, threading, math, collections | |
| from fastapi import FastAPI, Request, HTTPException | |
| from fastapi.responses import JSONResponse | |
| import httpx | |
| app = FastAPI(title="Cerebro F — The Liquidator v6.0 APEX POLIMÓRFICO") | |
| # ── Configuración base (heredada de v5.0) ───────────────────────────────────── | |
| MODEL_PATH = os.environ.get("MODEL_PATH", "/models/ggml-model-i2_s.gguf") | |
| KILL_THRESHOLD = float(os.environ.get("KILL_THRESHOLD", "0.82")) | |
| KILL_THRESHOLD_SCALP = float(os.environ.get("KILL_THRESHOLD_SCALP", "0.80")) | |
| HOLD_THRESHOLD = float(os.environ.get("HOLD_THRESHOLD", "0.18")) | |
| STAGNATION_BARS = int(os.environ.get("STAGNATION_BARS", "5")) | |
| # OSI Zones | |
| OSI_FAST_EXIT = float(os.environ.get("OSI_FAST_EXIT", "80")) | |
| OSI_KILL_ZONE = float(os.environ.get("OSI_KILL_ZONE", "60")) | |
| OSI_HOLD_ZONE = float(os.environ.get("OSI_HOLD_ZONE", "30")) | |
| OSI_FALLBACK_KILL = float(os.environ.get("OSI_FALLBACK_KILL", "55")) | |
| # ── v6.0: Nuevas configuraciones ───────────────────────────────────────────── | |
| # Cognitive Trailing Stop | |
| CTS_PROFIT_FLOOR_SCALP = float(os.environ.get("CTS_PROFIT_FLOOR_SCALP", "0.50")) # % | |
| CTS_PROFIT_FLOOR_MOMENTUM = float(os.environ.get("CTS_PROFIT_FLOOR_MOMENTUM", "1.00")) # % | |
| CTS_VELOCITY_STALL_BARS = int(os.environ.get("CTS_VELOCITY_STALL_BARS", "10")) | |
| CTS_VELOCITY_STALL_BARS_M = int(os.environ.get("CTS_VELOCITY_STALL_BARS_M", "15")) # MOMENTUM | |
| # Entropy Z-Score | |
| EXHAUSTION_ZSCORE_THRESH = float(os.environ.get("EXHAUSTION_ZSCORE_THRESH", "-1.5")) | |
| # Short protection | |
| SHORT_STOP_PCT = float(os.environ.get("SHORT_STOP_PCT", "0.015")) # 1.5% | |
| MAX_ACCOUNT_RISK_PCT = float(os.environ.get("MAX_ACCOUNT_RISK_PCT", "0.005")) # 0.5% | |
| # BitNet | |
| BITNET_PORT = int(os.environ.get("BITNET_PORT", "8080")) | |
| BITNET_HOST = os.environ.get("BITNET_HOST", "127.0.0.1") | |
| BITNET_BASE = f"http://{BITNET_HOST}:{BITNET_PORT}" | |
| BITNET_TIMEOUT = float(os.environ.get("BITNET_TIMEOUT", "30.0")) | |
| LLM_MAX_TOKENS = int(os.environ.get("LLM_MAX_TOKENS", "48")) | |
| _bitnet_client = None | |
| def _get_bitnet_client(): | |
| global _bitnet_client | |
| if _bitnet_client is None or _bitnet_client.is_closed: | |
| _bitnet_client = httpx.AsyncClient( | |
| base_url=BITNET_BASE, | |
| timeout=httpx.Timeout(BITNET_TIMEOUT), | |
| limits=httpx.Limits(max_connections=4, max_keepalive_connections=2), | |
| ) | |
| return _bitnet_client | |
| async def _bitnet_infer(prompt_text: str, max_tokens: int = 48) -> dict: | |
| client = _get_bitnet_client() | |
| payload = { | |
| "prompt": prompt_text, "n_predict": max_tokens, "temperature": 0.0, | |
| "top_p": 0.95, "top_k": 5, "repeat_penalty": 1.0, "stream": False, | |
| "cache_prompt": False, | |
| "stop": ["<|eot_id|>", "<|end_of_text|>", "<|im_end|>"], | |
| } | |
| try: | |
| resp = await client.post("/completion", json=payload) | |
| resp.raise_for_status() | |
| return {"raw": resp.json().get("content", "").strip(), "ok": True} | |
| except httpx.TimeoutException: | |
| return {"raw": "", "ok": False, "error": "TIMEOUT"} | |
| except Exception as e: | |
| return {"raw": "", "ok": False, "error": str(e)[:80]} | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # HERRAMIENTA 1: VWAP DINÁMICO DE ALTA FRECUENCIA | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _compute_vwap(bars: list) -> tuple: | |
| """ | |
| VWAP rodante desde barras OHLCV. | |
| Usa precio típico = (H+L+C)/3 × volumen. | |
| Retorna (vwap: float, price_vs_vwap: str) | |
| """ | |
| if not bars or len(bars) < 2: | |
| return 0.0, "AT" | |
| cum_pv = 0.0 | |
| cum_v = 0.0 | |
| for b in bars: | |
| h = b.get("high", b.get("h", 0)) | |
| l = b.get("low", b.get("l", 0)) | |
| c = b.get("close", b.get("c", 0)) | |
| v = b.get("volume", b.get("v", 0)) | |
| if h == 0 or v == 0: | |
| continue | |
| typical = (h + l + c) / 3.0 | |
| cum_pv += typical * v | |
| cum_v += v | |
| if cum_v == 0: | |
| return 0.0, "AT" | |
| vwap = round(cum_pv / cum_v, 8) | |
| last_close = bars[-1].get("close", bars[-1].get("c", vwap)) | |
| if last_close > vwap * 1.0005: | |
| pos = "ABOVE" | |
| elif last_close < vwap * 0.9995: | |
| pos = "BELOW" | |
| else: | |
| pos = "AT" | |
| return vwap, pos | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # HERRAMIENTA 2: ENTROPY Z-SCORE OBI — Filtro de Fatiga del Impulso | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _compute_exhaustion_zscore(bars: list, velocity_history: list = None) -> tuple: | |
| """ | |
| Cruza OBI Velocity (desequilibrio bid/ask por barra) con RSI micro-adaptativo. | |
| Retorna (market_exhaustion: bool, zscore: float, rsi_micro: float) | |
| velocity_history: lista de OBI scores pasados del shadow cache del símbolo. | |
| Si no hay historial externo, se recalcula desde las barras. | |
| Z-Score < EXHAUSTION_ZSCORE_THRESH (-1.5) → MARKET_EXHAUSTION = True | |
| """ | |
| if not bars or len(bars) < 8: | |
| return False, 0.0, 50.0 | |
| # ── OBI Velocity por barra ──────────────────────────────────────────────── | |
| obi_series = [] | |
| for b in bars[-20:]: | |
| bid_v = b.get("bid_volume", b.get("volume", 0)) * 0.55 # estimación | |
| ask_v = b.get("ask_volume", b.get("volume", 0)) * 0.45 | |
| total = bid_v + ask_v + 1e-9 | |
| obi_series.append((bid_v - ask_v) / total) | |
| if len(obi_series) < 5: | |
| return False, 0.0, 50.0 | |
| # ── RSI Micro-adaptativo (14 periodos sobre returns de closes) ──────────── | |
| closes = [b.get("close", b.get("c", 0)) for b in bars[-16:] if b.get("close", b.get("c", 0)) > 0] | |
| rsi_micro = 50.0 | |
| if len(closes) >= 3: | |
| gains = [max(0, closes[i] - closes[i-1]) for i in range(1, len(closes))] | |
| losses = [max(0, closes[i-1] - closes[i]) for i in range(1, len(closes))] | |
| avg_gain = sum(gains) / max(len(gains), 1) | |
| avg_loss = sum(losses) / max(len(losses), 1) + 1e-9 | |
| rs = avg_gain / avg_loss | |
| rsi_micro = round(100 - (100 / (1 + rs)), 2) | |
| # ── Z-Score del OBI velocity ────────────────────────────────────────────── | |
| series = obi_series | |
| n = len(series) | |
| mu = sum(series) / n | |
| var = sum((x - mu) ** 2 for x in series) / n | |
| std = math.sqrt(var) + 1e-9 | |
| z_score = round((series[-1] - mu) / std, 3) | |
| # Combinar: Z-Score OBI + RSI sobrecomprado/vendido | |
| # Si RSI > 70 Y Z < -1.0 → agotamiento claro | |
| rsi_factor = -0.5 if rsi_micro > 68 else (0.3 if rsi_micro < 35 else 0.0) | |
| combined_z = round(z_score + rsi_factor, 3) | |
| exhausted = combined_z < EXHAUSTION_ZSCORE_THRESH | |
| return exhausted, combined_z, rsi_micro | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # COGNITIVE TRAILING STOP (CTS) — Erradicación del Estancamiento | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _cognitive_trailing_stop( | |
| current_pnl_pct: float, | |
| velocity_zero_bars: int, | |
| c_style: str, | |
| trade_side: str, | |
| market_exhaustion: bool, | |
| exhaustion_zscore: float, | |
| ) -> tuple: | |
| """ | |
| Dispara KILL si el capital flotante positivo está en riesgo por inactividad. | |
| Lógica: | |
| - Si profit >= profit_floor Y (velocity_stall >= umbral O market_exhaustion) | |
| → KILL inmediato para embolsar el flotante | |
| Retorna (triggered: bool, reason: str) | |
| """ | |
| is_scalp = c_style.upper() != "MOMENTUM" | |
| profit_floor = CTS_PROFIT_FLOOR_SCALP if is_scalp else CTS_PROFIT_FLOOR_MOMENTUM | |
| stall_bars = CTS_VELOCITY_STALL_BARS if is_scalp else CTS_VELOCITY_STALL_BARS_M | |
| in_profit = current_pnl_pct >= profit_floor | |
| if not in_profit: | |
| return False, "below_floor" | |
| # Stagnation por velocidad nula | |
| if velocity_zero_bars >= stall_bars: | |
| reason = (f"cts_velocity_stall bars={velocity_zero_bars}>={stall_bars} " | |
| f"profit={current_pnl_pct:.2f}%") | |
| return True, reason | |
| # Market exhaustion detectado por Z-Score | |
| if market_exhaustion: | |
| reason = (f"cts_market_exhaustion z={exhaustion_zscore:.2f}<{EXHAUSTION_ZSCORE_THRESH} " | |
| f"profit={current_pnl_pct:.2f}%") | |
| return True, reason | |
| return False, "no_trigger" | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # HERRAMIENTA 3: CROSS-ASSET CORRELATION CIRCUIT BREAKER | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _check_anchor_correlation( | |
| trade_side: str, | |
| anchor_correlation: str, | |
| price_vs_vwap: str, | |
| a_bias: str = "N", | |
| ) -> tuple: | |
| """ | |
| Circuit breaker basado en correlación cruzada BTC/ETH/SPY. | |
| anchor_correlation: "BULLISH" | "BEARISH" | "NEUTRAL" (del shadow_cache) | |
| Reglas: | |
| LONG + BEARISH anchor + precio ABOVE VWAP → GATEKEEPER REJECT | |
| SHORT + BULLISH anchor + precio BELOW VWAP → GATEKEEPER REJECT | |
| Retorna (blocked: bool, reason: str) | |
| """ | |
| corr = anchor_correlation.upper() if anchor_correlation else "NEUTRAL" | |
| if trade_side == "long": | |
| if corr == "BEARISH" and price_vs_vwap == "ABOVE": | |
| return True, "anchor_bearish_long_above_vwap" | |
| # Señal adicional: si sesgo A también es bear, refuerza el bloqueo | |
| if corr == "BEARISH" and a_bias == "bear": | |
| return True, "anchor_bearish_a_bear_confluence" | |
| elif trade_side == "short": | |
| if corr == "BULLISH" and price_vs_vwap == "BELOW": | |
| return True, "anchor_bullish_short_below_vwap" | |
| if corr == "BULLISH" and a_bias == "bull": | |
| return True, "anchor_bullish_a_bull_confluence" | |
| return False, "anchor_ok" | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # HERRAMIENTA 4: NEWS IMPACT WEIGHT — Cerebro E integration | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _check_macro_kill( | |
| trade_side: str, | |
| e_sentiment: str, | |
| macro_impact_weight: int, | |
| a_bias: str = "N", | |
| ) -> tuple: | |
| """ | |
| Si el peso institucional de noticias es bearish y alto → MACRO_KILL. | |
| macro_impact_weight: 0=standard, 1=alert, 2=institutional | |
| Reglas: | |
| weight=2 (institutional) + sentiment S (bearish) → KILL long | |
| weight=2 (institutional) + sentiment B (bullish) → KILL short | |
| """ | |
| if macro_impact_weight < 2: | |
| return False, "macro_weight_low" | |
| sent = e_sentiment.upper() if e_sentiment else "N" | |
| if trade_side == "long" and sent == "S": | |
| return True, f"macro_institutional_bearish weight={macro_impact_weight}" | |
| if trade_side == "short" and sent == "B": | |
| return True, f"macro_institutional_bullish_vs_short weight={macro_impact_weight}" | |
| return False, "macro_neutral" | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # SHORT HARD STOP — Cerebro H Integration | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _check_short_hard_stop( | |
| trade_side: str, | |
| current_price: float, | |
| entry_price: float, | |
| account_balance: float = 72696.0, | |
| ) -> tuple: | |
| """ | |
| Para SHORT: si el precio sube más del SHORT_STOP_PCT desde entrada → KILL incondicional. | |
| Garantiza que el riesgo nunca supere MAX_ACCOUNT_RISK_PCT del capital. | |
| Cálculo del lot size máximo para short: | |
| max_loss_usd = account_balance × MAX_ACCOUNT_RISK_PCT | |
| stop_distance = entry_price × SHORT_STOP_PCT | |
| max_qty = max_loss_usd / stop_distance | |
| Retorna (triggered: bool, stop_price: float, max_qty_hint: float) | |
| """ | |
| if trade_side != "short" or entry_price <= 0: | |
| return False, 0.0, 0.0 | |
| short_stop_price = round(entry_price * (1.0 + SHORT_STOP_PCT), 8) | |
| max_loss_usd = account_balance * MAX_ACCOUNT_RISK_PCT | |
| stop_distance = entry_price * SHORT_STOP_PCT | |
| max_qty = round(max_loss_usd / max(stop_distance, 1e-9), 6) | |
| triggered = current_price >= short_stop_price | |
| return triggered, short_stop_price, max_qty | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # MOTOR DE ORDERFLOW v5.0 (heredado — NO MODIFICADO) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _compute_orderflow_from_bars(bars: list) -> dict: | |
| if not bars or len(bars) < 3: | |
| return { | |
| "delta_cvd": 0.0, "prev_delta_cvd": 0.0, "absorption_score": 0.5, | |
| "imbalance_score": 0.0, "pv_divergence": False, | |
| "cvd_price_divergence": False, "wick_rejection": False, | |
| "vol_climax": False, "delta_acceleration": 0.0, | |
| "delta_accelerating_negative": False, "bars_used": 0, | |
| } | |
| recent = bars[-15:] | |
| total_vol = sum(b.get("volume", b.get("v", 1)) for b in recent) or 1 | |
| deltas = [] | |
| for b in recent: | |
| o = b.get("open", b.get("o", 0)); h = b.get("high", b.get("h", 0)) | |
| l = b.get("low", b.get("l", 0)); c = b.get("close", b.get("c", 0)) | |
| vol = b.get("volume", b.get("v", 0)) | |
| if h == l or vol == 0: deltas.append(0.0); continue | |
| close_pos = (c - l) / (h - l) | |
| deltas.append((close_pos * 2 - 1) * vol) | |
| cumulative_delta = sum(deltas) | |
| delta_cvd = max(-1.0, min(1.0, cumulative_delta / total_vol)) | |
| half = max(1, len(deltas) // 2) | |
| prev_vol = sum(b.get("volume", b.get("v", 1)) for b in recent[:half]) or 1 | |
| prev_delta_cvd = max(-1.0, min(1.0, sum(deltas[:half]) / prev_vol)) | |
| if len(deltas) >= 6: | |
| v_r = sum(b.get("volume", b.get("v", 1)) for b in recent[-3:]) or 1 | |
| v_p = sum(b.get("volume", b.get("v", 1)) for b in recent[-6:-3]) or 1 | |
| delta_acceleration = sum(deltas[-3:]) / v_r - sum(deltas[-6:-3]) / v_p | |
| else: | |
| delta_acceleration = 0.0 | |
| delta_accelerating_negative = (delta_cvd < -0.10 and delta_acceleration < -0.10) | |
| absorption_scores = [] | |
| for b in recent: | |
| o = b.get("open", b.get("o", 0)); h = b.get("high", b.get("h", 0)) | |
| l = b.get("low", b.get("l", 0)); c = b.get("close", b.get("c", 0)) | |
| vol = b.get("volume", b.get("v", 0)) | |
| body = abs(c - o); rang = h - l | |
| if rang == 0 or total_vol == 0: continue | |
| absorption = (vol / (total_vol / len(recent))) * (1.0 - body / rang) | |
| absorption_scores.append(absorption) | |
| absorption_score = min(1.0, sum(absorption_scores) / max(len(absorption_scores), 1) / 2.0) | |
| bull_vol = sum(b.get("volume", b.get("v", 0)) for b in recent if b.get("close", b.get("c", 0)) >= b.get("open", b.get("o", 0))) | |
| bear_vol = sum(b.get("volume", b.get("v", 0)) for b in recent if b.get("close", b.get("c", 0)) < b.get("open", b.get("o", 0))) | |
| imbalance_score = bear_vol / (bull_vol + bear_vol + 1) | |
| pv_divergence = False | |
| if len(recent) >= 5: | |
| closes_r = [b.get("close", b.get("c", 0)) for b in recent[-5:]] | |
| vols_r = [b.get("volume", b.get("v", 0)) for b in recent[-5:]] | |
| pv_divergence = (closes_r[-1] > closes_r[0]) and (vols_r[-1] < vols_r[0]) | |
| cvd_price_divergence = False | |
| if len(recent) >= 6: | |
| current_close = recent[-1].get("close", recent[-1].get("c", 0)) | |
| mid_close = recent[len(recent)//2].get("close", recent[len(recent)//2].get("c", 0)) | |
| cvd_price_divergence = (current_close > mid_close) and (delta_cvd < prev_delta_cvd) | |
| wick_rejection = False | |
| for b in recent[-5:]: | |
| o = b.get("open", b.get("o", 0)); h = b.get("high", b.get("h", 0)) | |
| c_ = b.get("close", b.get("c", 0)) | |
| body = abs(c_ - o); upper_wick = h - max(o, c_) | |
| if body > 0 and upper_wick > body * 1.5: | |
| wick_rejection = True; break | |
| avg_vol = total_vol / len(recent) | |
| vol_climax = any( | |
| b.get("close", b.get("c", 0)) < b.get("open", b.get("o", 0)) and | |
| b.get("volume", b.get("v", 0)) > avg_vol * 2.0 | |
| for b in recent[-3:]) | |
| return { | |
| "delta_cvd": round(delta_cvd, 4), | |
| "prev_delta_cvd": round(prev_delta_cvd, 4), | |
| "absorption_score": round(absorption_score, 4), | |
| "imbalance_score": round(imbalance_score, 4), | |
| "pv_divergence": pv_divergence, | |
| "cvd_price_divergence": cvd_price_divergence, | |
| "wick_rejection": wick_rejection, | |
| "vol_climax": vol_climax, | |
| "delta_acceleration": round(delta_acceleration, 4), | |
| "delta_accelerating_negative": delta_accelerating_negative, | |
| "bars_used": len(recent), | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # OSI v5.0 con ajuste bidireccional v6.0 | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def _compute_osi(of: dict, bars_held: int, current_pnl: float, | |
| trade_type: str, trade_side: str = "long") -> tuple: | |
| """ | |
| Para SHORT: invertimos la presión dominante. | |
| delta_cvd positivo en un short = precio sube = presión de cierre. | |
| """ | |
| delta_cvd = of.get("delta_cvd", 0.0) | |
| # Para SHORT, presión de cierre es cuando delta_cvd > 0 (precio sube) | |
| if trade_side == "short": | |
| # Invertir delta_cvd para que el OSI siga midiendo "presión de cierre" | |
| effective_delta = -delta_cvd | |
| else: | |
| effective_delta = delta_cvd | |
| delta_score = max(0.0, min(30.0, ((-effective_delta + 1) / 2) * 30)) | |
| imb = of.get("imbalance_score", 0.0) | |
| # Para SHORT: imbalance bull = presión al alza = riesgo | |
| if trade_side == "short": | |
| imb = 1.0 - imb | |
| imb_score = imb * 20 | |
| abs_score = of.get("absorption_score", 0.0) * 15 | |
| div_score = 0.0 | |
| if of.get("cvd_price_divergence"): div_score += 8.0 | |
| if of.get("pv_divergence"): div_score += 4.0 | |
| if of.get("wick_rejection"): div_score += 3.0 | |
| div_score = min(15.0, div_score) | |
| stag_score = 0.0 | |
| if bars_held >= STAGNATION_BARS: | |
| stag_score = min(10.0, (bars_held - STAGNATION_BARS + 1) * 2.0) | |
| pnl_score = 0.0 | |
| if current_pnl < 0: | |
| pnl_score = min(10.0, abs(current_pnl) * 100) | |
| osi = delta_score + imb_score + abs_score + div_score + stag_score + pnl_score | |
| osi = round(min(100.0, max(0.0, osi)), 2) | |
| kill_score = round(osi / 100.0, 4) | |
| kill_threshold = KILL_THRESHOLD_SCALP if trade_type == "scalp" else KILL_THRESHOLD | |
| components = { | |
| "delta_score": round(delta_score, 2), | |
| "imb_score": round(imb_score, 2), | |
| "abs_score": round(abs_score, 2), | |
| "div_score": round(div_score, 2), | |
| "stag_score": round(stag_score, 2), | |
| "pnl_score": round(pnl_score, 2), | |
| } | |
| return osi, kill_score, kill_threshold, components | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # LLM PANIC TRIGGER (heredado v5.0 con fix bug raw_out) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| async def _llm_panic_trigger(osi: float, kill_score: float, of: dict, | |
| sym: str, trade_type: str, bars_held: int, | |
| trade_side: str = "long") -> dict: | |
| t0_llm = time.perf_counter() | |
| cvd = of.get("delta_cvd", 0.0) | |
| cvd_div = "Y" if of.get("cvd_price_divergence") else "N" | |
| vol_cl = "Y" if of.get("vol_climax") else "N" | |
| accel_n = "Y" if of.get("delta_accelerating_negative") else "N" | |
| side_t = trade_side[0].upper() # L o S | |
| llm_prompt = ( | |
| "<|im_start|>system\n" | |
| f'Exit trigger {side_t}. SOLO JSON: {{"action":"HOLD"}} o {{"action":"KILL_TRADE"}}. Sin pensar.\n' | |
| "<|im_end|>\n" | |
| "<|im_start|>user\n" | |
| f'{{"s":"{sym[:6]}","osi":{osi:.0f},"cvd":{cvd:.2f},' | |
| f'"div":"{cvd_div}","vc":"{vol_cl}","acc":"{accel_n}",' | |
| f'"bh":{bars_held},"t":"{trade_type[0]}","side":"{side_t}"}}\n' | |
| "<|im_end|>\n" | |
| "<|im_start|>assistant\n{" | |
| ) | |
| try: | |
| result = await _bitnet_infer(llm_prompt) | |
| raw_out_text = result.get("raw", "") | |
| llm_ms = (time.perf_counter() - t0_llm) * 1000 | |
| raw_t = "{" + raw_out_text | |
| m = re.search(r'"action"\s*:\s*"(HOLD|KILL_TRADE)"', raw_t, re.IGNORECASE) | |
| if m: | |
| action = m.group(1).upper() | |
| print(f"[F/LLM] {sym}: action={action} osi={osi:.0f} side={side_t} | {llm_ms:.1f}ms") | |
| return {"action": action, "_llm_ms": round(llm_ms, 1)} | |
| action = "KILL_TRADE" if "KILL" in raw_t.upper() else "HOLD" | |
| print(f"[F/LLM] {sym}: partial={action} | {llm_ms:.1f}ms") | |
| return {"action": action, "_llm_ms": round(llm_ms, 1)} | |
| except Exception as e: | |
| llm_ms = (time.perf_counter() - t0_llm) * 1000 | |
| print(f"[F/LLM-ERR] {sym}: {type(e).__name__} | {llm_ms:.1f}ms → OSI fallback") | |
| action = "KILL_TRADE" if osi > OSI_FALLBACK_KILL else "HOLD" | |
| return {"action": action, "_llm_ms": round(llm_ms, 1), "_fallback": True} | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # PIPELINE PRINCIPAL v6.0 — POLIMÓRFICO | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| async def _liquidate(payload: dict) -> dict: | |
| t0 = time.perf_counter() | |
| sym = str(payload.get("symbol", "?")) | |
| bars = payload.get("bars", []) | |
| bars_held = int(payload.get("bars_held", 0)) | |
| current_pnl = float(payload.get("current_pnl", 0.0)) # decimal, ej 0.0102 = +1.02% | |
| entry_price = float(payload.get("entry_price", 0.0)) | |
| trade_type = str(payload.get("trade_type", "scalp")).lower() | |
| tp = float(payload.get("tp", 0.0)) | |
| sl = float(payload.get("sl", 0.0)) | |
| current_price = float(payload.get("current_price", entry_price)) | |
| # ── v6.0: Extracción elástica multi-capa del snapshot cognitivo ────────── | |
| # El payload puede venir con las métricas en la raíz, bajo "metrics", | |
| # bajo "cognition", o bajo "risk" según quién lo envíe (swarm_engine, | |
| # orders_processor, o el nuevo data_manager_anchors). | |
| # La cascada de .get() garantiza que NUNCA quede vacío ni lance excepción. | |
| _metrics = payload.get("metrics", {}) or {} | |
| _cognition = payload.get("cognition", {}) or {} | |
| _risk = payload.get("risk", {}) or {} | |
| def _mget(key: str, default, cast=str): | |
| """Busca una clave en raíz → metrics → cognition → risk, con cast seguro.""" | |
| for src in (payload, _metrics, _cognition, _risk): | |
| v = src.get(key) | |
| if v is not None: | |
| try: | |
| return cast(v) | |
| except (ValueError, TypeError): | |
| continue | |
| return default | |
| trade_side = _mget("trade_side", "long", str).lower() | |
| c_style = _mget("C_style", _mget("c_style", "SCALP", str), str).upper() | |
| a_bias_raw = _mget("A_bias", _mget("a_bias", "N", str), str) | |
| a_bias = a_bias_raw.lower() # "bull" | "bear" | "neutral" | "l" | "s" | "n" | |
| # Normaliza tokens ternarios de A (L/S/N) a los equivalentes semánticos | |
| _a_map = {"l": "bull", "s": "bear", "n": "neutral"} | |
| a_bias = _a_map.get(a_bias, a_bias) | |
| e_sentiment_raw = _mget("E_sentiment", _mget("e_sentiment", "N", str), str) | |
| e_sentiment = e_sentiment_raw.upper()[:1] # primer char: "B" | "N" | "S" | |
| try: | |
| macro_impact_weight = int(_mget("macro_impact_weight", 0, int)) | |
| except (ValueError, TypeError): | |
| macro_impact_weight = 0 | |
| anchor_correlation = _mget("anchor_correlation", "NEUTRAL", str).upper() | |
| try: | |
| velocity_zero_bars = int(_mget("velocity_zero_bars", 0, int)) | |
| except (ValueError, TypeError): | |
| velocity_zero_bars = 0 | |
| try: | |
| account_balance = float( | |
| payload.get("account_balance", | |
| _risk.get("account_balance", | |
| _metrics.get("account_balance", 72696.0))) | |
| ) | |
| except (ValueError, TypeError): | |
| account_balance = 72696.0 | |
| # Override VWAP si ya viene calculado por el radar (evita recalcularlo desde barras) | |
| _pvwap_pre = _mget("price_vs_vwap", None, str) | |
| _pvwap_pre = _pvwap_pre.upper() if _pvwap_pre and _pvwap_pre.upper() in ("ABOVE","BELOW","AT") else None | |
| # Override market_exhaustion si ya viene del radar | |
| _exh_pre = _mget("market_exhaustion", None, str) | |
| _exh_pre = (str(_exh_pre).lower() == "true") if _exh_pre is not None else None | |
| # current_pnl en porcentaje para CTS | |
| current_pnl_pct = current_pnl * 100.0 # 0.0102 → 1.02% | |
| # Diagnóstico de origen del payload | |
| _has_radar_data = bool(_metrics or _cognition) | |
| if not _has_radar_data and bars_held > 2: | |
| print(f"[F/PAYLOAD] {sym}: payload plano sin metrics/cognition — usando defaults seguros") | |
| # ── FASE MATH: OrderFlow ────────────────────────────────────────────────── | |
| of = _compute_orderflow_from_bars(bars) | |
| # ── VWAP (Herramienta 1) — usa el pre-calculado del radar si existe ─────── | |
| vwap_from_bars, pvwap_from_bars = _compute_vwap(bars) | |
| if _pvwap_pre is not None: | |
| vwap, price_vs_vwap = vwap_from_bars, _pvwap_pre | |
| else: | |
| vwap, price_vs_vwap = vwap_from_bars, pvwap_from_bars | |
| # ── Entropy Z-Score / Market Exhaustion (Herramienta 2) ─────────────────── | |
| market_exhaustion, exhaustion_zscore, rsi_micro = _compute_exhaustion_zscore(bars) | |
| if _exh_pre is not None: | |
| market_exhaustion = _exh_pre | |
| # ── OSI (bidireccional v6.0) ────────────────────────────────────────────── | |
| osi, kill_score, kill_threshold, osi_components = _compute_osi( | |
| of, bars_held, current_pnl, trade_type, trade_side | |
| ) | |
| math_ms = (time.perf_counter() - t0) * 1000 | |
| # Zona OSI | |
| if osi >= OSI_FAST_EXIT: | |
| osi_zone = "fast_exit" | |
| elif osi >= OSI_KILL_ZONE: | |
| osi_zone = "kill" | |
| elif osi <= OSI_HOLD_ZONE: | |
| osi_zone = "hold" | |
| else: | |
| osi_zone = "watch" | |
| print(f"[F/MATH] {sym}({trade_side}): OSI={osi:.1f} zone={osi_zone} " | |
| f"vwap={price_vs_vwap} exh={market_exhaustion}(z={exhaustion_zscore:.2f}) " | |
| f"| {math_ms:.1f}ms") | |
| # ── COGNITIVE TRAILING STOP (máxima prioridad sobre profit flotante) ────── | |
| cts_triggered, cts_reason = _cognitive_trailing_stop( | |
| current_pnl_pct, velocity_zero_bars, c_style, | |
| trade_side, market_exhaustion, exhaustion_zscore, | |
| ) | |
| if cts_triggered: | |
| close_act = "buy" if trade_side == "short" else "sell" | |
| total_ms = (time.perf_counter() - t0) * 1000 | |
| print(f"[F/CTS] {sym}: COGNITIVE TRAILING STOP → {close_act.upper()} | {cts_reason} | {total_ms:.1f}ms") | |
| return _build_result( | |
| decision="SELL", confidence=0.92, kill_score=kill_score, | |
| trigger=f"cognitive_trailing_stop|{cts_reason}", | |
| urgency="critical", osi=osi, osi_zone=osi_zone, | |
| vwap=vwap, price_vs_vwap=price_vs_vwap, | |
| market_exhaustion=market_exhaustion, exhaustion_zscore=exhaustion_zscore, | |
| anchor_blocked=False, macro_kill=False, | |
| cts_triggered=True, short_stop_triggered=False, | |
| trade_side=trade_side, close_action=close_act, | |
| c_style=c_style, of=of, osi_components=osi_components, | |
| math_ms=math_ms, llm_ms=0.0, total_ms=total_ms, | |
| rsi_micro=rsi_micro, | |
| ) | |
| # ── SHORT HARD STOP (Herramienta H — incondicional) ────────────────────── | |
| short_stop_hit, short_stop_price, max_qty_hint = _check_short_hard_stop( | |
| trade_side, current_price, entry_price, account_balance | |
| ) | |
| if short_stop_hit: | |
| total_ms = (time.perf_counter() - t0) * 1000 | |
| print(f"[F/SHORT-STOP] {sym}: SHORT HARD STOP precio={current_price:.6f} >= stop={short_stop_price:.6f} | {total_ms:.1f}ms") | |
| return _build_result( | |
| decision="SELL", confidence=0.99, kill_score=1.0, | |
| trigger=f"short_hard_stop|price={current_price:.4f}>=stop={short_stop_price:.4f}", | |
| urgency="critical", osi=osi, osi_zone=osi_zone, | |
| vwap=vwap, price_vs_vwap=price_vs_vwap, | |
| market_exhaustion=market_exhaustion, exhaustion_zscore=exhaustion_zscore, | |
| anchor_blocked=False, macro_kill=False, | |
| cts_triggered=False, short_stop_triggered=True, | |
| trade_side=trade_side, close_action="buy", | |
| c_style=c_style, of=of, osi_components=osi_components, | |
| math_ms=math_ms, llm_ms=0.0, total_ms=total_ms, | |
| rsi_micro=rsi_micro, | |
| extra={"short_stop_price": short_stop_price, "max_qty_hint": max_qty_hint}, | |
| ) | |
| # ── MACRO KILL (Herramienta 4) ──────────────────────────────────────────── | |
| macro_kill, macro_reason = _check_macro_kill( | |
| trade_side, e_sentiment, macro_impact_weight, a_bias | |
| ) | |
| if macro_kill: | |
| close_act = "buy" if trade_side == "short" else "sell" | |
| total_ms = (time.perf_counter() - t0) * 1000 | |
| print(f"[F/MACRO] {sym}: MACRO KILL → {macro_reason} | {total_ms:.1f}ms") | |
| return _build_result( | |
| decision="SELL", confidence=0.88, kill_score=kill_score, | |
| trigger=f"macro_kill|{macro_reason}", | |
| urgency="high", osi=osi, osi_zone=osi_zone, | |
| vwap=vwap, price_vs_vwap=price_vs_vwap, | |
| market_exhaustion=market_exhaustion, exhaustion_zscore=exhaustion_zscore, | |
| anchor_blocked=False, macro_kill=True, | |
| cts_triggered=False, short_stop_triggered=False, | |
| trade_side=trade_side, close_action=close_act, | |
| c_style=c_style, of=of, osi_components=osi_components, | |
| math_ms=math_ms, llm_ms=0.0, total_ms=total_ms, | |
| rsi_micro=rsi_micro, | |
| ) | |
| # ── ANCHOR CORRELATION CIRCUIT BREAKER (Herramienta 3) ──────────────────── | |
| anchor_blocked, anchor_reason = _check_anchor_correlation( | |
| trade_side, anchor_correlation, price_vs_vwap, a_bias | |
| ) | |
| if anchor_blocked: | |
| # Solo bloquea NUEVAS entradas; si ya estamos en posición, no cierra | |
| # (el bloqueo de entrada lo maneja orders_processor.py con este flag) | |
| print(f"[F/ANCHOR] {sym}: ANCHOR BLOCKED ({anchor_reason}) — flag en payload") | |
| # ── PIPELINE OSI + LLM (heredado v5.0) ──────────────────────────────────── | |
| llm_ms = 0.0 | |
| close_action = "buy" if trade_side == "short" else "sell" | |
| if osi_zone == "fast_exit": | |
| decision = "SELL" | |
| confidence = round(kill_score, 4) | |
| trigger = f"osi_fast_exit={osi:.0f}" | |
| urgency = "critical" | |
| elif osi_zone == "hold": | |
| decision = "HOLD" | |
| confidence = round(1.0 - kill_score, 4) | |
| trigger = f"osi_hold={osi:.0f}" | |
| urgency = "low" | |
| elif osi_zone == "kill" and kill_score >= kill_threshold: | |
| decision = "SELL" | |
| confidence = round(kill_score, 4) | |
| trigger = f"osi_kill_math={osi:.0f}" | |
| urgency = "high" | |
| else: | |
| llm_result = await _llm_panic_trigger( | |
| osi, kill_score, of, sym, trade_type, bars_held, trade_side | |
| ) | |
| llm_ms = llm_result.get("_llm_ms", 0.0) | |
| action = llm_result.get("action", "HOLD") | |
| decision = "SELL" if action == "KILL_TRADE" else "HOLD" | |
| confidence = round(kill_score if decision == "SELL" else 1.0 - kill_score, 4) | |
| trigger = f"osi_watch_llm={osi:.0f}_action={action}" | |
| urgency = "medium" if decision == "SELL" else "low" | |
| # Override kill_score >= 0.85 (heredado) | |
| if kill_score >= 0.85 and decision == "HOLD": | |
| decision = "SELL" | |
| confidence = kill_score | |
| trigger = f"f_override_osi={osi:.0f}" | |
| urgency = "critical" | |
| total_ms = (time.perf_counter() - t0) * 1000 | |
| print(f"[F/TOTAL] {sym}({trade_side}): {decision} osi={osi:.0f} zone={osi_zone} " | |
| f"conf={confidence:.3f} math={math_ms:.1f}ms llm={llm_ms:.1f}ms total={total_ms:.1f}ms") | |
| return _build_result( | |
| decision=decision, confidence=confidence, kill_score=kill_score, | |
| trigger=trigger, urgency=urgency, | |
| osi=osi, osi_zone=osi_zone, | |
| vwap=vwap, price_vs_vwap=price_vs_vwap, | |
| market_exhaustion=market_exhaustion, exhaustion_zscore=exhaustion_zscore, | |
| anchor_blocked=anchor_blocked, macro_kill=macro_kill, | |
| cts_triggered=False, short_stop_triggered=False, | |
| trade_side=trade_side, close_action=close_action, | |
| c_style=c_style, of=of, osi_components=osi_components, | |
| math_ms=math_ms, llm_ms=llm_ms, total_ms=total_ms, | |
| rsi_micro=rsi_micro, | |
| ) | |
| def _build_result( | |
| decision, confidence, kill_score, trigger, urgency, | |
| osi, osi_zone, vwap, price_vs_vwap, | |
| market_exhaustion, exhaustion_zscore, | |
| anchor_blocked, macro_kill, | |
| cts_triggered, short_stop_triggered, | |
| trade_side, close_action, c_style, | |
| of, osi_components, math_ms, llm_ms, total_ms, | |
| rsi_micro=50.0, extra: dict = None, | |
| ) -> dict: | |
| """Construye el payload de respuesta unificado v6.0.""" | |
| result = { | |
| # Core (compatibilidad v5.0) | |
| "decision": decision, | |
| "confidence": confidence, | |
| "kill_score": kill_score, | |
| "trigger": trigger, | |
| "urgency": urgency, | |
| "action": decision, # alias legacy | |
| "kill": decision == "SELL", | |
| # v6.0: Bidireccional | |
| "trade_side": trade_side, | |
| "close_action": close_action, # "sell"=cierra long, "buy"=cierra short | |
| "c_style": c_style, | |
| # v6.0: Herramientas cognitivas | |
| "vwap": round(vwap, 8), | |
| "price_vs_vwap": price_vs_vwap, | |
| "market_exhaustion": market_exhaustion, | |
| "exhaustion_zscore": exhaustion_zscore, | |
| "rsi_micro": rsi_micro, | |
| "anchor_blocked": anchor_blocked, | |
| "macro_kill": macro_kill, | |
| "cognitive_trailing_triggered": cts_triggered, | |
| "short_stop_triggered": short_stop_triggered, | |
| # OSI | |
| "osi": osi, | |
| "osi_zone": osi_zone, | |
| "osi_components": osi_components, | |
| # OrderFlow (legacy) | |
| "orderflow": of, | |
| # Telemetría | |
| "_math_ms": round(math_ms, 2), | |
| "_llm_ms": round(llm_ms, 1), | |
| "_total_ms": round(total_ms, 1), | |
| "cerebro": "F", | |
| "version": "6.0-APEX-POLIMÓRFICO", | |
| } | |
| if extra: | |
| result.update(extra) | |
| return result | |
| # ── Endpoints FastAPI ───────────────────────────────────────────────────────── | |
| async def root(): | |
| return {"status": "online", "cerebro": "F", "version": "6.0-APEX-POLIMÓRFICO"} | |
| async def health(): | |
| return { | |
| "status": "online", "cerebro": "F", "version": "6.0-APEX-POLIMÓRFICO", | |
| "features": ["cognitive_trailing_stop", "bidirectional_long_short", | |
| "vwap_dynamic", "entropy_zscore_obi", "cross_asset_correlation", | |
| "news_impact_weight", "short_hard_stop"], | |
| "osi_thresholds": {"fast_exit": OSI_FAST_EXIT, "kill_zone": OSI_KILL_ZONE, "hold_zone": OSI_HOLD_ZONE}, | |
| "cts_config": { | |
| "profit_floor_scalp_pct": CTS_PROFIT_FLOOR_SCALP, | |
| "profit_floor_momentum_pct": CTS_PROFIT_FLOOR_MOMENTUM, | |
| "velocity_stall_bars_scalp": CTS_VELOCITY_STALL_BARS, | |
| "velocity_stall_bars_mom": CTS_VELOCITY_STALL_BARS_M, | |
| }, | |
| "short_config": {"stop_pct": SHORT_STOP_PCT, "max_account_risk_pct": MAX_ACCOUNT_RISK_PCT}, | |
| } | |
| async def analyze_exit(request: Request): | |
| try: payload = await request.json() | |
| except Exception: return JSONResponse({"error": "JSON inválido"}, status_code=400) | |
| result = await _liquidate(payload) | |
| return JSONResponse(result) | |
| async def analyze_compat(request: Request): | |
| return await analyze_exit(request) | |
| async def analyze_batch(request: Request): | |
| """Procesamiento batch paralelo — compatible con CerebroFMux.""" | |
| import asyncio | |
| try: payload = await request.json() | |
| except Exception: return JSONResponse({"error": "JSON inválido"}, status_code=400) | |
| positions = payload.get("positions", []) | |
| if not positions: | |
| return JSONResponse({"results": [], "latency_ms": 0}) | |
| t0 = time.perf_counter() | |
| tasks = [_liquidate(pos) for pos in positions] | |
| results = await asyncio.gather(*tasks, return_exceptions=True) | |
| safe_results = [] | |
| for r in results: | |
| if isinstance(r, Exception): | |
| safe_results.append({"decision": "HOLD", "confidence": 0.5, | |
| "kill_score": 0.25, "trigger": f"batch_error:{str(r)[:40]}", | |
| "urgency": "low", "action": "HOLD", "kill": False}) | |
| else: | |
| safe_results.append(r) | |
| lat = round((time.perf_counter() - t0) * 1000, 1) | |
| print(f"[F/BATCH] {len(safe_results)} posiciones en {lat}ms") | |
| return JSONResponse({"results": safe_results, "latency_ms": lat}) | |
| async def orderflow_only(request: Request): | |
| try: payload = await request.json() | |
| except Exception: return JSONResponse({"error": "JSON inválido"}, status_code=400) | |
| bars = payload.get("bars", []) | |
| of = _compute_orderflow_from_bars(bars) | |
| vwap, pvwap = _compute_vwap(bars) | |
| exh, ezsc, rsi = _compute_exhaustion_zscore(bars) | |
| of.update({"vwap": vwap, "price_vs_vwap": pvwap, | |
| "market_exhaustion": exh, "exhaustion_zscore": ezsc, "rsi_micro": rsi}) | |
| return JSONResponse(of) | |
| print("[F] ✅ The Liquidator v6.0 APEX POLIMÓRFICO — CTS + Bidireccional + 4 Herramientas cognitivas") | |