Files
goldenChart/backend/src/main/java/com/goldenchart/service/IndicatorService.java
T
2026-05-23 15:11:48 +09:00

819 lines
38 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.goldenchart.service;
import com.goldenchart.dto.IndicatorResponse;
import com.goldenchart.dto.IndicatorResponse.PlotPoint;
import com.goldenchart.dto.OhlcvBar;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.ta4j.core.*;
import org.ta4j.core.indicators.*;
import org.ta4j.core.indicators.adx.*;
import org.ta4j.core.indicators.aroon.*;
import org.ta4j.core.indicators.averages.*;
import org.ta4j.core.indicators.bollinger.*;
import org.ta4j.core.indicators.donchian.*;
import org.ta4j.core.indicators.helpers.*;
import org.ta4j.core.indicators.ichimoku.*;
import org.ta4j.core.indicators.keltner.*;
import org.ta4j.core.indicators.numeric.NumericIndicator;
import org.ta4j.core.indicators.numeric.UnaryOperationIndicator;
import org.ta4j.core.indicators.statistics.*;
import org.ta4j.core.indicators.volume.*;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.num.DoubleNumFactory;
import org.ta4j.core.num.Num;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.IntStream;
/**
* Ta4j 기술 지표 계산 서비스.
* frontend indicatorRegistry.ts 에 등록된 지표를 동일 로직으로 구현.
*/
@Service
@Slf4j
public class IndicatorService {
// ── Public API ────────────────────────────────────────────────────────────
public IndicatorResponse calculate(List<OhlcvBar> bars, String type, Map<String, Object> params, String timeframe) {
BarSeries series = buildSeries(bars, timeframe);
int n = series.getBarCount();
Map<String, Object> p = params != null ? params : Map.of();
Map<String, List<PlotPoint>> result = switch (type) {
// Moving Averages
case "SMA" -> calcSMA(series, p);
case "EMA" -> single("plot0", series, new EMAIndicator(src(series, p), intP(p, "length", 21)));
case "WMA" -> single("plot0", series, new WMAIndicator(src(series, p), intP(p, "length", 20)));
case "HMA" -> single("plot0", series, new HMAIndicator(src(series, p), intP(p, "length", 16)));
case "VWMA" -> single("plot0", series, new VWMAIndicator(new ClosePriceIndicator(series), intP(p, "length", 20)));
case "DEMA" -> single("plot0", series, new DoubleEMAIndicator(src(series, p), intP(p, "length", 21)));
case "TEMA" -> single("plot0", series, new TripleEMAIndicator(src(series, p), intP(p, "length", 21)));
case "RMA", "SMMA" -> single("plot0", series, new WildersMAIndicator(src(series, p), intP(p, "length", 14)));
case "LSMA" -> single("plot0", series, new LSMAIndicator(src(series, p), intP(p, "length", 25)));
case "MACross" -> calcMACross(series, p);
// Bands
case "BollingerBands" -> calcBB(series, p);
case "KeltnerChannels" -> calcKC(series, p);
case "DonchianChannels" -> calcDC(series, p);
case "BBPercentB" -> calcBBPercentB(series, p);
case "BBBandWidth" -> calcBBBandWidth(series, p);
// Oscillators
case "RSI" -> calcRSI(series, p);
case "Stochastic" -> calcStochastic(series, p);
case "StochRSI" -> calcStochRSI(series, p);
case "CCI" -> calcCCI(series, p);
case "WilliamsPercentRange" -> calcWilliamsR(series, p);
case "AwesomeOscillator" -> calcAO(series);
case "DPO" -> single("plot0", series, new DPOIndicator(src(series, p), intP(p, "length", 21)));
// Momentum
case "MACD" -> calcMACD(series, p);
case "Momentum" -> calcMomentumInd(series, p);
case "ROC" -> calcROC(series, p);
case "TSI" -> calcTSI(series, p);
case "TRIX" -> calcTRIX(series, p);
// Trend
case "ADX" -> calcADX(series, p);
case "DMI" -> calcDMI(series, p);
case "Aroon" -> calcAroon(series, p);
case "IchimokuCloud"-> calcIchimoku(series, p);
case "ParabolicSAR" -> calcParabolicSAR(series, p);
case "Choppiness" -> calcChoppiness(series, p);
case "MassIndex" -> calcMassIndex(series, p);
// Volatility
case "ATR" -> single("plot0", series, new ATRIndicator(series, intP(p, "length", 14)));
case "StandardDeviation"-> single("plot0", series, new StandardDeviationIndicator(src(series, p), intP(p, "length", 20)));
case "HistoricalVolatility" -> calcHV(series, p);
// Volume
case "OBV" -> calcOBV(series, p);
case "MFI" -> single("plot0", series, new MoneyFlowIndexIndicator(series, intP(p, "length", 14)));
case "ChaikinMF"-> single("plot0", series, new ChaikinMoneyFlowIndicator(series, intP(p, "length", 20)));
case "VolumeOscillator" -> calcVolumeOscillator(series, p);
case "VR" -> calcVR(series, p);
case "Disparity" -> calcDisparity(series, p);
case "Psychological", "NewPsychological" -> calcPsychological(series, p);
case "InvestPsychological"-> calcInvestPsychological(series, p);
default -> throw new IllegalArgumentException("지원하지 않는 지표: " + type);
};
List<Long> times = IntStream.range(0, n)
.mapToObj(i -> {
Bar bar = series.getBar(i);
return bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
})
.toList();
return IndicatorResponse.builder()
.indicatorType(type)
.times(times)
.values(result)
.build();
}
// ── Series Builder ────────────────────────────────────────────────────────
/**
* OhlcvBar 목록으로 Ta4j BarSeries 를 생성한다.
*
* @param bars OHLCV 바 데이터
* @param timeframe 타임프레임 문자열 (1m/5m/15m/30m/1h/4h/1D/1W/1M).
* null 또는 알 수 없는 값이면 1분으로 fallback.
*/
private BarSeries buildSeries(List<OhlcvBar> bars, String timeframe) {
BarSeries series = new BaseBarSeriesBuilder()
.withNumFactory(DoubleNumFactory.getInstance())
.build();
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
Duration period = timeframeToDuration(timeframe);
for (OhlcvBar b : bars) {
// b.getTime() 은 봉 시작 시각(Unix 초).
// ta4j Bar 의 endTime 은 봉 종료 시각이므로 duration 을 더해야 한다.
java.time.Instant endInst = java.time.Instant.ofEpochSecond(b.getTime()).plus(period);
factory.createBarBuilder(series)
.timePeriod(period)
.endTime(endInst)
.openPrice(b.getOpen())
.highPrice(b.getHigh())
.lowPrice(b.getLow())
.closePrice(b.getClose())
.volume(b.getVolume())
.add();
}
return series;
}
/**
* 타임프레임 문자열을 Duration 으로 변환.
* frontend Timeframe 타입 (1m/5m/15m/30m/1h/4h/1D/1W/1M)과 1:1 대응.
*/
private static Duration timeframeToDuration(String tf) {
if (tf == null) return Duration.ofMinutes(1);
return switch (tf) {
case "1m" -> Duration.ofMinutes(1);
case "3m" -> Duration.ofMinutes(3);
case "5m" -> Duration.ofMinutes(5);
case "15m" -> Duration.ofMinutes(15);
case "30m" -> Duration.ofMinutes(30);
case "1h" -> Duration.ofHours(1);
case "4h" -> Duration.ofHours(4);
case "1D" -> Duration.ofDays(1);
case "1W" -> Duration.ofDays(7);
case "1M" -> Duration.ofDays(30);
default -> Duration.ofMinutes(1);
};
}
// ── Helpers ───────────────────────────────────────────────────────────────
private int intP(Map<String, Object> p, String k, int def) {
Object v = p.get(k); return v == null ? def : ((Number) v).intValue();
}
private double dblP(Map<String, Object> p, String k, double def) {
Object v = p.get(k); return v == null ? def : ((Number) v).doubleValue();
}
private Indicator<Num> src(BarSeries s, Map<String, Object> 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 Double safe(Num n) {
if (n == null) return null;
double v = n.doubleValue();
return Double.isNaN(v) || Double.isInfinite(v) ? null : v;
}
private List<PlotPoint> toPlot(BarSeries s, Indicator<Num> ind) {
int n = s.getBarCount();
List<PlotPoint> pts = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
// 프론트 차트의 time 축은 봉 시작 시각 기준.
// endTime - timePeriod = 봉 시작 시각
long startEpoch = bar.getEndTime().getEpochSecond()
- bar.getTimePeriod().getSeconds();
pts.add(PlotPoint.builder()
.time(startEpoch)
.value(safe(ind.getValue(i)))
.build());
}
return pts;
}
private Map<String, List<PlotPoint>> single(String id, BarSeries s, Indicator<Num> ind) {
return Map.of(id, toPlot(s, ind));
}
// ── Moving Averages ───────────────────────────────────────────────────────
/** MA1~MA11 기본 기간: 5, 12, 16, 20, 26, 34, 50, 60, 72, 90, 120 */
private static final int[] SMA_DEFAULT_PERIODS = { 5, 12, 16, 20, 26, 34, 50, 60, 72, 90, 120 };
private Map<String, List<PlotPoint>> calcSMA(BarSeries s, Map<String, Object> p) {
Indicator<Num> source = src(s, p);
Map<String, List<PlotPoint>> result = new LinkedHashMap<>();
for (int i = 0; i < SMA_DEFAULT_PERIODS.length; i++) {
String periodKey = "period" + (i + 1);
int period = intP(p, periodKey, SMA_DEFAULT_PERIODS[i]);
// 레거시 단일 len → MA1
if (i == 0 && p.containsKey("len")) {
period = intP(p, "len", SMA_DEFAULT_PERIODS[0]);
}
period = Math.max(1, period);
result.put("plot" + i, toPlot(s, new SMAIndicator(source, period)));
}
return result;
}
private Map<String, List<PlotPoint>> calcMACross(BarSeries s, Map<String, Object> p) {
Indicator<Num> src = src(s, p);
return Map.of(
"plot0", toPlot(s, new EMAIndicator(src, intP(p, "fastLength", 9))),
"plot1", toPlot(s, new EMAIndicator(src, intP(p, "slowLength", 21)))
);
}
// ── Bands ─────────────────────────────────────────────────────────────────
private Map<String, List<PlotPoint>> calcBB(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 20);
double mult = dblP(p, "mult", 2.0);
int offset = intP(p, "offset", 0);
// 업비트 BB: 종가 SMA + 모집단 표준편차 × 곱
ClosePriceIndicator close = new ClosePriceIndicator(s);
SMAIndicator basis = new SMAIndicator(close, len);
StandardDeviationIndicator std = StandardDeviationIndicator.ofPopulation(close, len);
BollingerBandsMiddleIndicator mid = new BollingerBandsMiddleIndicator(basis);
Num k = s.numFactory().numOf(mult);
BollingerBandsUpperIndicator upper = new BollingerBandsUpperIndicator(mid, std, k);
BollingerBandsLowerIndicator lower = new BollingerBandsLowerIndicator(mid, std, k);
return Map.of(
"plot0", shiftPlotOffset(toPlot(s, mid), offset),
"plot1", shiftPlotOffset(toPlot(s, upper), offset),
"plot2", shiftPlotOffset(toPlot(s, lower), offset)
);
}
/** TradingView/업비트 오프셋: 양수 = 플롯을 시간축 오른쪽으로 이동 */
private List<PlotPoint> shiftPlotOffset(List<PlotPoint> pts, int offset) {
if (offset == 0) return pts;
int n = pts.size();
List<PlotPoint> out = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
int src = i - offset;
Double val = (src >= 0 && src < n) ? pts.get(src).getValue() : null;
out.add(PlotPoint.builder().time(pts.get(i).getTime()).value(val).build());
}
return out;
}
private Indicator<Num> maOf(Indicator<Num> source, String maType, int len, BarSeries s) {
return switch (maType) {
case "EMA" -> new EMAIndicator(source, len);
case "WMA" -> new WMAIndicator(source, len);
case "SMMA (RMA)", "RMA" -> new WildersMAIndicator(source, len);
case "VWMA" -> new VWMAIndicator(source, len);
default -> new SMAIndicator(source, len);
};
}
private Map<String, List<PlotPoint>> calcKC(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 20);
double mult = dblP(p, "multiplier", 2.0);
EMAIndicator ema = new EMAIndicator(new ClosePriceIndicator(s), len);
KeltnerChannelMiddleIndicator mid = new KeltnerChannelMiddleIndicator(s, len);
ATRIndicator atr = new ATRIndicator(s, len);
KeltnerChannelUpperIndicator upper = new KeltnerChannelUpperIndicator(mid, atr, mult);
KeltnerChannelLowerIndicator lower = new KeltnerChannelLowerIndicator(mid, atr, (double) mult);
return Map.of(
"plot0", toPlot(s, upper),
"plot1", toPlot(s, ema),
"plot2", toPlot(s, lower)
);
}
private Map<String, List<PlotPoint>> calcDC(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 20);
DonchianChannelUpperIndicator upper = new DonchianChannelUpperIndicator(s, len);
DonchianChannelLowerIndicator lower = new DonchianChannelLowerIndicator(s, len);
// basis = (upper + lower) / 2
NumericIndicator basis = NumericIndicator.of(upper).plus(lower).dividedBy(2);
return Map.of(
"plot0", toPlot(s, upper),
"plot1", toPlot(s, basis),
"plot2", toPlot(s, lower)
);
}
private Map<String, List<PlotPoint>> calcBBPercentB(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 20);
double mult = dblP(p, "mult", 2.0);
Indicator<Num> src = src(s, p);
SMAIndicator sma = new SMAIndicator(src, len);
StandardDeviationIndicator std = StandardDeviationIndicator.ofSample(src, len);
BollingerBandsMiddleIndicator mid = new BollingerBandsMiddleIndicator(sma);
Num k = s.numFactory().numOf(mult);
BollingerBandsUpperIndicator upper = new BollingerBandsUpperIndicator(mid, std, k);
BollingerBandsLowerIndicator lower = new BollingerBandsLowerIndicator(mid, std, k);
// %B = (price - lower) / (upper - lower)
NumericIndicator pctB = NumericIndicator.of(src)
.minus(lower)
.dividedBy(NumericIndicator.of(upper).minus(lower));
return single("plot0", s, pctB);
}
private Map<String, List<PlotPoint>> calcBBBandWidth(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 20);
double mult = dblP(p, "mult", 2.0);
Indicator<Num> src = src(s, p);
SMAIndicator sma = new SMAIndicator(src, len);
StandardDeviationIndicator std = StandardDeviationIndicator.ofSample(src, len);
BollingerBandsMiddleIndicator mid = new BollingerBandsMiddleIndicator(sma);
Num k = s.numFactory().numOf(mult);
BollingerBandsUpperIndicator upper = new BollingerBandsUpperIndicator(mid, std, k);
BollingerBandsLowerIndicator lower = new BollingerBandsLowerIndicator(mid, std, k);
BollingerBandWidthIndicator bw = new BollingerBandWidthIndicator(upper, mid, lower);
return single("plot0", s, bw);
}
// ── Oscillators ───────────────────────────────────────────────────────────
/**
* RSI + 선택적 신호선 (SMA / EMA / WMA).
* maType == "None" 이면 plot1 을 null 로 채워 비활성 처리.
*/
private Map<String, List<PlotPoint>> calcRSI(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 14);
int maLen = intP(p, "maLength", 14);
String maType = p.getOrDefault("maType", "SMA").toString();
RSIIndicator rsi = new RSIIndicator(src(s, p), len);
Indicator<Num> maInd = switch (maType) {
case "EMA" -> new EMAIndicator(rsi, maLen);
case "WMA" -> new WMAIndicator(rsi, maLen);
case "None" -> null;
default -> new SMAIndicator(rsi, maLen);
};
if (maInd == null) {
int n = s.getBarCount();
List<PlotPoint> empty = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
empty.add(PlotPoint.builder().time(t).value(null).build());
}
return Map.of("plot0", toPlot(s, rsi), "plot1", empty);
}
return Map.of("plot0", toPlot(s, rsi), "plot1", toPlot(s, maInd));
}
/**
* CCI + 선택적 MA 오버레이 (업비트 동일: SMA / EMA / WMA).
* maType == "None" 이면 MA 라인(plot1) 값을 null 로 채워 빈 시리즈로 반환.
*/
private Map<String, List<PlotPoint>> calcCCI(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 13);
int maLen = intP(p, "maLength", 20);
String maType = p.getOrDefault("maType", "SMA").toString();
CCIIndicator cci = new CCIIndicator(s, len);
Indicator<Num> maInd = switch (maType) {
case "EMA" -> new EMAIndicator(cci, maLen);
case "WMA" -> new WMAIndicator(cci, maLen);
case "None" -> null;
default -> new SMAIndicator(cci, maLen); // SMA
};
if (maInd == null) {
// MA 비활성 — plot1 을 null 값으로 채워 반환
int n = s.getBarCount();
List<PlotPoint> empty = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
empty.add(PlotPoint.builder().time(t).value(null).build());
}
return Map.of("plot0", toPlot(s, cci), "plot1", empty);
}
return Map.of("plot0", toPlot(s, cci), "plot1", toPlot(s, maInd));
}
private Map<String, List<PlotPoint>> calcStochastic(BarSeries s, Map<String, Object> p) {
int kLen = intP(p, "kLength", 14);
int smooth = intP(p, "smooth", 3);
int dSmooth = intP(p, "dSmoothing", 3);
StochasticOscillatorKIndicator k = new StochasticOscillatorKIndicator(s, kLen);
SMAIndicator kSmooth = new SMAIndicator(k, smooth);
SMAIndicator d = new SMAIndicator(kSmooth, dSmooth);
return Map.of("plot0", toPlot(s, kSmooth), "plot1", toPlot(s, d));
}
private Map<String, List<PlotPoint>> calcStochRSI(BarSeries s, Map<String, Object> p) {
int rsiLen = intP(p, "lengthRSI", 14);
int stochLen = intP(p, "lengthStoch", 14);
int smoothK = intP(p, "smoothK", 3);
int smoothD = intP(p, "smoothD", 3);
RSIIndicator rsi = new RSIIndicator(src(s, p), rsiLen);
StochasticRSIIndicator stochRsi = new StochasticRSIIndicator(rsi, stochLen);
SMAIndicator k = new SMAIndicator(stochRsi, smoothK);
SMAIndicator d = new SMAIndicator(k, smoothD);
return Map.of("plot0", toPlot(s, k), "plot1", toPlot(s, d));
}
private Map<String, List<PlotPoint>> calcAO(BarSeries s) {
// AO = SMA(median,5) - SMA(median,34)
MedianPriceIndicator median = new MedianPriceIndicator(s);
NumericIndicator ao = NumericIndicator.of(new SMAIndicator(median, 5))
.minus(new SMAIndicator(median, 34));
return single("plot0", s, ao);
}
// ── Momentum ──────────────────────────────────────────────────────────────
private Map<String, List<PlotPoint>> calcMACD(BarSeries s, Map<String, Object> p) {
int fast = intP(p, "fastLength", 12);
int slow = intP(p, "slowLength", 26);
int signal = intP(p, "signalLength", 9);
Indicator<Num> src = src(s, p);
MACDIndicator macd = new MACDIndicator(src, fast, slow);
EMAIndicator sigLine = new EMAIndicator(macd, signal);
NumericIndicator histogram = NumericIndicator.of(macd).minus(sigLine);
return Map.of(
"plot0", toPlot(s, histogram),
"plot1", toPlot(s, macd),
"plot2", toPlot(s, sigLine)
);
}
private Map<String, List<PlotPoint>> calcMomentumInd(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 10);
return single("plot0", s, new ROCIndicator(src(s, p), len));
}
private Map<String, List<PlotPoint>> calcROC(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 9);
NumericIndicator rocPct = NumericIndicator.of(new ROCIndicator(src(s, p), len))
.multipliedBy(100);
return single("plot0", s, rocPct);
}
/**
* TSI (True Strength Index) — 수동 구현 (Ta4j에 없음).
* TSI = 100 * EMA(EMA(diff, longLen), shortLen) / EMA(EMA(absDiff, longLen), shortLen)
*/
private Map<String, List<PlotPoint>> calcTSI(BarSeries s, Map<String, Object> p) {
int longLen = intP(p, "longLength", 25);
int shortLen = intP(p, "shortLength", 13);
int sigLen = intP(p, "signalLength", 13);
int n = s.getBarCount();
ClosePriceIndicator close = new ClosePriceIndicator(s);
// momentum = close - close[1]
List<PlotPoint> tsiPts = new ArrayList<>(n);
List<PlotPoint> sigPts = new ArrayList<>(n);
double[] diff = new double[n];
double[] absDiff = new double[n];
for (int i = 0; i < n; i++) {
if (i == 0) { diff[i] = 0; absDiff[i] = 0; }
else {
Double c1 = safe(close.getValue(i));
Double c0 = safe(close.getValue(i - 1));
diff[i] = (c1 != null && c0 != null) ? c1 - c0 : 0;
absDiff[i] = Math.abs(diff[i]);
}
}
double[] ema1d = ema(diff, longLen, n);
double[] ema2d = ema(ema1d, shortLen, n);
double[] ema1a = ema(absDiff, longLen, n);
double[] ema2a = ema(ema1a, shortLen, n);
double[] tsi = new double[n];
for (int i = 0; i < n; i++) {
tsi[i] = ema2a[i] == 0 ? 0 : 100.0 * ema2d[i] / ema2a[i];
}
double[] sig = ema(tsi, sigLen, n);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
tsiPts.add(new PlotPoint(t, tsi[i], null));
sigPts.add(new PlotPoint(t, sig[i], null));
}
return Map.of("plot0", tsiPts, "plot1", sigPts);
}
// ── Trend ─────────────────────────────────────────────────────────────────
private Map<String, List<PlotPoint>> calcADX(BarSeries s, Map<String, Object> p) {
int adxSmoothing = intP(p, "adxSmoothing", 14);
int diLen = intP(p, "diLength", 14);
return Map.of(
"plot0", toPlot(s, new ADXIndicator(s, diLen, adxSmoothing)),
"plot1", toPlot(s, new PlusDIIndicator(s, diLen)),
"plot2", toPlot(s, new MinusDIIndicator(s, diLen))
);
}
private Map<String, List<PlotPoint>> calcDMI(BarSeries s, Map<String, Object> p) {
int diLen = intP(p, "diLength", intP(p, "length", 14));
int adxSmooth = intP(p, "adxSmoothing", 14);
PlusDIIndicator plusDI = new PlusDIIndicator(s, diLen);
MinusDIIndicator minusDI = new MinusDIIndicator(s, diLen);
ADXIndicator adx = new ADXIndicator(s, diLen, adxSmooth);
NumericIndicator dx = NumericIndicator.of(plusDI).minus(minusDI).abs()
.dividedBy(NumericIndicator.of(plusDI).plus(minusDI))
.multipliedBy(100);
List<PlotPoint> adxPlot = toPlot(s, adx);
return Map.of(
"plot0", toPlot(s, plusDI),
"plot1", toPlot(s, minusDI),
"plot2", toPlot(s, dx),
"plot3", adxPlot,
"plot4", calcAdxrFromAdx(adxPlot, adxSmooth)
);
}
/** ADXR = (ADX + ADX[n]) / 2 — Wilder·업비트 */
private List<PlotPoint> calcAdxrFromAdx(List<PlotPoint> adxPlot, int lag) {
List<PlotPoint> out = new ArrayList<>(adxPlot.size());
for (int i = 0; i < adxPlot.size(); i++) {
PlotPoint cur = adxPlot.get(i);
if (i < lag) {
out.add(PlotPoint.builder().time(cur.getTime()).value(Double.NaN).build());
continue;
}
Double a = cur.getValue();
Double b = adxPlot.get(i - lag).getValue();
Double v = (a != null && b != null && !a.isNaN() && !b.isNaN()) ? (a + b) / 2.0 : Double.NaN;
out.add(PlotPoint.builder().time(cur.getTime()).value(v).build());
}
return out;
}
private Map<String, List<PlotPoint>> calcAroon(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 14);
return Map.of(
"plot0", toPlot(s, new AroonUpIndicator(s, len)),
"plot1", toPlot(s, new AroonDownIndicator(s, len))
);
}
private Map<String, List<PlotPoint>> calcIchimoku(BarSeries s, Map<String, Object> p) {
int conv = intP(p, "conversionPeriods", 9);
int base = intP(p, "basePeriods", 26);
int span2 = intP(p, "laggingSpan2Periods", 52);
int disp = intP(p, "displacement", 26);
IchimokuTenkanSenIndicator tenkan = new IchimokuTenkanSenIndicator(s, conv);
IchimokuKijunSenIndicator kijun = new IchimokuKijunSenIndicator(s, base);
IchimokuSenkouSpanAIndicator spanA = new IchimokuSenkouSpanAIndicator(s, tenkan, kijun, disp);
// 선행스팬B displacement 도 spanA·chikou 와 동일한 disp 를 적용해야 한다
IchimokuSenkouSpanBIndicator spanB = new IchimokuSenkouSpanBIndicator(s, span2, disp);
IchimokuChikouSpanIndicator chikou = new IchimokuChikouSpanIndicator(s, disp);
return Map.of(
"plot0", toPlot(s, tenkan),
"plot1", toPlot(s, kijun),
"plot2", toPlot(s, spanA),
"plot3", toPlot(s, spanB),
"plot4", toPlot(s, chikou)
);
}
private Map<String, List<PlotPoint>> calcParabolicSAR(BarSeries s, Map<String, Object> p) {
double start = dblP(p, "start", 0.02);
double inc = dblP(p, "increment", 0.02);
double max = dblP(p, "maximum", 0.2);
return single("plot0", s, new ParabolicSarIndicator(s,
s.numFactory().numOf(start), s.numFactory().numOf(max), s.numFactory().numOf(inc)));
}
private Map<String, List<PlotPoint>> calcChoppiness(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 14);
// Ta4j ChopIndicator(BarSeries, ciTimeFrame, scaleTo=100)
ChopIndicator chop = new ChopIndicator(s, len, 100);
return single("plot0", s, chop);
}
private Map<String, List<PlotPoint>> calcMassIndex(BarSeries s, Map<String, Object> p) {
int emaLen = intP(p, "length2", 9); // inner EMA
int total = intP(p, "length", 25); // total period
return single("plot0", s, new MassIndexIndicator(s, emaLen, total));
}
// ── Volatility ────────────────────────────────────────────────────────────
private Map<String, List<PlotPoint>> calcHV(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 20);
int annualBars = intP(p, "annualBars", 252);
int n = s.getBarCount();
ClosePriceIndicator close = new ClosePriceIndicator(s);
List<PlotPoint> pts = new ArrayList<>(n);
double[] logRet = new double[n];
for (int i = 0; i < n; i++) {
if (i == 0) { logRet[i] = 0; continue; }
Double c0 = safe(close.getValue(i - 1));
Double c1 = safe(close.getValue(i));
logRet[i] = (c0 != null && c0 > 0 && c1 != null) ? Math.log(c1 / c0) : 0;
}
double factor = Math.sqrt(annualBars) * 100;
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
if (i < len) { pts.add(new PlotPoint(t, null, null)); continue; }
double mean = 0;
for (int j = i - len + 1; j <= i; j++) mean += logRet[j];
mean /= len;
double var = 0;
for (int j = i - len + 1; j <= i; j++) var += Math.pow(logRet[j] - mean, 2);
double hv = Math.sqrt(var / (len - 1)) * factor;
pts.add(new PlotPoint(t, hv, null));
}
return Map.of("plot0", pts);
}
// ── Williams %R / OBV / TRIX / Volume Osc / VR / 이격도 / 심리도 ─────────
private Map<String, List<PlotPoint>> calcWilliamsR(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 14);
// ta4j WilliamsR는 시고저 종가 기준 — 업비트와 동일
return single("plot0", s, new WilliamsRIndicator(s, len));
}
/** 업비트 OBV: OBV + 신호선(SMA/EMA/WMA, 기본 SMA 9) */
private Map<String, List<PlotPoint>> calcOBV(BarSeries s, Map<String, Object> p) {
OnBalanceVolumeIndicator obv = new OnBalanceVolumeIndicator(s);
String maType = p.getOrDefault("maType", "SMA").toString();
int maLen = intP(p, "maLength", 9);
Indicator<Num> maInd = switch (maType) {
case "EMA" -> new EMAIndicator(obv, maLen);
case "WMA" -> new WMAIndicator(obv, maLen);
default -> new SMAIndicator(obv, maLen);
};
return Map.of("plot0", toPlot(s, obv), "plot1", toPlot(s, maInd));
}
/** 업비트 TRIX: ln(종가) → 3×EMA → 1봉 차분 × 10000, 신호선 = TRIX EMA */
private Map<String, List<PlotPoint>> calcTRIX(BarSeries s, Map<String, Object> p) {
int len = intP(p, "length", 12);
int sigLen = intP(p, "signalLength", 9);
Indicator<Num> logClose = UnaryOperationIndicator.log(new ClosePriceIndicator(s));
EMAIndicator e3 = new EMAIndicator(new EMAIndicator(new EMAIndicator(logClose, len), len), len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
NumericIndicator trix = NumericIndicator.of(e3).minus(prev3).multipliedBy(10_000);
EMAIndicator signal = new EMAIndicator(trix, sigLen);
return Map.of("plot0", toPlot(s, trix), "plot1", toPlot(s, signal));
}
private Map<String, List<PlotPoint>> calcVolumeOscillator(BarSeries s, Map<String, Object> p) {
int shortLen = intP(p, "shortLength", 5);
int longLen = intP(p, "longLength", 10);
VolumeIndicator vol = new VolumeIndicator(s, 1);
EMAIndicator shortEma = new EMAIndicator(vol, shortLen);
EMAIndicator longEma = new EMAIndicator(vol, longLen);
NumericIndicator osc = NumericIndicator.of(shortEma).minus(longEma)
.dividedBy(longEma).multipliedBy(100);
return single("plot0", s, osc);
}
private Map<String, List<PlotPoint>> calcVR(BarSeries s, Map<String, Object> p) {
int period = intP(p, "length", 10);
int n = s.getBarCount();
List<PlotPoint> pts = new ArrayList<>(n);
ClosePriceIndicator close = new ClosePriceIndicator(s);
VolumeIndicator vol = new VolumeIndicator(s, 1);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
if (i < period) {
pts.add(PlotPoint.builder().time(t).value(null).build());
continue;
}
double upVol = 0, downVol = 0;
for (int j = i - period + 1; j <= i; j++) {
Double c = safe(close.getValue(j));
Double cPrev = safe(close.getValue(j - 1));
Double v = safe(vol.getValue(j));
if (c == null || cPrev == null || v == null) continue;
if (c > cPrev) upVol += v;
else if (c < cPrev) downVol += v;
}
Double val = downVol == 0 ? null : (upVol / downVol) * 100.0;
pts.add(PlotPoint.builder().time(t).value(val).build());
}
return Map.of("plot0", pts);
}
/** 업비트 이격도: 종가 ÷ SMA × 100 */
private Map<String, List<PlotPoint>> calcDisparity(BarSeries s, Map<String, Object> p) {
Indicator<Num> source = src(s, p);
Map<String, List<PlotPoint>> result = new LinkedHashMap<>();
result.put("plot0", disparityLine(s, source, intP(p, "ultraLength", 5)));
result.put("plot1", disparityLine(s, source, intP(p, "shortLength", 10)));
result.put("plot2", disparityLine(s, source, intP(p, "midLength", 20)));
result.put("plot3", disparityLine(s, source, intP(p, "longLength", 60)));
return result;
}
private List<PlotPoint> disparityLine(BarSeries s, Indicator<Num> source, int period) {
SMAIndicator sma = new SMAIndicator(source, period);
int n = s.getBarCount();
List<PlotPoint> pts = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
Double c = safe(source.getValue(i));
Double m = safe(sma.getValue(i));
if (c == null || m == null || m == 0) {
pts.add(PlotPoint.builder().time(t).value(null).build());
} else {
pts.add(PlotPoint.builder().time(t).value(c / m * 100.0).build());
}
}
return pts;
}
private Map<String, List<PlotPoint>> calcPsychological(BarSeries s, Map<String, Object> p) {
int period = intP(p, "length", 12);
return Map.of("plot0", psychologicalPoints(s, period, false));
}
private Map<String, List<PlotPoint>> calcInvestPsychological(BarSeries s, Map<String, Object> p) {
int period = intP(p, "length", 10);
return Map.of("plot0", psychologicalPoints(s, period, true));
}
private List<PlotPoint> psychologicalPoints(BarSeries s, int period, boolean volumeWeighted) {
int n = s.getBarCount();
ClosePriceIndicator close = new ClosePriceIndicator(s);
VolumeIndicator vol = new VolumeIndicator(s, 1);
List<PlotPoint> pts = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Bar bar = s.getBar(i);
long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
if (i < period) {
pts.add(PlotPoint.builder().time(t).value(null).build());
continue;
}
double val;
if (!volumeWeighted) {
int up = 0;
for (int j = i - period + 1; j <= i; j++) {
Double c = safe(close.getValue(j));
Double cPrev = safe(close.getValue(j - 1));
if (c != null && cPrev != null && c > cPrev) up++;
}
val = (double) up / period * 100.0;
} else {
double upVol = 0, total = 0;
for (int j = i - period + 1; j <= i; j++) {
Double c = safe(close.getValue(j));
Double cPrev = safe(close.getValue(j - 1));
Double v = safe(vol.getValue(j));
if (v == null) continue;
total += v;
if (c != null && cPrev != null && c > cPrev) upVol += v;
}
val = total == 0 ? Double.NaN : upVol / total * 100.0;
}
pts.add(PlotPoint.builder().time(t).value(Double.isNaN(val) ? null : val).build());
}
return pts;
}
// ── EMA helper (double[], for TSI manual calc) ────────────────────────────
private double[] ema(double[] src, int len, int n) {
double[] out = new double[n];
double k = 2.0 / (len + 1);
out[0] = src[0];
for (int i = 1; i < n; i++) {
out[i] = src[i] * k + out[i - 1] * (1 - k);
}
return out;
}
}