Files
signal-platform/app/schemas/admin.py
T
dennisthiessenandClaude Opus 5 083c9dbf7c refactor(jobs): derive job topology from one catalog, make next-run coherent
Groundwork for the Admin -> Jobs cleanup. Three sources of truth collapse into
app/job_catalog.py, which imports nothing from app so both the scheduler and
admin_service can import it at module level (admin_service otherwise has to
import the scheduler inside functions to dodge a cycle).

PIPELINE_MEMBERS is now DERIVED from the four pipeline step lists instead of
being a literal set in admin_service duplicating four lists in scheduler.py with
nothing asserting they agreed. A test pins that the derivation reproduces the
previous hand-maintained 9 names exactly, so this is behaviour-preserving.

Deletes the private _JOB_NAMES list, which held 16 of the 19 jobs:
benchmark_collector, outcome_evaluator and shadow_book had no runtime row, and
so no "last run" line in the panel, until their first run in a given process.
_job_runtime is now seeded from the catalog, and a test pins the invariant.

Next-run is decided by category rather than by reading a timestamp. A pipeline
step has no schedule of its own, so it reports its parent's ("next via Morning
Pipeline in 3h") instead of nothing; a manual job says manual_only rather than
rendering a date. This also fixes a real bug: triggering a paused job set
next_run_time=now, APScheduler re-armed the 520-week backstop behind it, and the
panel displayed "next run in ~87600h". Two independent guards -- the category
rule, plus _visible_next_run dropping anything past a year -- and an APScheduler
listener that re-pauses steps and manual jobs once their run finishes. The
listener is registered at module level because configure_scheduler is called
more than once and add_listener does not deduplicate.

Migrates backtest and ticker_universe_sync from interval to cron (Sun 03:00 ET
and 01:00 ET). configure_scheduler calls remove_all_jobs() on every startup, so
an interval countdown restarts each deploy -- a 168h backtest needed a week of
uninterrupted uptime to fire even once. The codebase already documented this
pitfall as the reason cron was adopted; these two were never migrated. Both are
now editable in Admin -> Schedule.

Also: list_jobs went from one settings query per job (19) to one for all of
them, and data_backfill is hidden from the listing while staying registered and
API-triggerable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:08:01 +02:00

134 lines
5.2 KiB
Python

"""Admin request/response schemas."""
from typing import Literal
from pydantic import BaseModel, Field
class UserManagement(BaseModel):
"""Schema for user access management."""
has_access: bool
class PasswordReset(BaseModel):
"""Schema for resetting a user's password."""
new_password: str = Field(..., min_length=6)
class CreateUserRequest(BaseModel):
"""Schema for admin-created user accounts."""
username: str = Field(..., min_length=1)
password: str = Field(..., min_length=6)
role: str = Field(default="user", pattern=r"^(user|admin)$")
has_access: bool = False
class RegistrationToggle(BaseModel):
"""Schema for toggling registration on/off."""
enabled: bool
class SystemSettingUpdate(BaseModel):
"""Schema for updating a system setting."""
value: str = Field(..., min_length=1)
class DataCleanupRequest(BaseModel):
"""Schema for data cleanup — delete records older than N days."""
older_than_days: int = Field(..., gt=0)
class JobToggle(BaseModel):
"""Schema for enabling/disabling a scheduled job."""
enabled: bool
class JobTriggerRequest(BaseModel):
"""Optional parameters for a one-time manual job run."""
target_model: Literal["production_gtl", "structural_sr"] | None = None
cadence: Literal["weekly", "daily"] | None = None
class RecommendationConfigUpdate(BaseModel):
high_confidence_threshold: float | None = Field(default=None, ge=0, le=100)
moderate_confidence_threshold: float | None = Field(default=None, ge=0, le=100)
confidence_diff_threshold: float | None = Field(default=None, ge=0, le=100)
signal_alignment_weight: float | None = Field(default=None, ge=0, le=1)
sr_strength_weight: float | None = Field(default=None, ge=0, le=1)
momentum_technical_divergence_threshold: float | None = Field(default=None, ge=0, le=100)
fundamental_technical_divergence_threshold: float | None = Field(default=None, ge=0, le=100)
class TickerUniverseUpdate(BaseModel):
universe: Literal["sp500", "nasdaq100", "nasdaq_all"]
class ActivationConfigUpdate(BaseModel):
"""Activation gate: what counts as an actionable signal."""
min_momentum_percentile: float | None = Field(default=None, ge=0, le=100)
min_rr: float | None = Field(default=None, ge=0)
min_confidence: float | None = Field(default=None, ge=0, le=100)
require_high_conviction: bool | None = None
exclude_conflicts: bool | None = None
exclude_neutral: bool | None = None
class ScheduleConfigUpdate(BaseModel):
"""Cron schedule for the pipelines + fundamentals. Crons are 5-field
(min hour dom month dow); timezone is an IANA name (e.g. America/New_York)."""
schedule_timezone: str | None = Field(default=None, max_length=64)
schedule_daily_pipeline_cron: str | None = Field(default=None, max_length=120)
schedule_dolt_earnings_cron: str | None = Field(default=None, max_length=120)
schedule_sec_fundamentals_cron: str | None = Field(default=None, max_length=120)
schedule_near_close_pipeline_cron: str | None = Field(default=None, max_length=120)
schedule_after_close_pipeline_cron: str | None = Field(default=None, max_length=120)
schedule_intraday_pipeline_cron: str | None = Field(default=None, max_length=120)
schedule_backtest_cron: str | None = Field(default=None, max_length=120)
schedule_ticker_universe_cron: str | None = Field(default=None, max_length=120)
class PerformanceConfigUpdate(BaseModel):
"""Window for the Performance comparison.
``start_date`` is an ISO date, or empty string to show all history. The
strategy has been revised repeatedly; pinning a start keeps the shadow-vs-
manual comparison inside one configuration instead of averaging across
rules that no longer exist.
"""
start_date: str | None = Field(default=None, max_length=10)
class ShadowBookConfigUpdate(BaseModel):
"""Auto-traded shadow book: the validated strategy with no human input."""
enabled: bool | None = None
capacity: int | None = Field(default=None, ge=1, le=100)
risk_pct: float | None = Field(default=None, gt=0, le=10)
start_equity: float | None = Field(default=None, ge=1000)
class SentimentConfigUpdate(BaseModel):
"""Runtime sentiment LLM config. api_key is write-only; omit/empty to keep
the stored key."""
provider: Literal["openai", "gemini", "deepseek", "xai", "openai_compatible"] | None = None
model: str | None = Field(default=None, max_length=100)
api_key: str | None = Field(default=None, max_length=400)
base_url: str | None = Field(default=None, max_length=300)
class SentimentTestRequest(BaseModel):
ticker: str = Field(default="AAPL", max_length=10)
class AlertConfigUpdate(BaseModel):
"""Telegram alert config. bot_token is write-only; omit/empty to keep the
stored token."""
enabled: bool | None = None
bot_token: str | None = Field(default=None, max_length=200)
telegram_chat_id: str | None = Field(default=None, max_length=64)
qualified_enabled: bool | None = None
sr_proximity_enabled: bool | None = None
score_drop_enabled: bool | None = None
digest_enabled: bool | None = None
regime_quadrant_enabled: bool | None = None
trade_closed_enabled: bool | None = None