640 lines
26 KiB
Markdown
640 lines
26 KiB
Markdown
# 전략편집기 로직 및 동작흐름
|
||
|
||
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` — 1m 확정 시 전략 DSL에 포함된 상위봉(3m·5m·10m·15m …)까지 전파
|
||
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)` 등
|
||
|
||
### 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`에 `market`, `Ta4jStorage`가 있으면:
|
||
|
||
- `TIMEFRAME.candleType`에 맞는 시리즈를 storage에서 로드
|
||
- 트리거 봉(예: 1m 마감) 인덱스에서 **다른 분봉 시리즈 최신 봉**과 `CrossSeriesRule`로 평가
|
||
|
||
백테스트는 단일 요청 `BarSeries`만 사용 → 교차 TF는 primary series fallback.
|
||
|
||
### 7.3 Strategy 조립 (`LiveStrategyEvaluator.buildStrategy`)
|
||
|
||
```java
|
||
Rule entryRule = buildRule(buyConditionJson, series, params, market, storage);
|
||
Rule exitRule = buildRule(sellConditionJson, series, params, market, storage);
|
||
BaseStrategy strategy = new BaseStrategy(entryRule, exitRule);
|
||
```
|
||
|
||
캐시 키: `market:candleType:strategyId`
|
||
|
||
---
|
||
|
||
## 8. 백엔드: 데이터 수집 → 평가 트리거
|
||
|
||
### 8.1 캔들 파이프라인
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
UWS[Upbit WebSocket 틱]
|
||
MTP[MarketTickProcessor]
|
||
BB[BarBuilder]
|
||
TS[Ta4jStorage BarSeries]
|
||
|
||
UWS --> MTP --> BB --> TS
|
||
```
|
||
|
||
- 동일 분: `updateLastBar` (진행 중 봉 갱신)
|
||
- 분 변경: `commitBar` → 확정봉 + 상위봉(3m~1d) 집계
|
||
|
||
### 8.2 어떤 분봉을 평가할지
|
||
|
||
`StrategyConditionTimeframeService`:
|
||
|
||
- 전략 DSL을 walk하여 **`TIMEFRAME` 노드의 candleType** 수집
|
||
- `usesTimeframe(strategyId, candleType)` — 해당 봉 마감/스케줄에서만 평가
|
||
|
||
`LiveStrategyTimeframeService`는 UI/설정용 `gc_live_strategy_settings.candle_type` resolve (가상투자 카드 시간봉 드롭다운 등).
|
||
|
||
---
|
||
|
||
## 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**를 캐시해 중복 진입 방지.
|
||
|
||
결과: `"BUY"` | `"SELL"` | `"NONE"`
|
||
|
||
### 9.3 방식 A — 봉 마감 직후 (`CANDLE_CLOSE`)
|
||
|
||
**트리거:** `BarBuilder.commitBar()` → `evaluateStrategyOnBarClose()`
|
||
|
||
**조건 (각 설정 행):**
|
||
|
||
1. `isLiveCheck == true`
|
||
2. `executionType == CANDLE_CLOSE`
|
||
3. `usesTimeframe(strategyId, candleType)`
|
||
4. `Ta4jStorage`에 해당 분봉 데이터 존재
|
||
|
||
**평가:** `LiveStrategyEvaluator.evaluateCandleClose(market, candleType, maturedIndex)`
|
||
|
||
- **확정된 직전 봉 인덱스**에서 1회 판정
|
||
|
||
### 9.4 방식 B — 실시간 틱 (`REALTIME_TICK`)
|
||
|
||
**트리거:** `LiveStrategyScheduler` — **3초 주기** (`@Scheduled`)
|
||
|
||
**조건:** `executionType == REALTIME_TICK` 등 동일
|
||
|
||
**평가:** `evaluateSettingRealtimeTick(setting, market, candleType)`
|
||
|
||
- `currentIndex = series.getEndIndex()` — **진행 중(provisional) 봉**
|
||
- **중복 방지:** 동일 `(market, candleType:strategyId)`에서 같은 index에 이미 시그널 냈으면 skip
|
||
- **Rule 캐시 완전 우회:** 매 평가마다 신규 Rule 인스턴스 빌드 (`evaluateWithFreshRules`)
|
||
- Ta4j `CachedIndicator` stale 캐시 원천 차단
|
||
- `triggerRulesCache` 복합 연산(remove→put) 스레드 안전성 문제 제거
|
||
- **멀티 TF:** `useConfirmedOnly=false` — 비트리거 상위봉 현재 진행 중인 봉 기준 평가
|
||
|
||
### 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 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
|
||
|
||
**토픽:** `/sub/charts/{market}/{candleType}`
|
||
**엔드포인트:** `ws://host/ws/trading` (SockJS)
|
||
|
||
**페이로드 (`CandleBarDto`):** OHLCV + 지표값 + **`signal`**: `"BUY"` | `"SELL"` | null
|
||
|
||
```json
|
||
{
|
||
"t": 1779229260,
|
||
"o": 92080000.0,
|
||
"c": 92050000.0,
|
||
"signal": "BUY"
|
||
}
|
||
```
|
||
|
||
별도 “시그널 전용” 토픽은 없음 — **차트 캔들 스트림에 시그널 필드**로 포함.
|
||
|
||
### 10.2 DB·알림·주문
|
||
|
||
| 단계 | 클래스 |
|
||
|------|--------|
|
||
| DB 저장 | `TradeSignalService.save` → `GcTradeSignal` |
|
||
| FCM | `FcmPushService` |
|
||
| 주문 큐 | `OrderExecutionQueue.submitSignal` (deviceId 있을 때) |
|
||
|
||
### 10.3 프론트 차트 반영
|
||
|
||
- STOMP 구독 → 캔들 수신 시 `signal` 필드로 **마커(setMarkers)** 표시
|
||
- `LiveSignalNotifier`, 차트 슬롯 등
|
||
|
||
---
|
||
|
||
## 11. 조건 현황 API (시그널과 별개)
|
||
|
||
**용도:** 가상투자 카드, 모니터링 UI — “지금 각 조건이 얼마나 충족됐는지”
|
||
|
||
| 항목 | 내용 |
|
||
|------|------|
|
||
| API | `GET /api/strategy/live-conditions?market=&strategyId=` |
|
||
| 구현 | `LiveConditionStatusService` |
|
||
| 방식 | buy/sell JSON의 **각 CONDITION 리프**를 개별 `Rule`로 평가 |
|
||
| matchRate | 충족 리프 수 / 평가 가능 리프 수 (전체 AND/OR 트리 충족과 다름) |
|
||
|
||
실제 BUY/SELL 발화는 §9 경로만 사용.
|
||
|
||
---
|
||
|
||
## 12. 백테스트 vs 실시간
|
||
|
||
| 항목 | 백테스트 (`BacktestingService`) | 실시간 (`LiveStrategyEvaluator`) |
|
||
|------|--------------------------------|----------------------------------|
|
||
| BarSeries | 요청 `bars`로 일회성 생성 | `Ta4jStorage` 실시간 누적 |
|
||
| 멀티 TF | **DSL에서 상위봉 타입 추출 → 집계 시리즈 생성** (→ seriesOverrides) | `Ta4jStorage` 다중 시리즈 |
|
||
| adapter | `toRule(dsl, series, params, seriesOverrides)` (백테스트 멀티 TF) | `toRule(..., market, storage)` |
|
||
| CrossSeriesRule | **타임스탬프 기반 룩업** (isBacktest=true) | getEndIndex() 또는 endIndex-1 |
|
||
| 스캔 | **전 구간** 0..barCount-1 | **단일 index** (마감 or provisional) |
|
||
| 청산 | DSL sell + **손절/익절/트레일링** OR 합성 | DSL sell (+ 리스크 모니터 별도) |
|
||
| 출력 | `BacktestResponse.signals`, `gc_backtest_result` | STOMP + `gc_trade_signal` |
|
||
| 멀티 TF | storage 없으면 단일 시리즈 fallback | `TIMEFRAME` / 교차 TF 지원 |
|
||
|
||
백테스트도 **동일 DSL·동일 어댑터**를 사용하므로, 편집기에서 저장한 전략이 백테스트와 라이브에서 같은 의미로 해석됩니다 (데이터·타이밍 차이는 있음).
|
||
|
||
---
|
||
|
||
## 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[STOMP signal]
|
||
EVAL --> TSIG[gc_trade_signal]
|
||
```
|
||
|
||
1. **전략편집기** — DSL 작성·저장
|
||
2. **설정 / 가상투자** — 종목별 `strategyId`, `executionType`, `positionMode`, 평가 분봉
|
||
3. **백테스트** — 과거 봉으로 전 구간 시뮬레이션
|
||
4. **라이브** — Upbit 틱 → Ta4j → 시그널 → STOMP·DB·(자동매매)
|
||
|
||
---
|
||
|
||
## 14. 주요 소스 파일
|
||
|
||
### 프론트엔드
|
||
|
||
| 경로 | 역할 |
|
||
|------|------|
|
||
| `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` |
|
||
|
||
### 백엔드
|
||
|
||
| 경로 | 역할 |
|
||
|------|------|
|
||
| `backend/.../entity/GcStrategy.java` | 전략 엔티티 |
|
||
| `backend/.../entity/GcLiveStrategySettings.java` | 실시간 설정 |
|
||
| `backend/.../service/StrategyDslToTa4jAdapter.java` | DSL → Rule |
|
||
| `backend/.../service/LiveStrategyEvaluator.java` | 실시간 평가 |
|
||
| `backend/.../service/StrategySignalDeterminer.java` | BUY/SELL 판정 |
|
||
| `backend/.../service/BacktestingService.java` | 백테스트 |
|
||
| `backend/.../service/LiveConditionStatusService.java` | 조건 현황 API |
|
||
| `backend/.../service/StrategyConditionTimeframeService.java` | DSL 분봉 수집 |
|
||
| `backend/.../websocket/BarBuilder.java` | 봉 조립·마감 시 평가 트리거 |
|
||
| `backend/.../service/BarCloseStrategyEvaluationService.java` | 봉 마감 전략 평가 단일 진입점 |
|
||
| `backend/.../websocket/LiveStrategyScheduler.java` | 3초 REALTIME_TICK |
|
||
| `backend/.../websocket/TradingWebSocketBroker.java` | STOMP 발행 |
|
||
| `backend/.../storage/Ta4jStorage.java` | BarSeries 저장소 |
|
||
|
||
---
|
||
|
||
## 15. 핵심 체크리스트 (디버깅용)
|
||
|
||
1. **저장 DSL** — `encodeConditionForSave` 후 JSON에 `TIMEFRAME` / `AND`+`TIMEFRAME` 구조가 의도대로인가?
|
||
2. **분봉** — 조건 리프의 `timeframe` vs `TIMEFRAME` 래핑 `candleType` 일치 여부
|
||
3. **실시간 ON** — `isLiveCheck`, `strategyId` 연결
|
||
4. **실행 방식** — `CANDLE_CLOSE` vs `REALTIME_TICK`
|
||
5. **데이터** — `Ta4jStorage.exists(market, candleType)`, warm-up(`pinCandleWatch`)
|
||
6. **포지션 모드** — `LONG_ONLY`에서 이미 포지션일 때 BUY 무시
|
||
7. **STOMP** — 구독 토픽 `/sub/charts/{market}/{candleType}` 와 평가 분봉 일치
|
||
8. **UI 현황 vs 시그널** — `live-conditions` matchRate ≠ 실제 BUY/SELL 발화 조건 (UI는 리프별 Rule, 시그널은 트리 전체)
|
||
9. **마감봉 평가** — `BarCloseStrategyEvaluationService` + DSL `usesTimeframe` 게이트; 갭은 `CandleGapBackfillService` 후 보정
|
||
10. **프론트 평가 금지** — 가상투자 일치율·자동매매는 백엔드 API/STOMP 만 사용
|
||
|
||
---
|
||
|
||
*문서 작성 기준: goldenChart 저장소 프론트/백엔드 소스 구조. API·클래스명은 변경될 수 있으므로 구현 확인 시 §14 파일 목록을 참고하세요.*
|