APEX_BRAIN_G / server_G.py
peruchia's picture
Upload server_G.py
29baa9d verified
Raw
History Blame Contribute Delete
29.2 kB
"""
server_G.py β€” Cerebro G β€” LiquidityStrategist v4.0 (APEX KMeans+LLM Auditor)
=============================================================================
v4.0 β€” RemodelaciΓ³n segΓΊn Plan Maestro APEX-ASYMMETRIC SWARM (Fase 3):
ARQUITECTURA LLM+MATH (filosofΓ­a de Cerebro A):
FASE MATH (<5ms) β€” LocalizaciΓ³n de zonas institucionales:
Β· Volume Profile completo: POC, VAL, VAH (70% del volumen).
Β· K-Means Clustering (numpy puro): agrupa los picos de volumen histΓ³rico
en K=3 clusters para detectar los Nodos de Alto Volumen (HVN) EXACTOS.
Sin aproximaciones: el centroide de cada cluster = HVN real del mercado.
Β· Fibonacci Pivots: niveles F236, F382, F500, F618, F786.
Β· Breakout Score: quΓ© tan cerca estΓ‘ el precio de cruzar un HVN.
Β· SeΓ±al clara (POC_HOT, POC_BOUNCE, ABOVE_VAH, BELOW_VAL): retorno
inmediato sin LLM (<5ms) β†’ [G/MATH-CLEAR].
FASE LLM (Auditor de Rupturas) β€” SOLO en zona ambigua:
· El LLM actúa ÚNICAMENTE como auditor de breakouts.
Β· Recibe: {"hvn_dist_pct": X, "price_vs_poc": Y, "vol_trend": "up|down"}
Β· Pregunta: ΒΏEs este breakout de HVN genuino o una trampa institucional?
Β· Output estricto: {"decision": "BUY|WAIT", "type": "scalp|long", "conf": 0.XX}
Β· Si LLM falla β†’ Math da la decisiΓ³n conservadora (WAIT).
ASIMETRÍA por RANK (heredada de v3.1, mejorada):
RANK 1: Math + LLM si ambiguo
RANK 2+: 100% Math (LLM PROHIBIDA) β†’ [G/MATH-ONLY]
ENDPOINTS:
POST /batch β†’ lote asimΓ©trico (endpoint principal)
POST /analyze_liquidity β†’ anΓ‘lisis individual
GET /health
GET /
TELEMETRÍA:
[G/MATH-CLEAR] sym: decision conf | Xms
[G/LLM] sym: decision conf | Xms (auditor de ruptura)
[G/MATH-ONLY] sym: decision conf | Xms (rank 2+)
[G/BATCH] N activos | LLM=X Clear=X Math=X | Xms
"""
import os, json, re, time, threading, math
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import httpx # BitNet v6.0
try:
import numpy as np
_NP_OK = True
except ImportError:
_NP_OK = False
app = FastAPI(title="Cerebro G β€” LiquidityStrategist v4.0 KMeans+LLM")
# ── ConfiguraciΓ³n ──────────────────────────────────────────────────────────────
MODEL_PATH = os.environ.get("MODEL_PATH", "/models/ggml-model-i2_s.gguf")
N_CTX = int(os.environ.get("N_CTX", "128"))
N_THREADS = int(os.environ.get("N_THREADS", "2"))
N_BATCH = int(os.environ.get("N_BATCH", "64"))
# ── BitNet Infrastructure v6.0 ───────────────────────────────────────────────
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.05,
"top_p": 0.95, "top_k": 5, "repeat_penalty": 1.0, "stream": False,
"stop": ["<|eot_id|>", "<|end_of_text|>"]}
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]}
VP_MIN_BARS = int(float(os.environ.get("VP_MIN_BARS", "5")))
KILL_ZONE_PCT = float(os.environ.get("KILL_ZONE_PCT", "0.020"))
HOT_ZONE_PCT = float(os.environ.get("HOT_ZONE_PCT", "0.005"))
CONF_VP_STRONG = float(os.environ.get("CONF_VP_STRONG", "0.72"))
CONF_VP_WEAK = float(os.environ.get("CONF_VP_WEAK", "0.28"))
CACHE_TTL = float(os.environ.get("CACHE_TTL", "90.0"))
AMBIG_CONF_LOW = float(os.environ.get("AMBIG_CONF_LOW", "0.48"))
AMBIG_CONF_HIGH = float(os.environ.get("AMBIG_CONF_HIGH", "0.58"))
KMEANS_K = int(os.environ.get("KMEANS_K", "3")) # clusters HVN
# ── BitNet v6.0: modelo servido por llama-server externo (ik_llama.cpp) ──────
# llm = Llama(...) eliminado β€” inferencias via HTTP a BITNET_BASE
print(f"[G] βœ… LiquidityStrategist v4.0 KMeans+BitNet β€” numpy={'βœ…' if _NP_OK else '⚠️'}")
print(f"[G] BitNet server: {BITNET_BASE} | n_ctx={N_CTX}")
# ── Cache ──────────────────────────────────────────────────────────────────────
_G_CACHE: dict = {}
_G_LOCK = threading.Lock()
def _cache_get(sym: str):
with _G_LOCK:
e = _G_CACHE.get(sym)
if e and (time.time() - e["ts"]) < CACHE_TTL:
age = int(time.time() - e["ts"])
return {**e["result"], "_cached": True, "_cache_age_s": age}
return None
def _cache_set(sym: str, result: dict):
with _G_LOCK:
_G_CACHE[sym] = {"result": result, "ts": time.time()}
# ══════════════════════════════════════════════════════════════════════════════
# HELPER: extracciΓ³n robusta de barras desde cualquier payload
# ══════════════════════════════════════════════════════════════════════════════
def _extract_bars(payload: dict) -> list:
bars = payload.get("bars") or payload.get("b") or []
return bars if isinstance(bars, list) else []
# ══════════════════════════════════════════════════════════════════════════════
# K-MEANS CLUSTERING (numpy puro β€” sin sklearn)
# Detecta Nodos de Alto Volumen (HVN) exactos desde el Volume Profile
# ══════════════════════════════════════════════════════════════════════════════
def _kmeans_hvn(price_levels: list, vol_levels: list, k: int = 3,
max_iter: int = 20) -> list:
"""
K-Means simplificado para detectar HVN (High Volume Nodes).
Entrada:
price_levels: lista de precios de cada nivel del VP
vol_levels: volumen correspondiente a cada nivel
k: nΓΊmero de clusters (default 3 = bajo/medio/alto volumen)
Retorna:
lista de k HVNs (precios de los centroides con mayor volumen acumulado)
Algoritmo:
1. Inicializar centroides con K-Means++ (distribuciΓ³n uniforme en el rango)
2. Iterar: asignar cada nivel al centroide mΓ‘s cercano
3. Recalcular centroides como promedio ponderado por volumen
4. Seleccionar los k centroides con mayor volumen promedio del cluster
"""
if not price_levels or len(price_levels) < k:
return price_levels[:k] if price_levels else []
if _NP_OK:
prices = np.array(price_levels, dtype=float)
vols = np.array(vol_levels, dtype=float)
# InicializaciΓ³n: centroides uniformemente distribuidos en el rango
p_min, p_max = float(prices.min()), float(prices.max())
if p_min == p_max:
return [p_min] * k
centroids = np.linspace(p_min, p_max, k)
for _ in range(max_iter):
# AsignaciΓ³n: cada precio al centroide mΓ‘s cercano
dists = np.abs(prices[:, None] - centroids[None, :])
labels = np.argmin(dists, axis=1)
# ActualizaciΓ³n: centroide = media ponderada por volumen del cluster
new_centroids = np.zeros(k)
for j in range(k):
mask = labels == j
if mask.sum() > 0:
w = vols[mask]
new_centroids[j] = float(np.average(prices[mask], weights=w))
else:
new_centroids[j] = centroids[j]
if np.allclose(centroids, new_centroids, rtol=1e-4):
break
centroids = new_centroids
# Seleccionar los HVNs: centroides con mayor volumen promedio del cluster
cluster_vols = []
labels = np.argmin(np.abs(prices[:, None] - centroids[None, :]), axis=1)
for j in range(k):
mask = labels == j
avg_vol = float(vols[mask].mean()) if mask.sum() > 0 else 0.0
cluster_vols.append((avg_vol, float(centroids[j])))
# Ordenar por volumen descendente y retornar precios
cluster_vols.sort(key=lambda x: x[0], reverse=True)
return [round(p, 6) for _, p in cluster_vols]
else:
# Fallback puro-Python: Top-K por volumen sin clustering real
paired = sorted(zip(vol_levels, price_levels), reverse=True)
return [round(p, 6) for _, p in paired[:k]]
# ══════════════════════════════════════════════════════════════════════════════
# VOLUME PROFILE + FIBONACCI + HVN K-MEANS (FASE MATH COMPLETA)
# ══════════════════════════════════════════════════════════════════════════════
def _compute_vp_kmeans(bars: list, px: float) -> dict:
"""
Volume Profile completo + K-Means HVN + Fibonacci en <5ms.
Nuevo en v4.0:
hvn_levels: lista de 3 HVNs exactos por K-Means (precios)
hvn_nearest: HVN mΓ‘s cercano al precio actual
hvn_dist_pct: distancia % del precio al HVN mΓ‘s cercano
breakout_score: quΓ© tan cerca estΓ‘ el precio de cruzar un HVN (0-1)
vol_trend: tendencia del volumen (last 3 vs prev 3 barras)
"""
t0 = time.perf_counter()
closes, volumes = [], []
for b in bars:
c = b.get("c") or b.get("close") or 0.0
v = b.get("v") or b.get("volume") or 0.0
if c > 0:
closes.append(float(c))
volumes.append(float(v) or 1.0)
def math_ms():
return (time.perf_counter() - t0) * 1000
if len(closes) < VP_MIN_BARS or px <= 0:
return {
"poc": px, "val": px, "vah": px,
"poc_dist_pct": 0.0, "zone": "degenerate",
"conf_math": CONF_VP_WEAK, "is_ambiguous": False,
"fib_levels": {}, "dec_math": "WAIT",
"hvn_levels": [], "hvn_nearest": px, "hvn_dist_pct": 0.0,
"breakout_score": 0.0, "vol_trend": "flat",
"logic": f"Sin datos VP ({len(closes)} barras < {VP_MIN_BARS}). Degradado.",
"_math_ms": round(math_ms(), 2),
}
price_min, price_max = min(closes), max(closes)
# ── Fibonacci ─────────────────────────────────────────────────────────────
fib_range = price_max - price_min
fib_levels = {}
if fib_range > 0:
for ratio, name in [(0.236, "F236"), (0.382, "F382"), (0.500, "F500"),
(0.618, "F618"), (0.786, "F786")]:
fib_levels[name] = round(price_max - fib_range * ratio, 6)
if price_max == price_min:
poc = val = vah = price_min
conf_math = CONF_VP_WEAK
zone = "degenerate"
logic = f"VAL=VAH=${poc:.4f}. Sin rango detectable."
poc_dist_pct = 0.0
vp_prices = [price_min] * len(closes)
vp_vols = volumes
else:
n_levels = min(10, len(closes))
step = (price_max - price_min) / n_levels
levels = []
for i in range(n_levels):
ll = price_min + i * step
lh = ll + step
lv = sum(v for c, v in zip(closes, volumes) if ll <= c < lh)
mid = (ll + lh) / 2
levels.append({"low": ll, "high": lh, "mid": mid, "vol": lv})
poc_level = max(levels, key=lambda x: x["vol"])
poc = poc_level["mid"]
total_vol = sum(lv["vol"] for lv in levels) or 1.0
sorted_levels = sorted(levels, key=lambda x: x["vol"], reverse=True)
cum_vol, val, vah = 0.0, poc, poc
for lv in sorted_levels:
cum_vol += lv["vol"]
val = min(val, lv["low"])
vah = max(vah, lv["high"])
if cum_vol >= total_vol * 0.70:
break
poc_dist_pct = abs(px - poc) / poc if poc > 0 else 0.0
vp_prices = [lv["mid"] for lv in levels]
vp_vols = [lv["vol"] for lv in levels]
if poc_dist_pct <= HOT_ZONE_PCT:
zone, conf_math = "POC_HOT", CONF_VP_STRONG
logic = f"Precio en POC. Magneto {poc_dist_pct*100:.2f}% del POC ${poc:.4f}."
elif poc_dist_pct <= KILL_ZONE_PCT:
zone, conf_math = "POC_BOUNCE", 0.60
logic = f"Precio en Kill Zone. POC ${poc:.4f} dist={poc_dist_pct*100:.2f}%."
elif px > vah:
zone, conf_math = "ABOVE_VAH", 0.62
logic = f"Precio ENCIMA del VAH ${vah:.4f}. Zona de extensiΓ³n."
elif px < val:
zone, conf_math = "BELOW_VAL", CONF_VP_WEAK
logic = f"Precio DEBAJO del VAL ${val:.4f}. Zona de riesgo."
else:
zone, conf_math = "VALUE_AREA", 0.50
logic = f"Precio dentro del Value Area [{val:.4f}–{vah:.4f}]."
# ── K-Means HVN ───────────────────────────────────────────────────────────
hvn_levels = _kmeans_hvn(vp_prices, vp_vols, k=KMEANS_K)
hvn_nearest = min(hvn_levels, key=lambda h: abs(px - h)) if hvn_levels else px
hvn_dist_pct = abs(px - hvn_nearest) / hvn_nearest if hvn_nearest > 0 else 0.0
# Breakout Score: 1.0 si estΓ‘ cruzando el HVN ahora, 0.0 si estΓ‘ lejos
breakout_score = max(0.0, 1.0 - hvn_dist_pct / max(KILL_ZONE_PCT, 0.001))
breakout_score = round(min(1.0, breakout_score), 3)
# Tendencia del volumen: last 3 vs prev 3 barras
vol_trend = "flat"
if len(volumes) >= 6:
v_recent = sum(volumes[-3:]) / 3
v_prev = sum(volumes[-6:-3]) / 3
if v_recent > v_prev * 1.2:
vol_trend = "up"
elif v_recent < v_prev * 0.8:
vol_trend = "down"
# DecisiΓ³n matemΓ‘tica base
if zone in ("POC_HOT", "POC_BOUNCE", "ABOVE_VAH"):
dec_math = "BUY"
elif zone == "BELOW_VAL":
dec_math = "WAIT"
else:
dec_math = "WAIT" # VALUE_AREA ambigua β†’ LLM decide
# AmbigΓΌedad: VALUE_AREA + conf_math entre AMBIG_CONF_LOW y AMBIG_CONF_HIGH
is_ambiguous = (zone == "VALUE_AREA" and
AMBIG_CONF_LOW <= conf_math <= AMBIG_CONF_HIGH)
return {
"poc": round(poc, 6),
"val": round(val, 6),
"vah": round(vah, 6),
"poc_dist_pct": round(poc_dist_pct, 5),
"zone": zone,
"conf_math": round(conf_math, 4),
"is_ambiguous": is_ambiguous,
"fib_levels": fib_levels,
"dec_math": dec_math,
# KMeans HVN (nuevos v4.0)
"hvn_levels": hvn_levels,
"hvn_nearest": round(hvn_nearest, 6),
"hvn_dist_pct": round(hvn_dist_pct, 5),
"breakout_score": breakout_score,
"vol_trend": vol_trend,
"logic": logic,
"_math_ms": round(math_ms(), 2),
}
# ══════════════════════════════════════════════════════════════════════════════
# TIPO DE TRADE (heredado de v3.1)
# ══════════════════════════════════════════════════════════════════════════════
def _classify_trade_type(zone: str, conf: float, cronos: float, px: float,
poc: float, vah: float, val: float) -> str:
poc_dist = abs(px - poc) / poc if poc > 0 else 0.0
if poc_dist <= HOT_ZONE_PCT and cronos >= 0.70:
return "scalp"
if zone == "ABOVE_VAH" and conf >= 0.65:
return "long"
if zone in ("POC_BOUNCE", "POC_HOT") and conf >= 0.65:
return "scalp"
return "scalp"
# ══════════════════════════════════════════════════════════════════════════════
# FASE LLM β€” Auditor de Rupturas (solo rank=1 en zona ambigua)
# ══════════════════════════════════════════════════════════════════════════════
async def _llm_breakout_audit(math_data: dict, sym: str, px: float,
cronos: float, a_data: dict, b_data: dict) -> dict:
"""
El LLM actΓΊa como Auditor de Rupturas β€” NO calcula nada.
Recibe el panel de control destilado por MATH:
{"hvn_dist_pct": X, "price_vs_poc": Y, "vol_trend": "up|down",
"breakout_score": Z, "zone": "VALUE_AREA", "cronos": X}
Pregunta: ΒΏEs este breakout de HVN genuino o una trampa institucional?
Output: {"decision": "BUY|WAIT", "type": "scalp|long", "conf": 0.XX}
"""
t0_llm = time.perf_counter()
hvn_dist = math_data.get("hvn_dist_pct", 0.0)
bk_score = math_data.get("breakout_score", 0.0)
vol_trend = math_data.get("vol_trend", "flat")[0] # "u"/"d"/"f"
zone = math_data.get("zone", "VALUE_AREA")[:3]
poc_d = math_data.get("poc_dist_pct", 0.0)
g_sent = a_data.get("bias", "neutral")[0] if a_data else "n"
b_trend = b_data.get("trend", "side")[0] if b_data else "s"
llm_prompt = (
"<|im_start|>system\n"
'Breakout auditor. SOLO JSON: {"decision":"BUY|WAIT","type":"scalp|long","conf":0.XX}.\n'
"true=breakout real | false=trampa. Sin pensar.\n"
"<|im_end|>\n"
"<|im_start|>user\n"
f'{{"s":"{sym[:8]}","cr":{cronos:.2f},"hvn":{hvn_dist:.3f},'
f'"bk":{bk_score:.2f},"vt":"{vol_trend}","z":"{zone}",'
f'"poc":{poc_d:.3f},"a":"{g_sent}","b":"{b_trend}"}}\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 = "{" + raw_out_text
m = re.search(r'\{[^{}]*"decision"\s*:\s*"(BUY|WAIT)"[^{}]*\}', raw, re.IGNORECASE)
if m:
parsed = json.loads(m.group())
dec = parsed.get("decision", "WAIT").upper()
ttype = parsed.get("type", "scalp").lower()
conf = float(parsed.get("conf", 0.55))
if dec not in ("BUY", "WAIT"):
dec = "WAIT"
if ttype not in ("scalp", "long"):
ttype = "scalp"
conf = round(max(0.10, min(0.95, conf)), 4)
print(f"[G/LLM] {sym}: {dec} {ttype} conf={conf:.2f} | {llm_ms:.1f}ms")
return {"decision": dec, "type": ttype, "confidence": conf,
"_llm_ms": round(llm_ms, 1), "_source": "llm"}
# Parseo parcial
dec = "BUY" if "BUY" in raw.upper() else "WAIT"
print(f"[G/LLM] {sym}: partial={dec} | {llm_ms:.1f}ms")
return {"decision": dec, "type": "scalp", "confidence": 0.52,
"_llm_ms": round(llm_ms, 1), "_source": "llm_partial"}
except Exception as e:
llm_ms = (time.perf_counter() - t0_llm) * 1000
print(f"[G/LLM-ERR] {sym}: {type(e).__name__}: {str(e)[:50]} | {llm_ms:.1f}ms β†’ WAIT")
return {"decision": "WAIT", "type": "scalp", "confidence": 0.45,
"_llm_ms": round(llm_ms, 1), "_source": "llm_fallback"}
# ══════════════════════════════════════════════════════════════════════════════
# ANÁLISIS INDIVIDUAL (combina MATH + LLM si ambiguo)
# ══════════════════════════════════════════════════════════════════════════════
async def _analyze_single(sym: str, px: float, bars: list, cronos: float,
a_data: dict, b_data: dict, rank: int = 1) -> dict:
"""
Pipeline completo para un solo activo.
rank=1: MATH + LLM si ambiguo β†’ [G/MATH-CLEAR] o [G/LLM]
rank>1: solo MATH β†’ [G/MATH-ONLY]
"""
t0 = time.perf_counter()
# ── FASE MATH ─────────────────────────────────────────────────────────────
math_data = _compute_vp_kmeans(bars, px)
math_ms = math_data["_math_ms"]
zone = math_data["zone"]
conf_math = math_data["conf_math"]
dec_math = math_data["dec_math"]
is_ambiguous = math_data["is_ambiguous"]
poc = math_data["poc"]
vah = math_data["vah"]
val = math_data["val"]
logic = math_data["logic"]
trade_type = _classify_trade_type(zone, conf_math, cronos, px, poc, vah, val)
# ── SeΓ±al clara β†’ sin LLM ─────────────────────────────────────────────────
if zone != "VALUE_AREA" or not is_ambiguous or rank > 1:
label = "MATH-CLEAR" if rank == 1 else "MATH-ONLY"
total_ms = (time.perf_counter() - t0) * 1000
print(f"[G/{label}] {sym}: {dec_math} {trade_type} conf={conf_math:.2f} "
f"zone={zone} | {total_ms:.1f}ms")
return {
"decision": dec_math,
"type": trade_type,
"confidence": conf_math,
"poc": poc,
"vah": vah,
"val": val,
"logic": logic,
"zone": zone,
"hvn_levels": math_data["hvn_levels"],
"hvn_nearest": math_data["hvn_nearest"],
"breakout_score": math_data["breakout_score"],
"fib_levels": math_data["fib_levels"],
"cerebro": "G",
"_math_ms": round(math_ms, 2),
"_llm_ms": 0.0,
"_total_ms": round(total_ms, 1),
}
# ── Zona ambigua (VALUE_AREA) + rank=1 β†’ LLM Auditor ────────────────────
llm_result = await _llm_breakout_audit(math_data, sym, px, cronos, a_data, b_data)
llm_ms = llm_result.get("_llm_ms", 0.0)
decision = llm_result.get("decision", dec_math)
trade_type = llm_result.get("type", trade_type)
confidence = llm_result.get("confidence", conf_math)
total_ms = (time.perf_counter() - t0) * 1000
return {
"decision": decision,
"type": trade_type,
"confidence": confidence,
"poc": poc,
"vah": vah,
"val": val,
"logic": logic + f" | LLM-audit: {decision}",
"zone": zone,
"hvn_levels": math_data["hvn_levels"],
"hvn_nearest": math_data["hvn_nearest"],
"breakout_score": math_data["breakout_score"],
"fib_levels": math_data["fib_levels"],
"cerebro": "G",
"_math_ms": round(math_ms, 2),
"_llm_ms": round(llm_ms, 1),
"_total_ms": round(total_ms, 1),
}
# ── Endpoints FastAPI ─────────────────────────────────────────────────────────
@app.get("/")
async def root():
return {
"status": "online",
"cerebro": "G",
"version": "4.0-APEX-KMeans+LLM",
"endpoints": ["/batch", "/analyze_liquidity", "/health"],
}
@app.get("/health")
async def health():
return {
"status": "online",
"cerebro": "G",
"version": "4.0-APEX-KMeans+LLM",
"model": "BitNet-b1.58-2B-4T-i2_s (ik_llama.cpp)", "bitnet_server": BITNET_BASE,
"numpy_ok": _NP_OK,
"kmeans_k": KMEANS_K,
"features": {
"math": "VP + K-Means HVN + Fibonacci (<5ms)",
"llm": "Auditor de Rupturas (solo rank=1 zona ambigua)",
"async": "rank=1 LLM|CLEAR + rank2+ MATH-ONLY",
},
}
@app.post("/analyze_liquidity")
async def analyze_liquidity(request: Request):
try:
payload = await request.json()
except Exception:
return JSONResponse({"error": "JSON invΓ‘lido"}, status_code=400)
sym = str(payload.get("sym", "?"))
px = float(payload.get("px", 0))
cronos = float(payload.get("cronos_score", 0.5))
rank = int(payload.get("rank", 1))
bars = _extract_bars(payload)
a_data = payload.get("A", {})
b_data = payload.get("B", {})
cached = _cache_get(sym)
if cached:
return JSONResponse(cached)
result = await _analyze_single(sym, px, bars, cronos, a_data, b_data, rank)
_cache_set(sym, result)
return JSONResponse(result)
@app.post("/batch")
async def batch(request: Request):
"""
Procesamiento asimΓ©trico en lote:
Activo rank=1 β†’ Math + LLM si ambiguo
Activos rank>1 β†’ 100% Math (LLM prohibida)
"""
try:
payload = await request.json()
except Exception:
return JSONResponse({"error": "JSON invΓ‘lido"}, status_code=400)
assets = payload.get("assets", [])
if not assets:
return JSONResponse({"error": "assets vacΓ­o"}, status_code=400)
t0_batch = time.perf_counter()
results = {}
llm_count, clear_count, math_count = 0, 0, 0
for asset in assets:
sym = str(asset.get("sym", "?"))
px = float(asset.get("px", 0))
cronos = float(asset.get("cronos_score", 0.5))
rank = int(asset.get("rank", asset.get("_rank", 0)) + 1) # 1-based
bars = _extract_bars(asset)
a_data = asset.get("A", {})
b_data = asset.get("B", {})
cached = _cache_get(sym)
if cached:
results[sym] = cached
math_count += 1
continue
result = await _analyze_single(sym, px, bars, cronos, a_data, b_data, rank)
_cache_set(sym, result)
results[sym] = result
src = result.get("_source", "math")
if "llm" in str(src):
llm_count += 1
elif result.get("_llm_ms", 0) == 0.0 and rank == 1:
clear_count += 1
else:
math_count += 1
batch_ms = (time.perf_counter() - t0_batch) * 1000
print(f"[G/BATCH] {len(assets)} activos | LLM={llm_count} Clear={clear_count} "
f"Math={math_count} | {batch_ms:.0f}ms")
return JSONResponse({
"results": results,
"llm_count": llm_count,
"math_count": math_count,
"cache_count": 0,
"latency_ms": round(batch_ms, 1),
})
@app.post("/slot_lock")
async def slot_lock(request: Request):
"""Endpoint de notificaciΓ³n de slot (heredado β€” no altera la lΓ³gica)."""
try:
body = await request.json()
except Exception:
return JSONResponse({"ok": True})
sym = body.get("sym", "?")
print(f"[G/SLOT-LOCK] {sym}: trade abierto β€” monitoreando")
return JSONResponse({"ok": True, "sym": sym})