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:
+89
-2
@@ -1,11 +1,13 @@
|
||||
"""Market-level endpoints (benchmark regime)."""
|
||||
"""Market-level endpoints (benchmark regime + AI/Tech regime-change monitor)."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, require_access
|
||||
from app.dependencies import get_db, require_access, require_admin
|
||||
from app.models.user import User
|
||||
from app.schemas.common import APIEnvelope
|
||||
from app.services import regime_monitor_service
|
||||
from app.services.backtest_service import get_backtest_report
|
||||
from app.services.market_regime_service import get_market_regime
|
||||
|
||||
@@ -30,3 +32,88 @@ async def backtest_report(
|
||||
"""Latest cached historical backtest report (None until the job runs)."""
|
||||
data = await get_backtest_report(db)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI/Tech Regime-Change Monitor (standalone, observational)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RegimeConfigUpdate(BaseModel):
|
||||
weights: dict[str, float] | None = None
|
||||
alert_threshold: float | None = None
|
||||
tickers: dict | None = None
|
||||
leader_weight: float | None = None
|
||||
rs_lookback: int | None = None
|
||||
fundamental_staleness_days: int | None = None
|
||||
|
||||
|
||||
class RegimeFundamentalsUpdate(BaseModel):
|
||||
f1_score: float | None = None
|
||||
f3_score: float | None = None
|
||||
locked: bool | None = None
|
||||
|
||||
|
||||
@router.get("/regime/monitor", response_model=APIEnvelope)
|
||||
async def regime_monitor(
|
||||
_user: User = Depends(require_access),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Latest AI/Tech regime-change index (0-100) + per-signal breakdown + trend."""
|
||||
data = await regime_monitor_service.get_regime_monitor(db)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.get("/regime/config", response_model=APIEnvelope)
|
||||
async def regime_config(
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Editable weights / thresholds / ticker lists for the regime monitor."""
|
||||
data = await regime_monitor_service.get_regime_config(db)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.put("/regime/config", response_model=APIEnvelope)
|
||||
async def update_regime_config(
|
||||
body: RegimeConfigUpdate,
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Merge the supplied fields into the stored regime-monitor config."""
|
||||
updates = body.model_dump(exclude_none=True)
|
||||
data = await regime_monitor_service.update_regime_config(db, updates)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.get("/regime/fundamentals", response_model=APIEnvelope)
|
||||
async def regime_fundamentals(
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Current F1 (capex) / F3 (earnings reaction) override (LLM-proposed or manual)."""
|
||||
data = await regime_monitor_service.get_fundamental_overrides(db)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.put("/regime/fundamentals", response_model=APIEnvelope)
|
||||
async def update_regime_fundamentals(
|
||||
body: RegimeFundamentalsUpdate,
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Manually override F1/F3 (locks out the LLM refresh until unlocked)."""
|
||||
data = await regime_monitor_service.set_fundamental_overrides(
|
||||
db, f1_score=body.f1_score, f3_score=body.f3_score, locked=body.locked
|
||||
)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.post("/regime/fundamentals/refresh", response_model=APIEnvelope)
|
||||
async def refresh_regime_fundamentals(
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Ask the configured LLM to re-estimate F1/F3 now (forces past a lock)."""
|
||||
data = await regime_monitor_service.refresh_fundamental_overrides(db, force=True)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
Reference in New Issue
Block a user