goldenChat base source add
This commit is contained in:
@@ -0,0 +1,818 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user