97 lines
3.2 KiB
Python
97 lines
3.2 KiB
Python
"""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
|