diff --git a/services/api_gateway/routes/meet_kevin.py b/services/api_gateway/routes/meet_kevin.py index c37c933..d1a8913 100644 --- a/services/api_gateway/routes/meet_kevin.py +++ b/services/api_gateway/routes/meet_kevin.py @@ -612,14 +612,15 @@ async def get_dashboard( } # Top conviction last 7 days - seven_days_ago = func.now() - func.cast("7 days", type_=None) + from sqlalchemy import text + seven_days_ago = text("now() - interval '7 days'") top_conviction_result = await session.execute( select( KevinStockMention.symbol, func.max(KevinStockMention.conviction).label("max_conviction"), func.count().label("mention_count"), ) - .where(KevinStockMention.created_at >= func.now() - func.cast("7 days", type_=None)) + .where(KevinStockMention.created_at >= text("now() - interval '7 days'")) .group_by(KevinStockMention.symbol) .order_by(desc(func.max(KevinStockMention.conviction))) .limit(10) @@ -634,18 +635,17 @@ async def get_dashboard( ] # 14-day outlook trend: date × direction → count + # Use a single trunc expression so SELECT/GROUP BY/ORDER BY reference the same SQL fragment + day_trunc = func.date_trunc("day", KevinAnalysis.created_at) outlook_result = await session.execute( select( - func.date_trunc("day", KevinAnalysis.created_at).label("day"), + day_trunc.label("day"), KevinAnalysis.market_outlook_direction, func.count().label("count"), ) - .where(KevinAnalysis.created_at >= func.now() - func.cast("14 days", type_=None)) - .group_by( - func.date_trunc("day", KevinAnalysis.created_at), - KevinAnalysis.market_outlook_direction, - ) - .order_by(func.date_trunc("day", KevinAnalysis.created_at)) + .where(KevinAnalysis.created_at >= text("now() - interval '14 days'")) + .group_by(day_trunc, KevinAnalysis.market_outlook_direction) + .order_by(day_trunc) ) outlook_trend = [ { diff --git a/shared/models/meet_kevin.py b/shared/models/meet_kevin.py index 28df77e..06e0cd1 100644 --- a/shared/models/meet_kevin.py +++ b/shared/models/meet_kevin.py @@ -125,7 +125,7 @@ class KevinVideo(TimestampMixin, Base): duration_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True) thumbnail_url: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[KevinVideoStatus] = mapped_column( - SAEnum(KevinVideoStatus, name="kevin_video_status"), + SAEnum(KevinVideoStatus, name="kevin_video_status", values_callable=lambda x: [e.value for e in x]), nullable=False, default=KevinVideoStatus.DISCOVERED, index=True, @@ -159,7 +159,7 @@ class KevinTranscript(Base): BigInteger, ForeignKey("kevin_videos.id"), unique=True, nullable=False ) source: Mapped[KevinTranscriptSource] = mapped_column( - SAEnum(KevinTranscriptSource, name="kevin_transcript_source"), nullable=False + SAEnum(KevinTranscriptSource, name="kevin_transcript_source", values_callable=lambda x: [e.value for e in x]), nullable=False ) language: Mapped[str] = mapped_column(String(8), nullable=False) raw_text: Mapped[str] = mapped_column(Text, nullable=False) @@ -185,7 +185,7 @@ class KevinAnalysis(Base): model: Mapped[str] = mapped_column(String(100), nullable=False) prompt_version: Mapped[str] = mapped_column(String(50), nullable=False) market_outlook_direction: Mapped[KevinMarketOutlook] = mapped_column( - SAEnum(KevinMarketOutlook, name="kevin_market_outlook"), nullable=False + SAEnum(KevinMarketOutlook, name="kevin_market_outlook", values_callable=lambda x: [e.value for e in x]), nullable=False ) market_outlook_reasoning: Mapped[str] = mapped_column(Text, nullable=False) macro_themes_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True) @@ -222,11 +222,11 @@ class KevinStockMention(Base): String(16), nullable=False, index=True ) action: Mapped[KevinTickerAction] = mapped_column( - SAEnum(KevinTickerAction, name="kevin_ticker_action"), nullable=False + SAEnum(KevinTickerAction, name="kevin_ticker_action", values_callable=lambda x: [e.value for e in x]), nullable=False ) conviction: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False) time_horizon: Mapped[KevinTimeHorizon] = mapped_column( - SAEnum(KevinTimeHorizon, name="kevin_time_horizon"), nullable=False + SAEnum(KevinTimeHorizon, name="kevin_time_horizon", values_callable=lambda x: [e.value for e in x]), nullable=False ) rationale_quote: Mapped[str] = mapped_column(Text, nullable=False) video_timestamp_seconds: Mapped[int | None] = mapped_column(