"""book.leva_tetto — la leva lorda massima del libro, in UNA sola formula (#28). Prima della correzione la leva lorda tornava con tre formule diverse: `usde.frazione_cuscino` (N_ASSET fisso, senza scala), la regola `leva` del giornale (senza scala) e `journal.movimenti_capitale` (con scala, ma ridichiarata). A scala 1,25 il cuscino USDC risultava 0,30 dell'equity invece di 0,375 — il caso che T2 riproduce. """ from __future__ import annotations import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src.live import book as B # noqa: E402 from src.live import journal as J # noqa: E402 from src.live import tradesdb as T # noqa: E402 from src.live import usde as U # noqa: E402 def _cfg(**kw) -> dict: base = dict(max_notional_per_asset_frac=0.5, disaster_sl_pct=0.3) base.update(kw) return base # ---------------------------------------------------------------- T1: scala assente def test_T1_scala_assente_e_frac_uguale_al_weight(): leva, come = B.leva_tetto(_cfg()) assert leva == pytest.approx(len(B.ASSETS) * B.WEIGHT) assert "scala 1.0" in come # ---------------------------------------------------------------- T2: il caso dell'issue def test_T2_scala_1_25_alza_la_leva_e_il_cuscino_usde(tmp_path, monkeypatch): """A scala 1,25 il cuscino USDC deve valere 0,375 dell'equity, non 0,30 (#28).""" cfg = _cfg(book_scale_k=1.25) leva, _ = B.leva_tetto(cfg) assert leva == pytest.approx(1.25) # frazione_cuscino legge da CFG_PATH: si monkeypatcha il file, come fanno gli altri test # su usde/_scala (grep "CFG_PATH\|CONFIG" tests/). cpath = tmp_path / "live.json" cpath.write_text(json.dumps(cfg)) monkeypatch.setattr(U, "CFG_PATH", cpath) f, come = U.frazione_cuscino() assert f == pytest.approx(0.375) assert "SL 30%" in come # ---------------------------------------------------------------- T3: fuori scaletta non solleva def test_T3_scala_fuori_scaletta_non_solleva_e_torna_il_tetto_di_codice(): """cuscino_watch non deve morire proprio quando la config e' sbagliata (verifica fable B5).""" leva, come = B.leva_tetto(_cfg(book_scale_k=1.1)) assert leva == pytest.approx(B.LEVA_LORDA_MAX) assert "tetto di codice" in come # ---------------------------------------------------------------- T4: frac assente def test_T4_frac_assente_usa_weight(): cfg = dict(disaster_sl_pct=0.3) # niente max_notional_per_asset_frac leva, come = B.leva_tetto(cfg) assert leva == pytest.approx(len(B.ASSETS) * B.WEIGHT) assert f"frac {B.WEIGHT}" in come # ---------------------------------------------------------------- T5: il tronco al tetto di codice def test_T5_leva_oltre_il_tetto_di_codice_viene_troncata(monkeypatch): """Verifica diretta del troncamento. Con WEIGHT alzato il prodotto supera LEVA_LORDA_MAX pur restando `_scala` silenziosa (frac ASSENTE dalla config -> il suo controllo sul prodotto non scatta, perche' guarda solo `max_notional_per_asset_frac` quando e' dichiarato).""" monkeypatch.setattr(B, "WEIGHT", 0.9) leva, come = B.leva_tetto(dict(book_scale_k=1.25)) # frac assente -> usa WEIGHT = 0.9 assert leva == pytest.approx(B.LEVA_LORDA_MAX) assert "troncato" in come # ---------------------------------------------------------------- T6: il giornale usa la stessa leva_tetto @pytest.fixture() def db(tmp_path): con = T.connect(tmp_path / "t.db") yield con con.close() def _voce(mercato=None, libro=None, pnl=None, salute=None, giorno="2026-08-21"): return dict(giorno=giorno, ts_scritto=T.ora(), mercato=mercato or {}, libro=libro or {"errore": "nessun giro", "giri": 0}, pnl=pnl or {}, salute=salute or dict(giri_book=24, feed_skh_min=0, atteso_giri=24, giorno_in_corso=False, giri_mancanti=0)) def _libro(**asset): a = {k: dict(dict(tp_frac=0.0, skh_sign=0, skh_entry=None, target=0.0, posizione=0.0, azione="HOLD (a target)"), **v) for k, v in asset.items()} lordo = sum(abs(v["posizione"]) for v in a.values()) return dict(giri=24, ultimo_giro="2026-08-21T23:07:01+00:00", equity=600.0, barra="2026-08-21", feed_skh_min=0, nozionale_lordo=lordo, leva_lorda=lordo / 600.0, asset=a) def _pnl(**kw): base = dict(equity_inizio=600.0, equity_fine=600.0, delta_equity=0.0, realizzato_lordo=0.0, realizzato_netto=0.0, roundtrip_chiusi=0, fill=0, fee=0.0, cumulato_da_arming=0.0, letture_equity=24) return {**base, **kw} def test_T6_regola_leva_margine_positivo_a_scala_1_25(db, monkeypatch, tmp_path): """lv=1.2 su un tetto di 1.25x (frac 0.5, scala 1.25): margine positivo, non negativo.""" (tmp_path / "config").mkdir() (tmp_path / "config" / "live.json").write_text( json.dumps(dict(max_notional_per_asset_frac=0.5, book_scale_k=1.25))) monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path) lb = _libro(BTC=dict(posizione=360.0), ETH=dict(posizione=360.0)) # 720/600 = 1.2x assert lb["leva_lorda"] == pytest.approx(1.2) r = J.lettura(db, _voce(libro=lb, pnl=_pnl())) riga = next(x for x in r if x["id"] == "leva") assert "1.25x" in riga["testo"] and "margine 0.05x" in riga["testo"] # ---------------------------------------------------------------- A7 (#35): min_order_usd da config def test_A7_build_book_order_rispetta_min_usd_di_config(): """book_report legge min_order_usd da config (default 5.0 se assente, come book_execute.py). Qui si verifica direttamente build_book_order, che e' il punto dove il valore viene usato: con min_usd=20 un delta di $10 non genera ordine, con min_usd=5 (il default di oggi) si.""" order_stretto = B.build_book_order("BTC_USDC-PERPETUAL", 100.0, 90.0, 50000.0, min_usd=20.0) assert order_stretto is None # |delta|=10 < 20: sotto soglia col tetto alzato order_default = B.build_book_order("BTC_USDC-PERPETUAL", 100.0, 90.0, 50000.0, min_usd=5.0) assert order_default is not None # |delta|=10 >= 5: lo stesso caso, oggi, genera ordine