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
+43
View File
@@ -0,0 +1,43 @@
"""Strategy configuration: schema, loader, validation."""
from cerbero_bite.config.schema import (
AssetConfig,
DvolAdjustmentBand,
EntryConfig,
ExecutionConfig,
ExitConfig,
KellyConfig,
LiquidityConfig,
McpConfig,
MonitoringConfig,
ShortStrikeSpec,
SizingConfig,
SpreadType,
SpreadWidthSpec,
StorageConfig,
StrategyConfig,
StructureConfig,
TelegramConfig,
golden_config,
)
__all__ = [
"AssetConfig",
"DvolAdjustmentBand",
"EntryConfig",
"ExecutionConfig",
"ExitConfig",
"KellyConfig",
"LiquidityConfig",
"McpConfig",
"MonitoringConfig",
"ShortStrikeSpec",
"SizingConfig",
"SpreadType",
"SpreadWidthSpec",
"StorageConfig",
"StrategyConfig",
"StructureConfig",
"TelegramConfig",
"golden_config",
]
+298
View File
@@ -0,0 +1,298 @@
"""Pydantic schema for ``strategy.yaml``.
The configuration is the single source of truth for every threshold used
by the :mod:`cerbero_bite.core` algorithms. The schema is intentionally
verbose: every numeric threshold from
``docs/10-config-spec.md`` is exposed as a typed field so that callers
can refer to the same name they would write in YAML.
All monetary and price-related thresholds use :class:`decimal.Decimal`
to preserve the precision conventions documented in
``docs/03-algorithms.md §0.2`` (no ``float`` for money or option prices).
Only the fields required by Phase 1 (core algorithms) are populated with
defaults that match the golden config; fields belonging to runtime,
storage, MCP, etc. are typed as ``dict[str, Any]`` or made optional so
that the schema can absorb the full ``strategy.yaml`` without forcing
non-core sections to be implemented before their owning module is
written.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
# ---------------------------------------------------------------------------
# Asset
# ---------------------------------------------------------------------------
class AssetConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
symbol: str = "ETH"
exchange: str = "deribit"
# ---------------------------------------------------------------------------
# Entry
# ---------------------------------------------------------------------------
class EntryConfig(BaseModel):
"""Filters and bias thresholds documented in ``01-strategy-rules.md``."""
model_config = ConfigDict(frozen=True, extra="forbid")
cron: str = "0 14 * * MON"
skip_holidays_country: str = "IT"
# access filters (§2)
capital_min_usd: Decimal = Field(default=Decimal("720"))
dvol_min: Decimal = Field(default=Decimal("35"))
dvol_max: Decimal = Field(default=Decimal("90"))
funding_perp_abs_max_annualized: Decimal = Field(default=Decimal("0.80"))
eth_holdings_pct_max: Decimal = Field(default=Decimal("0.30"))
no_position_concurrent: bool = True
exclude_macro_severity: list[str] = Field(default_factory=lambda: ["high"])
exclude_macro_countries: list[str] = Field(default_factory=lambda: ["US", "EU"])
# directional bias (§3.1)
trend_window_days: int = 30
trend_bull_threshold_pct: Decimal = Field(default=Decimal("0.05"))
trend_bear_threshold_pct: Decimal = Field(default=Decimal("-0.05"))
funding_bull_threshold_annualized: Decimal = Field(default=Decimal("0.20"))
funding_bear_threshold_annualized: Decimal = Field(default=Decimal("-0.20"))
iron_condor_dvol_min: Decimal = Field(default=Decimal("55"))
iron_condor_adx_max: Decimal = Field(default=Decimal("20"))
iron_condor_trend_neutral_band_pct: Decimal = Field(default=Decimal("0.05"))
# ---------------------------------------------------------------------------
# Structure
# ---------------------------------------------------------------------------
class ShortStrikeSpec(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
delta_target: Decimal = Field(default=Decimal("0.12"))
delta_min: Decimal = Field(default=Decimal("0.10"))
delta_max: Decimal = Field(default=Decimal("0.15"))
distance_otm_pct_min: Decimal = Field(default=Decimal("0.15"))
distance_otm_pct_max: Decimal = Field(default=Decimal("0.25"))
class SpreadWidthSpec(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
target_pct_of_spot: Decimal = Field(default=Decimal("0.04"))
min_pct_of_spot: Decimal = Field(default=Decimal("0.03"))
max_pct_of_spot: Decimal = Field(default=Decimal("0.05"))
class StructureConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
dte_target: int = 18
dte_min: int = 14
dte_max: int = 21
short_strike: ShortStrikeSpec = Field(default_factory=ShortStrikeSpec)
spread_width: SpreadWidthSpec = Field(default_factory=SpreadWidthSpec)
credit_to_width_ratio_min: Decimal = Field(default=Decimal("0.30"))
# ---------------------------------------------------------------------------
# Liquidity
# ---------------------------------------------------------------------------
class LiquidityConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
open_interest_min: int = 100
volume_24h_min: int = 20
bid_ask_spread_pct_max: Decimal = Field(default=Decimal("0.15"))
book_depth_top3_min: int = 5
slippage_pct_of_credit_max: Decimal = Field(default=Decimal("0.08"))
# ---------------------------------------------------------------------------
# Sizing
# ---------------------------------------------------------------------------
class DvolAdjustmentBand(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
dvol_under: Decimal
multiplier: Decimal
def _default_dvol_bands() -> list[DvolAdjustmentBand]:
return [
DvolAdjustmentBand(dvol_under=Decimal("45"), multiplier=Decimal("1.00")),
DvolAdjustmentBand(dvol_under=Decimal("60"), multiplier=Decimal("0.85")),
DvolAdjustmentBand(dvol_under=Decimal("80"), multiplier=Decimal("0.65")),
]
class SizingConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
kelly_fraction: Decimal = Field(default=Decimal("0.13"))
cap_per_trade_eur: Decimal = Field(default=Decimal("200"))
cap_aggregate_open_eur: Decimal = Field(default=Decimal("1000"))
max_concurrent_positions: int = 1
max_contracts_per_trade: int = 4
dvol_adjustment: list[DvolAdjustmentBand] = Field(default_factory=_default_dvol_bands)
dvol_no_entry_threshold: Decimal = Field(default=Decimal("80"))
# ---------------------------------------------------------------------------
# Exit
# ---------------------------------------------------------------------------
class ExitConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
profit_take_pct_of_credit: Decimal = Field(default=Decimal("0.50"))
stop_loss_mark_x_credit: Decimal = Field(default=Decimal("2.50"))
vol_stop_dvol_increase: Decimal = Field(default=Decimal("10"))
time_stop_dte_remaining: int = 7
time_stop_skip_if_close_to_profit_pct: Decimal = Field(default=Decimal("0.70"))
delta_breach_threshold: Decimal = Field(default=Decimal("0.30"))
adverse_move_4h_pct: Decimal = Field(default=Decimal("0.05"))
monitor_cron: str = "0 2,14 * * *"
user_confirmation_timeout_min: int = 30
escalate_on_timeout: list[str] = Field(
default_factory=lambda: ["CLOSE_STOP", "CLOSE_VOL", "CLOSE_DELTA"]
)
# ---------------------------------------------------------------------------
# Kelly recalibration
# ---------------------------------------------------------------------------
class KellyConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
lookback_days: int = 365
min_sample_low_confidence: int = 30
min_sample_high_confidence: int = 100
weight_when_medium_confidence: Decimal = Field(default=Decimal("0.50"))
# ---------------------------------------------------------------------------
# Sections that core/ algorithms do not consume — kept loose for now
# ---------------------------------------------------------------------------
class _LooseSection(BaseModel):
"""Catch-all section used for parts of strategy.yaml not yet typed."""
model_config = ConfigDict(frozen=True, extra="allow")
class ExecutionConfig(_LooseSection): ...
class MonitoringConfig(_LooseSection): ...
class StorageConfig(_LooseSection): ...
class McpConfig(_LooseSection): ...
class TelegramConfig(_LooseSection): ...
# ---------------------------------------------------------------------------
# Root
# ---------------------------------------------------------------------------
class StrategyConfig(BaseModel):
"""Validated representation of ``strategy.yaml``."""
model_config = ConfigDict(frozen=True, extra="forbid")
config_version: str
config_hash: str
last_review: str
last_reviewer: str
asset: AssetConfig = Field(default_factory=AssetConfig)
entry: EntryConfig = Field(default_factory=EntryConfig)
structure: StructureConfig = Field(default_factory=StructureConfig)
liquidity: LiquidityConfig = Field(default_factory=LiquidityConfig)
sizing: SizingConfig = Field(default_factory=SizingConfig)
exit: ExitConfig = Field(default_factory=ExitConfig)
kelly_recalibration: KellyConfig = Field(default_factory=KellyConfig)
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
monitoring: MonitoringConfig = Field(default_factory=MonitoringConfig)
storage: StorageConfig = Field(default_factory=StorageConfig)
mcp: McpConfig = Field(default_factory=McpConfig)
telegram: TelegramConfig = Field(default_factory=TelegramConfig)
@model_validator(mode="after")
def _validate_consistency(self) -> StrategyConfig:
s = self.structure.short_strike
if not (s.delta_min <= s.delta_target <= s.delta_max):
raise ValueError("delta_target outside [delta_min, delta_max]")
if s.distance_otm_pct_min >= s.distance_otm_pct_max:
raise ValueError("OTM range invalid")
sw = self.structure.spread_width
if not (sw.min_pct_of_spot <= sw.target_pct_of_spot <= sw.max_pct_of_spot):
raise ValueError("spread_width target outside min/max")
if not (Decimal("0") < self.sizing.kelly_fraction < Decimal("0.5")):
raise ValueError("kelly_fraction out of safe range (0, 0.5)")
if self.exit.profit_take_pct_of_credit >= Decimal("1"):
raise ValueError("profit_take >= 100% impossible")
if self.exit.stop_loss_mark_x_credit <= Decimal("1"):
raise ValueError("stop_loss multiple <= 1× makes no sense")
if self.entry.dvol_min >= self.entry.dvol_max:
raise ValueError("dvol_min must be < dvol_max")
return self
# ---------------------------------------------------------------------------
# Convenience
# ---------------------------------------------------------------------------
SpreadType = Literal["bull_put", "bear_call", "iron_condor"]
def golden_config(**overrides: Any) -> StrategyConfig:
"""Return a :class:`StrategyConfig` populated with the golden defaults.
The caller may pass per-section overrides (``entry={...}``,
``sizing={...}``, …) to mutate specific values without rebuilding the
whole tree. Useful in tests.
"""
base: dict[str, Any] = {
"config_version": "1.0.0-test",
"config_hash": "0" * 64,
"last_review": "2026-04-26",
"last_reviewer": "test",
}
base.update(overrides)
return StrategyConfig(**base)