# 모의투자·매매 시그널 알림 처리 과정 goldenChart에서 **매매 시그널 알림**과 **모의투자(투자관리) 자동·수동 체결**이 어떻게 연결되는지, 처리 조건·흐름·관련 코드를 정리한 문서입니다. > **핵심:** 시그널 **알림**과 모의 **체결**은 같은 전략 평가에서 출발하지만, **이후 파이프라인과 통과 조건이 다릅니다.** > 알림이 많아도 체결이 적을 수 있는 구조가 정상 동작에 가깝습니다. --- ## 1. 개요 | 구분 | 역할 | 결과물 | |------|------|--------| | **시그널 발생** | 실시간 전략이 BUY/SELL 조건 충족 판정 | STOMP, `gc_trade_signal`, FCM, 프론트 알림 목록 | | **모의 자동 체결** | 설정·예수금·포지션 규칙 통과 시 모의 계좌 주문 | `gc_paper_trade`, 원장·KPI 갱신 | | **모의 수동 체결** | 사용자가 투자관리·차트에서 주문 | 동일 `gc_paper_*` 계좌 | 자동 체결은 **백엔드 전용**입니다. 브라우저·가상매매 화면이 꺼져 있어도 서버가 업비트 WS·봉 데이터를 받는 한, Match로 동기화된 live 설정이 있으면 평가·체결 시도가 이어집니다. ### 1.1 사용자 범위 (정식 로그인 vs 게스트) | 구분 | 식별 | 매매·모의·전략·자동매매 | |------|------|------------------------| | **정식 로그인** | `X-User-Id` (회원 ID) | **지원** — 모의 계좌·설정·live 설정·체결 모두 `user_id` 단일 기준 (기기 무관) | | **게스트 입장** | `X-Device-Id`만 (비로그인) | **미지원** — API 403, 프론트 `FormalLoginGate`·로그인 유도 | - 모의 계좌: `gc_paper_account`는 `findByUserId` 1계좌, `device_id`는 `user:{userId}` 합성 키(UK용). - 시그널·주문 큐: `gc_live_strategy_settings.user_id IS NOT NULL` 인 행만 평가·`OrderExecutionQueue` 적재. - 마이그레이션 `V54`: 기존 거래·시그널 데이터 초기화, device-only live 체크 OFF. --- ## 2. 전체 아키텍처 ```mermaid flowchart TB subgraph ingest [시세·봉] WS[업비트 WebSocket 틱] BB[BarBuilder 1m 확정·상위봉 전파] TS[Ta4jStorage] WS --> BB --> TS end subgraph eval [전략 평가] BCE[BarCloseStrategyEvaluationService] LSS[LiveStrategyScheduler 3초] LSE[LiveStrategyEvaluator] TS --> BCE TS --> LSS BCE --> LSE LSS --> LSE end subgraph signal_out [시그널 산출 - 알림 경로] STOMP[TradingWebSocketBroker STOMP] TSS[TradeSignalService.save] FCM[FcmPushService] LSE -->|BUY/SELL| BCE LSE -->|BUY/SELL| LSS BCE --> STOMP BCE --> TSS LSS --> STOMP LSS --> TSS TSS --> FCM end subgraph order_path [주문 경로 - 체결] OEQ[OrderExecutionQueue] TES[TradingExecutionService] PTS[PaperTradingService.tryExecuteOnSignal] BCE -->|deviceId != null| OEQ LSS -->|deviceId != null| OEQ OEQ --> TES TES --> PTS end subgraph fe [프론트] LSN[LiveSignalNotifier / STOMP] TNC[TradeNotificationContext] PTP[PaperTradingPage / TradeOrderPanel] STOMP --> LSN --> TNC PTS --> PTP end ``` --- ## 3. 사전 설정 (DB·앱) ### 3.1 종목별 실시간 전략 — `gc_live_strategy_settings` | 필드 | 설명 | |------|------| | `market` | 종목 코드 (예: `KRW-BTC`) | | `user_id` / `device_id` | **회원:** `user_id`만 저장, `device_id=null` · **비회원:** `device_id`만 | | `strategy_id` | 사용할 `GcStrategy` | | `is_live_check` | 실시간 체크 ON → 평가·시그널 대상 | | `execution_type` | `CANDLE_CLOSE`(봉 마감) 또는 `REALTIME_TICK`(3초 틱) | | `position_mode` | `LONG_ONLY`(포지션 추적) 또는 `SIGNAL_ONLY`(규칙만) | | `candle_type` | UI 기본 분봉; 실제 평가 분봉은 DSL `StrategyConditionTimeframeService` 기준 | **저장 API:** `PUT /api/strategy/settings` (`LiveStrategySettingsService`) - 가상투자 Match 시 `syncVirtualTargetsToBackend()` → 종목별 `isLiveCheck=true` 일괄 반영 (`frontend/src/utils/virtualLiveStrategySync.ts`) - 차트·설정의 전역 실시간 체크는 `gc_app_settings.live_strategy_check` / `live_strategy_id` 와 연동 ### 3.2 모의투자·실행 모드 — `gc_app_settings` | 필드 | 자동 체결 관련 | |------|----------------| | `paper_trading_enabled` | 모의투자 마스터 ON | | `paper_auto_trade_enabled` | **전략 시그널 → 모의 자동 체결** ON | | `paper_auto_trade_budget_pct` | 자동 매수 시 가용 현금 대비 사용 비율 (기본 95%) | | `paper_initial_capital` | 초기 자본 | | `paper_fee_rate_pct` / `paper_slippage_pct` | 수수료·슬리피지 | | `paper_min_order_krw` | 최소 주문 금액 (기본 5,000원) | | `trading_mode` | `PAPER` / `LIVE` / `BOTH` — 자동 체결 대상 분기 | | `live_strategy_check` | 전역 실시간 체크 (종목별 `is_live_check` 없을 때 보조 조건) | **조회·저장:** `GET/POST /api/app-settings` (`X-Device-Id`, 로그인 시 `X-User-Id`) ### 3.3 모의 계좌 — `gc_paper_account` - **키:** `device_id` (HTTP `X-Device-Id`; 없으면 `anonymous`) - 로그인 사용자도 요청 헤더의 `device_id`로 계좌를 구분 ( `user_id`는 부가 정보) - 보유: `gc_paper_position`, 체결: `gc_paper_trade`, 한도: `gc_paper_symbol_allocation` --- ## 4. 시그널 발생 상세 ### 4.1 데이터·구독 전제 1. `LiveStrategyStartupRunner` — 기동 시 `is_live_check=true` 종목 WS·Ta4j 고정 구독 2. `BarBuilder.onTick` — 1m 확정 후 상위 분봉(5m 등) 전파 3. `StrategyConditionTimeframeService` — 전략 DSL에 포함된 분봉만 평가 ### 4.2 방식 A — 봉 마감 (`CANDLE_CLOSE`) **트리거:** `BarBuilder.commitBar()` → `BarCloseStrategyEvaluationService.onMaturedBarClose()` **진입 조건 (종목·설정 행마다):** 1. `is_live_check = true` 이고 `strategy_id` 존재 2. `execution_type = CANDLE_CLOSE` (또는 마감 시 `evaluateRealtimeAtClose` 보조) 3. `usesTimeframe(strategyId, candleType)` — DSL에 해당 분봉 포함 4. 동일 `market:candleType:barEndEpoch` **중복 평가 방지** (`evaluatedBarCloseKeys`) **평가:** `LiveStrategyEvaluator.evaluateSettingOnCandleClose()` — **확정된 직전 봉 인덱스**에서 1회 ### 4.3 방식 B — 실시간 틱 (`REALTIME_TICK`) **트리거:** `LiveStrategyScheduler` — **3초 `fixedDelay`** (`@Scheduled`) **조건:** `findAllByIsLiveCheckTrue()` 중 `execution_type = REALTIME_TICK` **평가:** `evaluateSettingRealtimeTick()` — **진행 중 봉** `series.getEndIndex()` 동일 `(market, candleType, index)` 중복 시그널 방지 ### 4.4 시그널 판정 (`LiveStrategyEvaluator` + `StrategySignalDeterminer`) | `position_mode` | BUY | SELL | |-----------------|-----|------| | **LONG_ONLY** | 진입 규칙 + 가상 `TradingRecord` 미보유 | 청산 규칙 + 보유 중 | | **SIGNAL_ONLY** | `entryRule` 충족 | `exitRule` 충족 | `LONG_ONLY`에서 이미 보유인데 BUY, 무포지션인데 SELL이면 evaluator 내부에서 **`NONE`으로 정리**할 수 있음 (캐시된 포지션 상태 기준). 결과: `"BUY"` | `"SELL"` | `"NONE"` --- ## 5. 시그널 후처리 (알림 파이프라인) BUY/SELL이 확정되면 **설정 행(`GcLiveStrategySettings`)마다** 아래가 순서대로 실행됩니다. ### 5.1 STOMP 실시간 푸시 | 항목 | 내용 | |------|------| | 토픽 | `/sub/charts/{market}/{candleType}` | | 페이로드 | `CandleBarDto` — OHLCV + `signal`, `strategyId`, `userId`, `deviceId`, `executionType` | | 발행 | `TradingWebSocketBroker.publish()` | ### 5.2 DB 이력 저장 `TradeSignalService.save(deviceId, userId, market, strategyId, …)` → **`gc_trade_signal`** - **로그인:** `user_id` 저장, `device_id`는 null일 수 있음 - **비회원:** `device_id` 저장 **API:** `GET /api/trade-signals` — 로그인 시 `user_id`, 비로그인 시 `device_id` 기준 목록 ### 5.3 FCM 푸시 (선택) `FcmPushService.sendTradeSignalIfEnabled()` — 앱 알림 설정·토큰 등록 시 로그인 사용자는 **userId** 기준 발송 (deviceId null 허용) ### 5.4 주문 큐 적재 (자동 체결 **시도**만) ```java // BarCloseStrategyEvaluationService, LiveStrategyScheduler 공통 if (s.getDeviceId() != null) { orderExecutionQueue.submitSignal( s.getDeviceId(), s.getUserId(), market, s.getStrategyId(), signal, price); } ``` > **주문 큐:** `user_id`가 있는 live 설정만 `OrderExecutionQueue`에 적재합니다. (게스트 device-only 설정은 평가 대상에서 제외) --- ## 6. 프론트 — 매매 시그널 알림 ### 6.1 STOMP 수신 | 컴포넌트 | 역할 | |----------|------| | `useLiveStrategyMarkers` | `/sub/charts/...` 구독, `signal` 필드 처리 | | `signalBelongsToCurrentSession` | `userId` 또는 `deviceId` 소유자 필터 (`tradeSignalOwnership.ts`) | | `LiveSignalNotifier` | `App.tsx` 전역, `monitoredMarkets` 대상 | **구독 대상 (`monitoredMarkets`):** - 가상투자 실행 중 → 가상투자 대상 종목 - 아니면 → `liveStrategyCheck` + `liveStrategyId` + 관심종목 ### 6.2 알림 Context `TradeNotificationContext`: - STOMP 수신 → `addNotification()` (미확인 배지·토스트·사운드) - 20초마다 `loadTradeSignals()` → 서버 이력과 병합 - 알림 ID: `{market}:{candleTime}:{signalType}` - **알림 목록 화면:** `TradeNotificationListPage` (메뉴 알림목록) ### 6.3 알림 vs 체결 (프론트) - 프론트는 시그널 수신 시 **`placePaperOrder`를 호출하지 않음** (과거 STOMP→주문 훅 제거됨) - 투자관리·가상매매 UI는 **모니터링·수동 주문·설정**; 자동 체결은 백엔드만 --- ## 7. 모의 자동매매 실행 파이프라인 ### 7.1 주문 큐 `OrderExecutionQueue` — 단일 워커 스레드 + `RateLimiter` (업비트 429 완화) | 단계 | 동작 | |------|------| | `submitSignal` | `OrderRequest.strategySignal` 큐 적재 | | `executeNow` | `deviceId` blank면 **즉시 return** | | 실행 | `TradingExecutionService.executeSignal()` | ### 7.2 실행 대상 분기 — `TradingExecutionService` ```text trading_mode: PAPER → paperTradingService.tryExecuteOnSignal (paper_trading_enabled 시) LIVE → liveTradingService.tryExecuteOnSignal BOTH → 둘 다 시도 ``` ### 7.3 모의 체결 본체 — `PaperTradingService.tryExecuteOnSignal` **진입 전 필수 (하나라도 실패 시 return, 예외는 warn 로그):** | # | 조건 | |---|------| | 1 | `deviceId` not blank | | 2 | `signalType` ∈ {BUY, SELL} | | 3 | `paper_trading_enabled = true` | | 4 | `paper_auto_trade_enabled = true` | | 5 | `live_strategy_check = true` **OR** 해당 `market`에 `is_live_check=true` 설정 존재 | | 6 | `trading_mode`가 PAPER 또는 BOTH (`TradingExecutionService` 단계) | **포지션 모드 `LONG_ONLY` (실제 계좌 보유 기준):** | 시그널 | 스킵 조건 | |--------|-----------| | BUY | 해당 종목 **이미 보유** (`quantity > 0`) | | SELL | 해당 종목 **미보유** | **매수 금액 계산:** ```text budget = (cash_balance - reserved_cash) × paper_auto_trade_budget_pct / 100 execPrice = price × (1 + slippage%) qty = budget / (execPrice × (1 + fee%)) ``` | # | 매수 스킵 | |---|-----------| | 7 | `budget < paper_min_order_krw` | | 8 | `qty × execPrice < min_order` | **매도:** 보유 수량 전량(가용 수량, 미체결 매도 예약 제외) **체결:** `executeTrade(..., source="STRATEGY", autoTrade=true)` - 종목 한도: `PaperAllocationService.validateBuy` — `getOrDefault`로 한도 행 없으면 초기자본 규모 한도 자동 생성 - 체결 기록: `gc_paper_trade`, 원장: `PaperLedgerService` --- ## 8. 모의 수동매매 (시그널과 별도) **경로:** `TradeOrderPanel` → `POST /api/paper/orders` → `PaperTradingService.placeOrder` | 구분 | 자동 (`tryExecuteOnSignal`) | 수동 (`placeOrder`) | |------|----------------------------|---------------------| | `paper_auto_trade_enabled` | 필수 ON | 불필요 | | 예산 | 전역 `paper_auto_trade_budget_pct` | 요청 수량·금액·한도 | | `source` | `STRATEGY` | `MANUAL` 등 | | 한도 검증 | `autoTrade=true` | `autoTrade=false` | 수동 주문도 `paper_trading_enabled` 및 동일 계좌·한도 규칙을 따릅니다. --- ## 9. 가상투자 ↔ 백엔드 동기화 가상투자 **Match(실행)** 시: 1. `syncVirtualTargetsToBackend(targets, session, isLiveCheck=true)` 2. 종목별 `saveLiveStrategySettings` — `isLiveCheck`, `executionType`, `positionMode` 3. `putPaperAllocationsBulk` — 종목별 한도 행(베스트 에포트) 4. 전략 분봉 pin — `pinStrategyEvaluationTimeframes` **중지 시:** `stopVirtualLiveOnBackend` — `isLiveCheck=false` 서버는 이후 **BarClose / LiveStrategyScheduler**만으로 평가·시그널·(조건 충족 시) 체결을 진행합니다. --- ## 10. 처리 조건 요약표 ### 10.1 시그널 알림이 쌓이려면 | 조건 | 필수 | |------|:----:| | `gc_live_strategy_settings.is_live_check = true` | ✓ | | `strategy_id` 지정 | ✓ | | DSL에 해당 `candle_type` 포함 | ✓ | | Ta4jStorage에 봉 데이터 존재 | ✓ | | 전략 규칙 BUY/SELL 판정 | ✓ | → **DB 저장·STOMP·(설정 시) FCM·프론트 알림** (deviceId 없어도 가능) ### 10.2 모의 자동 체결이 되려면 위 시그널 조건 **+** 아래 **전부** (실무상): | 조건 | 필수 | |------|:----:| | live 설정 `user_id != null` (주문 큐 진입) | ✓ | | `paper_trading_enabled` | ✓ | | `paper_auto_trade_enabled` | ✓ | | `trading_mode` ∈ {PAPER, BOTH} | ✓ | | `live_strategy_check` OR 종목 `is_live_check` | ✓ | | LONG_ONLY: BUY=미보유, SELL=보유 | ✓ | | 예수금·한도·최소주문금액 | ✓ | --- ## 11. 알림은 많은데 체결이 적은 대표 사례 | 현상 | 원인 | |------|------| | 미확인 알림 수백 건, 체결 3건 | 종목 다수 × 5분봉 × 매수·매도 시그널; 체결은 예수금·포지션 규칙으로 소수만 통과 | | 연속 매수 알림, 체결 없음 | **예수금 소진** — 자동 매수가 남은 현금의 95%씩 사용 (1천만 → 약 3종목 후 5천원 미만) | | 매도 알림만 많음 | **LONG_ONLY** + 미보유 → 매도 시그널은 알림만, 체결 스킵 | | 같은 종목 매수 알림 반복 | 이미 보유 → BUY 체결 스킵 (알림은 evaluator/DB 경로에 따라 발생 가능) | | 게스트 입장 | 매매 API 403, STOMP 시그널 구독 없음 | | KPI 1천만, 거래 탭 과거 3건 | **계좌 초기화** 후 현금 복구, **체결 이력은 유지** | | 자동매매 OFF | `paper_auto_trade_enabled=false` — 수동만 가능 | **예수금 소진 예 (초기 1천만, budget 95%):** 1. 1차 매수 ≈ 950만 → 잔액 ≈ 50만 2. 2차 ≈ 47.5만 → 잔액 ≈ 2.5만 3. 3차 ≈ 2.4만 → 잔액 ≈ 1천원대 → 이후 BUY 시그널 전부 `budget < min_order` 스킵 --- ## 12. 엔드투엔드 시퀀스 (봉 마감 + 자동 모의) ```mermaid sequenceDiagram participant UB as 업비트 WS participant BB as BarBuilder participant BCE as BarCloseEval participant LSE as LiveStrategyEvaluator participant TSS as TradeSignalService participant OEQ as OrderExecutionQueue participant PTS as PaperTradingService participant FE as 프론트 STOMP UB->>BB: tick BB->>BCE: onMaturedBarClose (5m 등) BCE->>LSE: evaluateSettingOnCandleClose LSE-->>BCE: BUY|SELL|NONE alt BUY or SELL BCE->>FE: STOMP CandleBarDto.signal BCE->>TSS: save → gc_trade_signal alt setting.deviceId != null BCE->>OEQ: submitSignal OEQ->>PTS: tryExecuteOnSignal alt 모든 체결 조건 통과 PTS->>PTS: executeTrade STRATEGY else 조건 실패 PTS-->>PTS: silent return end end end ``` --- ## 13. 주요 클래스·파일 ### 백엔드 | 영역 | 클래스 | |------|--------| | 봉·마감 평가 | `BarBuilder`, `BarCloseStrategyEvaluationService` | | 틱 평가 | `LiveStrategyScheduler` | | 전략 판정 | `LiveStrategyEvaluator`, `StrategySignalDeterminer` | | 시그널 저장 | `TradeSignalService`, `GcTradeSignal` | | STOMP | `TradingWebSocketBroker` | | 주문 큐 | `OrderExecutionQueue`, `OrderRequest` | | 실행 분기 | `TradingExecutionService`, `TradingMode` | | 모의 체결 | `PaperTradingService`, `PaperAllocationService` | | Live 설정 | `LiveStrategySettingsService`, `GcLiveStrategySettings` | | 설정 | `AppSettingsService`, `GcAppSettings` | ### 프론트 | 영역 | 파일 | |------|------| | 전역 STOMP 알림 | `LiveSignalNotifier.tsx`, `useLiveStrategyMarkers.ts` | | 알림 상태 | `TradeNotificationContext.tsx`, `TradeNotificationListPage.tsx` | | 소유자 필터 | `tradeSignalOwnership.ts` | | 투자관리 UI | `PaperTradingPage.tsx`, `TradeOrderPanel.tsx` | | 가상 동기화 | `virtualLiveStrategySync.ts` | | API | `backendApi.ts` — `loadTradeSignals`, paper APIs | ### DB (핵심 테이블) | 테이블 | 용도 | |--------|------| | `gc_live_strategy_settings` | 종목별 실시간 전략·체크 ON | | `gc_trade_signal` | 매매 시그널 알림 이력 | | `gc_app_settings` | 모의·자동매매·실행모드 전역 | | `gc_paper_account` | 모의 계좌 (device_id) | | `gc_paper_trade` | 체결 내역 | | `gc_paper_symbol_allocation` | 종목별 투자 한도 | --- ## 14. 운영·디버깅 체크리스트 1. **설정 → 모의투자:** 활성화 ON, 자동매매 ON, 실행 대상 PAPER/BOTH 2. **가상투자 Match:** 대상 종목 `isLiveCheck` 백엔드 반영 여부 3. **예수금:** 투자관리 KPI `주문가능`, 체결 후 `cash_after` 4. **포지션:** LONG_ONLY 시 매도 알림 vs 실제 보유 5. **로그인:** `device_id` null 이슈 — 자동 체결 기대 시 비회원 테스트 또는 백엔드 보완 여부 확인 6. **로그 키워드:** `[TradeSignal] saved`, `[Paper] STRATEGY BUY`, `[Paper] auto trade failed`, `[OrderExecutionQueue]` --- ## 15. 관련 문서 - [전략편집기 로직 및 동작흐름.md](./전략편집기%20로직%20및%20동작흐름.md) — §9 시그널 감지, §10 시그널 발행 - [docs/모의투자_투자금_관리_설계.md](./docs/모의투자_투자금_관리_설계.md) — 계좌·한도·원장 설계 - [backend/FCM_BACKEND.md](./backend/FCM_BACKEND.md) — 푸시 연동 - [db_struct.md](./db_struct.md) — 테이블 상세 --- *문서 기준: goldenChart 저장소 백엔드·프론트 구현 (자동 체결 백엔드 전용, 회원 live 설정 `device_id=null` 주문 큐 분기 포함).*