diff --git a/signal_v2/main.py b/signal_v2/main.py index 554bc6c..87ab3a8 100644 --- a/signal_v2/main.py +++ b/signal_v2/main.py @@ -82,6 +82,8 @@ async def lifespan(app: FastAPI): _ctx.client, state_mod.state, _ctx.shutdown, kis_client=_ctx.kis_client, chronos=_ctx.chronos, + dedup=_ctx.dedup, + settings=settings, ) ) diff --git a/signal_v2/pull_worker.py b/signal_v2/pull_worker.py index 8b09041..fe7401e 100644 --- a/signal_v2/pull_worker.py +++ b/signal_v2/pull_worker.py @@ -19,6 +19,8 @@ async def poll_loop( client: StockClient, state: PollState, shutdown: asyncio.Event, kis_client: KISClient | None = None, chronos=None, + dedup=None, + settings=None, ) -> None: """FastAPI lifespan 에서 asyncio.create_task 로 시작.""" logger.info("poll_loop started") @@ -40,6 +42,13 @@ async def poll_loop( await _run_post_close_cycle(kis_client, chronos, state) except Exception: logger.exception("post-close cycle failed") + # Phase 4: generate signals + if dedup is not None and settings is not None: + try: + from signal_v2.signal_generator import generate_signals + generate_signals(state, dedup, settings) + except Exception: + logger.exception("generate_signals failed") interval = _next_interval(now) try: await asyncio.wait_for(shutdown.wait(), timeout=interval) diff --git a/signal_v2/tests/test_pull_worker.py b/signal_v2/tests/test_pull_worker.py index fe76954..220f58e 100644 --- a/signal_v2/tests/test_pull_worker.py +++ b/signal_v2/tests/test_pull_worker.py @@ -95,3 +95,37 @@ async def test_post_close_cycle_updates_chronos_predictions(): assert state.chronos_predictions["005930"]["conf"] == 0.85 assert "005930" in state.daily_ohlcv assert "chronos/005930" in state.last_updated + + +def test_poll_loop_calls_generate_signals_after_cycle(monkeypatch): + """Phase 4: generate_signals 가 cycle 후 state.signals 를 갱신한다.""" + from unittest.mock import MagicMock + from signal_v2.state import PollState + from signal_v2.signal_generator import generate_signals + + state = PollState() + state.portfolio = {"holdings": [{ + "ticker": "005930", "name": "삼성전자", + "avg_price": 75000, "current_price": 69000, + "pnl_pct": -0.08, "profit_rate": -8.0, + "quantity": 100, "broker": "키움", + }]} + state.screener_preview = {"items": []} + + dedup = MagicMock() + dedup.is_recent.return_value = False + + settings = MagicMock() + settings.stop_loss_pct = -0.07 + settings.take_profit_pct = 0.15 + settings.chronos_spread_threshold = 0.6 + settings.asking_bid_ratio_threshold = 0.6 + settings.confidence_threshold = 0.7 + settings.min_momentum_for_buy = "strong_up" + + generate_signals(state, dedup, settings) + + assert "005930" in state.signals + assert state.signals["005930"]["action"] == "sell" + assert state.signals["005930"]["confidence_webai"] == 1.0 + dedup.record.assert_called_with("005930", "sell", confidence=1.0)