goldenChat base source add
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user