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
+273
View File
@@ -0,0 +1,273 @@
"""TDD for :mod:`cerbero_bite.core.exit_decision`.
Spec: ``docs/01-strategy-rules.md §7`` and ``docs/03-algorithms.md §6``.
The eight mandatory cases listed in §6 are exercised here, with the
``mark=30% credit`` case re-interpreted to satisfy the time-stop
exception consistently with rule 1 (which always fires earlier when
mark ≤ 50% credit).
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from uuid import uuid4
import pytest
from cerbero_bite.config import SpreadType, StrategyConfig, golden_config
from cerbero_bite.core.exit_decision import PositionSnapshot, evaluate
from cerbero_bite.core.types import OptionLeg
@pytest.fixture
def cfg() -> StrategyConfig:
return golden_config()
def _legs(spread_type: SpreadType = "bull_put", size: int = 1) -> list[OptionLeg]:
expiry = datetime(2026, 5, 15, 8, 0, tzinfo=UTC)
if spread_type == "bull_put":
return [
OptionLeg(
instrument="ETH-X-2475-P",
side="SELL",
strike=Decimal("2475"),
expiry=expiry,
type="P",
size=size,
mid_price_eth=Decimal("0.020"),
delta=Decimal("-0.12"),
gamma=Decimal("0.001"),
theta=Decimal("-0.0005"),
vega=Decimal("0.10"),
),
OptionLeg(
instrument="ETH-X-2350-P",
side="BUY",
strike=Decimal("2350"),
expiry=expiry,
type="P",
size=size,
mid_price_eth=Decimal("0.005"),
delta=Decimal("-0.08"),
gamma=Decimal("0.001"),
theta=Decimal("-0.0003"),
vega=Decimal("0.07"),
),
]
return [
OptionLeg(
instrument="ETH-X-3525-C",
side="SELL",
strike=Decimal("3525"),
expiry=expiry,
type="C",
size=size,
mid_price_eth=Decimal("0.020"),
delta=Decimal("0.12"),
gamma=Decimal("0.001"),
theta=Decimal("-0.0005"),
vega=Decimal("0.10"),
),
OptionLeg(
instrument="ETH-X-3645-C",
side="BUY",
strike=Decimal("3645"),
expiry=expiry,
type="C",
size=size,
mid_price_eth=Decimal("0.005"),
delta=Decimal("0.08"),
gamma=Decimal("0.001"),
theta=Decimal("-0.0003"),
vega=Decimal("0.07"),
),
]
def _snapshot(
*,
spread_type: SpreadType = "bull_put",
credit_received_eth: str = "0.030",
mark_combo_now_eth: str = "0.020",
dvol_at_entry: str = "50",
dvol_now: str = "50",
delta_short_now: str | None = None,
return_4h_now: str = "0",
days_to_expiry: int = 14,
spot_at_entry: str = "3000",
spot_now: str = "3000",
eth_price_usd_now: str = "3000",
) -> PositionSnapshot:
now = datetime(2026, 4, 27, 14, 0, tzinfo=UTC)
expiry = now + timedelta(days=days_to_expiry)
if delta_short_now is None:
delta_short_now = "-0.12" if spread_type == "bull_put" else "0.12"
legs = _legs(spread_type=spread_type)
return PositionSnapshot(
proposal_id=uuid4(),
spread_type=spread_type,
legs=legs,
credit_received_eth=Decimal(credit_received_eth),
credit_received_usd=Decimal(credit_received_eth) * Decimal(eth_price_usd_now),
spot_at_entry=Decimal(spot_at_entry),
dvol_at_entry=Decimal(dvol_at_entry),
expiry=expiry,
opened_at=now - timedelta(days=4),
eth_price_usd_now=Decimal(eth_price_usd_now),
spot_now=Decimal(spot_now),
dvol_now=Decimal(dvol_now),
mark_combo_now_eth=Decimal(mark_combo_now_eth),
delta_short_now=Decimal(delta_short_now),
return_4h_now=Decimal(return_4h_now),
now=now,
)
# ---------------------------------------------------------------------------
# Mandatory cases from docs/03-algorithms.md §6
# ---------------------------------------------------------------------------
def test_mark_at_50pct_credit_triggers_close_profit(cfg: StrategyConfig) -> None:
snap = _snapshot(credit_received_eth="0.030", mark_combo_now_eth="0.015")
res = evaluate(snap, cfg)
assert res.action == "CLOSE_PROFIT"
def test_mark_at_250pct_credit_triggers_close_stop(cfg: StrategyConfig) -> None:
snap = _snapshot(credit_received_eth="0.030", mark_combo_now_eth="0.075")
res = evaluate(snap, cfg)
assert res.action == "CLOSE_STOP"
def test_dvol_increase_above_band_triggers_close_vol(cfg: StrategyConfig) -> None:
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.020", # 67% credit, no profit/stop
dvol_at_entry="50",
dvol_now="62", # +12, above +10 trigger
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_VOL"
def test_six_days_left_mark_above_skip_threshold_triggers_close_time(
cfg: StrategyConfig,
) -> None:
# mark = 80% credit > 70% credit (skip threshold) → CLOSE_TIME
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.024",
days_to_expiry=6,
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_TIME"
def test_six_days_left_mark_in_skip_zone_holds(cfg: StrategyConfig) -> None:
# mark = 60% credit ∈ (50%, 70%] → close to profit, time stop is skipped.
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.018",
days_to_expiry=6,
)
res = evaluate(snap, cfg)
assert res.action == "HOLD"
def test_short_delta_breach_triggers_close_delta(cfg: StrategyConfig) -> None:
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.020",
delta_short_now="-0.32",
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_DELTA"
def test_4h_adverse_move_bull_put_triggers_close_averse(cfg: StrategyConfig) -> None:
snap = _snapshot(
spread_type="bull_put",
credit_received_eth="0.030",
mark_combo_now_eth="0.020",
return_4h_now="-0.07",
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_AVERSE"
def test_4h_adverse_move_bear_call_triggers_close_averse(cfg: StrategyConfig) -> None:
snap = _snapshot(
spread_type="bear_call",
credit_received_eth="0.030",
mark_combo_now_eth="0.020",
return_4h_now="0.07",
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_AVERSE"
def test_neutral_state_returns_hold(cfg: StrategyConfig) -> None:
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.020",
)
res = evaluate(snap, cfg)
assert res.action == "HOLD"
# ---------------------------------------------------------------------------
# Trigger ordering — first match wins
# ---------------------------------------------------------------------------
def test_profit_wins_over_vol_stop(cfg: StrategyConfig) -> None:
# mark = 30% credit AND DVOL +12 → profit takes precedence
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.009",
dvol_at_entry="50",
dvol_now="62",
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_PROFIT"
def test_stop_wins_over_delta_breach(cfg: StrategyConfig) -> None:
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.075", # 250% → stop
delta_short_now="-0.40", # would also be CLOSE_DELTA
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_STOP"
# ---------------------------------------------------------------------------
# PnL reporting
# ---------------------------------------------------------------------------
def test_pnl_in_eth_and_usd_reflect_credit_minus_debit(cfg: StrategyConfig) -> None:
snap = _snapshot(
credit_received_eth="0.030",
mark_combo_now_eth="0.018",
eth_price_usd_now="3100",
)
res = evaluate(snap, cfg)
# pnl_eth = 0.030 - 0.018 = 0.012; pnl_usd = 0.012 × 3100 = 37.2
assert res.pnl_estimate_eth == Decimal("0.012")
assert res.pnl_estimate_usd == Decimal("37.2")
def test_iron_condor_adverse_move_either_direction(cfg: StrategyConfig) -> None:
snap = _snapshot(
spread_type="iron_condor",
credit_received_eth="0.030",
mark_combo_now_eth="0.020",
return_4h_now="-0.06",
)
res = evaluate(snap, cfg)
assert res.action == "CLOSE_AVERSE"