[BE-25] 사다리 판정 창 분리·전환 이력

작업 내용 (설계 의도)

변경 사항

리스크 사다리 판정이 하나의 최근 20거래일 창으로 승급도 강등도 재단하고 있습니다. 창 하나로 성격이 다른 다섯 가지 판단을 처리해 다섯 곳이 동시에 틀어졌습니다. 근거: _후속-DAG-codex-재검수 wave C, PRD FR-38·FR-4.

1. 강등 조건이 시간이 지나면 저절로 풀립니다

RiskTierPromotionDomainService.kt:124 는 최근 20거래일만 읽고, :133 이 그 20일 합계를 cumulativeRealizedPnl 로 채웁니다. 이 값이 그대로 reachedDemotionDrawdown() 의 입력이 됩니다.

강등 트리거는 총자본 -5%(자본 100만원 기준 -50,000원)입니다. 21거래일 전에 낸 손실은 창 밖으로 밀려나 합계에서 사라지므로, 손실을 회복하지 않았는데도 누계가 줄어 강등 조건이 해제됩니다. FR-4 는 누적 손실을 MDD(10%) 기준으로 보는데, 20일 슬라이딩 합계는 누적이 아닙니다.

승급 관찰창(최근 20거래일)과 강등 원장(전체 누적 실현손익·MDD)을 분리합니다.

2. 같은 손실로 매일 다시 강등됩니다

:146evaluate() 는 매 실행마다 같은 -5% 손실을 새로 평가합니다. T3 에서 T2 로 강등된 다음 날, 손실이 그대로 남아 있으니 또 조건을 만족해 T1 까지 내려갑니다. “이 임계 도달 이벤트는 이미 한 번 소비했다” 는 기준점이 없습니다.

강등 기준선을 도입합니다. 강등을 적용할 때 그 시점의 누적 실현손익을 기준선으로 이력에 남기고, 다음 판정부터는 기준선 대비 추가로 -5% 가 더 빠진 경우에만 다시 강등합니다.

3. 승급 직후 바로 다음 단계 후보가 됩니다

:197promotionCandidateOf() 는 T2→T3 판정에도 같은 최근 20거래일 지표를 쓰고 t > 2 만 덧붙입니다. 정책이 T2 로 바뀐 직후에도 과거 T1 기간의 20거래일을 재사용하므로 곧바로 T3 후보가 나옵니다. PRD 는 T3 승급 조건을 “T2 를 20거래일 추가 유지” 로 정의합니다.

→ 관찰창을 마지막 단계 전환 시점 이후의 거래일로 한정합니다. 체류 거래일이 20일 미만이면 승급 후보를 내지 않습니다.

4. 앱이 꺼져 있던 날을 무중단으로 셉니다

TradingDay#isUninterrupted!halted 입니다. 이건 “가동했다” 가 아니라 “리스크 중단이 없었다” 입니다. :124 는 최근 20개 행을 가져올 뿐 날짜 연속성을 검사하지 않으므로, 앱이 며칠 꺼져 행 자체가 안 생기면 그 구멍을 조용히 건너뛰고 과거 정상 행 20개를 모아 승급시킵니다. PRD Q-9 이 “앱 가동률이 곧 표본 누적률” 이라 짚은 문제이고, 2026-08-27 에 24일 누락이 실제로 발견된 이력이 있습니다.

→ 무중단을 거래 캘린더 연속성으로 판정합니다. 판정일 기준 직전 20거래일이 모두 행으로 존재하고 전부 halted = false 여야 통과입니다. 별도 거래 캘린더 원본이 없으므로 주말을 제외한 평일에 행이 없으면 결측 = 중단으로 봅니다. 공휴일에도 행이 없어 승급이 늦어질 수 있지만, 승급을 늦추는 방향의 오차는 안전하므로 그대로 둡니다(공휴일 캘린더 도입은 별도 과제).

5. 전환 이력이 없어 위 결함을 감사할 수 없습니다

AutoTradingPolicy.kt:60changeTierTo() 는 현재 단계와 tierChangedAt 만 덮어씁니다. FR-38 의 “단계 전환 이력을 기록한다” 를 충족하지 못하고, 2번의 중복 강등이 실제로 일어나도 사후에 확인할 수단이 없습니다. 승급 판정이 수익률이 아니라 운영 지표 기반이므로, 그 지표가 좋은 예측자였는지 사후 평가하려면 전환 시점과 그때의 지표가 함께 남아야 합니다.

RiskTierTransition append-only 이력을 남깁니다 — 이전 단계·다음 단계·사유·판정 시점 지표·기준선이 될 누적 실현손익·전환 시각.

작업 범위

  • RiskTierPromotionDomainService#collectMetrics 를 승급 관찰창과 강등 원장 두 입력으로 분리합니다.
  • 관찰창을 마지막 전환 시점 이후로 한정하고, 체류 거래일 수를 승급 조건에 반영합니다.
  • 무중단 판정을 결측일 검사 포함으로 바꿉니다.
  • 강등 기준선을 최신 강등 이력에서 읽어 중복 강등을 차단합니다.
  • RiskTierTransition·RiskTierTransitionRepository(신규 도메인) + MySQL 구현을 만들고, 강등 적용 경로에서 기록합니다.

파일 경계 — 건드리지 않는 것

  • AutoTradingPolicy.kt 는 BE-23 이 단독 소유합니다. 이 티켓은 정책 Entity 를 수정하지 않고, 전환 이력 기록을 RiskTierPromotionDomainService 에서 수행합니다. 정책에 필드가 필요해지면 직접 고치지 말고 BE-23 으로 되돌려 보고합니다.
  • TradingDayRepository.kt 는 BE-24 가 단독 소유합니다. 관찰창 조회에 모드 인자가 필요하면 BE-24 에 요청합니다.
  • 승급 확정 경로(사용자 확인 후 정책 단계를 올리는 흐름)는 운영 UseCase(BE-14) 소유입니다. 이 티켓은 RiskTierTransitionRepository 계약과 강등 경로의 기록까지 만들고, 승급 확정 시점의 기록 연결은 BE-14 in-place 수정에 위임합니다.

롤백: 이력 기록은 신규 테이블 쓰기라 되돌려도 기존 동작에 영향이 없습니다 — 이전 태그 재기동으로 판정 로직만 되돌립니다(이력 행은 남아도 무해합니다).

BE-22 흡수

기존 BE-22 는 이 티켓이 흡수합니다 — 같은 파일을 건드려 그대로 두면 Single Writer 충돌입니다. 스키마는 DB-02 로 갔습니다.

의존

  • DB-02 (risk_tier_transitions 테이블)

다이어그램

처리 흐름

sequenceDiagram
    participant Tier as RiskTierPromotionDomainService
    participant Days as TradingDayRepository
    participant Hist as RiskTierTransitionRepository
    participant Policy as AutoTradingPolicy
    Tier->>Hist: 최신 강등 이력 조회 (기준선)
    Tier->>Days: 전체 누적 손익 + 전환 이후 관찰창
    Days-->>Tier: 원장·관찰창 (결측일 포함)
    Tier->>Tier: 기준선 대비 추가 -5% 인가
    Tier->>Policy: demoteTier()
    Tier->>Hist: save(T3→T2, 사유, 지표, 새 기준선)

클래스 의존

flowchart LR
    Tier[RiskTierPromotionDomainService] --> Metrics[TierOperationMetrics]
    Tier --> DayRepo[TradingDayRepository]
    Tier --> HistRepo[RiskTierTransitionRepository]
    Tier --> Alert[AutoTradingAlertGateway]
    HistRepo -.->|implements| HistImpl[RiskTierTransitionRepositoryImpl]
    HistImpl --> Entity[RiskTierTransitionEntity]
    Entity --> Table[(risk_tier_transitions)]
    Trans[RiskTierTransition] --> HistRepo

테스트 케이스

  • 21거래일 전 손실이 관찰창에서 빠져도 강등 원장에는 남아 강등 판정이 유지된다.
  • 강등이 적용된 직후 같은 손실로 다시 평가하면 재강등되지 않는다(기준선 소비).
  • 기준선 이후 총자본 -5% 가 추가로 발생하면 한 단계 더 강등된다.
  • T2 로 전환된 직후에는 T1 기간 거래일을 세지 않아 T3 후보가 되지 않는다.
  • T2 전환 이후 20거래일을 채우고 엣지 t > 2 를 만족하면 T3 후보가 된다.
  • 관찰창 안에 행이 없는 평일이 있으면 무중단 판정이 실패해 승급 후보가 나오지 않는다.
  • 관찰창 안의 주말은 결측으로 세지 않는다(경계값).
  • 강등이 적용되면 이전 단계·다음 단계·사유·판정 시점 지표가 이력에 남는다.
  • 최하 단계에서의 MDD 경고는 단계가 바뀌지 않으므로 이력을 만들지 않는다.
  • 이력 조회가 전환 시각 역순으로 반환된다.