ADR-004 손절 위임과 비용 모델 중복 처리
상태
결정 (확정)
맥락
두 가지 결정을 함께 다룬다. 둘 다 “이미 존재하는 것을 어떻게 쓸 것인가”의 문제다.
- 손절을 누가 지키는가 — 봇이 손절가를 감시하다가 도달하면 매도 주문을 내는 방식은, 봇이 죽어 있는 동안 손절이 작동하지 않는다. 앱 가동률이 100%가 아님은 이미 확인됐다(2026-08-27, 스택 다운으로 일봉 배치 24일 누락).
- 비용 모델이 Python에만 있다 —
ml/app/cost_model.py의CostModel(43줄, 수수료·거래세·슬리피지 3파라미터)이 백테스트용으로 존재한다. 페이퍼 체결에도 같은 계산이 필요한데 집행은 Kotlin이다.
결정
1. 손절·익절을 토스 OCO 조건부 주문으로 브로커에 위임한다
토스가 /api/v1/conditional-orders로 OCO를 제공한다 — STOP(손절 트리거가) + PROFIT_RATE(목표 수익률)를 쌍으로 걸면 한쪽 체결 시 다른 쪽이 취소된다. live 매수 체결 직후 이를 등록한다.
OCO 등록에 실패하면 해당 포지션을 즉시 시장가로 청산한다. 손절 없는 포지션을 보유하지 않는다.
2. 비용 모델은 Kotlin에 재구현하되 파라미터를 단일 출처화한다
TradingCostModel 값 객체를 autotrading 도메인에 두고, 수수료율·거래세율·슬리피지율 3개를 설정으로 단일화한다. ml/app/cost_model.py와 동일 입력 → 동일 출력을 교차 검증 테스트로 강제한다.
근거
손절 위임
- 봇 가용성과 손실 방어를 분리한다. 브로커에 걸린 주문은 우리 프로세스가 죽어도, 배포 중이어도, 네트워크가 끊겨도 작동한다. 이것이 이 설계에서 가장 값싼 안전장치다.
- 봇이 감시하는 방식은 감시 주기만큼 손절이 늦고, 장 중 프로세스가 내려가면 방어가 통째로 사라진다.
- 등록 실패 시 청산하는 규칙이 없으면 “손절이 걸린 줄 알았는데 아닌 포지션”이 생긴다. 이건 손절이 아예 없는 것보다 나쁘다 — 방어되고 있다고 착각하게 만든다.
비용 모델 재구현
- 원본이 43줄·3파라미터·분기 없는 순수 함수다. 이걸 위해 체결 경로에 HTTP 홉을 만드는 비용이 중복 비용보다 크다.
- 진짜 위험은 코드 중복이 아니라 파라미터 드리프트(한쪽 수수료율만 바뀌는 것)다. 그래서 파라미터를 단일화하고 동일 픽스처 교차 검증으로 막는다.
- 젠트레이더가 “백테스트한 전략 정의를 그대로 실전에 배포”하는 것과 같은 목적 — 검증한 것과 집행하는 것이 같은 가정 위에 서야 한다.
고려한 대안
| 대안 | 미채택 사유 |
|---|---|
| 봇이 손절가 감시 후 매도 주문 | 봇이 죽으면 손절도 죽는다. 가동률에 손실 방어를 의존시키는 설계 |
| OCO 등록 실패를 경고만 하고 보유 유지 | 방어되고 있다는 착각을 만든다. 손절 없는 포지션이 조용히 쌓인다 |
비용 계산을 ml에 HTTP 위임 | 체결 경로에 네트워크 장애점 추가. 산술 한 줄에 홉을 붙이는 과설계 |
Python 로직을 Kotlin으로 옮기고 ml 쪽 제거 | 백테스트가 ml에 있어 그쪽도 계산이 필요하다. 제거하면 백테스트가 깨진다 |
| 언어 중립 공유 모듈 추출 | Kotlin·Python 간 실질적으로 불가능하거나 과도한 인프라(gRPC·WASM)를 요구한다 |
영향
order도메인에ConditionalOrderGatewayinterface +TossConditionalOrderGatewayImpl이 신규 파일로 추가된다. 기존OrderGateway는 수정하지 않는다 (Single Writer 보호).TradingPosition이oco_order_id를 보유한다. 포지션 종료·킬 스위치 시 조건부 주문도 함께 취소해야 한다.CONDITIONAL_ORDER레이트리밋(초당 5회)에 맞춰 OCO 등록을 체결 이벤트 단위로 직렬화한다.- 교차 검증 테스트가
ml픽스처와 Kotlin 픽스처 양쪽에 유지돼야 한다 — 비용 파라미터를 바꿀 때 두 테스트가 함께 갱신된다. paper모드는 OCO를 등록하지 않는다. 손절·익절 도달을PaperTradeExecutor가 가격 조회로 판정한다 (돈이 나가지 않으므로 브로커 위임이 불가능하고 불필요하다).