Nineteen jobs rendered as one alphabetical list in which a pipeline, one of its
steps, a standalone cron job and a manual-only job were indistinguishable.
Four sections, ordered by the API (category rank, then trading-day order within
it) so the client does not re-derive ordering: Pipelines, Pipeline steps,
Standalone scheduled, Manual only. An unrecognised category still renders, under
"Other" -- a stray section beats a job silently vanishing from the admin page.
Sections rather than nesting steps under their parent, which is what the flat
"runs via pipeline" label invited. Membership is many-to-many -- data_collector
runs in all four pipelines, alerts and outcome_evaluator in two each -- so
nesting means duplicating those rows, and the duplicates would each carry a
Trigger button despite not being distinct actions: only plain collect_ohlcv is
registered, while the near-close and after-close variants are different
coroutines that are not individually triggerable. Instead each pipeline card
lists its step sequence and each step says which pipelines run it, which is the
same information without a button that lies.
Every job now answers "when does this next run" the same way: its own timer, its
soonest enabled parent's ("Next via Intraday Pipeline in 42m"), or "manual
only". Jobs with no recorded run say so explicitly rather than showing nothing.
The status chip and the rate-limit banner still read runtime_* only, so a
persisted failure cannot pin either to a stale state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Job outcomes lived only in scheduler._job_runtime, an in-memory dict. Every
deploy wiped it, so Admin -> Jobs could report "Active" with no indication a job
had ever run or how it ended -- which is the main thing that page is for.
New job_run_state table (migration 031): one row per job, upserted on job_name.
Deliberately not history -- system_events already grows unbounded with no
retention job, and a second append-only operational table would repeat that
debt. Adding history later is purely additive.
Written from two hooks, NOT from _runtime_finish. That looked cheapest (one
function, ~40 call sites) but unit tests invoke job coroutines directly, so it
would fire detached DB writes at the real session factory throughout the suite,
and there is no testing flag to guard on.
- An APScheduler EVENT_JOB_EXECUTED/ERROR listener covers everything the
scheduler fires, including manual triggers. Its detached task is held in a
module-level set (a bare create_task result can be collected mid-flight) and
drained in the app lifespan before engine.dispose().
- _run_pipeline persists directly, and must: pipeline steps are plain
coroutine calls that emit no scheduler events, so the listener cannot see
them. The step persist sits AFTER the except that swallows step errors --
inside it, exactly the failed runs worth seeing would be skipped. The
orchestrator persists in the finally, and the disabled early-return persists
too, or "skipped" is silently dropped.
_persist_job_run never raises: a persistence failure must not break an otherwise
successful pipeline.
The API reports this as last_run_* and leaves runtime_* meaning strictly live
in-memory state. Reusing runtime_status would have been a regression, not a
no-op: JobControls drives the status chip from it (a job that errored eight days
ago would read "Last run error" forever instead of "Active") and picks the
rate-limit banner from it (a week-old rate limit would pin the banner
permanently). Tests pin the split.
The table starts empty; each job fills its row the next time it finishes. No
backfill from system_events, which records only warning/error outcomes under a
different status vocabulary and would invent successes that never happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
The Jobs panel only surfaced live progress; once a job finished you couldn't see
when it ran, whether it succeeded, or its message (e.g. a regime/collector error).
Add a "Last run <ago> · <status> — <message>" line per job, colored by status,
from the runtime_* fields the backend already returns.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add shared UI primitives: Button, Field/Input/Select, PageHeader,
Section, Callout, Tabs, Disclosure
- Replace gradient buttons with single blue-accent btn-primary
- Reserve gradient text for the brand wordmark only
- Rework Scanner page onto the glass system; collapse explainer and
glossary into a disclosure, move filters into a glass toolbar
- Restructure Ticker Detail into tabs (Analysis / Indicators / S/R)
with chart and recommendation always visible
- Align Watchlist, Rankings, Admin, Login/Register to shared primitives
- Unify stray indigo/violet/gray accents into the blue family
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>