# BAT-048 Verification - Status: verified (confirm-batch-verification 팀장 2026-06-21) - Batch: BAT-048 - Scope: REQ-095 (지정가 백필 가드), REQ-096 (백필 WARN 로깅) - REQ-097: 알람 능력 스파이크 산출물로 완료 — 알람 실제 구현은 사장님 결정에 따라 이 batch 완료 후 별도 트랙 이월 --- ## 구현 범위 요약 | 변경 파일 | 변경 내용 | |---|---| | `trex_core/application/execution.py` | `_resolve_fill_price(fs, slot)` 헬퍼 신설; `_on_filled`·`_on_partial_cancel`에 백필 분기 + skip 경로 삽입; `_record_ledger`에 `resolved_price` 키워드 파라미터 추가(기본 None, 하위호환) | | `tests/application/test_execution_fill_price_backfill.py` | 신규 단위 테스트 21개 (REQ-095/096 AC 전체 커버 + EC-3 매도 경로 5개 추가; QC PASS-WITH-NOTES 정정 반영) | | `docs/runtime-flows.md` | "주문 집행 루프" Flow — poll 항목에 체결가 백필 가드 분기 문서화 | | `docs/operations.md` | §5 신설: 체결가 백필 WARN 발생 시 운영자 점검 절차; 디버깅 표에 2행 추가 | 불변 영역 확인: - `trex_core/adapters/broker/kis.py` — 수정 없음 (KD-1: application 단일화) - `trex_core/adapters/broker/paper.py` — 수정 없음 - AMQP wire 계약 (`docs/interface-contract.md`) — 변경 없음 (NFR-1) --- ## 단위 테스트 실행 결과 **실행 명령**: `python3 -m pytest tests/application/test_execution_fill_price_backfill.py -v` **신규 테스트 결과 (21개, QC 정정 후):** ``` tests/application/test_execution_fill_price_backfill.py::TestOnFilledBackfill::test_avg_price_none_with_limit_price_uses_limit_price PASSED tests/application/test_execution_fill_price_backfill.py::TestOnFilledBackfill::test_avg_price_zero_with_limit_price_uses_limit_price PASSED tests/application/test_execution_fill_price_backfill.py::TestOnFilledBackfill::test_happy_path_uses_avg_price_no_backfill_warn PASSED tests/application/test_execution_fill_price_backfill.py::TestOnFilledBackfill::test_avg_price_none_limit_price_none_skips_fill PASSED tests/application/test_execution_fill_price_backfill.py::TestOnFilledBackfill::test_avg_price_none_limit_price_zero_skips_fill PASSED tests/application/test_execution_fill_price_backfill.py::TestOnPartialCancelBackfill::test_avg_price_none_partial_cancel_uses_limit_price PASSED tests/application/test_execution_fill_price_backfill.py::TestOnPartialCancelBackfill::test_avg_price_zero_partial_cancel_uses_limit_price PASSED tests/application/test_execution_fill_price_backfill.py::TestOnPartialCancelBackfill::test_happy_path_partial_cancel_no_backfill_warn PASSED tests/application/test_execution_fill_price_backfill.py::TestOnPartialCancelBackfill::test_partial_cancel_unresolvable_skips_fill PASSED tests/application/test_execution_fill_price_backfill.py::TestRecordLedgerBackfillPrice::test_ledger_price_equals_backfill_limit_price_on_filled PASSED tests/application/test_execution_fill_price_backfill.py::TestRecordLedgerBackfillPrice::test_ledger_price_equals_backfill_limit_price_on_partial_cancel PASSED tests/application/test_execution_fill_price_backfill.py::TestRecordLedgerBackfillPrice::test_ledger_price_on_happy_path_uses_avg_price PASSED tests/application/test_execution_fill_price_backfill.py::TestRecordLedgerBackfillPrice::test_ledger_not_written_when_fill_price_unresolvable PASSED tests/application/test_execution_fill_price_backfill.py::TestWarnLogFields::test_backfill_warn_contains_required_fields PASSED tests/application/test_execution_fill_price_backfill.py::TestWarnLogFields::test_unresolvable_warn_contains_required_fields PASSED tests/application/test_execution_fill_price_backfill.py::TestSellPathBackfill::test_sell_filled_avg_price_none_with_limit_price_uses_backfill PASSED tests/application/test_execution_fill_price_backfill.py::TestSellPathBackfill::test_sell_partial_cancel_avg_price_none_with_limit_price_uses_backfill PASSED tests/application/test_execution_fill_price_backfill.py::TestSellPathBackfill::test_sell_filled_avg_price_none_limit_price_none_skips PASSED tests/application/test_execution_fill_price_backfill.py::TestSellPathBackfill::test_sell_partial_cancel_avg_price_none_limit_price_none_skips PASSED tests/application/test_execution_fill_price_backfill.py::TestSellPathBackfill::test_sell_filled_happy_path_no_backfill_warn PASSED tests/application/test_execution_fill_price_backfill.py::TestPaperBrokerRegression::test_paper_broker_buy_no_backfill_warn PASSED 21 passed in 0.21s ``` **전체 스위트 회귀 확인 결과:** ``` 575 passed, 47 skipped in 1.40s ``` 회귀: 0건. 기존 테스트 전부 통과. --- ## REQ-095 AC별 충족 여부 | AC | 내용 | 충족 여부 | 근거 테스트 | |---|---|---|---| | AC-1 | avg_price=None + limit_price>0 → _on_filled에서 slot.avg_price = limit_price(0 아님) | 충족 | `test_avg_price_none_with_limit_price_uses_limit_price` | | AC-2 | avg_price=0.0 + limit_price>0 → _on_partial_cancel에서 slot.avg_price = limit_price(0 아님) | 충족 | `test_avg_price_zero_with_limit_price_uses_limit_price`, `test_avg_price_zero_partial_cancel_uses_limit_price` | | AC-3 | avg_price=None → _record_ledger LedgerWorkItem.price = 백필 지정가(0.0 아님) | 충족 | `test_ledger_price_equals_backfill_limit_price_on_filled`, `test_ledger_price_equals_backfill_limit_price_on_partial_cancel` | | AC-4 | avg_price 정상 양수(happy path) → 기존 fs.avg_price 사용, 백필 미개입 | 충족 | `test_happy_path_uses_avg_price_no_backfill_warn`, `test_ledger_price_on_happy_path_uses_avg_price` | | AC-5 | wire 계약(slots RPC 스키마) 불변 — 값 정정만 | 충족 | interface-contract.md 수정 없음, KD-1 분석 | | AC-6 | limit_price=None/≤0 극단 케이스 → 체결 skip + slot_id·symbol·broker_order_id·avg_price·limit_price·reason=fill_price_unresolvable 강 WARN | 충족 | `test_avg_price_none_limit_price_none_skips_fill`, `test_avg_price_none_limit_price_zero_skips_fill`, `test_ledger_not_written_when_fill_price_unresolvable`, `test_unresolvable_warn_contains_required_fields` | --- ## REQ-096 AC별 충족 여부 | AC | 내용 | 충족 여부 | 근거 테스트 | |---|---|---|---| | AC-1 | 백필 발동 시 slot_id·symbol·broker_order_id·원avg_price·backfill_price·reason=avg_price_backfill 포함 WARN 정확히 1건 | 충족 | `test_avg_price_none_with_limit_price_uses_limit_price`, `test_backfill_warn_contains_required_fields` | | AC-2 | 지정가마저 부재/≤0 극단 케이스 — WARN 이상 1건 이상, 무경고 통과 없음 | 충족 | `test_avg_price_none_limit_price_none_skips_fill`, `test_unresolvable_warn_contains_required_fields` | | AC-3 | 정상 avg_price(happy path) — 백필 관련 WARN 미발생 | 충족 | `test_happy_path_uses_avg_price_no_backfill_warn`, `test_happy_path_partial_cancel_no_backfill_warn`, `test_paper_broker_buy_no_backfill_warn` | | AC-4 | 백필 WARN 로그에서 어떤 슬롯·체결에서 백필이 발생했는지, 원래 가격, 대체가격 식별 가능(NFR-3) | 충족 | `test_backfill_warn_contains_required_fields` — slot_id/symbol/avg_price(None)/backfill_price 필드 단언 | --- ## QC PASS-WITH-NOTES 정정 내역 QC 독립 검증에서 PASS-WITH-NOTES 판정 후 아래 2건을 정정했다. ### NOTE-1 (ruff 위생) - `:18` `import pytest` F401(미사용) → 제거 - `:276` `r = execution.run_once()` F841(미사용 변수) → `execution.run_once()`로 변경 - `ruff check tests/application/test_execution_fill_price_backfill.py` → `All checks passed!` ### NOTE-2 (매도 경로 테스트 보강) - `TestSellPathBackfill` 클래스 신설(5개 테스트) — 상세는 아래 섹션 참조. - 매도 경로에서 설계와 다른 실제 동작 발견 없음 — 프로덕션 코드 수정 없음. --- ## EC-3 매도(PENDING_SELL) 경로 커버리지 (QC NOTE-2 정정) design.md EC-3 설계 의도: 매도 체결 시 슬롯 avg_price(cost basis)는 보존되며, `_record_ledger`의 price 필드에만 매도 체결가(`resolved_price`)가 기록된다. 매도 슬롯의 `limit_price`는 HOLDING 진입 시 None으로 클리어되므로, market sell(limit_price=None)에서 avg_price=None이면 skip+강WARN이 설계 의도된 동작이다. | 테스트 | 케이스 | 단언 | |---|---|---| | `test_sell_filled_avg_price_none_with_limit_price_uses_backfill` | EC-3a: _on_filled PENDING_SELL + avg_price=None + limit_price>0 | 슬롯 INACTIVE, 1 WARN reason=avg_price_backfill, 원장 price=limit_price(0 아님), side=sell | | `test_sell_partial_cancel_avg_price_none_with_limit_price_uses_backfill` | EC-3b: _on_partial_cancel PENDING_SELL + avg_price=None + limit_price>0 | 슬롯 HOLDING(잔량), 1 WARN reason=avg_price_backfill, 원장 price=limit_price | | `test_sell_filled_avg_price_none_limit_price_none_skips` | EC-3c: _on_filled PENDING_SELL + avg_price=None + limit_price=None (TSL market sell) | 슬롯 PENDING_SELL 유지, 원장 미생성, 강WARN reason=fill_price_unresolvable | | `test_sell_partial_cancel_avg_price_none_limit_price_none_skips` | EC-3d: _on_partial_cancel PENDING_SELL + avg_price=None + limit_price=None | 슬롯 PENDING_SELL 유지, 원장 미생성, 강WARN | | `test_sell_filled_happy_path_no_backfill_warn` | EC-3e: 정상 매도 (avg_price>0) | 슬롯 INACTIVE, WARN 미발생, 원장 price=avg_price | 설계와 다른 동작 없음 — 코드 수정 없이 테스트만 추가. --- ## REQ-097 처리 REQ-097(알람 능력 스파이크)은 `design.md`의 "알람 능력 스파이크 — REQ-097" 섹션에서 후보 A/B/C 분석 산출물로 완결됐다. - **후보 B(AMQP 이벤트 기반)**: wire 계약 변경 수반 + trex-orb-v1/trex-tsl-v1 blast radius 확인 → 팀장에게 보고, Out of Scope 확정. - **후보 A(구조화 로그 기반)**: REQ-096 WARN 로그가 알람 소스 역할. 코어 추가 작업 없음. - **후보 C(외부 푸시 직접 연동)**: 구현 범위 크고 자격증명 관리 추가 → M3 이월 권고. 알람 실제 구현(알람 파이프라인 구축 또는 후보 C 코어 구현)은 사장님 결정(OQ-1)에 따라 이 batch 완료 후 별도 트랙으로 이월됨. REQ-097은 스파이크 산출물로 완료 처리. --- ## 통합 테스트 — 팀장 직접 실행 필요 단위 테스트는 모두 in-memory 어댑터로 검증됐다. 아래 항목은 실 인프라(KIS/PostgreSQL/RabbitMQ)가 필요한 통합 테스트로, 팀장이 실 환경에서 직접 실행해야 한다. 1. **KIS avg_price=None 케이스 재현**: KIS sandbox에서 체결조회가 avg=0을 반환하는 순간 포착 → backfill WARN 발행 + 슬롯 avg_price/원장 price가 지정가로 저장됨을 확인. 2. **PostgreSQL ledger_outbox price 확인**: 실 PostgreSQL에서 backfill된 체결 건의 `ledger_outbox.trade_data.price`가 0.0이 아닌 지정가 값인지 확인. 3. **reconcile 경로 일관성**: 데몬 재시작 후 `reconcile()`이 backfill 헬퍼를 동일하게 탄다는 것을 KIS sandbox에서 확인. --- ## 남은 위험 | 위험 | 수준 | 설명 | |---|---|---| | R-1 | Medium | 지정가 백필 자체가 추정치 — 실제 체결가와 다를 수 있다. WARN + 추적 가능 플래그(reason=avg_price_backfill)으로 완화. 운영자 판단 여지. | | R-2 | Low | fill_price_unresolvable 케이스에서 슬롯 PENDING 고착 → watchdog/stale_pending이 탐지 후 운영자 개입 필요. 자동 복구 없음(설계 의도). | | R-3 (해소) | — | 기존 테스트 영향 없음(회귀 0). |