c34f3cb1a4
Diagnosing "no qualified signals for 5 days": setups were generated but none qualified. The gate required BOTH a high min_rr (2.0) AND a high min_target_probability (60), which became contradictory after the Jun-15 probability recalibration — probability already embeds R:R via the 1/(rr+1) ruin term, so high-R:R targets are inherently low-probability and nothing cleared both. Gate is now expected value (R): p*rr - (1-p) from the primary target's probability. R:R and confidence stay as floors; high-conviction / exclude-conflicts / min-target-probability become optional tighteners (default off). Defaults: min_expected_value=0.15, min_rr=1.2, min_confidence=55. EV is only enforced when computable. Migration 009 clears stored activation_* rows so the new defaults apply. Backtest sweeps min_expected_value instead of target probability. Scheduling: pipelines are now cron-configurable in Admin -> Jobs. daily_pipeline (full, default 0 7 * * *) plus a new light intraday_pipeline (OHLCV + outcome eval, default hourly US session) that keeps prices/live-R:R current without setup churn. Fundamentals on its own early weekly cron. Timezone configurable (default Europe/Berlin). Moving interval->CronTrigger also fixes the restart-deferral bug where an interval job's countdown resets on every process restart. 319 backend unit tests pass; frontend tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
2.2 KiB
Python
62 lines
2.2 KiB
Python
"""Unit tests for the cron pipeline schedule config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.exceptions import ValidationError
|
|
from app.scheduler import SCHEDULE_DEFAULTS, validate_cron
|
|
from app.services.admin_service import get_schedule_config, update_schedule_config
|
|
|
|
|
|
@pytest.fixture
|
|
async def session() -> AsyncSession:
|
|
from tests.conftest import _test_session_factory
|
|
|
|
async with _test_session_factory() as session:
|
|
yield session
|
|
|
|
|
|
class TestValidateCron:
|
|
def test_accepts_valid(self):
|
|
validate_cron("0 7 * * *", "Europe/Berlin")
|
|
validate_cron("0 14-22 * * 1-5", "UTC")
|
|
|
|
def test_rejects_bad_cron(self):
|
|
with pytest.raises(Exception):
|
|
validate_cron("not a cron", "UTC")
|
|
|
|
def test_rejects_bad_timezone(self):
|
|
with pytest.raises(Exception):
|
|
validate_cron("0 7 * * *", "Mars/Phobos")
|
|
|
|
|
|
class TestScheduleConfig:
|
|
async def test_defaults_when_unset(self, session: AsyncSession):
|
|
config = await get_schedule_config(session)
|
|
assert config == SCHEDULE_DEFAULTS
|
|
|
|
async def test_update_and_read_back(self, session: AsyncSession):
|
|
updated = await update_schedule_config(
|
|
session, {"schedule_daily_pipeline_cron": "30 6 * * *"}
|
|
)
|
|
assert updated["schedule_daily_pipeline_cron"] == "30 6 * * *"
|
|
# untouched keys keep their defaults
|
|
assert updated["schedule_intraday_pipeline_cron"] == SCHEDULE_DEFAULTS["schedule_intraday_pipeline_cron"]
|
|
|
|
config = await get_schedule_config(session)
|
|
assert config["schedule_daily_pipeline_cron"] == "30 6 * * *"
|
|
|
|
async def test_rejects_bad_cron(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await update_schedule_config(session, {"schedule_fundamentals_cron": "every monday"})
|
|
|
|
async def test_rejects_bad_timezone(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await update_schedule_config(session, {"schedule_timezone": "Nowhere/Void"})
|
|
|
|
async def test_rejects_unknown_key(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await update_schedule_config(session, {"schedule_bogus": "0 0 * * *"})
|