diff --git a/src/main.py b/src/main.py index e9b484f..9ccdd28 100644 --- a/src/main.py +++ b/src/main.py @@ -27,6 +27,7 @@ from src.core.criticality import CriticalityAssessor from src.core.priority_queue import PriorityTaskQueue from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected, RiskManager from src.db import get_latest_buy_trade, init_db, log_trade +from src.evolution.daily_review import DailyReviewer from src.logging.decision_logger import DecisionLogger from src.logging_config import setup_logging from src.markets.schedule import MarketInfo, get_next_market_open, get_open_markets @@ -736,6 +737,41 @@ async def run_daily_session( logger.info("Daily trading session completed") +async def _handle_market_close( + market_code: str, + market_name: str, + market_timezone: Any, + telegram: TelegramClient, + context_aggregator: ContextAggregator, + daily_reviewer: DailyReviewer, +) -> None: + """Handle market-close tasks: notify, aggregate, review, and store context.""" + await telegram.notify_market_close(market_name, 0.0) + + market_date = datetime.now(market_timezone).date().isoformat() + context_aggregator.aggregate_daily_from_trades( + date=market_date, + market=market_code, + ) + + scorecard = daily_reviewer.generate_scorecard(market_date, market_code) + daily_reviewer.store_scorecard_in_context(scorecard) + + lessons = await daily_reviewer.generate_lessons(scorecard) + if lessons: + scorecard.lessons = lessons + daily_reviewer.store_scorecard_in_context(scorecard) + + await telegram.send_message( + f"Daily Review ({market_code})\n" + f"Date: {scorecard.date}\n" + f"Decisions: {scorecard.total_decisions}\n" + f"P&L: {scorecard.total_pnl:+.2f}\n" + f"Win Rate: {scorecard.win_rate:.2f}%\n" + f"Lessons: {', '.join(scorecard.lessons) if scorecard.lessons else 'N/A'}" + ) + + async def run(settings: Settings) -> None: """Main async loop — iterate over open markets on a timer.""" broker = KISBroker(settings) @@ -751,6 +787,7 @@ async def run(settings: Settings) -> None: context_selector = ContextSelector(context_store) scenario_engine = ScenarioEngine() playbook_store = PlaybookStore(db_conn) + daily_reviewer = DailyReviewer(db_conn, context_store, gemini_client=brain) pre_market_planner = PreMarketPlanner( gemini_client=brain, context_store=context_store, @@ -1029,13 +1066,13 @@ async def run(settings: Settings) -> None: market_info = MARKETS.get(market_code) if market_info: - await telegram.notify_market_close(market_info.name, 0.0) - market_date = datetime.now( - market_info.timezone - ).date().isoformat() - context_aggregator.aggregate_daily_from_trades( - date=market_date, - market=market_code, + await _handle_market_close( + market_code=market_code, + market_name=market_info.name, + market_timezone=market_info.timezone, + telegram=telegram, + context_aggregator=context_aggregator, + daily_reviewer=daily_reviewer, ) except Exception as exc: logger.warning("Market close notification failed: %s", exc) diff --git a/tests/test_main.py b/tests/test_main.py index 4952297..2b2a363 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,6 @@ """Tests for main trading loop integration.""" -from datetime import date +from datetime import UTC, date from unittest.mock import ANY, AsyncMock, MagicMock, patch import pytest @@ -8,8 +8,9 @@ import pytest from src.context.layer import ContextLayer from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected from src.db import init_db, log_trade +from src.evolution.scorecard import DailyScorecard from src.logging.decision_logger import DecisionLogger -from src.main import safe_float, trading_cycle +from src.main import _handle_market_close, safe_float, trading_cycle from src.strategy.models import ( DayPlaybook, ScenarioAction, @@ -1219,3 +1220,78 @@ async def test_sell_updates_original_buy_decision_outcome() -> None: assert updated_buy is not None assert updated_buy.outcome_pnl == 20.0 assert updated_buy.outcome_accuracy == 1 + + +@pytest.mark.asyncio +async def test_handle_market_close_runs_daily_review_flow() -> None: + """Market close should aggregate, create scorecard, lessons, and notify.""" + telegram = MagicMock() + telegram.notify_market_close = AsyncMock() + telegram.send_message = AsyncMock() + + context_aggregator = MagicMock() + reviewer = MagicMock() + reviewer.generate_scorecard.return_value = DailyScorecard( + date="2026-02-14", + market="KR", + total_decisions=3, + buys=1, + sells=1, + holds=1, + total_pnl=12.5, + win_rate=50.0, + avg_confidence=75.0, + scenario_match_rate=66.7, + ) + reviewer.generate_lessons = AsyncMock(return_value=["Cut losers faster"]) + + await _handle_market_close( + market_code="KR", + market_name="Korea", + market_timezone=UTC, + telegram=telegram, + context_aggregator=context_aggregator, + daily_reviewer=reviewer, + ) + + telegram.notify_market_close.assert_called_once_with("Korea", 0.0) + context_aggregator.aggregate_daily_from_trades.assert_called_once() + reviewer.generate_scorecard.assert_called_once() + assert reviewer.store_scorecard_in_context.call_count == 2 + reviewer.generate_lessons.assert_called_once() + telegram.send_message.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_market_close_without_lessons_stores_once() -> None: + """If no lessons are generated, scorecard should be stored once.""" + telegram = MagicMock() + telegram.notify_market_close = AsyncMock() + telegram.send_message = AsyncMock() + + context_aggregator = MagicMock() + reviewer = MagicMock() + reviewer.generate_scorecard.return_value = DailyScorecard( + date="2026-02-14", + market="US", + total_decisions=1, + buys=0, + sells=1, + holds=0, + total_pnl=-3.0, + win_rate=0.0, + avg_confidence=65.0, + scenario_match_rate=100.0, + ) + reviewer.generate_lessons = AsyncMock(return_value=[]) + + await _handle_market_close( + market_code="US", + market_name="United States", + market_timezone=UTC, + telegram=telegram, + context_aggregator=context_aggregator, + daily_reviewer=reviewer, + ) + + assert reviewer.store_scorecard_in_context.call_count == 1