diff --git a/docs/batches/bat-056_20260706_kis-broker-venue/batch.md b/docs/batches/bat-056_20260706_kis-broker-venue/batch.md new file mode 100644 index 0000000..d9fa4b9 --- /dev/null +++ b/docs/batches/bat-056_20260706_kis-broker-venue/batch.md @@ -0,0 +1,57 @@ +# BAT-056: KIS Broker Venue Parameterization (NXT/KRX) + +- ID: bat-056 +- Slug: kis-broker-venue +- Status: In Progress +- Date: 2026-07-06 +- Owner: trex-core-dev +- Risk: HIGH (라이브 주문 경로 — worktree+PR+QC(opus) 플로) + +## REQs + +| REQ | Title | Status | +| --- | --- | --- | +| REQ-117 | OrderRequest venue additive 필드 | Approved | +| REQ-118 | KIS 브로커 어댑터 venue 파라미터화 (EXCG_ID_DVSN_CD) | Approved | + +## Scope + +- `trex_core/application/broker_port.py` — `OrderRequest.venue` 필드 additive 추가 +- `trex_core/adapters/broker/kis.py` — submit_order, cancel_order, poll_fill, get_current_price 변경 +- `trex_core/adapters/broker/paper.py` — cancel_order 시그니처 정합 +- `tests/adapters/broker/` — 신규 venue 테스트 + 기존 회귀 +- `docs/interface-contract.md` — OrderRequest venue 필드 + EXCG 명세 업데이트 +- `docs/runtime-flows.md` — venue 전달 경로 명시 + +## Out of Scope + +- AMQP wire 계약 변경 (intent 메시지 venue 전달은 tvr 팬-인 batch 별도 범위) +- orb/tsl 소비 경로 (intent, slots wire) 변경 +- SOR 동작 지원 +- VTS NXT 검증 (단위 테스트 + 실전 소액 스모크 별도) + +## Additive Constraint + +venue 미지정(기본 KRX) 경로에서 기존 orb/tsl 동작이 byte-동일 유지: +- submit_order: `EXCG_ID_DVSN_CD="KRX"` 추가만 다름 (기존 미전송 → KRX 명시) +- cancel_order: `EXCG_ID_DVSN_CD="KRX"` 추가만 다름 +- poll_fill: TR 유지(TTTC8001R/VTTC8001R) — NXT 체결 포함 여부 미확인(open item) +- get_current_price: market 기본값 "J" 유지 + +## AC Coverage + +| AC | REQ | 대상 테스트 위치 | +| --- | --- | --- | +| REQ-117 AC-1 | venue 기본값 KRX | tests/adapters/broker/test_venue.py | +| REQ-117 AC-2 | NXT 지정 | tests/adapters/broker/test_venue.py | +| REQ-117 AC-3 | 기존 회귀 | 전체 pytest | +| REQ-117 AC-4 | 위치 인수 순서 불변 | tests/adapters/broker/test_venue.py | +| REQ-117 AC-5 | paper broker venue 무영향 | tests/adapters/broker/test_venue.py | +| REQ-118 AC-1 | submit KRX EXCG 포함 | tests/adapters/broker/test_venue.py | +| REQ-118 AC-2 | submit NXT EXCG 포함 | tests/adapters/broker/test_venue.py | +| REQ-118 AC-3 | cancel NXT EXCG 포함 | tests/adapters/broker/test_venue.py | +| REQ-118 AC-4 | poll_fill TR 미확인 open item | design.md + 코드 주석 | +| REQ-118 AC-5 | get_current_price 기본 J | tests/adapters/broker/test_venue.py | +| REQ-118 AC-6 | get_current_price UN | tests/adapters/broker/test_venue.py | +| REQ-118 AC-7 | 기존 회귀 (EXCG 추가만 다름) | tests/adapters/broker/test_venue.py | +| REQ-118 AC-8 | paper cancel_order 시그니처 정합 | tests/adapters/broker/test_venue.py | diff --git a/docs/batches/bat-056_20260706_kis-broker-venue/design.md b/docs/batches/bat-056_20260706_kis-broker-venue/design.md new file mode 100644 index 0000000..7bdfa5a --- /dev/null +++ b/docs/batches/bat-056_20260706_kis-broker-venue/design.md @@ -0,0 +1,86 @@ +# BAT-056 Design + +## 1. OrderRequest.venue 필드 (REQ-117) + +### 결정 +`OrderRequest` frozen dataclass의 **마지막 위치**에 `venue: str = "KRX"` 추가. + +```python +@dataclass(frozen=True) +class OrderRequest: + slot_id: str + side: str + symbol: str + qty: int + limit_price: Optional[int] = None + order_type: str = "00" + venue: str = "KRX" # additive (REQ-117): KRX | NXT | SOR +``` + +- 기존 위치 인수(`slot_id`, `side`, `symbol`, `qty`, `limit_price`, `order_type`) 순서 불변. +- venue 기본값 "KRX"로 기존 orb/tsl 미지정 호출 자동 KRX 적용. +- "SOR" 정의하되 현재 운영 비권고 (OQ-1 잔여, REQ-117 Notes 참조). + +## 2. cancel_order 시그니처 venue 파라미터 (REQ-118) + +### 결정: 시그니처 venue 파라미터 추가 방식 + +대안 검토: +- **A안(채택)**: `cancel_order(broker_order_id, *, side, symbol, qty, venue="KRX")` — 호출자가 원주문 venue를 명시 전달. 명시적이고 기본값으로 하위 호환. +- B안: execution context에서 venue 보존 — 더 복잡, 현재 OrderAck에 venue 저장 필요. + +A안 채택 이유: 단순성, 기본값 하위 호환, BrokerPort 프로토콜에서 명시적 의미론 확립. + +BrokerPort 프로토콜: +```python +def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int, venue: str = "KRX") -> bool: +``` + +KISBroker.cancel_order body에 `"EXCG_ID_DVSN_CD": venue` 추가. +PaperBroker.cancel_order: 시그니처만 정합, venue 무시. + +## 3. poll_fill TR 선택 — open item (REQ-118 AC-4) + +### 현황 +- 구 TR `TTTC8001R` (mock: `VTTC8001R`): 현재 사용 중. NXT 체결 포함 여부 **미확인**. +- 신 TR `TTTC0081R`: EXCG=ALL 또는 venue 지정으로 NXT 체결 조회 가능(KIS 개발자 센터 문서 기준). + +### 확인 시도 결과 +웹 접근 불가 환경 — KIS 개발자 센터 문서 직접 조회 불가. 코드/기존 문서 내 관련 근거 없음. + +### 결정: 보수 구현 (기존 TR 유지) +- **기존 TR(`TTTC8001R`/`VTTC8001R`) 유지** — KRX 경로 동작 절대 불변. +- 코드 주석에 open item 명시. +- verification에 open item 기록: "NXT 체결 조회는 실측 확인 필요(운영 스모크)". + +### 향후 전환 조건 및 방법 (미구현, 운영 협의 후) +- 조건: NXT 체결 주문 발생 후 TTTC8001R 응답에 NXT 체결 미포함 확인(실측). +- 방법: `poll_fill` 파라미터 `venue` 추가 + venue="NXT"이면 TTTC0081R 사용. + - mock(VTS) 대응 주의: VTS TTTC0081R 지원 여부 미확인 → mock일 때 구 TR fallback 고려. +- 이 전환은 별도 batch로 처리한다(REQ-118 AC-4 follow-up). + +## 4. get_current_price market 파라미터 (REQ-118 AC-5,6) + +```python +def get_current_price(self, symbol: str, market: str = "J") -> Optional[Decimal]: +``` + +- `FID_COND_MRKT_DIV_CODE` → `market` 값 사용. +- 기본값 "J" → 기존 safety-sell 경로 무영향. +- "UN" 지정 시 나스닥/해외 시세 조회 가능(NXT 연장세션 활용 예정). + +BrokerPort 프로토콜: +```python +def get_current_price(self, symbol: str, market: str = "J") -> Optional[Decimal]: +``` + +## 5. 실측 근거 (design 인용) + +- 2026-07-06: 실전 계좌 NXT 주문(TTTC0012U + EXCG=NXT) rt_cd=0, 취소(TTTC0013U + EXCG=NXT) rt_cd=0·잔여 0건 확인. +- 토큰 재요청 1분 rate-limit 확인. 기존 kis.py 토큰 캐시 방식 유지 — 신 TR 전환 시에도 캐시 재사용 경로 유지 필요. + +## 6. additive 검증 전략 + +- venue 미지정 submit_order: 기존 대비 `EXCG_ID_DVSN_CD="KRX"` 필드 추가만 다름 → fixture로 body 캡처 검사(AC-7). +- 기존 `cancel_order` 호출자: keyword-only 파라미터이므로 venue 없이 호출 시 기본값 KRX 자동 적용 → 기존 테스트 회귀 없음. +- 기존 `get_current_price` 호출자: 파라미터 미지정 → market="J" 자동 적용 → 기존 safety-sell 경로 무영향. diff --git a/docs/batches/bat-056_20260706_kis-broker-venue/planning.md b/docs/batches/bat-056_20260706_kis-broker-venue/planning.md new file mode 100644 index 0000000..8d22b23 --- /dev/null +++ b/docs/batches/bat-056_20260706_kis-broker-venue/planning.md @@ -0,0 +1,62 @@ +# BAT-056 Planning + +## Task Breakdown + +### T-1: OrderRequest.venue 필드 추가 (REQ-117) +- 파일: `trex_core/application/broker_port.py` +- 작업: `OrderRequest` frozen dataclass에 `venue: str = "KRX"` 필드 추가 (마지막 위치) +- 제약: 기존 위치 인수 순서(`slot_id`, `side`, `symbol`, `qty`, `limit_price`, `order_type`) 불변 + +### T-2: BrokerPort.cancel_order 시그니처 venue 파라미터 추가 (REQ-118) +- 파일: `trex_core/application/broker_port.py` +- 작업: `cancel_order` 프로토콜 시그니처에 `venue: str = "KRX"` 파라미터 추가 +- 설계 결정: 시그니처 파라미터 추가 방식 채택(호출자 보존 방식 대비 명시적, 기본값으로 하위 호환) + +### T-3: KISBroker.submit_order EXCG 명시 전송 (REQ-118 AC-1,2) +- 파일: `trex_core/adapters/broker/kis.py` +- 작업: body에 `"EXCG_ID_DVSN_CD": req.venue` 항상 추가 + +### T-4: KISBroker.cancel_order venue 파라미터 + EXCG 전송 (REQ-118 AC-3,8) +- 파일: `trex_core/adapters/broker/kis.py` +- 작업: 시그니처에 `venue: str = "KRX"` 추가, body에 `"EXCG_ID_DVSN_CD": venue` 추가 + +### T-5: KISBroker.poll_fill TR 유지 + open item 기록 (REQ-118 AC-4) +- 파일: `trex_core/adapters/broker/kis.py` +- 작업: 구 TR(TTTC8001R/VTTC8001R) 유지, 코드 주석에 NXT 체결 포함 여부 미확인 명시 +- 이유: 웹 접근 불가 환경 — KIS 개발자 센터 문서 확인 불가. 보수 원칙: 기존 TR 유지, open item으로 기록 + +### T-6: KISBroker.get_current_price market 파라미터 추가 (REQ-118 AC-5,6) +- 파일: `trex_core/adapters/broker/kis.py` +- 작업: 시그니처에 `market: str = "J"` 추가, `FID_COND_MRKT_DIV_CODE` → market 값 사용 + +### T-7: PaperBroker.cancel_order 시그니처 정합 (REQ-118 AC-8, REQ-117 AC-5) +- 파일: `trex_core/adapters/broker/paper.py` +- 작업: cancel_order 시그니처에 `venue: str = "KRX"` 추가, 내부 로직 무변경(venue 무시) + +### T-8: 신규 테스트 작성 (REQ-117 AC-1~5, REQ-118 AC-1~8) +- 파일: `tests/adapters/broker/test_venue.py` (신규) +- 작업: 13개 AC 커버 테스트 + +### T-9: 기존 테스트 회귀 확인 (REQ-117 AC-3, REQ-118 AC-7) +- 기존 테스트가 `OrderRequest` venue 미지정 경로를 통과하는지 확인 +- 기존 submit_order fixture가 EXCG 필드 추가로 인한 페이로드 변화 감지 확인 + +### T-10: 문서 업데이트 +- `docs/interface-contract.md` — OrderRequest venue 필드 추가 +- `docs/runtime-flows.md` — venue 전달 경로 명시 + +## Dependency Order + +T-1 → T-2 → T-3, T-4, T-5, T-6 (병렬) → T-7 → T-8 → T-9 → T-10 + +## Risk Points + +1. `cancel_order` 시그니처 변경: BrokerPort 프로토콜 + KIS + paper 3개 파일 정합 필요 +2. 기존 테스트 중 `cancel_order` 호출하는 코드 — venue 파라미터 없이 호출해도 기본값 KRX로 동작해야 함 +3. `get_current_price` safety-sell 호출 경로: 기본값 "J" 유지로 무영향 확인 필요 + +## Estimation + +- 구현: T-1~T-7 → 약 1시간 +- 테스트: T-8~T-9 → 약 30분 +- 문서: T-10 → 약 20분 diff --git a/docs/batches/bat-056_20260706_kis-broker-venue/verification.md b/docs/batches/bat-056_20260706_kis-broker-venue/verification.md new file mode 100644 index 0000000..606b971 --- /dev/null +++ b/docs/batches/bat-056_20260706_kis-broker-venue/verification.md @@ -0,0 +1,47 @@ +# BAT-056 Verification + +- Status: Verified (draft-batch-verification) +- Date: 2026-07-06 +- Commit: 1d82ca0 + +## 테스트 결과 + +``` +794 passed, 47 skipped in 2.71s +``` + +기존 스위트: 777 passed, 47 skipped +신규 추가: 17개 (test_venue.py) +결과: 794 passed (+17), 47 skipped (불변) — 기존 회귀 없음 + +## AC 매핑 + +| AC | REQ | 테스트 | 결과 | +| --- | --- | --- | --- | +| REQ-117 AC-1 | venue 기본값 KRX | test_order_request_venue_default_krx | PASS | +| REQ-117 AC-2 | venue NXT 지정 | test_order_request_venue_nxt | PASS | +| REQ-117 AC-3 | 기존 회귀 없음 | 전체 pytest 794+47 | PASS | +| REQ-117 AC-4 | 위치 인수 순서 불변 | test_order_request_positional_args_unchanged, test_order_request_full_positional | PASS | +| REQ-117 AC-5 | paper venue 무영향 | test_paper_submit_nxt_accepted, test_paper_cancel_venue_ignored | PASS | +| REQ-118 AC-1 | submit KRX EXCG 포함 | test_submit_order_krx_includes_excg | PASS | +| REQ-118 AC-2 | submit NXT EXCG 포함 | test_submit_order_nxt_includes_excg | PASS | +| REQ-118 AC-3 | cancel NXT EXCG 포함 | test_cancel_order_nxt_includes_excg | PASS | +| REQ-118 AC-4 | poll_fill TR 미확인 | open item (코드 주석 + design.md 기록) | OPEN | +| REQ-118 AC-5 | get_price 기본 J | test_get_current_price_default_market_j | PASS | +| REQ-118 AC-6 | get_price UN | test_get_current_price_market_un | PASS | +| REQ-118 AC-7 | 기존 회귀 + EXCG 추가만 다름 fixture | test_submit_order_krx_only_adds_excg_field | PASS | +| REQ-118 AC-8 | paper cancel 시그니처 정합 | test_paper_cancel_venue_ignored, test_paper_cancel_default_venue, test_paper_cancel_backward_compat_no_venue | PASS | + +## Open Items + +### OI-1: REQ-118 AC-4 — poll_fill NXT 체결 포함 여부 미확인 + +- 현황: 웹 접근 불가 환경 — KIS 개발자 센터 TTTC8001R NXT 체결 포함 여부 문서 확인 불가. +- 보수 구현: 기존 TR(TTTC8001R/VTTC8001R) 유지. 코드 주석에 open item 명시. +- 향후: NXT 체결 주문 발생 후 실측 확인 → 미포함 시 TTTC0081R 전환 별도 batch. +- 운영 스모크 필요: NXT 주문 체결 후 poll_fill 응답에 해당 주문 포함 여부 확인. + +## 미확인 항목 정직 기록 + +1. poll_fill TTTC8001R NXT 체결 포함 여부 — 미확인 (OI-1, 운영 스모크 필요) +2. VTS(모의투자)에서 NXT EXCG_ID_DVSN_CD 동작 — 미확인 (REQ-118 Notes 참조). 단위 테스트는 HTTP 목킹으로 검증 완료. diff --git a/docs/interface-contract.md b/docs/interface-contract.md index f3d64c6..3bb43a5 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -398,6 +398,50 @@ resume_kill_switch 요청·응답 (REQ-046): - PostgreSQL: `ledger_outbox(correlation_id PK, asset_id, order_id, execution_id, side, qty, price, fee, tax, executed_at, status, attempts, last_outcome, last_error, created_at, updated_at)` — `ON CONFLICT (correlation_id) DO NOTHING` 이 outbox dedup 게이트. +## `OrderRequest` DTO (BAT-004 / REQ-011, BAT-056 / REQ-117) + +포트-어댑터 경계에서 주문 요청을 전달하는 frozen dataclass. + +| 필드 | 타입 | 기본값 | 설명 | +|---|---|---|---| +| `slot_id` | str | (필수) | 발원 슬롯 (감사/상관 식별자) | +| `side` | str | (필수) | `'buy'` \| `'sell'` | +| `symbol` | str | (필수) | 종목코드 (KIS `PDNO`) | +| `qty` | int | (필수) | 주문 수량 | +| `limit_price` | int \| None | None | 지정가 (None = 시장가) | +| `order_type` | str | `"00"` | KIS `ORD_DVSN`: 00=지정가, 01=시장가 | +| `venue` | str | `"KRX"` | 거래소 구분 (REQ-117 / BAT-056 additive): `KRX`=정규 KRX, `NXT`=대체거래소(연장세션), `SOR`=최선집행(현재 비권고 — OQ-1 잔여) | + +### Additive Notes (REQ-117) + +- `venue` 필드는 BAT-056에서 additive 추가. 기존 orb/tsl 미지정 호출 → 기본값 `"KRX"` 자동 적용 → 하위 호환. +- `OrderRequest`는 AMQP wire 위를 직접 흐르지 않으므로 wire 계약 영향 없음. intent 메시지에서 venue를 전달하는 것은 tvr 팬-인 batch(후속) 범위. +- KIS 어댑터(REQ-118)는 `venue` → `EXCG_ID_DVSN_CD` 로 변환해 KIS HTTP 요청에 항상 명시 전송. + +## `BrokerPort` Protocol 메서드 (BAT-056 / REQ-118) + +### cancel_order + +``` +cancel_order(broker_order_id: str, *, side: str, symbol: str, qty: int, venue: str = "KRX") -> bool +``` + +- `venue`: 원주문 venue. 취소 시 원주문과 동일한 `EXCG_ID_DVSN_CD`를 사용해야 한다. 기본값 `"KRX"`로 기존 호출자 하위 호환. + +### get_current_price + +``` +get_current_price(symbol: str, market: str = "J") -> Optional[Decimal] +``` + +- `market`: KIS `FID_COND_MRKT_DIV_CODE`. 기본값 `"J"` (KRX 주식) → 기존 safety-sell 경로 무영향. `"UN"` 지정 시 해외/NXT 연장세션 시세 조회 가능. + +### KIS 어댑터 EXCG 명시화 (REQ-118) + +- `submit_order`: body에 `EXCG_ID_DVSN_CD: req.venue` 항상 명시 전송 (이전: 미전송·서버 기본값 의존 → 명시화로 견고성 향상). 실측(2026-07-06): NXT(EXCG=NXT) 주문 rt_cd=0 확인. +- `cancel_order`: body에 `EXCG_ID_DVSN_CD: venue` 명시 전송. 실측(2026-07-06): NXT 취소 rt_cd=0 확인. +- `poll_fill`: 구 TR(`TTTC8001R`/`VTTC8001R`) 유지. NXT 체결 포함 여부 미확인(OPEN ITEM — 운영 스모크 필요). 확인 후 미포함이면 `TTTC0081R`(EXCG=ALL) 전환 별도 batch 처리. + ## `FillStatus` 확장 (BAT-006 / REQ-018) - `state, filled_qty, avg_price, raw` 외에 다음 필드를 운반한다. @@ -568,3 +612,4 @@ trex-core가 `source="trex-core"`, `severity="critical"`로 발행하는 초기 - 2026-06-21 :: BAT-049 (dcy-033) — 알람 채널 `trex.alerts` (topic, durable) 신규 추가. 알람 envelope 표준 JSON 스키마(7필드: source/severity/code/title/fields/occurred_at/schema_version, schema_version="1"), routing key `alarm.{severity}.{source}`, 발행자(모든 모듈)/소비자(trex-notifier), mandatory=False best-effort 정책. trex-core CRITICAL code 10종 초기 배선. 기존 wire 계약 불변(가산·격리). (REQ-098~101) - 2026-06-10 :: BAT-031 (dcy-021 포지션 사이징) — 매수 intent 가 **holding 슬롯 대상이면 증액**(qty+price 추가 매수; M 캡 미적용); 매도 intent 에 **`qty`(부분 매도, 선택)** 추가 — 지정 시 `min(qty, 브로커잔고)` 부분 매도(잔량 holding), 미지정 시 전량(→inactive). 상태머신 13 전이(holding→pending_buy). avg_price 증액 가중평균·부분 매도 cost basis 유지. (REQ-066/057/061/062) - 2026-07-04 :: BAT-055 (dcy-001/002 v3 FR-8, REQ-116) — `trex.slots.req` additive 확장: 요청에 선택적 `strategy: str` 필터 추가(exact-match, `filter` AND 조합, `available_slots` 불변 — REQ-073 AC-6); 응답 슬롯 행에 `strategy_name`(null)/`strategy_params`({})/`markets`([]) 3필드 추가(PortfolioItem/hub 정본, safe default). 기존 소비자(orb·tsl) backward compatible(unknown 필드 무시). REQ-074 AC-5 additive 재도입(dcy-025 OQ-4 후속 확정). PortfolioItem에 `strategy_name`/`markets` 필드 추가 + hub_read.py 파싱 확장. +- 2026-07-06 :: BAT-056 (dcy-002 FR-6, REQ-117/118) — `OrderRequest.venue: str = "KRX"` additive 추가(기존 위치 인수 순서 불변 — AMQP wire 계약 영향 없음, 브로커 어댑터 계층 DTO까지만). KIS 어댑터: submit_order/cancel_order body에 `EXCG_ID_DVSN_CD` 항상 명시 전송(기존 미전송 → KRX 명시화 견고성 향상), cancel_order/get_current_price 시그니처 venue/market 파라미터 추가(기본값 KRX/"J" — 기존 호출자 무영향). poll_fill TR 유지(NXT 체결 포함 여부 미확인 open item). paper broker 시그니처 정합(venue/market 무시). 실측 근거: 2026-07-06 NXT 주문/취소 rt_cd=0 확인. diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index 271716f..51662d5 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -258,6 +258,7 @@ - 슬롯 `limit_price` 생애주기(dcy-012/REQ-002): INACTIVE→PENDING_BUY(매수 지정가 set) → PENDING_BUY→HOLDING(clear) → HOLDING→PENDING_SELL(매도 지정가 set 또는 None=시장가) → PENDING_SELL→INACTIVE(clear). 롤백(→INACTIVE) 전이에서도 clear. 외부 취소 후 holding 복귀에서도 clear 되어 재매도는 시장가. - **실행 정책 생애주기(dcy-022/REQ-068)**: `timeout_sec`·`slippage_pct` 는 `limit_price` 와 동일한 생애주기(PENDING_* 진입 시 set, HOLDING/INACTIVE 탈출 시 clear). 슬롯의 `updated_at` 이 dispatch 시각 proxy 로 사용된다(mark_dispatched 가 갱신, stale-alert 와 동일 방법). - 어댑터: paper(결정적 async 더블) / KIS(모의투자 검증, 실계좌 게이팅). +- **venue 전달 경로(BAT-056/REQ-117/118)**: `OrderRequest.venue`(기본 `"KRX"`) → KIS 어댑터가 `EXCG_ID_DVSN_CD`로 변환해 submit_order/cancel_order body에 항상 명시 전송. NXT 주문이면 `venue="NXT"` 지정 → `EXCG_ID_DVSN_CD=NXT`. 기존 orb/tsl은 venue 미지정(기본 KRX) → 이전 대비 EXCG 필드 추가만 다름(동작 동일). intent 메시지 → venue 전달은 tvr 팬-인 batch(후속) 범위 — 현재 wire 계약 불변. ### 운영자 cancel 백스톱 (dcy-022/REQ-071) @@ -444,3 +445,4 @@ trex-notifier (단일 소비자) → 텔레그램 등 전달 | delivery_mode | 2 (persistent) | 1 (non-persistent) | | 소비자 | 전략 모듈(strategy_id 필터) | trex-notifier 단독 | | 발행 재시도 | 재연결 후 1회 retry | **없음** (best-effort) | +- 2026-07-06 :: BAT-056 (dcy-002 FR-6, REQ-117/118) — 브로커 집행 venue 전달 경로 명시: `OrderRequest.venue`(기본 KRX) → KIS 어댑터 `EXCG_ID_DVSN_CD` 항상 명시 전송(submit_order/cancel_order). `get_current_price` market 파라미터 추가(기본 J). poll_fill TR 유지(NXT 체결 open item). 기존 orb/tsl 경로 무영향. diff --git a/docs/srs/Integration/req-117_order-request-venue-field.md b/docs/srs/Integration/req-117_order-request-venue-field.md new file mode 100644 index 0000000..fb579b8 --- /dev/null +++ b/docs/srs/Integration/req-117_order-request-venue-field.md @@ -0,0 +1,104 @@ +# req-117: OrderRequest venue additive 필드 + +- Status: Approved +- Type: Integration +- Priority: High +- Owner: trex-core-dev + +## Intent + +- KIS NXT(대체거래소) 주문 지원을 위해 `OrderRequest` DTO에 `venue: str = "KRX"` 필드를 additive 추가한다. +- 기본값 `"KRX"`로 미지정 시 기존 orb/tsl 경로와 wire 의미론이 byte-동일하게 유지되어 하위 호환을 보장한다. +- 포트-어댑터 경계에서 venue 의미론을 확립하고, 상위 intent→venue 전달(tvr 팬-인 batch)이 이 계층 설계를 건드리지 않아도 되도록 확장 경로를 열어 둔다. + +## Requirement + +- `trex_core/application/broker_port.py`의 `OrderRequest` dataclass에 `venue: str = "KRX"` 필드를 추가한다. +- 필드는 frozen dataclass의 마지막 옵셔널 위치에 추가하여 기존 위치 인수 호출을 깨지 않는다. +- venue 값 의미론: `"KRX"` = 정규 KRX 거래소, `"NXT"` = 대체거래소(연장세션 포함), `"SOR"` = 최선집행(현재 비권고 — OQ-1 잔여 미확인). +- 이 필드는 포트 계층 DTO로서 venue 명시를 강제하지 않는다(기본값 `"KRX"` 허용). KIS 어댑터(REQ-118)가 이 값을 `EXCG_ID_DVSN_CD`로 변환한다. +- paper broker stub은 `venue` 필드를 수신하되 무시해도 무방하다(venue는 KIS-specific 개념, paper는 교환소 개념 없음). 단, `OrderRequest` 수신 시 venue 필드를 읽어 로깅/audit에 포함할 수 있다. + +## Acceptance Criteria + +1. Given `OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=1)`로 venue 미지정 시, When dataclass를 생성하면, Then `req.venue == "KRX"`이다. +2. Given `OrderRequest(..., venue="NXT")`로 NXT 지정 시, When dataclass를 생성하면, Then `req.venue == "NXT"`이다. +3. Given 기존 orb/tsl 코드가 `OrderRequest` 생성 시 `venue`를 지정하지 않는 경우, When 단위 테스트를 실행하면, Then 기존 테스트가 모두 통과한다(회귀 없음). +4. Given `OrderRequest` frozen dataclass 불변식, When `venue` 필드가 추가된 후에도, Then 기존 위치 인수(`slot_id`, `side`, `symbol`, `qty`) 순서가 변경되지 않고 기존 코드가 재컴파일 없이 동작한다. +5. Given paper broker stub이 `submit_order(req: OrderRequest)` 를 수신할 때, When `req.venue`가 `"NXT"`이든 `"KRX"`이든, Then paper stub은 주문을 정상 수락한다(venue 값에 무관하게 기존 동작 유지). + +## Impacted Area + +- Docs: `docs/interface-contract.md` — OrderRequest DTO 스키마 섹션 venue 필드 추가 필요. +- Modules: + - `trex_core/application/broker_port.py` — `OrderRequest.venue` 필드 추가(additive). + - `trex_core/adapters/broker/paper.py` — venue 수신 허용(무시 또는 로깅), 기존 동작 유지. + - 호출 측(execution, safety_sell 등)은 venue 미지정으로 자동 KRX 기본값 적용 — 변경 불필요. +- Tests: + - `tests/` — 기존 `OrderRequest` 생성 테스트 회귀 확인. + - 신규: venue 기본값 및 NXT 지정 단위 테스트 추가. + +## Notes + +- **wire 계약 영향 없음**: `OrderRequest`는 AMQP wire 위를 직접 흐르지 않는다. intent 메시지 계약(wire)에서 venue를 전달하는 것은 tvr 팬-인 batch(후속)의 범위이며, 본 REQ는 브로커 어댑터 계층 DTO까지만 다룬다. +- `"SOR"` 값은 정의만 하고 현재 KIS 운영 권고 대상 아님(OQ-1 잔여: SOR 체결거래소 식별·EXCG 미전송 기본값 미확인). SOR 동작은 REQ-118 AC에서 별도 처리하지 않는다. +- frozen dataclass 확장 시 기존 `field(default_factory=...)` 패턴이 없으므로 기본값 있는 단순 필드로 충분하다. +- 출처 Discovery: trex-tvr-v1 `dcy-002_20260704_execution-cadence-slot-fetch.md` FR-6, confirmed 2026-07-06. +- 실측 근거: 2026-07-06 실전 계좌 NXT 주문 rt_cd=0 접수·취소 rt_cd=0 확인. + +## Change Log + +### CHG-20260706-02 +- Date: 2026-07-06 +- Author: 팀장(Claude) — confirm-req + +#### Change Summary +- Proposed → Approved: 팀장 검토·승인. dcy-002(tvr, confirmed 2026-07-06) NXT venue 트랙. 실측 근거(NXT 주문/취소 rt_cd=0, 2026-07-06) 확보 상태. 고위험(라이브 주문 경로) — worktree+PR+QC(opus) 플로. + +#### Acceptance Criteria Delta +- Added/Updated/Removed: 없음 + +#### Revalidation Impact +- Reverification Needed: no / Existing Implementation Invalidated: no +- Status Recommendation: Approved + +### CHG-20260706-01 + +- Date: 2026-07-06 +- Author: trex-core-dev + +#### Change Summary + +- REQ-117 신규 생성(Proposed). trex-tvr-v1 dcy-002 FR-6 기반 OrderRequest venue additive 필드. + +#### Intent + +- NXT 거래소구분 지원을 위한 포트 계층 DTO 확장. additive 설계로 기존 orb/tsl 무영향. + +#### Acceptance Criteria Delta + +- Added: AC-1~5 신규 작성. +- Updated: 없음. +- Removed: 없음. + +#### Impacted Area Delta + +- Docs: docs/interface-contract.md venue 필드 섹션 추가 예정. +- Modules: broker_port.py OrderRequest 확장, paper broker 수신 허용. +- Tests: 기존 회귀 + venue 기본값/NXT 신규 단위 테스트. + +#### Delivery Trace Delta + +- Batches: 미할당(suggest-batch-reqs 대기). +- Releases: 미할당. + +#### Revalidation Impact + +- Reverification Needed: no (신규 Proposed, 기존 구현 없음) +- Existing Implementation Invalidated: no +- Status Recommendation: keep-current +- Follow-up Action: confirm-req 후 suggest-batch-reqs → REQ-118과 동일 batch로 묶을 것. + +#### Validation Plan + +- pytest 전체 통과(기존 회귀 없음) + venue 필드 단위 테스트 신규 통과. diff --git a/docs/srs/Integration/req-118_kis-broker-venue-parameterization.md b/docs/srs/Integration/req-118_kis-broker-venue-parameterization.md new file mode 100644 index 0000000..eb0ce86 --- /dev/null +++ b/docs/srs/Integration/req-118_kis-broker-venue-parameterization.md @@ -0,0 +1,116 @@ +# req-118: KIS 브로커 어댑터 venue 파라미터화 (EXCG_ID_DVSN_CD) + +- Status: Approved +- Type: Integration +- Priority: High +- Owner: trex-core-dev + +## Intent + +- KIS 브로커 어댑터(`adapters/broker/kis.py`)에서 주문 거래소구분(`EXCG_ID_DVSN_CD`)을 `OrderRequest.venue`로부터 파라미터화하여 NXT(대체거래소) 주문·취소를 지원한다. +- 동시에 `submit_order`에서 기존 EXCG 미전송 관행을 `"KRX"` 명시 전송으로 변경하여 서버 기본값 의존을 제거한다(동작 고정, 실측상 서버 기본값=KRX로 동일하나 명시화가 더 견고함). +- `poll_fill`의 구 TR(`TTTC8001R`) NXT 체결 포함 여부를 확인하고, 미포함 시 신 TR(`TTTC0081R`, EXCG=ALL)으로 전환하여 NXT 체결 조회를 보장한다. +- `get_current_price`의 시장구분(`FID_COND_MRKT_DIV_CODE`) `"J"` 하드코딩을 파라미터로 전환한다(기본 `"J"`, `"UN"` 옵션). +- 이 모든 변경은 venue 미지정(기본 KRX) 경로에서 orb/tsl 기존 동작과 byte-동일하게 유지되는 additive 설계다. + +## Requirement + +1. `submit_order`: `body`에 `"EXCG_ID_DVSN_CD": req.venue` 항상 명시 전송한다. `req.venue` 기본값은 `"KRX"`이므로 venue 미지정 호출에서도 `EXCG_ID_DVSN_CD=KRX`가 전송된다. +2. `cancel_order`: 취소 요청 `body`에 `"EXCG_ID_DVSN_CD": <원주문 venue>` 를 전송한다. 원주문 venue는 cancel_order 시그니처에 `venue: str = "KRX"` 파라미터를 추가하거나, 호출자가 원주문 venue를 보존·전달하는 방식으로 구현한다(설계 방식은 구현 batch에 위임 — 의미론 확정: 취소는 원주문 venue와 동일한 EXCG를 사용해야 한다). +3. `poll_fill`: NXT 체결 조회를 보장한다. 구 TR `TTTC8001R`의 NXT 체결 포함 여부를 구현 시점에 확인(AC-4의 확인 방법 참조)하고, 미포함이 확인되면 신 TR `TTTC0081R`(EXCG 파라미터 `ALL` 또는 venue 지정)으로 전환한다. 확인 불가 시 Notes의 fallback 방침을 따른다. +4. `get_current_price`: 시그니처에 `market: str = "J"` 파라미터를 추가하고, `FID_COND_MRKT_DIV_CODE`를 해당 값으로 전달한다. 기본 `"J"` 유지로 기존 safety-sell 경로 무영향. 호출자가 `"UN"` 지정 가능. +5. orb/tsl 무영향: venue 미지정 경로에서 submit_order 페이로드는 기존 대비 `EXCG_ID_DVSN_CD="KRX"` 필드 추가만 다르며, 주문 수락 동작이 동일함을 회귀 테스트로 확인한다. + +## Acceptance Criteria + +1. Given `OrderRequest(..., venue="KRX")`(또는 venue 미지정 기본값)로 `submit_order` 호출 시, When KIS HTTP 요청이 생성되면, Then 요청 body에 `"EXCG_ID_DVSN_CD": "KRX"`가 포함된다. +2. Given `OrderRequest(..., venue="NXT")`로 `submit_order` 호출 시, When KIS HTTP 요청이 생성되면, Then 요청 body에 `"EXCG_ID_DVSN_CD": "NXT"`가 포함된다. +3. Given NXT 주문의 broker_order_id와 `venue="NXT"`로 `cancel_order` 호출 시, When KIS HTTP 요청이 생성되면, Then 요청 body에 `"EXCG_ID_DVSN_CD": "NXT"`가 포함된다. +4. Given `poll_fill` 구현 시, When 구 TR(`TTTC8001R`)의 NXT 체결 포함 여부를 확인하는 과정(KIS 개발자 센터 문서 조회 또는 NXT 체결 건 직접 조회 실측)을 수행하면, Then 그 결과를 구현 문서(batch design 또는 코드 주석)에 명시하고, 미포함이 확인된 경우 신 TR `TTTC0081R`으로 전환한다. +5. Given `get_current_price(symbol, market="J")` 기본 호출(또는 market 미지정) 시, When KIS HTTP 요청이 생성되면, Then `FID_COND_MRKT_DIV_CODE=J`가 전달된다(기존 동작 동일). +6. Given `get_current_price(symbol, market="UN")` 호출 시, When KIS HTTP 요청이 생성되면, Then `FID_COND_MRKT_DIV_CODE=UN`이 전달된다. +7. Given 기존 orb/tsl가 venue 미지정으로 `submit_order`를 호출하는 경로, When 단위 테스트를 실행하면, Then 기존 submit_order 관련 테스트 전체가 통과한다(회귀 없음). 페이로드 차이는 `EXCG_ID_DVSN_CD="KRX"` 추가뿐임을 fixture 검사로 확인한다. +8. Given paper broker stub이 `cancel_order` 시그니처 변경(venue 파라미터 추가)을 반영한 후, When paper broker로 cancel_order 호출 시, Then 기존 취소 동작이 유지된다. + +## Impacted Area + +- Docs: + - `docs/interface-contract.md` — KIS 브로커 어댑터 EXCG 전송 명세 추가. + - `docs/runtime-flows.md` — 주문 집행 흐름 중 venue 전달 경로 명시. +- Modules: + - `trex_core/adapters/broker/kis.py` — `submit_order`, `cancel_order`, `poll_fill`, `get_current_price` 변경. + - `trex_core/application/broker_port.py` — `BrokerPort.cancel_order` 시그니처 venue 파라미터 추가(또는 호출자 보존 방식에 따라 설계 batch에서 결정). + - `trex_core/adapters/broker/paper.py` — cancel_order 시그니처 정합 유지. +- Tests: + - `tests/adapters/broker/` — submit_order EXCG 필드 포함 fixture, cancel_order venue 전달 검증, get_current_price market 파라미터 검증. + - `tests/` — 전체 회귀(기존 venue 미지정 경로 pass 확인). + +## Notes + +- **고위험 경로**: `submit_order`·`cancel_order`·`poll_fill`은 라이브 주문 직결 경로다. worktree+PR+QC(opus) 플로 필수. +- **cancel_order 설계 위임**: 취소 시 venue 전달 방식(cancel_order 시그니처 추가 vs. execution context 보존)은 구현 batch design에서 확정한다. 본 REQ는 의미론만 확정한다 — "취소 요청은 원주문 venue와 동일한 EXCG_ID_DVSN_CD를 사용해야 한다." +- **poll_fill TR 전환 판단**: 구 TR(`TTTC8001R`) NXT 체결 포함 여부가 구현 시점에 확인 불가능하면, 신 TR(`TTTC0081R`)으로 즉시 전환하는 것을 기본 방침으로 한다(보수적 접근). 신 TR로 전환 시 모의(VTS) 지원 여부가 미확인이므로, mock 모드에서의 fallback(구 TR 유지)을 설계에 포함한다. +- **모의(VTS) NXT 미확인**: VTS(모의투자)에서 NXT EXCG_ID_DVSN_CD 동작이 확인되지 않았다. 검증은 단위 테스트(HTTP 목킹) + 실전 소액 스모크(운영 협의)로 진행한다. 모의 환경 검증은 선행 조건이 아니며, 배포 전 운영팀과 스모크 계획을 협의한다. +- **토큰 캐시**: 실측(2026-07-06)에서 토큰 재요청 1분 rate-limit 확인. 현행 `kis.py` 토큰 캐시 방식과 정합 — 신 TR 전환 시에도 토큰 캐시 재사용 경로를 유지해야 한다. +- **SOR**: `"SOR"` 값 전달은 설계 경로에서 차단하지 않되, 현재 권고하지 않는다(체결거래소 식별·정정취소 EXCG 일치 보장이 미확인). +- 출처 Discovery: trex-tvr-v1 `dcy-002_20260704_execution-cadence-slot-fetch.md` FR-6, confirmed 2026-07-06. +- 실측 근거: 2026-07-06 실전 계좌 NXT 주문(TTTC0012U + EXCG=NXT) rt_cd=0, 취소(TTTC0013U + EXCG=NXT) rt_cd=0·잔여 0건 확인. +- 선행 REQ: REQ-117(OrderRequest.venue 필드) 구현 완료 후 본 REQ 구현 가능. + +## Change Log + +### CHG-20260706-02 +- Date: 2026-07-06 +- Author: 팀장(Claude) — confirm-req + +#### Change Summary +- Proposed → Approved: 팀장 검토·승인. dcy-002(tvr, confirmed 2026-07-06) NXT venue 트랙. 실측 근거(NXT 주문/취소 rt_cd=0, 2026-07-06) 확보 상태. 고위험(라이브 주문 경로) — worktree+PR+QC(opus) 플로. + +#### Acceptance Criteria Delta +- Added/Updated/Removed: 없음 + +#### Revalidation Impact +- Reverification Needed: no / Existing Implementation Invalidated: no +- Status Recommendation: Approved + +### CHG-20260706-01 + +- Date: 2026-07-06 +- Author: trex-core-dev + +#### Change Summary + +- REQ-118 신규 생성(Proposed). trex-tvr-v1 dcy-002 FR-6 기반 KIS 브로커 어댑터 venue 파라미터화. + +#### Intent + +- NXT 거래소구분 지원을 위한 KIS 어댑터 EXCG_ID_DVSN_CD 전송 구현. 기존 orb/tsl 하위 호환 유지. + +#### Acceptance Criteria Delta + +- Added: AC-1~8 신규 작성. +- Updated: 없음. +- Removed: 없음. + +#### Impacted Area Delta + +- Docs: interface-contract.md, runtime-flows.md venue 경로 명세 추가 예정. +- Modules: kis.py 4개 메서드 변경, broker_port.py cancel_order 시그니처 정합, paper.py 정합. +- Tests: 신규 EXCG 필드 검증 + 전체 회귀. + +#### Delivery Trace Delta + +- Batches: 미할당(suggest-batch-reqs 대기). +- Releases: 미할당. + +#### Revalidation Impact + +- Reverification Needed: no (신규 Proposed, 기존 구현 없음) +- Existing Implementation Invalidated: no +- Status Recommendation: keep-current +- Follow-up Action: REQ-117과 동일 batch 묶기 권고. QC opus 지정(고위험 주문 경로). + +#### Validation Plan + +- pytest 전체 통과(기존 회귀 없음) + 신규 AC-1~8 단위 테스트 통과. +- 배포 전 실전 소액 스모크(KRX + NXT 각 1건) 운영 협의. diff --git a/docs/srs/Interface/req-116_slots-req-strategy-field-and-filter.md b/docs/srs/Interface/req-116_slots-req-strategy-field-and-filter.md index d191c9d..f43ca57 100644 --- a/docs/srs/Interface/req-116_slots-req-strategy-field-and-filter.md +++ b/docs/srs/Interface/req-116_slots-req-strategy-field-and-filter.md @@ -1,6 +1,6 @@ # req-116: `trex.slots.req` 전략 필드 노출 + `strategy` 요청 필터 (additive) -- Status: Approved +- Status: Implemented - Type: Interface - Priority: High - Owner: trex-core-dev @@ -80,6 +80,21 @@ ## Change Log +### CHG-20260704-03 +- Date: 2026-07-04 +- Author: 팀장(Claude) — confirm-req-implemented + +#### Change Summary +- Approved → **Implemented**: bat-055 구현 완료. 독립 QC **opus 승격** PASS-WITH-NOTES(blocker 0) — 777 passed 일치, AC 8/8, ★watchdog 굶김 없음 실증 프로브 확인(필터는 응답에만 적용), 미지정 요청 byte-동일 검증. PR #37 사장님 결재·머지(dc6d7a9) = confirm-batch-verification 완료. +- **배포 게이트**: portfolio-hub REQ-055(PR #20) 선행 배포 후 본 변경 배포(선행 전엔 safe-default 무해). + +#### Delivery Trace Delta +- Batches: bat-055_20260704_slots-strategy-field-filter (merged, PR #37) + +#### Revalidation Impact +- Reverification Needed: no / Existing Implementation Invalidated: no +- Status Recommendation: Implemented + ### CHG-20260704-02 - Date: 2026-07-04 - Author: 팀장(Claude) — confirm-req diff --git a/docs/srs/index.md b/docs/srs/index.md index bf2104d..7123b57 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -74,7 +74,7 @@ ## Next Requirement ID -- Current: `REQ-117` +- Current: `REQ-119` - Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다. ## Requirement Register @@ -196,12 +196,14 @@ | REQ-113 | KISConfig 소스 우선순위 — portfolio-hub fetch 1차 + env break-glass + opt-in | Configuration | Approved | High | trex-core-dev | [req-113](./Configuration/req-113_kisconfig-fetch-source-priority-and-break-glass.md) | | REQ-114 | 그날-시작 로드 게이트 — fail-closed 신규 진입 차단 + 알람 | Integration | Implemented | High | trex-core-dev | [req-114](./Integration/req-114_daystart-load-gate-fail-closed-entry.md) | | REQ-115 | 그날-시작 장중 무중단 보장 — 그날 고정 인증정보 캐시 + portfolio-hub 장애 비중단 | Non-Functional | Approved | High | trex-core-dev | [req-115](./Non-Functional/req-115_daystart-intraday-resilience-credential-cache.md) | -| REQ-116 | `trex.slots.req` 전략 필드 노출 + `strategy` 요청 필터 (additive) | Interface | Approved | High | trex-core-dev | [req-116](./Interface/req-116_slots-req-strategy-field-and-filter.md) | +| REQ-116 | `trex.slots.req` 전략 필드 노출 + `strategy` 요청 필터 (additive) | Interface | Implemented | High | trex-core-dev | [req-116](./Interface/req-116_slots-req-strategy-field-and-filter.md) | +| REQ-117 | OrderRequest venue additive 필드 (KRX/NXT 거래소구분 포트 DTO) | Integration | Approved | High | trex-core-dev | [req-117](./Integration/req-117_order-request-venue-field.md) | +| REQ-118 | KIS 브로커 어댑터 venue 파라미터화 — EXCG_ID_DVSN_CD 전송 + NXT 지원 | Integration | Approved | High | trex-core-dev | [req-118](./Integration/req-118_kis-broker-venue-parameterization.md) | ## Recent Change Log Summary -- 2026-07-04 :: REQ-116 **신규 생성(Approved, draft-req, trex-tvr-v1 dcy-001/dcy-002 v3 FR-8)**: `trex.slots.req` additive 확장 — 행 `strategy_name`·`strategy_params`·`markets` 필드 노출(hub 정본, safe-default) + 요청 `strategy` 필터(exact-match, status 필터 AND 조합, available_slots 불변). REQ-074 AC-5 additive 재도입(dcy-025 OQ-4 후속). 라이브 wire 계약 고위험 — worktree+PR+QC 정식 플로, 배포 선행조건=portfolio-hub REQ① 먼저. +- 2026-07-06 :: REQ-117/118 **신규 생성(Approved, draft-req, trex-tvr-v1 dcy-002 FR-6)**: KIS 브로커 venue 파라미터화 — REQ-117 OrderRequest.venue additive 필드(기본 KRX, 포트 DTO 계층), REQ-118 KIS 어댑터 EXCG_ID_DVSN_CD 전송 + cancel venue 의미론 + poll_fill NXT 체결 보장 + get_current_price market 파라미터화. 실측 근거: 2026-07-06 실전 NXT 주문/취소 rt_cd=0. 고위험(라이브 주문 경로) — worktree+PR+QC(opus) 필수. confirm-req 대기. +- 2026-07-04 :: REQ-116 **신규 생성(Implemented, draft-req, trex-tvr-v1 dcy-001/dcy-002 v3 FR-8)**: `trex.slots.req` additive 확장 — 행 `strategy_name`·`strategy_params`·`markets` 필드 노출(hub 정본, safe-default) + 요청 `strategy` 필터(exact-match, status 필터 AND 조합, available_slots 불변). REQ-074 AC-5 additive 재도입(dcy-025 OQ-4 후속). 라이브 wire 계약 고위험 — worktree+PR+QC 정식 플로, 배포 선행조건=portfolio-hub REQ① 먼저. - 2026-06-28 :: REQ-112/114 **`Approved` → `Implemented`**, REQ-113/115 **`Approved` 유지(confirm-req-implemented, dcy-036/BAT-053)**: BAT-053 PR #34 머지(01655ab). QC PASS-WITH-NOTES(opus, blocker 0). 745 passed/0 회귀. REQ-112(fetch 포트+어댑터+stub AC-1~8 완료), REQ-114(fail-closed 게이트 AC-1~9, BuyIntent 차단/SellIntent 통과 비대칭·CRITICAL 알람·opt-in off 비활성 완료). REQ-113/115 스캐폴드 완료 — KISBroker 런타임 재조립(REQ-113 AC-2, REQ-115 AC-1) M3 후속 batch 연기. - 2026-06-28 :: REQ-112~115 **`Proposed` → `Approved`(confirm-req 팀장 승인)**: dcy-036 FR/NFR 추적·AC testable·blocker 0. 진입 fail-closed/청산·DISPATCHED 통과 비대칭(REQ-114 AC-4/5), 그날 고정 캐시·ledger 무손실(REQ-115), TradingContext 원자 read(REQ-112), opt-in 회귀차단(REQ-113 AC-1). OQ-4(trex-notifier) Deferred. 선행 의존 portfolio-hub dcy-010(머지완료). 다음=suggest-batch-reqs. - 2026-06-26 :: REQ-109~111 **Approved → Implemented** (confirm-req-implemented, dcy-035): BAT-052 머지(PR #33, 81d06d9) 완료. QC PASS. 717 passed/47 skipped/0 fail. REQ-109(portfolio-hub /pnl/realized 포트+어댑터+stub AC-1~6), REQ-110(WAC 운영리포트 통합 AC-1~7, CHG-20260626-02 buy_fee편입 반영), REQ-111(degrade AC-1~5) 전 AC 검증 완료. M3 배포 대기. -- 2026-06-26 :: REQ-102~108 **Approved → Implemented** (confirm-req-implemented, dcy-034): BAT-050(core-A, PR #31 ced7eda) + BAT-051(core-B, PR #32 07b393d) origin/main 머지 완료. QC(opus) PASS + 팀장 confirm-batch-verification. 최종 683 passed/47 skipped/0 fail. 차단형 reconcile(REQ-102·103), 전략 차단 게이트(REQ-104), sync/verify CLI(REQ-105·106), 차단/해제 로깅+알람(REQ-107), NFR(REQ-108) 전 AC 검증 완료. M3 배포 대기. diff --git a/tests/adapters/broker/test_venue.py b/tests/adapters/broker/test_venue.py new file mode 100644 index 0000000..8dfdd61 --- /dev/null +++ b/tests/adapters/broker/test_venue.py @@ -0,0 +1,342 @@ +"""Venue parameterization tests (REQ-117 AC-1~5, REQ-118 AC-1~8, BAT-056). + +All tests stub the HTTP layer (token pre-seeded, httpx faked) and run always +without KIS credentials. Tests verify additive-only changes: existing KRX +path behavior is byte-equivalent to before, and NXT path uses correct EXCG. + +REQ-117 AC coverage: + AC-1: venue default KRX → test_order_request_venue_default_krx + AC-2: venue NXT → test_order_request_venue_nxt + AC-3: 기존 회귀 → all existing tests pass (run via pytest) + AC-4: 위치 인수 순서 불변 → test_order_request_positional_args_unchanged + AC-5: paper venue 무영향 → test_paper_submit_nxt_accepted, test_paper_cancel_venue_ignored + +REQ-118 AC coverage: + AC-1: submit KRX EXCG → test_submit_order_krx_includes_excg + AC-2: submit NXT EXCG → test_submit_order_nxt_includes_excg + AC-3: cancel NXT EXCG → test_cancel_order_nxt_includes_excg + AC-4: poll_fill TR 미확인 → (open item; see design.md) + AC-5: get_price default J → test_get_current_price_default_market_j + AC-6: get_price UN → test_get_current_price_market_un + AC-7: 기존 회귀 EXCG 추가 → test_submit_order_krx_only_adds_excg_field + AC-8: paper cancel 정합 → test_paper_cancel_venue_ignored +""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from decimal import Decimal + +import httpx +import pytest + +from trex_core.adapters.broker import kis as kis_mod +from trex_core.adapters.broker.kis import KISBroker +from trex_core.adapters.broker.paper import PaperBroker +from trex_core.application.broker_port import BrokerPort, OrderRequest + +_MOCK_URL = "https://openapivts.koreainvestment.com:29443" +_LIVE_URL = "https://openapi.koreainvestment.com:9443" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _mock_broker() -> KISBroker: + b = KISBroker("12345678-01", app_key="k", app_secret="s", base_url=_MOCK_URL) + b._token = "tok" + b._token_exp = datetime.now(tz=timezone.utc) + timedelta(hours=1) + return b + + +def _live_broker() -> KISBroker: + b = KISBroker("12345678-01", app_key="k", app_secret="s", + base_url=_LIVE_URL, allow_live=True) + b._token = "tok" + b._token_exp = datetime.now(tz=timezone.utc) + timedelta(hours=1) + return b + + +class _FakeResp: + def __init__(self, payload): + self._payload = payload + + def json(self): + return self._payload + + +# --------------------------------------------------------------------------- +# REQ-117 AC-1: venue default KRX +# --------------------------------------------------------------------------- + +def test_order_request_venue_default_krx(): + """REQ-117 AC-1: venue 미지정 시 기본값 KRX.""" + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=1) + assert req.venue == "KRX" + + +# --------------------------------------------------------------------------- +# REQ-117 AC-2: venue NXT +# --------------------------------------------------------------------------- + +def test_order_request_venue_nxt(): + """REQ-117 AC-2: venue="NXT" 지정 시 NXT.""" + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=1, venue="NXT") + assert req.venue == "NXT" + + +# --------------------------------------------------------------------------- +# REQ-117 AC-4: 기존 위치 인수 순서 불변 +# --------------------------------------------------------------------------- + +def test_order_request_positional_args_unchanged(): + """REQ-117 AC-4: 기존 위치 인수(slot_id, side, symbol, qty) 순서 불변.""" + req = OrderRequest("s1", "buy", "005930", 10) + assert req.slot_id == "s1" + assert req.side == "buy" + assert req.symbol == "005930" + assert req.qty == 10 + assert req.limit_price is None + assert req.order_type == "00" + assert req.venue == "KRX" # 기본값 + + +def test_order_request_full_positional(): + """기존 위치 인수 + 키워드 혼용 가능 확인.""" + req = OrderRequest("s1", "sell", "000660", 5, 50000, "00", "NXT") + assert req.slot_id == "s1" + assert req.side == "sell" + assert req.symbol == "000660" + assert req.qty == 5 + assert req.limit_price == 50000 + assert req.order_type == "00" + assert req.venue == "NXT" + + +# --------------------------------------------------------------------------- +# REQ-117 AC-5 + REQ-118 AC-8: paper broker venue 무영향 +# --------------------------------------------------------------------------- + +def test_paper_submit_nxt_accepted(): + """REQ-117 AC-5: paper broker는 NXT든 KRX든 venue 무관하게 주문 수락.""" + b = PaperBroker(fill_after_polls=0) + req_nxt = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=1, venue="NXT") + req_krx = OrderRequest(slot_id="s2", side="buy", symbol="005930", qty=1, venue="KRX") + ack_nxt = b.submit_order(req_nxt) + ack_krx = b.submit_order(req_krx) + assert ack_nxt.accepted + assert ack_krx.accepted + + +def test_paper_cancel_venue_ignored(): + """REQ-117 AC-5 + REQ-118 AC-8: paper cancel_order는 venue 파라미터를 받되 무시한다.""" + b = PaperBroker(fill_after_polls=1) + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=5) + ack = b.submit_order(req) + # NXT venue로 cancel — paper는 venue 무관하게 취소 성공 + result = b.cancel_order(ack.broker_order_id, side="buy", symbol="005930", qty=5, venue="NXT") + assert result is True + + +def test_paper_cancel_default_venue(): + """REQ-118 AC-8: venue 미지정(기본 KRX) cancel도 동작 유지.""" + b = PaperBroker(fill_after_polls=1) + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=3) + ack = b.submit_order(req) + result = b.cancel_order(ack.broker_order_id, side="buy", symbol="005930", qty=3) + assert result is True + + +# --------------------------------------------------------------------------- +# REQ-118 AC-1: submit_order KRX EXCG 포함 +# --------------------------------------------------------------------------- + +def test_submit_order_krx_includes_excg(monkeypatch): + """REQ-118 AC-1: venue="KRX"(기본) submit 시 EXCG_ID_DVSN_CD="KRX" 포함.""" + captured = {} + + def fake_post(url, headers=None, json=None, timeout=None): + captured["body"] = json + return _FakeResp({"rt_cd": "0", "output": {"ODNO": "0001", "KRX_FWDG_ORD_ORGNO": "06010"}}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "post", fake_post) + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=1, limit_price=10000) + b.submit_order(req) + assert captured["body"]["EXCG_ID_DVSN_CD"] == "KRX" + + +# --------------------------------------------------------------------------- +# REQ-118 AC-2: submit_order NXT EXCG 포함 +# --------------------------------------------------------------------------- + +def test_submit_order_nxt_includes_excg(monkeypatch): + """REQ-118 AC-2: venue="NXT" submit 시 EXCG_ID_DVSN_CD="NXT" 포함.""" + captured = {} + + def fake_post(url, headers=None, json=None, timeout=None): + captured["body"] = json + return _FakeResp({"rt_cd": "0", "output": {"ODNO": "0002", "KRX_FWDG_ORD_ORGNO": "06010"}}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "post", fake_post) + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=1, + limit_price=10000, venue="NXT") + b.submit_order(req) + assert captured["body"]["EXCG_ID_DVSN_CD"] == "NXT" + + +# --------------------------------------------------------------------------- +# REQ-118 AC-3: cancel_order NXT EXCG 포함 +# --------------------------------------------------------------------------- + +def test_cancel_order_nxt_includes_excg(monkeypatch): + """REQ-118 AC-3: venue="NXT" cancel 시 EXCG_ID_DVSN_CD="NXT" 포함.""" + captured = {} + + def fake_post(url, headers=None, json=None, timeout=None): + captured["body"] = json + return _FakeResp({"rt_cd": "0"}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "post", fake_post) + b.cancel_order("06010:0000123456", side="sell", symbol="005930", qty=10, venue="NXT") + assert captured["body"]["EXCG_ID_DVSN_CD"] == "NXT" + + +def test_cancel_order_krx_includes_excg(monkeypatch): + """cancel_order 기본(KRX) 시 EXCG_ID_DVSN_CD="KRX" 포함.""" + captured = {} + + def fake_post(url, headers=None, json=None, timeout=None): + captured["body"] = json + return _FakeResp({"rt_cd": "0"}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "post", fake_post) + b.cancel_order("06010:0000123456", side="sell", symbol="005930", qty=10) + assert captured["body"]["EXCG_ID_DVSN_CD"] == "KRX" + + +# --------------------------------------------------------------------------- +# REQ-118 AC-5: get_current_price 기본 market=J +# --------------------------------------------------------------------------- + +def test_get_current_price_default_market_j(monkeypatch): + """REQ-118 AC-5: market 미지정 시 FID_COND_MRKT_DIV_CODE=J 전달.""" + captured = {} + + def fake_get(url, headers=None, params=None, timeout=None): + captured["params"] = params + return _FakeResp({"rt_cd": "0", "output": {"stck_prpr": "49000"}}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "get", fake_get) + price = b.get_current_price("005930") + assert price == Decimal("49000") + assert captured["params"]["FID_COND_MRKT_DIV_CODE"] == "J" + + +# --------------------------------------------------------------------------- +# REQ-118 AC-6: get_current_price market=UN +# --------------------------------------------------------------------------- + +def test_get_current_price_market_un(monkeypatch): + """REQ-118 AC-6: market="UN" 지정 시 FID_COND_MRKT_DIV_CODE=UN 전달.""" + captured = {} + + def fake_get(url, headers=None, params=None, timeout=None): + captured["params"] = params + return _FakeResp({"rt_cd": "0", "output": {"stck_prpr": "150000"}}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "get", fake_get) + price = b.get_current_price("005930", market="UN") + assert price == Decimal("150000") + assert captured["params"]["FID_COND_MRKT_DIV_CODE"] == "UN" + + +# --------------------------------------------------------------------------- +# REQ-118 AC-7: 기존 회귀 — venue 미지정 submit 페이로드는 EXCG 추가만 다름 +# --------------------------------------------------------------------------- + +def test_submit_order_krx_only_adds_excg_field(monkeypatch): + """REQ-118 AC-7: venue 미지정 submit body는 기존 대비 EXCG_ID_DVSN_CD="KRX" 추가만 다름. + + 기존 submit body fields: CANO, ACNT_PRDT_CD, PDNO, ORD_DVSN, ORD_QTY, ORD_UNPR + 신규: 위 6개 + EXCG_ID_DVSN_CD="KRX" + """ + captured = {} + + def fake_post(url, headers=None, json=None, timeout=None): + captured["body"] = json + return _FakeResp({"rt_cd": "0", "output": {"ODNO": "9999", "KRX_FWDG_ORD_ORGNO": ""}}) + + b = _mock_broker() + monkeypatch.setattr(kis_mod.httpx, "post", fake_post) + req = OrderRequest(slot_id="s1", side="buy", symbol="005930", qty=2, limit_price=50000) + b.submit_order(req) + + body = captured["body"] + # 기존 6개 필드 존재 확인 + assert body["CANO"] == "12345678" + assert body["ACNT_PRDT_CD"] == "01" + assert body["PDNO"] == "005930" + assert body["ORD_DVSN"] == "00" + assert body["ORD_QTY"] == "2" + assert body["ORD_UNPR"] == "50000" + # 신규 추가 필드 — EXCG_ID_DVSN_CD="KRX"만 다름 + assert body["EXCG_ID_DVSN_CD"] == "KRX" + # 7개 필드만 있음 (다른 필드 추가 없음) + assert set(body.keys()) == { + "CANO", "ACNT_PRDT_CD", "PDNO", "ORD_DVSN", + "ORD_QTY", "ORD_UNPR", "EXCG_ID_DVSN_CD", + } + + +# --------------------------------------------------------------------------- +# BrokerPort 프로토콜 정합 검증 +# --------------------------------------------------------------------------- + +def test_kis_broker_conforms_to_broker_port(): + """KISBroker가 BrokerPort 프로토콜을 만족하는지 구조적 확인.""" + import inspect + # submit_order 시그니처: req: OrderRequest + sig = inspect.signature(KISBroker.submit_order) + assert "req" in sig.parameters + + # cancel_order 시그니처: venue 파라미터 있음, 기본값 KRX + sig = inspect.signature(KISBroker.cancel_order) + assert "venue" in sig.parameters + assert sig.parameters["venue"].default == "KRX" + + # get_current_price 시그니처: market 파라미터 있음, 기본값 J + sig = inspect.signature(KISBroker.get_current_price) + assert "market" in sig.parameters + assert sig.parameters["market"].default == "J" + + +def test_paper_broker_conforms_to_broker_port(): + """PaperBroker가 BrokerPort 프로토콜을 만족하는지 구조적 확인.""" + import inspect + sig = inspect.signature(PaperBroker.cancel_order) + assert "venue" in sig.parameters + assert sig.parameters["venue"].default == "KRX" + + sig = inspect.signature(PaperBroker.get_current_price) + assert "market" in sig.parameters + assert sig.parameters["market"].default == "J" + + +# --------------------------------------------------------------------------- +# 기존 cancel_order 호출자 호환: venue 없이 호출 가능 +# --------------------------------------------------------------------------- + +def test_paper_cancel_backward_compat_no_venue(): + """기존 코드가 venue 없이 cancel_order를 호출해도 동작한다(기본값 KRX).""" + b = PaperBroker(fill_after_polls=1) + req = OrderRequest(slot_id="s1", side="sell", symbol="005930", qty=2) + ack = b.submit_order(req) + # 기존 스타일 호출 (venue 없음) + result = b.cancel_order(ack.broker_order_id, side="sell", symbol="005930", qty=2) + assert result is True diff --git a/trex_core/adapters/broker/kis.py b/trex_core/adapters/broker/kis.py index 4c4addd..e0633f7 100644 --- a/trex_core/adapters/broker/kis.py +++ b/trex_core/adapters/broker/kis.py @@ -172,6 +172,10 @@ class KISBroker: "ORD_DVSN": req.order_type, "ORD_QTY": str(req.qty), "ORD_UNPR": str(req.limit_price or 0), + # REQ-118: always send EXCG_ID_DVSN_CD from req.venue (default "KRX"). + # Previously omitted (server defaulted to KRX); explicit is more robust. + # Real-account NXT order confirmed rt_cd=0 on 2026-07-06. + "EXCG_ID_DVSN_CD": req.venue, } try: resp = httpx.post( @@ -197,6 +201,9 @@ class KISBroker: cano, product = self._account() odno = broker_order_id.split(":")[-1] today = datetime.now(tz=_KST).strftime("%Y%m%d") + # REQ-118 AC-4 OPEN ITEM: TTTC8001R NXT 체결 포함 여부 미확인 (웹 접근 불가 환경, + # KIS 개발자 센터 문서 조회 불가). 보수 구현: 기존 TR 유지. KRX 경로 동작 절대 불변. + # 향후 NXT 체결 미포함 실측 확인 시 TTTC0081R(EXCG=ALL) 전환 별도 batch 처리. tr_id = "VTTC8001R" if self._mock else "TTTC8001R" params = { "CANO": cano, "ACNT_PRDT_CD": product, @@ -311,14 +318,16 @@ class KISBroker: out.append(BalanceItem(symbol=symbol, hldg_qty=qty)) return out - def get_current_price(self, symbol: str) -> Optional[Decimal]: - """Current traded price `stck_prpr` via KIS inquire-price (REQ-038). + def get_current_price(self, symbol: str, market: str = "J") -> Optional[Decimal]: + """Current traded price `stck_prpr` via KIS inquire-price (REQ-038, REQ-118). - tr_id: `TTTC8397R` (live) / `VTTC8397R` (mock) per REQ-038 AC-7. + tr_id: `FHKST01010100` per REQ-038 AC-7 (live and mock share the same tr_id). + market: KIS FID_COND_MRKT_DIV_CODE (REQ-118). Default "J" (KRX equities) + preserves backward compatibility for existing safety-sell callers. Returns None on network/parse error or missing price so the safety-sell worker skips the slot this tick (no false trigger). """ - params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol} + params = {"FID_COND_MRKT_DIV_CODE": market, "FID_INPUT_ISCD": symbol} try: resp = httpx.get( f"{self._base_url}/uapi/domestic-stock/v1/quotations/inquire-price", @@ -338,7 +347,13 @@ class KISBroker: except (InvalidOperation, ValueError): return None - def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int) -> bool: + def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int, venue: str = "KRX") -> bool: + """Cancel a previously submitted order (REQ-118). + + venue: exchange venue of the original order. Must match the EXCG_ID_DVSN_CD + used at submit time. Default "KRX" preserves backward compatibility. + Real-account NXT cancel (EXCG=NXT) confirmed rt_cd=0 on 2026-07-06. + """ cano, product = self._account() parts = broker_order_id.split(":") orgno, odno = (parts[0], parts[1]) if len(parts) == 2 else ("", parts[-1]) @@ -347,6 +362,8 @@ class KISBroker: "KRX_FWDG_ORD_ORGNO": orgno, "ORGN_ODNO": odno, "ORD_DVSN": "00", "RVSE_CNCL_DVSN_CD": "02", "ORD_QTY": str(qty), "ORD_UNPR": "0", "QTY_ALL_ORD_YN": "Y", + # REQ-118: cancel must use the same EXCG_ID_DVSN_CD as the original submit. + "EXCG_ID_DVSN_CD": venue, } tr_id = "VTTC0013U" if self._mock else "TTTC0013U" try: diff --git a/trex_core/adapters/broker/paper.py b/trex_core/adapters/broker/paper.py index 01f79c0..5afb701 100644 --- a/trex_core/adapters/broker/paper.py +++ b/trex_core/adapters/broker/paper.py @@ -105,7 +105,10 @@ class PaperBroker: ) return FillStatus(state="partially_filled", filled_qty=o.filled, avg_price=o.price) - def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int) -> bool: + def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int, venue: str = "KRX") -> bool: + """Cancel a paper order. venue is accepted for BrokerPort protocol conformance + (REQ-118) but ignored — paper has no exchange concept. Existing behavior preserved. + """ with self._lock: o = self._orders.get(broker_order_id) if o is None or o.filled >= o.qty: @@ -135,7 +138,11 @@ class PaperBroker: with self._lock: self._prices[symbol] = Decimal(str(price)) - def get_current_price(self, symbol: str) -> Optional[Decimal]: - """Return the programmed current price for symbol, or None if unset.""" + def get_current_price(self, symbol: str, market: str = "J") -> Optional[Decimal]: + """Return the programmed current price for symbol, or None if unset. + + market accepted for BrokerPort protocol conformance (REQ-118) but + ignored — paper has no market distinction concept. + """ with self._lock: return self._prices.get(symbol) diff --git a/trex_core/application/broker_port.py b/trex_core/application/broker_port.py index 31a5a6e..adac710 100644 --- a/trex_core/application/broker_port.py +++ b/trex_core/application/broker_port.py @@ -21,6 +21,7 @@ class OrderRequest: qty: int limit_price: Optional[int] = None # None => market order order_type: str = "00" # KIS ORD_DVSN: 00 limit, 01 market + venue: str = "KRX" # exchange venue (REQ-117): KRX | NXT | SOR @dataclass(frozen=True) @@ -93,8 +94,13 @@ class BrokerPort(Protocol): """Observe current fill status for a previously accepted order.""" ... - def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int) -> bool: - """Best-effort cancel. Returns True if the broker accepted the cancel.""" + def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int, venue: str = "KRX") -> bool: + """Best-effort cancel. Returns True if the broker accepted the cancel. + + venue: exchange venue of the original order (REQ-118). Cancel must use + the same EXCG_ID_DVSN_CD as the original submit. Default "KRX" preserves + backward compatibility for existing callers (orb/tsl). + """ ... def get_balance(self) -> list[BalanceItem]: @@ -106,12 +112,15 @@ class BrokerPort(Protocol): """ ... - def get_current_price(self, symbol: str) -> Optional[Decimal]: + def get_current_price(self, symbol: str, market: str = "J") -> Optional[Decimal]: """Return the current traded price (`stck_prpr`) for a symbol (REQ-038). Used by the safety-sell worker to evaluate stop-loss/take-profit thresholds. Returns None when the price is unavailable (network error, unknown symbol, or — for the paper double — no programmed price). + + market: KIS FID_COND_MRKT_DIV_CODE (REQ-118). Default "J" (KRX equities) + preserves backward compatibility. Use "UN" for overseas/NXT extended session. """ ...