position_usd legge uno SHORT come LONG: size arriva in valore assoluto, direction ignorata #17

Closed
opened 2026-09-27 13:12:09 +00:00 by Adriano · 3 comments
Owner

Revisione 2026-09-27 (goldrake): rilievo Opus 5.5, verifica avversariale fable. Sola lettura, nessun codice toccato.

Rilievo D1 [bug-critico]

FILE: TB:src/live/deribit.py:291-297 (identico in PG:src/live/deribit.py:238-243); forma da TB:src/gateway/deribit.py:307-324 (e cerbero-mcp exchanges/deribit/client.py:381)
COSA: position_usd() restituisce size che il gateway (e cerbero-mcp) mette in abs(), ignorando direction: una posizione SHORT si legge POSITIVA.
SCENARIO: tp_frac=0, skh_sign=-1 -> book_net_target = 0.5eq(-0.25) ≈ -$560 (eq $4.5k). Giro 1: flat -> SELL $560, posizione vera -560. Giro 2: position_usd=+560 -> crossing -> close() manda SELL reduce_only (verso sbagliato: su uno short e' rifiutato/no-op) poi apre SELL -> short cresce ogni ora. Disaster-SL (execution.py:219-225): long = pos>0 -> stop SOTTO il mark lato sell reduce_only -> lo short resta SCOPERTO. Idem nel replay/confronto: l'ombra registra posizione_usd>0.
PROVA: VERIFICATO: lettura gateway positions() "size": abs(size), "direction": ...; position_usd legge solo p.get("size"); book_net_target (book.py:226-227) ammette net<0. Mai accaduto finora: 0 righe con net<0 in PG data/live/book_executions.jsonl (166) e in TB data/live/ombra.jsonl (335) — il caso non e' ancora stato esercitato, non e' smentito. Nessun test passa una posizione direction: short attraverso position_usd (i test mockano position_usd direttamente).
PROPOSTA: in position_usd: s = float(p.get("size") or 0); return -abs(s) if str(p.get("direction","")).lower() in ("short","sell") else abs(s); test che attraversa gateway.positions -> DeribitRead.position_usd con size negativa. Da correggere in PG PRIMA che SKH01 vada short a TP01 flat, e in TB prima della promozione.

Verifica fable D1 VERDETTO: CONFERMATO

GRAVITA': critico
PERCHE': cerbero-mcp sorgente /opt/docker/cerbero-mcp/src/cerbero_mcp/exchanges/deribit/client.py:376-382 e IMMAGINE IN ESECUZIONE (docker exec cerbero-mcp grep -> /app/src/cerbero_mcp/exchanges/deribit/client.py:381) restituiscono "size": abs(size), "direction": "long"|"short"; tools.py:277 e routers/deribit.py:90 passano la lista senza trasformarla. TB src/gateway/deribit.py:307-320 identico (abs + direction). PG src/live/deribit.py:238-243 position_usd legge solo size -> uno short si legge positivo; execution.py:117,177,180,226 decidono side e verso del disaster-SL da pos > 0. Nessun short reale finora: logs/cron_book.log 0 righe net $-/pos $- su 2296 giri, book_executions.jsonl 0 ordini sell non reduce_only; il tp_frac minimo visto con SKH -1 e' 0.399 (serve tp < 1/3 perche' net vada sotto zero: 0.75*tp < 0.25), ma tp_frac=0 con SKH aperto e' uno stato raggiungibile (tp_frac 0.0 registrato il 18/07, 05/08, 19/08). Nessun test attraversa positions()->position_usd con direction short.
CORREZIONE: ok (usare direction; nota: size arriva in USD notional anche per i lineari — verificato empiricamente: pos_after 756.27 = 0.2807 ETH * 2694.26 — quindi la conversione resta com'e', cambia solo il segno). Aggiungere anche il caso size_currency nel ramo (mai usato per i lineari).

Revisione 2026-09-27 (goldrake): rilievo Opus 5.5, verifica avversariale fable. Sola lettura, nessun codice toccato. ### Rilievo D1 [bug-critico] FILE: TB:src/live/deribit.py:291-297 (identico in PG:src/live/deribit.py:238-243); forma da TB:src/gateway/deribit.py:307-324 (e cerbero-mcp exchanges/deribit/client.py:381) COSA: position_usd() restituisce `size` che il gateway (e cerbero-mcp) mette in abs(), ignorando `direction`: una posizione SHORT si legge POSITIVA. SCENARIO: tp_frac=0, skh_sign=-1 -> book_net_target = 0.5*eq*(-0.25) ≈ -$560 (eq $4.5k). Giro 1: flat -> SELL $560, posizione vera -560. Giro 2: position_usd=+560 -> crossing -> close() manda SELL reduce_only (verso sbagliato: su uno short e' rifiutato/no-op) poi apre SELL -> short cresce ogni ora. Disaster-SL (execution.py:219-225): long = pos>0 -> stop SOTTO il mark lato sell reduce_only -> lo short resta SCOPERTO. Idem nel replay/confronto: l'ombra registra posizione_usd>0. PROVA: VERIFICATO: lettura gateway positions() `"size": abs(size), "direction": ...`; position_usd legge solo p.get("size"); book_net_target (book.py:226-227) ammette net<0. Mai accaduto finora: 0 righe con net<0 in PG data/live/book_executions.jsonl (166) e in TB data/live/ombra.jsonl (335) — il caso non e' ancora stato esercitato, non e' smentito. Nessun test passa una posizione `direction: short` attraverso position_usd (i test mockano position_usd direttamente). PROPOSTA: in position_usd: `s = float(p.get("size") or 0); return -abs(s) if str(p.get("direction","")).lower() in ("short","sell") else abs(s)`; test che attraversa gateway.positions -> DeribitRead.position_usd con size negativa. Da correggere in PG PRIMA che SKH01 vada short a TP01 flat, e in TB prima della promozione. ### Verifica fable D1 VERDETTO: CONFERMATO GRAVITA': critico PERCHE': cerbero-mcp sorgente /opt/docker/cerbero-mcp/src/cerbero_mcp/exchanges/deribit/client.py:376-382 e IMMAGINE IN ESECUZIONE (`docker exec cerbero-mcp grep` -> /app/src/cerbero_mcp/exchanges/deribit/client.py:381) restituiscono `"size": abs(size), "direction": "long"|"short"`; tools.py:277 e routers/deribit.py:90 passano la lista senza trasformarla. TB src/gateway/deribit.py:307-320 identico (abs + direction). PG src/live/deribit.py:238-243 `position_usd` legge solo `size` -> uno short si legge positivo; execution.py:117,177,180,226 decidono side e verso del disaster-SL da `pos > 0`. Nessun short reale finora: logs/cron_book.log 0 righe `net $-`/`pos $-` su 2296 giri, book_executions.jsonl 0 ordini sell non reduce_only; il tp_frac minimo visto con `SKH -1` e' 0.399 (serve tp < 1/3 perche' net vada sotto zero: 0.75*tp < 0.25), ma tp_frac=0 con SKH aperto e' uno stato raggiungibile (tp_frac 0.0 registrato il 18/07, 05/08, 19/08). Nessun test attraversa positions()->position_usd con direction short. CORREZIONE: ok (usare `direction`; nota: `size` arriva in USD notional anche per i lineari — verificato empiricamente: pos_after 756.27 = 0.2807 ETH * 2694.26 — quindi la conversione resta com'e', cambia solo il segno). Aggiungere anche il caso `size_currency` nel ramo (mai usato per i lineari).
Adriano added the bug label 2026-09-27 13:12:09 +00:00
Author
Owner

ANALISI (passo 1, Opus) — unita' U1, ramo lavoro/goldrake-u1, repo gitea:Adriano/PythagorasGoal
RIF: #17 (D1), #18 (A1), #24 (A5)
FILE: src/live/deribit.py, src/live/execution.py, tests/ (nuovo tests/test_execution_u1.py)

  1. #17 — DeribitRead.position_usd (src/live/deribit.py ~238): il gateway manda size in abs() e direction a parte.
    Nuova regola: s = float(size or size_currency or 0); dir = str(p.get("direction","")).lower();
    dir in ("sell","short") -> -abs(s); dir in ("buy","long") -> abs(s); altrimenti (assente/"zero") -> s cosi' com'e' (compatibile con un gateway che un giorno mandi il segno).
    Cercare con grep in src/ e scripts/live/ (escluso scripts/research, Old) ogni altro lettore di positions( o di ["size"]/.get("size") di una posizione e applicare la stessa regola SOLO se legge posizioni perp del libro; elencare nel rapporto quelli trovati.
    Verifica a valle: close() e ensure_disaster_sl() decidono lato e stop da pos > 0: con il segno giusto uno short produce close BUY reduce_only e stop SOPRA il mark (disaster_stop_price long=False). Test che lo provano passando per positions() mockato (NON mockare position_usd).
  2. #18 — tetto per ordine d'apertura: MAX_AMOUNT {BTC 0.01, ETH 0.25} resta come DEFAULT (microtest lo usa), ma DeribitTrader acquista un tetto per strumento derivato dal cap di config:
    • attributo self._open_cap: dict[str, float] = {}; metodo set_open_cap(instrument, usd_cap, mark) -> amount_max = 1.10 * usd_cap / mark (usd_cap>0, mark>0 altrimenti ValueError).
    • _submit sul ramo non reduce_only usa self._open_cap.get(instrument, MAX_AMOUNT[instrument]).
    • NON toccare book_execute.py (lo fa l'unita' U2, che chiamera' trader.set_open_cap(inst, r["cap_per_asset"] * r["scala"], mark) prima di rebalance_signed). Scrivi nel rapporto la firma esatta.
    • aggiornare il commento di MAX_AMOUNT e del docstring del modulo (niente "micro-test / deploy gated": il libro e' armato dal 23/06/2026).
  3. #24 — DeribitRead.open_orders: oggi ingoia gli errori e ritorna []. Nuovo: se UNA delle due chiamate ("all", "trigger_all") solleva, sollevare RuntimeError("open_orders illeggibile: : ") — un bracket perso su trigger_all e' lo stesso difetto.
    ensure_disaster_sl: la lettura di posizione+open_orders in try; su eccezione ritorna {"state": "unknown", "notes": ": "} SENZA cancellare ne' piazzare. Aggiornare il docstring (stati possibili).
    Chiamanti di open_orders: grep; shadow.shadow_report lo chiama gia' dentro try/except (ok, non toccare shadow.py: e' di U2).

TEST (tutti in tests/test_execution_u1.py, client finto che sostituisce _post/positions, nessuna rete):

  • position_usd con {"size": 560, "direction": "sell"} -> -560; "buy" -> +560; senza direction e size -3 -> -3.
  • close() su short -> _submit con side "buy", reduce_only.
  • ensure_disaster_sl su short -> stop sopra il mark, side "buy".
  • set_open_cap: equity 4583, cap/asset 2291.5, ETH mark 2694 -> apertura di 0,2807 ETH NON solleva; apertura di 3x il tetto solleva GuardrailError; senza set_open_cap il default MAX_AMOUNT resta.
  • open_orders: "trigger_all" solleva -> RuntimeError; ensure_disaster_sl con open_orders che solleva -> state "unknown", nessuna cancel/place chiamata.
    Suite completa: cd <worktree> && /opt/docker/PythagorasGoal/.venv/bin/python -m pytest -q -p no:cacheprovider -> baseline 1098 passed, 3 skipped. Nessun test esistente deve rompersi; se uno esistente blinda il comportamento vecchio, aggiornalo e dillo.

NON TOCCARE: scripts/live/book_execute.py, src/live/shadow.py, src/live/book.py (U2); config/live.json; tutto sotto /opt/docker/PythagorasGoal (produzione); nessuna chiamata di rete, nessun ordine.

ANALISI (passo 1, Opus) — unita' U1, ramo lavoro/goldrake-u1, repo gitea:Adriano/PythagorasGoal RIF: #17 (D1), #18 (A1), #24 (A5) FILE: src/live/deribit.py, src/live/execution.py, tests/ (nuovo tests/test_execution_u1.py) 1. #17 — DeribitRead.position_usd (src/live/deribit.py ~238): il gateway manda `size` in abs() e `direction` a parte. Nuova regola: s = float(size or size_currency or 0); dir = str(p.get("direction","")).lower(); dir in ("sell","short") -> -abs(s); dir in ("buy","long") -> abs(s); altrimenti (assente/"zero") -> s cosi' com'e' (compatibile con un gateway che un giorno mandi il segno). Cercare con grep in src/ e scripts/live/ (escluso scripts/research, Old) ogni altro lettore di `positions(` o di `["size"]`/`.get("size")` di una posizione e applicare la stessa regola SOLO se legge posizioni perp del libro; elencare nel rapporto quelli trovati. Verifica a valle: close() e ensure_disaster_sl() decidono lato e stop da `pos > 0`: con il segno giusto uno short produce close BUY reduce_only e stop SOPRA il mark (disaster_stop_price long=False). Test che lo provano passando per positions() mockato (NON mockare position_usd). 2. #18 — tetto per ordine d'apertura: MAX_AMOUNT {BTC 0.01, ETH 0.25} resta come DEFAULT (microtest lo usa), ma DeribitTrader acquista un tetto per strumento derivato dal cap di config: - attributo `self._open_cap: dict[str, float] = {}`; metodo `set_open_cap(instrument, usd_cap, mark)` -> amount_max = 1.10 * usd_cap / mark (usd_cap>0, mark>0 altrimenti ValueError). - `_submit` sul ramo non reduce_only usa `self._open_cap.get(instrument, MAX_AMOUNT[instrument])`. - NON toccare book_execute.py (lo fa l'unita' U2, che chiamera' `trader.set_open_cap(inst, r["cap_per_asset"] * r["scala"], mark)` prima di rebalance_signed). Scrivi nel rapporto la firma esatta. - aggiornare il commento di MAX_AMOUNT e del docstring del modulo (niente "micro-test / deploy gated": il libro e' armato dal 23/06/2026). 3. #24 — DeribitRead.open_orders: oggi ingoia gli errori e ritorna []. Nuovo: se UNA delle due chiamate ("all", "trigger_all") solleva, sollevare RuntimeError("open_orders illeggibile: <tipo>: <errore>") — un bracket perso su trigger_all e' lo stesso difetto. ensure_disaster_sl: la lettura di posizione+open_orders in try; su eccezione ritorna {"state": "unknown", "notes": "<Tipo>: <msg>"} SENZA cancellare ne' piazzare. Aggiornare il docstring (stati possibili). Chiamanti di open_orders: grep; shadow.shadow_report lo chiama gia' dentro try/except (ok, non toccare shadow.py: e' di U2). TEST (tutti in tests/test_execution_u1.py, client finto che sostituisce _post/positions, nessuna rete): - position_usd con {"size": 560, "direction": "sell"} -> -560; "buy" -> +560; senza direction e size -3 -> -3. - close() su short -> _submit con side "buy", reduce_only. - ensure_disaster_sl su short -> stop sopra il mark, side "buy". - set_open_cap: equity 4583, cap/asset 2291.5, ETH mark 2694 -> apertura di 0,2807 ETH NON solleva; apertura di 3x il tetto solleva GuardrailError; senza set_open_cap il default MAX_AMOUNT resta. - open_orders: "trigger_all" solleva -> RuntimeError; ensure_disaster_sl con open_orders che solleva -> state "unknown", nessuna cancel/place chiamata. Suite completa: `cd <worktree> && /opt/docker/PythagorasGoal/.venv/bin/python -m pytest -q -p no:cacheprovider` -> baseline 1098 passed, 3 skipped. Nessun test esistente deve rompersi; se uno esistente blinda il comportamento vecchio, aggiornalo e dillo. NON TOCCARE: scripts/live/book_execute.py, src/live/shadow.py, src/live/book.py (U2); config/live.json; tutto sotto /opt/docker/PythagorasGoal (produzione); nessuna chiamata di rete, nessun ordine.
Author
Owner

ESITO: fatto sul ramo, non in produzione
RIF: gitea:Adriano/PythagorasGoal#17
COSA: correzione su lavoro/goldrake (pushato), analisi Opus, codice Sonnet, controllo finale Opus
FILE: commit 2bf4f9c
PROVA: /home/adriano/lavoro/PythagorasGoal pytest -q → 1166 passed, 3 skipped
MANCA: merge su main — richiede ok esplicito di Adriano (libro vivo). Chiusura con --soluzione dopo il merge.
PROSSIMO: ok di Adriano → merge lavoro/goldrake su main → git pull in /opt/docker/PythagorasGoal

ESITO: fatto sul ramo, non in produzione RIF: gitea:Adriano/PythagorasGoal#17 COSA: correzione su lavoro/goldrake (pushato), analisi Opus, codice Sonnet, controllo finale Opus FILE: commit 2bf4f9c PROVA: /home/adriano/lavoro/PythagorasGoal pytest -q → 1166 passed, 3 skipped MANCA: merge su main — richiede ok esplicito di Adriano (libro vivo). Chiusura con --soluzione dopo il merge. PROSSIMO: ok di Adriano → merge lavoro/goldrake su main → git pull in /opt/docker/PythagorasGoal
Adriano added the attesa:adriano label 2026-09-27 14:17:35 +00:00
Author
Owner

Soluzione

src/live/deribit.py position_usd: segno da direction (sell/short -> negativo), non dal solo size che cerbero manda in abs().
COMMIT: 2bf4f9c, su main dal 27/09/2026 (merge con ok di Adriano). Produzione aggiornata.

**Soluzione** src/live/deribit.py position_usd: segno da direction (sell/short -> negativo), non dal solo size che cerbero manda in abs(). COMMIT: 2bf4f9c, su main dal 27/09/2026 (merge con ok di Adriano). Produzione aggiornata.
Adriano removed the attesa:adriano label 2026-09-27 14:27:52 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Adriano/PythagorasGoal#17