viarl199020 commited on
Commit
ce0de59
Β·
verified Β·
1 Parent(s): 46a0918

Upload server_H.py

Browse files
Files changed (1) hide show
  1. server_H.py +290 -125
server_H.py CHANGED
@@ -1,32 +1,37 @@
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,
@@ -37,8 +42,10 @@ OUTPUT (100% compatible con swarm_engine):
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,
@@ -57,9 +64,9 @@ try:
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}"
@@ -76,7 +83,7 @@ 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:
@@ -106,28 +113,20 @@ async def _bitnet_single_token(prompt: str) -> str:
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
@@ -144,14 +143,13 @@ def _ring_push(sym: str, token: str):
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
 
@@ -161,11 +159,9 @@ def _shadow_get(sym: str, current_temp: float) -> str | None:
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"]
@@ -174,24 +170,131 @@ 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)
@@ -199,40 +302,73 @@ def _monte_carlo(win_rate, avg_win, avg_loss, kelly_f, horizon, scenarios) -> fl
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
 
@@ -243,18 +379,27 @@ def _math_audit(payload: dict) -> dict:
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,
@@ -262,12 +407,15 @@ def _math_audit(payload: dict) -> dict:
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"
@@ -276,47 +424,49 @@ _SYS_H = (
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)
@@ -324,54 +474,69 @@ async def _background_h_update(sym: str, prompt: str, temp: float):
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):
@@ -382,4 +547,4 @@ async def audit(request: Request):
382
  result = await _audit(payload)
383
  return JSONResponse(result)
384
 
385
- print("[H] βœ… RiskAuditor v21.5 β€” Risk Thermometer + Shadow + Single-Token G/Y/R")
 
1
  """
2
+ server_H.py β€” Cerebro H β€” RiskAuditor v22.5 (APEX Elastic Payload + PDT-Aware)
3
+ ===============================================================================
4
+ CAMBIOS v22.5 sobre v21.5 (compatibilidad total preservada):
5
+
6
+ PROBLEMA CORREGIDO:
7
+ "[H-VIRTUAL] AVAX/USD: ... (H offline -> autonomo)"
8
+ El payload del nuevo data_manager_anchors.py inyecta mΓ©tricas bajo
9
+ claves anidadas ("metrics", "cognition", "risk") que v21.5 no buscaba.
10
+ Resultado: balance extraΓ­do como 0, Kelly como KELLY_MIN, H caΓ­a offline.
11
+
12
+ FIX 1 β€” EXTRACCIΓ“N ELÁSTICA MULTI-CAPA:
13
+ _math_audit() ahora usa cascadas de .get() que buscan cada variable
14
+ en: raΓ­z β†’ metrics β†’ cognition β†’ risk β†’ default seguro.
15
+ Garantiza que NUNCA falle ni devuelva 0 por ausencia de clave.
16
+
17
+ FIX 2 β€” DEGRADED ONLINE (no offline):
18
+ Si faltan datos crΓ­ticos (balance=0, bars=[]), H responde con un tamaΓ±o
19
+ dinΓ‘mico de emergencia basado en el balance conocido: 1% fijo.
20
+ El canal permanece ONLINE y el flag "degraded_graceful"=True avisa al swarm.
21
+ Esto elimina el "[H-VIRTUAL] ... (H offline -> autonomo)".
22
+
23
+ FIX 3 β€” CONTEXT ENRICHMENT desde mΓ©tricas del radar:
24
+ anchor_correlation "BEARISH" β†’ reduce Kelly en 20% (contexto bajista macro).
25
+ velocity_zero_bars > 10 β†’ penalizaciΓ³n Kelly adicional 10% (capital muerto).
26
+ macro_impact_weight = 2 β†’ activa modo "institucional" = mayor precauciΓ³n.
27
+
28
+ PRESERVADO de v21.5:
29
+ βœ… Kelly Continuo + Monte Carlo + Risk Thermometer
30
+ βœ… Shadow Cache con TTL adaptativo + propagaciΓ³n de temperatura
31
+ βœ… Single-Token BitNet G/Y/R + ring buffer + ensemble histΓ³rico
32
+ βœ… Output 100% compatible con swarm_engine v17.5+
33
+
34
+ OUTPUT v22.5 (superset de v21.5):
35
  {
36
  "pdt_ok": bool,
37
  "size_usd": float,
 
42
  "zscore": float,
43
  "monte_carlo_ruin_prob": float,
44
  "panic_activated": bool,
45
+ "risk_token": str, # "G"|"Y"|"R"
46
+ "risk_temperature": float, # 0.0-1.0
47
+ "degraded_graceful": bool, # True si faltan datos pero H sigue online
48
+ "anchor_context": str, # "BULLISH"|"BEARISH"|"NEUTRAL" recibido
49
  "_math_ms": float,
50
  "_llm_ms": float,
51
  "_total_ms": float,
 
64
  except ImportError:
65
  _NP = False
66
 
67
+ app = FastAPI(title="Cerebro H β€” RiskAuditor v22.5 APEX Elastic")
68
 
69
+ # ── Config (idΓ©ntica a v21.5) ─────────────────────────────────────────────────
70
  BITNET_PORT = int(os.environ.get("BITNET_PORT", "8080"))
71
  BITNET_HOST = os.environ.get("BITNET_HOST", "127.0.0.1")
72
  BITNET_BASE = f"http://{BITNET_HOST}:{BITNET_PORT}"
 
83
  RUIN_THRESHOLD = float(os.environ.get("RUIN_THRESHOLD","0.25"))
84
  RUIN_DRAWDOWN = float(os.environ.get("RUIN_DRAWDOWN", "0.20"))
85
 
86
+ # ��─ BitNet Client (idΓ©ntico a v21.5) ─────────────────────────────────────────
87
  _bitnet_client: httpx.AsyncClient | None = None
88
 
89
  def _get_client() -> httpx.AsyncClient:
 
113
  except Exception:
114
  return ""
115
 
116
+ # ── Risk Thermometer (idΓ©ntico a v21.5) ──────────────────────────────────────
117
+ _risk_temp: dict = {}
118
+ _token_ring: dict = {}
119
  _thermo_lock = threading.Lock()
120
 
121
  def _update_thermometer(sym: str, mc_ruin: float, streak: int,
122
  drawdown: float, zscore: float) -> float:
 
 
 
 
 
123
  streak_norm = min(streak / 5.0, 1.0)
124
  drawdown_norm = min(drawdown / RUIN_DRAWDOWN, 1.0)
125
  zscore_norm = min(abs(zscore) / ZSCORE_PANIC, 1.0)
 
126
  new_temp = (mc_ruin * 0.40 + streak_norm * 0.30 +
127
  drawdown_norm * 0.20 + zscore_norm * 0.10)
 
128
  with _thermo_lock:
129
  prev = _risk_temp.get(sym, new_temp)
 
130
  smoothed = prev * 0.30 + new_temp * 0.70
131
  _risk_temp[sym] = round(smoothed, 4)
132
  return smoothed
 
143
  _token_ring[sym].append(token)
144
 
145
  def _ring_last5_all_R(sym: str) -> bool:
 
146
  with _thermo_lock:
147
  ring = _token_ring.get(sym, [])
148
  if len(ring) < 5:
149
  return False
150
  return all(t == "R" for t in list(ring)[-5:])
151
 
152
+ # ── Shadow Cache (idΓ©ntico a v21.5) ──────────────────────────────────────────
153
  _shadow: dict = {}
154
  _shadow_lock = threading.Lock()
155
 
 
159
  if not e:
160
  return None
161
  age = time.time() - e["ts"]
 
162
  ttl = 10.0 if current_temp > 0.65 else (20.0 if current_temp > 0.35 else CACHE_TTL)
163
  if age > ttl:
164
  return None
 
165
  if abs(current_temp - e["temp"]) > 0.15:
166
  return None
167
  return e["token"]
 
170
  with _shadow_lock:
171
  _shadow[sym] = {"token": token, "ts": time.time(), "temp": temp}
172
 
 
173
 
174
+ # ══════════════════════════════════════════════════════════════════════════════
175
+ # v22.5: EXTRACTOR ELÁSTICO MULTI-CAPA
176
+ # Busca cada clave en: raΓ­z β†’ metrics β†’ cognition β†’ risk β†’ default
177
+ # ══════════════════════════════════════════════════════════════════════════════
178
+
179
+ def _elastic_get(payload: dict, key: str, default, cast=None):
180
+ """
181
+ Extrae una clave buscando en mΓΊltiples capas del payload de forma segura.
182
+ Nunca lanza excepciΓ³n; retorna `default` si no encuentra nada vΓ‘lido.
183
+
184
+ Capas buscadas en orden:
185
+ 1. raΓ­z del payload
186
+ 2. payload["metrics"]
187
+ 3. payload["cognition"]
188
+ 4. payload["risk"]
189
+ """
190
+ _layers = [
191
+ payload,
192
+ payload.get("metrics", {}) or {},
193
+ payload.get("cognition", {}) or {},
194
+ payload.get("risk", {}) or {},
195
+ ]
196
+ for layer in _layers:
197
+ v = layer.get(key)
198
+ if v is not None:
199
+ if cast is None:
200
+ return v
201
+ try:
202
+ return cast(v)
203
+ except (ValueError, TypeError):
204
+ continue
205
+ return default
206
+
207
+
208
+ def _extract_h_fields(payload: dict) -> dict:
209
+ """
210
+ Extrae todos los campos que necesita _math_audit de forma elΓ‘stica.
211
+ Retorna un dict limpio con valores garantizados y sus flags de calidad.
212
+ """
213
+ sym = str(_elastic_get(payload, "sym", payload.get("symbol", "?"), str))
214
+ balance = _elastic_get(payload, "balance", None, float)
215
+ # balance puede venir como "account_balance" en el payload del swarm_engine
216
+ if balance is None or balance <= 0:
217
+ balance = _elastic_get(payload, "account_balance", None, float)
218
+ if balance is None or balance <= 0:
219
+ balance = 10000.0 # default seguro
220
+ balance = float(balance)
221
+
222
+ daytrades = _elastic_get(payload, "daytrade_count", 0, int)
223
+ is_crypto = bool(_elastic_get(payload, "is_crypto", False, str) in (True,"true","True","1")) \
224
+ or "/" in str(sym)
225
+ bars = payload.get("bars", []) or []
226
+ win_rate = _elastic_get(payload, "win_rate", 0.45, float)
227
+ avg_win = _elastic_get(payload, "avg_win_pct", 0.012, float)
228
+ avg_loss = _elastic_get(payload, "avg_loss_pct", 0.008, float)
229
+ streak = _elastic_get(payload, "streak_losses", 0, int)
230
+ n_trades = _elastic_get(payload, "n_trades", 0, int)
231
+ cronos = _elastic_get(payload, "cronos_score", 0.5, float)
232
+ bs_pct = _elastic_get(payload, "bootstrap_pct", 0.010, float)
233
+ open_pos = _elastic_get(payload, "open_positions", 0, int)
234
+
235
+ # ── Campos del nuevo radar (data_manager_anchors v22.5) ──────────────────
236
+ anchor_corr = str(_elastic_get(payload, "anchor_correlation", "NEUTRAL", str)).upper()
237
+ vel_zero = _elastic_get(payload, "velocity_zero_bars", 0, int)
238
+ macro_weight = _elastic_get(payload, "macro_impact_weight", 0, int)
239
+ trade_side = str(_elastic_get(payload, "trade_side", "long", str)).lower()
240
+
241
+ # Flag de calidad de datos
242
+ degraded = (balance <= 10001.0 and not payload.get("balance") and
243
+ not (payload.get("metrics") or {}).get("balance"))
244
+
245
+ return {
246
+ "sym": sym, "balance": balance, "daytrades": daytrades,
247
+ "is_crypto": is_crypto, "bars": bars,
248
+ "win_rate": win_rate, "avg_win": avg_win, "avg_loss": avg_loss,
249
+ "streak": streak, "n_trades": n_trades, "cronos": cronos,
250
+ "bs_pct": bs_pct, "open_pos": open_pos,
251
+ "anchor_corr": anchor_corr, "vel_zero": vel_zero,
252
+ "macro_weight": macro_weight, "trade_side": trade_side,
253
+ "degraded": degraded,
254
+ }
255
+
256
+
257
+ # ══════════════════════════════════════════════════════════════════════════════
258
+ # MATH: Kelly + MC + ZScore (actualizado con contexto del radar)
259
+ # ══════════════════════════════════════════════════════════════════════════════
260
+
261
+ def _kelly_continuous(win_rate, avg_win, avg_loss, streak, cronos, n_trades, bs_pct,
262
+ anchor_corr: str = "NEUTRAL", vel_zero: int = 0,
263
+ macro_weight: int = 0) -> float:
264
+ """
265
+ Kelly Continuo v22.5: aΓ±ade penalizaciones por contexto del radar.
266
+ - anchor_corr "BEARISH" β†’ -20% (mercado lΓ­der bajista)
267
+ - vel_zero > 10 β†’ -10% (capital estancado)
268
+ - macro_weight = 2 β†’ -15% (evento institucional activo)
269
+ """
270
  if n_trades < 20:
271
+ base = max(KELLY_MIN, min(bs_pct, KELLY_MAX))
272
+ elif avg_loss <= 0 or avg_win <= 0:
273
+ base = KELLY_MIN
274
+ else:
275
+ kelly_raw = (win_rate * avg_win - (1 - win_rate) * avg_loss) / avg_win
276
+ kelly_half = kelly_raw / 2.0
277
+ streak_pen = 1.0 - min(streak * 0.15, 0.45)
278
+ cronos_amp = 0.80 + cronos * 0.40
279
+ base = max(KELLY_MIN, min(KELLY_MAX, kelly_half * streak_pen * cronos_amp))
280
+
281
+ # ── Penalizaciones del radar (v22.5) ──────────────────────────────────────
282
+ radar_factor = 1.0
283
+ if anchor_corr == "BEARISH":
284
+ radar_factor *= 0.80 # -20%
285
+ if vel_zero > 10:
286
+ radar_factor *= 0.90 # -10%: capital muerto > 10 barras
287
+ if macro_weight >= 2:
288
+ radar_factor *= 0.85 # -15%: evento institucional
289
+
290
+ return round(max(KELLY_MIN, min(KELLY_MAX, base * radar_factor)), 6)
291
+
292
 
293
  def _monte_carlo(win_rate, avg_win, avg_loss, kelly_f, horizon, scenarios) -> float:
294
  if _NP:
295
  np.random.seed(42)
296
+ wins = np.random.random((scenarios, horizon)) < win_rate
297
+ factors = np.where(wins, 1.0 + kelly_f * avg_win, 1.0 - kelly_f * avg_loss)
298
  portfolios = np.cumprod(factors, axis=1).min(axis=1)
299
  return round(float(np.sum(portfolios < (1.0 - RUIN_DRAWDOWN))) / scenarios, 4)
300
  import random; random.seed(42)
 
302
  for _ in range(scenarios):
303
  p = 1.0
304
  for _ in range(horizon):
305
+ p *= ((1 + kelly_f * avg_win) if random.random() < win_rate
306
+ else (1 - kelly_f * avg_loss))
307
+ if p < 1 - RUIN_DRAWDOWN:
308
+ ruin += 1; break
309
  return round(ruin / scenarios, 4)
310
 
311
+
312
  def _zscore_bars(bars: list) -> float:
313
+ closes = [float(b.get("c", b.get("close", 0))) for b in bars
314
+ if b.get("c") or b.get("close")]
315
+ if len(closes) < 4:
316
+ return 0.0
317
+ rets = [abs(closes[i] / closes[i-1] - 1) for i in range(1, len(closes))]
318
  if _NP:
319
  a = np.array(rets)
320
  return round(float(abs((rets[-1] - a.mean()) / (a.std() or 0.001))), 3)
321
+ mean = sum(rets) / len(rets)
322
+ std = math.sqrt(sum((r - mean) ** 2 for r in rets) / len(rets)) or 0.001
323
+ return round(abs(rets[-1] - mean) / std, 3)
324
+
325
 
326
  def _math_audit(payload: dict) -> dict:
327
+ """
328
+ v22.5: usa _extract_h_fields() para extracciΓ³n elΓ‘stica.
329
+ Si el balance viene de un payload degradado, responde con tamaΓ±o dinΓ‘mico
330
+ de emergencia (1% del balance real) en lugar de caer offline.
331
+ """
332
  t0 = time.perf_counter()
333
+ f = _extract_h_fields(payload)
334
+
335
+ sym = f["sym"]
336
+ balance = f["balance"]
337
+ daytrades = f["daytrades"]
338
+ is_crypto = f["is_crypto"]
339
+ bars = f["bars"]
340
+ win_rate = f["win_rate"]
341
+ avg_win = f["avg_win"]
342
+ avg_loss = f["avg_loss"]
343
+ streak = f["streak"]
344
+ n_trades = f["n_trades"]
345
+ cronos = f["cronos"]
346
+ bs_pct = f["bs_pct"]
347
+ open_pos = f["open_pos"]
348
+ anchor_corr = f["anchor_corr"]
349
+ vel_zero = f["vel_zero"]
350
+ macro_weight = f["macro_weight"]
351
+ degraded = f["degraded"]
352
 
353
  pdt_ok = True if is_crypto else not (daytrades >= PDT_LIMIT and balance < PDT_MIN_BALANCE)
354
+
355
+ kelly = _kelly_continuous(
356
+ win_rate, avg_win, avg_loss, streak, cronos, n_trades, bs_pct,
357
+ anchor_corr=anchor_corr, vel_zero=vel_zero, macro_weight=macro_weight,
358
+ )
359
+
360
+ # ── TamaΓ±o de emergencia si el payload estaba degradado ───────────────────
361
+ # Garantiza que H permanezca ONLINE aunque falten datos del radar.
362
+ # El swarm_engine recibirΓ‘ degraded_graceful=True para saberlo.
363
+ if degraded:
364
+ # 1% del balance como tamaΓ±o de emergencia β€” igual que KELLY-BOOTSTRAP
365
+ emergency_kelly = max(KELLY_MIN, min(bs_pct, 0.010))
366
+ size = round(balance * emergency_kelly, 2)
367
+ kelly = emergency_kelly
368
+ print(f"[H/GRACEFUL] {sym}: payload incompleto β†’ size_emergencia=${size:.2f} (1%) balance=${balance:.2f}")
369
+ else:
370
+ size = round(balance * kelly, 2)
371
+
372
  zscore = _zscore_bars(bars)
373
  mc = _monte_carlo(win_rate, avg_win, avg_loss, kelly, MC_HORIZON, MC_SCENARIOS)
374
 
 
379
  approx_dd = streak * avg_loss * kelly
380
  temp = _update_thermometer(sym, mc, streak, approx_dd, zscore)
381
 
382
+ if open_pos >= 8:
383
+ size = round(size * 0.70, 2)
384
 
385
+ risk = ("monte_carlo_ruin" if mc >= RUIN_THRESHOLD else
386
+ "high_zscore" if zscore >= ZSCORE_PANIC else
387
+ "streak_loss" if streak >= 3 else "none")
388
+
389
+ vault = ("blocked_pdt" if not pdt_ok else
390
+ "conservative" if risk in ("monte_carlo_ruin", "high_zscore", "streak_loss") else
391
  "protected")
392
 
393
  math_ms = (time.perf_counter() - t0) * 1000
394
+
395
+ anchor_note = ""
396
+ if anchor_corr == "BEARISH":
397
+ anchor_note = " anchor=BEARISH(-20%)"
398
+ if vel_zero > 10:
399
+ anchor_note += f" vel_zero={vel_zero}(-10%)"
400
+
401
  print(f"[H/MATH] {sym}: pdt={pdt_ok} kelly={kelly:.4f} ${size:.0f} "
402
+ f"mc={mc:.3f} temp={temp:.2f}{anchor_note} | {math_ms:.1f}ms")
403
 
404
  return {
405
  "pdt_ok": pdt_ok, "size_usd": size, "risk_warning": risk,
 
407
  "kelly_continuous": kelly, "zscore": zscore,
408
  "monte_carlo_ruin_prob": mc, "panic_activated": False,
409
  "risk_temperature": round(temp, 4),
410
+ "degraded_graceful": degraded,
411
+ "anchor_context": anchor_corr,
412
  "_math_ms": round(math_ms, 2), "_sym": sym,
413
  "_balance": balance, "_temp": temp,
414
  "_approx_dd": round(approx_dd, 4),
415
  }
416
 
417
+
418
+ # ── Prompt BitNet (mismo formato que v21.5) ───────────────────────────────────
419
  _SYS_H = (
420
  "<|im_start|>system\n"
421
  "Risk level G/Y/R. Respond ONLY with one letter.\n"
 
424
  "<|im_start|>user\n"
425
  )
426
 
427
+ def _build_h_prompt(mc: float, temp: float, streak: int, dd: float,
428
+ anchor: str = "NEUTRAL") -> str:
429
  return (
430
  _SYS_H
431
+ + f'{{"mc":{mc:.2f},"t":{temp:.2f},"s":{streak},"d":{dd:.2f},"a":"{anchor[:1]}"}}\n'
432
  + "<|im_end|>\n"
433
  + "<|im_start|>assistant\n"
434
  )
435
 
436
+
437
  def _token_to_output(token: str, math: dict, llm_ms: float,
438
  is_shadow: bool, total_ms: float) -> dict:
439
+ sym = math["_sym"]
 
440
  if _ring_last5_all_R(sym) and token != "R":
441
  token = "R"
442
  print(f"[H/RING-VETO] {sym}: ΓΊltimos 5 tokens R β†’ bloqueo automΓ‘tico")
443
 
444
  if token == "R":
445
+ math["size_usd"] = 0.0
446
  math["kelly_fraction"] = 0.0
447
+ math["vault_status"] = "emergency_hold"
448
  math["panic_activated"] = True
449
 
450
  _ring_push(sym, token)
451
 
452
  out = {k: v for k, v in math.items() if not k.startswith("_")}
453
  out.update({
454
+ "risk_token": token,
455
+ "_math_ms": math["_math_ms"],
456
+ "_llm_ms": round(llm_ms, 1),
457
+ "_total_ms": round(total_ms, 1),
458
+ "_shadow": is_shadow,
459
  })
460
  return out
461
 
462
+
463
+ # ── Background LLM update (idΓ©ntico a v21.5) ─────────────────────────────────
464
 
465
  async def _background_h_update(sym: str, prompt: str, temp: float):
466
  t0 = time.perf_counter()
467
+ raw = await _bitnet_single_token(prompt)
468
  llm_ms = (time.perf_counter() - t0) * 1000
469
+ token = raw.strip()[:1].upper()
470
  if token in ("G", "Y", "R"):
471
  _shadow_set(sym, token, temp)
472
  _ring_push(sym, token)
 
474
  else:
475
  print(f"[H/LLM-GUARD] {sym}: token invΓ‘lido='{raw[:10]}' β†’ shadow preservado")
476
 
477
+
478
+ # ── Pipeline principal v22.5 ────────────────────────���─────────────────────────
479
 
480
  async def _audit(payload: dict) -> dict:
481
  t0 = time.perf_counter()
482
+
483
+ math = _math_audit(payload)
484
+ sym = math["_sym"]
485
+ temp = math["_temp"]
486
+ mc = math["monte_carlo_ruin_prob"]
487
+ streak = _elastic_get(payload, "streak_losses", 0, int)
488
+ dd = math["_approx_dd"]
489
+ anchor = math.get("anchor_context", "NEUTRAL")
490
 
491
  cached_token = _shadow_get(sym, temp)
492
+ is_shadow = cached_token is not None
493
 
494
  if is_shadow:
495
+ token = cached_token
496
  llm_ms = 0.0
 
497
  with _shadow_lock:
498
  age = time.time() - _shadow.get(sym, {}).get("ts", 0)
499
  if age > CACHE_TTL * 0.5:
500
  asyncio.create_task(
501
+ _background_h_update(sym, _build_h_prompt(mc, temp, streak, dd, anchor), temp))
502
+ print(f"[H/SHADOW] {sym}: token={token} temp={temp:.2f} degraded={math['degraded_graceful']} | 0ms")
503
  else:
504
+ t_llm = time.perf_counter()
505
+ prompt = _build_h_prompt(mc, temp, streak, dd, anchor)
506
  raw = await _bitnet_single_token(prompt)
507
  llm_ms = (time.perf_counter() - t_llm) * 1000
508
  token = raw.strip()[:1].upper()
509
  if token not in ("G", "Y", "R"):
 
510
  token = _temp_to_token(temp)
511
  print(f"[H/LLM-GUARD] {sym}: token='{raw[:10]}' β†’ math_fallback={token}")
512
  else:
513
  _shadow_set(sym, token, temp)
514
+ print(f"[H/LLM] {sym}: token={token} anchor={anchor} | {llm_ms:.1f}ms")
515
 
516
  total_ms = (time.perf_counter() - t0) * 1000
517
  return _token_to_output(token, math, llm_ms, is_shadow, total_ms)
518
 
519
+
520
  # ── Endpoints ─────────────────────────────────────────────────────────────────
521
 
522
+ @app.get("/")
523
+ async def root():
524
+ return {"status": "online", "cerebro": "H", "version": "22.5-APEX-ELASTIC"}
525
+
526
  @app.get("/health")
527
  async def health():
528
+ return {
529
+ "status": "online", "cerebro": "H", "version": "22.5-APEX-ELASTIC",
530
+ "model": "BitNet-b1.58-2B-4T-i2_s", "engine": "ik_llama.cpp",
531
+ "fixes": [
532
+ "elastic_multi_layer_payload_extraction",
533
+ "degraded_graceful_online_mode",
534
+ "anchor_correlation_kelly_penalty",
535
+ "velocity_zero_bars_kelly_penalty",
536
+ "macro_weight_kelly_penalty",
537
+ ],
538
+ "architecture": "Kelly+MC+Thermometer + Shadow-Cache + Single-Token G/Y/R + ElasticGet",
539
+ }
540
 
541
  @app.post("/audit")
542
  async def audit(request: Request):
 
547
  result = await _audit(payload)
548
  return JSONResponse(result)
549
 
550
+ print("[H] βœ… RiskAuditor v22.5 APEX β€” Elastic Payload + Degraded-Online + Anchor-Kelly")