Imported from kgcrom/cluefin (
packages/cluefin-openapi/AGENTS.md). Install upstream withnpx skills add kgcrom/cluefin --skill cluefin-openapi. Copyright stays with the author.
AGENTS.md — cluefin-openapi
Non-obvious constraints only; see the root AGENTS.md for repo-wide rules.
Dangerous integration tests
- Running the full
integrationsuite withKIWOOM_ENV=prodsubmits live market orders (the domestic order tests use market-order typetrde_tp="3") andust31302executes a real currency exchange. Run only read-only tests against prod. - KIS debug artifacts (
/tmp/cluefin-kis-debug/*.json) are written with unredacted raw response bodies on every integration response —KIS_DEBUG_ON_FAILUREonly gates printing, not the file write. _sanitize_request_context(_http_base.py) is the only redaction layer for request context in exceptions/logs: it strips headers but keepsparams/bodyas-is, so never put secrets in params/body context.
Response models
-
Never put
max_length(or any length constraint) on a response model. The portal docs state field lengths, but the live servers exceed them (2026-09-02: a Kiwoom ka90001 theme name over 20 chars made pydantic reject the whole response withstring_too_long, blanking the desk theme screen). A constraint on a response can only reject good data;tests/test_response_models_unit.pyfails if one comes back.json_schema_extrametadata is fine — it doesn't validate. -
DART 응답 본문은
DartHttpBody.parse()가 채우는result안에만 있다. 모델 최상위에list같은 필드를 선언해도 parse() 는 건드리지 않아 항상 기본값(빈 리스트)이 남고, 읽는 쪽은 예외 없이 0건을 받는다.UniqueNumber가 실제로 그랬다 — 실서버는 119,313행을 돌려주는데 CLI 는 빈 목록을 내보내고 있었다. 행은result.list로 읽는다. -
validate_kis_response는 로그를 남기지 않고KISValidationError만 던진다. 원문은 예외의response_data에 실려 있다. 같은 내용을logger.error로도 찍으면 호출부가 예외를 잡아 처리해도 에러 로그가 남고, 계좌 모듈로 넓히면 원문 속 계좌번호가 로그에 남는다 (2026-09-20 결정). 다른 KIS 모듈로 넓힐 때도 로그를 다시 넣지 말 것.
Broker server behavior the code can't show
- KIS may invalidate tokens before their stated 24h expiry —
MAX_CACHE_AGE=6hin the token manager is deliberate. KIS also rate-limits token generation to 1/min server-side. - Kiwoom can return HTTP 200 with a failing body
return_code; the_postwrapper resolves body codes before HTTP status. Never assume 200 == success. - WebSocket auth differs per broker: KIS needs a separate
approval_keyfromAuth.approve(), while Kiwoom reuses the plain access token. Don't assume symmetry. - Kiwoom's mock (
dev) domestic WebSocket supports KRX only. - Mixed dev/prod tokens are rejected server-side (Kiwoom
8031, KISEGW00123) — this is why token caches are scoped by env/app_key. - Kiwoom occasionally removes TRs from its official docs while the API keeps working (e.g. ka10009). On integration failures, check the docs list before debugging code.
- KIS 금리종합(
comp-interest, FHPST07020000)은FID_DIV_CLS_CODE에 따라 배열의 의미가 바뀐다 (2026-09-20 실측). 문서는1:해외금리지표만 적어 두었지만 실제로는0/공백 → output1·output2 모두 국내 19종,1→ output1 해외 7종 + output2 국내(뒤 8종만, 앞 10행은 필드가 한두 칸씩 밀리고 한글도 깨진다),2→ output1 에 국내 19 + 해외 7 이 온전히 온다. 문서에 없는 값이지만 전체를 주는 것은2뿐이라 CLI 기본값으로 쓴다. 배열 이름은 믿을 수 없으니 국내/해외는bcdt_code접두어(Y01/Y02)로 가른다. - The gitignored
CLAUDE.local.mdrecords the working procedure for scraping the official KIS/Kiwoom doc portals (Kiwoom's POST doc endpoints are blocked by AhnLab Eversafe; only GET works). Read it before re-deriving that.
NH PLUG (nhplug)
- Token issuance (
/oauth2/token) is live-domain only (no mock endpoint), rate-limited to 1/sec server-side, and every unnecessary re-issue triggers a security alert on the account — always go throughAuth.generate()(TokenManager cache), never call the raw endpoint in loops or retries. On 429, retry with the SAME token. - One token serves both live (
api.nhplug.com) and mock (moapi.nhplug.com) calls, which is why the nhplug token cache is scoped by app_key only (no env) — don't "fix" it to match kis/kiwoom. - nhplug's
TokenManagerdeliberately has noMAX_CACHE_AGE(no early server-side invalidation) and computes expiry fromcached_at + expires_in. - All four gbstock 시세 APIs (
/gbstock/quote/v1/*) are live-domain only. moapi rejectscurrentwithIGW40019 "종목코드(iem_cd)를 확인해주세요"for every ticker format — a misleading message that means "not provided on mock", not a bad code (2026-08-22 실측). - gbstock quote responses return the stock name as
iem_nm, while the spec declareskor_name(current,period) /hts_kor_isnm(symbolIndexFxPeriod). Both are modelled; readiem_nm. - The portal spec backend is KIS-portal-style JSON:
/api/apis/public/api-list/{groupId}→/api/apis/guide/tr/{apiId}→/api/apis/guide/tr/property/{trId}(no auth needed). Asset-class specs are also public athttps://www.nhplug.com/openapi-docs/<slug>/openapi.json(the declared source of truth; slugs: common·krstock·gbstock·krfuture·gbfuture·krbond·krgold).
Kiwoom scope
- Kiwoom US-stock (overseas) support is Python-only; the sibling
cluefin-openapi-tspackage'soverseas-*files are KIS, not Kiwoom.
Conventions that are easy to mis-infer
- The three
TokenManagerclasses (kis/kiwoom/nhplug) are copy-pasted, not shared — mirror cache-behavior changes by hand in all three. - Unit-test styles are per-broker and not interchangeable: Kiwoom uses the table-driven
EndpointCase/run_post_caseharness (tests/kiwoom/_helpers.py), KIS uses JSON fixture case files (tests/kis/*_cases.json). - Integration skip helpers encode different meanings:
real_account_only= permanently unsupported on mock;skip_if_env_blocked= transient account/market state. Mixing them up masks real regressions. .env.testmust be loaded at module import (collection) time, not inside a fixture — module-levelskipifs readKIWOOM_ENVduring collection.- Both integration suites add an autouse
time.sleep(1)between tests on top of the in-client rate limiter; the limiter alone is not enough against live throttling. examples/*.ipynb노트북은 커밋 전에 output·execution_count 를 지운다 (출력에 계좌번호가 섞인다). 워크스페이스 루트에서:uv run --with jupyter jupyter nbconvert --clear-output --inplace packages/cluefin-openapi/examples/<노트북>.ipynb