- 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
6.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project overview
finestock is a Python package unifying Korean stock brokerage OpenAPIs (EBest, LS, KIS, Kiwoom, NH — live and mock/simulated variants) behind one facade object. It wraps each broker's REST/WebSocket API and normalizes results into shared dataclasses (Price, OrderBook, Account, Order, Trade, ...).
Commands
pip install -e . # install package in editable mode (requirements: websockets, requests, loguru)
python -m unittest discover tests # run tests (unittest, not pytest — no pytest config in repo)
python -m unittest tests.test_model -v # run a single test module
There is no lint/format tooling configured in the repo.
Architecture
Facade + Factory + ISP
finestock.create_api(APIProvider.X) (thin wrapper around APIFactory.create_api, in finestock/api_factory.py) lazy-imports and instantiates one broker class. Every broker class multiply-inherits BaseProvider (finestock/comm/api_interface.py), a union of segregated ABC interfaces:
AuthenticationProvider— oauth/token handlingMarketDataProvider— price/OHLCV/index/orderbook queriesTradingProvider— order placement/cancellationRealtimeProvider— WebSocket subscribe/unsubscribe (recv_price,recv_orderbook, etc.)AccountProvider— balance/holdingsInfoProvider— stock/index master lists
Callers narrow the fat facade object to one interface via type hints (market_api: MarketDataProvider = full_api) purely for IDE autocomplete — there is no runtime restriction. When adding a method, add it to the relevant interface in api_interface.py first, then implement it in each concrete broker.
Class hierarchy per broker
Each broker directory (finestock/{ebest,ls,kis,kiwoom,nh}/) has a real/live class and, in most cases, a mock-trading (*V) subclass that only overrides DOMAIN/DOMAIN_WS (via finestock/path.py) or a handful of TR IDs:
finestock.comm.api.API— shared base (finestock/comm/api.py): holdsapp_key/app_secret/access_token, genericheadersdict, genericoauth()(client_credentials POST), andset_data_queue/add_datafor realtime fan-out.LS(API)— the fullest, canonical implementation (finestock/ls/ls.py, ~860 lines): TR-code-based REST calls (t8410,t8407,t8452, ...) plus a WebSocketconnect/run/recv_*loop keyed bytr_cd.EBest(LS)— EBest's API is wire-compatible with LS, so it inherits everything and only swapsDOMAIN/DOMAIN_WSviapath.py.LSV(LS)— LS mock trading; same overrides pattern.
Kis(API)(finestock/kis/kis.py) — KIS usestr_idheader-based REST TRs.KisV(Kis)— mock trading; overridesget_balance/do_orderwith VTS-prefixedtr_ids (VTTC...vsTTTC...).
Kiwoom(API)(finestock/kiwoom/kiwoom.py, ~850 lines) — Kiwoom's REST+WS shape differs most from the others (api-idheader instead oftr_cd/tr_id,trnm/REGWS subscription protocol).KiwoomV(Kiwoom)— mock trading,DOMAIN/DOMAIN_WSswapped tomockapi.kiwoom.com.
Nh(API)(finestock/nh/nh.py) — NH투자증권(나무/Namuh) uses a uniformPOST {"Input_0": {...}}→{rsp_cd, Output_0[, Output_1, Output_2], message}envelope for every REST TR, auth viaAuthorization/x-client-id/x-client-secretheaders (set_oauth_infooverridden to set the latter two). No dedicated 호가/지수 TRs —get_orderbookreuses thecurrentPriceTR'saskp1..10/bidp1..10fields, andget_index/get_index_list/get_stock_listare unimplemented stubs (지수 TR 없음; 종목은 REST가 아니라.mst마스터 파일로만 제공).oauth()always targets the fixed live domain (OAUTH_DOMAINinpath.py) even forNhV, since 접근토큰발급 is live-only regardless of which domain trades run against.NhV(Nh)— mock trading (moapi.nhplug.com); no method overrides needed, onlyDOMAIN/DOMAIN_WSswapped viapath.py.
finestock/path.py centralizes all per-broker base URLs and endpoint path fragments in dict constants (_LS_, _KIS_, _KIWOOM_, ...) merged into _API_PATH_, keyed by class name; API._init_path() reads _API_PATH_[self.api_type] and sets each entry as an instance attribute (so self.DOMAIN, self.CHART, self.ORDER, etc. exist post-__init__). When adding a broker or endpoint, edit path.py, not the broker class, unless the URL needs runtime logic.
Data models (finestock/model/)
All public dataclasses are frozen (@dataclass(frozen=True)) and re-exported through finestock/model/__init__.py and top-level finestock/__init__.py. Broker methods parse raw JSON/TR responses and construct these dataclasses directly (e.g. finestock.Price(...), finestock.Order(...)) rather than returning raw dicts — new broker code should follow the same convention. Price.from_values / Price.from_series are the two supported construction paths for Price beyond the raw constructor.
Realtime data flow
Realtime WebSocket data does not use callbacks; a caller injects a queue.Queue-like object via set_data_queue() (in comm/api.py), and broker WS loops (LS.run, Kiwoom.run) push parsed messages onto it via add_data/add_price/add_trade/add_orderbook. Broker-specific TR/type codes in the incoming WS frame determine how a message is parsed and dispatched.
Conventions worth knowing
- Continuation/pagination in REST calls follows a broker-specific
cts_date/cts_time/tr_cont_key(LS, Kiwoom) orCTX_AREA_FK100/NK100(KIS) pattern — recursive calls withtime.sleep()between pages are the existing pattern for LS'sget_ohlcv. - Method names are standardized across brokers per
CHANGELOG.md(PEP 8, single-underscore private prefix,do_order_cancelnotdo_order_cancle) — butKisV.do_order_cancle(kis_v.py) still uses the old misspelled name and is a stub (pass); don't assume it matches the interface'sdo_order_cancelwithout checking. debug.logat the repo root is loguru output, not source.