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

22 KiB

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_listprint("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"). 그래서 KisVget_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(...)가 브로커당 1015곳에 그대로 호출된다. 재시도, 레이트리밋, 타임아웃 정책, 목(mock) 대체를 넣으려면 그 1015곳을 전부 고쳐야 한다는 뜻이다. 트랜스포트 계층(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이 하드코딩되어 있다.

# 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 . 한 번이면 실키가 원격 저장소에 올라간다. 이미 커밋되지 않았는지도 확인이 필요하다.

조치:

  • 즉시 위 키/토큰을 브로커사 콘솔에서 폐기(재발급)한다.
  • .gitignoredebug.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에 트레일링 공백

# 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 시점에 고정됨

# 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

# 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 반환

# 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 키를 두 번 정의

# 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. 에러 처리 부재 — 트레이딩 라이브러리로서 가장 큰 리스크

거의 모든 브로커 메서드가 다음 패턴을 따른다:

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()을 호출한다(반면 Kiwoomstatus_code == 200 체크를 하는 편이라 상대적으로 낫다). 브로커 서버가 502/503을 반환하거나 HTML 에러 페이지를 주면 JSONDecodeError가 호출자에게 그대로 전파된다.
  • 실패 시 반환값이 전부 None(혹은 []) 이고 원인 구분이 불가능하다. "종목이 없어서 결과가 비었다"와 "네트워크 오류로 실패했다"와 "토큰 만료로 인증 실패했다"를 호출자가 구분할 방법이 없다.

조치 제안:

  1. finestock/comm/errors.pyFinestockAPIError, 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줄짜리 보일러플레이트를 그대로 반복한다:

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 == 200try: 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_cancelfinestock.Order 데이터클래스가 아니라 브로커 원본 응답 dict를 그대로 반환한다. LS/Kisfinestock.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.pyPrice 데이터클래스 생성 하나만 검증한다. 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_balanceprint(response.text) / print(res)). 특히 Kiwoom.get_balance가 계좌 잔고 원본 응답을 print로 stdout에 그대로 흘리는 것은 1.2와 같은 맥락의 정보 노출이다.
  • __del__에서 logger.debug(...)를 호출하는 패턴(API, LS)은 인터프리터 종료 시점에 모듈 전역이 이미 해제되어 있을 수 있어 예외를 유발할 수 있다. 일반적으로 __del__에 로깅/IO를 넣는 것은 권장되지 않는다 — 필요하다면 명시적 close()/context manager 패턴으로 대체하는 것이 안전하다.

조치: 모든 printlogger.debug/logger.info로 통일하고, 민감 데이터(잔고, 토큰, 키)는 로그에서 마스킹한다.

8. 패키징 / 배포 메타데이터

  • setup.pyinstall_requiresasyncio가 포함되어 있다. 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' / classifiers3.6~3.9만 나열되어 있지만, 리포지토리의 __pycache__에는 cpython-310/311/312 산출물이 있어 실제 개발/검증은 3.103.12에서 이뤄지고 있는 것으로 보인다. 코드 안에서 f-string(3.6+)과 dataclass(frozen=True)(3.7+)를 쓰므로 최소 버전을 3.73.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 정비