34 KiB
전략편집기 로직 및 동작흐름
goldenChart 전략편집기(Strategy Editor) 의 화면 구성, 편집·저장 방식, 그리고 저장된 전략이 백엔드에서 매매 시그널(BUY/SELL)을 감지하기까지의 전체 흐름을 정리한 문서입니다.
관련 문서: 실시간 전략체크 구현.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 봉 마감별 전략 체크 (스킵 방지)
- 업비트 틱 →
BarBuilder.commitBar— 2-Phase 처리 (§9.3 참고)- Phase 1: 1m 확정 후 상위봉(3m·5m·10m·15m …) 전부 스토리지에 저장
- Phase 2: 모든 봉 저장 완료 후 전략 평가 일괄 트리거 (멀티TF 레이스 컨디션 해소)
- 각 상위봉 마감 시
BarCloseStrategyEvaluationService.onMaturedBarClose—StrategyConditionTimeframeService.usesTimeframe이 true 인 분봉만 평가 - WS 갭 →
CandleGapBackfillServiceREST 병합 후evaluateRecentMaturedBarsAfterBackfill로 병합 구간 마감봉 보정 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→ localStoragegc_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)
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 리프 예시
{
"id": "n_abc123",
"type": "CONDITION",
"condition": {
"indicatorType": "RSI",
"conditionType": "CROSS_UP",
"leftField": "RSI_VALUE",
"rightField": "K_70",
"period": 14
}
}
3.4 논리 노드 예시
{
"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)
{
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 + 해당 분봉 - 그 외 → 기본
1mSTART, 루트 = 전체 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)
{
"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_NICHIMOKU→ 구름/선행스팬 전용conditionType(ABOVE_CLOUD,CLOUD_BREAK_UP, …)
6. 저장·로드 프로세스 (프론트엔드)
6.1 저장 (handleSave)
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 로드
loadStrategies()— 서버 우선- 실패 시
loadStratsLocal()(gc_strat_v2) - 전략 선택 →
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 구조
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가 비어 있지 않으면trueresolveSeries(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)합니다.
Map<String, BarSeries> seriesOverrides = buildSeriesOverrides(series, primaryTf, buyDsl, sellDsl);
Rule entryRule = adapter.toRule(buyDsl, series, params, seriesOverrides);
CrossSeriesRule에서isBacktest=true시 타임스탬프 기반 인덱스 룩업으로 정확한 과거 봉 참조- 라이브와 동일한 DSL·어댑터를 사용해 백테스트 결과의 신뢰성 확보
8. 백엔드: 데이터 수집 → 평가 트리거
8.1 캔들 파이프라인
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가 마감된 경우
조건 (각 설정 행):
isLiveCheck == trueexecutionType == CANDLE_CLOSEusesTimeframe(strategyId, candleType)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
CachedIndicatorstale 캐시 원천 차단 (provisional 봉 가격 변경 반영) triggerRulesCache복합 연산(remove→put) 스레드 안전성 문제 제거
- Ta4j
- 멀티 TF:
useConfirmedOnly=false— 비트리거 상위봉 현재 진행 중인 봉 기준 평가 - 오버헤드 경감: 앱-레벨 캐시(
strategyCache,indicatorCache, 30초 TTL)로 매 평가마다 DB 조회하지 않음
9.5 보완 — 마감 직후 REALTIME 재평가
봉 마감 시 → evaluateSettingRealtimeAtClose(setting, market, candleType, maturedIndex)
- 봉 마감 시점의 교차(CROSS) 를 3초 스케줄러가 놓치는 경우 대비
useConfirmedOnly=true— 확정봉 인덱스 기준 + 상위봉도 확정봉 사용- Rule 캐시 우회
9.6 평가 흐름 (단일 종목·단일 시점)
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):
{
"t": 1779229260,
"o": 92080000.0,
"c": 92050000.0,
"signal": "BUY",
"strategyId": 5,
"userId": 123
}
시그널 전용 페이로드 (SignalEventDto):
{
"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. 전략 사용 경로 (편집기 이후)
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]
- 전략편집기 — DSL 작성·저장
- 설정 / 가상투자 — 종목별
strategyId,executionType,positionMode, 평가 분봉 - 백테스트 — 과거 봉으로 전 구간 시뮬레이션 (멀티TF 포함)
- 라이브 — 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. 핵심 체크리스트 (디버깅용)
- 저장 DSL —
encodeConditionForSave후 JSON에TIMEFRAME/AND+TIMEFRAME구조가 의도대로인가? - 분봉 — 조건 리프의
timeframevsTIMEFRAME래핑candleType일치 여부 - 실시간 ON —
isLiveCheck,strategyId연결 - 실행 방식 —
CANDLE_CLOSEvsREALTIME_TICK - 데이터 warm-up —
Ta4jStorage.exists(market, candleType),warmup-count ≥ max_period×2 - 포지션 모드 —
LONG_ONLY에서 서버 재시작 후 포지션 상태가 DB에서 올바르게 복원됐는지 (gc_trade_signal최신 레코드) - 멀티TF 레이스 컨디션 —
BarBuilder.commitBarPhase 1(스토리지 전부 확정) 후 Phase 2(평가) 순서 준수 확인 - STOMP 이중 채널 — 차트
/sub/charts/{market}/{candleType}+ 시그널 전용/sub/signals/user/{userId}구독 모두 확인 - UI 현황 vs 시그널 —
matchRate(리프 단순 비율, 참고용) vsoverallEntryMet(전체 트리 평가, 실제 발화와 일치) - 마감봉 평가 —
BarCloseStrategyEvaluationService+ DSLusesTimeframe게이트; 갭은CandleGapBackfillService후 보정 - 프론트 평가 금지 — 가상투자 일치율·자동매매는 백엔드 API/STOMP 만 사용
- @Deprecated API —
evaluateCandleClose(market),evaluateRealtimeTick(market)은 멀티 전략 환경에서 사용 금지
문서 작성 기준: goldenChart 저장소 프론트/백엔드 소스 구조. API·클래스명은 변경될 수 있으므로 구현 확인 시 §15 파일 목록을 참고하세요.