"""Multi-mention windowed aggregation. Reads kevin_stock_mentions since the cursor, groups by symbol within a 48h trailing window, applies a conviction boost = boost_per_repeat * extras capped at max_boost. """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta from decimal import Decimal from typing import Any, Callable from sqlalchemy import and_, select from shared.models.meet_kevin import KevinStockMention @dataclass class AggregatedMention: """Mention proxy with effective_conviction set after aggregation.""" id: int symbol: str action: Any conviction: Decimal time_horizon: Any created_at: datetime rationale_quote: str effective_conviction: Decimal class MentionAggregator: def __init__( self, session_factory: Callable[..., Any], window_hours: int = 48, boost_per_repeat: Decimal = Decimal("0.05"), max_boost: Decimal = Decimal("0.20"), ) -> None: self.session_factory = session_factory self.window_hours = window_hours self.boost_per_repeat = boost_per_repeat self.max_boost = max_boost async def fetch_pending(self, since_id: int) -> list[AggregatedMention]: async with self.session_factory() as session: unprocessed = ( ( await session.execute( select(KevinStockMention) .where(KevinStockMention.id > since_id) .order_by(KevinStockMention.created_at.asc()) ) ) .scalars() .all() ) if not unprocessed: return [] out: list[AggregatedMention] = [] for m in unprocessed: window_start = m.created_at - timedelta(hours=self.window_hours) same_symbol_in_window = ( ( await session.execute( select(KevinStockMention).where( and_( KevinStockMention.symbol == m.symbol, KevinStockMention.created_at >= window_start, KevinStockMention.created_at <= m.created_at, ) ) ) ) .scalars() .all() ) extras = max(0, len(same_symbol_in_window) - 1) boost = min(self.max_boost, self.boost_per_repeat * extras) effective = min(Decimal("1.0"), m.conviction + boost) out.append( AggregatedMention( id=m.id, symbol=m.symbol, action=m.action, conviction=m.conviction, time_horizon=m.time_horizon, created_at=m.created_at, rationale_quote=m.rationale_quote, effective_conviction=effective, ) ) return out