fix: add error handling to evolution loop telegram notification

Wrap evolution notification in try/except so telegram failures don't
crash the evolution loop. Add integration tests for market close flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-14 23:56:04 +09:00
parent 75320eb587
commit e0a6b307a2
2 changed files with 110 additions and 7 deletions

View File

@@ -832,6 +832,7 @@ async def _run_evolution_loop(
logger.info("Evolution loop skipped on %s (no actionable failures)", market_date) logger.info("Evolution loop skipped on %s (no actionable failures)", market_date)
return return
try:
await telegram.send_message( await telegram.send_message(
"<b>Evolution Update</b>\n" "<b>Evolution Update</b>\n"
f"Date: {market_date}\n" f"Date: {market_date}\n"
@@ -839,6 +840,8 @@ async def _run_evolution_loop(
f"Branch: {pr_info.get('branch', 'N/A')}\n" f"Branch: {pr_info.get('branch', 'N/A')}\n"
f"Status: {pr_info.get('status', 'N/A')}" f"Status: {pr_info.get('status', 'N/A')}"
) )
except Exception as exc:
logger.warning("Evolution notification failed on %s: %s", market_date, exc)
async def run(settings: Settings) -> None: async def run(settings: Settings) -> None:

View File

@@ -1304,6 +1304,82 @@ async def test_handle_market_close_without_lessons_stores_once() -> None:
assert reviewer.store_scorecard_in_context.call_count == 1 assert reviewer.store_scorecard_in_context.call_count == 1
@pytest.mark.asyncio
async def test_handle_market_close_triggers_evolution_for_us() -> None:
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=2,
buys=1,
sells=1,
holds=0,
total_pnl=3.0,
win_rate=50.0,
avg_confidence=80.0,
scenario_match_rate=100.0,
)
reviewer.generate_lessons = AsyncMock(return_value=[])
evolution_optimizer = MagicMock()
evolution_optimizer.evolve = AsyncMock(return_value=None)
await _handle_market_close(
market_code="US",
market_name="United States",
market_timezone=UTC,
telegram=telegram,
context_aggregator=context_aggregator,
daily_reviewer=reviewer,
evolution_optimizer=evolution_optimizer,
)
evolution_optimizer.evolve.assert_called_once()
@pytest.mark.asyncio
async def test_handle_market_close_skips_evolution_for_kr() -> None:
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=1,
buys=1,
sells=0,
holds=0,
total_pnl=1.0,
win_rate=100.0,
avg_confidence=90.0,
scenario_match_rate=100.0,
)
reviewer.generate_lessons = AsyncMock(return_value=[])
evolution_optimizer = MagicMock()
evolution_optimizer.evolve = AsyncMock(return_value=None)
await _handle_market_close(
market_code="KR",
market_name="Korea",
market_timezone=UTC,
telegram=telegram,
context_aggregator=context_aggregator,
daily_reviewer=reviewer,
evolution_optimizer=evolution_optimizer,
)
evolution_optimizer.evolve.assert_not_called()
def test_run_context_scheduler_invokes_scheduler() -> None: def test_run_context_scheduler_invokes_scheduler() -> None:
"""Scheduler helper should call run_if_due with provided datetime.""" """Scheduler helper should call run_if_due with provided datetime."""
scheduler = MagicMock() scheduler = MagicMock()
@@ -1354,3 +1430,27 @@ async def test_run_evolution_loop_notifies_when_pr_generated() -> None:
optimizer.evolve.assert_called_once() optimizer.evolve.assert_called_once()
telegram.send_message.assert_called_once() telegram.send_message.assert_called_once()
@pytest.mark.asyncio
async def test_run_evolution_loop_notification_error_is_ignored() -> 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(side_effect=RuntimeError("telegram down"))
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()