Files
goldenChart/모의투자 처리과정.md
2026-05-31 17:34:26 +09:00

494 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 모의투자·매매 시그널 알림 처리 과정
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 | 해당 종목 **미보유** |
**분할 매매 (종목별 1~3차, `gc_paper_symbol_allocation`):**
- 투자관리 **설정** 탭 → 선택 종목: `buy_stage1~3_krw`, `sell_stage1~3_krw` (KRW)
- 매수·매도 단계 금액 중 **하나라도 > 0** 이면 분할 모드 (`PaperTradeStageService.usesStagedTrading`)
- **BUY 시그널마다** 다음 매수 차수 금액만 체결 (`buy_stage_done` 0→1→2→3)
- **SELL 시그널마다** 다음 매도 차수 금액만큼 수량 체결 (`sell_stage_done` 증가)
- **전량 매도** 후 `buy_stage_done`·`sell_stage_done` **0으로 초기화**
- 분할 모드에서는 보유 중에도 2·3차 **추가 매수** 가능 (LONG_ONLY 일괄 매수 스킵 해제)
- 단계 금액이 모두 0이면 기존 방식(가용현금 % 일괄 매수·전량 매도)
**매수 금액 (분할 미설정 시):**
```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` 주문 큐 분기 포함).*