Files
goldenChart/전략편집기 로직 및 동작흐름.md
2026-06-05 12:22:29 +09:00

785 lines
34 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 **전략편집기(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<String, Map<String, Object>> indicatorParams,
Map<String, Map<String, Object>> indicatorVisual,
String market,
Ta4jStorage storage,
/**
* CANDLE_CLOSE 평가 시 true — CrossSeriesRule 이 상위봉의 마지막 확정봉
* (endIndex-1)을 사용하도록 강제. provisional 봉 오염을 방지.
*/
boolean useConfirmedOnly,
/**
* 백테스트용 사전 집계 시리즈 맵 candleType→BarSeries.
* null 이 아니고 비어 있지 않으면 백테스트 모드로 동작하며
* CrossSeriesRule 에서 타임스탬프 기반 룩업을 수행한다.
*/
Map<String, BarSeries> 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<String, BarSeries> 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 파일 목록을 참고하세요.*