feat(execution): 체결가 0/None 시 지정가 백필 가드 + WARN 로깅 (dcy-032/BAT-048)

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
This commit is contained in:
2026-06-21 13:34:31 +09:00
co-authored by Claude Opus 4.8
parent d182464b7a
commit f1b649cf2a
15 changed files with 2134 additions and 11 deletions
@@ -0,0 +1,337 @@
# BAT-048 Design
- Status: draft
- Batch: BAT-048
---
> **Design Review 게이트**: 이 문서의 Key Decisions(KD-1, KD-2)와 REQ-097 알람 스파이크 분석(섹션 "알람 능력 스파이크 — REQ-097")은 팀장·사장님의 design review 승인 후 run-batch-implementation으로 이어진다. 아래 권고안은 trex-core-dev가 코드 분석 근거로 작성한 것이며 팀장이 KD-1/KD-2를, 사장님이 알람 M2 포함 여부(OQ-1)를 최종 확정한다.
---
## Architecture Summary
이 batch의 핵심 변경은 `application/execution.py` 내 체결가 결정 로직에 국소화된다. 아키텍처 계층상 변경 위치는 **application 레이어 전용**이며, wire 계약·adapter·domain은 불변이다.
현재 문제 흐름:
```
KIS.poll_fill()
→ FillStatus(avg_price=None or 0.0)
→ _on_filled / _on_partial_cancel
fill_price = 0.0 [← 현재: avg_price=None이면 old_avg(=0) 사용]
→ slot.avg_price = 0 [슬롯 평단 0 오염, 영구 — clear_dispatch 이후 재폴링 없음]
→ _record_ledger
price = 0.0 [← 현재: avg_price=None이면 0.0 통과]
→ LedgerWorkItem(price=0.0) [원장 cost basis 0 오염]
```
변경 후 흐름:
```
KIS.poll_fill()
→ FillStatus(avg_price=None or 0.0)
→ _resolve_fill_price(fs, slot) [신규 헬퍼, application 레이어]
→ slot.limit_price 백필 [지정가 양수이면 사용]
→ 폴백 정책 적용(KD-2 확정 후) [지정가마저 없으면]
→ _log.warning(백필 WARN) [REQ-096 AC-1]
→ _on_filled / _on_partial_cancel
fill_price = _resolve_fill_price 결과 [0 아닌 값]
→ slot.avg_price = 정상 가중평균
→ _record_ledger
price = 동일 결과값 [0 아닌 값]
→ LedgerWorkItem(price > 0)
```
변경 계층:
- `trex_core/application/execution.py` (application 레이어) — 백필 헬퍼 + 분기 + WARN 로그
- `tests/application/test_execution.py` — 신규 AC 테스트
- `docs/runtime-flows.md`, `docs/operations.md` — 갱신 (implementation 후)
불변 영역:
- `trex_core/adapters/broker/kis.py` — adapter 불변 (KD-1 근거)
- `trex_core/adapters/broker/paper.py` — adapter 불변 (paper는 항상 양수 avg_price 반환)
- AMQP wire 계약 (slots RPC payload / trex.events 스키마) — NFR-1
## Changed Areas
- Docs:
- `docs/runtime-flows.md` — 체결 처리 흐름에 백필 분기 추가 (implementation 완료 후)
- `docs/operations.md` — 백필 WARN 발생 시 운영자 점검 절차 추가 (implementation 완료 후)
- Modules:
- `trex_core/application/execution.py` — `_resolve_fill_price`(신규 헬퍼), `_on_filled`·`_on_partial_cancel`·`_record_ledger`에 분기 삽입
- `tests/application/test_execution.py` — 신규 AC 커버 테스트
- Interfaces:
- `LedgerTrade.price` 필드: 0 → 양수 값으로 정정. 필드 타입/이름 변경 없음(NFR-1).
- slots RPC `avg_price` 필드: 0 → 양수 값으로 정정. 스키마 변경 없음(NFR-1).
## Key Decisions
### KD-1: 백필 결정 지점 — application 레이어 단일화 권고
**권고: `application/execution.py`의 `_resolve_fill_price` 헬퍼로 단일화. `adapters/broker/kis.py`는 불변.**
#### 분석 근거
**현재 kis.py의 avg_price 처리 패턴** (`:209`, `:219`, `:232`, `:238`, `:243`):
```python
# 완전체결(:209)
fill_price = float(avg) if avg else 0.0
# ...
avg_price=fill_price or None, # 0.0 → None으로 변환(:219)
# 부분취소(:232)
fill_price = float(avg) if avg else 0.0
# ...
avg_price=fill_price or None, # 동일 패턴(:238)
# 부분체결(아직 pending, :243)
avg_price=float(avg) if avg else None # 미체결분은 그대로
```
kis.py는 이미 `avg_price=None`을 FillStatus로 정직하게 전달하는 구조다. 이 층에서 백필을 하려면 **kis.py가 `slot.limit_price`를 알아야** 하는데, `poll_fill(broker_order_id, side, symbol)` 시그니처에 슬롯 컨텍스트(`slot`)가 없다. 따라서 adapter에서 백필하려면 시그니처 변경(`BrokerPort.poll_fill` 확장)이 필요하고 이는 포트 계약 변경이다.
**paper.py 일관성**: paper 브로커는 `_Order.__init__`에서 `self.price = float(req.limit_price) if req.limit_price is not None else 1000.0`으로 항상 양수 avg_price를 반환한다. paper에는 백필 분기가 필요 없어 adapter 통일 구현의 이점이 없다.
**dcy-021 잔고 재구성 경로**: `reconcile()`은 `_poll(r)`을 호출 → 동일한 `_on_filled`/`_on_partial_cancel`을 통과한다. application에 백필 헬퍼를 두면 재구성 경로도 동일 헬퍼를 자동으로 탄다. adapter에 두면 재구성 경로에서 kis.py → FillStatus(avg_price=None) → application으로 None이 그대로 전달되는 차이가 생긴다(일관성 깨짐).
**결론**: application 단일화가 유일하게 porter 계약을 건드리지 않고, 모든 경로(KIS 완전체결/부분취소/재구성/paper)에서 동일 헬퍼를 탄다.
**adapter 방식의 장단점 (기각 이유)**:
- 장점: adapter 경계에서 일찍 정정 → application이 항상 양수 fill_price를 받는다는 불변식 성립.
- 단점: `BrokerPort.poll_fill` 시그니처에 `slot` 컨텍스트 추가 필요(포트 계약 변경) / paper도 수정 필요 / paper는 avg_price가 이미 양수여서 불필요한 중복 코드.
**application 방식의 장단점 (권고 이유)**:
- 장점: BrokerPort/adapter 불변 / paper 불변 / reconcile 자동 일관성 / `slot.limit_price` 직접 접근 가능.
- 단점: `_on_filled`/`_on_partial_cancel`/`_record_ledger` 세 지점에 분기 삽입 필요 → 헬퍼로 추출해 DRY 유지.
**팀장 승인 필요**: 이 결정이 확정되어야 Phase A 구현이 시작된다.
---
### KD-2: 지정가 부재/≤0 최종 폴백 정책 — skip+강 WARN 권고
**권고: `slot.limit_price`마저 None 또는 ≤0인 극단 케이스는 "체결 처리를 skip하되 강 WARN 로그 발행" 정책.**
#### 분석 대상 후보
| 후보 | 설명 | 장점 | 단점 | 안전성 | 구현비용 |
|---|---|---|---|---|---|
| **A: 재폴링 (체결 처리 보류)** | avg_price=None + limit_price=None 이면 이번 틱 처리를 skip, 다음 틱 재폴링 | KIS가 다음 틱에 정상 avg를 줄 수 있음 | 완전체결 후 `clear_dispatch` 전에 재폴링이 막힘 — 완전체결 케이스에서 재폴링 경로가 없어 영구 대기. `_on_filled` 내부에서 skip하면 슬롯이 PENDING_BUY에 잠기고 타임아웃/watchdog 경로가 구조에 의존. | 낮음 (영구 교착 가능) | 높음 (complete fill 케이스 구분 필요) |
| **B: 직전가/현재가 조회** | KIS에 현재가(`get_current_price`) 추가 조회 → 해당 가격으로 백필 | 지정가보다 실제 시세에 가까운 근사 | (1) `BrokerPort`에 `get_current_price` 추가 필요(포트 계약 변경) (2) KIS 추가 API 호출(타임아웃, 네트워크 실패) (3) 매우 드문 케이스를 위한 복잡도 증가 (4) safety_sell.py의 `get_current_price`(paper 전용 구현)가 있지만 KIS에는 미구현 | 중간 | 높음 |
| **C: skip + 강 WARN (권고)** | avg_price=None + limit_price=None 이면 체결 처리를 **skip**하되 slot_id·symbol·reason을 포함한 WARN 이상 로그 발행. 슬롯 전이 미수행, 다음 틱 재폴링 | 단순·안전 / 무경고 통과 0(REQ-096 AC-2 충족) / 추가 포트 계약 변경 없음 / "0 저장도 없음 + 강한 WARN"으로 REQ-095 AC-6 최소 기준 충족 | (1) 완전체결 상황에서 슬롯이 PENDING 상태에 잠길 수 있음 (2) 운영자 개입 필요(수기 확인 후 `trex-core cancel`·재진입) | 높음 | 낮음 |
#### 권고 근거
- A(재폴링)은 **완전체결(is_filled=True) 케이스에서 실행 불가**다. `_on_filled` 내부에서 skip하면 `clear_dispatch`가 호출되지 않아 슬롯은 PENDING_BUY로 잠기고, 다음 틱에 동일 주문을 다시 poll → `is_filled=True` 반복 → 루프. 이는 dcy-003 R-1(pending order stuck)과 동일한 패턴이다.
- B(현재가 조회)는 `BrokerPort` 포트 계약 변경이며 구현비용 대비 발생 빈도가 낮다. Discovery A-2: "KIS 평균가 누락은 드문 순간 레이스". 또한 KIS `get_current_price` 구현은 현재 없어 신규 adapter 작업이 필요하다.
- C(skip + 강 WARN)은 **REQ-095 AC-6 최소 기준("이 케이스에서 최소한 0 저장은 발생하지 않거나 강한 WARN이 남아야 한다")을 충족**하고, REQ-096 AC-2("무경고 통과 없음")도 충족한다. 슬롯이 PENDING에 잠기는 것은 watchdog/stale_pending 경보가 탐지해 운영자 개입을 유도하므로 안전망이 있다.
**구현 스펙 (C 채택 시)**:
```python
def _resolve_fill_price(self, fs: FillStatus, slot) -> tuple[float | None, bool]:
"""
Returns (fill_price, was_backfilled).
fill_price=None → skip signal (폴백 불가, 강 WARN 이미 발행).
"""
avg = fs.avg_price
if avg is not None and float(avg) > 0:
return float(avg), False
# avg_price None or ≤0: 지정가 백필 시도
limit = slot.limit_price
if limit is not None and float(limit) > 0:
_log.warning(
"fill price backfill: slot=%s symbol=%s broker_order_id=%s "
"avg_price=%s backfill_price=%s reason=avg_price_backfill",
slot.slot_id, slot.symbol, slot.broker_order_id,
avg, limit,
)
return float(limit), True
# 지정가마저 부재/≤0: skip + 강 WARN
_log.warning(
"fill price unresolvable: slot=%s symbol=%s broker_order_id=%s "
"avg_price=%s limit_price=%s reason=fill_price_unresolvable "
"(slot stays PENDING — operator attention required)",
slot.slot_id, slot.symbol, slot.broker_order_id,
avg, limit,
)
return None, False
```
`_resolve_fill_price`가 `None`을 반환하면 호출자(`_on_filled`, `_on_partial_cancel`, `_record_ledger`)에서 해당 체결 처리를 skip한다.
**팀장 승인 필요**: 폴백 정책 확정 후 REQ-095 AC-6 구체화. 다른 후보 채택 시 설계가 변경된다.
---
### KD-3: `_record_ledger` 컨텍스트 패싱 — 파라미터 확장 권고
**권고: `_on_filled`/`_on_partial_cancel`에서 산출한 `fill_price`(백필 결과)를 `_record_ledger`에 직접 전달.**
#### 현재 흐름 분석
`_on_filled`(`:231~285`)는 `fill_price`(avg_price 가공값)를 계산해 `slot.avg_price` 갱신에만 사용한다. 이후 `:281`에서 `self._record_ledger(slot, fs, side, broker_order_id, r)`를 호출하는데, `_record_ledger`(`:401~437`)는 `:424`에서 `fs.avg_price`를 다시 꺼내 쓴다.
즉 **같은 `fs.avg_price`를 두 곳에서 독립적으로 해석**한다. 백필 헬퍼를 추가하면 `_on_filled`에서 한 번 계산한 결과를 `_record_ledger`에서 재계산 없이 전달하는 것이 일관성 있는 설계다.
**구현 스펙**:
```python
# _on_filled 내부:
fill_price, was_backfilled = self._resolve_fill_price(fs, slot)
if fill_price is None:
return # skip: 폴백 불가, WARN 이미 발행됨
# ... slot avg_price 계산 ...
self._record_ledger(slot, fs, side, broker_order_id, r, resolved_price=fill_price)
# _record_ledger 시그니처 변경:
def _record_ledger(self, slot, fs, side: str, broker_order_id: str,
r: ExecutionResult, *, resolved_price: float | None = None) -> None:
# ...
price = resolved_price if resolved_price is not None else (
float(fs.avg_price) if fs.avg_price is not None else 0.0
)
```
`resolved_price` 기본값을 `None`으로 두면 기존 `_record_ledger` 호출 코드(다른 경로가 있다면)의 하위 호환성이 유지된다. 현재 코드베이스에서 `_record_ledger`는 `_on_filled`와 `_on_partial_cancel`에서만 호출된다.
---
## 알람 능력 스파이크 — REQ-097
> REQ-097 AC-1: 후보 A·B·C 각각 장점·단점·최소 구현 범위·M2 일정 적합성 명시.
> REQ-097 AC-2: 후보 B wire 계약 변경 여부와 blast radius 명시.
### 후보 A: 구조화 로그 기반
**개요**: FR-3 WARN 로그(REQ-096)를 알람 소스로 삼아 외부 알림 파이프라인(Loki → Grafana Alert / PagerDuty / etc.)에서 소비.
**장점**:
- trex-core 코드 변경 최소 — REQ-096(WARN 로그)만 구현하면 코어 측 작업 완료.
- dcy-029(BAT-045/046) 구조화 로그 인프라(json formatter, correlation_id) 이미 구축됨.
- wire 계약 불변, blast radius 없음.
- M2 일정 내 코어 변경 없음.
**단점**:
- 운영자가 로그 파이프라인(Loki + 알림 규칙)을 별도로 구성해야 함(인프라 의존).
- 알림 지연이 로그 수집 파이프라인 지연에 종속된다(수십 초 ~ 분 단위).
- trex-core 자체에는 알람을 "보내는" 능력이 없음 — 알람은 로그 소비자가 담당.
**최소 구현 범위**: REQ-096 WARN 로그 구현만으로 충분. 알람 파이프라인 구축은 인프라 작업(trex-core 범위 밖).
**M2 일정 적합성**: 코어 측 추가 작업 없음 → **M2 적합**.
---
### 후보 B: AMQP 이벤트 기반
**개요**: `trex.events` exchange 또는 신규 `trex.alerts` exchange에 알람 이벤트(예: `fill_price_backfill`, `fill_price_unresolvable`)를 publish.
**wire 계약 변경 여부 분석**:
현재 `trex.events` 토폴로지 (interface-contract.md):
- routing key 패턴: `{event_type}.{strategy_id}.{symbol}`
- 소비자: `trex.events.strategy.{strategy_id}` 큐가 `*.{strategy_id}.#` 패턴으로 바인딩
- trex-orb-v1, trex-tsl-v1이 이 큐를 소비
알람 이벤트를 `trex.events`에 추가하면:
- `alert.fill_backfill.{strategy_id}.{symbol}` 형태의 routing key를 사용할 경우 → 기존 `*.{strategy_id}.#` 바인딩에 매칭 → **orb/tsl에 알람 이벤트가 전달됨**. orb/tsl이 미처리 메시지를 어떻게 처리하는지 불명확.
- 신규 `trex.alerts` exchange를 추가할 경우 → exchange 신설 + 큐 + 바인딩 추가 = **AMQP 토폴로지 변경** = **wire 계약 변경**.
- 기존 `slot_filled_buy` 등 이벤트 스키마에 알람 관련 필드를 추가하는 것도 **payload 스키마 변경** = **wire 계약 변경**.
**결론: 후보 B는 어떤 구현 형태를 취하더라도 wire 계약(AMQP 토폴로지 또는 payload 스키마) 변경을 수반한다.**
**blast radius**: trex-orb-v1, trex-tsl-v1 양쪽 모두 영향. 특히 trex-tsl-v1은 M2 라이브 목표(6/28) 직전 wire 변경은 위험 수준 High.
**wire 계약 변경 필요 여부**: **예 (후보 B는 Out of Scope)**
> CLAUDE.md 교차 영향 사안 정책에 따라: 후보 B 분석에서 wire 계약 변경 필요성이 확인됨. 이 사안을 팀장에게 보고한다. **후보 B는 본 batch 범위 밖이며 구현 후보에서 제외 권고**.
---
### 후보 C: 외부 푸시 직접 연동
**개요**: trex-core 내에서 직접 Slack webhook / Telegram API / 이메일 등 외부 채널로 알람 전송.
**장점**:
- 운영자가 로그 파이프라인 없이 즉시 수신.
- 지연 최소(수 초 이내).
**단점**:
- trex-core에 외부 HTTP 의존성 추가(Slack/webhook URL 환경변수, 인증 토큰).
- 외부 채널 장애 시 trex-core 알람 발행 실패가 체결 처리에 영향 줄 수 있음(best-effort 설계 필요).
- 채널별 구현(Slack vs Telegram vs email)에 따라 코드 복잡도 증가.
- 자격증명(webhook URL / API key) 관리가 운영 절차에 추가됨.
- 알람 어댑터 포트 신규 정의 필요 + 구체 어댑터 구현 + 테스트.
**최소 구현 범위**: `AlertPort` (application 레이어 포트) + `SlackWebhookAlertAdapter` (adapter 레이어) + `DaemonConfig`에 webhook URL 추가 + `execution.py`에서 포트 호출. 최소 5개 파일 변경.
**M2 일정 적합성**: 구현 범위가 상당하고 자격증명 운영 절차가 추가됨 → **M2 일정 압박 (R-3)**. M3 후보 더 적합.
---
### 알람 스파이크 권고 요약
| 후보 | M2 포함 권고 | 이유 |
|---|---|---|
| A: 구조화 로그 기반 | **권고** (코어 작업 불필요) | REQ-096 WARN 구현만으로 완결, wire 계약 불변, M2 적합 |
| B: AMQP 이벤트 기반 | **Out of Scope** | wire 계약 변경 수반, orb/tsl blast radius, M2 위험 High |
| C: 외부 푸시 직접 연동 | **이월 권고** (M3 후보) | 구현 범위 크고 자격증명 관리 추가, M2 일정 압박 |
**trex-core-dev 권고**: 후보 A(구조화 로그 기반)를 기반 경로로 채택. REQ-096 WARN 로그 → Loki → 알람 파이프라인 연결은 인프라 작업으로 분리. 후보 C의 실제 구현은 M3에서 별도 Discovery로 정의.
FR-3 WARN 로깅(REQ-096)은 알람 채택 여부와 무관하게 M2 필수이며 본 batch에 포함된다.
**사장님 결정 필요 (OQ-1)**: 알람 실제 구현(후보 A 인프라 파이프라인 구축 or 후보 C 코어 구현)의 M2 포함 여부. trex-core-dev는 이 결정을 임의로 내리지 않는다.
---
## Edge Cases
### EC-1: 완전체결 + avg_price=None + limit_price=None
- 발생 경로: KIS가 완전체결 보고 + avg=0 + 슬롯에 limit_price가 None(드문 케이스, 예: orb가 market 주문 시)
- 현재: `fill_price=0.0` → `slot.avg_price=0` 영구 오염.
- KD-2 C 채택 시: `_resolve_fill_price` → `None` 반환 → `_on_filled` skip → `clear_dispatch` 미호출 → 슬롯이 PENDING_BUY 유지. 다음 틱 재폴링 → 동일 `is_filled=True` → skip 반복.
- 안전망: `_check_stale_pending`이 `stale_threshold_sec` 경과 후 WARN을 발생시키고, watchdog 만료 후 safety-sell이 개입한다.
- 운영자 조치: `trex-core cancel <slot_id>` → 슬롯 롤백 → 재진입.
- 이 케이스는 Discovery A-1(orb 매수는 지정가 주문)에 위배되는 예외 케이스이므로 저빈도라고 가정.
### EC-2: 부분취소 + avg_price=None + limit_price > 0
- `_on_partial_cancel`에서 `_resolve_fill_price` → `slot.limit_price` 백필 + WARN.
- 백필 가격으로 `new_avg` 계산 + `_record_ledger` price 기록 → 정상 흐름.
### EC-3: 매도 체결 + avg_price=None
- `_on_filled`에서 side=PENDING_SELL 분기: 이 분기는 `fill_price`/`avg_price`를 슬롯 평단 계산에 사용하지 않는다(매도는 avg_price 보존). 따라서 **매도 체결에서는 `_on_filled` 내 slot 전이에 백필 영향 없음**.
- 단, `_record_ledger`의 `price`(매도 체결가)는 여전히 `fs.avg_price`를 사용 → 여기서도 백필 헬퍼 적용 필요.
- 매도 `limit_price`는 `_dispatch`에서 HOLDING 진입 시 `None`으로 클리어되므로, 매도 시 `slot.limit_price=None`이 정상이다. 따라서 매도 체결에서 avg_price=None이면 EC-1과 유사한 skip+강 WARN 경로를 탄다(KD-2 C 확정 시).
### EC-4: happy path 회귀
- `fs.avg_price`가 정상 양수이면 `_resolve_fill_price`는 `(fs.avg_price, False)` 반환 → 기존과 동일 경로. WARN 미발생, 분기 overhead 없음.
### EC-5: paper 브로커에서 avg_price=None 발생 가능성
- paper 브로커(`paper.py:92,103,106`)는 `avg_price=o.price`를 반환하며 `o.price`는 `__init__`에서 `float(req.limit_price) if req.limit_price is not None else 1000.0`로 항상 양수다.
- 따라서 paper 브로커 사용 시 백필 경로 진입 없음 → paper 관련 테스트에서 WARN 미발생 단언 가능.
## Architecture Impact
- runtime-flows (체결 처리 흐름 변경 — `_on_filled`/`_on_partial_cancel`/`_record_ledger`에 백필 분기 추가)
## Reference Doc Update Plan
- `docs/runtime-flows.md`: implementation 완료 후 "Flow: 체결 처리" 섹션에 백필 분기 flowchart 추가. `_resolve_fill_price` 헬퍼 경로(avg_price=None/≤0 → limit_price 백필 → WARN 로그) 명시.
- `docs/operations.md`: implementation 완료 후 "백필 WARN 발생 시 운영자 점검 절차" 추가. 포함 내용: WARN 로그 식별 방법(`reason=avg_price_backfill` or `reason=fill_price_unresolvable`), 수기 점검 항목(slotID·원장 확인), `trex-core cancel` 조치 절차.
- `docs/project-structure.md`: `_resolve_fill_price` 헬퍼 추가는 execution.py 내 사적 메서드로 project-structure 변경 없음.
- `docs/interface-contract.md`: AMQP wire 계약 불변 — 변경 없음.
---
## Design Review 확정 항목 (팀장·사장님)
| # | 항목 | 권고 | 결정 권한 | 상태 |
|---|---|---|---|---|
| D-1 | 백필 결정 지점: application 단일화 vs adapter | application 권고(KD-1) | 팀장 | 미확정 |
| D-2 | 지정가 부재/≤0 최종 폴백: skip+강WARN vs 재폴링 vs 현재가 조회 | skip+강WARN 권고(KD-2) | 팀장 | 미확정 |
| D-3 | 알람 구현 M2 포함 여부 | 후보 A(로그 기반) + 실제 파이프라인은 후속 이월 권고 | 사장님 | 미확정 |
| D-4 | (D-3 M2 포함 시) 후보 A/C 중 무엇을 코어에서 구현 | 후보 C는 M3, 후보 A는 인프라 작업으로 분리 권고 | 사장님 | 미확정 |
@@ -0,0 +1,37 @@
# BAT-048: 체결가 0/None 백필 가드 + WARN 로깅 + 알람 능력 스파이크 (dcy-032)
- Status: verified (confirm-batch-verification 팀장 2026-06-21 — REQ-095/096 Implemented, 575 passed/47 skipped/0 fail 독립 재실행 + ruff clean, opus QC PASS-WITH-NOTES→NOTE-1/2 정정완료, 통합 3건 미실행 결정 기록[영속/전송 diff 0·KIS avg=None 결정적 재현 불가])
- Profile: standard
- Source Discovery: dcy-032
- Included REQ: REQ-095 (구현 완료), REQ-096 (구현 완료), REQ-097 (스파이크 산출물 완료 — 알람 impl 이월)
- Excluded REQ: 없음
## Summary
- dcy-032 발견: KIS 체결조회에서 체결수량은 반환되나 평균체결가(`avg`)가 None/0일 때, 슬롯 평단(`slot.avg_price`)과 원장 cost basis(`_record_ledger` price)가 동시에 0으로 무경고·영구 오염된다. 완전체결 시 `clear_dispatch`로 재폴링이 없어 0이 영속된다.
- 세 REQ가 단일 인과 사슬(체결가 누락 → 무결성 가드 → 가시성 → 알람 검토)을 형성하므로 같은 batch로 묶는다. REQ-095/096은 구현·테스트가 동일 코드 경로(`execution.py:_on_filled`, `_on_partial_cancel`, `_record_ledger`)에 있어 분리 구현의 이점이 없다. REQ-097은 같은 batch design 산출물로 제출되어 사장님 결정의 근거가 된다.
## Scope
- In Scope:
- `trex_core/application/execution.py` — `_on_filled`/`_on_partial_cancel`의 `fill_price` 백필 분기(REQ-095) + WARN 로그(REQ-096)
- `trex_core/application/execution.py` — `_record_ledger`의 `price` 백필 분기(REQ-095) + 백필/0가격 통과 WARN(REQ-096)
- 알람 채널 후보 A/B/C 분석 문서(REQ-097) — design 산출물
- `tests/application/test_execution.py` — avg_price=None, avg_price=0, happy path, WARN 로그 발생 AC 커버
- `docs/runtime-flows.md` — 체결 처리 흐름에 백필 분기 추가 (implementation 후 갱신)
- `docs/operations.md` — 백필 WARN 발생 시 운영자 점검 절차 추가 (implementation 후 갱신)
- Out of Scope:
- `trex_core/adapters/broker/kis.py` 수정 — 백필 결정 지점을 application으로 단일화하므로 adapter 변경 없음 (design 확정 결과)
- slots RPC / AMQP wire 계약 변경 — NFR-1 명시적 금지
- 과거 0 기록 원장/슬롯의 소급 정정
- 알람 실제 구현 — design review 후 사장님 결정 사안(REQ-097 OQ-1)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: `trex_core/application/execution.py`, `tests/application/test_execution_fill_price_backfill.py`
- Verification: [verification.md](./verification.md)
## Notes
- R-1(Medium, Discovery R-1): 지정가 백필도 추정치. 실제 체결가 ≠ 지정가일 수 있음. 완화: WARN+추적 가능 플래그, 런북 명시.
- R-2(Medium, Discovery R-2): 백필 결정 지점(adapter vs application) 선택에 따라 paper/dcy-021 재구성 경로 일관성이 달라짐. design에서 단일 지점 확정 필수 — 팀장 승인 게이트.
- R-3(Low, Discovery R-3): 알람 구현 포함 시 범위 확대 → M2 일정 압박. design review에서 사장님 결정.
- wire 계약 안전: 후보 B(AMQP 이벤트 알람) 분석에서 wire 계약 변경 필요성이 발생하면 즉시 팀장 보고·중단(CLAUDE.md 교차 영향 사안 정책).
@@ -0,0 +1,79 @@
# BAT-048 Planning
- Status: confirmed
- Batch: BAT-048
- Profile: standard
## Included REQ
- REQ-095: 체결가 0/None 지정가 백필 가드 (슬롯 평단 + 원장 price)
- REQ-096: 체결가 백필·0가격 통과 WARN 로깅
- REQ-097: 알람(alert) 능력 검토 스파이크 산출물
## Delivery Plan
REQ-095/096은 같은 코드 경로(`execution.py`)의 같은 함수들에 걸쳐 있어 분리 구현의 이점이 없다. 두 REQ를 묶어 구현한 뒤 테스트에서 값 정정(REQ-095)과 로그 가시성(REQ-096)을 동시에 검증한다. REQ-097은 design 산출물로 완결된다.
구현 순서(run-batch-implementation 기준):
1. **design 확정 사항 선행 (이 planning 이전 게이트)**:
- 백필 결정 지점: application 단일화(후술 Design Gate). paper 브로커·dcy-021 재구성 경로 일관성 분석 후 팀장 승인.
- 지정가 부재/≤0 최종 폴백 정책: 후보별 분석 → 팀장 승인 (design review 게이트).
- REQ-097 알람 분석 산출물: design 문서 내 포함 → design review에서 사장님 결정.
2. **Phase A — `_on_filled` / `_on_partial_cancel` 백필 분기(REQ-095 AC-1/2) + WARN 로그(REQ-096 AC-1)**:
- 두 함수 내 `fill_price = float(fs.avg_price) if fs.avg_price is not None else old_avg` 를 백필 헬퍼(`_resolve_fill_price`)로 추출.
- `_resolve_fill_price(fs, slot)`: avg_price None/≤0 시 `slot.limit_price` 사용, 지정가도 없으면 폴백 정책 적용.
- 백필 시 `_log.warning(...)` 발행(REQ-096 AC-1 필드 집합).
3. **Phase B — `_record_ledger` price 백필(REQ-095 AC-3) + WARN 로그(REQ-096 연동)**:
- `price = float(fs.avg_price) if fs.avg_price is not None else 0.0` 라인을 `_resolve_fill_price` 결과로 교체.
- 이미 Phase A에서 백필 WARN을 발행했으면 중복 WARN 없음; `_record_ledger` 진입 전에 백필 여부를 파악할 수 있는 컨텍스트 설계 필요 (design에서 결정).
4. **Phase C — 테스트 작성**:
- `tests/application/test_execution.py`: AC-1~4(REQ-095) + AC-1~4(REQ-096) 커버.
- avg_price=None, avg_price=0, happy_path(정상 양수), limit_price 부재 극단 케이스.
- WARN 로그 발생 단언(`caplog` 사용).
- 기존 execution 테스트 회귀 유지.
5. **Phase D — baseline 문서 갱신**:
- `docs/runtime-flows.md` — 체결 처리 흐름에 백필 분기 추가.
- `docs/operations.md` — 백필 WARN 발생 시 운영자 점검 절차.
선행 조건: Phase A~C는 설계 결정(백필 헬퍼 구조, `_record_ledger` 컨텍스트 패싱)이 확정되어야 시작 가능.
## Dependencies
- Internal:
- `trex_core/application/execution.py` — 변경 대상 핵심 파일(BAT-004에서 도입, 이후 지속 갱신)
- `trex_core/adapters/broker/kis.py` — 읽기 참조만(백필은 application에서, adapter 불변)
- `trex_core/adapters/broker/paper.py` — 읽기 참조만(paper는 `o.price`를 항상 양수로 반환 → 백필 분기 미진입 정상 케이스)
- `tests/application/test_execution.py` — 기존 회귀 스위트 전체 통과 유지 필수
- dcy-029(BAT-045/046) — 구조화 로깅 인프라(`_log`, json formatter) 재사용
- External:
- portfolio-hub `LedgerPort` — `price` 필드 타입(float) 불변, 값만 정정. HTTP 계약 변경 없음.
- slots RPC(trex-orb-v1, trex-tsl-v1) — `avg_price` 필드 값이 0→정상값으로 바뀌는 것은 wire 계약 변경이 아님(NFR-1). trex-tsl-v1은 avg_price≤0 시 tier 무시 2% 폴백에서 정상 tier 평가로 자동 개선.
## Design Gate
- Design Required Before Implementation: yes
- Reason:
- **백필 결정 지점 (Discovery R-2, REQ-095 Notes)**: adapter(kis.py)에서 처리 vs application(execution.py)에서 처리. paper 브로커·dcy-021 잔고 재구성 경로와의 일관성이 다르다. 설계 없이 구현하면 경로별 일관성 깨짐 리스크. design에서 단일 지점 확정 후 팀장 승인 필수.
- **지정가 부재/≤0 폴백 정책 (Discovery OQ-2, REQ-095 AC-6)**: 재폴링 / 직전가 조회 / skip+강알림 등 후보별 구현비용·안전성이 다르다. 설계 확정 없이 구현하면 AC-6가 미완성이다. design review에서 팀장 확정 필수.
- **`_record_ledger` 컨텍스트 패싱 설계**: Phase A 백필 결과(`fill_price`, `was_backfilled`)를 `_record_ledger`까지 전달하는 방법(파라미터 확장 vs 재계산)이 함수 시그니처를 변경할 수 있다.
- **REQ-097 알람 스파이크 분석**: 알람 채널 후보 B(AMQP 이벤트)가 wire 계약 변경을 수반하는지 여부가 설계에서 확인되어야 한다. wire 변경 수반 시 즉시 팀장 보고·중단.
- architecture impact가 `runtime-flows`(체결 처리 흐름 변경)이므로 standard profile design이 필요하다.
## Milestones
1. draft-batch-design 완료 — 백필 결정 지점, 폴백 정책, `_record_ledger` 컨텍스트 패싱, REQ-097 알람 분석 산출물 확정(팀장·사장님 design review 게이트)
2. run-batch-implementation 완료 — Phase A~D 신규 테스트 포함, 기존 execution 회귀 유지
3. draft-batch-verification 완료 — REQ-095/096 AC별 커버, REQ-097 산출물 제출 확인
4. QC reviewer 검증 대기 (confirm-batch-verification은 팀장 게이트)
## Risks
- R-1(Medium): 지정가 백필이 추정치라 실제 체결가와 다를 수 있다. 완화: WARN + 추적 가능 플래그, 런북 명시. "0보다 합리적인 근사 + 추적 가능"이 목표(Discovery R-1).
- R-2(Medium): 백필 결정 지점을 adapter로 두면 paper 브로커에도 동일 백필 로직 복제가 필요해진다. application 단일화가 권고. design에서 확정.
- R-3(Low): `_resolve_fill_price` 헬퍼 추출 시 함수 시그니처 변경이 기존 테스트에 영향 가능. 완화: 기존 테스트 회귀 유지를 verification 기준으로 명시.
- R-4(Low): 알람 후보 B 분석 중 wire 계약 변경 필요 여부가 나오면 즉시 중단·팀장 보고.
@@ -0,0 +1,156 @@
# 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 위생)
- `: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). |
+1
View File
@@ -55,3 +55,4 @@
| BAT-045 | standard | released | REQ-084, REQ-085, REQ-087 | dcy-029 (issue #29, P0) | [bat-045](./bat-045_20260616_observability-logging-foundation/) |
| BAT-046 | standard | released | REQ-086, REQ-088, REQ-089, REQ-090 | dcy-029 (issue #29, P1) | [bat-046](./bat-046_20260616_observability-classification-pnl-runbook/) |
| BAT-047 | standard | release-candidate | REQ-091, REQ-092, REQ-093, REQ-094 | dcy-031 | [bat-047](./bat-047_20260619_event-publisher-connection-reliability/) |
| BAT-048 | standard | in-delivery | REQ-095, REQ-096, REQ-097 | dcy-032 | [bat-048](./bat-048_20260621_fill-price-backfill-guard-logging-alert-spike/) |