diff --git a/backend/src/main/java/com/goldenchart/service/LiveStrategyEvaluator.java b/backend/src/main/java/com/goldenchart/service/LiveStrategyEvaluator.java index cdadd4a..7ea9f4c 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveStrategyEvaluator.java +++ b/backend/src/main/java/com/goldenchart/service/LiveStrategyEvaluator.java @@ -103,6 +103,8 @@ public class LiveStrategyEvaluator { if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue; if (!"CANDLE_CLOSE".equals(s.getExecutionType())) continue; if (s.getStrategyId() == null) continue; + if (!candleType.equals(LiveStrategyTimeframeService.normalize( + s.getCandleType() != null ? s.getCandleType() : "1m"))) continue; String result = evaluate(market, candleType, s.getStrategyId(), s.getPositionMode(), maturedIndex, @@ -153,6 +155,8 @@ public class LiveStrategyEvaluator { if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue; if (!"REALTIME_TICK".equals(s.getExecutionType())) continue; if (s.getStrategyId() == null) continue; + if (!candleType.equals(LiveStrategyTimeframeService.normalize( + s.getCandleType() != null ? s.getCandleType() : "1m"))) continue; // ★ Ta4j CachedIndicator 캐시 무효화: // updateLastBar 로 provisional 봉 가격이 바뀌어도 CCI 등 CachedIndicator 는 @@ -198,6 +202,8 @@ public class LiveStrategyEvaluator { if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue; if (!"REALTIME_TICK".equals(s.getExecutionType())) continue; if (s.getStrategyId() == null) continue; + if (!candleType.equals(LiveStrategyTimeframeService.normalize( + s.getCandleType() != null ? s.getCandleType() : "1m"))) continue; // CachedIndicator 캐시 무효화 — 확정봉의 최종 close 로 재계산 String cacheKey = market + ":" + candleType + ":" + s.getStrategyId(); diff --git a/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java b/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java index 43470c4..a71ed5e 100644 --- a/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java +++ b/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java @@ -46,7 +46,6 @@ public class BarBuilder { private final TradeSignalService tradeSignalService; private final OrderExecutionQueue orderExecutionQueue; private final GcLiveStrategySettingsRepository liveSettingsRepo; - private final LiveStrategyTimeframeService timeframeService; /** 상위 집계 타임프레임 (분 단위 집계 주기: 3, 5, 15, 30, 60, 240, 1440) */ private static final int[] UPPER_MINUTES = {3, 5, 15, 30, 60, 240, 1440}; @@ -115,9 +114,8 @@ public class BarBuilder { ta4jStorage.addBar(market, "1m", bar); log.debug("[BarBuilder] 1m Bar 확정: {} @ {}", market, endTime); - String evalCandleType = timeframeService.resolveCandleTypeForMarket(market); - BarSeries series1m = ta4jStorage.getOrCreate(market, "1m"); - if ("1m".equals(evalCandleType)) { + if (shouldEvaluateOnClose(market, "1m")) { + BarSeries series1m = ta4jStorage.getOrCreate(market, "1m"); evaluateStrategyOnBarClose(market, "1m", series1m.getEndIndex(), bar); } @@ -147,7 +145,7 @@ public class BarBuilder { Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd); ta4jStorage.addBar(market, type, upperBar); BarSeries upperAfter = ta4jStorage.getOrCreate(market, type); - if (type.equals(evalCandleType)) { + if (shouldEvaluateOnClose(market, type)) { int maturedUpper = Math.max(upperAfter.getBeginIndex(), upperAfter.getEndIndex() - 1); Bar maturedUpperBar = upperAfter.getBar(maturedUpper); evaluateStrategyOnBarClose(market, type, maturedUpper, maturedUpperBar); @@ -157,6 +155,14 @@ public class BarBuilder { } } + /** 활성 실시간 전략 설정 중 해당 분봉(candleType)으로 평가하는 항목이 있는지 */ + private boolean shouldEvaluateOnClose(String market, String candleType) { + String ct = LiveStrategyTimeframeService.normalize(candleType); + return liveSettingsRepo.findActiveByMarket(market).stream() + .anyMatch(s -> ct.equals(LiveStrategyTimeframeService.normalize( + s.getCandleType() != null ? s.getCandleType() : "1m"))); + } + /** * 봉 마감 시점 전략 평가 + 시그널 STOMP 발행(평가 분봉 채널) + DB/주문 큐 적재. */ diff --git a/frontend/src/hooks/useIndicatorSettings.ts b/frontend/src/hooks/useIndicatorSettings.ts index ba14cef..c957893 100644 --- a/frontend/src/hooks/useIndicatorSettings.ts +++ b/frontend/src/hooks/useIndicatorSettings.ts @@ -22,7 +22,7 @@ */ import { useEffect, useRef, useCallback } from 'react'; -import { getIndicatorDef, mergePlotDefs, PlotDef, HLineDef } from '../utils/indicatorRegistry'; +import { getIndicatorDef, mergePlotDefs, normalizeHLines, PlotDef, HLineDef } from '../utils/indicatorRegistry'; import { createDefaultSmaParams, createDefaultSmaPlots, normalizeSmaConfig } from '../utils/smaConfig'; import { normalizeIchimokuConfig, mergeIchimokuPlots } from '../utils/ichimokuConfig'; import { @@ -170,7 +170,9 @@ export function useIndicatorSettings(sessionKey = 0) { const def = getIndicatorDef(type); const registryPlots = defaultPlots ?? def?.plots ?? []; let plots = mergePlotDefs(saved?.plots as PlotDef[] | undefined, registryPlots); - const hlines = (saved?.hlines as HLineDef[] | undefined) ?? defaultHlines ?? def?.hlines ?? []; + const registryHlines = defaultHlines ?? def?.hlines ?? []; + const rawHlines = (saved?.hlines as HLineDef[] | undefined) ?? registryHlines; + const hlines = normalizeHLines(rawHlines, type, registryHlines); if (type === 'SMA') { plots = normalizeSmaConfig({ diff --git a/frontend/src/utils/indicatorMainTab.ts b/frontend/src/utils/indicatorMainTab.ts index 5d79719..027c6e2 100644 --- a/frontend/src/utils/indicatorMainTab.ts +++ b/frontend/src/utils/indicatorMainTab.ts @@ -27,7 +27,7 @@ export const MAIN_INDICATOR_TYPES: readonly string[] = [ /** Main 탭 전용 표시 라벨 (업비트 주요 지표 명칭) */ export const MAIN_INDICATOR_LABELS: Record = { RSI: { ko: 'RSI (상대강도지수)', en: 'Relative Strength Index' }, - Stochastic: { ko: 'Stochastic Oscillator', en: 'Stochastic Oscillator' }, + Stochastic: { ko: 'Stochastic Slow', en: 'Stochastic Slow' }, BollingerBands: { ko: '볼린저밴드', en: 'Bollinger Bands' }, IchimokuCloud: { ko: '일목균형표', en: 'Ichimoku Cloud' }, ADX: { ko: 'ADX (평균방향성지수)', en: 'Average Directional Index' }, diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index 98a6c2b..ce27250 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -73,12 +73,33 @@ function migrateHLineLabel(label: string | undefined): string | undefined { return label; } +/** normalizeHLines 합성 중앙선(0) 등 레거시 저장값을 registry 기본 가격으로 교정 */ +function resolveMergedHLinePrice( + roleLabel: string, + savedPrice: number, + defPrice: number, +): number { + if (roleLabel === '중앙선' && savedPrice === 0 && defPrice !== 0) { + return defPrice; + } + return savedPrice; +} + /** 지표별 누락 수평선 합성 기본값 */ const HL_SYNTH_DEFAULTS: Record = { - ADX: { over: 40, mid: 25, under: 20 }, - DMI: { over: 30, mid: 20, under: 10 }, + ADX: { over: 40, mid: 25, under: 20 }, + DMI: { over: 30, mid: 20, under: 10 }, + Stochastic: { over: 80, mid: 50, under: 20 }, + StochRSI: { over: 80, mid: 50, under: 20 }, }; +/** Stochastic Slow 기본 수평선 — 과열 80 / 중앙 50(기본 off) / 침체 20 */ +export const STOCHASTIC_DEFAULT_HLINES: HLineDef[] = [ + { price: 80, color: '#EF5350', label: '과열선', lineStyle: 'dashed', lineWidth: 1 }, + { price: 50, color: '#607D8B', label: '중앙선', lineStyle: 'dashed', lineWidth: 1, visible: false }, + { price: 20, color: '#4CAF50', label: '침체선', lineStyle: 'dashed', lineWidth: 1 }, +]; + /** ADX 기본 수평선 — 강한 추세(과열) / 기준(중앙) / 약한 추세(침체) */ export const ADX_DEFAULT_HLINES: HLineDef[] = [ { price: 40, color: '#EF5350', label: '과열선', lineStyle: 'dashed', lineWidth: 1 }, @@ -130,10 +151,17 @@ export function mergeHlines( savedList.find(h => labelOf(h) === wantLabel) ?? (legacyMid && wantLabel === '중앙선' ? legacyMid : undefined); if (match) { - return { ...def, ...match, label: wantLabel, price: match.price }; + const price = resolveMergedHLinePrice(wantLabel, match.price, def.price); + return { ...def, ...match, label: wantLabel, price }; } return { ...def, label: wantLabel }; }) + .concat( + savedList.filter(h => { + const lbl = labelOf(h); + return !defs.some(d => (d.label ?? getHLineLabel(d.price, defs.map(x => x.price))) === lbl); + }).map(h => ({ ...h, label: labelOf(h) })), + ) .sort((a, b) => b.price - a.price); } @@ -257,8 +285,8 @@ export const INDICATOR_REGISTRY: IndicatorDef[] = [ // ── Oscillators ─────────────────────────────────────────────────────────── { type:'RSI', name:'Relative Strength Index', koreanName:'상대강도지수', shortName:'RSI', category:'Oscillators', overlay:false, defaultParams:{length:14, src:'close', maType:'SMA', maLength:14}, plots:[{id:'plot0',title:'RSI',color:'#7E57C2',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#F23645',type:'line',lineWidth:1}], hlines:[{price:70,color:'#EF5350'},{price:50,color:'#607D8B'},{price:30,color:'#4CAF50'}], description:'상대 강도 지수 (0~100)' }, - { type:'Stochastic', name:'Stochastic', koreanName:'스토캐스틱', shortName:'Stoch', category:'Oscillators', overlay:false, defaultParams:{kLength:14, dSmoothing:3, smooth:3}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:80,color:'#EF5350'},{price:20,color:'#4CAF50'}], description:'스토캐스틱 %K/%D' }, - { type:'StochRSI', name:'Stochastic RSI', koreanName:'스토캐스틱RSI', shortName:'StochRSI',category:'Oscillators',overlay:false, defaultParams:{lengthRSI:14, lengthStoch:14, smoothK:3, smoothD:3, src:'close'}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:80,color:'#EF5350'},{price:20,color:'#4CAF50'}], description:'스토캐스틱 RSI' }, + { type:'Stochastic', name:'Stochastic Slow', koreanName:'스토케스틱 슬로우', shortName:'Stoch', category:'Oscillators', overlay:false, defaultParams:{kLength:14, dSmoothing:3, smooth:3}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:STOCHASTIC_DEFAULT_HLINES, description:'Stochastic Slow %K/%D (Slowing 적용)' }, + { type:'StochRSI', name:'Stochastic RSI', koreanName:'스토캐스틱RSI', shortName:'StochRSI',category:'Oscillators',overlay:false, defaultParams:{lengthRSI:14, lengthStoch:14, smoothK:3, smoothD:3, src:'close'}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:STOCHASTIC_DEFAULT_HLINES, description:'스토캐스틱 RSI' }, { type:'CCI', name:'Commodity Channel Index', koreanName:'상품채널지수', shortName:'CCI', category:'Oscillators', overlay:false, defaultParams:{length:13, src:'hlc3', maType:'SMA', maLength:20}, plots:[{id:'plot0',title:'CCI',color:'#2962FF',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#F23645',type:'line',lineWidth:1}], hlines:[{price:100,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF50'}], description:'상품 채널 지수' }, { type:'WilliamsPercentRange',name:'Williams %R', koreanName:'윌리엄스 %R', shortName:'W%R', category:'Oscillators', overlay:false, defaultParams:{length:14, src:'close'}, plots:[{id:'plot0',title:'%R', color:'#9C27B0',type:'line',lineWidth:2}], hlines:[{price:-20,color:'#EF5350'},{price:-50,color:'#607D8B'},{price:-80,color:'#4CAF50'}], description:'윌리엄스 %R 오실레이터' }, { type:'AwesomeOscillator', name:'Awesome Oscillator', koreanName:'어썸오실레이터', shortName:'AO', category:'Oscillators', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'AO',color:'#26A69A',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'AO 오실레이터' },