From 09e44008cb34dbf899e3715e0928adad4390eeb6 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sat, 30 May 2026 00:46:31 +0900 Subject: [PATCH] =?UTF-8?q?cci=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CciOnSourceIndicator.java | 22 +++++++++ .../goldenchart/service/IndicatorService.java | 3 +- .../service/StrategyDslToTa4jAdapter.java | 4 +- frontend/src/App.css | 49 ++++++++++++++++++- frontend/src/components/RightSidePanel.tsx | 31 ++++++------ frontend/src/utils/customIndicators.ts | 10 ++-- frontend/src/utils/indicatorRegistry.ts | 13 +++-- 7 files changed, 107 insertions(+), 25 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java b/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java index f3061d1..d4ea0fa 100644 --- a/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java +++ b/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java @@ -7,12 +7,34 @@ import org.ta4j.core.indicators.averages.SMAIndicator; import org.ta4j.core.indicators.statistics.MeanDeviationIndicator; import org.ta4j.core.num.Num; +import java.util.HashMap; +import java.util.Map; + /** * TradingView/업비트 CCI — 임의 가격 소스에 대해 * (src - SMA) / (0.015 × MeanDeviation) 계산. */ public class CciOnSourceIndicator extends CachedIndicator { + /** 업비트·TradingView 기본: hlc3(고저종), 기간 13, 신호 SMA 10 */ + public static Map normalizeParams(Map p) { + Map out = new HashMap<>(p); + String maType = String.valueOf(p.getOrDefault("maType", "SMA")); + int length = intParam(p, "length", 13); + int maLength = intParam(p, "maLength", 10); + String src = String.valueOf(p.getOrDefault("src", "hlc3")); + if ("close".equals(src) && length == 13 && maLength == 10 && "SMA".equals(maType)) { + src = "hlc3"; + } + out.put("src", src); + return out; + } + + private static int intParam(Map p, String k, int def) { + Object v = p.get(k); + return v == null ? def : ((Number) v).intValue(); + } + private final Indicator price; private final SMAIndicator sma; private final MeanDeviationIndicator meanDeviation; diff --git a/backend/src/main/java/com/goldenchart/service/IndicatorService.java b/backend/src/main/java/com/goldenchart/service/IndicatorService.java index 66af4bc..4071cd5 100644 --- a/backend/src/main/java/com/goldenchart/service/IndicatorService.java +++ b/backend/src/main/java/com/goldenchart/service/IndicatorService.java @@ -398,8 +398,9 @@ public class IndicatorService { int len = intP(p, "length", 13); int maLen = intP(p, "maLength", 10); String maType = p.getOrDefault("maType", "SMA").toString(); + Map norm = CciOnSourceIndicator.normalizeParams(p); - CciOnSourceIndicator cci = new CciOnSourceIndicator(src(s, p), len); + CciOnSourceIndicator cci = new CciOnSourceIndicator(src(s, norm), len); Indicator maInd = switch (maType) { case "EMA" -> new EMAIndicator(cci, maLen); diff --git a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java index 3eed22a..7d3b5ea 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java @@ -599,10 +599,10 @@ public class StrategyDslToTa4jAdapter { // CCI — 기간 접미사(CCI_VALUE_13) 또는 기본 CCI_VALUE if (field.startsWith("CCI_VALUE_")) { int period = parseTrailingInt(field, "CCI_VALUE_", intP(p, "length", 13)); - return new CciOnSourceIndicator(resolvePriceSource(s, p), period); + return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)), period); } if (field.equals("CCI_VALUE") || indType.equals("CCI")) - return new CciOnSourceIndicator(resolvePriceSource(s, p), + return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)), periodOverride > 0 ? periodOverride : intP(p, "length", 13)); // RSI — 기간 접미사(RSI_VALUE_9) 또는 기본 RSI_VALUE if (field.startsWith("RSI_VALUE_")) { diff --git a/frontend/src/App.css b/frontend/src/App.css index 39e242f..8fc5399 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -9929,7 +9929,54 @@ html.theme-blue { overflow: hidden; } -/* 매매 탭: 매수·매도 카드 분리 */ +/* 실시간 차트 우측 — 알림목록·가상매매와 동일 PaperSplitPanel 레이아웃 */ +.rsp-body.ptd-right-body { + padding: 0; + gap: 0; +} +.rsp-body .ptd-split-panel--right { + flex: 1; + min-height: 0; + height: 100%; + padding: 8px 10px 6px; + box-sizing: border-box; +} +.rsp-body .ptd-ob-stack--fill { + flex: 1; + min-height: 0; + height: 100%; +} +.rsp-body .ptd-split-panel--right .ptd-order-card .top-actions { + margin-top: auto; +} + +.rsp-tabs.bps-right-tabs { + display: flex; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + background: var(--bg3); + min-width: 420px; +} +.rsp-tabs.bps-right-tabs .bps-right-tab { + flex: 1; + padding: 10px 6px; + border: none; + background: transparent; + color: var(--text3); + font-size: 13px; + cursor: pointer; + transition: color 0.12s; +} +.rsp-tabs.bps-right-tabs .bps-right-tab:hover { + color: var(--text); +} +.rsp-tabs.bps-right-tabs .bps-right-tab--on { + color: var(--accent); + box-shadow: inset 0 -2px 0 var(--accent); + font-weight: 600; +} + +/* 매매 탭: 매수·매도 카드 분리 (레거시 rsp-trade-stack — 호가·백테스트 등에서 사용) */ .rsp-trade-stack { flex: 1; min-height: 0; diff --git a/frontend/src/components/RightSidePanel.tsx b/frontend/src/components/RightSidePanel.tsx index ef89bbe..fc7315e 100644 --- a/frontend/src/components/RightSidePanel.tsx +++ b/frontend/src/components/RightSidePanel.tsx @@ -6,6 +6,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { OrderbookPanel } from './OrderbookPanel'; import TradeOrderPanel from './TradeOrderPanel'; +import PaperSplitPanel from './paper/PaperSplitPanel'; import type { OrderbookDisplayUnit, WsStatus } from '../hooks/useUpbitOrderbook'; import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades'; import type { OrderbookTickerInfo } from './OrderbookPanel'; @@ -130,12 +131,12 @@ const RightSidePanel: React.FC = ({
-
+
{PANEL_TABS.map(tab => (
-
+
{activeTab === 'trade' && ( -
-
-
매수
-
+ = ({ onPaperOrderFilled={onPaperOrderFilled} />
-
- -
-
매도
-
+ )} + bottom={( +
= ({ onPaperOrderFilled={onPaperOrderFilled} />
-
-
+ )} + /> )} {activeTab === 'orderbook' && ( -
+
실시간 호가
diff --git a/frontend/src/utils/customIndicators.ts b/frontend/src/utils/customIndicators.ts index 5e8e419..78b1c4f 100644 --- a/frontend/src/utils/customIndicators.ts +++ b/frontend/src/utils/customIndicators.ts @@ -10,7 +10,11 @@ function sma(values: number[], period: number): number[] { return values.map((_, i) => { if (i < period - 1) return NaN; let sum = 0; - for (let j = i - period + 1; j <= i; j++) sum += values[j]; + for (let j = i - period + 1; j <= i; j++) { + const v = values[j]; + if (!Number.isFinite(v)) return NaN; + sum += v; + } return sum / period; }); } @@ -252,7 +256,7 @@ export function calcRSI( /** * CCI + 신호선 (업비트·TradingView) * CCI = (src - SMA(src,N)) / (0.015 × MeanDeviation) - * 신호선 = CCI의 SMA/EMA/WMA (기본 SMA 10) + * src 기본 hlc3(고저종 평균), 신호선 = CCI의 SMA/EMA/WMA (기본 SMA 10) */ export function calcCCI( bars: OHLCVBar[], @@ -262,7 +266,7 @@ export function calcCCI( const maLen = Math.max(1, Number(params.maLength ?? 10) || 10); const rawMa = String(params.maType ?? 'SMA'); const maType = rawMa === 'EMA' || rawMa === 'WMA' ? rawMa : (rawMa === 'None' ? 'None' : 'SMA'); - const srcKey = String(params.src ?? 'close'); + const srcKey = String(params.src ?? 'hlc3'); const prices = bars.map(b => sourceAt(b, srcKey)); const cci = prices.map((_, i) => { diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index 4dcb956..7bfcf72 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -295,7 +295,7 @@ export const INDICATOR_REGISTRY: IndicatorDef[] = [ { 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 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:'close', maType:'SMA', maLength:10}, plots:[{id:'plot0',title:'CCI',color:'#F23645',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#2962FF',type:'line',lineWidth:1}], hlines:[{price:100,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF50'}], description:'상품 채널 지수 (업비트: 종가·신호 10)' }, + { type:'CCI', name:'Commodity Channel Index', koreanName:'상품채널지수', shortName:'CCI', category:'Oscillators', overlay:false, defaultParams:{length:13, src:'hlc3', maType:'SMA', maLength:10}, plots:[{id:'plot0',title:'CCI',color:'#F23645',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#2962FF',type:'line',lineWidth:1}], hlines:[{price:100,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF50'}], description:'상품 채널 지수 (업비트·TradingView: hlc3·신호 SMA 10)' }, { 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 오실레이터' }, { type:'ChandeMO', name:'Chande Momentum Oscillator', koreanName:'챈드모멘텀오실레이터', shortName:'CMO', category:'Oscillators', overlay:false, defaultParams:{length:9, src:'close'}, plots:[{id:'plot0',title:'CMO',color:'#4DB6AC',type:'line',lineWidth:2}], hlines:[{price:50,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-50,color:'#4CAF50'}], description:'챈드 모멘텀 오실레이터' }, @@ -454,15 +454,19 @@ export function normalizeObvParams( return { ...params, maType, maLength }; } -/** 업비트 CCI: 기간 13·종가·신호 SMA 10 */ +/** 업비트·TradingView CCI: 기간 13·hlc3(고저종)·신호 SMA 10 */ export function normalizeCciParams( params: Record, ): Record { const rawMa = String(params.maType ?? 'SMA'); const maType = rawMa === 'EMA' || rawMa === 'WMA' || rawMa === 'None' ? rawMa : 'SMA'; - const src = String(params.src ?? 'close'); + let src = String(params.src ?? 'hlc3'); const length = Math.max(1, Number(params.length ?? 13) || 13); const maLength = Math.max(1, Number(params.maLength ?? 10) || 10); + // 레거시: 잘못 저장된 close 기본값 → 업비트 동일 hlc3 + if (src === 'close' && length === 13 && maLength === 10 && maType === 'SMA') { + src = 'hlc3'; + } return { ...params, src, maType, length, maLength }; } @@ -629,6 +633,9 @@ export async function calculateIndicator( let calcBars = bars; let calcParams = params; + if (type === 'CCI') { + calcParams = normalizeCciParams(params); + } if (type === 'IchimokuCloud') { const { senkou } = resolveIchimokuDisplacements(params); calcParams = { ...params, displacement: senkou };