feat: 전략 진화 루프 연결 (issue #95) #126

Merged
jihoson merged 1 commits from feature/issue-95-evolution-loop into main 2026-02-14 23:42:30 +09:00
2 changed files with 90 additions and 1 deletions
Showing only changes of commit afb31b7f4b - Show all commits

View File

@@ -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(
"<b>Evolution Update</b>\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)

View File

@@ -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()