"""Daily review generator for market-scoped end-of-day scorecards.""" from __future__ import annotations import json import logging import re import sqlite3 from dataclasses import asdict from src.brain.gemini_client import GeminiClient from src.context.layer import ContextLayer from src.context.store import ContextStore from src.evolution.scorecard import DailyScorecard logger = logging.getLogger(__name__) class DailyReviewer: """Builds daily scorecards and optional AI-generated lessons.""" def __init__( self, conn: sqlite3.Connection, context_store: ContextStore, gemini_client: GeminiClient | None = None, ) -> None: self._conn = conn self._context_store = context_store self._gemini = gemini_client def generate_scorecard(self, date: str, market: str) -> DailyScorecard: """Generate a market-scoped scorecard from decision logs and trades.""" decision_rows = self._conn.execute( """ SELECT action, confidence, context_snapshot FROM decision_logs WHERE DATE(timestamp) = ? AND market = ? """, (date, market), ).fetchall() total_decisions = len(decision_rows) buys = sum(1 for row in decision_rows if row[0] == "BUY") sells = sum(1 for row in decision_rows if row[0] == "SELL") holds = sum(1 for row in decision_rows if row[0] == "HOLD") avg_confidence = ( round(sum(int(row[1]) for row in decision_rows) / total_decisions, 2) if total_decisions > 0 else 0.0 ) matched = 0 for row in decision_rows: try: snapshot = json.loads(row[2]) if row[2] else {} except json.JSONDecodeError: snapshot = {} scenario_match = snapshot.get("scenario_match", {}) if isinstance(scenario_match, dict) and scenario_match: matched += 1 scenario_match_rate = ( round((matched / total_decisions) * 100, 2) if total_decisions else 0.0 ) trade_stats = self._conn.execute( """ SELECT COALESCE(SUM(pnl), 0.0), SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END), SUM(CASE WHEN pnl < 0 THEN 1 ELSE 0 END) FROM trades WHERE DATE(timestamp) = ? AND market = ? """, (date, market), ).fetchone() total_pnl = round(float(trade_stats[0] or 0.0), 2) if trade_stats else 0.0 wins = int(trade_stats[1] or 0) if trade_stats else 0 losses = int(trade_stats[2] or 0) if trade_stats else 0 win_rate = round((wins / (wins + losses)) * 100, 2) if (wins + losses) > 0 else 0.0 top_winners = [ row[0] for row in self._conn.execute( """ SELECT stock_code, SUM(pnl) AS stock_pnl FROM trades WHERE DATE(timestamp) = ? AND market = ? GROUP BY stock_code HAVING stock_pnl > 0 ORDER BY stock_pnl DESC LIMIT 3 """, (date, market), ).fetchall() ] top_losers = [ row[0] for row in self._conn.execute( """ SELECT stock_code, SUM(pnl) AS stock_pnl FROM trades WHERE DATE(timestamp) = ? AND market = ? GROUP BY stock_code HAVING stock_pnl < 0 ORDER BY stock_pnl ASC LIMIT 3 """, (date, market), ).fetchall() ] return DailyScorecard( date=date, market=market, total_decisions=total_decisions, buys=buys, sells=sells, holds=holds, total_pnl=total_pnl, win_rate=win_rate, avg_confidence=avg_confidence, scenario_match_rate=scenario_match_rate, top_winners=top_winners, top_losers=top_losers, lessons=[], cross_market_note="", ) async def generate_lessons(self, scorecard: DailyScorecard) -> list[str]: """Generate concise lessons from scorecard metrics using Gemini.""" if self._gemini is None: return [] prompt = ( "You are a trading performance reviewer.\n" "Return ONLY a JSON array of 1-3 short lessons in English.\n" f"Market: {scorecard.market}\n" f"Date: {scorecard.date}\n" f"Total decisions: {scorecard.total_decisions}\n" f"Buys/Sells/Holds: {scorecard.buys}/{scorecard.sells}/{scorecard.holds}\n" f"Total PnL: {scorecard.total_pnl}\n" f"Win rate: {scorecard.win_rate}%\n" f"Average confidence: {scorecard.avg_confidence}\n" f"Scenario match rate: {scorecard.scenario_match_rate}%\n" f"Top winners: {', '.join(scorecard.top_winners) or 'N/A'}\n" f"Top losers: {', '.join(scorecard.top_losers) or 'N/A'}\n" ) try: decision = await self._gemini.decide( { "stock_code": "REVIEW", "market_name": scorecard.market, "current_price": 0, "prompt_override": prompt, } ) return self._parse_lessons(decision.rationale) except Exception as exc: logger.warning("Failed to generate daily lessons: %s", exc) return [] def store_scorecard_in_context(self, scorecard: DailyScorecard) -> None: """Store scorecard in L6 using market-scoped key.""" self._context_store.set_context( ContextLayer.L6_DAILY, scorecard.date, f"scorecard_{scorecard.market}", asdict(scorecard), ) def _parse_lessons(self, raw_text: str) -> list[str]: """Parse lessons from JSON array response or fallback text.""" raw_text = raw_text.strip() try: parsed = json.loads(raw_text) if isinstance(parsed, list): return [str(item).strip() for item in parsed if str(item).strip()][:3] except json.JSONDecodeError: pass match = re.search(r"\[.*\]", raw_text, re.DOTALL) if match: try: parsed = json.loads(match.group(0)) if isinstance(parsed, list): return [str(item).strip() for item in parsed if str(item).strip()][:3] except json.JSONDecodeError: pass lines = [line.strip("-* \t") for line in raw_text.splitlines() if line.strip()] return lines[:3]