diff --git a/src/main.py b/src/main.py index 965261a..5a7f711 100644 --- a/src/main.py +++ b/src/main.py @@ -29,6 +29,7 @@ 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.evolution.optimizer import EvolutionOptimizer 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 @@ -745,6 +746,7 @@ async def _handle_market_close( telegram: TelegramClient, context_aggregator: ContextAggregator, daily_reviewer: DailyReviewer, + evolution_optimizer: EvolutionOptimizer | None = None, ) -> None: """Handle market-close tasks: notify, aggregate, review, and store context.""" await telegram.notify_market_close(market_name, 0.0) @@ -772,6 +774,14 @@ async def _handle_market_close( f"Lessons: {', '.join(scorecard.lessons) if scorecard.lessons else 'N/A'}" ) + if evolution_optimizer is not None: + await _run_evolution_loop( + evolution_optimizer=evolution_optimizer, + telegram=telegram, + market_code=market_code, + market_date=market_date, + ) + def _run_context_scheduler( scheduler: ContextScheduler, now: datetime | None = None, @@ -802,6 +812,35 @@ def _run_context_scheduler( ) +async def _run_evolution_loop( + evolution_optimizer: EvolutionOptimizer, + telegram: TelegramClient, + market_code: str, + market_date: str, +) -> None: + """Run evolution loop once at US close (end of trading day).""" + if market_code != "US": + return + + try: + pr_info = await evolution_optimizer.evolve() + except Exception as exc: + logger.warning("Evolution loop failed on %s: %s", market_date, exc) + return + + if pr_info is None: + logger.info("Evolution loop skipped on %s (no actionable failures)", market_date) + return + + await telegram.send_message( + "Evolution Update\n" + f"Date: {market_date}\n" + f"PR: {pr_info.get('title', 'N/A')}\n" + f"Branch: {pr_info.get('branch', 'N/A')}\n" + f"Status: {pr_info.get('status', 'N/A')}" + ) + + async def run(settings: Settings) -> None: """Main async loop — iterate over open markets on a timer.""" broker = KISBroker(settings) @@ -816,6 +855,7 @@ async def run(settings: Settings) -> None: aggregator=context_aggregator, store=context_store, ) + evolution_optimizer = EvolutionOptimizer(settings) # V2 proactive strategy components context_selector = ContextSelector(context_store) @@ -1109,6 +1149,7 @@ async def run(settings: Settings) -> None: telegram=telegram, context_aggregator=context_aggregator, daily_reviewer=daily_reviewer, + evolution_optimizer=evolution_optimizer, ) 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 93318ee..378077a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -11,7 +11,13 @@ 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 _handle_market_close, _run_context_scheduler, safe_float, trading_cycle +from src.main import ( + _handle_market_close, + _run_context_scheduler, + _run_evolution_loop, + safe_float, + trading_cycle, +) from src.strategy.models import ( DayPlaybook, ScenarioAction, @@ -1306,3 +1312,45 @@ def test_run_context_scheduler_invokes_scheduler() -> None: _run_context_scheduler(scheduler, now=datetime(2026, 2, 14, tzinfo=UTC)) scheduler.run_if_due.assert_called_once() + + +@pytest.mark.asyncio +async def test_run_evolution_loop_skips_non_us_market() -> None: + optimizer = MagicMock() + optimizer.evolve = AsyncMock() + telegram = MagicMock() + telegram.send_message = AsyncMock() + + await _run_evolution_loop( + evolution_optimizer=optimizer, + telegram=telegram, + market_code="KR", + market_date="2026-02-14", + ) + + optimizer.evolve.assert_not_called() + telegram.send_message.assert_not_called() + + +@pytest.mark.asyncio +async def test_run_evolution_loop_notifies_when_pr_generated() -> None: + optimizer = MagicMock() + optimizer.evolve = AsyncMock( + return_value={ + "title": "[Evolution] New strategy: v20260214_050000", + "branch": "evolution/v20260214_050000", + "status": "ready_for_review", + } + ) + telegram = MagicMock() + telegram.send_message = AsyncMock() + + await _run_evolution_loop( + evolution_optimizer=optimizer, + telegram=telegram, + market_code="US", + market_date="2026-02-14", + ) + + optimizer.evolve.assert_called_once() + telegram.send_message.assert_called_once()