# trade-monitor 워커 — 구현 설계 > **2026-07-03 · web-ai 소유.** 실시간 매매 알람 파이프라인의 Windows-side 워커. > **권위 계약(원본 스펙)**: web-backend repo `docs/superpowers/specs/2026-07-02-realtime-trade-alerts-design.md` §5(계약)·§6(조건). > 이 문서는 그 계약을 Windows Docker 워커로 구현하기 위한 **구현 설계**(모듈 분해·조건 해석·배포)만 다룬다. --- ## 1. 역할 · 경계 `services/trade-monitor/` 는 형제 워커(`image-render`, `task-watcher`)와 동일한 관례를 따르는 **FastAPI + asyncio 루프** 워커다. WSL2 Docker(`services/docker-compose.yml`)에서 구동. **책임** 1. 60초 루프로 NAS `monitor-set` 조회 → 세션 게이트. 2. 비-KRX(알파벳) 티커 skip. 3. KIS 실시간 현재가 + 일봉 OHLCV 조회 → TA 지표 계산. 4. 매수/매도 조건 평가 → 발화집합 F 구성. 5. `POST report`로 F 전체 전송(무상태 — dedup은 NAS 영속). 6. Redis heartbeat 발신(`worker:trade-monitor:heartbeat` EX45). 7. KIS 오류는 사이클/종목 단위 격리(다음 분 재시도). **경계 밖(안 함)**: dedup 상태 보관, 텔레그램 전송(NAS 담당), KST 세션/휴장 캘린더 재구현(NAS가 `session` 판정), 주문 실행. --- ## 2. 모듈 분해 | 파일 | 책임 | 인터페이스(순수/부작용) | |------|------|------| | `main.py` | FastAPI app + lifespan. `monitor_loop` + `heartbeat_loop` 스폰, `/health` | 부작용(태스크 스폰) | | `monitor.py` | 오케스트레이션 루프. monitor-set→게이트→종목순회→firing→report. 공유 `MonitorState` 갱신 | 부작용 | | `nas_client.py` | `get_monitor_set()` / `post_report(as_of, firing)` — `X-WebAI-Key` + retry | 부작용(HTTP) | | `kis_client.py` | KIS REST: `_issue_token()`(OAuth 자체 발급, 24h 캐시) + `get_quote()` + `get_daily_ohlcv()` + 0.5s throttle | 부작용(HTTP) | | `indicators.py` | `sma`, `rsi`, `avg_volume`, `highest_high` | **순수** | | `conditions.py` | `evaluate_buy(ctx, buy_params)` / `evaluate_sell(ctx, exit_params)` → `list[firing]` | **순수** | | `config.py` | `Settings` — env 로드 | 순수 | 순수 모듈(`indicators`, `conditions`)에 조건 로직을 격리해 테이블 기반 단위 테스트로 검증 가능하게 한다. HTTP·시간·Redis는 경계 모듈에만. --- ## 3. 데이터 흐름 (monitor_loop, 60초) ``` 매 사이클: ms = nas.get_monitor_set() # §5.1 state.session = ms.session if ms.session == "closed": state.hb = "market_closed"; sleep; continue # KIS 호출 0 targets = filter_krx(ms.buy_targets, ms.sell_targets) # 알파벳 티커 skip firing = [] for t in targets: # 종목 단위 try/except 격리 quote = kis.get_quote(t.ticker) # 현재가 + 당일 누적 거래량 daily = kis.get_daily_ohlcv(t.ticker, 250) # MA200·52주 고점용 ctx = build_ctx(t, quote, daily) if t is buy_target: firing += evaluate_buy(ctx, ms.buy_params) if t is sell_target: firing += evaluate_sell(ctx, ms.exit_params) if firing: state.last_alert_at = now nas.post_report(as_of=now_kst_iso, firing=firing) # §5.2 — 빈 배열도 전송(edge clear 위해) state.hb = "market_open" stats.jobs_done += 1 ``` `heartbeat_loop`(별도 15초 태스크)이 `state`를 읽어 §5.4 페이로드 발신. --- ## 4. 조건 로직 해석 (§6) 지표는 **일봉 시계열**(최신 last, 오름차순) + **실시간 현재가**(`price`) 기준. 데이터 부족(예: MA200용 200봉 미만)이면 해당 조건은 **미발화**(graceful skip). 각 firing은 `{ticker, kind, condition, price, detail}`. ### 매수 (`buy_targets`, `buy_params={rsi_oversold, breakout_vol_mult, pullback_pct}`) | condition | 발화 규칙(해석) | detail | |-----------|----------------|--------| | `buy_ma20_pullback` | `ma20>ma50>ma200`(정배열) **AND** 최근 3봉 최저가가 `ma20*(1+pullback_pct)` 이하로 접근 **AND** `price>ma20`(반등 복귀) | `ma20, ma50, ma200, recent_low` | | `buy_breakout` | `price > 직전 20봉 최고가`(당일 제외) **AND** `today_volume > breakout_vol_mult × avg_volume(20)` | `prior_high_20, vol_mult, avg_vol_20` | | `buy_rsi_bounce` | RSI(14) 시계열에서 `min(rsi[-3:]) < rsi_oversold` **AND** `rsi[-1] > rsi_oversold` **AND** `rsi[-1] > rsi[-2]`(반등). 사이클마다 재계산·무상태 | `rsi, rsi_prev, rsi_oversold` | 종합: 각 조건 독립 발화(신뢰도 가중합은 NAS/텔레그램 단계 책임 아님 — 워커는 조건 발화만). ### 매도 (`sell_targets`, `exit_params={stop_pct, take_pct, trailing_pct}`, target에 `avg_price, qty, holding_high`) | condition | 발화 규칙 | detail | |-----------|----------|--------| | `sell_stop_loss` | `(price-avg)/avg ≤ -stop_pct` | `avg_price, pnl_pct, stop_pct` | | `sell_take_profit` | `(price-avg)/avg ≥ take_pct` | `avg_price, pnl_pct, take_pct` | | `sell_trailing_stop` | `price ≤ holding_high × (1-trailing_pct)` (기본 0.10) | `holding_high, trailing_pct, drawdown_pct` | | `sell_ma_break` | `price < ma50` (추가 `price ⚠️ **운영 함정**: ai_trade와 KIS를 동시 호출하면 전용 app_key라도 KIS 계정 전체 rate limit을 공유할 수 있음. 별도 app_key로 무효화는 회피되나, 운영 시 동시 부하 모니터링 필요(Phase 7 백로그 연계). --- ## 6. heartbeat (§5.4) `_shared.heartbeat.heartbeat_loop(redis, "trade-monitor", "trader", stats, interval=15, ttl=45, state_fn=...)`. - `state_fn`이 `MonitorState`를 읽어 `state ∈ {market_open, market_closed, idle}` + `extra={"last_alert_at": ...}` 반환. - **디커플링 이유**: 루프 60초 > TTL 45초 → 인라인 발신 시 만료 갭. 15초 독립 태스크로 해소(형제 워커와 동일 구조). §5.4 필수 필드(name/kind/state/ts/last_alert_at) 충족, `jobs_done/jobs_failed`는 형제 워커처럼 superset 유지. - 초기 상태 `idle`(첫 monitor-set 조회 전). --- ## 7. 설정 (env) — `TM_` 접두사로 ai_trade와 분리 | env | 기본값 | 용도 | |-----|--------|------| | `NAS_BASE_URL` | `http://192.168.45.54:18500` | stock 백엔드 | | `WEBAI_API_KEY` | (필수) | `X-WebAI-Key` | | `REDIS_URL` | `redis://192.168.45.54:6379` | heartbeat | | `TM_KIS_APP_KEY` / `TM_KIS_APP_SECRET` | (필수) | KIS 자체 토큰 | | `TM_KIS_ACCOUNT` | (필수) | KIS 계좌 | | `TM_KIS_IS_VIRTUAL` | `0` | 실전/모의 | | `TM_LOOP_INTERVAL` | `60` | 루프 주기(초) | | `TM_CLIMAX_VOL_MULT` | `3.0` | sell_climax 임계 | --- ## 8. 에러 처리 - **monitor-set 실패**: 사이클 skip(report 안 함), heartbeat=`idle`, 다음 분 재시도. - **KIS 종목 실패**: 해당 종목만 skip(로그 warning), 나머지 종목 계속. - **report 실패**: 로그 error, 다음 사이클 신선 firing 재전송(무상태라 손실 허용). - 루프 최상위 `try/except` — 어떤 예외도 루프를 죽이지 않음(task-watcher 패턴). --- ## 9. 테스트 전략 (pytest, 시스템 Python) | 파일 | 검증 | |------|------| | `test_indicators.py` | sma/rsi/avg_volume/highest_high 수치(알려진 시계열), 데이터 부족 시 None | | `test_conditions.py` | 8개 조건 테이블 기반(발화/미발화 경계), detail 필드 | | `test_nas_client.py` | respx — monitor-set 파싱, report 페이로드, X-WebAI-Key 헤더, retry | | `test_kis_client.py` | respx — 토큰 발급/캐시, quote/daily 파싱, throttle | | `test_monitor.py` | 루프 1회(mock): closed skip, 비-KRX skip, firing 조립, last_alert_at 갱신, 종목 실패 격리 | --- ## 10. 배포 - `services/trade-monitor/Dockerfile`: task-watcher 관례 복제 — `COPY _shared /app/_shared` **필수**(빌드 컨텍스트 `.` 에서), `COPY trade-monitor/. /app/`, `PYTHONPATH=/app`, uvicorn `:8000`. - `services/docker-compose.yml`: `trade-monitor` 서비스 추가, 포트 **18715**(image-render 18714 다음), `TZ=Asia/Seoul`, KIS/WEBAI/REDIS env, healthcheck `/health`. - `services/.env`(비커밋): `TM_KIS_*`, `WEBAI_API_KEY` 실값. `.env.example`에 키만 기재. --- ## 11. 미해결 플래그 / 후속 1. **sell_climax** — ✅ 2026-07-03 holdings_intel 정합 완료(`price < day_high × climax_close_pct` + `exit_params` 파라미터화). BE 회신 기준. 2. **KIS 지표 필드 실검증** — quote의 `acml_vol`/`stck_oprc`, daily TR 응답 필드는 첫 운영 raw 캡처로 대조. 3. **`buy_ma20_pullback`·`buy_rsi_bounce` 해석** — "current candle series" 문구를 일봉 시계열로 해석. 첫 운영 4주 IC 검증 시 재조정 가능. 4. **KIS rate limit 공존** — ai_trade와 동시 부하. 전용 app_key로 토큰 무효화는 회피, 초당 호출 총량은 운영 모니터링. 5. **after 세션 시간외 시세** — `inquire-price`가 시간외 단일가를 반영하는지 첫 운영 대조.