[BE-05] TradingPosition·TradingFill 애그리게이트

작업 내용 (설계 의도)

변경 사항

포지션과 체결을 한 티켓으로 묶는다 — 체결이 포지션의 수량·평단·실현손익을 바꾸므로 두 Entity가 강결합이고, 나누면 서로를 기다리게 된다.

  • TradingPosition — 수량·평단·손절가·익절가·OCO 주문 ID. applyBuyFill()이 부분 체결을 누적 반영하고, applySellFill()이 실현손익을 산출한다. isStopLossHit(price)·isTakeProfitHit(price)로 도달 판정을 캡슐화한다.
  • TradingFill — 체결가·실효단가·비용·실현손익. execution_mode로 paper/live를 구분해 같은 테이블에 저장하되 집계는 분리한다(PRD FR-23).

같은 종목·같은 모드에 열린 포지션이 하나만 존재해야 한다 — 이 불변식은 DB 유일성 제약(DB-01)과 findOpenBy() 양쪽으로 지킨다.

의존

  • BE-01 (도메인 계약), DB-01 (스키마)

다이어그램

처리 흐름

sequenceDiagram
    participant Exec as TradeExecutor
    participant Pos as TradingPosition
    participant Fill as TradingFill
    Exec->>Fill: 매수 체결 생성
    Exec->>Pos: applyBuyFill(fill)
    Pos-->>Exec: 수량·평단 갱신
    Exec->>Pos: isStopLossHit(현재가)
    Pos-->>Exec: true

클래스 의존

flowchart LR
    Pos[TradingPosition] --> Fill[TradingFill]
    Fill --> Mode[ExecutionMode]
    Fill --> Reason[SellReason]
    PosRepo[TradingPositionRepository] -.->|implements| PosImpl[TradingPositionRepositoryImpl]
    FillRepo[TradingFillRepository] -.->|implements| FillImpl[TradingFillRepositoryImpl]
    PosImpl --> T1[(trading_positions)]
    FillImpl --> T2[(trading_fills)]

테스트 케이스

  • 매수 체결 2건을 적용하면 수량이 합산되고 평단이 가중평균으로 갱신된다(부분 체결).
  • 매도 체결 시 실현손익이 실효단가 기준으로 산출된다.
  • 전량 매도 시 포지션이 종료 상태가 된다.
  • 현재가가 손절가 이하면 isStopLossHit()이 true를 반환한다(경계값 포함).
  • 현재가가 손절가보다 1원 높으면 false를 반환한다(경계값).
  • findAllOpen(PAPER)가 LIVE 포지션을 반환하지 않는다(모드 분리, TestContainers MySQL).
  • 같은 종목·모드로 열린 포지션을 두 개 저장하면 제약에 걸린다.