819 lines
38 KiB
Java
819 lines
38 KiB
Java
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;
|
||
}
|
||
}
|