feat: add standalone AI/Tech regime-change monitor tab
A new /regime tab scoring how far the AI/Tech bull regime has deteriorated toward a re-rating as a single 0-100 index with per-signal breakdown and a 7/30-day trend. Intentionally decoupled: nothing reads its output to gate or score trades — the daily-pipeline membership is scheduling only. - regime_monitor_service: price sub-scores (P1-P6 via Alpaca, like market_regime), VIX + HY credit spreads via a small FRED helper, weighted aggregation over available signals (missing source -> n/a, dropped from the denominator), one snapshot row/day, and a ~90-day history backfill by replaying the already-fetched series as-of each past day. - F1/F3 fundamentals proposed by the configured grounded LLM (reuses sentiment_provider_service config resolution), with a manual override + lock. - regime_snapshots table (migration 011); endpoints on the existing market router; admin-editable weights/threshold; standalone /regime page. Data needs: prices via Alpaca, VIX/credit via FRED (optional key — signals show n/a without it). No LLM needed for history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ from app.services import fundamental_service, ingestion_service, sentiment_servi
|
||||
from app.services.alert_service import dispatch_alerts
|
||||
from app.services.backtest_service import run_and_store as run_backtest_and_store
|
||||
from app.services.market_regime_service import update_market_regime
|
||||
from app.services.regime_monitor_service import update_regime_monitor
|
||||
from app.services.outcome_service import evaluate_pending_setups
|
||||
from app.services.rr_scanner_service import scan_all_tickers
|
||||
from app.services.sentiment_provider_service import build_sentiment_provider
|
||||
@@ -80,6 +81,7 @@ _JOB_NAMES = [
|
||||
"ticker_universe_sync",
|
||||
"alerts",
|
||||
"market_regime",
|
||||
"regime_monitor",
|
||||
"backtest",
|
||||
"daily_pipeline",
|
||||
"intraday_pipeline",
|
||||
@@ -799,6 +801,42 @@ async def compute_market_regime() -> None:
|
||||
_log_event(logging.ERROR, "job_error", job=job_name, error_type=type(exc).__name__, message=str(exc))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job: Regime Monitor
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def compute_regime_monitor() -> None:
|
||||
"""Refresh the standalone AI/Tech regime-change index (observational only).
|
||||
|
||||
Pulls sector/benchmark prices via Alpaca + VIX/credit spreads via FRED,
|
||||
computes the 0-100 index, and persists a daily snapshot. Output feeds nothing
|
||||
else — it only powers its own tab. Pipeline membership is scheduling only.
|
||||
"""
|
||||
job_name = "regime_monitor"
|
||||
_log_event(logging.INFO, "job_start", job=job_name)
|
||||
_runtime_start(job_name, total=1)
|
||||
|
||||
try:
|
||||
async with async_session_factory() as db:
|
||||
if not await _is_job_enabled(db, job_name):
|
||||
_log_event(logging.INFO, "job_skipped", job=job_name, reason="disabled")
|
||||
_runtime_finish(job_name, "skipped", processed=0, total=1, message="Disabled")
|
||||
return
|
||||
|
||||
result = await update_regime_monitor(db)
|
||||
|
||||
_runtime_progress(job_name, processed=1, total=1)
|
||||
_runtime_finish(
|
||||
job_name, "completed", processed=1, total=1,
|
||||
message=f"Index: {result.get('total_score')} ({result.get('band')})",
|
||||
)
|
||||
_log_event(logging.INFO, "job_complete", job=job_name, score=result.get("total_score"))
|
||||
except Exception as exc:
|
||||
_runtime_finish(job_name, "error", processed=0, total=1, message=str(exc))
|
||||
_log_event(logging.ERROR, "job_error", job=job_name, error_type=type(exc).__name__, message=str(exc))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job: Backtest
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -881,6 +919,8 @@ _DAILY_PIPELINE_STEPS = [
|
||||
("rr_scanner", "scan_rr"),
|
||||
("outcome_evaluator", "evaluate_outcomes"),
|
||||
("market_regime", "compute_market_regime"),
|
||||
# Observational only — runs here for scheduling; its output feeds nothing else.
|
||||
("regime_monitor", "compute_regime_monitor"),
|
||||
]
|
||||
|
||||
# Intraday (light): keep prices current and resolve outcomes through the day,
|
||||
@@ -1039,6 +1079,7 @@ def configure_scheduler(schedule_config: dict[str, str] | None = None) -> None:
|
||||
(scan_rr, "rr_scanner", "R:R Scanner"),
|
||||
(evaluate_outcomes, "outcome_evaluator", "Outcome Evaluator"),
|
||||
(compute_market_regime, "market_regime", "Market Regime"),
|
||||
(compute_regime_monitor, "regime_monitor", "Regime Monitor"),
|
||||
]
|
||||
for fn, job_id, job_name in _members:
|
||||
scheduler.add_job(
|
||||
|
||||
Reference in New Issue
Block a user