feat(trade-monitor): monitor 오케스트레이션 (run_cycle/loop/state_fn)
This commit is contained in:
95
services/trade-monitor/tests/test_monitor.py
Normal file
95
services/trade-monitor/tests/test_monitor.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user