추세선 전략추가

This commit is contained in:
Macbook
2026-06-25 02:45:39 +09:00
parent ee8cd50781
commit f9447ea52f
31 changed files with 1793 additions and 49 deletions
@@ -1,6 +1,8 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.goldenchart.dto.TrendLineConfig;
import com.goldenchart.dto.TrendLineKind;
import com.goldenchart.storage.Ta4jStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -122,20 +124,37 @@ public class StrategyDslToTa4jAdapter {
* null 이 아니고 비어 있지 않으면 백테스트 모드로 동작하며
* CrossSeriesRule 에서 타임스탬프 기반 룩업을 수행한다.
*/
Map<String, BarSeries> seriesOverrides
Map<String, BarSeries> seriesOverrides,
/** 추세선 조건(TREND_LINE_CROSS_*) — 전략설정 gc_backtest_settings */
TrendLineConfig trendLineConfig
) {
/** visual 미전달 호환 (레거시) */
public RuleBuildContext(BarSeries primarySeries,
Map<String, Map<String, Object>> indicatorParams,
String market,
Ta4jStorage storage) {
this(primarySeries, indicatorParams, Map.of(), market, storage, false, Map.of());
this(primarySeries, indicatorParams, Map.of(), market, storage, false, Map.of(), null);
}
public RuleBuildContext(BarSeries primarySeries,
Map<String, Map<String, Object>> indicatorParams,
Map<String, Map<String, Object>> indicatorVisual,
String market,
Ta4jStorage storage,
boolean useConfirmedOnly,
Map<String, BarSeries> seriesOverrides) {
this(primarySeries, indicatorParams, indicatorVisual, market, storage,
useConfirmedOnly, seriesOverrides, null);
}
public TrendLineConfig effectiveTrendLineConfig() {
return trendLineConfig != null ? trendLineConfig : TrendLineConfig.defaults();
}
/** primarySeries 만 교체하고 나머지 필드 복사 (하위 컨텍스트 생성용) */
public RuleBuildContext withPrimary(BarSeries newPrimary) {
return new RuleBuildContext(newPrimary, indicatorParams, indicatorVisual,
market, storage, useConfirmedOnly, seriesOverrides);
market, storage, useConfirmedOnly, seriesOverrides, trendLineConfig);
}
/** MA1~11 슬롯 → period1~11 (SMA 보조지표 설정) */
@@ -170,20 +189,20 @@ public class StrategyDslToTa4jAdapter {
public Rule toRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> indicatorParams) {
return toRule(node, new RuleBuildContext(series, indicatorParams, Map.of(), null, null, false, Map.of()));
return toRule(node, new RuleBuildContext(series, indicatorParams, Map.of(), null, null, false, Map.of(), null));
}
public Rule toRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> indicatorParams,
String market, Ta4jStorage storage) {
return toRule(node, new RuleBuildContext(series, indicatorParams, Map.of(), market, storage, false, Map.of()));
return toRule(node, new RuleBuildContext(series, indicatorParams, Map.of(), market, storage, false, Map.of(), null));
}
public Rule toRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> indicatorParams,
Map<String, Map<String, Object>> indicatorVisual,
String market, Ta4jStorage storage) {
return toRule(node, new RuleBuildContext(series, indicatorParams, indicatorVisual, market, storage, false, Map.of()));
return toRule(node, new RuleBuildContext(series, indicatorParams, indicatorVisual, market, storage, false, Map.of(), null));
}
/**
@@ -194,7 +213,7 @@ public class StrategyDslToTa4jAdapter {
Map<String, Map<String, Object>> indicatorParams,
Map<String, BarSeries> seriesOverrides) {
return toRule(node, new RuleBuildContext(series, indicatorParams, Map.of(), null, null, false,
seriesOverrides != null ? seriesOverrides : Map.of()));
seriesOverrides != null ? seriesOverrides : Map.of(), null));
}
/**
@@ -204,7 +223,7 @@ public class StrategyDslToTa4jAdapter {
Map<String, Map<String, Object>> indicatorParams,
Map<String, Map<String, Object>> indicatorVisual,
String market, Ta4jStorage storage) {
return toRule(node, new RuleBuildContext(series, indicatorParams, indicatorVisual, market, storage, true, Map.of()));
return toRule(node, new RuleBuildContext(series, indicatorParams, indicatorVisual, market, storage, true, Map.of(), null));
}
public Rule toRule(JsonNode node, RuleBuildContext ctx) {
@@ -508,6 +527,15 @@ public class StrategyDslToTa4jAdapter {
// 직전봉≤·현재봉> 교차 (차트·CrossTimeframeCompositeRule 과 동일, NaN 구간 제외)
case "CROSS_UP" -> buildCrossUpRule(left, right);
case "CROSS_DOWN" -> buildCrossDownRule(left, right);
case "TREND_LINE_CROSS_UP", "TREND_LINE_CROSS_DOWN" -> {
int lb = cond.path("lookbackPeriod").asInt(0);
if (lb <= 0) lb = ctx.effectiveTrendLineConfig().getDefaultLookback();
lb = Math.max(2, lb);
boolean priceScale = isTrendLinePriceScaleField(leftField);
yield "TREND_LINE_CROSS_UP".equals(condType)
? buildTrendLineCrossUpRule(left, series, lb, ctx.effectiveTrendLineConfig(), priceScale)
: buildTrendLineCrossDownRule(left, series, lb, ctx.effectiveTrendLineConfig(), priceScale);
}
case "SLOPE_UP" -> buildSlopeTrendRule(left, true, candleRangeMode, candleRange, slopePeriod);
case "SLOPE_DOWN" -> buildSlopeTrendRule(left, false, candleRangeMode, candleRange, slopePeriod);
case "DIFF_GT" -> {
@@ -1657,6 +1685,66 @@ public class StrategyDslToTa4jAdapter {
};
}
/**
* N봉 추세선 돌파 — [i−N, i−1] 구간으로 고정된 동일 선을 i−1·i 에서 평가.
* (일반 CROSS 와 달리 추세선 창이 봉마다 밀리지 않음)
*/
private Rule buildTrendLineCrossUpRule(
Indicator<Num> left, BarSeries series, int lookback, TrendLineConfig config, boolean priceScale) {
return new Rule() {
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
if (index < 1) return false;
TrendLineKind kind = TrendLineKind.forCrossUp();
double linePrev = TrendLineMath.valueAtEval(
series, left, index, index - 1, lookback, config, kind, priceScale);
double lineCurr = TrendLineMath.valueAtEval(
series, left, index, index, lookback, config, kind, priceScale);
if (!Double.isFinite(linePrev) || !Double.isFinite(lineCurr)) return false;
Num l0 = left.getValue(index - 1);
Num l1 = left.getValue(index);
if (l0 == null || l1 == null || l0.isNaN() || l1.isNaN()) return false;
Num r0 = series.numFactory().numOf(linePrev);
Num r1 = series.numFactory().numOf(lineCurr);
return l0.isLessThanOrEqual(r0) && l1.isGreaterThan(r1);
}
};
}
private Rule buildTrendLineCrossDownRule(
Indicator<Num> left, BarSeries series, int lookback, TrendLineConfig config, boolean priceScale) {
return new Rule() {
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
if (index < 1) return false;
TrendLineKind kind = TrendLineKind.forCrossDown();
double linePrev = TrendLineMath.valueAtEval(
series, left, index, index - 1, lookback, config, kind, priceScale);
double lineCurr = TrendLineMath.valueAtEval(
series, left, index, index, lookback, config, kind, priceScale);
if (!Double.isFinite(linePrev) || !Double.isFinite(lineCurr)) return false;
Num l0 = left.getValue(index - 1);
Num l1 = left.getValue(index);
if (l0 == null || l1 == null || l0.isNaN() || l1.isNaN()) return false;
Num r0 = series.numFactory().numOf(linePrev);
Num r1 = series.numFactory().numOf(lineCurr);
return l0.isGreaterThanOrEqual(r0) && l1.isLessThan(r1);
}
};
}
/** 추세선 — 가격축(OHLC·MA 등) vs 오실레이터 */
private static boolean isTrendLinePriceScaleField(String field) {
if (field == null || field.isBlank() || "NONE".equalsIgnoreCase(field)) return true;
String f = field.toUpperCase();
return f.contains("CLOSE") || f.contains("PRICE") || f.contains("HIGH") || f.contains("LOW")
|| f.contains("OPEN") || f.contains("MA") || f.contains("EMA")
|| f.contains("UPPER") || f.contains("MIDDLE") || f.contains("LOWER")
|| f.contains("BAND") || f.contains("DC_") || f.contains("VWAP")
|| f.contains("CONVERSION") || f.contains("BASE") || f.contains("SPAN")
|| f.contains("LAGGING") || f.contains("CLOUD");
}
/**
* CCI_VALUE_20 등 좌측 본선 기간과 신호선(CCI_SIGNAL) 본선 기간을 일치시킨다.
* sidePeriod(우측)는 신호 SMA 길이용 — 본선 CCI/RSI/TRIX 기간으로 쓰지 않는다.