Files
signal-platform/tests/unit/test_scheduler.py
T
dennisthiessen 099846513b
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 39s
Deploy / deploy (push) Successful in 25s
deepen OHLCV history + make the factor-IC pass honest about overlap/regime
Two changes so the cross-sectional signal results can actually be trusted.

(a) History depth — the binding constraint. Ingestion defaulted to 365 days, so
long-lookback factors (12-month momentum, 52-week high) were only computable on a
handful of weeks at the tail, and every IC reflected a single market regime.
- New `settings.ohlcv_history_days` (default 1825 ≈ 5y); new tickers backfill this
  far instead of 1 year.
- New manual "data_backfill" job (Admin → Jobs) re-fetches the full window for
  every ticker, ignoring incremental resume — run once to deepen existing
  1-year histories. Idempotent (upsert); resumes after rate limits.

(b) Factor-IC honesty. The IC was averaged over weekly rebalances whose 30-day
forward windows overlap, inflating the t-stat ~sqrt(6)x.
- IC now measured on NON-OVERLAPPING windows (weeks thinned to ~HORIZON apart).
- Each signal carries a `reliable` flag (>= 12 independent windows); BacktestPanel
  greys out and de-stars thin signals so a lucky 9-week IC of 0.3 can't masquerade
  as an edge.

332 backend tests pass; frontend build clean. No migration (config + job + an
added JSON field on the cached backtest report).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:20:59 +02:00

115 lines
3.6 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_weekly_maps_to_one_week(self):
assert _parse_frequency("weekly") == {"weeks": 1}
def test_unknown_defaults_to_daily(self):
assert _parse_frequency("monthly") == {"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",
"data_backfill",
"sentiment_collector",
"fundamental_collector",
"rr_scanner",
"ticker_universe_sync",
"outcome_evaluator",
"alerts",
"market_regime",
"backtest",
"daily_pipeline",
"intraday_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",
"intraday_pipeline",
"data_collector",
"data_backfill",
"fundamental_collector",
"market_regime",
"outcome_evaluator",
"rr_scanner",
"sentiment_collector",
"ticker_universe_sync",
])