Files
signal-platform/tests/unit/test_scheduler.py
T
dennisthiessen e982487abd
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 39s
Deploy / deploy (push) Successful in 25s
coordinate jobs: daily pipeline orchestrator runs the flow in order
Jobs were independent 24h timers with no ordering, so the scanner could run on
stale OHLCV, and manual runs desynced the offsets. New daily_pipeline job runs
the data→signal flow in dependency order: OHLCV → fundamentals → sentiment →
R:R scan → outcome eval (+paper close) → market regime. Each step keeps its own
enable flag and runtime status; a failing step is logged and the pipeline
continues.

The member jobs are registered PAUSED (no auto-fire) so they only run via the
pipeline — but stay manually triggerable from Admin → Jobs (shown as "runs in
daily pipeline"). Alerts (hourly), ticker universe sync, and backtest keep their
own independent cadence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:16:41 +02:00

108 lines
3.4 KiB
Python

"""Unit tests for app.scheduler module."""
import pytest
from app.scheduler import (
_is_job_enabled,
_parse_frequency,
_resume_tickers,
_last_successful,
configure_scheduler,
scheduler,
)
class TestParseFrequency:
def test_hourly(self):
assert _parse_frequency("hourly") == {"hours": 1}
def test_daily(self):
assert _parse_frequency("daily") == {"hours": 24}
def test_case_insensitive(self):
assert _parse_frequency("Hourly") == {"hours": 1}
assert _parse_frequency("DAILY") == {"hours": 24}
def test_unknown_defaults_to_daily(self):
assert _parse_frequency("weekly") == {"hours": 24}
assert _parse_frequency("") == {"hours": 24}
class TestResumeTickers:
def test_no_previous_returns_full_list(self):
symbols = ["AAPL", "GOOG", "MSFT"]
_last_successful["test_job"] = None
result = _resume_tickers(symbols, "test_job")
assert result == ["AAPL", "GOOG", "MSFT"]
def test_resume_after_first(self):
symbols = ["AAPL", "GOOG", "MSFT"]
_last_successful["test_job"] = "AAPL"
result = _resume_tickers(symbols, "test_job")
# Should start from GOOG, then wrap around
assert result == ["GOOG", "MSFT", "AAPL"]
def test_resume_after_middle(self):
symbols = ["AAPL", "GOOG", "MSFT", "TSLA"]
_last_successful["test_job"] = "GOOG"
result = _resume_tickers(symbols, "test_job")
assert result == ["MSFT", "TSLA", "AAPL", "GOOG"]
def test_resume_after_last(self):
symbols = ["AAPL", "GOOG", "MSFT"]
_last_successful["test_job"] = "MSFT"
result = _resume_tickers(symbols, "test_job")
# All already processed, wraps to full list
assert result == ["AAPL", "GOOG", "MSFT"]
def test_unknown_last_returns_full_list(self):
symbols = ["AAPL", "GOOG", "MSFT"]
_last_successful["test_job"] = "NVDA"
result = _resume_tickers(symbols, "test_job")
assert result == ["AAPL", "GOOG", "MSFT"]
def test_empty_list(self):
_last_successful["test_job"] = "AAPL"
result = _resume_tickers([], "test_job")
assert result == []
class TestConfigureScheduler:
def test_configure_adds_all_jobs(self):
# Remove any existing jobs first
scheduler.remove_all_jobs()
configure_scheduler()
jobs = scheduler.get_jobs()
job_ids = {j.id for j in jobs}
assert job_ids == {
"data_collector",
"sentiment_collector",
"fundamental_collector",
"rr_scanner",
"ticker_universe_sync",
"outcome_evaluator",
"alerts",
"market_regime",
"backtest",
"daily_pipeline",
}
def test_configure_is_idempotent(self):
scheduler.remove_all_jobs()
configure_scheduler()
configure_scheduler() # Should replace, not duplicate
job_ids = [j.id for j in scheduler.get_jobs()]
# Each ID should appear exactly once
assert sorted(job_ids) == sorted([
"alerts",
"backtest",
"daily_pipeline",
"data_collector",
"fundamental_collector",
"market_regime",
"outcome_evaluator",
"rr_scanner",
"sentiment_collector",
"ticker_universe_sync",
])