Files
finestock/doc/IMPROVEMENTS.md
T
shinalokandClaude Sonnet 5 eaf18362f4 Add NH(나무) broker integration and fix balance/holds parsing
- finestock/nh/: Nh/NhV 브로커 클래스 추가 (시세/주문/잔고/실시간 WS)
- api_factory.py, path.py: APIProvider.NH/NHV 등록, 도메인/엔드포인트 매핑
- kis.py: get_holds/get_ohlcv_min/get_index_min/get_stock_list 스텁 추가,
  oauth() Content-Type 헤더 수정
- get_balance()의 실전 디버깅으로 드러난 버그 수정:
  - Hold.total(매입금액)이 존재하지 않는 byn_amt 필드를 참조해 항상 0이던 것을
    eal_amt - eal_pls_amt로 계산하도록 수정
  - rsp_cd를 "00000" 단일 값으로만 성공 판정해 정상 응답('00218' 연속조회 중,
    '00166' 마지막 페이지 등)을 실패로 오판하던 것을 Output_0 존재 여부로 판정
  - 응답 헤더의 cts/cts_flag로 연속조회를 재귀 처리해 10건 넘는 보유종목도
    전부 합쳐서 반환하도록 구현
- doc/, tests/, example_*.py, setup.py, requirements.txt, CLAUDE.md 등 추가
- README.md에 .env 환경변수 설정 가이드 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225Lu4Fc2UpMz6QixEcNT8
2026-08-31 14:35:30 +09:00

218 lines
22 KiB
Markdown

# finestock 개선 제안
코드베이스 전반(`finestock/`, `tests/`, 루트 예제 스크립트, 패키징 파일)을 훑어보고 발견한 문제점과 개선 방향을 정리했다. 심각도가 높은 순으로 배치했다.
## 0. 아키텍처
버그/코드 스멜과는 별개로, 설계 자체를 평가하면 다음과 같다.
### 잘 된 부분
- **Facade + Factory**: `create_api(APIProvider.X)`로 브로커 하나를 받아 인증/시세/주문/실시간을 한 객체에서 다루게 한 것은 "브로커를 갈아끼워도 호출부는 그대로"라는 목표에 잘 맞는 선택이다. `APIFactory`가 브로커별 모듈을 lazy-import하는 것도 미사용 브로커의 의존성을 끌고 오지 않는 합리적인 설계다.
- **`path.py`로 도메인/엔드포인트를 데이터화**: URL 문자열이 코드 곳곳에 흩어지지 않고 `_API_PATH_[클래스명]` 한 곳에 모여 있다. `EBest(LS)`, `LSV(LS)`, `KiwoomV(Kiwoom)`처럼 실전↔모의 차이가 "도메인만 다름"인 브로커는 실제로 상속만으로 몇 줄짜리 서브클래스가 된다(`ebest.py`, `ls_v.py`, `kiwoom_v.py` 확인 완료). **이 축(도메인 차이)에 대해서는 설계 의도대로 잘 작동한다.**
- **Queue 주입 방식의 실시간 데이터 흐름**: 콜백 지옥 대신 `set_data_queue()`로 소비자가 원하는 큐를 주입받는 구조라, WS 루프와 소비자 로직이 느슨하게 결합된다. asyncio 루프 안에 비즈니스 로직을 얽어 넣지 않아도 되는 점은 장점이다.
- **frozen dataclass로 정규화된 모델**: 브로커마다 제각각인 JSON 응답을 `Price`/`Order`/`Trade` 등 불변 객체로 통일해서 반환하는 규약 자체는 일관되게 지켜지고 있다(`Kiwoom.do_order`/`do_order_cancel`만 이 규약을 깨고 raw dict를 반환하는 예외 — 5장 참고).
### 걸리는 부분
**0.1 ISP가 타입 힌트 수준에서만 존재하고, 실제로는 거꾸로 작동한다.**
`BaseProvider`가 6개 인터페이스를 전부 합친 하나의 거대 클래스라서, 브로커가 지원하지 않는 기능도 무조건 구현해야 한다. 그 결과가 `Kis.get_index_list``print("Kis not supported")`, `Kis.recv_price`/`recv_index`/... 의 빈 `pass`들이다. ISP의 원래 취지는 "클라이언트가 쓰지 않는 메서드에 의존하지 않게 하자"인데, 여기서는 "구현체가 쓸 수 없는 메서드도 강제로 구현하게" 만드는 쪽으로 뒤집혀 있다. 게다가 타입 힌트로 좁히는 것(`market_api: MarketDataProvider = full_api`)은 IDE 자동완성용일 뿐 런타임 강제력이 없어서, `TradingProvider`로 좁혀 받은 코드가 실수로 `do_order`를 호출하는 걸 막아주지 않는다. mypy 같은 정적 타입 검사가 파이프라인에 없어 이 이점조차 실제로는 활용되지 못하고 있다.
**0.2 상속 축이 하나뿐이라, 두 번째 변형 축(TR ID 차이)이 나오자 코드 중복으로 샌다.**
`path.py` 기반 상속은 "도메인만 다름" 케이스에는 잘 맞지만, KIS는 실전/모의가 **도메인도 다르고 TR ID도 다르다**. TR ID는 `path.py`처럼 데이터화되어 있지 않고 각 메서드 안에 문자열 리터럴로 박혀 있다(`header["tr_id"] = "TTTC8434R"`). 그래서 `KisV``get_balance`, `do_order`를 새로 쓰는 대신 **거의 통째로 복붙**해서 TR ID 한 줄만 바꾼 형태가 됐다(`kis_v.py`). 이는 상속 설계 자체보다 "브로커별로 달라지는 축이 URL 하나가 아니라는 것"을 처음부터 모델링하지 않은 결과다. TR ID/설정값도 `path.py`처럼 별도 딕셔너리(`_TR_IDS_["Kis"]` vs `_TR_IDS_["KisV"]`)로 빼서 메서드 본문은 공유하고 설정만 주입하는 방식이었다면 `KisV`도 지금의 `EBest`/`LSV`만큼 얇아졌을 것이다. 이건 4장에 적은 코드 중복의 근본 원인이기도 해서, 아키텍처 레벨에서 고치면 4장 항목이 상당 부분 같이 해결된다.
**0.3 HTTP/WS 클라이언트가 각 메서드에 직접 박혀 있어 교체·테스트·횡단 관심사 주입이 불가능하다.**
`requests.post(...)`가 브로커당 10~15곳에 그대로 호출된다. 재시도, 레이트리밋, 타임아웃 정책, 목(mock) 대체를 넣으려면 그 10~15곳을 전부 고쳐야 한다는 뜻이다. 트랜스포트 계층(`self._client.post(...)` 같은 얇은 어댑터)을 한 겹 두는 것만으로 테스트 가능성과 횡단 관심사 적용이 동시에 해결되는데, 지금은 그 경계가 아예 없다(6장 테스트 커버리지 부재의 근본 원인이기도 하다).
**0.4 `_init_path()`가 동적 `setattr`로 인스턴스 속성을 만들어서, "IDE 자동완성"이라는 설계 목표를 스스로 깎아먹는다.**
`API._init_path()`는 딕셔너리를 순회하며 `self.DOMAIN`, `self.CHART` 등을 런타임에 만든다(`api.py:29-32`). README가 내세우는 설계 목표 중 하나가 "타입 힌팅으로 IDE 자동완성이 되게 한다"인데, 정작 `self.DOMAIN`/`self.CHART` 같은 핵심 속성은 정적으로 선언되어 있지 않아 IDE/mypy가 이 속성들의 존재를 모른다. `ClassVar`가 선언된 설정 dataclass(브로커별 서브클래스)로 바꾸면 중앙화 이점은 유지하면서 정적 분석 이점도 되찾을 수 있다.
**0.5 동기(REST)와 비동기(WS)가 한 객체에 공존하는데 그 경계에 대한 설계가 없다.**
같은 파사드 객체가 블로킹 `requests` 호출 메서드와 `async def run()`/`recv_price` 같은 코루틴 메서드를 동시에 갖고 있다. asyncio 이벤트 루프를 돌리면서(`run()`) 그 안에서 블로킹 `get_ohlcv`를 호출하면 루프 전체가 멈춘다. 브로커 API 자체가 REST+WS 혼합이라 완전히 피하긴 어렵지만, 최소한 "REST 메서드는 블로킹이니 `asyncio.to_thread`로 감싸서 써라" 같은 경계 가이드나 헬퍼가 전혀 없다.
**0.6 실시간 연결의 회복탄력성이 설계에 아예 없다.**
`LS.run()`/`Kiwoom.run()`은 예외가 나면 그냥 `is_run = False`로 루프를 끝내버린다(재연결, 백오프, 재구독 없음). 실거래 봇의 실시간 클라이언트에서 이건 버그라기보다 "애초에 그 관심사를 다루는 레이어가 설계에 없다"는 문제라, 지금 구조 위에 패치를 얹기보다 재연결/재구독을 담당하는 별도 계층을 설계 단계에서 넣는 게 낫다.
### 아키텍처 개선 우선순위
Facade+Factory+ISP라는 큰 뼈대 자체는 목적에 맞고, "도메인 URL이 다른" 축까지는 실제로 깔끔하게 작동한다. 문제는 그 뼈대가 **한 개의 변형 축(도메인)만 상정**하고 있어서, TR ID 차이·기능 미지원·동기/비동기 혼재·트랜스포트 계층 부재 같은 나머지 축들이 전부 상속 복붙이나 빈 스텁으로 새어나가고 있다는 점이다. 다음 순서로 손대면 4~6장의 개별 항목 상당수가 부수적으로 정리된다.
1. TR ID/설정을 `path.py`처럼 데이터화해서 `KisV`류의 메서드 복붙 제거
2. 트랜스포트 계층(공통 요청 헬퍼/어댑터) 한 겹 추가 — 3장의 에러 처리 개선과 통합
3. `BaseProvider`를 강제 합성 대신, 브로커가 실제 지원하는 인터페이스만 mix-in 하는 구조로 전환
## 1. 보안 (긴급)
### 1.1 실제 API 키/토큰이 저장소에 평문으로 존재
`example.py`, `example_async.py`에 실제로 보이는 `app_key`, `app_secret`, 계좌번호, JWT 형식의 `access_token`이 하드코딩되어 있다.
```python
# example.py
app_key = "PS6HIMzSNcpfSU29Qkdlr17szGoZmPMZ3kOC"
app_secret = "fkaR4TJD3SU2MrbjIHHWbCufxIPcMAs1"
account_num = "207087079"
access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9..."
```
**문제**: 현재 `.gitignore`는 JetBrains 관련 항목만 포함하고 있어 `example.py`, `debug.log`, `venv/`, `build/`, `dist/`, `__pycache__/`, `finestock.egg-info/`를 걸러주지 않는다(`git status`에 전부 `??`로 나타남). 즉 무심코 `git add .` 한 번이면 실키가 원격 저장소에 올라간다. 이미 커밋되지 않았는지도 확인이 필요하다.
**조치**:
- 즉시 위 키/토큰을 브로커사 콘솔에서 폐기(재발급)한다.
- `.gitignore``debug.log`, `__pycache__/`, `build/`, `dist/`, `*.egg-info/`, `venv/`를 추가한다.
- 예제 파일의 실키를 `os.environ["LS_APP_KEY"]` 같은 환경변수 참조 또는 `YOUR_APP_KEY` 플레이스홀더로 교체한다.
- 저장소 히스토리에 이미 커밋된 적이 있다면 `git filter-repo`/BFG로 히스토리에서 제거하는 것도 검토한다.
### 1.2 자격증명이 디버그 로그에 그대로 남는다
`API.oauth()`(`finestock/comm/api.py:67`), `LS`의 거의 모든 TR 메서드, `Kis.get_ohlcv` 등에서 요청 헤더/바디를 통째로 `logger.debug`로 남긴다. 헤더에는 `appkey`, `appsecret`, `authorization: Bearer <token>`이 포함되어 있어 `debug.log`가 곧 자격증명 덤프가 된다. `debug.log``.gitignore`에도 없어 실수로 커밋될 위험까지 겹친다(1.1 참고).
**조치**: 로깅 전에 민감 필드를 마스킹하는 헬퍼(`_redact(headers)`)를 만들어 `appkey`, `appsecret`, `authorization` 값을 `***`로 치환한 뒤 로깅한다.
## 2. 실제 동작 버그
### 2.1 `Kis.do_order`의 매도 TR ID에 트레일링 공백
```python
# finestock/kis/kis.py:152
header["tr_id"] = "TTTC0802U" if buy_flag == finestock.ORDER_FLAG.BUY else "TTTC0801U " # ← 끝에 공백
```
매도 주문 시 TR ID가 `"TTTC0801U "`로 전송되어 KIS 서버가 이를 유효하지 않은 TR로 거부할 가능성이 높다. 실거래 주문 코드의 오타이므로 우선순위가 높다.
### 2.2 KIS 조회 메서드의 기본 인자가 import 시점에 고정됨
```python
# finestock/kis/kis.py:47
def get_ohlcv(self, code, frdate=datetime.now().strftime('%Y%m%d'), todate=datetime.now().strftime('%Y%m%d')):
```
`get_index`도 동일 패턴(`kis.py:70`). 파이썬은 함수 정의 시점에 기본값을 한 번만 평가하므로, 모듈을 import한 날짜가 그대로 굳어버린다. 프로세스를 며칠 이상 띄워두는 트레이딩 봇에서 인자를 생략하고 호출하면 실제로는 "오늘"이 아니라 "프로세스 시작일"의 시세를 조회하게 된다.
**조치**: `frdate=None`으로 받고 함수 본문에서 `frdate = frdate or datetime.now().strftime('%Y%m%d')`로 매 호출마다 평가한다.
### 2.3 `Kiwoom.get_ohlcv_min`에 중복 `return`
```python
# finestock/kiwoom/kiwoom.py:127-129
return self._get_chart_sync("ka10080", params, next_key=tr_cont_key, frdate=cts_date if cts_date.strip() else "")
return self._get_chart_sync("ka10080", params, next_key=tr_cont_key, frdate=cts_date if cts_date.strip() else "")
```
두 번째 줄은 도달 불가능한 죽은 코드. 복붙 실수로 보이며 삭제 대상.
### 2.4 `LS.get_index_list`가 항상 `None` 반환
```python
# finestock/ls/ls.py:282-299
def get_index_list(self):
...
response = requests.post(...)
res = response.json()
logger.debug(...)
# ← 여기서 끝. res를 파싱해서 return하는 코드가 없음
```
호출부는 항상 `None`을 받는다. 인터페이스(`InfoProvider.get_index_list`)의 계약을 지키지 못하고 있어 구현이 비어있는 상태에 가깝다.
### 2.5 `path.py`의 `_KIWOOM_V_`에서 `DOMAIN` 키를 두 번 정의
```python
# finestock/path.py:66-76
_KIWOOM_V_ = {
**_KIWOOM_,
"DOMAIN": "https://api.kiwoom.com", # ← 바로 아래서 덮어써짐, 의미 없는 죽은 코드
...
"DOMAIN": "https://mockapi.kiwoom.com",
"DOMAIN_WS": "wss://mockapi.kiwoom.com:10000/api/dostk/websocket",
}
```
동작 결과는 두 번째 값(`mockapi.kiwoom.com`)으로 정상 동작하지만, 첫 값과 그 위의 탐색용 주석("Wait, main DOMAIN is..." 등)이 그대로 남아있어 다음에 코드를 읽는 사람이 혼란스럽다. 결론만 남기고 정리 필요.
### 2.6 `LSV`(모의투자)가 `LS.oauth()`의 `scope: "oob"`를 그대로 상속
`finestock/ls/ls_v.py`를 보면 `DOMAIN`/`DOMAIN_WS`만 오버라이드하고 `oauth()`는 상속받는데, 모의투자 서버가 실전과 다른 scope 처리를 요구하는지 확인이 필요하다(문제가 없다면 무시해도 됨 — 다만 EBest/LS/Kiwoom 각 모의 서버별 OAuth 파라미터 차이를 점검할 가치가 있다).
## 3. 에러 처리 부재 — 트레이딩 라이브러리로서 가장 큰 리스크
거의 모든 브로커 메서드가 다음 패턴을 따른다:
```python
response = requests.post(url, headers=header, data=json.dumps(body))
res = response.json() # ← HTTP 에러/타임아웃/비-JSON 응답이면 여기서 그대로 예외 전파
if res['rsp_cd'] == "00000": # ← 키가 없으면 KeyError
...
return ohlcvs
# else: 암묵적으로 None 반환 — 실패 사유를 알 수 없음
```
- **`requests` 호출에 `timeout`이 전혀 지정되지 않았다.** 브로커 서버가 응답을 지연하면 스레드가 무기한 블록된다. 실거래 봇에서는 치명적이다. 최소 `timeout=(3, 10)` 같은 값을 전역 상수로 정해 모든 호출에 적용해야 한다.
- **HTTP 상태 코드/네트워크 예외를 확인하지 않는다.** `LS`, `Kis`의 대부분 메서드는 `response.status_code`도 안 보고 바로 `.json()`을 호출한다(반면 `Kiwoom``status_code == 200` 체크를 하는 편이라 상대적으로 낫다). 브로커 서버가 502/503을 반환하거나 HTML 에러 페이지를 주면 `JSONDecodeError`가 호출자에게 그대로 전파된다.
- **실패 시 반환값이 전부 `None`(혹은 `[]`) 이고 원인 구분이 불가능하다.** "종목이 없어서 결과가 비었다"와 "네트워크 오류로 실패했다"와 "토큰 만료로 인증 실패했다"를 호출자가 구분할 방법이 없다.
**조치 제안**:
1. `finestock/comm/errors.py``FinestockAPIError`, `FinestockAuthError`, `FinestockNetworkError` 같은 예외 계층을 만든다.
2. `API` 베이스에 공통 요청 헬퍼(`_post_json(url, headers, body, timeout=...)`)를 추가해 timeout 지정, HTTP 상태 확인, JSON 파싱 실패 처리, 표준 로깅을 한 곳에서 담당하게 하고, 각 브로커는 이 헬퍼를 호출하도록 리팩터링한다.
3. 실패 시 `None`을 반환하는 대신 예외를 던지거나, 최소한 로그 레벨을 `error`로 남겨 원인이 추적 가능하게 한다.
## 4. 심한 코드 중복 — 유지보수 비용
`finestock/ls/ls.py`의 거의 모든 메서드(`get_ohlcv`, `get_ohlcv_min`, `get_index`, `get_index_min`, `get_index_list`, `get_stock_list`, `get_news_list`, `get_condition_list`, `get_condition_price`, `get_orderbook`, `get_balance`, `get_holds`, `do_order`, `get_order_status`, `do_order_cancel`)가 다음 5줄짜리 보일러플레이트를 그대로 반복한다:
```python
response = requests.post(url, headers=header, data=json.dumps(body))
res = response.json()
logger.debug(f"[API: oauth]\n" # ← 메서드 이름과 무관하게 항상 "oauth"라고 찍힘(복붙 흔적)
f"[URL: {url}]\n"
f"[header: {header}]\n"
f"[param: {body}]\n"
f"[response: {res}]")
```
로그 라벨이 실제 호출과 무관하게 전부 `[API: oauth]`로 찍히는 것도 복붙의 흔적이며, 로그로 문제를 추적할 때 오히려 혼선을 준다.
`Kiwoom` 쪽도 `header = {..., "authorization": ..., "api-id": tr_code, "cont-yn": ..., "next-key": ...}` 패턴과 `response.status_code == 200``try: res = response.json() ...` 블록이 `_get_chart_sync`, `get_stock_list`, `get_index_list`, `get_orderbook`, `do_order`, `do_order_cancel`, `get_balance`, `get_holds`에서 거의 동일하게 반복된다.
**조치**: 브로커별로 `_request(tr_cd_or_api_id, block_key, body) -> dict` 형태의 공통 헬퍼를 만들어 URL 조립, 헤더 구성, 요청, 로깅, 에러 처리를 한 곳으로 모은다. TR별 바디 구성과 응답 파싱만 각 메서드에 남기면 코드량이 절반 이하로 줄고, 3장의 에러 처리 개선도 자연히 여기에 녹여 넣을 수 있다.
## 5. 인터페이스(계약) 불일치
`api_interface.py`가 정의한 추상 메서드 시그니처와 실제 구현이 어긋나는 곳들:
- `MarketDataProvider.get_stock_list(self, mrkt_tp: str = "0")``LS.get_stock_list(self)`는 인자를 아예 받지 않는다(`ls.py:301`). `Kiwoom.get_stock_list(self, mrkt_tp="0")`만 계약을 지킨다.
- `RealtimeProvider.recv_trade(self, code, status=True)``async` 추상 메서드인데 `LS.recv_trade(self, code)`(`ls.py:731`)는 **동기 함수**이고 `status` 인자도 없다. `await api.recv_trade(...)`로 호출하면 코루틴이 아니라서 `TypeError`가 난다.
- `Kiwoom.do_order` / `Kiwoom.do_order_cancel``finestock.Order` 데이터클래스가 아니라 브로커 원본 응답 `dict`를 그대로 반환한다. `LS`/`Kis``finestock.Order`를 반환하므로, `Kiwoom`으로 브로커를 바꾸는 순간 호출부 코드가 깨진다 — 파사드 패턴의 "브로커를 갈아끼워도 동일하게 동작"이라는 목적이 깨지는 지점이다.
- `KisV.do_order_cancle`(오타, 스텁)은 CLAUDE.md에도 이미 기록되어 있지만 아직 수정되지 않았다. `finestock.model.flag.TRADE_FLAG.CANCLE`도 동일한 오타가 enum 멤버명에 박혀 있어(`flag.py:6`) 모든 브로커 구현이 이 오타를 그대로 참조하고 있다 — 지금 고치면 파급 범위가 크므로, `CANCEL`을 별도 별칭으로 추가하고 `CANCLE`을 deprecated 처리 후 다음 메이저 버전에서 제거하는 단계적 마이그레이션을 권장한다.
**조치**: 브로커 간 치환 가능성이 핵심 가치이므로, 위 불일치들을 CI에서 잡을 수 있도록 6장의 계약 테스트를 도입하는 것이 근본적 해법이다.
## 6. 테스트 커버리지
- `tests/test_model.py``Price` 데이터클래스 생성 하나만 검증한다. `LS`/`Kis`/`Kiwoom`의 JSON 파싱 로직(`_parse_ohlcv`, `_parse_orderbook`, `_parse_real_price` 등)은 테스트가 전혀 없다 — 5장에서 지적한 회귀들이 아무 신호 없이 계속 잠재해 있던 이유이기도 하다.
- `requests`/`websockets` 호출을 `unittest.mock`으로 가로채는 테스트가 없어, 브로커가 응답 스키마를 바꿔도 감지할 방법이 없다.
- CI 설정(`.github/workflows/*.yml`)이 없어 PR/push 시 `python -m unittest discover tests`조차 자동 실행되지 않는다.
**조치**:
1. 각 브로커의 대표 응답 JSON을 fixture로 저장하고, `requests.post`/`.get`을 mock으로 대체해 파싱 로직만 검증하는 단위 테스트를 추가한다.
2. `BaseProvider`의 각 추상 메서드에 대해 "모든 구현체가 동일한 시그니처인지" 확인하는 계약 테스트(예: `inspect.signature` 비교)를 추가해 5장 같은 회귀를 CI에서 잡는다.
3. GitHub Actions로 `python -m unittest discover tests`를 push/PR마다 실행한다.
## 7. 로깅/디버그 잔재 정리
- `print()``loguru.logger`가 뒤섞여 있다(`LS.__init__``print("create LS Components")`, `Kis`/`KisV`의 여러 `print(res)`, `Kiwoom.get_balance``print(response.text)` / `print(res)`). 특히 `Kiwoom.get_balance`가 계좌 잔고 원본 응답을 `print`로 stdout에 그대로 흘리는 것은 1.2와 같은 맥락의 정보 노출이다.
- `__del__`에서 `logger.debug(...)`를 호출하는 패턴(`API`, `LS`)은 인터프리터 종료 시점에 모듈 전역이 이미 해제되어 있을 수 있어 예외를 유발할 수 있다. 일반적으로 `__del__`에 로깅/IO를 넣는 것은 권장되지 않는다 — 필요하다면 명시적 `close()`/context manager 패턴으로 대체하는 것이 안전하다.
**조치**: 모든 `print``logger.debug`/`logger.info`로 통일하고, 민감 데이터(잔고, 토큰, 키)는 로그에서 마스킹한다.
## 8. 패키징 / 배포 메타데이터
- `setup.py``install_requires``asyncio`가 포함되어 있다. `asyncio`는 Python 3.4+ 표준 라이브러리이며 PyPI 패키지로 명시하면 안 된다(과거 PyPI에 이름이 겹치는 악성 패키지가 올라온 사례도 있었다). 제거해야 한다.
- `version='1.0.1.0'`은 4-part 버전으로 PEP 440 기준 `1.0.1.0`도 파싱은 되지만 관례상 `MAJOR.MINOR.PATCH` 3-part(semver 유사)를 쓰는 것이 `CHANGELOG.md`와 대응시키기 쉽다.
- `python_requires='>=3.6'` / `classifiers``3.6~3.9`만 나열되어 있지만, 리포지토리의 `__pycache__`에는 `cpython-310/311/312` 산출물이 있어 실제 개발/검증은 3.10~3.12에서 이뤄지고 있는 것으로 보인다. 코드 안에서 f-string(3.6+)과 `dataclass(frozen=True)`(3.7+)를 쓰므로 최소 버전을 3.7~3.8 이상으로 올리고, 실제 CI로 검증한 버전만 classifiers에 남기는 것을 권장한다.
- `setup.py`만 있고 `pyproject.toml`이 없다. 최신 packaging 관례(PEP 517/518)를 따르는 `pyproject.toml` 도입을 검토할 만하다.
## 9. 문서
- `README.md`의 "2. 실시간 데이터" 섹션에서 ` ```python ` 코드 펜스가 닫히지 않은 채 "3. 타입 힌팅 활용" 섹션으로 이어지고, 그 뒤에 다시 ` ```python `이 열려 파일 끝의 ` ``` ` 한 줄과 짝이 맞지 않는다(`README.md:67~97`). 현재 GitHub에서 렌더링하면 3번 섹션 전체가 코드 블록으로 표시될 가능성이 높다.
- 목차에 `5. License` 항목이 있지만 실제 License 섹션 본문이 파일에 없다 — 링크가 깨진 앵커로 남아있다. 라이선스 파일(`LICENSE`)도 리포지토리에 없다. 배포 패키지로서 라이선스 명시가 필요하다.
- README가 LS/KIS 예제만 보여준다. 최근 추가된 Kiwoom 브로커(`finestock/kiwoom/`)에 대한 사용 예시가 없어, `CLAUDE.md`가 설명하는 4개 브로커 지원 범위와 README의 소개 범위("LS, KIS")가 어긋난다.
## 요약: 우선순위별 실행 순서
| 우선순위 | 항목 |
|---|---|
| 1 (즉시) | 1.1 노출된 실키/토큰 폐기 및 `.gitignore` 보강, 1.2 로그 마스킹 |
| 2 | 2.1 KIS 매도 TR ID 공백 버그, 2.2 KIS 기본 인자 고정 버그 |
| 3 | 0.2 TR ID/설정 데이터화, 0.3 트랜스포트 계층 도입 → 3장 공통 요청 헬퍼(timeout, 에러 처리, 예외 계층)와 통합 |
| 4 | 0.1 인터페이스 mix-in 구조 전환, 5장 인터페이스 불일치 해소 + 6장 계약/파싱 테스트 및 CI 도입 |
| 5 | 4장 중복 제거 리팩터링(0.2와 연동), 7장 로깅 정리, 0.5/0.6 동기·비동기 경계·재연결 설계 |
| 6 | 0.4 설정 객체 정적 타입화, 8장 패키징 정리, 9장 README/LICENSE 정비 |