add Telegram alerts: qualified setups, S/R proximity, score drops, daily digest
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 23s

Closes the action loop — instead of polling the dashboard, the platform pushes
actionable signals to Telegram. New hourly 'alerts' job dispatches four
toggleable triggers, deduped via a new alert_log table (cooldown-based for
qualified/S-R/digest, watermark-based for score deterioration). Admin → Settings
gains a Telegram panel (write-only bot token, chat ID, per-trigger toggles, Send
Test). Credentials follow DB > env precedence (TELEGRAM_BOT_TOKEN / _CHAT_ID).

Backend: alert_service + AlertLog model + migration 005, scheduler job, admin
endpoints/schema. Frontend: AlertSettings panel, hooks, api, types.

Deploy: run alembic upgrade (new alert_log table).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 19:42:18 +02:00
parent 9d0bef369f
commit 5d41ccac1c
17 changed files with 976 additions and 2 deletions
+33
View File
@@ -10,6 +10,7 @@ from app.dependencies import get_db, require_admin
from app.models.user import User
from app.schemas.admin import (
ActivationConfigUpdate,
AlertConfigUpdate,
CreateUserRequest,
DataCleanupRequest,
JobToggle,
@@ -24,6 +25,7 @@ from app.schemas.admin import (
)
from app.schemas.common import APIEnvelope
from app.services import admin_service
from app.services import alert_service
from app.services import sentiment_provider_service
from app.services import ticker_universe_service
@@ -210,6 +212,37 @@ async def test_sentiment_settings(
return APIEnvelope(status="success", data=result)
@router.get("/admin/settings/alerts", response_model=APIEnvelope)
async def get_alert_settings(
_admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
config = await alert_service.get_alert_config(db)
return APIEnvelope(status="success", data=config)
@router.put("/admin/settings/alerts", response_model=APIEnvelope)
async def update_alert_settings(
body: AlertConfigUpdate,
_admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
config = await alert_service.update_alert_config(
db, **body.model_dump(exclude_unset=True)
)
return APIEnvelope(status="success", data=config)
@router.post("/admin/settings/alerts/test", response_model=APIEnvelope)
async def test_alert_settings(
_admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""Send a test Telegram message with the current config."""
result = await alert_service.send_test_alert(db)
return APIEnvelope(status="success", data=result)
@router.get("/admin/settings/ticker-universe", response_model=APIEnvelope)
async def get_ticker_universe_setting(
_admin: User = Depends(require_admin),