REQ-095(슬롯 평단·원장 price 지정가 백필, 0 오염 방지) + REQ-096(백필/미해결 WARN 로깅) 구현. 지정가 부재 시 skip+강WARN. application 단일화(_resolve_fill_price), wire/LedgerTrade 스키마 불변. 575 passed/0 fail, opus QC PASS. REQ-097 알람=스파이크 산출물만(impl 후속). - _resolve_fill_price 헬퍼 추가: avg_price 검증, limit_price 백필, 미해결 WARN - _on_filled/_on_partial_cancel 통합: 백필 적용, 슬롯/원장 일관성 - _record_ledger 시그니처: resolved_price 파라미터 (backward compat) - test_execution_fill_price_backfill.py: 21 시나리오 (avg=None/0, limit=있음/없음 등) - docs: dcy-032, req-095/096/097, bat-048 설계/검증 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
11 KiB
BAT-048 Verification
- Status: draft
- 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 위생)
:18import pytestF401(미사용) → 제거:276r = 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)가 필요한 통합 테스트로, 팀장이 실 환경에서 직접 실행해야 한다.
- KIS avg_price=None 케이스 재현: KIS sandbox에서 체결조회가 avg=0을 반환하는 순간 포착 → backfill WARN 발행 + 슬롯 avg_price/원장 price가 지정가로 저장됨을 확인.
- PostgreSQL ledger_outbox price 확인: 실 PostgreSQL에서 backfill된 체결 건의
ledger_outbox.trade_data.price가 0.0이 아닌 지정가 값인지 확인. - 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). |