feat(strategy): abbandono gating settimanale — entry daily 24/7
Crypto opera 24/7: la cadenza settimanale lunedì-only era un retaggio TradFi senza giustificazione. La nuova cadenza è giornaliera (cron 0 14 * * *), con i gate quantitativi a decidere se entrare o saltare. Cambiamenti principali: * runtime/orchestrator.py — _CRON_ENTRY 0 14 * * * (era MON) * runtime/auto_pause.py — pause_until(days=) (era weeks=); minimo clamp 1 giorno (era 1 settimana) * core/backtest.py — MondayPick→DailyPick, monday_picks→daily_picks (1 pick per calendar-day all'ora target); Sharpe annualization su ~120 trade/anno (era 52) * config/schema.py — default cron daily; max_concurrent_positions 1→5; AutoPauseConfig.pause_weeks→pause_days, default 14 * runtime/option_chain_snapshot_cycle.py + orchestrator — cron */15 per accumulo continuo dataset di backtest empirico Strategy yamls (config_version 1.3.0 → 1.4.0, hash rigenerati): * strategy.yaml — max_concurrent 1→5, cap_aggregate coerente * strategy.aggressiva.yaml — max_concurrent 2→8, cap_aggregate 3200→6400, max_contracts_per_trade invariato a 16 * strategy.conservativa.yaml — max_concurrent 1→3 * tutti — pause_weeks→pause_days: 14 GUI (pages/7_📚_Strategia.py): * slider Trade/anno: range 20-200 (era 8-30), default 110, help riallineato sulla math 365 candidature × pass-rate 30-40% * card profili: versione letta dinamicamente da config_version invece che hard-coded "v1.2.0" * warning "entrambi perdono soldi" ora valuta i P/L effettivi (cons['annual_pl'], aggr['annual_pl']) invece del win_rate grezzo; aggiunto stato intermedio quando solo conservativo è in perdita Tests (450/450 passati): * test_auto_pause: pause_days, clamp ≥1 giorno * test_backtest: rinomina + ridisegno daily picks (assert su calendar-day dedupe e hour filter) * test_sizing_engine: other_open_positions=5 per cap default * test_config_loader: version 1.4.0 Docs (README + 9 file in docs/) — tutti i riferimenti weekly/lunedì allineati a daily/24-7, volume option_chain ricalcolato per cron */15 (~1.1 MB/giorno, ~400 MB/anno). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,16 +68,16 @@ def test_is_paused_returns_false_when_until_in_past() -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_pause_until_adds_weeks() -> None:
|
||||
until = pause_until(_NOW, weeks=2)
|
||||
assert until == _NOW + timedelta(weeks=2)
|
||||
def test_pause_until_adds_days() -> None:
|
||||
until = pause_until(_NOW, days=14)
|
||||
assert until == _NOW + timedelta(days=14)
|
||||
|
||||
|
||||
def test_pause_until_clamps_to_one_week_minimum() -> None:
|
||||
# weeks <= 0 deve cmq dare almeno 1 settimana di pausa, altrimenti
|
||||
# la cron settimanale potrebbe scattare comunque.
|
||||
assert pause_until(_NOW, weeks=0) == _NOW + timedelta(weeks=1)
|
||||
assert pause_until(_NOW, weeks=-3) == _NOW + timedelta(weeks=1)
|
||||
def test_pause_until_clamps_to_one_day_minimum() -> None:
|
||||
# days <= 0 deve cmq dare almeno 1 giorno di pausa, altrimenti
|
||||
# la cron giornaliera potrebbe scattare comunque.
|
||||
assert pause_until(_NOW, days=0) == _NOW + timedelta(days=1)
|
||||
assert pause_until(_NOW, days=-3) == _NOW + timedelta(days=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -90,7 +90,7 @@ def _cfg(**overrides: object) -> AutoPauseConfig:
|
||||
"enabled": True,
|
||||
"lookback_trades": 5,
|
||||
"max_drawdown_pct": Decimal("0.10"),
|
||||
"pause_weeks": 2,
|
||||
"pause_days": 14,
|
||||
}
|
||||
base.update(overrides)
|
||||
return AutoPauseConfig(**base) # type: ignore[arg-type]
|
||||
|
||||
+28
-28
@@ -11,9 +11,9 @@ from cerbero_bite.config import StrategyConfig, golden_config
|
||||
from cerbero_bite.core.backtest import (
|
||||
bs_put_delta,
|
||||
bs_put_price,
|
||||
daily_picks,
|
||||
estimate_credit_eth,
|
||||
find_strike_for_delta,
|
||||
monday_picks,
|
||||
normal_cdf,
|
||||
run_backtest,
|
||||
simulate_entry_filters,
|
||||
@@ -87,7 +87,7 @@ def test_estimate_credit_returns_positive_credit_in_normal_regime() -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Monday picks + entry filter simulation
|
||||
# Daily picks + entry filter simulation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -114,35 +114,35 @@ def _snap(
|
||||
)
|
||||
|
||||
|
||||
def test_monday_picks_extracts_one_per_iso_week() -> None:
|
||||
monday_2026_05_04 = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
monday_2026_05_11 = datetime(2026, 5, 11, 14, 0, tzinfo=UTC)
|
||||
def test_daily_picks_extracts_one_per_calendar_day() -> None:
|
||||
day1 = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
day2 = datetime(2026, 5, 5, 14, 0, tzinfo=UTC) # Tuesday: PICKED ora (crypto 24/7)
|
||||
snapshots = [
|
||||
_snap(ts=monday_2026_05_04),
|
||||
_snap(ts=monday_2026_05_04 + timedelta(minutes=15)), # not picked
|
||||
_snap(ts=monday_2026_05_11),
|
||||
_snap(ts=day1),
|
||||
_snap(ts=day1 + timedelta(minutes=15)), # stesso giorno, deduplicato
|
||||
_snap(ts=day2),
|
||||
]
|
||||
picks = monday_picks(snapshots)
|
||||
picks = daily_picks(snapshots)
|
||||
assert len(picks) == 2
|
||||
assert picks[0].timestamp == monday_2026_05_04
|
||||
assert picks[1].timestamp == monday_2026_05_11
|
||||
assert picks[0].timestamp == day1
|
||||
assert picks[1].timestamp == day2
|
||||
|
||||
|
||||
def test_monday_picks_skips_other_days_and_hours() -> None:
|
||||
def test_daily_picks_skips_other_hours() -> None:
|
||||
snapshots = [
|
||||
_snap(ts=datetime(2026, 5, 4, 13, 0, tzinfo=UTC)), # Monday 13:00
|
||||
_snap(ts=datetime(2026, 5, 5, 14, 0, tzinfo=UTC)), # Tuesday 14:00
|
||||
_snap(ts=datetime(2026, 5, 4, 13, 0, tzinfo=UTC)), # 13:00 → skipped
|
||||
_snap(ts=datetime(2026, 5, 5, 15, 30, tzinfo=UTC)), # 15:30 → skipped
|
||||
]
|
||||
assert monday_picks(snapshots) == []
|
||||
assert daily_picks(snapshots) == []
|
||||
|
||||
|
||||
def test_monday_picks_filters_by_asset() -> None:
|
||||
monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
def test_daily_picks_filters_by_asset() -> None:
|
||||
day = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
snapshots = [
|
||||
_snap(ts=monday, asset="BTC"),
|
||||
_snap(ts=monday, asset="ETH"),
|
||||
_snap(ts=day, asset="BTC"),
|
||||
_snap(ts=day, asset="ETH"),
|
||||
]
|
||||
picks = monday_picks(snapshots, asset="ETH")
|
||||
picks = daily_picks(snapshots, asset="ETH")
|
||||
assert len(picks) == 1
|
||||
assert picks[0].snapshot.asset == "ETH"
|
||||
|
||||
@@ -156,8 +156,8 @@ def test_simulate_entry_filters_accepts_clean_snapshot(
|
||||
type("MP", (), {"timestamp": monday, "snapshot": snap})() # type: ignore[arg-type]
|
||||
]
|
||||
# Hack: build via real dataclass
|
||||
from cerbero_bite.core.backtest import MondayPick
|
||||
picks = [MondayPick(timestamp=monday, snapshot=snap)]
|
||||
from cerbero_bite.core.backtest import DailyPick
|
||||
picks = [DailyPick(timestamp=monday, snapshot=snap)]
|
||||
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
|
||||
assert len(results) == 1
|
||||
assert results[0].accepted is True
|
||||
@@ -167,8 +167,8 @@ def test_simulate_entry_filters_rejects_dvol_out_of_band() -> None:
|
||||
cfg = golden_config()
|
||||
monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
snap = _snap(ts=monday, dvol=20, funding=0.10) # below 35
|
||||
from cerbero_bite.core.backtest import MondayPick
|
||||
picks = [MondayPick(timestamp=monday, snapshot=snap)]
|
||||
from cerbero_bite.core.backtest import DailyPick
|
||||
picks = [DailyPick(timestamp=monday, snapshot=snap)]
|
||||
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
|
||||
assert results[0].accepted is False
|
||||
assert any("dvol" in r.lower() for r in results[0].reasons)
|
||||
@@ -182,8 +182,8 @@ def test_simulate_entry_filters_skips_incomplete_snapshot() -> None:
|
||||
# dvol=None ⇒ skipped
|
||||
fetch_ok=False,
|
||||
)
|
||||
from cerbero_bite.core.backtest import MondayPick
|
||||
picks = [MondayPick(timestamp=monday, snapshot=incomplete)]
|
||||
from cerbero_bite.core.backtest import DailyPick
|
||||
picks = [DailyPick(timestamp=monday, snapshot=incomplete)]
|
||||
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
|
||||
assert results[0].accepted is False
|
||||
assert results[0].skipped_for_data is True
|
||||
@@ -208,8 +208,8 @@ def _synthetic_year_of_snapshots(
|
||||
base = monday + timedelta(weeks=week)
|
||||
# Lunedì 14:00 è il pick
|
||||
rows.append(_snap(ts=base, spot=spot, dvol=dvol, funding=funding))
|
||||
# Tick intermedi che NON cadono di lunedì alle 14:00:
|
||||
# offset +1h così vengono ignorati da `monday_picks`.
|
||||
# Tick intermedi che NON cadono alle 14:00:
|
||||
# offset +1h (=15:00) così vengono ignorati da `daily_picks`.
|
||||
for d in (2, 8, 14, 19):
|
||||
rows.append(
|
||||
_snap(
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_compute_hash_is_independent_of_recorded_hash_value(tmp_path: Path) -> N
|
||||
def test_load_repo_strategy_yaml(tmp_path: Path) -> None:
|
||||
"""The committed strategy.yaml validates with the recorded hash."""
|
||||
result = load_strategy(REPO_ROOT / "strategy.yaml")
|
||||
assert result.config.config_version == "1.3.0"
|
||||
assert result.config.config_version == "1.4.0"
|
||||
assert result.config.sizing.kelly_fraction == Decimal("0.13")
|
||||
assert result.computed_hash == result.config.config_hash
|
||||
|
||||
|
||||
@@ -144,7 +144,8 @@ def test_aggregate_cap_at_975_reduces_to_one(cfg: StrategyConfig) -> None:
|
||||
|
||||
|
||||
def test_concurrent_positions_at_cap_returns_zero(cfg: StrategyConfig) -> None:
|
||||
res = compute_contracts(_ctx(capital_usd="1500", other_open_positions=1), cfg)
|
||||
# Default cap = 5 (entry daily). other_open_positions=5 ⇒ cap raggiunto.
|
||||
res = compute_contracts(_ctx(capital_usd="1500", other_open_positions=5), cfg)
|
||||
assert res.n_contracts == 0
|
||||
assert res.reason_if_zero is not None
|
||||
assert "position" in res.reason_if_zero.lower()
|
||||
|
||||
Reference in New Issue
Block a user