[BE-26] 집행 실패 원장

작업 내용 (설계 의도)

변경 사항

승급 게이트의 운영 지표 4항목 중 2항목이 원장을 보지 않습니다. 근거: _후속-DAG-codex-재검수 wave C, PRD FR-38·Success Metrics.

TierOperationMetrics#satisfiesOperationalBar 는 ① 무중단 20거래일 ② 집행 실패 0건 ③ 리스크 한도 위반 0건 ④ 슬리피지 ±0.3%p 를 모두 요구합니다. 이 중 ②·③ 의 실제 상태는 이렇습니다.

지표현재결과
executionFailureCountRiskTierPromotionDomainService.kt:113 — 저장 경로 없이 호출부가 넘기는 숫자. 주석이 “값의 출처가 생기기 전까지 승급 게이트의 이 항목은 사실상 무효” 라고 자인합니다호출부가 0을 넘기면 무조건 통과
riskLimitViolationCount:131NO_RISK_LIMIT_VIOLATION 상수 0항상 통과

PRD Success Metrics 는 “자동 집행 실패 0건 / 주 — 감사 로그 집계”, “리스크 한도 위반 집행 0건 — 감사 로그 + 마감 점검” 으로 측정 방법까지 지정했는데, 집계할 감사 원장이 없습니다. 테스트도 실경로로 만들 수 없는 riskLimitViolationCount = 1 을 직접 주입해 판정을 확인하므로, 안전 주장을 고정하지 못합니다.

이 티켓은 그 원장을 만듭니다.

실패 유형

유형언제근거
ORDER_SUBMIT_FAILED주문 제출 자체가 실패재시도 가능 — 접수되지 않은 것이 확실합니다
ACCEPTANCE_UNCONFIRMED제출은 했으나 접수 여부를 확인할 수 없음TDD:186 “재시도하면 안 되는 집행 실패. 접수 여부 불확실”
PROTECTION_REGISTRATION_FAILEDOCO 손절·익절 등록 실패FR-5 — 등록 실패 시 즉시 청산합니다. 손절 없는 포지션을 보유하지 않습니다
LIQUIDATION_FAILED위 청산마저 실패손절 없는 포지션이 남은 상태 — 즉시 사람이 봐야 합니다
RISK_LIMIT_VIOLATION마감 점검에서 한도 위반 집행이 드러남아래 “판단” 참조

판단 — riskLimitViolationCount 를 유지하되 재정의합니다

현재 구조에서는 리스크 게이트를 우회하는 집행 경로가 없으므로 “게이트를 통과하지 않고 집행된 건수” 는 정의상 항상 0 입니다. 항상 0인 조건을 승급 조건에 두면 조건이 없는 것과 같습니다. 세 가지를 놓고 판단했습니다.

내용채택 여부
제거승급 조건에서 이 항목을 뺀다✗ PRD Success Metrics 에 명시된 P0 지표라 PRD 변경이 선행돼야 합니다
상수 유지지금처럼 0을 둔다✗ 게이트 자체의 결함(사이징 버그·경합으로 한도를 넘긴 집행)은 게이트를 믿는 방식으로는 영원히 잡히지 않습니다
재정의”게이트를 우회한 집행” 이 아니라 “집행된 체결이 사후 검증에서 한도를 위반한 것으로 드러난 건수” 로 바꾼다✓ PRD 가 지정한 측정 방법(“감사 로그 + 마감 점검”)과 정확히 일치합니다

재정의된 판정은 마감 점검이 당일 체결 원장을 다시 훑어 ① 종목당 투입 금액이 단계 상한 초과 ② 보유 종목 수 상한 초과 ③ 일일 손실 한도 도달 이후의 신규 매수 존재 를 검사하고, 위반을 RISK_LIMIT_VIOLATION 으로 원장에 남기는 방식입니다.

단 사후 검증 판정 로직은 이 티켓 범위 밖입니다 — 정산 경로(TradingSettlementDomainService)는 BE-24 가 단독 소유합니다. 이 티켓은 유형과 집계 시그니처까지 정의하고, 판정을 정산에 붙이는 작업은 별도 후속 티켓으로 분리합니다. 그때까지 이 항목은 여전히 0을 반환하므로, 원장이 연결되기 전에는 T2 승급을 확정하지 않는다는 운영 조건을 함께 남깁니다.

작업 범위

  • ExecutionFailure 도메인(신규) — 거래일·실행 모드·종목·제안 id·실패 유형·사유 원문·토스 주문 id·발생 시각.
  • ExecutionFailureRepository(신규) — save(failure), 기간·모드별 집계 조회. 사다리 관찰창과 맞물리므로 거래일 범위로 셉니다.
  • MySQL 구현 + 통합 테스트.

경계 — 기록하는 호출부는 이 티켓이 아닙니다

실패를 실제로 기록하는 곳은 AutoTradingDomainService(BE-11 소유, 미머지)입니다. 이 티켓은 원장과 조회 계약만 만들고, 호출부 연결은 BE-11 in-place 수정에 위임합니다. 이 티켓에서 AutoTradingDomainService 를 수정하지 않습니다 — 같은 파일을 두 곳이 건드리면 머지 충돌입니다.

RiskTierPromotionDomainService#collectMetricsexecutionFailureCount 인자를 원장 집계로 바꾸는 일도 BE-25 소유 파일이므로, 이 티켓은 집계 시그니처를 제공하고 배선은 BE-25 가 소비합니다.

롤백: 신규 테이블·신규 클래스만 추가하고 읽는 경로가 아직 없어, 이전 태그 재기동으로 되돌리기가 안전합니다.

의존

  • DB-02 (execution_failures 테이블)

다이어그램

처리 흐름

sequenceDiagram
    participant Svc as AutoTradingDomainService (BE-11)
    participant Repo as ExecutionFailureRepository
    participant Tier as RiskTierPromotionDomainService (BE-25)
    Svc->>Repo: save(ORDER_SUBMIT_FAILED, 종목, 모드)
    Svc->>Repo: save(ACCEPTANCE_UNCONFIRMED, 재시도 없음)
    Tier->>Repo: countIn(관찰창 시작일, 종료일, PAPER)
    Repo-->>Tier: 2건
    Tier->>Tier: 운영 지표 미달 — 승급 후보 없음

클래스 의존

flowchart LR
    Failure[ExecutionFailure] --> Type[ExecutionFailureType]
    Failure --> Mode[ExecutionMode]
    Repo[ExecutionFailureRepository] --> Failure
    Repo -.->|implements| Impl[ExecutionFailureRepositoryImpl]
    Impl --> Entity[ExecutionFailureEntity]
    Entity --> Table[(execution_failures)]

테스트 케이스

  • 주문 제출 실패를 기록하면 유형·거래일·모드·종목·사유 원문이 원장에 남는다.
  • 접수 확인 불가 실패가 재시도 없이 1건으로 기록된다(제출 이전 실패라 토스 주문 id 는 NULL 이다).
  • 기간·모드로 집계하면 다른 모드의 실패가 포함되지 않는다.
  • 실패가 없는 기간을 집계하면 0을 반환한다(경계).
  • 같은 제안의 실패를 두 번 기록하면 두 행이 남는다(append-only — 재시도 이력이 보존돼야 한다).
  • 관찰창 첫날의 실패는 집계에 포함되고, 하루 앞선 실패는 포함되지 않는다(경계값).
  • 집계가 1건 이상이면 승급 게이트가 운영 지표 미달로 판정한다.
  • 제안 없이 발생한 청산 실패도 proposal_id NULL 로 기록된다.