Phase 1: core algorithms

Implementa i sette algoritmi puri di docs/03-algorithms.md con
disciplina TDD: 112 test, copertura statement+branch al 100% su
core/ e config/, mypy --strict pulito, ruff pulito.

Moduli:
- config/schema.py: StrategyConfig Pydantic v2 con validatori di
  consistenza (kelly, delta, OTM, spread width, profit/stop).
- core/types.py: OptionQuote e OptionLeg condivisi.
- core/entry_validator.py: validate_entry (accumula motivi) e
  compute_bias (bull_put/bear_call/iron_condor/None).
- core/liquidity_gate.py: check OI/volume/spread/depth + slippage
  stimato in % del credito.
- core/sizing_engine.py: Quarter Kelly con cap 200/1000 EUR e
  bande DVOL.
- core/combo_builder.py: select_strikes (DTE/OTM/delta/width/credit)
  e build (ComboProposal con credit/max_loss/breakeven).
- core/greeks_aggregator.py: somma firmata BUY/SELL, theta in USD.
- core/exit_decision.py: 6 trigger ordinati con eccezione skip-time
  vicino a profit (mark in (50%,70%] credito).
- core/kelly_recalibration.py: full/quarter Kelly, confidence per
  sample size, blend medio in fascia 30-99 trade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 10:14:06 +02:00
parent 881bc8a1bf
commit fbb7753cc6
20 changed files with 3090 additions and 1 deletions
+265
View File
@@ -0,0 +1,265 @@
"""TDD for :mod:`cerbero_bite.core.liquidity_gate`.
Spec: ``docs/01-strategy-rules.md §4`` and ``docs/03-algorithms.md §2``.
"""
from __future__ import annotations
from decimal import Decimal
import pytest
from cerbero_bite.config import StrategyConfig, golden_config
from cerbero_bite.core.liquidity_gate import InstrumentSnapshot, check
def _snap(
*,
instrument: str = "ETH-13MAY26-2400-P",
bid: str = "0.090",
ask: str = "0.100",
mid: str = "0.095",
open_interest: int = 500,
volume_24h: int = 80,
book_depth_top3: int = 20,
) -> InstrumentSnapshot:
return InstrumentSnapshot(
instrument=instrument,
bid=Decimal(bid),
ask=Decimal(ask),
mid=Decimal(mid),
open_interest=open_interest,
volume_24h=volume_24h,
book_depth_top3=book_depth_top3,
)
@pytest.fixture
def cfg() -> StrategyConfig:
return golden_config()
# Two-leg vertical: short at 0.095 mid, long at 0.060 mid → credit 0.035 ETH.
# Tight book: per-contract slippage 0.001 ETH → ~2.86% of credit at n=1.
def _good_pair() -> tuple[InstrumentSnapshot, InstrumentSnapshot]:
short = _snap(bid="0.0945", ask="0.0955", mid="0.0950")
long_ = _snap(
instrument="ETH-13MAY26-2300-P",
bid="0.0595",
ask="0.0605",
mid="0.0600",
)
return short, long_
def test_clean_pair_passes(cfg: StrategyConfig) -> None:
# tight book: per-contract slippage 0.001 ETH → 2.86% of 0.035 credit.
short, long_ = _good_pair()
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is True
assert res.reasons == []
def test_short_leg_oi_below_threshold_fails(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
short = _snap(
instrument=short.instrument,
bid=str(short.bid),
ask=str(short.ask),
mid=str(short.mid),
open_interest=99,
)
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
assert any("open interest" in r for r in res.reasons)
def test_long_leg_volume_below_threshold_fails(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
long_ = _snap(
instrument=long_.instrument,
bid=str(long_.bid),
ask=str(long_.ask),
mid=str(long_.mid),
volume_24h=10,
)
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
assert any("volume" in r for r in res.reasons)
def test_spread_pct_above_cap_fails(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
# Make bid-ask huge: bid 0.05, ask 0.20 → spread/mid > 0.15
short = _snap(
instrument=short.instrument,
bid="0.050",
ask="0.200",
mid="0.125",
)
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.060"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
assert any("bid-ask" in r for r in res.reasons)
def test_book_depth_below_threshold_fails(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
short = _snap(
instrument=short.instrument,
bid=str(short.bid),
ask=str(short.ask),
mid=str(short.mid),
book_depth_top3=4,
)
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
assert any("depth" in r for r in res.reasons)
def test_slippage_above_8pct_fails(cfg: StrategyConfig) -> None:
# tight book * n=10 → 0.010 / 0.035 ≈ 28% slippage
short, long_ = _good_pair()
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=10,
cfg=cfg,
)
assert res.accepted is False
assert any("slippage" in r for r in res.reasons)
assert res.estimated_slippage_pct_of_credit > Decimal("0.08")
def test_slippage_well_below_cap_passes(cfg: StrategyConfig) -> None:
# tight book: short ask-mid = 0.0005, long mid-bid = 0.0005 → slip = 0.001 per
# contract, credit 0.035 → 2.85% per contract.
short = _snap(bid="0.0945", ask="0.0955", mid="0.0950")
long_ = _snap(
instrument="ETH-13MAY26-2300-P",
bid="0.0595",
ask="0.0605",
mid="0.0600",
)
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is True
assert res.estimated_slippage_pct_of_credit < Decimal("0.08")
def test_zero_credit_is_rejected(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
assert any("credit" in r for r in res.reasons)
def test_negative_credit_is_rejected(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("-0.001"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
def test_zero_or_negative_n_contracts_is_rejected(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
for n in (0, -1):
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=n,
cfg=cfg,
)
assert res.accepted is False
assert any("contracts" in r for r in res.reasons)
def test_non_positive_mid_is_flagged(cfg: StrategyConfig) -> None:
short, long_ = _good_pair()
bad_short = _snap(
instrument=short.instrument,
bid="0",
ask="0",
mid="0",
open_interest=short.open_interest,
volume_24h=short.volume_24h,
book_depth_top3=short.book_depth_top3,
)
res = check(
short_leg=bad_short,
long_leg=long_,
credit=Decimal("0.035"),
n_contracts=1,
cfg=cfg,
)
assert res.accepted is False
assert any("mid price not positive" in r for r in res.reasons)
def test_failures_accumulate(cfg: StrategyConfig) -> None:
short = _snap(open_interest=10, volume_24h=2, book_depth_top3=1, bid="0.001", ask="0.500")
long_ = _snap(
instrument="ETH-13MAY26-2300-P",
open_interest=5,
volume_24h=1,
book_depth_top3=1,
bid="0.001",
ask="0.500",
)
res = check(
short_leg=short,
long_leg=long_,
credit=Decimal("0.001"),
n_contracts=10,
cfg=cfg,
)
assert res.accepted is False
# at least 4 categories per leg + slippage = many reasons
assert len(res.reasons) >= 5