# 2026-05-07 키움 REST API 전수 감사 + budget 기능 도입

코디 작업. 관리자님이 "삼성전자 100만원 매수" 같은 금액 기반 자연어 매매를 요청한 게 발단. 작업 중 발견된 키움 API 호출부의 사전 이슈들을 PDF 공식 명세 기반으로 전수 정정.

## 1차 작업: budget(예산) 기반 매매 도입

자연어 "100만원어치 매수" 처리. 키움 API에는 금액 입력 필드 자체가 없어서 우리가 환산해야 함.

**결정 사양:**
- 환산 가격: **매도1호가** (매수), **매수1호가** (매도)
- 슬리피지 마진: **0%** (예산 100% 사용)
- 소수점 매매: **안 함** (정수 주식, 버림)
- 예산 < 1주 가격: **거부** (`BUDGET_TOO_SMALL`)

**구현:**
- `orders/guards.py` `convert_budget_to_qty()` 신규
- `orders/handler.py` `propose_trade()` `budget` 파라미터 추가, CLI `--budget` 플래그
- `orders/card.py` 환산 정보 노출 ("예산 1,000,000원 → 매도1호가 75,100원 기준 환산, 잔액 23,700원")
- `skills/order-trading/SKILL.md` 페이로드 추출 규칙·예시 4개 추가
- 회귀 테스트 17건 추가 (총 111건 통과)

**키움 발주는 그대로:** 환산된 정수 qty + `trde_tp=3` 시장가로 진행. 환산용 가격은 키움에 안 감, 우리 내부 계산용.

## 2차 작업: ka10004 호가 API 사전 이슈 발견·수정

dry-run 검증하다 발견. ka10004(주식호가요청)이 처음부터 잘못된 endpoint(`/api/dostk/acnt`)로 가서 1504 에러로 항상 실패하고 있었음. ledger 36건 어디에도 호가 데이터 들어간 카드 없었음. budget 도입 전부터 있던 이슈.

**원인:** `kiwoom_client._call()` 이 모든 TR을 `ENDPOINT_ACNT` 로 보냄. ka10004는 시세 카테고리 → `/api/dostk/mrkcond` 사용해야 함.

**부수 효과:** ka10004 정상화로 시장가 카드의 호가창·평균체결가·슬리피지 표시도 처음으로 작동.

## 3차 작업: 키움 PDF 공식 명세 전수 대조

관리자님이 키움 PDF 제공 (`~/.openclaw/docs/키움 REST API 문서.pdf` — 528쪽). 코드의 모든 키움 호출(10개 TR)을 명세와 1:1 대조.

**PDF 페이지 매핑:**
- ka10001(주식기본정보) p.15, ka10004(호가) p.24, ka10099(종목리스트) p.229, ka10170(당일매매일지) p.241
- kt00001(예수금) p.377, kt00004(계좌평가) p.385, kt00007(체결내역) p.391, kt00018(잔고내역) p.420
- kt10000(매수) p.423, kt10001(매도) p.425

### 발견 사항

| 항목 | 코드 추측 | 명세 정답 | 영향 |
|---|---|---|---|
| **ka10001 상한가** | `uplmtprice`/`uplmt_pric`/`upper_limit` | **`upl_pric`** | ±30% 가격 가드가 0/0 으로 무의미하게 통과하고 있었음 |
| **ka10001 하한가** | `lwlmtprice`/`lwlmt_pric`/`lower_limit` | **`lst_pric`** | 동일 |
| **ka10001 거래정지** | `trde_susp_yn`/`halt_yn` 시도 | **명세에 없음** | 별도 TR 확인 필요 (지금은 보수적 False) |
| **ka10004 1호가** | `sel_fpr_bid_1` 같은 suffix 패턴 | **`sel_fpr_bid`** (suffix 없음) | (ka10004 endpoint 수정과 함께 정정) |
| **ka10004 2~10호가** | 같은 suffix 패턴 | **`sel_{N}th_pre_bid`** | (정정) |
| **ka10099 nxtEnable** | 미사용 (`_nxt_eligible` 하드코딩 True) | 명세에 존재 | 향후 NXT 가드 정확화에 활용 |
| **kt10000/kt10001 rt_cd** | `rt_cd` 변수 시도 후 fallback | 명세는 `return_code`만 | 동작은 함, 정리 잔여 |
| **그 외 6개 TR** | | | 모두 일치 |

### 코드 수정

`orders/datasource.py` `_safe_quote`:
```python
out = {
    'cur_price': abs(_to_int(raw.get('cur_prc'))),
    'prev_close': abs(_to_int(raw.get('base_pric'))),  # 기준가 = 전일종가
    'upper_limit': abs(_to_int(raw.get('upl_pric'))),  # 상한가
    'lower_limit': abs(_to_int(raw.get('lst_pric'))),  # 하한가
    'halt': False,  # ka10001 명세에 없음 — 별도 TR 보강 예정
}
```

검증:
- 삼성전자(005930): cur=273,500 / upper=345,500 / lower=186,500 — 정상
- 상한가 위 매수 시도(400,000) → `PRICE_ABOVE_UPPER` 거부 (이전엔 통과했을 것)

## 잔여 보강 항목

- ~~**#11** kt10000/kt10001 응답 `ord_seq_no` fallback 제거, `rt_cd` → `return_code` 변수명 통일~~ ✅ 2026-05-07 완료. `kiwoom_order.py:111-125` 정리 + 회귀 테스트 6건(`tests/test_kiwoom_order.py`) 신규. ledger `response_summary` 키도 `return_code`+`return_msg` 명시.
- ~~**#13b** list 응답 페이지네이션 안전판~~ ✅ 2026-05-07 완료. 명세상 list 반환 TR 응답 헤더에 cont-yn/next-key 정의돼 있는데 _call이 헤더 무시하고 1페이지만 받던 문제. `kiwoom_client._call_paginated(label, tr_id, body, list_field, ...)` 헬퍼 신규. **적용:** get_positions(kt00018) / get_trade_journal(ka10170) / get_order_executions(kt00007). cont-yn=Y + next-key 자동 후속 호출, list_field 누적, max_pages=50 safety cap. 첫 페이지 8005 토큰 만료 자동 재발급. 현재 4계좌 종목 30개 환경에선 cont-yn=N으로 누락 사고 없지만 **미래 종목 100+ 도달 시 안전판**. 회귀 테스트 8건(`tests/test_pagination.py`) 신규. 총 144건 통과.
- ~~**#12** ka10099 `nxtEnable` 활용 + 거래정지 별도 TR 보강~~ ✅ 2026-05-07 완료. 결과:
  - **`_nxt_eligible` 캐시 lookup 도입.** `kiwoom_client.py`에 `lookup_stock_meta(code)` 신규, `refresh_stock_codes`가 `nxt_enable`/`state`/`order_warning` 같이 저장하는 v2 스키마. `datasource._nxt_eligible(symbol, quote)`가 캐시 lookup 후 보수적 fallback. 캐시 갱신 후 4264 종목 중 3634이 NXT 미상장으로 판명 — 이전엔 모든 종목 True 통과로 NXT 시간대 매수 시도 시 키움이 사후 거부했을 것. 이제 가드 단계 사전 거부.
  - **거래정지(halt) 단독 TR 부재 확인.** ka10001/ka10099 등 REST 응답에 거래정지 boolean 플래그 없음. 실시간 websocket `1h` (VI 발동/해제) 만 존재 — OpenClaw 범위 밖. **사후 broker reject가 최종 안전판**으로 결정. ka10099의 `state`/`order_warning` 활용한 위험 종목 경고 카드는 후속 안건.
  - 회귀 테스트 5건(`tests/test_nxt_eligible.py`) 신규. 총 122건 통과.
  - **캐시 갱신 명령** (스키마 v2 적용 위해 1회 필요): `python3 ~/.openclaw/agents/stock/workspace/scripts/kiwoom_client.py refresh-codes`
- ~~**#13** 위험 종목 카드 경고~~ ✅ 2026-05-07 완료. 등급별 차등 정책(거부+경고). `guards.evaluate_stock_state(stock_meta)` 신규. handler가 propose_trade에서 호출. card.format_card에 `state_warning` 줄 추가. 회귀 테스트 14건(`tests/test_stock_state.py`). **거부 정책:** orderWarning ∈ {2 정리매매, 4 투자위험} OR state에 '거래정지'·'정리매매'. **경고 정책:** orderWarning ∈ {1 ETF주의, 3 단기과열, 5 투자경과} OR state에 '관리종목'. 거부 우선. 실 캐시 기준 거부 ~110+ 종목 (대부분 관리종목+거래정지 결합), 경고 ~175 종목.

## 코디 자동 메모리에도 등록

- `~/.claude/projects/-Users-snowoyh--openclaw/memory/reference_kiwoom_endpoints.md` 신규 — TR_ID 별 endpoint 도메인 매핑(`acnt`/`stkinfo`/`mrkcond`/`ordr`), ka10004 호가 필드 함정, 공식 PDF 위치
