live/execution: tetto di apertura per strumento da config, non solo MAX_AMOUNT fisso (#18)
DeribitTrader.set_open_cap(instrument, usd_cap, mark) deriva amount_max = 1,10*usd_cap/mark dal cap di config del libro; _submit lo usa quando presente, MAX_AMOUNT resta il DEFAULT per chi non lo chiama (il micro-test). Firma per chi la richiama prima di rebalance_signed (U2): trader.set_open_cap(inst, r["cap_per_asset"] * r["scala"], mark). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+31
-10
@@ -4,12 +4,17 @@ Estende DeribitRead (sola lettura) coi metodi di trading, con la logica PROVATA
|
|||||||
(Old/src/live/execution.py): entrata market verificata (state=='filled' + trade riscontrati, fill/fee
|
(Old/src/live/execution.py): entrata market verificata (state=='filled' + trade riscontrati, fill/fee
|
||||||
reali, filled_amount autorevole), uscita market reduce_only, disaster-bracket STOP_MARKET reduce_only.
|
reali, filled_amount autorevole), uscita market reduce_only, disaster-bracket STOP_MARKET reduce_only.
|
||||||
|
|
||||||
GUARDRAIL: solo strumenti in ALLOWED; cap di size SOLO sulle APERTURE (MAX_AMOUNT). Le CHIUSURE si
|
GUARDRAIL: solo strumenti in ALLOWED; cap di size SOLO sulle APERTURE — `set_open_cap()` lo deriva
|
||||||
tentano SEMPRE senza cap (principio di sicurezza di Old: si deve poter uscire da qualunque posizione).
|
per-strumento dal cap di config del libro (config/live.json, #18); MAX_AMOUNT resta il tetto di
|
||||||
Nessun parametro di leva (Deribit non la accetta per-ordine: l'esposizione la decide la SIZE).
|
DEFAULT per chi non lo chiama (il micro-test, size fissa minima). Le CHIUSURE si tentano SEMPRE
|
||||||
|
senza cap (principio di sicurezza di Old: si deve poter uscire da qualunque posizione). Nessun
|
||||||
|
parametro di leva (Deribit non la accetta per-ordine: l'esposizione la decide la SIZE).
|
||||||
|
|
||||||
⚠️ INVIA ORDINI REALI CON SOLDI VERI. Finestra d'uso attuale: micro-test (scripts/live/microtest.py).
|
⚠️ INVIA ORDINI REALI CON SOLDI VERI. Il libro (TP01+SKH01) e' ARMATO e LIVE su Deribit mainnet dal
|
||||||
Il deploy pieno di TP01 resta gated finche' il percorso live non e' abilitato esplicitamente.
|
2026-06-23 (commit db738bc): questo NON e' un percorso di micro-test, e' l'esecuzione di produzione
|
||||||
|
usata da `scripts/live/book_execute.py` (cron del book). Il micro-test
|
||||||
|
(scripts/live/microtest.py) resta un percorso a parte, sotto lo stesso guardrail, col cap di
|
||||||
|
DEFAULT (MAX_AMOUNT).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -17,11 +22,13 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from src.live.deribit import DISASTER_LABEL, DeribitRead, disaster_stop_price, notional_to_amount, quantize_price
|
from src.live.deribit import DISASTER_LABEL, DeribitRead, disaster_stop_price, notional_to_amount, quantize_price
|
||||||
|
|
||||||
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger
|
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger di
|
||||||
# (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello
|
# DEFAULT (~$630/$430 su un conto ~$600), usato SOLO quando nessuno ha chiamato set_open_cap() (il
|
||||||
# lo decide config/live.json max_notional_per_asset_usd). Il micro-test invia comunque size fissa minima.
|
# micro-test, che invia comunque size fissa minima). Sul libro il tetto per apertura lo deriva
|
||||||
|
# set_open_cap() dal cap di config (config/live.json max_notional_per_asset_usd, #18): backstop
|
||||||
|
# sopra il sizing di TP01/book, non il sizing operativo.
|
||||||
ALLOWED = {"BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"}
|
ALLOWED = {"BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"}
|
||||||
MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25}
|
MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25} # DEFAULT — vedi set_open_cap()
|
||||||
FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat
|
FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +62,20 @@ def _avg_price(order: dict, trades: list[dict]) -> float | None:
|
|||||||
class DeribitTrader(DeribitRead):
|
class DeribitTrader(DeribitRead):
|
||||||
"""Trading minimo e verificato. Apre/chiude solo entro i guardrail; le chiusure sempre."""
|
"""Trading minimo e verificato. Apre/chiude solo entro i guardrail; le chiusure sempre."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._open_cap: dict[str, float] = {} # per-strumento, da set_open_cap() (#18)
|
||||||
|
|
||||||
|
def set_open_cap(self, instrument: str, usd_cap: float, mark: float) -> None:
|
||||||
|
"""Tetto di apertura per `instrument`, derivato dal cap di config del libro (#18):
|
||||||
|
amount_max = 1,10 * usd_cap / mark — 10% sopra il cap nozionale per non rifiutare
|
||||||
|
un'apertura legittima per un arrotondamento di sizing/mark. Backstop anti-fat-finger
|
||||||
|
sopra il sizing di TP01/book, non il sizing operativo (quello resta config/live.json).
|
||||||
|
Senza chiamata, `_submit` resta sul DEFAULT MAX_AMOUNT (il micro-test)."""
|
||||||
|
if usd_cap <= 0 or mark <= 0:
|
||||||
|
raise ValueError(f"set_open_cap richiede usd_cap>0 e mark>0 (usd_cap={usd_cap}, mark={mark})")
|
||||||
|
self._open_cap[instrument] = 1.10 * usd_cap / mark
|
||||||
|
|
||||||
def _submit(self, instrument: str, side: str, amount: float, *, reduce_only: bool,
|
def _submit(self, instrument: str, side: str, amount: float, *, reduce_only: bool,
|
||||||
label: str, order_type: str = "market", price: float | None = None) -> Fill:
|
label: str, order_type: str = "market", price: float | None = None) -> Fill:
|
||||||
if instrument not in ALLOWED:
|
if instrument not in ALLOWED:
|
||||||
@@ -62,7 +83,7 @@ class DeribitTrader(DeribitRead):
|
|||||||
if side not in ("buy", "sell"):
|
if side not in ("buy", "sell"):
|
||||||
raise GuardrailError(f"side non valido: {side}")
|
raise GuardrailError(f"side non valido: {side}")
|
||||||
if not reduce_only: # cap SOLO sulle aperture; le chiusure si tentano sempre
|
if not reduce_only: # cap SOLO sulle aperture; le chiusure si tentano sempre
|
||||||
cap = MAX_AMOUNT.get(instrument, 0.0)
|
cap = self._open_cap.get(instrument, MAX_AMOUNT[instrument])
|
||||||
if amount <= 0 or amount > cap:
|
if amount <= 0 or amount > cap:
|
||||||
raise GuardrailError(f"size {amount} fuori dal cap apertura (0, {cap}]")
|
raise GuardrailError(f"size {amount} fuori dal cap apertura (0, {cap}]")
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ che dovrebbe farle — non discusse.
|
|||||||
#17. `DeribitRead.position_usd` leggeva `size`/`size_currency` in ASSOLUTO e ignorava
|
#17. `DeribitRead.position_usd` leggeva `size`/`size_currency` in ASSOLUTO e ignorava
|
||||||
`direction`: uno short risultava un long, e close()/ensure_disaster_sl decidevano lato
|
`direction`: uno short risultava un long, e close()/ensure_disaster_sl decidevano lato
|
||||||
ordine e prezzo dello stop dal segno sbagliato.
|
ordine e prezzo dello stop dal segno sbagliato.
|
||||||
|
#18. Tetto per ordine d'apertura: `MAX_AMOUNT` era un tetto FISSO uguale per tutti i conti;
|
||||||
|
ora il libro deriva il proprio tetto dal cap di config via `set_open_cap()`.
|
||||||
|
|
||||||
Nessun test tocca la rete.
|
Nessun test tocca la rete.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from src.live.deribit import DeribitRead
|
from src.live.deribit import DeribitRead
|
||||||
from src.live.execution import DeribitTrader, Fill
|
from src.live.execution import MAX_AMOUNT, DeribitTrader, Fill, GuardrailError
|
||||||
|
|
||||||
ETH = "ETH_USDC-PERPETUAL"
|
ETH = "ETH_USDC-PERPETUAL"
|
||||||
|
|
||||||
@@ -106,3 +110,51 @@ def test_ensure_disaster_sl_su_short_mette_lo_stop_sopra_il_mark_lato_buy():
|
|||||||
assert call["side"] == "buy"
|
assert call["side"] == "buy"
|
||||||
assert call["order_type"] == "stop_market"
|
assert call["order_type"] == "stop_market"
|
||||||
assert call["price"] > t._mark # stop SOPRA il mark per uno short
|
assert call["price"] > t._mark # stop SOPRA il mark per uno short
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
# #18 — set_open_cap(): tetto di apertura per strumento, derivato dal cap di config.
|
||||||
|
# =================================================================================================
|
||||||
|
class _FakeTraderCap(DeribitTrader):
|
||||||
|
"""DeribitTrader senza rete: `_post` risponde con un ordine fillato finto, cosi' la parte di
|
||||||
|
_submit DOPO il guardrail (che qui non ci interessa) non tocca la rete."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._open_cap: dict[str, float] = {}
|
||||||
|
|
||||||
|
def _post(self, path, payload):
|
||||||
|
amt = payload["amount"]
|
||||||
|
return {"order": {"order_id": "1", "order_state": "filled", "filled_amount": amt},
|
||||||
|
"trades": [{"price": 1.0, "amount": amt, "fee": 0.0}]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_open_cap_valida_gli_argomenti():
|
||||||
|
t = _FakeTraderCap()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
t.set_open_cap(ETH, 0.0, 2694.0)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
t.set_open_cap(ETH, 2291.5, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_open_cap_apertura_entro_il_tetto_non_solleva():
|
||||||
|
"""Equity 4583, cap/asset 2291.5, ETH mark 2694 -> apertura di 0,2807 ETH e' ben dentro il
|
||||||
|
tetto (1,10 * 2291.5 / 2694 ~= 0,9355 ETH)."""
|
||||||
|
t = _FakeTraderCap()
|
||||||
|
t.set_open_cap(ETH, 2291.5, 2694.0)
|
||||||
|
t._submit(ETH, "buy", 0.2807, reduce_only=False, label="book-open") # non deve solllevare
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_open_cap_3x_il_tetto_solleva_guardrail():
|
||||||
|
t = _FakeTraderCap()
|
||||||
|
t.set_open_cap(ETH, 2291.5, 2694.0)
|
||||||
|
cap = t._open_cap[ETH]
|
||||||
|
with pytest.raises(GuardrailError):
|
||||||
|
t._submit(ETH, "buy", cap * 3, reduce_only=False, label="book-open")
|
||||||
|
|
||||||
|
|
||||||
|
def test_senza_set_open_cap_resta_il_default_max_amount():
|
||||||
|
t = _FakeTraderCap()
|
||||||
|
dentro = MAX_AMOUNT[ETH] * 0.5
|
||||||
|
t._submit(ETH, "buy", dentro, reduce_only=False, label="book-open") # non deve solllevare
|
||||||
|
with pytest.raises(GuardrailError):
|
||||||
|
t._submit(ETH, "buy", MAX_AMOUNT[ETH] * 3, reduce_only=False, label="book-open")
|
||||||
|
|||||||
Reference in New Issue
Block a user