n봉 존재 조건 수정
This commit is contained in:
@@ -490,7 +490,9 @@ public class LiveConditionStatusService {
|
||||
String indType = c.path("indicatorType").asText("");
|
||||
String plotKey = plotKeyFromCondition(c, indType);
|
||||
String condType = c.path("conditionType").asText("GT");
|
||||
String condLabel = CONDITION_LABEL.getOrDefault(condType, condType);
|
||||
String baseLabel = CONDITION_LABEL.getOrDefault(condType, condType);
|
||||
String rangePrefix = StrategyDslToTa4jAdapter.candleRangeConditionPrefix(c);
|
||||
String condLabel = rangePrefix.isBlank() ? baseLabel : rangePrefix + baseLabel;
|
||||
Double target = readTargetNumeric(c, visual);
|
||||
return LiveConditionRowDto.builder()
|
||||
.id(p.id())
|
||||
@@ -535,7 +537,9 @@ public class LiveConditionStatusService {
|
||||
String indType = c.path("indicatorType").asText("");
|
||||
String plotKey = plotKeyFromCondition(c, indType);
|
||||
String condType = c.path("conditionType").asText("GT");
|
||||
String condLabel = CONDITION_LABEL.getOrDefault(condType, condType);
|
||||
String baseLabel = CONDITION_LABEL.getOrDefault(condType, condType);
|
||||
String rangePrefix = StrategyDslToTa4jAdapter.candleRangeConditionPrefix(c);
|
||||
String condLabel = rangePrefix.isBlank() ? baseLabel : rangePrefix + baseLabel;
|
||||
|
||||
return LiveConditionRowDto.builder()
|
||||
.id(p.id())
|
||||
|
||||
@@ -412,9 +412,12 @@ public class StrategyDslToTa4jAdapter {
|
||||
int slopePeriod = cond.path("slopePeriod").asInt(3);
|
||||
int holdDays = cond.path("holdDays").asInt(3);
|
||||
int lookbackPeriod = cond.path("lookbackPeriod").asInt(20);
|
||||
int candleRange = cond.path("candleRange").asInt(1);
|
||||
String candleRangeMode = resolveCandleRangeMode(cond, candleRange);
|
||||
|
||||
if (needsCrossTimeframeComposite(cond, ctx)) {
|
||||
return buildCrossTimeframeCompositeRule(cond, ctx);
|
||||
Rule composite = buildCrossTimeframeCompositeRule(cond, ctx);
|
||||
return applyCandleRangeWindow(composite, candleRangeMode, candleRange);
|
||||
}
|
||||
|
||||
PriceExtremeNorm px = normalizePriceExtremeCondition(cond);
|
||||
@@ -496,10 +499,11 @@ public class StrategyDslToTa4jAdapter {
|
||||
yield new BooleanRule(false);
|
||||
}
|
||||
};
|
||||
Rule ranged = applyCandleRangeWindow(core, candleRangeMode, candleRange);
|
||||
if (px != null) {
|
||||
return nanSafeCompareRule(core, left, right);
|
||||
return nanSafeCompareRule(ranged, left, right);
|
||||
}
|
||||
return core;
|
||||
return ranged;
|
||||
} catch (Exception e) {
|
||||
log.warn("[Adapter] 조건 빌드 실패 ind={} cond={}: {}", indType, condType, e.getMessage());
|
||||
return new BooleanRule(false);
|
||||
@@ -1041,6 +1045,71 @@ public class StrategyDslToTa4jAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
/** live-conditions·알림 UI — 캔들 범위 접두어 */
|
||||
public static String candleRangeConditionPrefix(JsonNode cond) {
|
||||
if (cond == null || cond.isNull()) return "";
|
||||
int candleRange = cond.path("candleRange").asInt(1);
|
||||
String mode = cond.path("candleRangeMode").asText("");
|
||||
if (mode.isBlank()) mode = candleRange <= 1 ? "CURRENT" : "EXISTS_IN";
|
||||
if ("CURRENT".equals(mode)) return "";
|
||||
int n = Math.max(2, candleRange);
|
||||
return switch (mode) {
|
||||
case "EXISTS_IN" -> "최근 " + n + "봉 내 ";
|
||||
case "NOT_EXISTS_IN" -> "최근 " + n + "봉 내 없음 · ";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
/** candleRangeMode 미설정 시 candleRange>1 → EXISTS_IN (구버전 호환) */
|
||||
private String resolveCandleRangeMode(JsonNode cond, int candleRange) {
|
||||
String mode = cond.path("candleRangeMode").asText("");
|
||||
if (!mode.isBlank()) return mode;
|
||||
return candleRange <= 1 ? "CURRENT" : "EXISTS_IN";
|
||||
}
|
||||
|
||||
/** 최근 N봉(현재 봉 포함) 윈도우에 inner 규칙 적용 */
|
||||
private Rule applyCandleRangeWindow(Rule core, String mode, int candleRange) {
|
||||
if ("CURRENT".equals(mode)) return core;
|
||||
int n = Math.max(2, candleRange);
|
||||
return switch (mode) {
|
||||
case "EXISTS_IN" -> buildExistsInWindowRule(core, n);
|
||||
case "NOT_EXISTS_IN" -> buildNotExistsInWindowRule(core, n);
|
||||
default -> core;
|
||||
};
|
||||
}
|
||||
|
||||
/** 최근 N봉 중 어느 한 봉이라도 inner 가 true */
|
||||
private Rule buildExistsInWindowRule(Rule inner, int n) {
|
||||
if (n <= 1) return inner;
|
||||
return new Rule() {
|
||||
@Override
|
||||
public boolean isSatisfied(int index, TradingRecord record) {
|
||||
int start = index - n + 1;
|
||||
for (int i = start; i <= index; i++) {
|
||||
if (i < 0) continue;
|
||||
if (inner.isSatisfied(i, record)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 최근 N봉 중 inner 가 true 인 봉이 하나도 없음 */
|
||||
private Rule buildNotExistsInWindowRule(Rule inner, int n) {
|
||||
if (n <= 1) return new NotRule(inner);
|
||||
return new Rule() {
|
||||
@Override
|
||||
public boolean isSatisfied(int index, TradingRecord record) {
|
||||
int start = index - n + 1;
|
||||
for (int i = start; i <= index; i++) {
|
||||
if (i < 0) continue;
|
||||
if (inner.isSatisfied(i, record)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── TRIX 헬퍼 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Indicator<Num> resolvePriceSource(BarSeries s, Map<String, Object> p) {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.Rule;
|
||||
import org.ta4j.core.TradingRecord;
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* candleRange / candleRangeMode — 최근 N봉 윈도우 존재·미존재 평가.
|
||||
*/
|
||||
class CandleRangeWindowRuleTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private StrategyDslToTa4jAdapter adapter;
|
||||
private BarSeries series;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
adapter = new StrategyDslToTa4jAdapter();
|
||||
series = buildSyntheticSeries(40);
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsIn_trueWhenInnerTrueInWindow() throws Exception {
|
||||
Rule rule = toGtRule("""
|
||||
"candleRangeMode": "EXISTS_IN",
|
||||
"candleRange": 4
|
||||
""");
|
||||
int idx = series.getEndIndex();
|
||||
assertTrue(rule.isSatisfied(idx, null),
|
||||
"close > 0 은 모든 봉에서 true → EXISTS_IN(4) 도 true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void notExistsIn_falseWhenInnerTrueInWindow() throws Exception {
|
||||
Rule rule = toGtRule("""
|
||||
"candleRangeMode": "NOT_EXISTS_IN",
|
||||
"candleRange": 4
|
||||
""");
|
||||
int idx = series.getEndIndex();
|
||||
assertFalse(rule.isSatisfied(idx, null),
|
||||
"close > 0 이 윈도우 내 항상 true → NOT_EXISTS_IN 은 false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyCandleRangeFour_treatedAsExistsIn() throws Exception {
|
||||
Rule rule = toGtRule("""
|
||||
"candleRange": 4
|
||||
""");
|
||||
int idx = series.getEndIndex();
|
||||
assertTrue(rule.isSatisfied(idx, null),
|
||||
"candleRangeMode 미설정 + candleRange=4 → EXISTS_IN 호환");
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentMode_sameAsInnerWithoutWindow() throws Exception {
|
||||
Rule current = toGtRule("""
|
||||
"candleRangeMode": "CURRENT",
|
||||
"candleRange": 1
|
||||
""");
|
||||
Rule plain = toGtRule("");
|
||||
int idx = series.getEndIndex();
|
||||
assertEquals(plain.isSatisfied(idx, null), current.isSatisfied(idx, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void notExistsIn_trueWhenInnerNeverTrueInWindow() throws Exception {
|
||||
Rule rule = toGtRule("""
|
||||
"candleRangeMode": "NOT_EXISTS_IN",
|
||||
"candleRange": 3,
|
||||
"rightField": "K_999999999999"
|
||||
""");
|
||||
int idx = series.getEndIndex();
|
||||
assertTrue(rule.isSatisfied(idx, null),
|
||||
"close > 매우 큰 값 은 윈도우 내 항상 false → NOT_EXISTS_IN true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void obvCrossUp_existsInWindow_compilesAndEvaluates() throws Exception {
|
||||
JsonNode cond = MAPPER.readTree("""
|
||||
{
|
||||
"id": "c-obv-exists",
|
||||
"type": "CONDITION",
|
||||
"condition": {
|
||||
"indicatorType": "OBV",
|
||||
"conditionType": "CROSS_UP",
|
||||
"leftField": "OBV_LINE",
|
||||
"rightField": "OBV_SIGNAL",
|
||||
"candleRangeMode": "EXISTS_IN",
|
||||
"candleRange": 4
|
||||
}
|
||||
}
|
||||
""");
|
||||
Rule rule = adapter.toRule(cond, series, java.util.Map.of());
|
||||
assertNotNull(rule);
|
||||
assertDoesNotThrow(() -> rule.isSatisfied(series.getEndIndex(), null));
|
||||
}
|
||||
|
||||
private Rule toGtRule(String extraFields) throws Exception {
|
||||
String extra = extraFields == null || extraFields.isBlank()
|
||||
? ""
|
||||
: ",\n " + extraFields.strip().replaceAll(",\\s*$", "");
|
||||
String json = """
|
||||
{
|
||||
"id": "c-range",
|
||||
"type": "CONDITION",
|
||||
"condition": {
|
||||
"indicatorType": "MA",
|
||||
"conditionType": "GT",
|
||||
"leftField": "CLOSE_PRICE",
|
||||
"rightField": "K_0"%s
|
||||
}
|
||||
}
|
||||
""".formatted(extra);
|
||||
JsonNode node = MAPPER.readTree(json);
|
||||
Rule rule = adapter.toRule(node, series, java.util.Map.of());
|
||||
assertNotNull(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
private static BarSeries buildSyntheticSeries(int barCount) {
|
||||
BarSeries s = new BaseBarSeriesBuilder()
|
||||
.withName("test-1m")
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.withBarBuilderFactory(new TimeBarBuilderFactory())
|
||||
.build();
|
||||
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
|
||||
Instant base = Instant.parse("2024-06-01T00:00:00Z");
|
||||
Duration period = Duration.ofMinutes(1);
|
||||
for (int i = 0; i < barCount; i++) {
|
||||
double close = 50_000_000 + i * 120;
|
||||
Instant end = base.plus(period.multipliedBy(i + 1L));
|
||||
factory.createBarBuilder(s)
|
||||
.timePeriod(period)
|
||||
.endTime(end)
|
||||
.openPrice(close - 500)
|
||||
.highPrice(close + 800)
|
||||
.lowPrice(close - 800)
|
||||
.closePrice(close)
|
||||
.volume(10 + i)
|
||||
.add();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user