"""monitor.run_cycle — 게이트/필터/조립/격리.""" from monitor import MonitorState, filter_krx, run_cycle from config import load_settings from _shared.heartbeat import WorkerStats def test_filter_krx_keeps_only_numeric6(): targets = [{"ticker": "005930"}, {"ticker": "AAPL"}, {"ticker": "00660"}, {"ticker": "000660"}, {"ticker": "0059301"}] kept = {t["ticker"] for t in filter_krx(targets)} assert kept == {"005930", "000660"} class _FakeNAS: def __init__(self, ms): self._ms = ms self.reported = None async def get_monitor_set(self): return self._ms async def post_report(self, as_of, firing): self.reported = {"as_of": as_of, "firing": firing} return {"new_alerts": len(firing), "cleared": 0} class _FakeKIS: def __init__(self, price=100, fail_on=None): self._price = price self._fail_on = fail_on or set() async def get_quote(self, ticker): if ticker in self._fail_on: raise RuntimeError("KIS down") return {"price": self._price, "day_open": 99, "day_high": 100, "today_volume": 1000, "as_of": "x"} async def get_daily_ohlcv(self, ticker, days=250): # 정배열 + 저가 근접 → ma20_pullback 발화 유도 return [{"open": 90, "high": 90, "low": 90, "close": 90, "volume": 1}] * 200 \ + [{"open": 100, "high": 100, "low": 100, "close": 100, "volume": 1}] * 20 async def test_closed_session_skips_kis(): nas = _FakeNAS({"session": "closed"}) state, stats = MonitorState(), WorkerStats() await run_cycle(nas, _FakeKIS(), state, stats, load_settings()) assert state.session_state == "market_closed" assert nas.reported is None # report도 안 함 async def test_non_krx_skipped_and_report_sent(): nas = _FakeNAS({"session": "regular", "buy_targets": [{"ticker": "AAPL", "name": "Apple"}], "sell_targets": [], "buy_params": {}, "exit_params": {}}) state, stats = MonitorState(), WorkerStats() await run_cycle(nas, _FakeKIS(), state, stats, load_settings()) assert state.session_state == "market_open" assert nas.reported is not None assert nas.reported["firing"] == [] # 알파벳 티커 skip → 빈 발화 async def test_firing_assembled_and_last_alert_set(): nas = _FakeNAS({"session": "regular", "buy_targets": [{"ticker": "005930", "name": "삼성전자"}], "sell_targets": [], "buy_params": {"pullback_pct": 0.02}, "exit_params": {}}) state, stats = MonitorState(), WorkerStats() await run_cycle(nas, _FakeKIS(price=101), state, stats, load_settings()) conds = {f["condition"] for f in nas.reported["firing"]} assert "buy_ma20_pullback" in conds assert state.last_alert_at is not None async def test_per_ticker_failure_isolated(): nas = _FakeNAS({"session": "regular", "buy_targets": [{"ticker": "005930"}, {"ticker": "000660"}], "sell_targets": [], "buy_params": {}, "exit_params": {}}) state, stats = MonitorState(), WorkerStats() # 005930은 실패, 000660은 성공 → 루프가 죽지 않고 report 전송 await run_cycle(nas, _FakeKIS(fail_on={"005930"}), state, stats, load_settings()) assert nas.reported is not None assert state.session_state == "market_open" async def test_monitor_set_failure_sets_idle(): class _BadNAS(_FakeNAS): async def get_monitor_set(self): raise RuntimeError("NAS down") nas = _BadNAS({}) state, stats = MonitorState(), WorkerStats() await run_cycle(nas, _FakeKIS(), state, stats, load_settings()) assert state.session_state == "idle" assert nas.reported is None