9.7 KiB
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)에서 구동.
책임
- 60초 루프로 NAS
monitor-set조회 → 세션 게이트. - 비-KRX(알파벳) 티커 skip.
- KIS 실시간 현재가 + 일봉 OHLCV 조회 → TA 지표 계산.
- 매수/매도 조건 평가 → 발화집합 F 구성.
POST report로 F 전체 전송(무상태 — dedup은 NAS 영속).- Redis heartbeat 발신(
worker:trade-monitor:heartbeatEX45). - 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<ma200이면 detail.severity="high") |
ma50, ma200, severity |
sell_climax |
휴리스틱(추후 holdings_intel 정합): today_volume ≥ climax_vol_mult × avg_volume(20) AND price < 당일시가(반전 캔들) |
vol_mult, day_open + TODO: holdings_intel 대조 |
climax_vol_mult 는 env TM_CLIMAX_VOL_MULT(기본 3.0)로 조정.
5. KIS 클라이언트 (자체 토큰)
_issue_token():POST {base}/oauth2/tokenP {grant_type, appkey, appsecret}→access_token(만료 24h). 메모리 캐시, 만료 10분 전 재발급. ai_trade와 분리된TM_KIS_APP_KEY/SECRET사용(같은 app_key 공유 시 토큰 상호 무효화 + EGW00201).get_quote(ticker):inquire-price(FHKST01010100) →stck_prpr(현재가),acml_vol(당일 누적 거래량),stck_oprc(당일 시가).get_daily_ohlcv(ticker, days=250):inquire-daily-itemchartprice(FHKST03010100) — ai_tradekis_client.py로직 복제, 오름차순.- throttle 0.5s(초당 2회) +
_throttle_lock직렬화 + 429/timeout 지수 backoff(ai_trade 패턴 재사용).
⚠️ 운영 함정: 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. 미해결 플래그 / 후속
- sell_climax 휴리스틱은 근사 — holdings_intel 원본 확보 후 정합(BE에 원본 요청).
- KIS 지표 필드 실검증 — quote의
acml_vol/stck_oprc, daily TR 응답 필드는 첫 운영 raw 캡처로 대조. buy_ma20_pullback·buy_rsi_bounce해석 — "current candle series" 문구를 일봉 시계열로 해석. 첫 운영 4주 IC 검증 시 재조정 가능.- KIS rate limit 공존 — ai_trade와 동시 부하. 전용 app_key로 토큰 무효화는 회피, 초당 호출 총량은 운영 모니터링.
- after 세션 시간외 시세 —
inquire-price가 시간외 단일가를 반영하는지 첫 운영 대조.