Files
ai-trade/services/trade-monitor/DESIGN.md
gahusb 5dbb11ac83 fix(trade-monitor): sell_climax holdings_intel 정합
BE 회신(holdings_intel.py:109-118)에 맞춰 반전 기준을
price<day_open → price<day_high×climax_close_pct(윗꼬리)로 변경.
- kis_client.get_quote에 day_high(stck_hgpr) 추가
- monitor._build_ctx가 day_high를 ctx로 전달
- climax_vol_x·climax_close_pct를 monitor-set exit_params에서 읽기
  (fallback: TM_CLIMAX_VOL_MULT/0.97)
- 테스트 36/36 (climax exit_params 2건 추가)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N83vbXEA8h83GMXQcg8fxD
2026-07-03 11:15:27 +09:00

9.9 KiB
Raw Blame History

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<ma200이면 detail.severity="high") ma50, ma200, severity
sell_climax holdings_intel 정합: today_volume ≥ climax_vol_x × avg_volume(20) AND price < day_high × climax_close_pct(윗꼬리) vol_mult, day_high, climax_close_pct

climax_vol_x(기본 3.0)·climax_close_pct(기본 0.97)는 monitor-set exit_params에서 읽음(BE 중앙화, main ed17193). 없으면 env TM_CLIMAX_VOL_MULT fallback. day_high는 KIS quote stck_hgpr(당일 세션 누적 고가).


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_trade kis_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_fnMonitorState를 읽어 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가 시간외 단일가를 반영하는지 첫 운영 대조.