추세선 전략추가
This commit is contained in:
@@ -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 기간으로 쓰지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user