docs(proofline): approve KIS balance pagination spec

This commit is contained in:
Hermes Agent
2026-08-10 00:01:46 +09:00
parent bdf9761b2d
commit 701fb06362
7 changed files with 156 additions and 2 deletions
+17
View File
@@ -0,0 +1,17 @@
---
id: "ac-0001"
status: active
---
# KIS 연속 잔고 전체 조회 및 종목별 집계
## Criterion
`KISBroker.get_balance()`는 KIS 국내주식 잔고조회 최초 요청에서 빈 continuation key와 공백/미지정 `tr_cont`를 사용해야 한다. 성공 응답의 response header `tr_cont`가 `F` 또는 `M`이면 body의 `ctx_area_fk100`과 `ctx_area_nk100`을 다음 요청 query에 전달하고 후속 요청 header에 `tr_cont=N`을 사용해야 하며, `D` 또는 `E`에서 종료해야 한다. 전체 성공 페이지의 유효한 양수 `hldg_qty`는 `pdno`별로 합산한 하나의 `BalanceItem`으로 반환하고 0 이하 수량 row는 제외해야 한다. `OwnershipAwareBroker`가 KIS broker를 감싸는 경우에도 완전한 raw snapshot이 기존 projection의 입력이 되어야 하며 ownership 및 `ownership_mismatch` 정책은 변경되지 않아야 한다.
## Verification
- 성공 조건: synthetic `F → M → E` 다중 페이지 응답에서 요청별 header와 continuation query가 계약대로 이어지고, 첫 페이지와 후속 페이지의 모든 양수 보유종목 및 페이지에 걸쳐 중복된 종목의 합산 수량이 최종 결과에 포함되며 `E` 이후 추가 요청이 없어야 한다. 별도 fixture로 `D` 종료도 확인한다.
- 실패·경계 조건: 정상 단일 페이지와 검증된 빈 잔고는 추가 요청 없이 기존 결과를 반환하고, 0 이하 수량 row는 결과에서 제외되어야 한다. 실전 50건·모의 20건이라는 page-size에 의존하지 않고 response header 계약만으로 순회해야 한다.
- ownership 보존 조건: 첫 페이지의 manual holding과 후속 페이지의 strategy-owned symbol을 포함한 KIS 응답을 `OwnershipAwareBroker`에 연결했을 때 후속 페이지 종목이 projection 결과에 포함되어야 한다. 전체 raw 수량이 전략 소유수량 이상이면 성공하고, 실제로 작을 때만 기존 `ownership_mismatch`가 유지되어야 한다.
- 검사 방법: `tests/adapters/broker/test_kis.py`에서 `httpx.get`을 페이지별 response header/body fixture로 대체하여 호출 횟수, 요청 header, query key, 종료 조건과 최종 `BalanceItem` 집계를 단언하고, ownership adapter 회귀 테스트에서 완전한 delegate 결과의 projection과 기존 mismatch 경계를 단언한다.
+18
View File
@@ -0,0 +1,18 @@
---
id: "ac-0002"
status: active
---
# 불완전하거나 진행하지 않는 잔고 페이지 체인의 fail-closed 처리
## Criterion
`KISBroker.get_balance()`는 최초 페이지를 포함한 어느 페이지에서든 HTTP/transport 실패, KIS `rt_cd` 실패, malformed JSON/body/row, response header `tr_cont` 누락 또는 지원하지 않는 값, `F/M`이 다음 페이지를 지시하면서 continuation key pair가 비어 있는 상태, 또는 하나의 호출에서 이미 방문한 정규화된 `(ctx_area_fk100, ctx_area_nk100)` pair가 즉시 또는 비인접하게 다시 나타나는 상태를 만나면 누적한 `BalanceItem`을 반환해서는 안 된다. non-object row, 누락되거나 정수 변환 불가능한 `hldg_qty`, 양수 수량인데 빈 `pdno`인 row는 malformed로 처리하고, `hldg_qty <= 0` row는 기존 normalization대로 제외한다. 해당 호출은 유한한 요청 횟수 안에 기존 allow-list의 정제된 `BrokerBalanceError`로 실패해야 하며, pagination 계약 위반을 이유로 새 public error taxonomy를 추가하거나 partial snapshot을 소비자에 전달해서는 안 된다.
## Verification
- 오류 matrix: HTTP status/transport, `rt_cd != 0`, JSON decode 실패, body non-object, `output1` non-list, row non-object, `hldg_qty` 누락·non-numeric, 양수 수량과 빈 `pdno`, `tr_cont` 누락·미지원, `F/M`과 빈 key pair를 각각 독립 fixture로 검증한다.
- 진행성 조건: 즉시 반복 `A → A`와 비인접 순환 `A → B → A` key-pair fixture가 모두 유한한 요청 횟수 안에 `BrokerBalanceError`를 발생시키고 결과 list를 반환하지 않아야 한다.
- 보존 조건: `hldg_qty <= 0` row는 실패가 아니라 제외로 유지하고, 검증된 정상 빈 잔고는 실패로 오인하지 않아야 한다.
- 안전 조건: 예외 code는 기존 `BrokerBalanceError` allow-list 안에 있어야 하며 pagination/shape 계약 위반은 기존의 정제된 malformed-response 계열로 귀결되어야 한다. 오류 객체나 메시지는 token, credential, raw payload 등 민감한 값을 노출하지 않아야 한다.
- 검사 방법: `tests/adapters/broker/test_kis.py`에 오류 종류별 parameterized test와 순환 fixture를 두고 예외 code, 호출 횟수, partial-return 부재, 기존 normalization과 민감정보 비노출을 단언한다.
+17
View File
@@ -0,0 +1,17 @@
---
id: "ac-0003"
status: active
---
# 후속 페이지 보유종목의 reconciliation 정합성
## Criterion
KIS 잔고조회 후속 페이지에만 존재하는 양수 보유종목도 완전한 broker snapshot에 포함되어야 한다. 해당 종목의 portfolio-hub 수량과 holding 슬롯 수량이 broker 수량과 일치하면 `reconcile_positions()`는 `case_code=C` 경고를 만들지 않아야 하고 `reconcile_core_holdings()`는 broker flat으로 판단하거나 슬롯을 `error`로 전환하지 않아야 한다. 반대로 첫 페이지 성공 후 후속 페이지가 실패하면 두 소비자는 누적된 첫 페이지 수량으로 어떤 broker-derived decision도 만들지 않고 `BrokerBalanceError`를 전파해야 하며, B/C warning 생성·해제, broker-flat `error` 전이, unmanaged/managed-non-holding 분류 또는 slot/warning write가 발생해서는 안 된다. 성공과 실패 결과는 두 소비 경로에서 각각 독립적으로 충족해야 한다.
## Verification
- 성공 조건: 첫 페이지에는 없고 후속 페이지에만 있는 종목을 가진 synthetic KIS 응답과 그 수량에 일치하는 SnapshotStore 및 holding 슬롯을 구성했을 때 `reconcile_positions()` 결과가 정상이고 durable C warning이 없으며, 별도 독립 실행한 `reconcile_core_holdings()` 결과가 `ok`이고 슬롯이 `holding`을 유지해야 한다.
- 실패 조건: 첫 페이지에 일부 보유수량이 있고 후속 페이지에서 HTTP, KIS 또는 pagination-contract 오류가 발생하는 fixture를 두 소비자에 각각 연결했을 때 `BrokerBalanceError`가 전파되고, 첫 페이지 수량을 사용한 decision map이나 warning/slot persistence side effect가 없어야 한다. 특히 `case_code=B/C`, warning set/clear, broker-flat `error`, `unmanaged_position`, `managed_non_holding_divergence`가 생성되지 않아야 한다.
- 정책 보존 조건: 성공 fixture에서 후속 페이지 종목을 의도적으로 제거한 대조 조건은 기존 소비자 정책대로 불일치 또는 broker-flat 경로를 검출해야 하며, 이 Line이 warning 분류나 상태 전이 정책 자체를 완화해서 통과해서는 안 된다.
- 검사 방법: adapter의 다중 페이지 결과와 중간 페이지 실패를 application reconciliation fixture에 연결한 repository-local 통합 테스트를 두 소비자별로 독립 실행하고 exception, decision map, warning store, slot write 기록 및 최종 슬롯 상태를 단언한다.
+2 -2
View File
@@ -1,5 +1,5 @@
{
"schema_version": 1,
"next_line_number": 1,
"next_ac_number": 1
"next_line_number": 2,
"next_ac_number": 4
}
+53
View File
@@ -0,0 +1,53 @@
---
id: "dcy-0001"
status: confirmed
---
# KIS 잔고조회 페이지네이션 완전성
## Problem
`trex-core`의 `KISBroker.get_balance()`는 KIS 국내주식 잔고조회 응답을 한 번만 요청하고 첫 페이지의 `output1`만 반환한다. 실전 계좌는 호출당 최대 50건, 모의 계좌는 최대 20건을 반환하므로 보유 row가 이 한도를 넘으면 성공 응답이어도 후속 페이지의 종목이 누락된 불완전한 broker snapshot이 만들어진다.
이 snapshot을 사용하는 두 reconciliation 경로는 서로 독립적으로 잘못된 결과를 낼 수 있다. `reconcile_positions()`는 portfolio-hub 수량과 누락된 broker 수량을 비교해 `case_code=C` 경고를 만들 수 있고, `reconcile_core_holdings()`는 holding 종목을 broker flat으로 오인해 슬롯을 `error`로 격리할 수 있다. 문제는 reconciliation 정책 자체가 아니라 두 소비자에게 전달되는 KIS 잔고 snapshot의 완전성이다.
기대 상태는 KIS가 지시하는 모든 연속 페이지를 성공적으로 읽고 병합한 경우에만 완전한 잔고를 반환하고, 페이지 체인이 불완전하거나 진행하지 않으면 부분 잔고를 정상 결과로 반환하지 않는 것이다.
## Evidence
- GitHub issue #53, `https://github.com/fourblocks2020/trex-core/issues/53` (OPEN, 2026-08-06T14:54:08Z 갱신)은 운영 계좌에서 첫 페이지 `output1=50`, 후속 페이지 `output1=9`를 관찰했고, 두 번째 페이지의 7개 양수 보유 종목이 첫 페이지 전용 구현에서 누락됐다고 기록한다. 두 요청 모두 HTTP `200`, `rt_cd=0`이어서 인증·권한·KIS 업무 오류와 구별된다.
- `trex_core/adapters/broker/kis.py:626-689`의 `KISBroker.get_balance()`는 최초 query에 빈 `CTX_AREA_FK100`/`CTX_AREA_NK100`을 설정한 뒤 `httpx.get()`을 한 번만 호출하고 그 응답의 `output1`만 변환해 반환한다. 응답의 `ctx_area_fk100`, `ctx_area_nk100`과 response header `tr_cont`를 읽는 경로가 없다.
- 저장소의 KIS 계약 문서 `docs/broker/kis/rest/국내주식/주문/계좌/015_v1_국내주식-006_주식잔고조회.md:18-20,38,59-60,68,78-80`은 실전 최대 50건·모의 최대 20건, 최초 요청의 빈 `tr_cont`, 후속 요청의 `tr_cont=N`, 이전 응답 continuation key 재사용, response header `tr_cont`의 `F/M`(다음 데이터 있음) 및 `D/E`(마지막 데이터)를 명시한다.
- 이슈 원문의 “첫 요청에 `tr_cont=F`”와 “`KIOK0510` 등으로 마지막 페이지 판정”은 위 계약과 다르다. 최초 요청은 공백/미지정이어야 하고, 후속 페이지 존재와 종료의 정본은 `msg_cd`가 아니라 response header `tr_cont`다. `KIOK0500`/`KIOK0510`은 운영 관찰 증거로만 취급한다.
- `trex_core/application/reconciliation.py:100-199`의 `reconcile_core_holdings()`는 반환된 balance map에 holding 종목이 없으면 `broker_qty=0`으로 판단해 `holding → error`를 수행한다. `trex_core/application/reconciliation.py:202-280`의 `reconcile_positions()`는 별도로 portfolio-hub 수량과 broker 수량을 비교하고 불일치 시 `case_code=C` 경고를 기록한다.
- 현재 tree `bdf9761b2d64166abfcf1ab5699800c37c076f04`에서 `.venv/bin/python -m pytest -q tests/adapters/broker/test_kis.py tests/application/test_holding_divergence.py tests/application/test_reconciliation.py`를 실행한 결과는 `33 passed, 1 skipped`였다. 기존 `get_balance()` 테스트는 실패 응답, 정상 빈 응답, 토큰 발급 실패를 다루지만 다중 페이지, continuation 진행, 중간 페이지 실패는 검증하지 않는다.
## Scope
- AC admission 분류는 새로운 장기 active 동작을 정의하는 `create`다. 현재 ProofLine AC inventory는 비어 있고, 이 Line은 KIS 잔고 snapshot 완전성에 대한 독립 PASS/FAIL 계약을 새로 만든다.
- `KISBroker.get_balance()`는 최초 요청에서 continuation key를 비우고 `tr_cont`를 공백/미지정으로 유지하며, response header `tr_cont`가 `F` 또는 `M`이면 직전 응답의 `ctx_area_fk100`/`ctx_area_nk100`을 다음 query에 전달하고 후속 요청 header에 `tr_cont=N`을 사용한다.
- response header `tr_cont`가 `D` 또는 `E`일 때 페이지 순회를 종료한다. `msg_cd` 값은 진단 정보일 뿐 pagination 제어 계약으로 사용하지 않는다.
- 모든 성공 페이지의 `output1`을 읽고 동일 종목의 양수 `hldg_qty`를 종목별로 합산하며, 기존의 0수량 row 제외 동작을 유지한다.
- 최초 페이지를 포함한 어느 페이지에서든 HTTP, KIS `rt_cd`, JSON shape, row shape 또는 continuation 계약이 실패하면 이미 모은 부분 결과를 반환하지 않고 기존의 정제된 `BrokerBalanceError` 경계로 실패한다.
- response header가 다음 페이지를 요구하지만 continuation key가 비어 있거나 이전과 동일해 진행하지 않는 경우 무한 반복하지 않고 fail-closed 한다.
- 단일 페이지, 실전/모의 page-size와 무관한 synthetic 다중 페이지, 종목별 병합, 중간 페이지 실패, 빈/반복 continuation key, 정상 빈 잔고를 단위 테스트로 검증한다.
- 다중 페이지 결과를 사용하는 기존 reconciliation에서 후속 페이지 보유 종목이 `broker_qty=0`으로 누락되지 않는 회귀 경계를 검증한다.
## Out of Scope
- portfolio-hub 저장소 또는 API 변경
- `case_code=B/C` 분류, warning persistence, `holding → error`를 포함한 reconciliation 정책 변경
- KIS 인증정보, token cache, broker provider 또는 account ownership projection 변경
- 잔고조회 외 주문·체결·시세 API의 pagination 일반화
- `INQR_DVSN`, 보유수량 0 row 제외 등 이 Line과 무관한 기존 KIS query·정규화 정책 변경
- 운영계좌에 대한 주문, 자동 슬롯 복구, 기존 warning/error 상태의 데이터 수정
- 배포, release 또는 운영환경 검증
## Risks and Unknowns
### Risks
- 첫 페이지가 성공한 뒤 후속 페이지가 실패했을 때 부분 snapshot을 반환하면 현재 결함이 다른 형태로 유지된다. 완화책은 전체 페이지 체인이 성공한 경우에만 결과를 반환하는 atomic read 경계다.
- KIS가 “다음 데이터 있음”을 표시하면서 빈 key나 동일 key를 반복하면 무한 호출이 발생할 수 있다. 완화책은 key 존재성과 진행성을 검증하고 위반 시 fail-closed 하는 것이다.
- response body의 `msg_cd`를 종료 조건으로 고정하면 KIS 메시지 코드 변화에 취약하다. 완화책은 저장소에 기록된 공식 계약대로 response header `tr_cont`만 pagination 제어에 사용하고 메시지 코드는 진단용으로 한정하는 것이다.
- 동일 종목이 여러 row 또는 페이지에 걸쳐 나타날 수 있으므로 마지막 row 덮어쓰기는 수량을 축소할 수 있다. 완화책은 유효한 양수 수량을 종목별로 합산하고 해당 경계를 테스트하는 것이다.
+3
View File
@@ -0,0 +1,3 @@
---
id: "line-0001"
---
+46
View File
@@ -0,0 +1,46 @@
---
id: "req-0001"
status: approved
discovery: "dcy-0001"
criteria:
create:
- "ac-0001"
- "ac-0002"
- "ac-0003"
update: []
retire: []
satisfy: []
---
# KIS 잔고조회 페이지네이션 완전성
## Objective
KIS 국내주식 잔고조회에서 단일 응답 한도를 넘는 계좌도 모든 연속 페이지를 완전하게 읽어 종목별 broker 잔고 snapshot을 반환하고, 불완전한 페이지 체인을 정상 잔고로 소비하지 않도록 한다. 이를 통해 후속 페이지 보유종목이 reconciliation에서 broker flat 또는 수량 불일치로 오인되지 않게 한다.
## Scope
- `KISBroker.get_balance()`의 국내주식 잔고조회에 KIS response header와 continuation key에 따른 연속조회 동작을 적용한다.
- 모든 성공 페이지의 유효한 `output1` 보유수량을 종목별 snapshot으로 집계한다.
- 페이지 체인의 완전성과 진행성을 검증하고 불완전한 결과를 기존 broker read error 경계로 차단한다.
- KIS adapter의 단일·다중 페이지 및 오류 경계, 기존 account ownership projection, 두 reconciliation 소비 경로의 회귀를 repository-local 자동화 테스트로 검증한다.
## Constraints
- 최초 요청은 `tr_cont`를 공백/미지정으로 유지하고 continuation key를 비우며, 후속 요청만 `tr_cont=N`과 직전 응답의 key를 사용한다.
- 다음 페이지와 종료 판정은 response header `tr_cont`의 `F/M` 및 `D/E` 계약을 따르며 `msg_cd`를 제어 흐름의 정본으로 사용하지 않는다.
- 하나의 `get_balance()` 호출 동안 정규화한 `(ctx_area_fk100, ctx_area_nk100)` pair의 방문 이력을 추적하고, 이미 방문한 pair가 즉시 또는 비인접하게 다시 나타나면 실패해야 한다.
- 어느 페이지에서든 실패하면 누적된 부분 잔고를 반환하거나 broker-derived reconciliation 판단·warning/slot write에 사용하지 않는다.
- row가 object가 아니거나, `hldg_qty`가 누락되거나 정수 변환 불가능하거나, 양수 수량의 `pdno`가 비어 있으면 malformed response로 실패한다. `hldg_qty <= 0` row는 기존 normalization대로 제외한다.
- pagination 계약 위반은 기존 `BrokerBalanceError` allow-list 안의 정제된 오류로 매핑하며 새 public error taxonomy를 추가하지 않는다.
- 정상적인 단일 페이지, 검증된 빈 잔고, 0 이하 수량 row 제외, account ownership projection 및 정제된 `BrokerBalanceError` 동작을 보존한다.
- 실계좌 주문, 운영 데이터 수정 또는 외부 저장소 변경 없이 repository-local fixture로 검증 가능해야 한다.
## Non-Goals
- portfolio-hub 저장소 또는 API 변경
- reconciliation의 warning 분류, 상태 전이 또는 자동 복구 정책 변경
- KIS credential, token cache, broker provider 또는 account ownership projection 변경
- 잔고조회 외 KIS API의 pagination 일반화
- `INQR_DVSN`을 포함한 이 Line과 무관한 query·정규화 정책 변경
- 운영계좌 주문, 기존 warning/error 데이터 수정, 배포 또는 release