add market-regime guard (SPY trend) — inform + warn
New market_regime_service computes a benchmark (SPY) trend from its 50/200-day SMAs, cached in a SystemSetting and refreshed by a nightly job; GET /market/regime exposes it. Dashboard shows a regime banner; setup cards flag a counter-trend caution when a setup fights the regime (LONG in a bearish market / SHORT in a bullish one). Informational only — nothing is suppressed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ from app.providers.fundamentals_chain import build_fundamental_provider_chain
|
||||
from app.providers.protocol import SentimentData
|
||||
from app.services import fundamental_service, ingestion_service, sentiment_service
|
||||
from app.services.alert_service import dispatch_alerts
|
||||
from app.services.market_regime_service import update_market_regime
|
||||
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
|
||||
@@ -133,6 +134,17 @@ _job_runtime: dict[str, dict[str, object]] = {
|
||||
"finished_at": None,
|
||||
"message": None,
|
||||
},
|
||||
"market_regime": {
|
||||
"running": False,
|
||||
"status": "idle",
|
||||
"processed": 0,
|
||||
"total": None,
|
||||
"progress_pct": None,
|
||||
"current_ticker": None,
|
||||
"started_at": None,
|
||||
"finished_at": None,
|
||||
"message": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -802,6 +814,42 @@ async def dispatch_alerts_job() -> None:
|
||||
}))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job: Market Regime
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def compute_market_regime() -> None:
|
||||
"""Refresh the cached benchmark (SPY) trend regime."""
|
||||
job_name = "market_regime"
|
||||
logger.info(json.dumps({"event": "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):
|
||||
logger.info(json.dumps({"event": "job_skipped", "job": job_name, "reason": "disabled"}))
|
||||
_runtime_finish(job_name, "skipped", processed=0, total=1, message="Disabled")
|
||||
return
|
||||
|
||||
regime = await update_market_regime(db)
|
||||
|
||||
_runtime_progress(job_name, processed=1, total=1)
|
||||
_runtime_finish(
|
||||
job_name, "completed", processed=1, total=1,
|
||||
message=f"Regime: {regime.get('label')}",
|
||||
)
|
||||
logger.info(json.dumps({"event": "job_complete", "job": job_name, "label": regime.get("label")}))
|
||||
except Exception as exc:
|
||||
_runtime_finish(job_name, "error", processed=0, total=1, message=str(exc))
|
||||
logger.error(json.dumps({
|
||||
"event": "job_error",
|
||||
"job": job_name,
|
||||
"error_type": type(exc).__name__,
|
||||
"message": str(exc),
|
||||
}))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job: Ticker Universe Sync
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -951,6 +999,16 @@ def configure_scheduler() -> None:
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
# Market Regime — nightly benchmark trend refresh
|
||||
scheduler.add_job(
|
||||
compute_market_regime,
|
||||
"interval",
|
||||
hours=24,
|
||||
id="market_regime",
|
||||
name="Market Regime",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
json.dumps({
|
||||
"event": "scheduler_configured",
|
||||
|
||||
Reference in New Issue
Block a user