trading/services/kevin_signal_bridge/aggregator.py
Viktor Barzin 3347847e38
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
feat(kevin_bridge): multi-mention aggregator with capped conviction boost
2026-05-24 01:01:02 +00:00

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