Optimize signal read paths and enforce score invariants
This commit is contained in:
@@ -17,11 +17,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
@@ -257,17 +258,6 @@ def _log_alert(db: AsyncSession, alert_type: str, key: str, value: float | None
|
||||
)
|
||||
|
||||
|
||||
async def _watermark(db: AsyncSession, symbol: str) -> float | None:
|
||||
result = await db.execute(
|
||||
select(AlertLog.value)
|
||||
.where(AlertLog.alert_type == WATERMARK_TYPE, AlertLog.dedup_key == symbol)
|
||||
.order_by(AlertLog.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
row = result.first()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trigger collectors
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -407,17 +397,49 @@ async def _collect_sr_proximity(db: AsyncSession) -> list[tuple[str, str]]:
|
||||
single alert. Scoped to the watchlist only — qualified tickers already get
|
||||
their own 'qualified setup' alert, so S/R on them would be redundant.
|
||||
"""
|
||||
watchlist = await _watchlist_tickers(db)
|
||||
if not watchlist:
|
||||
return []
|
||||
|
||||
ticker_ids = [ticker_id for ticker_id, _ in watchlist]
|
||||
latest_dates = (
|
||||
select(
|
||||
OHLCVRecord.ticker_id,
|
||||
func.max(OHLCVRecord.date).label("latest_date"),
|
||||
)
|
||||
.where(OHLCVRecord.ticker_id.in_(ticker_ids))
|
||||
.group_by(OHLCVRecord.ticker_id)
|
||||
.subquery()
|
||||
)
|
||||
prices_result = await db.execute(
|
||||
select(OHLCVRecord.ticker_id, OHLCVRecord.close).join(
|
||||
latest_dates,
|
||||
(OHLCVRecord.ticker_id == latest_dates.c.ticker_id)
|
||||
& (OHLCVRecord.date == latest_dates.c.latest_date),
|
||||
)
|
||||
)
|
||||
prices = {ticker_id: float(close) for ticker_id, close in prices_result.all()}
|
||||
|
||||
levels_result = await db.execute(
|
||||
select(SRLevel).where(SRLevel.ticker_id.in_(ticker_ids))
|
||||
)
|
||||
levels_by_ticker: dict[int, list[dict]] = defaultdict(list)
|
||||
for level in levels_result.scalars():
|
||||
levels_by_ticker[level.ticker_id].append(
|
||||
{
|
||||
"price_level": level.price_level,
|
||||
"strength": level.strength,
|
||||
"type": level.type,
|
||||
}
|
||||
)
|
||||
|
||||
out: list[tuple[str, str]] = []
|
||||
for tid, symbol in await _watchlist_tickers(db):
|
||||
price = await _latest_close(db, tid)
|
||||
for tid, symbol in watchlist:
|
||||
price = prices.get(tid)
|
||||
if not price:
|
||||
continue
|
||||
|
||||
levels_result = await db.execute(select(SRLevel).where(SRLevel.ticker_id == tid))
|
||||
levels = [
|
||||
{"price_level": lv.price_level, "strength": lv.strength, "type": lv.type}
|
||||
for lv in levels_result.scalars().all()
|
||||
]
|
||||
levels = levels_by_ticker[tid]
|
||||
if not levels:
|
||||
continue
|
||||
|
||||
@@ -445,17 +467,54 @@ async def _collect_score_drops(db: AsyncSession) -> list[tuple[str, str]]:
|
||||
doesn't re-fire; let the watermark rise with the score so the next drop is
|
||||
measured from the new high.
|
||||
"""
|
||||
out: list[tuple[str, str]] = []
|
||||
for tid, symbol in await _watchlist_tickers(db):
|
||||
comp_result = await db.execute(
|
||||
select(CompositeScore.score).where(CompositeScore.ticker_id == tid)
|
||||
)
|
||||
row = comp_result.first()
|
||||
if row is None or row[0] is None:
|
||||
continue
|
||||
current = float(row[0])
|
||||
watchlist = await _watchlist_tickers(db)
|
||||
if not watchlist:
|
||||
return []
|
||||
|
||||
base = await _watermark(db, symbol)
|
||||
ticker_ids = [ticker_id for ticker_id, _ in watchlist]
|
||||
symbols = [symbol for _, symbol in watchlist]
|
||||
scores_result = await db.execute(
|
||||
select(CompositeScore.ticker_id, CompositeScore.score).where(
|
||||
CompositeScore.ticker_id.in_(ticker_ids)
|
||||
)
|
||||
)
|
||||
scores = {ticker_id: float(score) for ticker_id, score in scores_result.all()}
|
||||
|
||||
ranked_watermarks = (
|
||||
select(
|
||||
AlertLog.dedup_key,
|
||||
AlertLog.value,
|
||||
func.row_number()
|
||||
.over(
|
||||
partition_by=AlertLog.dedup_key,
|
||||
order_by=(AlertLog.created_at.desc(), AlertLog.id.desc()),
|
||||
)
|
||||
.label("rank"),
|
||||
)
|
||||
.where(
|
||||
AlertLog.alert_type == WATERMARK_TYPE,
|
||||
AlertLog.dedup_key.in_(symbols),
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
watermarks_result = await db.execute(
|
||||
select(ranked_watermarks.c.dedup_key, ranked_watermarks.c.value).where(
|
||||
ranked_watermarks.c.rank == 1
|
||||
)
|
||||
)
|
||||
watermarks = {
|
||||
symbol: float(value)
|
||||
for symbol, value in watermarks_result.all()
|
||||
if value is not None
|
||||
}
|
||||
|
||||
out: list[tuple[str, str]] = []
|
||||
for tid, symbol in watchlist:
|
||||
current = scores.get(tid)
|
||||
if current is None:
|
||||
continue
|
||||
|
||||
base = watermarks.get(symbol)
|
||||
if base is None:
|
||||
_log_alert(db, WATERMARK_TYPE, symbol, value=current) # seed, no alert
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user