Bundle signal alert notifications
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 35s

This commit is contained in:
2026-07-03 13:32:59 +02:00
parent d4ccea2d69
commit eaad935a2a
2 changed files with 198 additions and 21 deletions
+78
View File
@@ -6,6 +6,7 @@ from datetime import date, datetime, timedelta, timezone
from types import SimpleNamespace
import pytest
from sqlalchemy import select
from app.models.alert import AlertLog
from app.models.ohlcv import OHLCVRecord
@@ -113,6 +114,26 @@ async def test_score_drop_seeds_then_alerts(session):
assert await svc._watermark(session, "AAA") == 60.0
def test_format_qualified_includes_current_price_and_target_move():
text = svc._format_qualified({
"symbol": "AAPL",
"direction": "long",
"current_price": 196.42,
"entry_price": 195.80,
"target": 207.50,
"stop_loss": 190.20,
"rr_ratio": 2.1,
"confidence_score": 76.0,
"targets": [{"probability": 63.0}],
})
assert "now 196.42" in text
assert "entry 195.80" in text
assert "target 207.50 (+5.6%)" in text
assert "stop 190.20" in text
assert "P(target) 63%" in text
async def _add_ticker(session, symbol: str, *, watchlisted: bool, close: float,
levels: list[tuple[float, str, int]]) -> int:
user = await session.get(User, 1)
@@ -142,6 +163,8 @@ async def test_sr_proximity_merges_close_levels_to_one_alert(session):
cvx = [m for m in msgs if "CVX" in m[1]]
assert len(cvx) == 1
assert "183.00185.00" in cvx[0][1]
assert "now 182.00 -> 183.00185.00 (+0.5%)" in cvx[0][1]
assert "strength 100" in cvx[0][1]
async def test_sr_proximity_skips_non_watchlist_unqualified(session):
@@ -172,6 +195,61 @@ async def test_dispatch_no_credentials(session):
assert res["status"] == "no_credentials"
async def test_dispatch_bundles_discovery_alerts_and_logs_each_item(session, monkeypatch):
async def fake_collect_qualified(_db):
return [
("qualified:AAPL:long", "🟢 <b>AAPL LONG</b> | now 196.42 | target 207.50 (+5.6%)"),
("qualified:TSLA:short", "🔴 <b>TSLA SHORT</b> | now 292.10 | target 276.50 (-5.3%)"),
]
async def fake_collect_sr(_db):
return [
("sr:MSFT:resistance", "📍 <b>MSFT</b> resistance | now 508.20 -> 512.00 (+0.7%)"),
]
sent: list[str] = []
async def fake_send(_client, _token, _chat_id, text):
sent.append(text)
monkeypatch.setattr(svc, "_collect_qualified", fake_collect_qualified)
monkeypatch.setattr(svc, "_collect_sr_proximity", fake_collect_sr)
monkeypatch.setattr(svc, "_send", fake_send)
await svc.update_alert_config(
session,
enabled=True,
bot_token="token",
telegram_chat_id="chat",
score_drop_enabled=False,
digest_enabled=False,
regime_quadrant_enabled=False,
trade_closed_enabled=False,
)
res = await svc.dispatch_alerts(session)
assert res == {"status": "ok", "sent": 1, "candidates": 3}
assert len(sent) == 1
assert "<b>Signal run</b> — 3 new alert(s)" in sent[0]
assert "<b>Qualified setups</b>" in sent[0]
assert "<b>Near support/resistance</b>" in sent[0]
assert "AAPL LONG" in sent[0]
assert "MSFT" in sent[0]
rows = (
await session.execute(
select(AlertLog.alert_type, AlertLog.dedup_key)
.order_by(AlertLog.alert_type, AlertLog.dedup_key)
)
).all()
assert rows == [
("qualified", "qualified:AAPL:long"),
("qualified", "qualified:TSLA:short"),
("sr_proximity", "sr:MSFT:resistance"),
]
async def _add_closed_trade(session, symbol: str, reason: str, *,
close: float = 110.0, closed_hours_ago: float = 1.0) -> None:
if await session.get(User, 1) is None: