viarl199020 commited on
Commit
def109d
Β·
verified Β·
1 Parent(s): ab8f1fb

Upload 2 files

Browse files
Files changed (2) hide show
  1. server_H.py +318 -484
  2. start_H.sh +18 -5
server_H.py CHANGED
@@ -1,536 +1,377 @@
1
  """
2
- server_H.py β€” Cerebro H β€” RiskAuditor v3.0 (APEX Kelly-Continuo + MonteCarlo)
3
- ===============================================================================
4
- v3.0 β€” RemodelaciΓ³n segΓΊn Plan Maestro APEX-ASYMMETRIC SWARM (Fase 4):
5
-
6
- ARQUITECTURA LLM+MATH (filosofΓ­a de Cerebro A):
7
-
8
- FASE MATH (<1ms) β€” 100% matemΓ‘tica, nunca necesita LLM:
9
- Β· Kelly Fraccional CONTINUO (numpy): no es Kelly clΓ‘sico escalar.
10
- Calcula f* = max(0, W/L - (1-W)/G) ajustado por:
11
- - Racha de pΓ©rdidas consecutivas (penalizaciΓ³n progresiva)
12
- - Cronos score (amplificador de seΓ±al)
13
- - Fase de bootstrap (<20 trades: override conservador 1%)
14
- Β· SimulaciΓ³n Monte Carlo Express (<1ms, 100 escenarios numpy):
15
- - Simula 100 trayectorias de P&L usando win_rate + avg_win/loss
16
- - Calcula probabilidad de ruina: P(drawdown > 20% en 20 trades)
17
- - Si P(ruina) > umbral crΓ­tico β†’ activa bandera de alerta
18
- Β· Z-Score de volatilidad reciente (para calibrar Kelly).
19
- Β· Toda la matemΓ‘tica en <1ms gracias a numpy vectorizado.
20
-
21
- FASE LLM (Interruptor de PΓ‘nico) β€” SOLO en condiciΓ³n CRÍTICA:
22
- Β· El LLM NO calcula Kelly ni Monte Carlo. Solo actΓΊa como veto.
23
- · Se activa ÚNICAMENTE si: P(ruina) > PANIC_RUIN_THRESHOLD AND
24
- drawdown_capital > PANIC_DRAWDOWN_PCT.
25
- Β· Pregunta: "ΒΏInyecto mΓ‘s capital ahora dado este riesgo acumulado?"
26
- Β· Output: {"veto": true|false, "motivo": "string"}
27
- Β· Si veto=true β†’ size_usd=0, vault_status="emergency_hold".
28
- Β· Si LLM falla β†’ math conservador decide (sin inyecciΓ³n extra).
29
-
30
- REGLAS:
31
- Β· 95% de las peticiones: Math responde en <1ms, LLM nunca se invoca.
32
- Β· 5% (crisis): LLM actΓΊa como cortocircuito de emergencia.
33
-
34
- OUTPUT (100% compatible con swarm_engine v16):
35
  {
36
- "pdt_ok": bool,
37
- "size_usd": float,
38
- "risk_warning": "none|streak_loss|high_zscore|monte_carlo_ruin|emergency",
39
- "vault_status": "protected|conservative|blocked_pdt|emergency_hold",
40
- "kelly_fraction": float,
41
- "zscore": float,
42
- # Nuevos v3.0:
43
- "monte_carlo_ruin_prob": float, # P(ruina en 20 trades)
44
- "kelly_continuous": float, # Kelly fraccional continuo ajustado
45
- "panic_activated": bool, # True si LLM fue invocado
46
- "_math_ms": float,
47
- "_llm_ms": float,
48
- "_total_ms": float,
 
 
49
  }
50
-
51
- TELEMETRÍA: [H/MATH] ms | [H/LLM] ms | [H/TOTAL] ms
52
  """
53
 
54
- import os, json, re, time, threading, math
55
  from fastapi import FastAPI, Request
56
  from fastapi.responses import JSONResponse
57
- import httpx # BitNet v6.0
58
 
59
  try:
60
  import numpy as np
61
- _NP_OK = True
62
  except ImportError:
63
- _NP_OK = False
64
 
65
- app = FastAPI(title="Cerebro H β€” RiskAuditor v3.0 Kelly+MonteCarlo")
66
 
67
  # ── Config ──────────────────────────────────────────────────────��─────────────
68
- MODEL_PATH = os.environ.get("MODEL_PATH", "/models/ggml-model-i2_s.gguf")
69
- N_CTX = int(os.environ.get("N_CTX", "128"))
70
- N_THREADS = int(os.environ.get("N_THREADS", "2"))
71
- N_BATCH = int(os.environ.get("N_BATCH", "64"))
72
-
73
- # ── BitNet Infrastructure v6.0 ───────────────────────────────────────────────
74
  BITNET_PORT = int(os.environ.get("BITNET_PORT", "8080"))
75
  BITNET_HOST = os.environ.get("BITNET_HOST", "127.0.0.1")
76
  BITNET_BASE = f"http://{BITNET_HOST}:{BITNET_PORT}"
77
- BITNET_TIMEOUT = float(os.environ.get("BITNET_TIMEOUT", "30.0"))
78
- LLM_MAX_TOKENS = int(os.environ.get("LLM_MAX_TOKENS", "48"))
79
-
80
- _bitnet_client = None
81
-
82
- def _get_bitnet_client():
 
 
 
 
 
 
 
 
 
 
 
83
  global _bitnet_client
84
  if _bitnet_client is None or _bitnet_client.is_closed:
85
  _bitnet_client = httpx.AsyncClient(
86
  base_url=BITNET_BASE,
87
- timeout=httpx.Timeout(BITNET_TIMEOUT),
88
- limits=httpx.Limits(max_connections=4, max_keepalive_connections=2),
 
89
  )
90
  return _bitnet_client
91
 
92
- async def _bitnet_infer(prompt_text: str, max_tokens: int = 48) -> dict:
93
- client = _get_bitnet_client()
94
- payload = {"prompt": prompt_text, "n_predict": max_tokens, "temperature": 0.05,
95
- "top_p": 0.95, "top_k": 5, "repeat_penalty": 1.0, "stream": False,
96
- "stop": ["<|eot_id|>", "<|end_of_text|>"]}
 
 
 
 
97
  try:
98
- resp = await client.post("/completion", json=payload)
99
  resp.raise_for_status()
100
- return {"raw": resp.json().get("content", "").strip(), "ok": True}
101
- except httpx.TimeoutException:
102
- return {"raw": "", "ok": False, "error": "TIMEOUT"}
103
- except Exception as e:
104
- return {"raw": "", "ok": False, "error": str(e)[:80]}
105
-
106
-
107
- # ParΓ‘metros Kelly
108
- KELLY_MAX_FRACTION = float(os.environ.get("KELLY_MAX", "0.05"))
109
- KELLY_MIN_FRACTION = float(os.environ.get("KELLY_MIN", "0.005"))
110
- ZSCORE_PANIC = float(os.environ.get("ZSCORE_PANIC", "2.5"))
111
- PDT_LIMIT = int(os.environ.get("PDT_LIMIT", "3"))
112
- PDT_MIN_BALANCE = float(os.environ.get("PDT_MIN", "25000"))
113
-
114
- # Monte Carlo
115
- MC_SCENARIOS = int(os.environ.get("MC_SCENARIOS", "100"))
116
- MC_HORIZON = int(os.environ.get("MC_HORIZON", "20")) # trades hacia adelante
117
- PANIC_RUIN_THRESHOLD = float(os.environ.get("PANIC_RUIN_THRESHOLD", "0.30")) # 30% prob ruina
118
- PANIC_DRAWDOWN_PCT = float(os.environ.get("PANIC_DRAWDOWN_PCT", "0.15")) # 15% drawdown
119
- RUIN_DRAWDOWN_DEF = float(os.environ.get("RUIN_DRAWDOWN_DEF", "0.20")) # 20% = "ruina"
120
-
121
- # Cache
122
- CACHE_TTL = float(os.environ.get("CACHE_TTL", "30.0"))
123
-
124
- # ── BitNet v6.0: modelo servido por llama-server externo ─────────────────────
125
- # llm = Llama(...) eliminado β€” inferencias via HTTP a BITNET_BASE
126
- print(f"[H] βœ… RiskAuditor v3.0 Kelly+MonteCarlo+BitNet β€” n_ctx={N_CTX}")
127
- print(f"[H] BitNet server: {BITNET_BASE} | numpy={'βœ…' if _NP_OK else '⚠️'} | MC={MC_SCENARIOS}")
128
-
129
- # ── Cache ──────────────────────────────────────────────────────────────────────
130
- _H_CACHE: dict = {}
131
- _H_LOCK = threading.Lock()
132
-
133
- def _cache_get(key: str):
134
- with _H_LOCK:
135
- e = _H_CACHE.get(key)
136
- if e and (time.time() - e["ts"]) < CACHE_TTL:
137
- return {**e["r"], "_cached": True}
138
- return None
139
-
140
- def _cache_set(key: str, r: dict):
141
- with _H_LOCK:
142
- _H_CACHE[key] = {"r": r, "ts": time.time()}
143
-
144
-
145
- # ══════════════════════════════════════════════════════════════════════════════
146
- # KELLY FRACCIONAL CONTINUO (numpy)
147
- # ══════════════════════════════════════════════════════════════════════════════
148
-
149
- def _kelly_continuous(win_rate: float, avg_win: float, avg_loss: float,
150
- streak_losses: int, cronos: float,
151
- n_trades: int, bootstrap_size: float) -> float:
152
- """
153
- Kelly Fraccional Continuo:
154
- f* = (W Γ— G - (1-W) Γ— L) / G donde G=avg_win, L=avg_loss, W=win_rate
155
 
156
- Ajustes:
157
- 1. Half-Kelly: f* /= 2 (conservador estΓ‘ndar)
158
- 2. Streak penalty: -15% por cada pΓ©rdida consecutiva (cap -45%)
159
- 3. Cronos amplifier: factor 0.8-1.2 segΓΊn signal strength
160
- 4. Bootstrap override: < 20 trades β†’ retornar bootstrap_size fijo
161
 
162
- Retorna: fracciΓ³n de Kelly ajustada (0.005 - KELLY_MAX_FRACTION)
 
163
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  if n_trades < 20:
165
- # Fase bootstrap: tamaΓ±o fijo conservador
166
- return max(KELLY_MIN_FRACTION, min(bootstrap_size, KELLY_MAX_FRACTION))
167
-
168
  if avg_loss <= 0 or avg_win <= 0:
169
- return KELLY_MIN_FRACTION
170
-
171
- # Kelly continuo: (WΓ—G - (1-W)Γ—L) / G
172
  kelly_raw = (win_rate * avg_win - (1 - win_rate) * avg_loss) / avg_win
173
-
174
- # Half-Kelly (conservador)
175
  kelly_half = kelly_raw / 2.0
176
-
177
- # PenalizaciΓ³n por racha de pΓ©rdidas consecutivas
178
- streak_pen = 1.0 - min(streak_losses * 0.15, 0.45) # -15%/pΓ©rdida, max -45%
179
-
180
- # Amplificador Cronos (0.80-1.20)
181
  cronos_amp = 0.80 + cronos * 0.40
182
-
183
- kelly_adj = kelly_half * streak_pen * cronos_amp
184
-
185
- return round(max(KELLY_MIN_FRACTION, min(KELLY_MAX_FRACTION, kelly_adj)), 6)
186
-
187
-
188
- # ══════════════════════════════════════════════════════════════════════════════
189
- # SIMULACIΓ“N MONTE CARLO EXPRESS (<1ms, 100 escenarios)
190
- # ══════════════════════════════════════════════════════════════════════════════
191
-
192
- def _monte_carlo_ruin(win_rate: float, avg_win_pct: float, avg_loss_pct: float,
193
- kelly_frac: float, balance: float,
194
- horizon: int = 20, scenarios: int = 100) -> float:
195
- """
196
- Simula `scenarios` trayectorias de P&L sobre `horizon` trades.
197
-
198
- Cada trade:
199
- P(win) = win_rate β†’ portfolio Γ— (1 + kelly_frac Γ— avg_win_pct)
200
- P(loss)= 1-win_rate β†’ portfolio Γ— (1 - kelly_frac Γ— avg_loss_pct)
201
-
202
- Ruina = drawdown > RUIN_DRAWDOWN_DEF del balance inicial.
203
-
204
- Retorna: P(ruina) entre 0.0 y 1.0.
205
- Target: <1ms con numpy.
206
- """
207
- if _NP_OK:
208
- # Vectorizado: scenarios Γ— horizon matriz de resultados
209
- np.random.seed(42) # reproducible (mismo seed por balance/win_rate)
210
- wins = np.random.random((scenarios, horizon)) < win_rate
211
-
212
- win_factor = 1.0 + kelly_frac * avg_win_pct
213
- loss_factor = 1.0 - kelly_frac * avg_loss_pct
214
-
215
- # Multiplicadores: (scenarios, horizon)
216
- factors = np.where(wins, win_factor, loss_factor)
217
- # Portfolio acumulado: producto acumulado de factores
218
- portfolios = np.cumprod(factors, axis=1) # (scenarios, horizon)
219
-
220
- # MΓ­nimo de cada trayectoria (peor punto)
221
- min_portfolios = portfolios.min(axis=1) # (scenarios,)
222
-
223
- # Ruina: mΓ­nimo < (1 - RUIN_DRAWDOWN_DEF)
224
- ruina_threshold = 1.0 - RUIN_DRAWDOWN_DEF
225
- ruin_count = float(np.sum(min_portfolios < ruina_threshold))
226
- return round(ruin_count / scenarios, 4)
227
-
228
- else:
229
- # Fallback puro-Python (mΓ‘s lento pero funcional)
230
- import random
231
- random.seed(42)
232
- ruin_count = 0
233
- for _ in range(scenarios):
234
- port = 1.0
235
- for _ in range(horizon):
236
- if random.random() < win_rate:
237
- port *= (1.0 + kelly_frac * avg_win_pct)
238
- else:
239
- port *= (1.0 - kelly_frac * avg_loss_pct)
240
- if port < (1.0 - RUIN_DRAWDOWN_DEF):
241
- ruin_count += 1
242
- break
243
- return round(ruin_count / scenarios, 4)
244
-
245
-
246
- # ══════════════════════════════════════════════════════════════════════════════
247
- # Z-SCORE DE VOLATILIDAD (heredado de v2.0, mejorado)
248
- # ══════════════════════════════════════════════════════════════════════════════
249
-
250
- def _compute_zscore(bars: list) -> float:
251
- if len(bars) < 5:
252
- return 0.0
253
- closes = []
254
- for b in bars:
255
- c = b.get("c") or b.get("close") or 0.0
256
- if c > 0:
257
- closes.append(float(c))
258
- if len(closes) < 4:
259
- return 0.0
260
- returns = [abs(closes[i] / closes[i-1] - 1) for i in range(1, len(closes))]
261
- if _NP_OK:
262
- ret_arr = np.array(returns)
263
- mean_r = float(np.mean(ret_arr))
264
- std_r = float(np.std(ret_arr)) or 0.001
265
- return round(float(abs((returns[-1] - mean_r) / std_r)), 3)
266
- else:
267
- mean_r = sum(returns) / len(returns)
268
- std_r = math.sqrt(sum((r - mean_r)**2 for r in returns) / len(returns)) or 0.001
269
- return round(abs(returns[-1] - mean_r) / std_r, 3)
270
-
271
-
272
- # ══════════════════════════════════════════════════════════════════════════════
273
- # FASE MATH COMPLETA
274
- # ══════════════════════════════════════════════════════════════════════════════
275
-
276
- def _math_audit_v3(payload: dict) -> dict:
277
- """
278
- AuditorΓ­a de riesgo completa en <1ms:
279
- 1. PDT check
280
- 2. Kelly Fraccional Continuo (numpy)
281
- 3. Monte Carlo Express (100 escenarios, <1ms)
282
- 4. Z-Score de volatilidad
283
- 5. Risk Warning + Vault Status
284
- """
285
  t0 = time.perf_counter()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- sym = payload.get("sym", "?")
288
- balance = float(payload.get("balance", 10000))
289
- open_pos = int(payload.get("open_positions", 0))
290
- daytrades = int(payload.get("daytrade_count", 0))
291
- trade_type = payload.get("trade_type", "scalp")
292
- g_conf = float(payload.get("g_confidence", 0.5))
293
- cronos = float(payload.get("cronos_score", 0.5))
294
- is_crypto = bool(payload.get("is_crypto", False)) or "/" in sym
295
- bars = payload.get("bars", [])
296
- win_rate = float(payload.get("win_rate", 0.45))
297
- avg_win = float(payload.get("avg_win_pct", 0.012))
298
- avg_loss = float(payload.get("avg_loss_pct", 0.008))
299
- streak_loss = int(payload.get("streak_losses", 0))
300
- n_trades = int(payload.get("n_trades", len(payload.get("trade_history", []))))
301
- kelly_override = payload.get("kelly_override")
302
- bootstrap_pct = float(payload.get("bootstrap_pct", 0.010))
303
-
304
- # ── PDT check ─────────────────────────────────────────────────────────────
305
- if is_crypto:
306
- pdt_ok = True
307
- else:
308
- pdt_ok = not (daytrades >= PDT_LIMIT and balance < PDT_MIN_BALANCE)
309
-
310
- # ── Kelly Fraccional Continuo ─────────────────────────────────────────────
311
- if kelly_override is not None:
312
- size_usd = float(kelly_override)
313
- kelly_f = size_usd / balance if balance > 0 else KELLY_MIN_FRACTION
314
- kelly_cont = kelly_f
315
- else:
316
- kelly_cont = _kelly_continuous(
317
- win_rate, avg_win, avg_loss,
318
- streak_loss, cronos, n_trades, bootstrap_pct)
319
- size_usd = round(balance * kelly_cont, 2)
320
 
321
- # ── Z-Score ────────────────────────────────────────────────────────────────
322
- zscore = _compute_zscore(bars)
323
 
324
- # ── Monte Carlo Express ────────────────────────────────────────────────────
325
- mc_ruin = _monte_carlo_ruin(
326
- win_rate, avg_win, avg_loss,
327
- kelly_cont, balance,
328
- horizon=MC_HORIZON, scenarios=MC_SCENARIOS)
329
 
330
- # ── Ajuste por Z-Score elevado ─────────────────────────────────────────────
331
- if zscore >= ZSCORE_PANIC:
332
- size_usd = round(size_usd * 0.50, 2) # reducir 50% en pΓ‘nico
333
- kelly_cont = round(kelly_cont * 0.50, 6)
334
-
335
- # ── Risk Warning ──────────────────────────────────────────────────────────
336
- if mc_ruin >= PANIC_RUIN_THRESHOLD:
337
- risk = "monte_carlo_ruin"
338
- elif zscore >= ZSCORE_PANIC:
339
- risk = "high_zscore"
340
- elif streak_loss >= 3:
341
- risk = "streak_loss"
342
- elif open_pos >= 8:
343
- risk = "slots_high"
344
- size_usd = round(size_usd * 0.70, 2)
345
- else:
346
- risk = "none"
347
-
348
- # ── Vault Status ──────────────────────────────────────────────────────────
349
- if not pdt_ok:
350
- vault = "blocked_pdt"
351
- elif risk in ("monte_carlo_ruin", "high_zscore"):
352
- vault = "conservative"
353
- elif risk == "streak_loss":
354
- vault = "conservative"
355
- else:
356
- vault = "protected"
357
-
358
- # ── Detectar si se necesita LLM (Interruptor de PΓ‘nico) ───────────────────
359
- # Calcular drawdown aproximado desde balance (necesitamos historial para exacto)
360
- # Usamos Kelly fraction como proxy del tamaΓ±o relativo de las pΓ©rdidas recientes
361
- approx_drawdown = streak_loss * avg_loss * kelly_cont
362
- needs_llm = (mc_ruin >= PANIC_RUIN_THRESHOLD and
363
- approx_drawdown >= PANIC_DRAWDOWN_PCT and
364
- not is_crypto and # crypto: pdt_ok=True siempre
365
- pdt_ok and n_trades >= 10) # mΓ­nimo 10 trades de historial
366
 
367
  math_ms = (time.perf_counter() - t0) * 1000
368
-
369
- print(f"[H/MATH] {sym}: pdt={pdt_ok} kelly={kelly_cont:.4f} "
370
- f"size=${size_usd:.2f} z={zscore:.2f} mc_ruin={mc_ruin:.3f} "
371
- f"risk={risk} | {math_ms:.1f}ms")
372
 
373
  return {
374
- "pdt_ok": pdt_ok,
375
- "size_usd": size_usd,
376
- "risk_warning": risk,
377
- "vault_status": vault,
378
- "kelly_fraction": round(kelly_cont, 6),
379
- "kelly_continuous": round(kelly_cont, 6),
380
- "zscore": zscore,
381
- "monte_carlo_ruin_prob": mc_ruin,
382
- "panic_activated": False,
383
- "needs_llm": needs_llm,
384
- "cerebro": "H",
385
- "_math_ms": round(math_ms, 2),
386
- "_llm_ms": 0.0,
387
- # Para el LLM si se activa
388
- "_sym": sym,
389
- "_balance": balance,
390
- "_approx_drawdown": round(approx_drawdown, 4),
391
  }
392
 
393
-
394
- # ══════════════════════════════════════════════════════════════════════════════
395
- # FASE LLM β€” Interruptor de PΓ‘nico (SOLO si P(ruina) crΓ­tica)
396
- # ══════════════════════════════════════════════════════════════════════════════
397
-
398
- async def _llm_panic_switch(math_result: dict) -> dict:
399
- """
400
- El LLM actΓΊa como cortocircuito de emergencia.
401
- Solo se invoca cuando:
402
- P(ruina_MC) > PANIC_RUIN_THRESHOLD AND drawdown_approx > PANIC_DRAWDOWN_PCT
403
-
404
- Pregunta: ΒΏVeto la inyecciΓ³n de capital en este trade dado el riesgo?
405
- Output: {"veto": true|false, "motivo": "string"}
406
- """
407
- t0_llm = time.perf_counter()
408
-
409
- sym = math_result.get("_sym", "?")
410
- balance = math_result.get("_balance", 10000)
411
- mc_ruin = math_result.get("monte_carlo_ruin_prob", 0.0)
412
- drawdown = math_result.get("_approx_drawdown", 0.0)
413
- risk = math_result.get("risk_warning", "none")
414
- streak = math_result.get("risk_warning", "")
415
-
416
- llm_prompt = (
417
- "<|im_start|>system\n"
418
- 'Risk switch. SOLO JSON: {"veto":true|false,"motivo":"<5w"}.\n'
419
- "veto=true=detener inyecciΓ³n capital. Sin pensar.\n"
420
- "<|im_end|>\n"
421
- "<|im_start|>user\n"
422
- f'{{"s":"{sym[:8]}","mc":{mc_ruin:.3f},"dd":{drawdown:.3f},'
423
- f'"bal":{balance:.0f},"r":"{risk[:6]}"}}\n'
424
- "<|im_end|>\n"
425
- "<|im_start|>assistant\n{"
426
  )
427
 
428
- try:
429
- result = await _bitnet_infer(llm_prompt)
430
- raw_out_text = result.get("raw", "")
431
-
432
- llm_ms = (time.perf_counter() - t0_llm) * 1000
433
- raw = "{" + raw_out_text
434
-
435
- m = re.search(r'\{[^{}]*"veto"\s*:\s*(true|false)[^{}]*\}', raw, re.IGNORECASE)
436
- if m:
437
- parsed = json.loads(m.group())
438
- veto = bool(parsed.get("veto", False))
439
- motivo = str(parsed.get("motivo", ""))[:60]
440
- print(f"[H/LLM] {sym}: PANIC veto={veto} motivo='{motivo}' | {llm_ms:.1f}ms")
441
- return {"veto": veto, "motivo": motivo, "_llm_ms": round(llm_ms, 1)}
442
-
443
- # Parseo parcial
444
- veto = "true" in raw.lower()
445
- print(f"[H/LLM] {sym}: partial veto={veto} | {llm_ms:.1f}ms")
446
- return {"veto": veto, "motivo": "partial_parse", "_llm_ms": round(llm_ms, 1)}
447
-
448
- except Exception as e:
449
- llm_ms = (time.perf_counter() - t0_llm) * 1000
450
- print(f"[H/LLM-ERR] {sym}: {type(e).__name__}: {str(e)[:50]} | {llm_ms:.1f}ms β†’ no_veto")
451
- # Fallback conservador: si LLM falla en pΓ‘nico, NO vetar (evitar falsos negativos)
452
- return {"veto": False, "motivo": "llm_error_no_veto", "_llm_ms": round(llm_ms, 1)}
453
-
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
- # ══════════════════════════════════════════════════════════════════════════════
456
- # PIPELINE PRINCIPAL (Math β†’ LLM opcional)
457
- # ══════════════════════════════════════════════════════════════════════════════
458
 
459
  async def _audit(payload: dict) -> dict:
460
  t0 = time.perf_counter()
461
-
462
- # ── FASE 1: MATH ───────��──────────────────────────────────────────────────
463
- result = _math_audit_v3(payload)
464
- math_ms = result["_math_ms"]
465
- sym = result.get("_sym", "?")
466
-
467
- llm_ms = 0.0
468
-
469
- # ── FASE 2: LLM Interruptor de PΓ‘nico (solo si condiciΓ³n crΓ­tica) ─────────
470
- if result.get("needs_llm", False):
471
- llm_result = await _llm_panic_switch(result)
472
- llm_ms = llm_result.get("_llm_ms", 0.0)
473
-
474
- if llm_result.get("veto", False):
475
- # LLM vetΓ³ β€” bloquear inyecciΓ³n de capital
476
- result["size_usd"] = 0.0
477
- result["kelly_fraction"] = 0.0
478
- result["kelly_continuous"] = 0.0
479
- result["vault_status"] = "emergency_hold"
480
- result["risk_warning"] = "emergency"
481
- result["panic_activated"] = True
482
- print(f"[H/PANIC-VETO] {sym}: inyecciΓ³n BLOQUEADA por LLM | mc={result['monte_carlo_ruin_prob']:.3f}")
 
 
 
 
 
 
 
 
483
  else:
484
- result["panic_activated"] = False
485
- print(f"[H/PANIC-ALLOWED] {sym}: inyecciΓ³n PERMITIDA a pesar de mc_ruin | LLM: {llm_result.get('motivo','')}")
486
-
487
- # Limpiar campos internos antes de retornar
488
- result.pop("needs_llm", None)
489
- result.pop("_sym", None)
490
- result.pop("_balance", None)
491
- result.pop("_approx_drawdown", None)
492
 
493
  total_ms = (time.perf_counter() - t0) * 1000
494
- result["_llm_ms"] = round(llm_ms, 1)
495
- result["_total_ms"] = round(total_ms, 1)
496
-
497
- print(f"[H/TOTAL] {sym}: math={math_ms:.1f}ms llm={llm_ms:.1f}ms total={total_ms:.1f}ms")
498
- return result
499
-
500
-
501
- # ── Endpoints FastAPI ─────────────────────────────────────────────────────────
502
-
503
- @app.get("/")
504
- async def root():
505
- return {
506
- "status": "online",
507
- "agent": "RiskAuditor",
508
- "cerebro": "H",
509
- "version": "3.0-APEX-Kelly+MonteCarlo",
510
- }
511
 
 
512
 
513
  @app.get("/health")
514
  async def health():
515
- return {
516
- "status": "online",
517
- "agent": "RiskAuditor",
518
- "cerebro": "H",
519
- "version": "3.0-APEX-Kelly+MonteCarlo",
520
- "model": "BitNet-b1.58-2B-4T-i2_s (ik_llama.cpp)", "bitnet_server": BITNET_BASE,
521
- "numpy_ok": _NP_OK,
522
- "mc_scenarios": MC_SCENARIOS,
523
- "mc_horizon": MC_HORIZON,
524
- "kelly_max": KELLY_MAX_FRACTION,
525
- "zscore_panic": ZSCORE_PANIC,
526
- "agents": ["RiskAuditor"],
527
- "architecture": {
528
- "math": "Kelly-Continuo + MC-100 + ZScore (<1ms, 95% requests)",
529
- "llm": "Interruptor de PΓ‘nico (solo P_ruin>30% AND drawdown>15%)",
530
- "trigger": f"mc_ruin>{PANIC_RUIN_THRESHOLD} AND drawdown>{PANIC_DRAWDOWN_PCT}",
531
- },
532
- }
533
-
534
 
535
  @app.post("/audit")
536
  async def audit(request: Request):
@@ -538,14 +379,7 @@ async def audit(request: Request):
538
  payload = await request.json()
539
  except Exception:
540
  return JSONResponse({"error": "JSON invΓ‘lido"}, status_code=400)
541
-
542
- sym = payload.get("sym", "?")
543
- cache_key = f"{sym}:{float(payload.get('balance', 0)):.0f}"
544
- cached = _cache_get(cache_key)
545
- if cached:
546
- print(f"[H/CACHE] {sym}: hit")
547
- return JSONResponse(cached)
548
-
549
  result = await _audit(payload)
550
- _cache_set(cache_key, result)
551
  return JSONResponse(result)
 
 
 
1
  """
2
+ server_H.py β€” Cerebro H β€” RiskAuditor v21.5 (APEX Risk Thermometer)
3
+ ====================================================================
4
+ ARQUITECTURA COGNITIVA v21.5:
5
+
6
+ FASE MATH (<1ms): Kelly Continuo + Monte Carlo Express + Risk Thermometer
7
+ FASE LLM (Shadow): BitNet responde con G/Y/R (1 token) en background
8
+ El shadow_cache sirve el ΓΊltimo estado conocido (<1ms)
9
+
10
+ INNOVACIONES v21.5:
11
+ Β· Risk Thermometer Continuo: en vez del pΓ‘nico binario (on/off),
12
+ H mantiene una temperatura de riesgo deslizante (0.0-1.0) que
13
+ se actualiza con cada call. La temperatura pondera: MC_ruinΓ—0.4 +
14
+ streak_lossΓ—0.3 + drawdownΓ—0.2 + zscore_normΓ—0.1.
15
+ Esto permite al swarm degradar gradualmente sin cortes abruptos.
16
+
17
+ Β· Shadow Cache con propagaciΓ³n de temperatura: si el estado G/Y/R
18
+ no cambia pero la temperatura sube >0.15, el shadow fuerza un
19
+ refresh de BitNet en background aunque el TTL no haya expirado.
20
+
21
+ Β· Single-Token Output: BitNet genera exactamente G, Y, o R.
22
+ n_predict=2, stop=["\n"]. Cero JSON overhead.
23
+
24
+ Β· Ensemble HistΓ³rico: H mantiene un ring-buffer de los ΓΊltimos 10
25
+ tokens de BitNet para detectar inconsistencias. Si los ΓΊltimos 5
26
+ tokens son todos R, H escala el size_usd a 0 automΓ‘ticamente
27
+ sin necesidad de nueva call.
28
+
29
+ OUTPUT (100% compatible con swarm_engine):
 
 
 
 
 
30
  {
31
+ "pdt_ok": bool,
32
+ "size_usd": float,
33
+ "risk_warning": str,
34
+ "vault_status": str,
35
+ "kelly_fraction": float,
36
+ "kelly_continuous": float,
37
+ "zscore": float,
38
+ "monte_carlo_ruin_prob": float,
39
+ "panic_activated": bool,
40
+ "risk_token": str, # "G"|"Y"|"R" β€” nuevo v21.5
41
+ "risk_temperature": float, # 0.0-1.0 continuo β€” nuevo v21.5
42
+ "_math_ms": float,
43
+ "_llm_ms": float,
44
+ "_total_ms": float,
45
+ "_shadow": bool,
46
  }
 
 
47
  """
48
 
49
+ import os, time, asyncio, threading, math, collections
50
  from fastapi import FastAPI, Request
51
  from fastapi.responses import JSONResponse
52
+ import httpx
53
 
54
  try:
55
  import numpy as np
56
+ _NP = True
57
  except ImportError:
58
+ _NP = False
59
 
60
+ app = FastAPI(title="Cerebro H β€” RiskAuditor v21.5")
61
 
62
  # ── Config ──────────────────────────────────────────────────────��─────────────
 
 
 
 
 
 
63
  BITNET_PORT = int(os.environ.get("BITNET_PORT", "8080"))
64
  BITNET_HOST = os.environ.get("BITNET_HOST", "127.0.0.1")
65
  BITNET_BASE = f"http://{BITNET_HOST}:{BITNET_PORT}"
66
+ BITNET_TIMEOUT = float(os.environ.get("BITNET_TIMEOUT", "15.0"))
67
+ CACHE_TTL = float(os.environ.get("CACHE_TTL", "30.0"))
68
+
69
+ KELLY_MAX = float(os.environ.get("KELLY_MAX", "0.05"))
70
+ KELLY_MIN = float(os.environ.get("KELLY_MIN", "0.005"))
71
+ ZSCORE_PANIC = float(os.environ.get("ZSCORE_PANIC", "2.5"))
72
+ PDT_LIMIT = int(os.environ.get("PDT_LIMIT", "3"))
73
+ PDT_MIN_BALANCE= float(os.environ.get("PDT_MIN", "25000"))
74
+ MC_SCENARIOS = int(os.environ.get("MC_SCENARIOS", "100"))
75
+ MC_HORIZON = int(os.environ.get("MC_HORIZON", "20"))
76
+ RUIN_THRESHOLD = float(os.environ.get("RUIN_THRESHOLD","0.25"))
77
+ RUIN_DRAWDOWN = float(os.environ.get("RUIN_DRAWDOWN", "0.20"))
78
+
79
+ # ── BitNet Client ─────────────────────────────────────────────────────────────
80
+ _bitnet_client: httpx.AsyncClient | None = None
81
+
82
+ def _get_client() -> httpx.AsyncClient:
83
  global _bitnet_client
84
  if _bitnet_client is None or _bitnet_client.is_closed:
85
  _bitnet_client = httpx.AsyncClient(
86
  base_url=BITNET_BASE,
87
+ timeout=httpx.Timeout(connect=3.0, read=BITNET_TIMEOUT,
88
+ write=3.0, pool=2.0),
89
+ limits=httpx.Limits(max_connections=8, max_keepalive_connections=4),
90
  )
91
  return _bitnet_client
92
 
93
+ async def _bitnet_single_token(prompt: str) -> str:
94
+ payload = {
95
+ "prompt": prompt,
96
+ "n_predict": 2,
97
+ "temperature": 0.0,
98
+ "stream": False,
99
+ "cache_prompt": False,
100
+ "stop": ["\n", "<|im_end|>", "<|end_of_text|>", "<|eot_id|>"],
101
+ }
102
  try:
103
+ resp = await _get_client().post("/completion", json=payload)
104
  resp.raise_for_status()
105
+ return resp.json().get("content", "").strip()
106
+ except Exception:
107
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ # ── Risk Thermometer β€” estado continuo deslizante ────────────────────────────
110
+ _risk_temp: dict = {} # sym β†’ temperatura actual (0.0-1.0)
111
+ _token_ring: dict = {} # sym β†’ deque de ΓΊltimos 10 tokens G/Y/R
112
+ _thermo_lock = threading.Lock()
 
113
 
114
+ def _update_thermometer(sym: str, mc_ruin: float, streak: int,
115
+ drawdown: float, zscore: float) -> float:
116
  """
117
+ Temperatura de riesgo continua (0.0=frΓ­o/verde, 1.0=caliente/rojo):
118
+ temp = MC_ruinΓ—0.40 + streak_normΓ—0.30 + drawdown_normΓ—0.20 + zscore_normΓ—0.10
119
+ Se promedia con la temperatura anterior (inercia 0.3) para suavizar spikes.
120
+ """
121
+ streak_norm = min(streak / 5.0, 1.0)
122
+ drawdown_norm = min(drawdown / RUIN_DRAWDOWN, 1.0)
123
+ zscore_norm = min(abs(zscore) / ZSCORE_PANIC, 1.0)
124
+
125
+ new_temp = (mc_ruin * 0.40 + streak_norm * 0.30 +
126
+ drawdown_norm * 0.20 + zscore_norm * 0.10)
127
+
128
+ with _thermo_lock:
129
+ prev = _risk_temp.get(sym, new_temp)
130
+ # Inercia: temperatura no sube bruscamente ni cae bruscamente
131
+ smoothed = prev * 0.30 + new_temp * 0.70
132
+ _risk_temp[sym] = round(smoothed, 4)
133
+ return smoothed
134
+
135
+ def _temp_to_token(temp: float) -> str:
136
+ if temp < 0.35: return "G"
137
+ if temp < 0.65: return "Y"
138
+ return "R"
139
+
140
+ def _ring_push(sym: str, token: str):
141
+ with _thermo_lock:
142
+ if sym not in _token_ring:
143
+ _token_ring[sym] = collections.deque(maxlen=10)
144
+ _token_ring[sym].append(token)
145
+
146
+ def _ring_last5_all_R(sym: str) -> bool:
147
+ """Si los ΓΊltimos 5 tokens son todos R β†’ bloqueo automΓ‘tico."""
148
+ with _thermo_lock:
149
+ ring = _token_ring.get(sym, [])
150
+ if len(ring) < 5:
151
+ return False
152
+ return all(t == "R" for t in list(ring)[-5:])
153
+
154
+ # ── Shadow Cache ──────────────────────────────────────────────────────────────
155
+ _shadow: dict = {}
156
+ _shadow_lock = threading.Lock()
157
+
158
+ def _shadow_get(sym: str, current_temp: float) -> str | None:
159
+ with _shadow_lock:
160
+ e = _shadow.get(sym)
161
+ if not e:
162
+ return None
163
+ age = time.time() - e["ts"]
164
+ # TTL adaptativo: temperatura alta β†’ TTL corto
165
+ ttl = 10.0 if current_temp > 0.65 else (20.0 if current_temp > 0.35 else CACHE_TTL)
166
+ if age > ttl:
167
+ return None
168
+ # Si la temperatura cambiΓ³ >0.15, forzar refresh
169
+ if abs(current_temp - e["temp"]) > 0.15:
170
+ return None
171
+ return e["token"]
172
+
173
+ def _shadow_set(sym: str, token: str, temp: float):
174
+ with _shadow_lock:
175
+ _shadow[sym] = {"token": token, "ts": time.time(), "temp": temp}
176
+
177
+ # ── MATH: Kelly + MC + ZScore ─────────────────────────────────────────────────
178
+
179
+ def _kelly_continuous(win_rate, avg_win, avg_loss, streak, cronos, n_trades, bs_pct):
180
  if n_trades < 20:
181
+ return max(KELLY_MIN, min(bs_pct, KELLY_MAX))
 
 
182
  if avg_loss <= 0 or avg_win <= 0:
183
+ return KELLY_MIN
 
 
184
  kelly_raw = (win_rate * avg_win - (1 - win_rate) * avg_loss) / avg_win
 
 
185
  kelly_half = kelly_raw / 2.0
186
+ streak_pen = 1.0 - min(streak * 0.15, 0.45)
 
 
 
 
187
  cronos_amp = 0.80 + cronos * 0.40
188
+ return round(max(KELLY_MIN, min(KELLY_MAX, kelly_half * streak_pen * cronos_amp)), 6)
189
+
190
+ def _monte_carlo(win_rate, avg_win, avg_loss, kelly_f, horizon, scenarios) -> float:
191
+ if _NP:
192
+ np.random.seed(42)
193
+ wins = np.random.random((scenarios, horizon)) < win_rate
194
+ factors = np.where(wins, 1.0 + kelly_f * avg_win, 1.0 - kelly_f * avg_loss)
195
+ portfolios = np.cumprod(factors, axis=1).min(axis=1)
196
+ return round(float(np.sum(portfolios < (1.0 - RUIN_DRAWDOWN))) / scenarios, 4)
197
+ import random; random.seed(42)
198
+ ruin = 0
199
+ for _ in range(scenarios):
200
+ p = 1.0
201
+ for _ in range(horizon):
202
+ p *= (1 + kelly_f * avg_win) if random.random() < win_rate else (1 - kelly_f * avg_loss)
203
+ if p < 1 - RUIN_DRAWDOWN: ruin += 1; break
204
+ return round(ruin / scenarios, 4)
205
+
206
+ def _zscore_bars(bars: list) -> float:
207
+ closes = [float(b.get("c", b.get("close", 0))) for b in bars if b.get("c") or b.get("close")]
208
+ if len(closes) < 4: return 0.0
209
+ rets = [abs(closes[i]/closes[i-1]-1) for i in range(1, len(closes))]
210
+ if _NP:
211
+ a = np.array(rets)
212
+ return round(float(abs((rets[-1] - a.mean()) / (a.std() or 0.001))), 3)
213
+ mean = sum(rets)/len(rets)
214
+ std = math.sqrt(sum((r-mean)**2 for r in rets)/len(rets)) or 0.001
215
+ return round(abs(rets[-1]-mean)/std, 3)
216
+
217
+ def _math_audit(payload: dict) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  t0 = time.perf_counter()
219
+ sym = payload.get("sym", "?")
220
+ balance = float(payload.get("balance", 10000))
221
+ daytrades = int(payload.get("daytrade_count", 0))
222
+ is_crypto = bool(payload.get("is_crypto", False)) or "/" in sym
223
+ bars = payload.get("bars", [])
224
+ win_rate = float(payload.get("win_rate", 0.45))
225
+ avg_win = float(payload.get("avg_win_pct", 0.012))
226
+ avg_loss = float(payload.get("avg_loss_pct", 0.008))
227
+ streak = int(payload.get("streak_losses", 0))
228
+ n_trades = int(payload.get("n_trades", 0))
229
+ cronos = float(payload.get("cronos_score", 0.5))
230
+ bs_pct = float(payload.get("bootstrap_pct", 0.010))
231
+ open_pos = int(payload.get("open_positions", 0))
232
+
233
+ pdt_ok = True if is_crypto else not (daytrades >= PDT_LIMIT and balance < PDT_MIN_BALANCE)
234
+ kelly = _kelly_continuous(win_rate, avg_win, avg_loss, streak, cronos, n_trades, bs_pct)
235
+ size = round(balance * kelly, 2)
236
+ zscore = _zscore_bars(bars)
237
+ mc = _monte_carlo(win_rate, avg_win, avg_loss, kelly, MC_HORIZON, MC_SCENARIOS)
238
 
239
+ if zscore >= ZSCORE_PANIC:
240
+ size = round(size * 0.50, 2)
241
+ kelly = round(kelly * 0.50, 6)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ approx_dd = streak * avg_loss * kelly
244
+ temp = _update_thermometer(sym, mc, streak, approx_dd, zscore)
245
 
246
+ if open_pos >= 8: size = round(size * 0.70, 2)
 
 
 
 
247
 
248
+ risk = ("monte_carlo_ruin" if mc >= RUIN_THRESHOLD else
249
+ "high_zscore" if zscore >= ZSCORE_PANIC else
250
+ "streak_loss" if streak >= 3 else "none")
251
+ vault = ("blocked_pdt" if not pdt_ok else
252
+ "conservative" if risk in ("monte_carlo_ruin","high_zscore","streak_loss") else
253
+ "protected")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  math_ms = (time.perf_counter() - t0) * 1000
256
+ print(f"[H/MATH] {sym}: pdt={pdt_ok} kelly={kelly:.4f} ${size:.0f} "
257
+ f"mc={mc:.3f} temp={temp:.2f} | {math_ms:.1f}ms")
 
 
258
 
259
  return {
260
+ "pdt_ok": pdt_ok, "size_usd": size, "risk_warning": risk,
261
+ "vault_status": vault, "kelly_fraction": kelly,
262
+ "kelly_continuous": kelly, "zscore": zscore,
263
+ "monte_carlo_ruin_prob": mc, "panic_activated": False,
264
+ "risk_temperature": round(temp, 4),
265
+ "_math_ms": round(math_ms, 2), "_sym": sym,
266
+ "_balance": balance, "_temp": temp,
267
+ "_approx_dd": round(approx_dd, 4),
 
 
 
 
 
 
 
 
 
268
  }
269
 
270
+ # ── Prompt sistema 100% estΓ‘tico ─────────────────────────────────────────────
271
+ _SYS_H = (
272
+ "<|im_start|>system\n"
273
+ "Risk level G/Y/R. Respond ONLY with one letter.\n"
274
+ "G=green free trade. Y=yellow scalps only. R=red block all.\n"
275
+ "<|im_end|>\n"
276
+ "<|im_start|>user\n"
277
+ )
278
+
279
+ def _build_h_prompt(mc: float, temp: float, streak: int, dd: float) -> str:
280
+ return (
281
+ _SYS_H
282
+ + f'{{"mc":{mc:.2f},"t":{temp:.2f},"s":{streak},"d":{dd:.2f}}}\n'
283
+ + "<|im_end|>\n"
284
+ + "<|im_start|>assistant\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  )
286
 
287
+ def _token_to_output(token: str, math: dict, llm_ms: float,
288
+ is_shadow: bool, total_ms: float) -> dict:
289
+ sym = math["_sym"]
290
+ # Ensemble: si los ΓΊltimos 5 son todos R β†’ bloqueo automΓ‘tico
291
+ if _ring_last5_all_R(sym) and token != "R":
292
+ token = "R"
293
+ print(f"[H/RING-VETO] {sym}: ΓΊltimos 5 tokens R β†’ bloqueo automΓ‘tico")
294
+
295
+ if token == "R":
296
+ math["size_usd"] = 0.0
297
+ math["kelly_fraction"] = 0.0
298
+ math["vault_status"] = "emergency_hold"
299
+ math["panic_activated"] = True
300
+
301
+ _ring_push(sym, token)
302
+
303
+ out = {k: v for k, v in math.items() if not k.startswith("_")}
304
+ out.update({
305
+ "risk_token": token,
306
+ "_math_ms": math["_math_ms"],
307
+ "_llm_ms": round(llm_ms, 1),
308
+ "_total_ms": round(total_ms, 1),
309
+ "_shadow": is_shadow,
310
+ })
311
+ return out
312
+
313
+ # ── Background LLM update ─────────────────────────────────────────────────────
314
+
315
+ async def _background_h_update(sym: str, prompt: str, temp: float):
316
+ t0 = time.perf_counter()
317
+ raw = await _bitnet_single_token(prompt)
318
+ llm_ms = (time.perf_counter() - t0) * 1000
319
+ token = raw.strip()[:1].upper()
320
+ if token in ("G", "Y", "R"):
321
+ _shadow_set(sym, token, temp)
322
+ _ring_push(sym, token)
323
+ print(f"[H/LLM] {sym}: token={token} temp={temp:.2f} | {llm_ms:.1f}ms (shadow actualizado)")
324
+ else:
325
+ print(f"[H/LLM-GUARD] {sym}: token invΓ‘lido='{raw[:10]}' β†’ shadow preservado")
326
 
327
+ # ── Pipeline principal ────────────────────────────────────────────────────────
 
 
328
 
329
  async def _audit(payload: dict) -> dict:
330
  t0 = time.perf_counter()
331
+ math = _math_audit(payload)
332
+ sym = math["_sym"]
333
+ temp = math["_temp"]
334
+ mc = math["monte_carlo_ruin_prob"]
335
+ streak = int(payload.get("streak_losses", 0))
336
+ dd = math["_approx_dd"]
337
+
338
+ cached_token = _shadow_get(sym, temp)
339
+ is_shadow = cached_token is not None
340
+
341
+ if is_shadow:
342
+ token = cached_token
343
+ llm_ms = 0.0
344
+ # Actualizar proactivamente en background
345
+ with _shadow_lock:
346
+ age = time.time() - _shadow.get(sym, {}).get("ts", 0)
347
+ if age > CACHE_TTL * 0.5:
348
+ asyncio.create_task(
349
+ _background_h_update(sym, _build_h_prompt(mc, temp, streak, dd), temp))
350
+ print(f"[H/SHADOW] {sym}: token={token} temp={temp:.2f} | 0ms")
351
+ else:
352
+ t_llm = time.perf_counter()
353
+ prompt = _build_h_prompt(mc, temp, streak, dd)
354
+ raw = await _bitnet_single_token(prompt)
355
+ llm_ms = (time.perf_counter() - t_llm) * 1000
356
+ token = raw.strip()[:1].upper()
357
+ if token not in ("G", "Y", "R"):
358
+ # Math fallback
359
+ token = _temp_to_token(temp)
360
+ print(f"[H/LLM-GUARD] {sym}: token='{raw[:10]}' β†’ math_fallback={token}")
361
  else:
362
+ _shadow_set(sym, token, temp)
363
+ print(f"[H/LLM] {sym}: token={token} | {llm_ms:.1f}ms")
 
 
 
 
 
 
364
 
365
  total_ms = (time.perf_counter() - t0) * 1000
366
+ return _token_to_output(token, math, llm_ms, is_shadow, total_ms)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
369
 
370
  @app.get("/health")
371
  async def health():
372
+ return {"status": "online", "cerebro": "H", "version": "21.5-APEX-BITNET-THERMOMETER",
373
+ "model": "BitNet-b1.58-2B-4T-i2_s", "engine": "ik_llama.cpp",
374
+ "architecture": "Kelly+MC+Thermometer + Shadow-Cache + Single-Token G/Y/R"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  @app.post("/audit")
377
  async def audit(request: Request):
 
379
  payload = await request.json()
380
  except Exception:
381
  return JSONResponse({"error": "JSON invΓ‘lido"}, status_code=400)
 
 
 
 
 
 
 
 
382
  result = await _audit(payload)
 
383
  return JSONResponse(result)
384
+
385
+ print("[H] βœ… RiskAuditor v21.5 β€” Risk Thermometer + Shadow + Single-Token G/Y/R")
start_H.sh CHANGED
@@ -2,20 +2,33 @@
2
  set -e
3
  MODEL_PATH="${MODEL_PATH:-/models/ggml-model-i2_s.gguf}"
4
  BITNET_PORT="${BITNET_PORT:-8080}"
5
- N_CTX="${N_CTX:-2048}"
6
  N_THREADS="${N_THREADS:-2}"
7
  PORT="${PORT:-7860}"
 
8
 
9
- echo "[START] Cerebro RiskAuditor v3.0 (BitNet REAL)"
10
  [ ! -f "$MODEL_PATH" ] && echo "[START] FATAL: modelo no encontrado" && exit 1
11
  echo "[START] Modelo: $(du -h $MODEL_PATH | cut -f1)"
12
 
13
  BITNET_LOG="/tmp/bitnet.log"
14
  > "$BITNET_LOG"
15
 
 
 
 
 
 
16
  llama-server \
17
- --model "$MODEL_PATH" --host 127.0.0.1 --port "$BITNET_PORT" \
18
- --ctx-size "$N_CTX" --threads "$N_THREADS" --gpu-layers 0 \
 
 
 
 
 
 
 
19
  2>&1 | tee "$BITNET_LOG" | sed 's/^/[BITNET] /' &
20
  LLAMA_PID=$!
21
 
@@ -29,4 +42,4 @@ done
29
  [ $ELAPSED -ge $MAX_WAIT ] && echo "[START] FATAL: timeout" && exit 1
30
  echo "[START] BitNet ONLINE β€” iniciando FastAPI..."
31
 
32
- exec uvicorn server_H:app --host 0.0.0.0 --port "$PORT" --workers 1 --log-level info
 
2
  set -e
3
  MODEL_PATH="${MODEL_PATH:-/models/ggml-model-i2_s.gguf}"
4
  BITNET_PORT="${BITNET_PORT:-8080}"
5
+ N_CTX="${N_CTX:-512}"
6
  N_THREADS="${N_THREADS:-2}"
7
  PORT="${PORT:-7860}"
8
+ SERVER="server_H"
9
 
10
+ echo "[START] Cerebro H v21.5 (BitNet REAL)"
11
  [ ! -f "$MODEL_PATH" ] && echo "[START] FATAL: modelo no encontrado" && exit 1
12
  echo "[START] Modelo: $(du -h $MODEL_PATH | cut -f1)"
13
 
14
  BITNET_LOG="/tmp/bitnet.log"
15
  > "$BITNET_LOG"
16
 
17
+ # Anti-alucinaciΓ³n v21.5:
18
+ # --cache-prompt-ram 0 β†’ desactiva cache RAM (evita bug {{{{ de ik_llama.cpp)
19
+ # --jinja β†’ habilita chat template nativo Llama-3/ChatML
20
+ # --no-mmap β†’ evita lecturas corruptas por memory-map
21
+ # --ctx-size 512 β†’ n_ctx pequeΓ±o para single-token output (<1ms evaluaciΓ³n)
22
  llama-server \
23
+ --model "$MODEL_PATH" \
24
+ --host 127.0.0.1 \
25
+ --port "$BITNET_PORT" \
26
+ --ctx-size "$N_CTX" \
27
+ --threads "$N_THREADS" \
28
+ --gpu-layers 0 \
29
+ --no-mmap \
30
+ --jinja \
31
+ --cache-prompt-ram 0 \
32
  2>&1 | tee "$BITNET_LOG" | sed 's/^/[BITNET] /' &
33
  LLAMA_PID=$!
34
 
 
42
  [ $ELAPSED -ge $MAX_WAIT ] && echo "[START] FATAL: timeout" && exit 1
43
  echo "[START] BitNet ONLINE β€” iniciando FastAPI..."
44
 
45
+ exec uvicorn ${SERVER}:app --host 0.0.0.0 --port "$PORT" --workers 1 --log-level info