- 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
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_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(...)가 브로커당 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장의 개별 항목 상당수가 부수적으로 정리된다.
- TR ID/설정을
path.py처럼 데이터화해서KisV류의 메서드 복붙 제거 - 트랜스포트 계층(공통 요청 헬퍼/어댑터) 한 겹 추가 — 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 . 한 번이면 실키가 원격 저장소에 올라간다. 이미 커밋되지 않았는지도 확인이 필요하다.
조치:
- 즉시 위 키/토큰을 브로커사 콘솔에서 폐기(재발급)한다.
.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에 트레일링 공백
# 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()을 호출한다(반면Kiwoom은status_code == 200체크를 하는 편이라 상대적으로 낫다). 브로커 서버가 502/503을 반환하거나 HTML 에러 페이지를 주면JSONDecodeError가 호출자에게 그대로 전파된다. - 실패 시 반환값이 전부
None(혹은[]) 이고 원인 구분이 불가능하다. "종목이 없어서 결과가 비었다"와 "네트워크 오류로 실패했다"와 "토큰 만료로 인증 실패했다"를 호출자가 구분할 방법이 없다.
조치 제안:
finestock/comm/errors.py에FinestockAPIError,FinestockAuthError,FinestockNetworkError같은 예외 계층을 만든다.API베이스에 공통 요청 헬퍼(_post_json(url, headers, body, timeout=...))를 추가해 timeout 지정, HTTP 상태 확인, JSON 파싱 실패 처리, 표준 로깅을 한 곳에서 담당하게 하고, 각 브로커는 이 헬퍼를 호출하도록 리팩터링한다.- 실패 시
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 == 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조차 자동 실행되지 않는다.
조치:
- 각 브로커의 대표 응답 JSON을 fixture로 저장하고,
requests.post/.get을 mock으로 대체해 파싱 로직만 검증하는 단위 테스트를 추가한다. BaseProvider의 각 추상 메서드에 대해 "모든 구현체가 동일한 시그니처인지" 확인하는 계약 테스트(예:inspect.signature비교)를 추가해 5장 같은 회귀를 CI에서 잡는다.- 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.PATCH3-part(semver 유사)를 쓰는 것이CHANGELOG.md와 대응시키기 쉽다.python_requires='>=3.6'/classifiers에3.6~3.9만 나열되어 있지만, 리포지토리의__pycache__에는cpython-310/311/312산출물이 있어 실제 개발/검증은 3.103.12에서 이뤄지고 있는 것으로 보인다. 코드 안에서 f-string(3.6+)과3.8 이상으로 올리고, 실제 CI로 검증한 버전만 classifiers에 남기는 것을 권장한다.dataclass(frozen=True)(3.7+)를 쓰므로 최소 버전을 3.7setup.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 정비 |