Files
trex-core/docs/batches/bat-031_20260610_position-sizing/design.md
T
genichinandClaude Opus 4.8 5ee5f7f4e2 docs(srs/batch): dcy-021 REQ + BAT-031 + Implemented 전환
- draft-req: REQ-066(사이징 도메인·end-to-end), REQ-067(baseline 정합) 신규
- change-req: REQ-057(CHG-03 holding→pending_buy)·REQ-061(CHG-04 사이징 intent)·
  REQ-062(CHG-03 부분 매도 min(주문,잔고)) — Implemented→Approved 후 재구현
- confirm-req: REQ-066/067 Approved
- BAT-031 batch(index/planning/design D1~D4/implementation/verification) → release-candidate
- confirm-batch-verification 승인 + confirm-req-implemented:
  REQ-057/061/062/066/067 → Implemented (BAT-031 verification 근거)
- srs/batch index + dcy-021 discovery 생성 REQ 역참조 갱신

(issue #21 dcy-022 는 별도 작업으로 미포함)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:53:53 +09:00

4.1 KiB
Raw Blame History

BAT-031 Design

  • Status: confirmed (D1~D4, 2026-06-10)
  • Batch: BAT-031

설계 원칙

  • 수량 정본 = 브로커 잔고(REQ-062). 슬롯 qty 는 (a) pending 동안 = 주문 수량, (b) 체결/롤백 후 = 브로커 잔고(보유) 로 재동기화한다. core 는 "의도 수량"을 별도 저장하지 않는다(dcy-021 OQ-1).
  • avg_price 는 증액 시 가중평균, 매도(부분/전량) 시 cost basis 유지(dcy-021 OQ-3).
  • 모든 전이는 dcy-020 자산 중심 모델에 additive(무회귀, NFR-2).

D1 — 증액: 주문 수량 vs 보유 수량 (브로커 정본 재동기화)

  • HOLDING→PENDING_BUY(증액): slot.qty := intent.qty(추가 주문 수량), avg_price 보존(가중평균용), limit_price := price. (generic 전이 규칙이 caller None 일 때 avg 보존 → 별도 service 분기 불요)
  • dispatch 매수: qty = slot.qty(주문 수량) 그대로.
  • PENDING_BUY→HOLDING(체결): 보유 = 브로커 잔고 total, avg_price = 가중평균.
    • old_qty = max(0, broker_total − filled), old_avg = slot.avg_price or 0, new_avg = (old_qty×old_avg + filled×fill_price)/broker_total(broker_total 0 이면 fill_price).
    • 초기 매수도 동일 공식(avg None→0, old_qty=0 → new_avg=fill_price) — 통일.

D2 — 초기 매수 vs 증액 판별

  • INACTIVE→PENDING_BUY(초기): slot 이 inactive 라 avg_price=None.
  • HOLDING→PENDING_BUY(증액): avg_price 보존(>0).
  • 체결/롤백 핸들러는 브로커 잔고로 판별(잔고>0 → 증액/보유 잔존, 0 → 초기/미보유). avg_price 보존은 가중평균 계산에만 사용.

D3 — 증액/매도 롤백

  • 매수 거부 _rollback: 브로커 잔고>0 → PENDING_BUY→HOLDING(qty=브로커 잔고, 증액 실패해도 기존 보유 유지) / 잔고 0 → PENDING_BUY→INACTIVE(초기 매수 실패).
  • 매도 거부: PENDING_SELL→HOLDING(qty=브로커 잔고) (기존).

D4 — 부분 매도 + 전량 매도 구분 + 부분 체결

  • pending_sell 의 slot.qty 의미: 부분 매도 intent(SellIntent.qty 지정) → slot.qty = 주문 수량; 전량 매도(svc.sell/safety-sell/kill-switch, qty 미지정) → slot.qty = None("전량" sentinel). → _apply_transition_fields 의 PENDING_SELL 진입 규칙: new_qty = caller qty(None 보존, mirror 안 씀).
  • dispatch 매도: qty = 브로커잔고 if slot.qty is None else min(int(slot.qty), 브로커잔고). 0 이면 skip+WARN(divergence, REQ-062). → 전량은 항상 브로커 정본(stale mirror 무관), 부분은 min(주문, 잔고)·잔고 이상이면 전량.
  • PENDING_SELL→체결: 체결 후 브로커 잔고로 분기 — 잔고>0 → HOLDING(qty=잔고, avg=cost basis 유지) / 0 → INACTIVE. (기존 항상 INACTIVE 에서 변경 — 부분 매도 잔량 holding 지원)
  • 부분 체결: 실제 체결분만 ledger(기존 _record_ledger = fs.filled_qty), 미체결 잔량 미추적. dcy-013 _on_partial_cancel 도 브로커 정본 resync 로 정합.

D-impl — 브로커 잔고 조회

  • _poll tick 시작 시 broker.get_balance() 1회 fetch → symbol→hldg_qty 맵을 fill/rollback 핸들러에 전달(체결당 중복 호출 회피). dispatch 의 매도 잔고 맵과 동일 패턴(BAT-030 M2).

intent (REQ-061)

  • 매수 admission: 대상 슬롯이 INACTIVE → 초기 매수(M 캡 적용); HOLDING → 증액(M 캡 미적용 — active 수 불변); pending_* → already_active. (증액은 별도 intent 타입 없이 holding 슬롯 buy 로 검출)
  • 매도 admission: SellIntent.qty(선택) 추가 — 지정 시 부분 매도(slot.qty=order), 미지정 시 전량(slot.qty=None).

상태머신 (REQ-057)

  • _BASE_TRANSITIONS 에 (HOLDING, PENDING_BUY) 추가 → 13 transitions.

롤백/회귀

  • 전부 additive. 기존 초기 매수/전량 매도/safety-sell/kill-switch/reconcile 경로 무회귀(전량 매도 slot.qty=None·dispatch 브로커 정본은 기존 동작과 동일 결과).

체크포인트 결정

  • D1~D4 확정(브로커 정본 재동기화로 "주문 수량 vs 보유 수량" 충돌 해소, 별도 필드 없음). 구현 진행.