""" 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 ───────────────────────────────────────────────────────── @app.get("/") async def root(): return {"status": "online", "cerebro": "F", "version": "6.0-APEX-POLIMÓRFICO"} @app.get("/health") 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}, } @app.post("/analyze_exit") 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) @app.post("/analyze") async def analyze_compat(request: Request): return await analyze_exit(request) @app.post("/analyze_batch") 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}) @app.post("/orderflow") 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")