From ad92dace2892b0d733dfb710013f1dee286ad884 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sat, 30 May 2026 00:13:22 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=EC=86=8C=EC=88=98=EC=A0=90=20=EC=B6=94=EA=B0=80,=20cci=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CciOnSourceIndicator.java | 45 +++++++ .../goldenchart/service/IndicatorService.java | 4 +- .../service/StrategyDslToTa4jAdapter.java | 16 ++- frontend/src/App.tsx | 2 + frontend/src/components/ChartRightToolbar.tsx | 48 ++++++- frontend/src/components/ChartSlot.tsx | 2 + frontend/src/components/TradingChart.tsx | 8 ++ frontend/src/utils/ChartManager.ts | 25 +++- frontend/src/utils/customIndicators.ts | 124 ++++++++++++++++-- frontend/src/utils/dataGenerator.ts | 13 ++ frontend/src/utils/indicatorRegistry.ts | 19 ++- 11 files changed, 286 insertions(+), 20 deletions(-) create mode 100644 backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java diff --git a/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java b/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java new file mode 100644 index 0000000..f3061d1 --- /dev/null +++ b/backend/src/main/java/com/goldenchart/service/CciOnSourceIndicator.java @@ -0,0 +1,45 @@ +package com.goldenchart.service; + +import org.ta4j.core.BarSeries; +import org.ta4j.core.Indicator; +import org.ta4j.core.indicators.CachedIndicator; +import org.ta4j.core.indicators.averages.SMAIndicator; +import org.ta4j.core.indicators.statistics.MeanDeviationIndicator; +import org.ta4j.core.num.Num; + +/** + * TradingView/업비트 CCI — 임의 가격 소스에 대해 + * (src - SMA) / (0.015 × MeanDeviation) 계산. + */ +public class CciOnSourceIndicator extends CachedIndicator { + + private final Indicator price; + private final SMAIndicator sma; + private final MeanDeviationIndicator meanDeviation; + private final Num factor; + private final int barCount; + + public CciOnSourceIndicator(Indicator price, int barCount) { + super(price); + this.price = price; + this.barCount = barCount; + this.factor = getBarSeries().numFactory().numOf(0.015); + this.sma = new SMAIndicator(price, barCount); + this.meanDeviation = new MeanDeviationIndicator(price, barCount); + } + + @Override + protected Num calculate(int index) { + final Num meanDeviation = this.meanDeviation.getValue(index); + if (meanDeviation.isZero()) { + return getBarSeries().numFactory().zero(); + } + return price.getValue(index).minus(sma.getValue(index)) + .dividedBy(meanDeviation.multipliedBy(factor)); + } + + @Override + public int getCountOfUnstableBars() { + return barCount - 1; + } +} diff --git a/backend/src/main/java/com/goldenchart/service/IndicatorService.java b/backend/src/main/java/com/goldenchart/service/IndicatorService.java index cfe404c..66af4bc 100644 --- a/backend/src/main/java/com/goldenchart/service/IndicatorService.java +++ b/backend/src/main/java/com/goldenchart/service/IndicatorService.java @@ -396,10 +396,10 @@ public class IndicatorService { */ private Map> calcCCI(BarSeries s, Map p) { int len = intP(p, "length", 13); - int maLen = intP(p, "maLength", 20); + int maLen = intP(p, "maLength", 10); String maType = p.getOrDefault("maType", "SMA").toString(); - CCIIndicator cci = new CCIIndicator(s, len); + CciOnSourceIndicator cci = new CciOnSourceIndicator(src(s, p), 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 bf15593..3eed22a 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java @@ -599,10 +599,11 @@ 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 CCIIndicator(s, period); + return new CciOnSourceIndicator(resolvePriceSource(s, p), period); } if (field.equals("CCI_VALUE") || indType.equals("CCI")) - return new CCIIndicator(s, periodOverride > 0 ? periodOverride : intP(p, "length", 13)); + return new CciOnSourceIndicator(resolvePriceSource(s, p), + periodOverride > 0 ? periodOverride : intP(p, "length", 13)); // RSI — 기간 접미사(RSI_VALUE_9) 또는 기본 RSI_VALUE if (field.startsWith("RSI_VALUE_")) { int period = parseTrailingInt(field, "RSI_VALUE_", intP(p, "length", 14)); @@ -865,6 +866,17 @@ public class StrategyDslToTa4jAdapter { // ── TRIX 헬퍼 ───────────────────────────────────────────────────────────── + private Indicator resolvePriceSource(BarSeries s, Map p) { + return switch (p.getOrDefault("src", "close").toString()) { + case "open" -> new OpenPriceIndicator(s); + case "high" -> new HighPriceIndicator(s); + case "low" -> new LowPriceIndicator(s); + case "hl2" -> new MedianPriceIndicator(s); + case "hlc3" -> new TypicalPriceIndicator(s); + default -> new ClosePriceIndicator(s); + }; + } + private EMAIndicator tripleEma(ClosePriceIndicator close, int len) { return new EMAIndicator(new EMAIndicator(new EMAIndicator(close, len), len), len); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e85973e..2863567 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2357,6 +2357,8 @@ function App() { onDuplicateIndicator={handleDuplicateIndicator} focusedIndicatorId={focusedIndicatorId} onRestoreIndicators={handleRestoreIndicators} + onToggleIndicatorHidden={handleToggleIndicatorHidden} + onOpenIndicatorSettings={id => setSettingsModalId(id)} volumeVisible={chartVolumeVisible} paneSeparatorOptions={chartPaneSeparator} /> diff --git a/frontend/src/components/ChartRightToolbar.tsx b/frontend/src/components/ChartRightToolbar.tsx index 95dbc64..c687c5b 100644 --- a/frontend/src/components/ChartRightToolbar.tsx +++ b/frontend/src/components/ChartRightToolbar.tsx @@ -20,6 +20,10 @@ export interface ChartRightToolbarProps { onRestore?: () => void; focusedId?: string | null; onDragStart?: (id: string, e: React.PointerEvent) => void; + /** 지표명 툴바 — 표시/숨기기 */ + onToggleHidden?: (id: string) => void; + /** 지표명 툴바 — 설정 모달 */ + onSettings?: (id: string) => void; } const BTN_SIZE = 32; @@ -89,6 +93,25 @@ const IconMore = () => ( ); +const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? ( + + + + +) : ( + + + + +); + +const IconGear = () => ( + + + + +); + interface MenuItemDef { key: string; title: string; @@ -109,6 +132,8 @@ function buildMenuItems( onExpand?: (id: string) => void; onRestore?: () => void; onRemove?: (id: string) => void; + onToggleHidden?: (id: string) => void; + onSettings?: (id: string) => void; onDone?: () => void; }, ): MenuItemDef[] { @@ -119,9 +144,28 @@ function buildMenuItems( const isFocused = opts.focusedId === item.id; const showExpand = !!(opts.onExpand || (isFocused && opts.onRestore)); const done = opts.onDone ?? (() => {}); + const cfg = indicators.find(ind => ind.id === item.id); + const isHidden = cfg?.hidden === true; const items: MenuItemDef[] = []; + if (opts.onToggleHidden) { + items.push({ + key: 'visibility', + title: isHidden ? '표시' : '숨기기', + icon: