자동매매봇 TDD

Background

PRD|자동매매봇 PRD]의 기술 설계를 정의한다. PRD가 정한 전제는 셋이다 — ① 검증된 엣지가 없으므로 목적은 수익이 아니라 집행 인프라 확보와 실시간 갭 측정, ② 토스에 sandbox가 없으므로 paper 안전성은 우리 코드가 유일한 방어선, ③ 리스크 사다리로 단계적 확대하되 승급 트리거는 수익률이 아닌 운영 지표.

이 설계의 최우선 목표는 “돈을 잘 버는 구조”가 아니라 **“틀렸을 때 손실이 설계된 한도를 넘지 않는 구조”**다.

Overview

  • 무엇을: backendautotrading 도메인을 신설해 신호 → 리스크 게이트 → 주문 집행 → 체결·손익 추적 루프를 만든다.
  • : 신호는 있으나 집행 경로가 없어 신호 품질을 측정할 수 없고, 손실을 끊는 장치가 없다.
  • 어떻게: 기존 signal·stock·order·account 도메인을 application 레이어에서 조합하고, paper/live 집행을 전략 패턴으로 분리한다. PaperTradeExecutor는 토스 Gateway를 주입받지 않아 실주문이 타입 수준에서 불가능하다.

Terminology

용어정의
Proposal(제안)신호·사이징·리스크 판정을 거친 주문 후보. 거부된 것도 감사 목적으로 전량 기록한다
RiskGate(리스크 게이트)모든 주문이 반드시 통과하는 필터. 우회 경로가 없다
RiskTier(사다리 단계)종목당 비율·보유 수·일일 한도를 묶은 세트 (T1/T2/T3)
ExecutionMode(실행 모드)PAPER(모의 체결 기록) / LIVE(토스 실주문)
OCOOne-Cancels-Other. 손절가·목표수익률을 쌍으로 걸어 한쪽 체결 시 다른 쪽이 취소되는 토스 조건부 주문
KillSwitch(킬 스위치)신규 주문 차단 + 미체결 취소 + 대기 제안 무효화를 1초 내 수행하는 즉시 정지 장치
TradingDay(거래일)자동매매의 하루 단위 상태. 당일 실현손익·집행 건수·중단 여부를 보유
Slippage(슬리피지)의도 가격과 실제 체결가의 차이

Define Problem

AS-IS

실제 코드 근거는 다음과 같다.

  • 신호는 있다. 4팩터 합성은 ml/app/signal.py#combine이 수행하고, 결과는 signal_snapshots 테이블(symbol UNIQUE, signal_json·refreshed_at)에 적재된다. 조회는 GetSignalSnapshotUseCase.
  • 주문 경로도 있다. PlaceOrderUseCase#executeOrderDomainService#placeOrderTossOrderGatewayImpl#placeOrder(POST /api/v1/orders)가 실동작한다. 정정·취소·미체결 조회까지 OrderGateway에 정의돼 있다.
  • 그런데 둘이 이어져 있지 않다. 신호를 읽어 주문을 만드는 코드는 존재하지 않는다. grep -r "OrderProposal\|RiskGate" backend/src/main 결과 0건.
  • 리스크 레이어가 없다. 포지션 사이징·손절가 관리·손실 한도·보유 종목 수 제한이 코드에 없다.
  • 조건부 주문을 안 쓴다. 토스는 /api/v1/conditional-orders로 OCO(STOP + PROFIT_RATE)를 제공하지만 OrderGateway에 해당 메서드가 없다.
  • 비용 모델은 Python에만 있다. ml/app/cost_model.pyCostModel.buy/sell(43줄, 수수료·거래세·슬리피지 3파라미터)이 백테스트 전용으로 존재하고, Kotlin 쪽에는 없다.
  • 가격 캐시는 문자열이다. stock_price_cache.last_pricevarchar(50)이라 금액 계산에 그대로 쓸 수 없다.
  • 일봉에 통화 컬럼이 없다. daily_pricesclose decimal(18,4) 하나에 KRW·USD가 섞여 있다(199종목 중 82종목이 미국 종목).

TO-BE

  • backendautotrading 도메인 신설 — 제안·포지션·체결·거래일·정책을 소유한다.
  • 모든 주문이 RiskGate를 통과한다. 게이트를 우회하는 집행 경로를 만들지 않는다.
  • 집행은 TradeExecutor 인터페이스의 두 구현으로 갈린다. PaperTradeExecutor는 외부 Gateway 의존이 없다.
  • live 매수 체결 직후 OCO 조건부 주문을 등록한다. 등록 실패 시 즉시 청산한다.
  • 크로스 컨텍스트(signal·stock·order·account) 조회·조합은 application 레이어가 담당한다.

Architecture Benchmarking

제품/사례해결 방식참고할 패턴미참고 사유
QuantConnect LEAN (Risk Management)리스크 관리를 알고리즘과 분리된 프레임워크 계층으로 두고 최대 낙폭 제한·주문 크기 제한·자동 청산을 강제. 전략 코드가 리스크를 우회할 수 없다리스크 게이트를 전략(신호) 바깥의 독립 컴포넌트로 배치. 집행 경로가 게이트를 통과하지 않을 방법이 없게 구조화다자산·다브로커 추상화는 미참고 — 토스 단일 브로커·KRX 단일 시장이라 과설계
freqtrade (Configuration, Bot Basics)dry-run 모드로 모의 자금 매매. dry-run에서는 거래소 API 키 자체가 필요 없다 — 실주문 경로가 설정이 아니라 의존성 수준에서 없다PaperTradeExecutorOrderGateway를 주입받지 않게 설계. 실수로 실주문이 나가는 것을 타입 시스템이 막는다 (PRD FR-35)FreqAI(ML 계층)는 미참고 — ADR-004 게이트상 엣지 미검증 단계에서 모델은 범위 밖
뉴지스탁 젠트레이더 (genport)백테스트한 전략 정의를 그대로 실전 예약매매로 배포. 검증한 것과 집행하는 것이 같은 정의를 공유백테스트(ml/app/cost_model.py)와 페이퍼 체결이 동일한 비용 파라미터를 쓰게 단일화전략 마켓플레이스·전략 공유는 미참고 — 사용자 1명 시스템

Possible Solutions

방안 1 — 실행 주체를 어디에 둘 것인가

방안설명왜 채택 / 미채택
backend 단독 (채택)autotrading 도메인·스케줄러·집행을 전부 backend(:50000)에 둔다주문·포지션·리스크는 강한 일관성이 필요하고, 필요한 데이터(signal_snapshots·orders·accounts·stock_price_cache)와 토스 주문 Gateway가 전부 backend 소유다. 네트워크 홉이 없어 킬 스위치가 같은 프로세스 안에서 즉시 반영된다 (PRD NFR 1초)
worker 오케스트레이션worker(:50002)가 주기 트리거하고 backend API를 호출미채택. ADR-008/010의 worker 역할(stateless 오케스트레이션)에는 부합하나, 집행은 stateless가 아니다. 홉이 늘면 킬 스위치 전파·주문 멱등·부분 실패 처리가 전부 분산 문제가 된다. 지금 규모에 과한 방안
ml 계산 위임사이징·리스크 판정을 ml(:50003)에 맡긴다미채택. 사이징·게이트는 산술 몇 줄이지 ML이 아니다. 홉만 늘고 장애점이 생긴다

방안 2 — 비용 모델 중복을 어떻게 다룰 것인가

ml/app/cost_model.py가 이미 있는데 Kotlin에도 필요하다.

방안설명왜 채택 / 미채택
Kotlin 재구현 + 파라미터 단일화 (채택)TradingCostModel 값 객체를 Kotlin에 두되, 수수료율·거래세율·슬리피지율 3개 파라미터를 설정으로 단일 출처화하고 동일 픽스처 교차 검증 테스트로 드리프트를 막는다원본이 43줄·3파라미터·분기 없는 순수 함수다. 이걸 위해 HTTP 홉을 만드는 것이 훨씬 비싸다. 드리프트 위험은 교차 검증 테스트로 상쇄된다
ml에 HTTP 위임체결마다 ml을 호출미채택. 체결 경로에 네트워크 장애점을 넣는다. 산술 한 줄에 홉을 붙이는 것은 과설계
공유 라이브러리 추출언어 중립 계산 모듈미채택. 언어가 달라 실질적으로 불가능하거나 과도한 인프라를 요구한다

방안 3 — paper 격리를 어떻게 보장할 것인가

토스에 sandbox가 없어 같은 자격증명·같은 엔드포인트로 실주문이 나간다.

방안설명왜 채택 / 미채택
의존성 분리 (채택)TradeExecutor 인터페이스의 두 구현. PaperTradeExecutorOrderGateway·ConditionalOrderGateway생성자에서 받지 않는다freqtrade의 dry-run이 API 키 없이 도는 것과 같은 원리. 실주문이 “설정 실수로 나갈 수 있는 것”이 아니라 참조할 대상이 없어 컴파일되지 않는 것이 된다
런타임 if 분기집행 직전 if (mode == PAPER) return미채택. 분기가 늘면 언젠가 하나가 빠진다. 유일한 방어선을 사람의 주의력에 맡기는 설계
별도 프로세스페이퍼 전용 앱 분리미채택. 제안→승인 흐름이 중복되고 배포·운영이 두 배가 된다

Detail Design

시스템 역할 경계

단위역할소유 데이터/책임노출 인터페이스의존
backend / autotrading자동매매 전 과정auto_trading_policy·trading_days·order_proposals·trading_positions·trading_fillsREST(제안 조회·승인·킬 스위치·상태), 스케줄러signal·stock·order·account 도메인 (application 조합)
backend / order토스 주문 접수·정정·취소 + OCO 조건부 주문(신규)ordersOrderGateway, ConditionalOrderGateway(신규)토스 API
backend / signal4팩터 신호 스냅샷signal_snapshotsGetSignalSnapshotUseCaseml
backend / stock종목 마스터·현재가stock·stock_price_cacheStockDomainService토스 API
backend / account계좌·매수가능금액accountsAccountDomainService토스 API
ml신호 산출·백테스트없음 (stateless)HTTP
worker·aggregator·frontend변경 없음

autotrading은 다른 도메인의 테이블을 직접 읽지 않는다 (no-crosscontext-raw-read). 조회는 소유 도메인의 DomainService를 거치고, application 매퍼가 autotrading의 값 객체로 변환한다.

인터페이스 시그니처

domain — Repository

interface AutoTradingPolicyRepository {
    fun load(): AutoTradingPolicy          // 싱글톤 1행. 없으면 기본값(PAPER/T1/killSwitch off)으로 생성
    fun save(policy: AutoTradingPolicy): AutoTradingPolicy
}
 
interface TradingDayRepository {
    fun findBy(tradeDate: LocalDate): TradingDay?
    fun save(tradingDay: TradingDay): TradingDay
}
 
interface OrderProposalRepository {
    fun save(proposal: OrderProposal): OrderProposal
    fun saveAll(proposals: List<OrderProposal>): List<OrderProposal>
    fun findBy(proposalId: Long): OrderProposal?
    fun findAllPending(): List<OrderProposal>
    fun findAllIn(tradeDate: LocalDate): List<OrderProposal>
}
 
interface TradingPositionRepository {
    fun findAllOpen(mode: ExecutionMode): List<TradingPosition>
    fun findOpenBy(symbol: String, mode: ExecutionMode): TradingPosition?
    fun save(position: TradingPosition): TradingPosition
}
 
interface TradingFillRepository {
    fun save(fill: TradingFill): TradingFill
    fun findAllIn(tradeDate: LocalDate, mode: ExecutionMode): List<TradingFill>
}

domain — Gateway

/** 토스 OCO 조건부 주문. order 도메인 소유 (신규 파일 — 기존 OrderGateway 미수정) */
interface ConditionalOrderGateway {
    /** 손절가·목표수익률을 쌍으로 등록하고 conditionalOrderId 를 반환한다. */
    fun placeOcoOrder(spec: OcoOrderSpec): String
    fun cancelConditionalOrder(accountSeq: Int, conditionalOrderId: String)
}
 
interface AutoTradingAlertGateway {
    fun notifyExecuted(fill: TradingFill)
    fun notifyProposalPending(proposal: OrderProposal)
    fun notifyHalted(reason: TradingHaltReason, tradingDay: TradingDay)
    fun notifyTierChanged(from: RiskTier, to: RiskTier, reason: String)
}

domain — 집행 전략

interface TradeExecutor {
    fun mode(): ExecutionMode
    fun buy(proposal: OrderProposal, marketPrice: BigDecimal): TradingFill
    fun sell(position: TradingPosition, marketPrice: BigDecimal, reason: SellReason): TradingFill
}
  • PaperTradeExecutor(costModel: TradingCostModel)외부 Gateway 의존 없음. 모의 체결만 만든다.
  • LiveTradeExecutor(orderGateway: OrderGateway, conditionalOrderGateway: ConditionalOrderGateway, costModel: TradingCostModel)

application — Command / Response

data class RunAutoTradingCycleCommand(val tradeDate: LocalDate)
data class ApproveOrderProposalCommand(val proposalId: Long)
data class ToggleKillSwitchCommand(val enabled: Boolean)
 
data class AutoTradingStatusResponse(
    val executionMode: ExecutionMode,
    val riskTier: RiskTier,
    val killSwitchEnabled: Boolean,
    val tradingCapital: BigDecimal,
    val openPositionCount: Int,
    val dailyRealizedPnl: BigDecimal,
    val dailyLossLimitUsageRate: BigDecimal,
    val halted: Boolean,
)

클래스 역할 정의

도메인 모델

클래스명역할핵심 책임
AutoTradingPolicy자동매매 정책 (싱글톤 1행)실행 모드·킬 스위치·현재 사다리 단계 보유. enableKillSwitch()·promoteTier()·demoteTier()로 상태 전이. canPlaceNewOrder() 질의
ExecutionModeenum PAPER/LIVE
RiskTierenum T1/T2/T3단계별 파라미터(종목당 비율·보유 상한·일일 손실 한도율)를 enum 자체가 보유. next()·previous()로 승강급
TradingDay거래일 상태당일 실현손익 누계·집행 건수·중단 여부. recordRealizedPnl(amount)·isDailyLossLimitReached(capital, tier)·halt(reason)
OrderProposal주문 제안 (거부 포함 전량 기록 = 감사 로그)신호 근거·사이징 근거·게이트 판정 결과 보유. approve()·reject(reason)·expire()·markExecuted(fillId)
TradingPosition보유 포지션수량·평단·손절가·익절가·OCO id. applyBuyFill()·applySellFill()·isStopLossHit(price)·attachOcoOrder(id)
TradingFill체결 기록 (paper·live 공통, mode 컬럼으로 구분)체결가·실효단가·비용·실현손익
TradingCostModel값 객체buy(intendedPrice, quantity)·sell(...)Fill. ml/app/cost_model.py와 동일 공식
PositionSizer값 객체sizeFor(capital, tier, marketPrice) → 매수 주수. 1주 미달이면 0 반환 (PRD FR-36)
TradeCandidate값 객체 (application이 매핑해 주입)symbol·신호 점수·등급·현재가. 다른 도메인 타입을 담지 않는다
TradingHaltReasonenumDAILY_LOSS_LIMIT·MAX_DRAWDOWN·KILL_SWITCH
SellReasonenumSTOP_LOSS·TAKE_PROFIT·KILL_SWITCH_LIQUIDATION·OCO_REGISTRATION_FAILED

서비스 클래스

클래스명역할입력 → 출력의존
AutoTradingDomainService사이클 전체 조율List<TradeCandidate>List<OrderProposal>5개 Repository, RiskGate, TradeExecutor 목록, AutoTradingAlertGateway
RiskGate모든 주문의 단일 관문(candidate, policy, tradingDay, openPositions)RiskDecision(없음 — 순수)
RiskTierPromotionDomainService승급·강등 판정(정책, 최근 20거래일 지표)RiskTierTradingDayRepository, TradingFillRepository
TradingSettlementDomainService일 마감 정산tradeDateTradingDayTradingFillRepository, TradingDayRepository

UseCase (1 UseCase = 1 파일)

클래스명트리거
RunAutoTradingCycleUseCase스케줄러 (장중 주기)
ApproveOrderProposalUseCase / RejectOrderProposalUseCaseREST
ToggleKillSwitchUseCaseREST
SyncPositionFillsUseCase스케줄러 (체결 동기화)
SettleTradingDayUseCase스케줄러 (장 마감 후)
EvaluateRiskTierUseCase스케줄러 (일 1회)
GetAutoTradingStatusUseCase / ListOrderProposalsUseCaseREST

실패 경로·동시성·멱등

상황처리
주문 제출 타임아웃·5xx재시도하지 않는다. fetchOpenOrders(accountSeq)로 접수 여부를 확인한다. 확인 불가 시 해당 종목을 당일 후보에서 제외하고 error-alert 발송 (PRD FR-20)
429 (ORDER 그룹 10 TPS)자체 스로틀 8 TPS. 기존 TossRateLimiterFacade 재사용. 초과분은 다음 사이클로 이월
OCO 등록 실패해당 포지션을 즉시 시장가 청산(SellReason.OCO_REGISTRATION_FAILED). 손절 없는 포지션을 보유하지 않는다 (PRD FR-5)
중복 주문clientOrderId = "AT-{proposalId}" 멱등 키. 같은 제안은 두 번 집행되지 않는다
사이클 중복 실행RunAutoTradingCycleUseCase@Transactional + trading_days 행 비관적 락. 이전 사이클 미완료 시 즉시 반환
킬 스위치 경합정책은 단일 행. 집행 직전 policy.canPlaceNewOrder()트랜잭션 내에서 재확인한다
프로세스 재기동상태가 전부 DB에 있어 30초 내 복구. 미체결 주문은 SyncPositionFillsUseCase가 토스 조회로 재동기화
부분 체결TradingPosition.applyBuyFill()이 누적 반영. OCO 수량은 체결 수량 기준으로 등록
휴장·시간 외GET /api/v1/market-calendar/KR 조회. 장 마감 5분 전부터 신규 진입 중단
통화 혼재유니버스를 KRX(6자리 숫자 심볼)로 한정해 회피 (PRD FR-16·FR-37)

이벤트 아키텍처 판단

이벤트를 도입하지 않는다. ApplicationEvent(Layer 1)·Kafka(Layer 2) 모두 미사용이다.

근거 — 자동매매 사이클은 신호 조회부터 체결 기록까지가 하나의 트랜잭션 경계 안에서 강한 일관성을 요구한다. 리스크 한도 소진량과 주문 집행이 비동기로 갈리면 한도를 넘긴 집행이 가능해진다. 도메인 간 결합을 끊을 이유(무관한 도메인·비동기 허용)가 없고, 소비자도 없다. private-be-architecture-rule 기준 Layer 1·2 어느 쪽 조건도 성립하지 않는다.

향후 알림을 비동기화할 필요가 생기면 그때 Layer 1(@TransactionalEventListener AFTER_COMMIT)을 검토한다. 지금은 AutoTradingAlertGateway 동기 호출로 충분하다.

FE 영향 분석

1차 범위에서 FE 변경은 없다. M1~M2는 스케줄러와 REST 관리 API만으로 동작하며, 화면 없이 Discord 알림으로 관측한다.

항목영향
기존 화면없음 — 기존 API 계약을 변경하지 않는다
신규 APIGET /api/v1/autotrading/status·GET /api/v1/autotrading/proposals·POST /api/v1/autotrading/proposals/{id}/approve·POST /api/v1/autotrading/kill-switch. 전부 신규 경로라 충돌 없음
화면 필요 시점PRD FR-26(제안 조회·승인 화면)은 P1 · M3. paper 전건 자동 구간에서는 승인 화면이 필요 없다
aggregator(BFF)변경 없음 — 관리 API는 FE 진입점을 거치지 않는다

상태 전이 표

OrderProposal

현재 상태 × 이벤트다음 상태거부 사유
(신규) × 게이트 통과 + 임계 이하AUTO_EXECUTED
(신규) × 게이트 통과 + 임계 초과PENDING
(신규) × 게이트 거부REJECTED_BY_RISK한도 초과·보유 수 초과·1주 미달·일일 중단
PENDING × 승인APPROVED → 집행 후 EXECUTED만료된 제안은 승인 불가
PENDING × 거부REJECTED_BY_USER
PENDING × 장 마감EXPIRED
PENDING × 킬 스위치INVALIDATED
EXECUTED/EXPIRED/REJECTED_* × 모든 이벤트(불변)종결 상태

RiskTier

현재 × 이벤트다음거부 사유
T1 × 승급 조건 충족 + 사용자 확인T220거래일 미달·집행 실패 존재·한도 위반 존재·슬리피지 초과
T2 × 승급 조건 충족 + 엣지 t>2 + 사용자 확인T3엣지 미검증
T2/T3 × 총자본 -5% 도달한 단계 강등 (즉시 자동)
T1 × 총자본 -5% 도달T1 유지 + MAX_DRAWDOWN 경고최하 단계

Component Diagram

flowchart LR
    Scheduler[AutoTradingCycleScheduler] --> Cycle[RunAutoTradingCycleUseCase]
    Cycle --> SignalSvc[signal DomainService]
    Cycle --> StockSvc[stock DomainService]
    Cycle --> AutoSvc[AutoTradingDomainService]
    AutoSvc --> Gate[RiskGate]
    AutoSvc --> Paper[PaperTradeExecutor]
    AutoSvc --> Live[LiveTradeExecutor]
    Live --> OrderGw[OrderGateway]
    Live --> OcoGw[ConditionalOrderGateway]
    AutoSvc --> Repos[(autotrading 5 tables)]
    AutoSvc --> Alert[AutoTradingAlertGateway]
    OrderGw --> Toss[Toss Open API]
    OcoGw --> Toss

PaperTradeExecutor에서 Toss Open API로 가는 간선이 없다 — 이것이 PRD FR-35의 구조적 보장이다.

Sequence Diagram

sequenceDiagram
    participant Sch as Scheduler
    participant UC as RunAutoTradingCycleUseCase
    participant Sig as signal DomainService
    participant Svc as AutoTradingDomainService
    participant Gate as RiskGate
    participant Exec as TradeExecutor
    participant Alert as AlertGateway

    Sch->>UC: execute(tradeDate)
    UC->>Sig: 최신 시그널 스냅샷 조회
    Sig-->>UC: 신호 목록
    UC->>Svc: runCycle(candidates)
    Svc->>Gate: evaluate(candidate, policy, tradingDay, positions)
    Gate-->>Svc: RiskDecision (통과 / 거부사유)
    Svc->>Exec: buy(proposal, marketPrice)
    Exec-->>Svc: TradingFill
    Svc->>Alert: notifyExecuted(fill)

ERD

erDiagram
    auto_trading_policy ||--o{ order_proposals : governs
    trading_days ||--o{ order_proposals : contains
    order_proposals ||--o| trading_fills : produces
    trading_positions ||--o{ trading_fills : accumulates

    auto_trading_policy {
        bigint id PK
        varchar execution_mode
        varchar risk_tier
        boolean kill_switch_enabled
        decimal trading_capital
    }
    trading_days {
        bigint id PK
        date trade_date UK
        decimal realized_pnl
        boolean halted
        varchar halt_reason
    }
    order_proposals {
        bigint id PK
        date trade_date
        varchar symbol
        varchar status
        int quantity
        decimal intended_price
        varchar reject_reason
    }
    trading_positions {
        bigint id PK
        varchar symbol
        varchar execution_mode
        int quantity
        decimal average_price
        decimal stop_price
        decimal target_price
        varchar oco_order_id
    }
    trading_fills {
        bigint id PK
        bigint position_id FK
        varchar execution_mode
        varchar side
        decimal fill_price
        decimal realized_pnl
    }
  • 컬럼 상세·인덱스는 DB 설계 문서에서 확정한다. trading_positions(symbol, execution_mode, closed_at IS NULL) 조합으로 열린 포지션 유일성을 보장한다.
  • 금액 컬럼은 전부 decimalstock_price_cache.last_pricevarchar(50)인 전례를 반복하지 않는다.

Testing Plan

TDD 순서(RED → GREEN → REFACTOR)를 강제한다. 프레임워크는 Kotest.

레벨범위핵심 케이스
domainRiskGate·RiskTier·PositionSizer·TradingCostModel·Entity 상태 전이종목당 한도 초과 거부 / 보유 수 초과 거부 / 1주 미달 종목 거부 / 일일 중단 시 전건 거부 / 킬 스위치 시 전건 거부 / T2→T1 강등이 -5%에서 발동 / 만료 제안 승인 불가 / 종결 상태 재전이 거부
domainTradingCostModel 교차 검증ml/app/cost_model.py동일 입력 → 동일 출력. 매수 실효가 상승·매도 실효가 하락·수량 0 처리
applicationUseCaseDomainService 모킹. 사이클 중복 실행 시 즉시 반환 / 승인 흐름 / 킬 스위치 토글
infrastructureRepository·GatewayTestContainers MySQL. 열린 포지션 유일성 제약 / 제안 멱등 저장. 토스 Gateway는 MockWebServer로 429·5xx·타임아웃
presentationController·SchedulerMockMvc. 킬 스위치 인증 요구 / 상태 조회
격리 보장PaperTradeExecutorOrderGateway·ConditionalOrderGateway를 생성자에 갖지 않음을 리플렉션으로 검증. paper 사이클 전체에서 토스 호출 0건 (MockWebServer 요청 수 0)
scenarioE2E신호 → 제안 → 자동 집행 → 손절 발동 → 실현손익 기록 / 일일 한도 도달 → 당일 중단 → 익일 해제 / 킬 스위치 → 미체결 취소 + 제안 무효화 / OCO 등록 실패 → 즉시 청산

Release Scenario — 무중단 배포

기존 기능에 영향이 없다. autotrading은 신규 도메인이고 기존 도메인 수정은 order에 파일 2개(ConditionalOrderGateway + 구현)를 추가하는 것뿐이다 — 기존 파일을 고치지 않는다.

단계배포 내용전환 조건롤백
1. 스키마Flyway로 5개 테이블 생성 (DDL만)마이그레이션 성공역방향 DDL로 테이블 제거
2. 도메인·게이트도메인·리스크 게이트·Repository 배포. 스케줄러 미등록테스트 통과코드 되돌리기 (아무도 호출하지 않음)
3. paper 집행PaperTradeExecutor + 스케줄러 배포. 정책 기본값 PAPER·T1·킬 스위치 off수동 1회 실행으로 제안 생성 확인킬 스위치 ON — 재배포 없이 즉시 정지
4. live 집행LiveTradeExecutor + OCO Gateway 배포. 정책은 PAPER 유지PRD M3 지표 충족코드가 있어도 모드가 PAPER면 실주문 경로에 진입하지 않는다
5. live 전환정책 행의 execution_modeLIVE로 변경 (배포 없음)PRD M4 게이트 + 입금 완료PAPER로 되돌리기 — 즉시 반영
  • 기능 토글은 @ConditionalOnProperty가 아니라 정책 행 조회로 한다 (no-conditional-on-property). 실행 모드·킬 스위치는 런타임에 DB에서 읽어 분기하므로 재기동 없이 전환·롤백된다.
  • 1~3단계는 실주문 경로가 코드에 존재하지 않으므로 되돌리기만으로 안전하다.
  • 플래그 제거 시점: 실행 모드는 영구 설정이라 제거 대상이 아니다. 킬 스위치도 상시 기능이다.

데이터 마이그레이션 계획

해당 없음 — 신규 테이블만 생성하고 기존 데이터를 채우지 않는다. 백필이 없으므로 Flyway 인라인 DML도 없다.

Open Questions

#질문영향
T-1사이클 주기를 몇 분으로 할 것인가?일봉 신호 기반이라 분 단위 반응이 불필요할 수 있다. 짧으면 토스 레이트리밋 압박, 길면 체결가 괴리
T-2PositionSizer가 참조할 총자본을 paper에서 어떻게 계산할 것인가? PRD FR-39의 고정/변동 전환을 정책 행 컬럼으로 둘 것인가사이징 기준값. M1~M2 고정 → M3 변동 전환 방식
T-3PRD Q-13(T3 승급의 엣지 t > 2 판정 표본)을 어느 데이터로 계산할 것인가페이퍼 거래만으로는 표본 부족. 기존 백테스트 엔진 재사용 시 “페이퍼 성과”가 아닌 “과거 재검증”이 된다
T-4stock_price_cache.last_pricevarchar(50)인데 그대로 파싱해 쓸 것인가, 컬럼 타입을 고칠 것인가고치면 stock 도메인 변경 = Single Writer 충돌 대상
T-5실시간 체결(WebSocket personal:order) 도입 시점PRD FR-28은 P1. 1차는 폴링으로 시작

Document History

날짜변경 내용
2026-08-27최초 작성. 실행 주체=backend 단독, 비용모델=Kotlin 재구현+파라미터 단일화, paper 격리=의존성 분리(PaperTradeExecutor가 Gateway 미보유)로 확정