[BE-18] RiskTier 총 노출 비율 추가

이 티켓은 BE-30 으로 흡수됐습니다RiskGate.kt·RiskTier.kt·TradeCandidate.kt 를 같이 건드리므로 분리하면 Single Writer 충돌입니다 (Tickets/BE-30-게이트-입력불변식-총노출.md). 이 파일은 이력 보존용이며 착수 대상이 아닙니다.

BE-30 이 흡수한 이유가 하나 더 있습니다 — codex 재검수에서 totalExposureLimit검증 없는 외부 입력이라 실제 T1 상한 50만원을 넘겨도 승인되는 p0 가 나왔습니다. 값의 출처를 만드는 것(이 티켓)과 입력을 검증하는 것(BE-30)이 같은 결함의 양면입니다.

작업 내용 (설계 의도)

변경 사항

RiskTiertotalExposureRatio를 보유해 종목당 비율·일일 손실 한도와 대칭을 이루게 한다. 현재 RiskGateRiskPolicySnapshot.totalExposureLimit을 외부 주입으로 받는데, 그 값의 출처가 코드 어디에도 없어 호출부(BE-11)가 값을 지어낼 위험이 있다.

값은 이론상 최대치positionRatio × maxOpenPositions다 (2026-08-28 확정, TDD “RiskGate 입력 계약”).

단계종목당보유 상한총 노출 상한
T110%5종목50%
T215%5종목75%
T320%4종목80%

종목당 한도와 보유 수 한도가 이미 같은 상한을 강제하므로, 이 검사는 구속력 있는 제약이 아니라 회계 드리프트를 잡는 안전망이다 — 포지션 종료 누락 등으로 장부 노출이 실제보다 크게 잡히면 걸린다. 별도 정책 컬럼을 두지 않는다(단계와 무관하게 움직일 이유가 없고, 컬럼을 두면 세 한도의 정합을 사람이 지켜야 한다).

의존

  • 없음 (main에 머지된 RiskTier 수정)

다이어그램

처리 흐름

해당 없음 — enum에 파생 계산 메서드를 추가하며 협력 객체 간 흐름이 없다.

클래스 의존

flowchart LR
    Tier[RiskTier] --> Limit[totalExposureLimit 산출]
    Svc[AutoTradingDomainService] --> Tier
    Svc --> Snap[RiskPolicySnapshot]

테스트 케이스

  • RiskTier.T1.totalExposureLimit(100만원)이 50만원을 반환한다.
  • RiskTier.T2.totalExposureLimit(100만원)이 75만원을 반환한다.
  • RiskTier.T3.totalExposureLimit(100만원)이 80만원을 반환한다 — 보유 상한이 4종목이라 T2(75%)보다 크되 T2×5(=100%)는 아니다.
  • 모든 단계에서 totalExposureLimit == positionLimit × maxOpenPositions가 성립한다(파생 관계 불변식).
  • 자본이 0이면 총 노출 상한도 0이다(경계값).