96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""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, "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
|