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:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user