#!/usr/bin/env python """r0906_usde_tetto_sonda.py — sonda il TETTO del venue sull'USDE a SALDO NEUTRO, passi da 1-2 USDE. INVIA ORDINI VERI (spot USDE_USDC, fee 0, ~$0,001 di spread per ordine). Metodo (diari 30-31/08, sezione «la lezione di metodo»): un probe unico di taglia sbagliata produce un falso negativo che si legge come risultato. Qui si sale a passi piccoli fino al primo rifiuto, si ripete il rifiuto dopo aver riletto il book (per escludere l'ask che si e' mosso: il messaggio del venue e' lo stesso `not_enough_funds_in_currency`), poi si CONFERMA a saldo neutro: SELL 1 -> BUY 1 (deve passare) -> BUY 1 (deve essere rifiutato). Il bracket e' [L, L+1) USDE, con l'equity totale letta nello stesso istante, cosi' la frazione ha la sua banda. Guardie: prezzo dal BOOK (ask+5 tick, tetto 1,0010 / bid-5 tick, pavimento 0,9990), banda [0,995, 1,005]; al massimo MAX_ADD USDE in piu' (quota mai oltre QUOTA_STOP); pausa fra ordini. uv run python scripts/research/r0906_usde_tetto_sonda.py # dry-run: stato e piano uv run python scripts/research/r0906_usde_tetto_sonda.py --esegui # sonda davvero """ from __future__ import annotations import json import sys import time from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "live")) from src.live import usde as U # noqa: E402 from src.live.deribit import DeribitRead # noqa: E402 import usde_convert as C # noqa: E402 (book_top, leggi_stato: stessi attrezzi) LOG = ROOT / "data" / "live" / "usde_convert.jsonl" PX_MIN, PX_MAX = C.PX_MIN, C.PX_MAX TICK, MARGINE, CAP_BUY, CAP_SELL = 0.0001, 5, 1.0010, 0.9990 MAX_ADD = 60 # USDE in piu' al massimo (--max-add N): oltre, «tetto non trovato entro il limite» QUOTA_HARD = 0.70 # mai oltre: e' la quota decisa dall'operatore il 30/08 (`usde.quota_target`), e coincide # col CUSCINO DI REGOLAMENTO derivato da config (usde_convert.cuscino_richiesto_usd), # che qui e' la guardia che morde davvero: si legge a ogni passo, non si ridichiara. SCALA = [100.0, 20.0, 4.0, 1.0] # passi: si scende di uno a ogni doppio rifiuto (M13: risoluzione dell'effetto) PAUSA, PAUSA_RIFIUTO = 12.0, 20.0 def stato(c: DeribitRead) -> dict: eq_usdc, eq_usde, px, fonte = C.leggi_stato(c) val, _ = U.valuta(eq_usde, px) return {"usdc": eq_usdc, "usde": eq_usde, "px": px, "fonte": fonte, "val": val, "tot": eq_usdc + val, "quota": val / (eq_usdc + val) if (eq_usdc + val) > 0 else None} def prezzo_ordine(side: str) -> float | None: bid, ask = C.book_top() if side == "buy": return None if ask is None else round(min(ask + MARGINE * TICK, CAP_BUY), 4) return None if bid is None else round(max(bid - MARGINE * TICK, CAP_SELL), 4) def ordine(c: DeribitRead, side: str, amt: float, px: float) -> tuple[bool, str]: resp = c._unwrap(c._post("/mcp-deribit/tools/place_order", {"instrument_name": U.SPOT, "side": side, "amount": float(amt), "type": "limit", "price": px, "label": "usde-sonda-0906"})) or {} if not isinstance(resp, dict) or resp.get("error") or "order" not in resp: err = resp.get("error", resp) if isinstance(resp, dict) else resp return False, str(err)[:120] o = resp["order"] return True, f"{o.get('order_state')} filled {o.get('filled_amount')}" def main() -> int: esegui = "--esegui" in sys.argv[1:] max_add = float(sys.argv[sys.argv.index("--max-add") + 1]) if "--max-add" in sys.argv else MAX_ADD c = DeribitRead() s0 = stato(c) if s0["px"] is None: print(" ✗ prezzo USDE non leggibile — non si sonda al buio (P5)"); return 2 print("=" * 78) print(f" SONDA TETTO USDE — {datetime.now(timezone.utc):%Y-%m-%dT%H:%M:%SZ} " f"{'ESECUZIONE' if esegui else 'DRY-RUN'}") print("=" * 78) cap = U.cfg().get("venue_cap_frac") print(f" conto : USDC ${s0['usdc']:,.2f} + USDE {s0['usde']:,.4f} (${s0['val']:,.2f}) = ${s0['tot']:,.2f}" f" quota {s0['quota']:.2%} · tetto in config {cap:.1%} = {cap * s0['tot']:,.1f} USDE") cusc, come = C.cuscino_richiesto_usd(s0["tot"]) print(f" piano : BUY a passi {'→'.join(f'{x:.0f}' for x in SCALA)} fino al rifiuto (max +{max_add:.0f} USDE;" f" quota ≤{QUOTA_HARD:.0%} e USDC ≥ cuscino ${cusc:,.2f} = {come}), conferma SELL 1 → BUY 1 ok → BUY 1 rifiutato") if not esegui: print(" → dry-run. Rilancia con --esegui."); return 0 eventi: list[dict] = [] def fai(side: str, amt: float) -> bool: px = prezzo_ordine(side) if px is None or not (PX_MIN <= px <= PX_MAX): print(f" ✗ prezzo d'ordine {px} non leggibile/fuori banda — mi fermo"); raise SystemExit(3) ok, nota = ordine(c, side, amt, px) time.sleep(3.0) s = stato(c) ev = {"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "side": side, "amount": amt, "px": px, "ok": ok, "nota": nota, "usde": s["usde"], "tot": s["tot"], "quota": s["quota"]} eventi.append(ev) print(f" · {side.upper():4} {amt:>3.0f} @ {px:.4f} -> {'OK ' if ok else 'RIFIUTATO'} " f"USDE {s['usde']:,.4f} eq ${s['tot']:,.2f} quota {s['quota']:.3%} [{nota}]") return ok # --- salita a passi piccoli ------------------------------------------------------------- i_step, aggiunti, muro, limite = 0, 0.0, None, None while True: step = SCALA[i_step] s = stato(c) cusc, _ = C.cuscino_richiesto_usd(s["tot"]) if aggiunti + step > max_add: limite = f"+{aggiunti:.0f} USDE raggiunti (max-add {max_add:.0f})"; break if (s["val"] + step * s["px"]) / s["tot"] > QUOTA_HARD: limite = f"quota {s['quota']:.2%} + passo supererebbe {QUOTA_HARD:.0%}"; break if s["usdc"] - step * s["px"] < cusc: limite = f"USDC ${s['usdc']:,.2f} − passo scenderebbe sotto il cuscino ${cusc:,.2f}"; break if fai("buy", step): aggiunti += step; time.sleep(PAUSA); continue time.sleep(PAUSA_RIFIUTO) # rifiuto: rileggo il book e riprovo UGUALE if fai("buy", step): aggiunti += step; time.sleep(PAUSA); continue if i_step < len(SCALA) - 1: i_step += 1; time.sleep(PAUSA); continue # doppio rifiuto: passo piu' fine muro = stato(c); break if limite: s = stato(c) print(f" ✗ limite della sonda: {limite} — tetto del venue NON trovato entro il limite;" f" quota {s['quota']:.2%}, USDE {s['usde']:,.4f}") if muro is None: s = stato(c) esito = {"verdetto": "NON_TROVATO", "limite": limite, "usde": s["usde"], "equity_tot": s["tot"], "quota": s["quota"], "aggiunti": aggiunti, "eventi": eventi} else: # --- conferma a saldo neutro --------------------------------------------------------- time.sleep(PAUSA) ok_sell = fai("sell", 1); time.sleep(PAUSA) ok_buy1 = fai("buy", 1) if ok_sell else False; time.sleep(PAUSA) ok_buy2 = fai("buy", 1) if ok_buy1 else None; time.sleep(3.0) fin = stato(c) confermato = bool(ok_sell and ok_buy1 and ok_buy2 is False) L = fin["usde"] lo, hi = L / fin["tot"], (L + 1) / fin["tot"] esito = {"verdetto": "CONFERMATO" if confermato else "NON_CONFERMATO", "bracket_usde": [L, L + 1], "equity_tot": fin["tot"], "frazione": [lo, hi], "aggiunti": aggiunti, "eventi": eventi} print() print(f" TETTO : [{L:,.4f} · {L + 1:,.4f}) USDE con equity ${fin['tot']:,.2f}" f" ⇒ frazione {lo:.2%} – {hi:.2%} ({esito['verdetto']}: " f"sell1 {ok_sell} / buy1 {ok_buy1} / buy1 {ok_buy2})") print(f" config : venue_cap_frac {cap:.3f} → bordo basso misurato oggi {lo:.4f}" + (" (config sotto il misurato: conservativa, ok)" if cap <= lo + 1e-9 else " ⚠️ config SOPRA il misurato")) print(f" conto : USDC ${fin['usdc']:,.2f} + USDE {fin['usde']:,.4f} = ${fin['tot']:,.2f} quota {fin['quota']:.2%}") with LOG.open("a") as f: f.write(json.dumps({"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "sonda_tetto": esito}) + "\n") print(f" registrato : {LOG.relative_to(ROOT)}") return 0 if esito["verdetto"] == "CONFERMATO" else 1 if __name__ == "__main__": raise SystemExit(main())