Etc

종목마다 따로 놀던 최고점이 멀쩡한 계좌를 손절 모드로 밀어넣었다

문제: 손실이 없는데 손절 모드였다

equity_peak의 정의:

  • 계좌 자산이 지금까지 찍은 최고점 기록
  • 여기서 얼마나 떨어졌는지(MDD, Max Drawdown) 계산
  • 일정 이상 하락 시 신규 매수 차단

그런데 실제 저장 상태는 달랐다.

  • 저장 단위: 종목별 전략 인스턴스마다 따로
  • 원인: 종목마다 전략 객체 생성 시점이 상이 → 그 시점의 잔고를 각자 자기 peak으로 채택

2026년 7월 30일 실측 — 운용 중인 46개 종목의 peak 값이 네 갈래로 분열.

peak 값 종목 수 시점
최고 (기준 100%) 7 A
최고 대비 -0.1% 24 B ← 차단 발동 기준
최고 대비 -15.4% 11 C
최고 대비 -78.3% 4 초기 자산(전부 ETF)
  • 계산 기준: 가장 오래되고 낮은 값 → MDD -18.4%
  • 결과: 신규 매수 전면 차단

그런데 실제로는 수익 중이었다

지표
1년 실현손익 플러스
승률 62.1% (18승 11패)
Profit Factor 4.27
대출·미수 없음
  • 손절 로직의 판단 근거: 실제 자산 흐름이 아니라 네 갈래로 오염된 기준값 중 가장 낮은 것

시스템은 잘못된 결론에 아주 논리적으로 도달한 셈이다.

원인을 어떻게 찾았나

realtime_engine.py의 전략 생성 로직:

  • 전략은 종목별로 개별 생성
  • 각자 생성 시점의 잔고를 peak으로 설정
  • import_state — 저장된 옛 peak를 그대로 우선 (코드 주석에도 명시)

  • 구조적 귀결: 종목별 생성 시점 차이로 값 분열 → 한 번 갈라지면 영구 고착
  • 버그의 정체: 로직 자체의 계산 실수가 아니라 “이 값을 누가 소유하는가”의 설계 오류
  • 구체적으로: equity_peak은 계좌 전체의 개념인데 종목 단위 상태에 탑재

어떻게 고쳤나

  1. 계좌 단위 상태 분리 — 상태 저장소에 __account__ 예약 키 신설 (종목 코드는 숫자·영문 조합이라 충돌 없음)
  2. 기존 분열 데이터 1회성 마이그레이션 — 여러 값 중 가장 낮은 값이 아니라 가장 높은 값 선택. peak는 정의상 최고점이고, 낮은 값을 고르면 MDD를 실제보다 작게 계산해 위험 방어가 느슨해짐. 보수적인 쪽 채택
  3. export_state에서 equity_peak 제거, export_account_state로 분리 — 이걸 안 하면 종목 파일로 값이 재유출돼 같은 분열 재발 가능
  4. get_restore_state()가 종목별 상태 + 계좌 상태를 합쳐 반환 — 호출하는 쪽에서 두 상태를 합치게 두면 누락 위험. 실제로 테스트 작성 중 재현됨
  • 검증 절차: 프로덕션 상태 파일 사본으로 먼저 실행 후 결과 확인
[StrategyState] 종목별 equity_peak 46종목/4개 값을 계좌 단위로 통합
  → A (기존 값: A, B, C, D — 가장 높은 A 채택)

계좌 peak       : A
종목에 남은 peak : 0  (0이어야 함)
잔여 상태       : consecutive_losses / cooldown_remaining / peak_price 보존

테스트 결과:

  • 통과 199건, 그중 신규 작성 31건
  • 신규 테스트 항목
    • 분열 재현
    • 최댓값 선택 로직
    • 마이그레이션의 멱등성(여러 번 실행해도 결과 동일)
    • 계좌 키가 종목 상태와 격리되는지
    • 복원 후 peak이 단일 값으로 유지되는지

일부러 하지 않은 것

  • 이 작업은 상태 구조만 수정. 차단 자체는 미해제
  • 이유 — 통합된 peak 값(가장 높은 A 시점 값)이 실제로 있었던 자산인지, 집계 오류가 섞인 값인지 이 시점에 미확정
  • 그 판단과 차단 해제는 완전히 별개의 문제로 분리

  • 수정 이후: peak 재분열 가능성 없음
  • 자산이 실제로 줄어드는 상황(예: 출금)에서도 계좌 단위 단일 값으로 일관 관리

남는 교훈

이 버그는 처음부터 “계산이 틀렸다”가 아니라 “상태를 누가 갖고 있어야 하는가”를 잘못 설계해서 생겼다. 로직 자체는 각 조각이 다 말이 됐다 — 전략은 종목별로 존재하고, 생성 시점 잔고를 시작점으로 잡는 것도 자연스럽다. 문제는 “계좌 전체의 최고점”이라는, 본질적으로 전역(global)인 값을 지역(local) 상태에 얹은 것뿐이었다.

시스템이 틀린 답을 낼 때 항상 로직부터 의심하게 되는데, 이번 경우엔 로직보다 한 단계 아래 — 그 로직이 참조하는 상태가 애초에 누구 것이었는지 — 를 봐야 했다.