goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,601 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.ta4j.core.BarSeries;
import org.ta4j.core.Indicator;
import org.ta4j.core.Rule;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.indicators.*;
import org.ta4j.core.indicators.adx.*;
import org.ta4j.core.indicators.averages.*;
import org.ta4j.core.indicators.bollinger.*;
import org.ta4j.core.indicators.helpers.*;
import org.ta4j.core.indicators.ichimoku.*;
import org.ta4j.core.indicators.statistics.StandardDeviationIndicator;
import org.ta4j.core.indicators.CachedIndicator;
import org.ta4j.core.indicators.helpers.VolumeIndicator;
import org.ta4j.core.indicators.volume.OnBalanceVolumeIndicator;
import org.ta4j.core.indicators.numeric.NumericIndicator;
import org.ta4j.core.indicators.numeric.UnaryOperationIndicator;
import org.ta4j.core.num.Num;
import org.ta4j.core.rules.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* frontend DSL(LogicNode 트리) → Ta4j Rule 변환 어댑터.
*
* <h3>DSL 타입 → 레지스트리(DB) 키 매핑</h3>
* <p>frontend strategyTypes.ts 의 PaletteItem.value (DSL indicatorType) 는
* 대문자 스네이크케이스이지만, indicatorRegistry.ts 의 type 키(=DB 저장 키)는
* PascalCase 이므로 반드시 변환 후 DB 파라미터를 조회해야 한다.</p>
*
* <h3>DSL 구조</h3>
* <pre>
* {
* "type": "AND" | "OR" | "NOT" | "CONDITION",
* "children": [ ...LogicNode ], // AND/OR/NOT 인 경우
* "condition": { // CONDITION 인 경우
* "indicatorType": "CCI",
* "conditionType": "CROSS_UP",
* "leftField": "CCI_VALUE",
* "rightField": "OVERBOUGHT_100",
* "targetValue": null,
* "slopePeriod": 3,
* "holdDays": 3,
* "candleRange": 1
* }
* }
* </pre>
*/
@Component
@Slf4j
public class StrategyDslToTa4jAdapter {
/**
* DSL indicatorType(대문자 스네이크) → indicatorRegistry type(PascalCase, DB 키).
* frontend strategyTypes.ts DSL_TO_REGISTRY 와 동기화.
*/
private static final Map<String, String> DSL_TO_REGISTRY = Map.ofEntries(
Map.entry("RSI", "RSI"),
Map.entry("MACD", "MACD"),
Map.entry("CCI", "CCI"),
Map.entry("ADX", "ADX"),
Map.entry("DMI", "DMI"),
Map.entry("OBV", "OBV"),
Map.entry("TRIX", "TRIX"),
Map.entry("EMA", "EMA"),
Map.entry("MA", "SMA"),
Map.entry("BOLLINGER", "BollingerBands"),
Map.entry("STOCHASTIC", "Stochastic"),
Map.entry("WILLIAMS_R", "WilliamsPercentRange"),
Map.entry("ICHIMOKU", "IchimokuCloud"),
Map.entry("VOLUME_OSC", "VolumeOscillator"),
Map.entry("PSYCHOLOGICAL", "Psychological"),
Map.entry("NEW_PSYCHOLOGICAL", "Psychological"),
Map.entry("INVEST_PSYCHOLOGICAL", "InvestPsychological"),
Map.entry("BWI", "BBBandWidth"),
Map.entry("VR", "VR"),
Map.entry("DISPARITY", "Disparity"),
Map.entry("VOLUME", "VOLUME")
);
/**
* DSL indicatorType 을 DB 저장 키(레지스트리 type)로 변환.
* 매핑 없으면 원본 반환 (이미 레지스트리 형식인 경우).
*/
private static String toRegistryKey(String dslType) {
return DSL_TO_REGISTRY.getOrDefault(dslType, dslType);
}
// ── Public API ────────────────────────────────────────────────────────────
public Rule toRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> indicatorParams) {
if (node == null || node.isNull()) return new BooleanRule(false);
String type = node.path("type").asText("CONDITION");
return switch (type) {
case "AND" -> buildAndRule(node, series, indicatorParams);
case "OR" -> buildOrRule(node, series, indicatorParams);
case "NOT" -> buildNotRule(node, series, indicatorParams);
default -> buildConditionRule(node.path("condition"), series, indicatorParams);
};
}
// ── AND / OR / NOT ────────────────────────────────────────────────────────
private Rule buildAndRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> rules = childRules(node, series, p);
if (rules.isEmpty()) return new BooleanRule(false);
Rule result = rules.get(0);
for (int i = 1; i < rules.size(); i++) result = new AndRule(result, rules.get(i));
return result;
}
private Rule buildOrRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> rules = childRules(node, series, p);
if (rules.isEmpty()) return new BooleanRule(false);
Rule result = rules.get(0);
for (int i = 1; i < rules.size(); i++) result = new OrRule(result, rules.get(i));
return result;
}
private Rule buildNotRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> rules = childRules(node, series, p);
if (rules.isEmpty()) return new BooleanRule(false);
return new NotRule(rules.get(0));
}
private List<Rule> childRules(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> list = new ArrayList<>();
JsonNode children = node.path("children");
if (children.isArray()) {
for (JsonNode child : children) list.add(toRule(child, series, p));
}
return list;
}
// ── CONDITION ─────────────────────────────────────────────────────────────
private Rule buildConditionRule(JsonNode cond, BarSeries series,
Map<String, Map<String, Object>> params) {
if (cond == null || cond.isNull()) return new BooleanRule(false);
String indType = cond.path("indicatorType").asText("");
String condType = cond.path("conditionType").asText("GT");
String leftField = cond.path("leftField").asText("NONE");
String rightField = cond.path("rightField").asText("NONE");
int slopePeriod = cond.path("slopePeriod").asInt(3);
int holdDays = cond.path("holdDays").asInt(3);
// DSL 타입(STOCHASTIC 등) → DB 레지스트리 키(Stochastic 등) 변환 후 파라미터 조회
String registryKey = toRegistryKey(indType);
Map<String, Object> indParams = params != null
? params.getOrDefault(registryKey, Map.of())
: Map.of();
try {
Indicator<Num> left = resolveField(leftField, indType, indParams, series);
Indicator<Num> right = resolveField(rightField, indType, indParams, series);
return switch (condType) {
case "GT" -> new OverIndicatorRule(left, right);
case "LT" -> new UnderIndicatorRule(left, right);
// GTE/LTE: OverIndicator OR 동일 (epsilon 비교)
case "GTE" -> new OrRule(new OverIndicatorRule(left, right),
buildEqRule(left, right, series));
case "LTE" -> new OrRule(new UnderIndicatorRule(left, right),
buildEqRule(left, right, series));
case "EQ" -> buildEqRule(left, right, series);
case "NEQ" -> new NotRule(buildEqRule(left, right, series));
// CrossedUp/Down: ta4j 0.22 에서는 2인자 생성자만 있음
case "CROSS_UP" -> new CrossedUpIndicatorRule(left, right);
case "CROSS_DOWN" -> new CrossedDownIndicatorRule(left, right);
case "SLOPE_UP" -> new IsRisingRule(left, slopePeriod);
case "SLOPE_DOWN" -> new IsFallingRule(left, slopePeriod);
case "DIFF_GT" -> {
double v = cond.path("compareValue").asDouble(0);
yield new OverIndicatorRule(
NumericIndicator.of(left).minus(right).abs(),
new ConstantIndicator<>(series, series.numFactory().numOf(v)));
}
case "DIFF_LT" -> {
double v = cond.path("compareValue").asDouble(0);
yield new UnderIndicatorRule(
NumericIndicator.of(left).minus(right).abs(),
new ConstantIndicator<>(series, series.numFactory().numOf(v)));
}
case "HOLD_N_DAYS" -> buildHoldRule(new OverIndicatorRule(left, right), holdDays);
// ── 일목균형표 전용 ────────────────────────────────────────────
case "ABOVE_CLOUD" -> buildCloudAboveRule(series, indParams);
case "BELOW_CLOUD" -> buildCloudBelowRule(series, indParams);
case "IN_CLOUD" -> buildInCloudRule(series, indParams);
case "CLOUD_BREAK_UP" -> buildCloudBreakRule(series, indParams, true);
case "CLOUD_BREAK_DOWN" -> buildCloudBreakRule(series, indParams, false);
case "SPAN1_GT_SPAN2" -> new OverIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
case "SPAN1_LT_SPAN2" -> new UnderIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
case "SPAN1_CROSS_UP_SPAN2" -> new CrossedUpIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
case "SPAN1_CROSS_DOWN_SPAN2" -> new CrossedDownIndicatorRule(ichimokuSpanA(series, indParams), ichimokuSpanB(series, indParams));
case "LAGGING_GT_PRICE" -> new OverIndicatorRule(ichimokuLagging(series, indParams), new ClosePriceIndicator(series));
case "LAGGING_LT_PRICE" -> new UnderIndicatorRule(ichimokuLagging(series, indParams), new ClosePriceIndicator(series));
default -> {
log.warn("[Adapter] 미지원 conditionType: {}", condType);
yield new BooleanRule(false);
}
};
} catch (Exception e) {
log.warn("[Adapter] 조건 빌드 실패 ind={} cond={}: {}", indType, condType, e.getMessage());
return new BooleanRule(false);
}
}
// ── 필드 Resolver ─────────────────────────────────────────────────────────
private Indicator<Num> resolveField(String field, String indType,
Map<String, Object> p, BarSeries s) {
if (field == null || field.isBlank() || field.equals("NONE")) {
return new ConstantIndicator<>(s, s.numFactory().numOf(0));
}
return switch (field) {
case "CLOSE_PRICE" -> new ClosePriceIndicator(s);
case "OPEN_PRICE" -> new OpenPriceIndicator(s);
case "HIGH_PRICE" -> new HighPriceIndicator(s);
case "LOW_PRICE" -> new LowPriceIndicator(s);
case "VOLUME_VALUE" -> new VolumeIndicator(s, 1);
default -> {
double constant = resolveConstantField(field);
if (!Double.isNaN(constant)) {
yield new ConstantIndicator<>(s, s.numFactory().numOf(constant));
}
yield resolveIndicatorField(field, indType, p, s);
}
};
}
private double resolveConstantField(String field) {
// K_ 접두사: 프론트엔드가 hline 실제값으로 생성한 상수 필드
// 예) K_60 → 60, K_-100 → -100, K_0 → 0
if (field.startsWith("K_")) {
try { return Double.parseDouble(field.substring(2)); } catch (NumberFormatException e) { /* fall */ }
}
// NEG 접두사 (레거시): OVERSOLD_NEG80 → -80
if (field.contains("_NEG")) {
String[] parts = field.split("_NEG");
try { return -Double.parseDouble(parts[parts.length - 1]); } catch (NumberFormatException e) { /* fall */ }
}
if (field.equals("ZERO_LINE")) return 0.0;
// 마지막 _ 이후 숫자 추출 (레거시): OVERBOUGHT_70 → 70, ADX_25 → 25
int idx = field.lastIndexOf('_');
if (idx >= 0 && idx < field.length() - 1) {
try { return Double.parseDouble(field.substring(idx + 1)); } catch (NumberFormatException e) { /* fall */ }
}
return Double.NaN;
}
private Indicator<Num> resolveIndicatorField(String field, String indType,
Map<String, Object> p, BarSeries s) {
ClosePriceIndicator close = new ClosePriceIndicator(s);
// CCI — 프론트엔드 indicatorRegistry 기본값 20 과 통일
if (field.equals("CCI_VALUE") || indType.equals("CCI"))
return new CCIIndicator(s, intP(p, "length", 13));
// RSI — 표준 기본값 14
if (field.equals("RSI_VALUE"))
return new RSIIndicator(close, intP(p, "length", 14));
// MACD
if (field.equals("MACD_LINE")) {
return new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26));
}
if (field.equals("SIGNAL_LINE")) {
return new EMAIndicator(
new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26)),
intP(p, "signalLength", 9));
}
if (field.equals("HISTOGRAM")) {
MACDIndicator macd = new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26));
return NumericIndicator.of(macd).minus(new EMAIndicator(macd, intP(p, "signalLength", 9)));
}
// ADX / DMI
if (field.equals("ADX_VALUE"))
return new ADXIndicator(s, intP(p, "diLength", 14), intP(p, "adxSmoothing", 14));
if (field.equals("PDI") || field.equals("PLUS_DI")) {
int diLen = intP(p, "diLength", intP(p, "length", 14));
return new PlusDIIndicator(s, diLen);
}
if (field.equals("MDI") || field.equals("MINUS_DI")) {
int diLen = intP(p, "diLength", intP(p, "length", 14));
return new MinusDIIndicator(s, diLen);
}
// Stochastic Slow — IndicatorService.calcStochastic 과 동일한 로직:
// raw %K = StochasticOscillatorKIndicator(kLen)
// slow %K = SMA(raw %K, smooth)
// %D = SMA(slow %K, dSmoothing)
if (field.equals("STOCH_K")) {
int kLen = intP(p, "kLength", 14);
int smooth = intP(p, "smooth", 3);
return new SMAIndicator(new StochasticOscillatorKIndicator(s, kLen), smooth);
}
if (field.equals("STOCH_D")) {
int kLen = intP(p, "kLength", 14);
int smooth = intP(p, "smooth", 3);
int dSmooth = intP(p, "dSmoothing", 3);
SMAIndicator slowK = new SMAIndicator(new StochasticOscillatorKIndicator(s, kLen), smooth);
return new SMAIndicator(slowK, dSmooth);
}
// TRIX — ln(종가) → 3×EMA → Δ×10000 (업비트)
if (field.equals("TRIX_VALUE")) {
int len = intP(p, "length", 12);
EMAIndicator e3 = tripleLogEma(close, len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
return NumericIndicator.of(e3).minus(prev3).multipliedBy(10_000);
}
if (field.equals("TRIX_SIGNAL")) {
int len = intP(p, "length", 12);
EMAIndicator e3 = tripleLogEma(close, len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
NumericIndicator trix = NumericIndicator.of(e3).minus(prev3).multipliedBy(10_000);
return new EMAIndicator(trix, intP(p, "signalLength", 9));
}
// OBV
if (field.equals("OBV_LINE")) return new OnBalanceVolumeIndicator(s);
if (field.equals("OBV_SIGNAL")) {
OnBalanceVolumeIndicator obv = new OnBalanceVolumeIndicator(s);
String maType = p != null ? p.getOrDefault("maType", "SMA").toString() : "SMA";
int maLen = intP(p, "maLength", 9);
return switch (maType) {
case "EMA" -> new EMAIndicator(obv, maLen);
case "WMA" -> new WMAIndicator(obv, maLen);
default -> new SMAIndicator(obv, maLen);
};
}
// VR
if (field.equals("VR_VALUE") || indType.equals("VR"))
return vrIndicator(s, intP(p, "length", 10));
// 이격도 DISPARITY5 → period 5
if (field.startsWith("DISPARITY")) {
int period = parseTrailingInt(field, "DISPARITY", 20);
return disparityIndicator(s, period, p);
}
if (field.equals("PSY_VALUE") || field.equals("NEW_PSY_VALUE")
|| indType.equals("PSYCHOLOGICAL") || indType.equals("NEW_PSYCHOLOGICAL"))
return newPsychIndicator(s, intP(p, "length", 12), false);
if (field.equals("INVEST_PSY_VALUE") || indType.equals("INVEST_PSYCHOLOGICAL"))
return newPsychIndicator(s, intP(p, "length", 10), true);
// Williams %R
if (field.equals("WILLIAMS_R_VALUE")) return new WilliamsRIndicator(s, intP(p, "length", 14));
// Bollinger Bands
if (field.equals("UPPER_BAND") || field.equals("LOWER_BAND") || field.equals("MIDDLE_BAND")) {
int len = intP(p, "length", 20);
double mult = dblP(p, "mult", 2.0);
String maType = p.getOrDefault("maType", "SMA").toString();
Indicator<Num> basis = maOfSource(close, s, maType, len);
StandardDeviationIndicator sd = StandardDeviationIndicator.ofSample(close, len);
BollingerBandsMiddleIndicator mid = new BollingerBandsMiddleIndicator(basis);
Num k = s.numFactory().numOf(mult);
return switch (field) {
case "UPPER_BAND" -> new BollingerBandsUpperIndicator(mid, sd, k);
case "LOWER_BAND" -> new BollingerBandsLowerIndicator(mid, sd, k);
default -> mid;
};
}
// MA (SMA)
if (field.startsWith("MA") && !field.startsWith("MACD")) {
try { return new SMAIndicator(close, Integer.parseInt(field.substring(2))); }
catch (NumberFormatException ignored) { /* fall */ }
}
if (field.startsWith("EMA")) {
try { return new EMAIndicator(close, Integer.parseInt(field.substring(3))); }
catch (NumberFormatException ignored) { /* fall */ }
}
// 일목균형표
if (field.equals("CONVERSION_LINE")) return ichimokuTenkan(s, p);
if (field.equals("BASE_LINE")) return ichimokuKijun(s, p);
if (field.equals("LEADING_SPAN1")) return ichimokuSpanA(s, p);
if (field.equals("LEADING_SPAN2")) return ichimokuSpanB(s, p);
if (field.equals("LAGGING_SPAN")) return ichimokuLagging(s, p);
// 거래량 MA
if (field.equals("VOLUME_MA")) return new SMAIndicator(new VolumeIndicator(s, 1), intP(p, "length", 20));
log.warn("[Adapter] 미지원 필드: {} (indicatorType={})", field, indType);
return new ClosePriceIndicator(s);
}
// ── 일목균형표 지표 빌더 ──────────────────────────────────────────────────
private IchimokuTenkanSenIndicator ichimokuTenkan(BarSeries s, Map<String, Object> p) {
return new IchimokuTenkanSenIndicator(s, intP(p, "conversionPeriods", 9));
}
private IchimokuKijunSenIndicator ichimokuKijun(BarSeries s, Map<String, Object> p) {
return new IchimokuKijunSenIndicator(s, intP(p, "basePeriods", 26));
}
private IchimokuSenkouSpanAIndicator ichimokuSpanA(BarSeries s, Map<String, Object> p) {
return new IchimokuSenkouSpanAIndicator(s, ichimokuTenkan(s, p), ichimokuKijun(s, p),
intP(p, "displacement", 26));
}
private IchimokuSenkouSpanBIndicator ichimokuSpanB(BarSeries s, Map<String, Object> p) {
// IndicatorService 와 동일하게 displacement 파라미터 반영
return new IchimokuSenkouSpanBIndicator(s, intP(p, "laggingSpan2Periods", 52),
intP(p, "displacement", 26));
}
private IchimokuChikouSpanIndicator ichimokuLagging(BarSeries s, Map<String, Object> p) {
return new IchimokuChikouSpanIndicator(s, intP(p, "displacement", 26));
}
/** 종가가 구름 위 */
private Rule buildCloudAboveRule(BarSeries s, Map<String, Object> p) {
NumericIndicator cloudTop = NumericIndicator.of(ichimokuSpanA(s, p)).max(ichimokuSpanB(s, p));
return new OverIndicatorRule(new ClosePriceIndicator(s), cloudTop);
}
/** 종가가 구름 아래 */
private Rule buildCloudBelowRule(BarSeries s, Map<String, Object> p) {
NumericIndicator cloudBottom = NumericIndicator.of(ichimokuSpanA(s, p)).min(ichimokuSpanB(s, p));
return new UnderIndicatorRule(new ClosePriceIndicator(s), cloudBottom);
}
/** 종가가 구름 안 */
private Rule buildInCloudRule(BarSeries s, Map<String, Object> p) {
ClosePriceIndicator close = new ClosePriceIndicator(s);
NumericIndicator cloudTop = NumericIndicator.of(ichimokuSpanA(s, p)).max(ichimokuSpanB(s, p));
NumericIndicator cloudBottom = NumericIndicator.of(ichimokuSpanA(s, p)).min(ichimokuSpanB(s, p));
return new AndRule(
new OrRule(new OverIndicatorRule(close, cloudBottom),
buildEqRule(close, cloudBottom, s)),
new OrRule(new UnderIndicatorRule(close, cloudTop),
buildEqRule(close, cloudTop, s)));
}
/** 구름 상향(true)/하향(false) 돌파 */
private Rule buildCloudBreakRule(BarSeries s, Map<String, Object> p, boolean up) {
NumericIndicator cloudTop = NumericIndicator.of(ichimokuSpanA(s, p)).max(ichimokuSpanB(s, p));
NumericIndicator cloudBottom = NumericIndicator.of(ichimokuSpanA(s, p)).min(ichimokuSpanB(s, p));
ClosePriceIndicator close = new ClosePriceIndicator(s);
if (up) return new CrossedUpIndicatorRule(close, cloudTop);
else return new CrossedDownIndicatorRule(close, cloudBottom);
}
// ── 범용 Rule 빌더 ────────────────────────────────────────────────────────
/** |a - b| < epsilon → equal */
private Rule buildEqRule(Indicator<Num> a, Indicator<Num> b, BarSeries series) {
NumericIndicator diff = NumericIndicator.of(a).minus(b).abs();
return new UnderIndicatorRule(diff,
new ConstantIndicator<>(series, series.numFactory().numOf(1e-9)));
}
/** N봉 연속으로 inner 규칙이 성립 */
private Rule buildHoldRule(Rule inner, int n) {
if (n <= 1) return inner;
return new Rule() {
@Override
public boolean isSatisfied(int index, TradingRecord record) {
if (index < n - 1) return false;
for (int i = index - n + 1; i <= index; i++) {
if (!inner.isSatisfied(i, record)) return false;
}
return true;
}
};
}
// ── TRIX 헬퍼 ─────────────────────────────────────────────────────────────
private EMAIndicator tripleLogEma(ClosePriceIndicator close, int len) {
Indicator<Num> logClose = UnaryOperationIndicator.log(close);
return new EMAIndicator(new EMAIndicator(new EMAIndicator(logClose, len), len), len);
}
private Indicator<Num> maOfSource(Indicator<Num> source, BarSeries s, String maType, int len) {
return switch (maType) {
case "EMA" -> new EMAIndicator(source, len);
case "WMA" -> new WMAIndicator(source, len);
case "SMMA (RMA)", "RMA" -> new WildersMAIndicator(source, len);
default -> new SMAIndicator(source, len);
};
}
private int parseTrailingInt(String field, String prefix, int def) {
if (!field.startsWith(prefix)) return def;
try {
return Integer.parseInt(field.substring(prefix.length()));
} catch (NumberFormatException e) {
return def;
}
}
/** 이격도 = 종가 / SMA(period) * 100 */
private Indicator<Num> disparityIndicator(BarSeries s, int period, Map<String, Object> p) {
ClosePriceIndicator close = new ClosePriceIndicator(s);
SMAIndicator sma = new SMAIndicator(close, period);
return NumericIndicator.of(close).dividedBy(sma).multipliedBy(100);
}
private Indicator<Num> vrIndicator(BarSeries s, int period) {
return new VrIndicator(s, period);
}
private Indicator<Num> newPsychIndicator(BarSeries s, int period, boolean volumeWeighted) {
return new PsychologicalIndicator(s, period, volumeWeighted);
}
/** VR(거래량비율) = 상승일 거래량합 / 하락일 거래량합 × 100 */
private static final class VrIndicator extends CachedIndicator<Num> {
private final int period;
private final ClosePriceIndicator close;
private final VolumeIndicator volume;
VrIndicator(BarSeries series, int period) {
super(series);
this.period = period;
this.close = new ClosePriceIndicator(series);
this.volume = new VolumeIndicator(series, 1);
}
@Override
protected Num calculate(int index) {
if (index < period) return null;
double upVol = 0, downVol = 0;
for (int j = index - period + 1; j <= index; j++) {
Num c = close.getValue(j);
Num cPrev = close.getValue(j - 1);
Num v = volume.getValue(j);
if (c == null || cPrev == null || v == null) continue;
if (c.isGreaterThan(cPrev)) upVol += v.doubleValue();
else if (c.isLessThan(cPrev)) downVol += v.doubleValue();
}
if (downVol == 0) return null;
return getBarSeries().numFactory().numOf(upVol / downVol * 100.0);
}
@Override
public int getCountOfUnstableBars() {
return period + 1;
}
}
/** 신심리도 / 투자심리도 */
private static final class PsychologicalIndicator extends CachedIndicator<Num> {
private final int period;
private final boolean volumeWeighted;
private final ClosePriceIndicator close;
private final VolumeIndicator volume;
PsychologicalIndicator(BarSeries series, int period, boolean volumeWeighted) {
super(series);
this.period = period;
this.volumeWeighted = volumeWeighted;
this.close = new ClosePriceIndicator(series);
this.volume = new VolumeIndicator(series, 1);
}
@Override
protected Num calculate(int index) {
if (index < period) return null;
if (!volumeWeighted) {
int up = 0;
for (int j = index - period + 1; j <= index; j++) {
Num c = close.getValue(j);
Num cPrev = close.getValue(j - 1);
if (c != null && cPrev != null && c.isGreaterThan(cPrev)) up++;
}
return getBarSeries().numFactory().numOf((double) up / period * 100.0);
}
double upVol = 0, total = 0;
for (int j = index - period + 1; j <= index; j++) {
Num c = close.getValue(j);
Num cPrev = close.getValue(j - 1);
Num v = volume.getValue(j);
if (v == null) continue;
total += v.doubleValue();
if (c != null && cPrev != null && c.isGreaterThan(cPrev)) upVol += v.doubleValue();
}
if (total == 0) return null;
return getBarSeries().numFactory().numOf(upVol / total * 100.0);
}
@Override
public int getCountOfUnstableBars() {
return period + 1;
}
}
// ── 파라미터 헬퍼 ─────────────────────────────────────────────────────────
private int intP(Map<String, Object> p, String k, int def) {
if (p == null) return def;
Object v = p.get(k);
return v == null ? def : ((Number) v).intValue();
}
private double dblP(Map<String, Object> p, String k, double def) {
if (p == null) return def;
Object v = p.get(k);
return v == null ? def : ((Number) v).doubleValue();
}
}