# 전략편집기 로직 및 동작흐름 goldenChart **전략편집기(Strategy Editor)** 의 화면 구성, 편집·저장 방식, 그리고 저장된 전략이 **백엔드에서 매매 시그널(BUY/SELL)을 감지**하기까지의 전체 흐름을 정리한 문서입니다. > 관련 문서: [실시간 전략체크 구현.md](./실시간%20전략체크%20구현.md) — 실시간 ON/OFF, `CANDLE_CLOSE` / `REALTIME_TICK` 설정 및 STOMP 프로토콜 --- ## 1. 개요 | 구분 | 설명 | |------|------| | **메뉴** | `strategy-editor` (`App.tsx` → `StrategyEditorPage`) | | **역할** | 매수·매도 조건을 **논리 트리(DSL)** 로 구성하고 `gc_strategy` 테이블에 저장 | | **평가 엔진** | 백엔드 **Ta4j** — `StrategyDslToTa4jAdapter`가 DSL JSON → `Rule` 변환 | | **실시간 시그널** | `LiveStrategyEvaluator` + `BarBuilder` / `LiveStrategyScheduler` | | **레거시 화면** | `StrategyPage` (`strategy`) — 동일 API·DSL, UI만 구형 트리 편집기 | 전략편집기에서 저장하는 것은 **조건 트리 JSON**(`buyCondition`, `sellCondition`)이며, 노드 좌표·팔레트 사용자 정의 항목 등은 **브라우저 localStorage**에만 둘 수 있습니다. ### 1.1 아키텍처 원칙 (단일 평가 원천) | 계층 | 역할 | |------|------| | **전략편집기** | `buy_condition_json` / `sell_condition_json` 만 DB에 저장. 그래프·목록 빌더는 **동일 DSL** 공유, 표현만 다름 | | **Logic Expression** | 저장된 DSL의 **설명용 미리보기** — 별도 조건 컬럼 없음 | | **`flow_layout_json`** | 캔버스 좌표·START 메타 등 **UI** — Ta4j 평가 입력이 아님 (DSL과 동기 저장) | | **백엔드** | Ta4j로 **유일한** BUY/SELL·조건 충족 평가 | | **프론트 가상투자 UI** | `GET /strategy/live-conditions` 표시·일치율만. **프론트에서 조건 재평가하지 않음** | | **가상 자동매매** | 백엔드 STOMP `signal` (BUY/SELL) 수신 시에만 모의 체결 | ### 1.2 봉 마감별 전략 체크 (스킵 방지) 1. 업비트 틱 → `BarBuilder.commitBar` — **2-Phase 처리** (§9.3 참고) - Phase 1: 1m 확정 후 상위봉(3m·5m·10m·15m …) 전부 스토리지에 저장 - Phase 2: 모든 봉 저장 완료 후 전략 평가 일괄 트리거 (멀티TF 레이스 컨디션 해소) 2. 각 **상위봉 마감** 시 `BarCloseStrategyEvaluationService.onMaturedBarClose` — `StrategyConditionTimeframeService.usesTimeframe` 이 true 인 분봉만 평가 3. WS 갭 → `CandleGapBackfillService` REST 병합 후 `evaluateRecentMaturedBarsAfterBackfill` 로 병합 구간 마감봉 보정 4. `REALTIME_TICK` 실행 방식은 추가로 `LiveStrategyScheduler`(3초)가 **동일 DSL 분봉**만 순회 예: 비트코인 전략 START에 `3m,5m,10m,15m` 이면 각 분봉 **마감봉마다** 백엔드 평가가 1회씩 실행되어야 하며, 평가 분봉은 DSL에 없는 `1h` 등에서는 실행되지 않음. --- ## 2. 화면 구성 (`StrategyEditorPage`) ### 2.1 레이아웃 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ BuilderPageShell — 제목: 전략편집기 / Strategy Builder │ ├──────────────┬──────────────────────────────────────────┬───────────────┤ │ 좌측 패널 │ 중앙 편집 영역 │ 우측 패널 │ │ (리사이즈) │ │ (리사이즈) │ │ │ [매수] [매도] 탭 │ │ │ 전략 목록 │ [그래프 | 목록] 편집 모드 │ 지표 | 템플릿 │ │ 새 전략 │ ┌────────────────────────────────────┐ │ 팔레트 │ │ 삭제 │ │ StrategyEditorCanvas (React Flow) │ │ START/AND/ │ │ Import/ │ │ 또는 StrategyListEditor │ │ OR/NOT 칩 │ │ Export │ └────────────────────────────────────┘ │ 보조/복합 │ │ │ 선택 노드 설정 바 (CondEditor) │ 지표 탭 │ ├──────────────┴──────────────────────────────────────────┴───────────────┤ │ 하단 터미널 — LogicExpressionPreview (논리 수식 미리보기) │ └─────────────────────────────────────────────────────────────────────────┘ ``` **스타일:** `strategyEditor.css`, `strategyEditorTheme.css` ### 2.2 주요 UI 상태 | State | 타입 | 용도 | |-------|------|------| | `signalTab` | `'buy' \| 'sell'` | 매수/매도 조건 편집 대상 | | `editorMode` | `'graph' \| 'list'` | React Flow vs 목록 트리 (`gc_se_editor_mode`) | | `rightTab` | `'indicators' \| 'templates'` | 지표 팔레트 vs 프리셋 템플릿 | | `indicatorSubTab` | `'auxiliary' \| 'composite'` | 단일 보조지표 vs 복합(2기간 교차) | | `buyCondition` / `sellCondition` | `LogicNode \| null` | 저장 대상 DSL 루트 | | `buyLayout` / `sellLayout` | `SignalFlowLayoutSnapshot` | 노드 좌표, 엣지, START 메타 (로컬 전용) | | `buyOrphans` / `sellOrphans` | `LogicNode[]` | 그래프에서 분리된 고아 노드 (DB 미저장) | | `selectedId` | `number \| 'draft'` | 선택 전략 ID | ### 2.3 중앙 편집 영역 동작 #### 그래프 모드 (`StrategyEditorCanvas`) - **React Flow** 기반: `StartNode`, `LogicGateNode`(AND/OR/NOT), `ConditionFlowNode` - 팔레트에서 드래그 앤 드롭 → `makeNode` / `mergeAtRoot`로 트리에 삽입 - START 노드에서 **평가 분봉** 선택 (`1m` ~ `1d`) - START를 추가하면 **다중 분기** (`extraStartIds`) — 저장 시 `AND`/`OR` + 여러 `TIMEFRAME`으로 직렬화 - 노드 이동·연결 변경 시 `saveStrategyFlowLayout` → localStorage `gc_se_flow_layout_v1` #### 목록 모드 (`StrategyListEditor`) - 동일 DSL을 트리/섹션 UI로 편집 - START 섹션별 분봉·조건 트리 관리 #### 선택 노드 설정 (`CondEditor` — `strategyEditorShared.tsx`) - `indicatorType`, `conditionType`, `leftField`, `rightField`, `period`, 복합지표 기간 등 - `buildStrategyEditorDef()` — **차트 보조지표 설정과 분리된** 고정 기본 파라미터 사용 ### 2.4 우측 팔레트 | 영역 | 내용 | |------|------| | **시작·논리** | START, AND, OR, NOT (`PaletteChip`) | | **밴드·추세** | MA, EMA, BOLLINGER, DONCHIAN, ICHIMOKU 등 | | **보조지표** | RSI, MACD, CCI, 신고가/신저가(NH_PRIOR/NL_PRIOR) 등 — `se-palette-auxiliary-v1` | | **복합지표** | RSI+RSI, MACD+MACD 등 2기간 교차 — `se-palette-composite-v1` | | **템플릿** | `strategyPresets.ts` — 터틀, 돈치안, 일목 등 원클릭 삽입 | 드래그 payload: `application/json` — `{ type, value, label, composite?, period?, ... }` ### 2.5 하단 Logic Expression `LogicExpressionPreview` — `collectEditorBranches` / `collectDslBranches`로 분기별 수식 표시 예: ``` [5m] (RSI(9) CROSS_UP K_70 AND MACD CROSS_UP) AND [1h] (ADX CROSS_UP K_25) ``` --- ## 3. 전략 데이터 모델 (DSL) ### 3.1 핵심 타입 (`strategyTypes.ts`) ```typescript type LogicNodeType = 'AND' | 'OR' | 'NOT' | 'CONDITION' | 'TIMEFRAME'; interface ConditionDSL { indicatorType: string; // RSI, MACD, DONCHIAN, NEW_HIGH, ... conditionType: string; // GT, CROSS_UP, CROSS_DOWN, ... leftField?: string; // RSI_VALUE, DC_UPPER_20, NH_PRIOR_9, ... rightField?: string; // K_70, SIGNAL_LINE, DC_LOWER_10, ... period?: number; composite?: boolean; // 복합지표 2기간 교차 leftPeriod?, rightPeriod?; leftCandleType?, rightCandleType?; // ... } interface LogicNode { id: string; type: LogicNodeType; children?: LogicNode[]; condition?: ConditionDSL; candleType?: string; // TIMEFRAME 전용 } ``` ### 3.2 DB/API 저장 형태 (`GcStrategy`) | 컬럼 | 내용 | |------|------| | `buy_condition_json` | 매수(진입) 조건 루트 `LogicNode` | | `sell_condition_json` | 매도(청산) 조건 루트 `LogicNode` | | `name`, `description`, `enabled` | 메타 | API: `POST /api/strategies` — `saveStrategy()` (`backendApi.ts`) ### 3.3 CONDITION 리프 예시 ```json { "id": "n_abc123", "type": "CONDITION", "condition": { "indicatorType": "RSI", "conditionType": "CROSS_UP", "leftField": "RSI_VALUE", "rightField": "K_70", "period": 14 } } ``` ### 3.4 논리 노드 예시 ```json { "id": "n_and1", "type": "AND", "children": [ { "type": "CONDITION", "condition": { "...": "RSI CROSS_UP" } }, { "type": "CONDITION", "condition": { "...": "MACD CROSS_UP" } } ] } ``` --- ## 4. START · TIMEFRAME · 다중 분봉 편집기 UI의 **START 노드**는 DB에 그대로 저장되지 않습니다. `strategyConditionSerde.ts`가 저장/로드 시 변환합니다. ### 4.1 편집기 내부 상태 (`EditorConditionState`) ```typescript { root: LogicNode | null; // 기본 START 분기의 조건 트리 startMeta: { [startId]: { candleType } }; extraStartIds: string[]; // 추가 START extraRoots: { [startId]: LogicNode }; startCombineOp?: 'AND' | 'OR'; // 다중 START 결합 } ``` - 기본 START ID: `__strategy_start__` - 추가 START: `__strategy_start_{uuid}__` ### 4.2 저장 시 인코딩 (`encodeConditionForSave`) | 경우 | 저장 결과 | |------|-----------| | 조건 없음 | `null` | | 단일 분기, 분봉 `1m` | 루트 트리만 (TIMEFRAME 생략) | | 단일 분기, 분봉 `5m` 등 | `{ type: "TIMEFRAME", candleType: "5m", children: [root] }` | | 다중 START | `{ type: "AND"\|"OR", children: [ TIMEFRAME×N ] }` | ### 4.3 로드 시 디코딩 (`decodeConditionForEditor`) - 최상위가 `AND`/`OR`이고 **자식이 전부 `TIMEFRAME`** → 다중 START로 복원 - 최상위가 `TIMEFRAME` 단일 → 기본 START + 해당 분봉 - 그 외 → 기본 `1m` START, 루트 = 전체 DSL ### 4.4 지원 분봉 (`strategyStartNodes.ts`) `1m`, `3m`, `5m`, `10m`, `15m`, `30m`, `1h`, `4h`, `1d` --- ## 5. 지표·조건 종류 ### 5.1 단일 보조지표 - 팔레트 또는 템플릿에서 `CONDITION` 노드 생성 - `leftField` / `rightField` — 지표 플롯 vs 상수(`K_70`) vs 다른 플롯(`SIGNAL_LINE`) - 기간은 `period` 또는 필드명 접미사 (`RSI_VALUE_9`) ### 5.2 복합지표 (`compositeIndicators.ts`) 동일 지표 **두 기간** 비교 — 예: RSI(9) 상향 돌파 RSI(20) ```json { "composite": true, "indicatorType": "RSI", "conditionType": "CROSS_UP", "leftField": "RSI_VALUE_9", "rightField": "RSI_VALUE_20", "leftPeriod": 9, "rightPeriod": 20 } ``` 요소별 **서로 다른 분봉**(`leftCandleType`, `rightCandleType`)은 백엔드 `buildCrossTimeframeCompositeRule`로 처리 (라이브 + storage 필요). ### 5.3 신고가·신저가 (단일 지표) - `NEW_HIGH` / `NEW_LOW` + `NH_PRIOR_N` / `NL_PRIOR_N` - 의미: **직전 N봉** 최고가/최저가 대비 종가 돌파/이탈 (당일 봉 제외) - 백엔드: `PreviousValueIndicator(HighestValueIndicator(high, N), 1)` 등 - **주의:** 라이브 환경에서 N이 클수록 warm-up 봉 수 이상의 데이터가 필요함 (§8.3 참고) ### 5.4 돈치안·일목 등 - `DONCHIAN` → `DC_UPPER_N`, `DC_LOWER_N` - `ICHIMOKU` → 구름/선행스팬 전용 `conditionType` (`ABOVE_CLOUD`, `CLOUD_BREAK_UP`, …) --- ## 6. 저장·로드 프로세스 (프론트엔드) ### 6.1 저장 (`handleSave`) ```mermaid sequenceDiagram participant U as 사용자 participant SE as StrategyEditorPage participant Serde as encodeConditionForSave participant API as POST /api/strategies participant DB as gc_strategy U->>SE: 저장 클릭 SE->>Serde: buyEditorState / sellEditorState Serde-->>SE: buyCondition, sellCondition (LogicNode) SE->>API: saveStrategy({ name, buyCondition, sellCondition }) API->>DB: JSON 컬럼 persist SE->>SE: persistFlowLayout(id) — localStorage만 ``` 검증: - 전략명 필수 - 매수·매도 중 **하나 이상** 조건 필요 - `draft` 저장 시 → DB ID 발급 후 `migrateStrategyFlowLayout('draft' → id)` ### 6.2 로드 1. `loadStrategies()` — 서버 우선 2. 실패 시 `loadStratsLocal()` (`gc_strat_v2`) 3. 전략 선택 → `decodeConditionForEditor(buy/sell)` + `loadStrategyFlowLayout(id)` ### 6.3 localStorage vs DB | 키 | 저장 내용 | DB | |----|-----------|-----| | `gc_strategy` (API) | buy/sell JSON | ✅ | | `gc_se_flow_layout_v1` | 노드 좌표, edge, startMeta, orphans | ❌ | | `gc_strat_v2` | 전략 목록 캐시 | ❌ (폴백) | | `se-palette-auxiliary-v1` | 사용자 보조지표 팔레트 | ❌ | | `se-palette-composite-v1` | 사용자 복합지표 팔레트 | ❌ | | `gc_se_editor_mode` | graph / list | ❌ | Import/Export: `strategyImportExport.ts` — DSL + `flowLayout` + `editorMode` JSON 파일 --- ## 7. 백엔드: DSL → Ta4j Rule ### 7.1 어댑터 (`StrategyDslToTa4jAdapter`) **진입:** `toRule(JsonNode dsl, RuleBuildContext ctx)` | `LogicNode.type` | 빌드 메서드 | Ta4j | |------------------|-------------|------| | `AND` | `buildAndRule` | `AndRule` | | `OR` | `buildOrRule` | `OrRule` | | `NOT` | `buildNotRule` | `NotRule` | | `TIMEFRAME` | `buildTimeframeRule` | 분기 `BarSeries` + `CrossSeriesRule` | | `CONDITION` | `buildConditionRule` | 비교/교차 Rule | **CONDITION `conditionType` 매핑 (요약):** | conditionType | Ta4j 개념 | |---------------|-----------| | GT, LT, GTE, LTE, EQ, NEQ | Over/Under/Equals | | CROSS_UP, CROSS_DOWN | `CrossedUpIndicatorRule`, `CrossedDownIndicatorRule` | | SLOPE_UP, SLOPE_DOWN | `IsRisingRule`, `IsFallingRule` | | HOLD_N_DAYS | N봉 연속 충족 | | 일목 전용 | 구름/선행스팬 규칙 | **필드 해석:** `resolveField` — OHLCV, `K_*` 상수, RSI/CCI/MACD/Donchian/NH_PRIOR/NL_PRIOR 등 **지표 파라미터:** DB `indicator_settings` + `DSL_TO_REGISTRY` 매핑 ### 7.2 `RuleBuildContext` 구조 ```java public record RuleBuildContext( BarSeries primarySeries, Map> indicatorParams, Map> indicatorVisual, String market, Ta4jStorage storage, /** * CANDLE_CLOSE 평가 시 true — CrossSeriesRule 이 상위봉의 마지막 확정봉 * (endIndex-1)을 사용하도록 강제. provisional 봉 오염을 방지. */ boolean useConfirmedOnly, /** * 백테스트용 사전 집계 시리즈 맵 candleType→BarSeries. * null 이 아니고 비어 있지 않으면 백테스트 모드로 동작하며 * CrossSeriesRule 에서 타임스탬프 기반 룩업을 수행한다. */ Map seriesOverrides ) ``` - `withPrimary(BarSeries)` — primarySeries만 교체해 하위 컨텍스트 생성 - `isBacktest()` — `seriesOverrides`가 비어 있지 않으면 `true` - `resolveSeries(candleType)` — seriesOverrides → Ta4jStorage 순 검색 ### 7.3 멀티 타임프레임 (라이브) `RuleBuildContext`에 `market`, `Ta4jStorage`가 있으면: - `TIMEFRAME.candleType`에 맞는 시리즈를 storage에서 로드 - 트리거 봉(예: 1m 마감) 인덱스에서 **다른 분봉 시리즈 최신 봉**과 `CrossSeriesRule`로 평가 - `useConfirmedOnly=true` — 상위봉은 `getEndIndex()-1` (확정봉 기준, provisional 봉 오염 방지) - `useConfirmedOnly=false` — 상위봉은 `getEndIndex()` (진행 중 봉 포함, REALTIME_TICK 용) ### 7.4 멀티 타임프레임 (백테스트) `BacktestingService.run` 진입 시 DSL에서 상위봉 타입을 자동 추출(`collectTimeframesFromDsl`)하여 기본 시리즈를 집계(`aggregateSeries`)합니다. ```java Map seriesOverrides = buildSeriesOverrides(series, primaryTf, buyDsl, sellDsl); Rule entryRule = adapter.toRule(buyDsl, series, params, seriesOverrides); ``` - `CrossSeriesRule`에서 `isBacktest=true` 시 **타임스탬프 기반 인덱스 룩업**으로 정확한 과거 봉 참조 - 라이브와 동일한 DSL·어댑터를 사용해 백테스트 결과의 신뢰성 확보 --- ## 8. 백엔드: 데이터 수집 → 평가 트리거 ### 8.1 캔들 파이프라인 ```mermaid flowchart LR UWS[Upbit WebSocket 틱] MTP[MarketTickProcessor] BB[BarBuilder] TS[Ta4jStorage BarSeries] UWS --> MTP --> BB --> TS ``` - 동일 분: `updateLastBar` (진행 중 봉 갱신) - 분 변경: `commitBar` → **Phase 1** 확정봉 + 상위봉(3m~1d) 전부 저장 → **Phase 2** 전략 평가 일괄 트리거 ### 8.2 어떤 분봉을 평가할지 `StrategyConditionTimeframeService`: - 전략 DSL을 walk하여 **`TIMEFRAME` 노드의 candleType** 수집 - `usesTimeframe(strategyId, candleType)` — 해당 봉 마감/스케줄에서만 평가 `LiveStrategyTimeframeService`는 UI/설정용 `gc_live_strategy_settings.candle_type` resolve (가상투자 카드 시간봉 드롭다운 등). ### 8.3 BarSeries 윈도우 크기 (warm-up) | 설정 | 기본값 | 설명 | |------|--------|------| | `Ta4jStorage.MAX_BAR_COUNT` | 1800봉 | 인메모리 최대 보관 수 | | `upbit.api.warmup-count` | 1500봉 | 구독 시 초기 로드 목표치 | **원칙:** 전략에서 사용하는 **최대 period × 2~3배** 이상으로 warmup-count를 설정해야 지표 NaN 반환을 방지할 수 있다. 예) HMA(200), 신고가(120봉) 사용 시 → warmup-count ≥ 600봉. 현재 기본값(1500봉)은 EMA(200) 완전 수렴(~1150봉)을 포함하여 충분한 여유를 제공한다. --- ## 9. 시그널 감지 프로세스 ### 9.1 전제: 실시간 전략 설정 (`GcLiveStrategySettings`) | 필드 | 값 | 의미 | |------|-----|------| | `isLiveCheck` | true/false | 실시간 체크 ON | | `executionType` | `CANDLE_CLOSE` / `REALTIME_TICK` | 평가 타이밍 | | `strategyId` | FK | 사용할 `GcStrategy` | | `positionMode` | `LONG_ONLY` / `SIGNAL_ONLY` | 포지션 추적 방식 | | `candleType` | `1m`…`1d` | 설정/UI 기본 분봉 | 가상투자·설정 화면에서 `PUT /api/strategy/settings`로 반영. ### 9.2 시그널 판정 (`StrategySignalDeterminer`) | positionMode | BUY | SELL | |--------------|-----|------| | **LONG_ONLY** | `strategy.shouldEnter(index, record)` | `strategy.shouldExit(index, record)` | | **SIGNAL_ONLY** | `entryRule.isSatisfied(index)` | `exitRule.isSatisfied(index)` | `LONG_ONLY`는 evaluator가 **가상 TradingRecord + positionOpenCache**를 캐시해 중복 진입 방지. **서버 재시작 시 포지션 상태 자동 복원:** `positionOpenCache` 캐시 미스 발생 시 `gc_trade_signal` 테이블의 최신 시그널을 DB에서 조회하여 포지션 상태(BUY = 열림, SELL/없음 = 닫힘)를 자동 복원한다. 서버 재시작 후 중복 BUY 발화를 방지한다. 결과: `"BUY"` | `"SELL"` | `"NONE"` ### 9.3 방식 A — 봉 마감 직후 (`CANDLE_CLOSE`) **트리거:** `BarBuilder.commitBar()` (Phase 2) → `BarCloseStrategyEvaluationService.onMaturedBarClose()` **2-Phase 처리 흐름:** ``` Phase 1 (스토리지 확정) ├─ ta4jStorage.addBar("1m", confirmedBar) ├─ ta4jStorage.updateLastBar("3m", ...) 또는 addBar (마감 시) ├─ ta4jStorage.updateLastBar("5m", ...) 또는 addBar (마감 시) └─ ... 모든 상위봉 처리 완료 Phase 2 (전략 평가 — 모든 봉이 확정된 후) ├─ onMaturedBarClose("1m", ...) ← 1m CrossSeriesRule이 1h 참조해도 1h가 이미 확정됨 ├─ onMaturedBarClose("5m", ...) ← 5m이 마감된 경우 └─ onMaturedBarClose("1h", ...) ← 1h가 마감된 경우 ``` **조건 (각 설정 행):** 1. `isLiveCheck == true` 2. `executionType == CANDLE_CLOSE` 3. `usesTimeframe(strategyId, candleType)` 4. `Ta4jStorage`에 해당 분봉 데이터 존재 **평가:** `LiveStrategyEvaluator.evaluateSettingOnCandleClose(setting, market, candleType, maturedIndex)` - **확정된 봉 인덱스**에서 1회 판정 - `useConfirmedOnly=true` — 상위봉 CrossSeriesRule도 확정봉(`getEndIndex()-1`) 기준 ### 9.4 방식 B — 실시간 틱 (`REALTIME_TICK`) **트리거:** `LiveStrategyScheduler` — **3초 주기** (`@Scheduled(fixedDelay = 3000)`) **조건:** `executionType == REALTIME_TICK` 등 동일 **평가:** `evaluateSettingRealtimeTick(setting, market, candleType)` - `currentIndex = series.getEndIndex()` — **진행 중(provisional) 봉** - **중복 방지:** 동일 `(market, candleType:strategyId)`에서 같은 index에 이미 시그널 냈으면 skip - **Rule 캐시 완전 우회:** 매 평가마다 신규 Rule 인스턴스 빌드 (`evaluateWithFreshRules`) - Ta4j `CachedIndicator` stale 캐시 원천 차단 (provisional 봉 가격 변경 반영) - `triggerRulesCache` 복합 연산(remove→put) 스레드 안전성 문제 제거 - **멀티 TF:** `useConfirmedOnly=false` — 비트리거 상위봉 현재 진행 중인 봉 기준 평가 - **오버헤드 경감:** 앱-레벨 캐시(`strategyCache`, `indicatorCache`, 30초 TTL)로 매 평가마다 DB 조회하지 않음 ### 9.5 보완 — 마감 직후 REALTIME 재평가 봉 마감 시 → `evaluateSettingRealtimeAtClose(setting, market, candleType, maturedIndex)` - 봉 마감 시점의 **교차(CROSS)** 를 3초 스케줄러가 놓치는 경우 대비 - `useConfirmedOnly=true` — 확정봉 인덱스 기준 + 상위봉도 확정봉 사용 - Rule 캐시 우회 ### 9.6 평가 흐름 (단일 종목·단일 시점) ```mermaid flowchart TD A[트리거: 봉 마감 또는 3초 틱] B{isLiveCheck?} C{usesTimeframe?} D1[CANDLE_CLOSE: evaluate - 캐시 사용 - useConfirmedOnly=true] D2[REALTIME_TICK: evaluateWithFreshRules - 캐시 우회 - useConfirmedOnly=false] E[buildTriggerRules - 앱캐시DSL - DSL to Ta4j with CrossSeriesRule] F[StrategySignalDeterminer.determineSignal] G{결과} H[설정별 독립 publishStrategySignal] I[NONE - 종료] A --> B B -->|false| I B -->|true| C C -->|false| I C -->|CANDLE_CLOSE| D1 C -->|REALTIME_TICK| D2 D1 --> E --> F --> G D2 --> E G -->|BUY/SELL| H G -->|NONE| I ``` **주의:** `BarCloseStrategyEvaluationService` / `LiveStrategyScheduler` 는 **설정별 독립 루프**로 멀티 전략 시그널을 각각 발행한다. `evaluateCandleClose(market)`, `evaluateRealtimeTick(market)` 마켓 단위 API 는 첫 non-NONE 만 반환하므로 실시간 시그널 발행 경로에서 사용 금지 (`@Deprecated`). --- ## 10. 시그널 발행·후처리 ### 10.1 STOMP WebSocket — 이중 채널 구조 시그널은 두 개의 독립된 STOMP 채널로 발행된다. | 채널 | 토픽 | 용도 | |------|------|------| | **차트 캔들 스트림** | `/sub/charts/{market}/{candleType}` | 차트 마커 렌더링 (하위 호환) | | **시그널 전용 채널** | `/sub/signals/user/{userId}` | 미션 크리티컬 BUY/SELL 알림 | | | `/sub/signals/device/{deviceId}` | 비로그인 사용자 전용 | **엔드포인트:** `ws://host/ws/trading` (SockJS) **차트 스트림 페이로드 (`CandleBarDto`):** ```json { "t": 1779229260, "o": 92080000.0, "c": 92050000.0, "signal": "BUY", "strategyId": 5, "userId": 123 } ``` **시그널 전용 페이로드 (`SignalEventDto`):** ```json { "market": "KRW-BTC", "candleType": "1m", "strategyId": 5, "signalType": "BUY", "price": 92050000.0, "candleTime": 1779229260, "executionType": "CANDLE_CLOSE", "timestamp": 1779229263000 } ``` **채널 분리 이유:** 차트 스트림은 틱 단위로 대용량 메시지가 발생하는 고부하 채널이다. 브로커 병목 발생 시 매매 타이밍에 치명적인 시그널이 지연될 수 있으므로, 빈도는 낮지만 실시간성이 보장되어야 하는 시그널을 별도 채널로 분리한다. **프론트엔드 구독 (`useLiveStrategyMarkers`):** 두 채널을 병행 구독하며 중복 시그널은 `(market:candleType:time:signal)` 키로 dedup한다. ### 10.2 DB·알림·주문 | 단계 | 클래스 | |------|--------| | DB 저장 | `TradeSignalService.save` → `GcTradeSignal` | | FCM | `FcmPushService` | | 주문 큐 | `OrderExecutionQueue.submitSignal` | ### 10.3 프론트 차트 반영 - STOMP 구독 → 캔들 수신 시 `signal` 필드로 **마커(setMarkers)** 표시 - 시그널 전용 토픽 수신 시 알림 팝업 표시 (차트 스트림 병목과 독립) --- ## 11. 조건 현황 API (시그널과 별개) **용도:** 가상투자 카드, 모니터링 UI — "지금 각 조건이 얼마나 충족됐는지" | 항목 | 내용 | |------|------| | API | `GET /api/strategy/live-conditions?market=&strategyId=` | | 구현 | `LiveConditionStatusService` | | 방식 | buy/sell JSON의 **각 CONDITION 리프**를 개별 `Rule`로 평가 | **응답 필드:** | 필드 | 타입 | 설명 | |------|------|------| | `matchRate` | int (0~100) | 충족 리프 수 / 평가 가능 리프 수 — 참고용, AND/OR 논리 미반영 | | `overallEntryMet` | Boolean | 매수 DSL **전체 논리 트리** 평가 결과 (null=평가 불가) | | `overallExitMet` | Boolean | 매도 DSL **전체 논리 트리** 평가 결과 (null=평가 불가) | | `rows` | List | 개별 조건 리프 충족 여부 상세 | **`matchRate` vs `overallEntryMet` 차이:** ``` 전략: A AND (B OR C) 현재: A=false, B=true, C=true matchRate = 2/3 = 66% (B, C 충족) overallEntryMet = false (A가 false 이므로 AND 전체가 false → 실제 BUY 미발화) ``` 실제 BUY/SELL 발화는 §9 경로만 사용하며, `overallEntryMet`이 실제 발화 여부와 일치한다. --- ## 12. 백테스트 vs 실시간 | 항목 | 백테스트 (`BacktestingService`) | 실시간 (`LiveStrategyEvaluator`) | |------|--------------------------------|----------------------------------| | BarSeries | 요청 `bars`로 일회성 생성 | `Ta4jStorage` 실시간 누적 | | 멀티 TF | **DSL에서 상위봉 타입 추출 → `aggregateSeries` 집계** (`seriesOverrides`) | `Ta4jStorage` 다중 시리즈 | | adapter | `toRule(dsl, series, params, seriesOverrides)` | `toRule(..., market, storage)` | | CrossSeriesRule | **타임스탬프 기반 룩업** (`isBacktest=true`) | `getEndIndex()-1` (확정봉) 또는 `getEndIndex()` (provisional) | | 스캔 | **전 구간** 0..barCount-1 | **단일 index** (마감 or provisional) | | 청산 | DSL sell + **손절/익절/트레일링** OR 합성 | DSL sell (+ 리스크 모니터 별도) | | 출력 | `BacktestResponse.signals`, `gc_backtest_result` | STOMP + `gc_trade_signal` | | `useConfirmedOnly` | 항상 `false` (과거 고정 데이터) | CANDLE_CLOSE: `true`, REALTIME_TICK: `false` | 백테스트도 **동일 DSL·동일 어댑터**를 사용하고 멀티 TF 시리즈를 실시간과 동일하게 집계하므로, 편집기에서 저장한 전략의 백테스트 결과와 라이브 결과가 일관되게 유지된다. --- ## 13. 전략 사용 경로 (편집기 이후) ```mermaid flowchart TB SE[전략편집기 저장] DB[(gc_strategy)] SE --> DB DB --> BT[백테스팅 화면] DB --> LIVE[실시간 전략 설정] DB --> VT[가상투자 카드] LIVE --> LSS[gc_live_strategy_settings] LSS --> EVAL[LiveStrategyEvaluator] VT --> LC[live-conditions API] EVAL --> STOMP_CHART[/sub/charts/{market}/{candleType}] EVAL --> STOMP_SIG[/sub/signals/user/{userId}] EVAL --> TSIG[gc_trade_signal] ``` 1. **전략편집기** — DSL 작성·저장 2. **설정 / 가상투자** — 종목별 `strategyId`, `executionType`, `positionMode`, 평가 분봉 3. **백테스트** — 과거 봉으로 전 구간 시뮬레이션 (멀티TF 포함) 4. **라이브** — Upbit 틱 → Ta4j → 시그널 → STOMP 이중 채널·DB·(자동매매) --- ## 14. 주요 개선 이력 ### [Round 1] 기본 안정성 개선 | 항목 | 개선 내용 | |------|-----------| | **REALTIME_TICK 캐시 우회** | `evaluateWithFreshRules` 도입 — CachedIndicator stale 캐시 원천 차단 | | **멀티TF 확정봉 제어** | `useConfirmedOnly` 플래그 — CANDLE_CLOSE 시 상위봉 확정봉 인덱스 강제 | | **백테스트 멀티TF 지원** | `aggregateSeries` + `seriesOverrides` — 라이브와 동일 방식으로 상위봉 평가 | | **마켓 단위 API Deprecated** | `evaluateCandleClose(market)`, `evaluateRealtimeTick(market)` → `@Deprecated` | ### [Round 2] 고급 신뢰성·확장성 개선 | 항목 | 개선 내용 | |------|-----------| | **[항목1] REALTIME_TICK 오버헤드 경감** | 전략 DSL · 지표 파라미터 앱-레벨 캐시(30s TTL) 도입 → DB 조회 최소화 | | **[항목2] 멀티TF 레이스 컨디션 해소** | `BarBuilder.commitBar` 2-Phase 처리 — Phase 1: 전 타임프레임 스토리지 확정, Phase 2: 전략 평가 | | **[항목3] LONG_ONLY 포지션 DB 복원** | 서버 재시작 후 `gc_trade_signal` 최신 레코드로 포지션 상태 자동 복원 (중복 BUY 방지) | | **[항목4] 지표 윈도우 크기 명세화** | `warmup-count: 1500`, `MAX_BAR_COUNT: 1800` — 최대 period×3배 여유 기준 명시 | | **[항목5] 시그널 전용 STOMP 채널 분리** | `SignalEventBroker` 신설 — `/sub/signals/user/{userId}` 전용 채널로 병목 독립 | | **[항목6] live-conditions 논리 트리 평가** | `overallEntryMet`/`overallExitMet` 필드 추가 — AND/OR/NOT 완전 반영한 전체 트리 결과 | --- ## 15. 주요 소스 파일 ### 프론트엔드 | 경로 | 역할 | |------|------| | `frontend/src/components/StrategyEditorPage.tsx` | 전략편집기 메인 | | `frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx` | React Flow 캔버스 | | `frontend/src/components/strategyEditor/StrategyListEditor.tsx` | 목록 편집기 | | `frontend/src/components/strategyEditor/LogicExpressionPreview.tsx` | 논리 수식 미리보기 | | `frontend/src/utils/strategyTypes.ts` | DSL 타입·조건 상수 | | `frontend/src/utils/strategyConditionSerde.ts` | START↔TIMEFRAME 직렬화 | | `frontend/src/utils/strategyEditorShared.tsx` | CondEditor, 트리 조작, DEF | | `frontend/src/utils/strategyEditorLayoutStorage.ts` | Flow 레이아웃 localStorage | | `frontend/src/utils/compositeIndicators.ts` | 복합지표 정의 | | `frontend/src/utils/strategyStartNodes.ts` | 분봉·START 상수 | | `frontend/src/utils/strategyPresets.ts` | 템플릿 | | `frontend/src/utils/backendApi.ts` | `saveStrategy`, `loadStrategies` | | `frontend/src/hooks/useLiveStrategyMarkers.ts` | 차트+시그널 STOMP 구독 (이중 채널) | | `frontend/src/utils/tradeSignalOwnership.ts` | 시그널 소유자 검증 | ### 백엔드 | 경로 | 역할 | |------|------| | `backend/.../entity/GcStrategy.java` | 전략 엔티티 | | `backend/.../entity/GcLiveStrategySettings.java` | 실시간 설정 | | `backend/.../entity/GcTradeSignal.java` | 시그널 이력 엔티티 | | `backend/.../repository/GcTradeSignalRepository.java` | 시그널 이력 저장소 (포지션 복원 쿼리 포함) | | `backend/.../service/StrategyDslToTa4jAdapter.java` | DSL → Rule (`RuleBuildContext`, `CrossSeriesRule`) | | `backend/.../service/LiveStrategyEvaluator.java` | 실시간 평가 (앱캐시, 포지션 복원) | | `backend/.../service/StrategySignalDeterminer.java` | BUY/SELL 판정 | | `backend/.../service/StrategyTriggerBranchEvaluator.java` | 분기 트리거 평가 (`useConfirmedOnly`) | | `backend/.../service/BacktestingService.java` | 백테스트 (멀티TF `seriesOverrides`) | | `backend/.../service/LiveConditionStatusService.java` | 조건 현황 API (`overallEntryMet`/`overallExitMet`) | | `backend/.../service/BarCloseStrategyEvaluationService.java` | 봉 마감 전략 평가 단일 진입점 | | `backend/.../service/StrategyConditionTimeframeService.java` | DSL 분봉 수집 | | `backend/.../websocket/BarBuilder.java` | 봉 조립 · 2-Phase 마감 처리 | | `backend/.../websocket/LiveStrategyScheduler.java` | 3초 REALTIME_TICK | | `backend/.../websocket/TradingWebSocketBroker.java` | 차트 캔들 STOMP 발행 | | `backend/.../websocket/SignalEventBroker.java` | 시그널 전용 STOMP 발행 (`/sub/signals/user/{userId}`) | | `backend/.../dto/SignalEventDto.java` | 시그널 전용 페이로드 | | `backend/.../dto/LiveConditionStatusDto.java` | 조건 현황 응답 (`overallEntryMet` 포함) | | `backend/.../storage/Ta4jStorage.java` | BarSeries 저장소 (MAX_BAR_COUNT=1800) | | `backend/.../config/WebSocketConfig.java` | STOMP 브로커 설정 | --- ## 16. 핵심 체크리스트 (디버깅용) 1. **저장 DSL** — `encodeConditionForSave` 후 JSON에 `TIMEFRAME` / `AND`+`TIMEFRAME` 구조가 의도대로인가? 2. **분봉** — 조건 리프의 `timeframe` vs `TIMEFRAME` 래핑 `candleType` 일치 여부 3. **실시간 ON** — `isLiveCheck`, `strategyId` 연결 4. **실행 방식** — `CANDLE_CLOSE` vs `REALTIME_TICK` 5. **데이터 warm-up** — `Ta4jStorage.exists(market, candleType)`, `warmup-count ≥ max_period×2` 6. **포지션 모드** — `LONG_ONLY`에서 서버 재시작 후 포지션 상태가 DB에서 올바르게 복원됐는지 (`gc_trade_signal` 최신 레코드) 7. **멀티TF 레이스 컨디션** — `BarBuilder.commitBar` Phase 1(스토리지 전부 확정) 후 Phase 2(평가) 순서 준수 확인 8. **STOMP 이중 채널** — 차트 `/sub/charts/{market}/{candleType}` + 시그널 전용 `/sub/signals/user/{userId}` 구독 모두 확인 9. **UI 현황 vs 시그널** — `matchRate`(리프 단순 비율, 참고용) vs `overallEntryMet`(전체 트리 평가, 실제 발화와 일치) 10. **마감봉 평가** — `BarCloseStrategyEvaluationService` + DSL `usesTimeframe` 게이트; 갭은 `CandleGapBackfillService` 후 보정 11. **프론트 평가 금지** — 가상투자 일치율·자동매매는 백엔드 API/STOMP 만 사용 12. **@Deprecated API** — `evaluateCandleClose(market)`, `evaluateRealtimeTick(market)` 은 멀티 전략 환경에서 사용 금지 --- *문서 작성 기준: goldenChart 저장소 프론트/백엔드 소스 구조. API·클래스명은 변경될 수 있으므로 구현 확인 시 §15 파일 목록을 참고하세요.*