전략 수정
This commit is contained in:
@@ -222,10 +222,14 @@ public class LiveConditionStatusService {
|
||||
Map<String, Map<String, Object>> params,
|
||||
Map<String, Map<String, Object>> visual) {
|
||||
try {
|
||||
JsonNode buyDslRaw = objectMapper.readTree(
|
||||
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
|
||||
JsonNode sellDslRaw = objectMapper.readTree(
|
||||
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
|
||||
JsonNode buyDslRaw = timeframeNormalizer.normalize(
|
||||
objectMapper.readTree(
|
||||
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null"),
|
||||
strategy.getName());
|
||||
JsonNode sellDslRaw = timeframeNormalizer.normalize(
|
||||
objectMapper.readTree(
|
||||
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null"),
|
||||
strategy.getName());
|
||||
|
||||
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
|
||||
JsonNode buyDsl = timeframeNormalizer.remapForChartTimeframe(buyDslRaw, primaryTf);
|
||||
@@ -325,6 +329,16 @@ public class LiveConditionStatusService {
|
||||
return primarySeries;
|
||||
}
|
||||
|
||||
/** 백테스트·scan-signals 와 동일 — 마지막 evalCount 봉만 스캔 */
|
||||
private static int resolveChartScanStartIndex(BarSeries series, int evalCount) {
|
||||
if (series == null || series.isEmpty() || evalCount <= 0) {
|
||||
return series != null ? series.getBeginIndex() : 0;
|
||||
}
|
||||
int total = series.getBarCount();
|
||||
if (evalCount >= total) return series.getBeginIndex();
|
||||
return series.getEndIndex() - evalCount + 1;
|
||||
}
|
||||
|
||||
private Boolean evaluateOverallRuleOnChart(JsonNode dsl, BarSeries primarySeries,
|
||||
Map<String, BarSeries> seriesOverrides,
|
||||
String market,
|
||||
@@ -687,10 +701,15 @@ public class LiveConditionStatusService {
|
||||
indicatorSettingsService.getAllVisual(userId, deviceId);
|
||||
|
||||
try {
|
||||
JsonNode buyDslRaw = objectMapper.readTree(
|
||||
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
|
||||
JsonNode sellDslRaw = objectMapper.readTree(
|
||||
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
|
||||
String strategyName = strategy.getName();
|
||||
JsonNode buyDslRaw = timeframeNormalizer.normalize(
|
||||
objectMapper.readTree(
|
||||
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null"),
|
||||
strategyName);
|
||||
JsonNode sellDslRaw = timeframeNormalizer.normalize(
|
||||
objectMapper.readTree(
|
||||
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null"),
|
||||
strategyName);
|
||||
|
||||
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
|
||||
JsonNode buyDsl = timeframeNormalizer.remapForChartTimeframe(buyDslRaw, primaryTf);
|
||||
@@ -706,22 +725,27 @@ public class LiveConditionStatusService {
|
||||
int evalCount = evaluationBarCount != null && evaluationBarCount > 0
|
||||
? evaluationBarCount
|
||||
: primarySeries.getBarCount();
|
||||
int startIdx = Math.max(
|
||||
primarySeries.getBeginIndex(),
|
||||
primarySeries.getEndIndex() - evalCount + 1);
|
||||
int startIdx = resolveChartScanStartIndex(primarySeries, evalCount);
|
||||
|
||||
StrategyDslToTa4jAdapter.RuleBuildContext ruleCtx =
|
||||
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||
primarySeries, params, visual, market, null, false, seriesOverrides);
|
||||
Rule entryRule = (buyDsl != null && !buyDsl.isNull())
|
||||
? adapter.toRule(buyDsl, ruleCtx)
|
||||
: new org.ta4j.core.rules.BooleanRule(false);
|
||||
Rule exitRule = (sellDsl != null && !sellDsl.isNull())
|
||||
? adapter.toRule(sellDsl, ruleCtx)
|
||||
: new org.ta4j.core.rules.BooleanRule(false);
|
||||
|
||||
List<BacktestResponse.Signal> signals = new ArrayList<>();
|
||||
int buyHits = 0;
|
||||
int sellHits = 0;
|
||||
for (int i = startIdx; i <= primarySeries.getEndIndex(); i++) {
|
||||
long barTimeSec = barOpenEpochSec(primarySeries, i);
|
||||
double close = primarySeries.getBar(i).getClosePrice().doubleValue();
|
||||
|
||||
Boolean entryMet = evaluateOverallRuleOnChart(
|
||||
buyDsl, primarySeries, seriesOverrides, market, params, visual, barTimeSec);
|
||||
Boolean exitMet = evaluateOverallRuleOnChart(
|
||||
sellDsl, primarySeries, seriesOverrides, market, params, visual, barTimeSec);
|
||||
|
||||
// SIGNAL_ONLY — 조건 충족 봉마다 마커 (포지션 교대 없음, 차트 교차점 표시용)
|
||||
if (Boolean.TRUE.equals(entryMet)) {
|
||||
if (entryRule.isSatisfied(i, null)) {
|
||||
buyHits++;
|
||||
signals.add(BacktestResponse.Signal.builder()
|
||||
.time(barTimeSec)
|
||||
.type("BUY")
|
||||
@@ -730,7 +754,8 @@ public class LiveConditionStatusService {
|
||||
.quantity(0)
|
||||
.build());
|
||||
}
|
||||
if (Boolean.TRUE.equals(exitMet)) {
|
||||
if (exitRule.isSatisfied(i, null)) {
|
||||
sellHits++;
|
||||
signals.add(BacktestResponse.Signal.builder()
|
||||
.time(barTimeSec)
|
||||
.type("SELL")
|
||||
@@ -740,6 +765,14 @@ public class LiveConditionStatusService {
|
||||
.build());
|
||||
}
|
||||
}
|
||||
if (signals.isEmpty()) {
|
||||
log.info("[LiveCondition:scan] 0 signals market={} strategy={} tf={} bars={} eval={}..{}",
|
||||
market, strategyId, primaryTf, primarySeries.getBarCount(), startIdx,
|
||||
primarySeries.getEndIndex());
|
||||
} else {
|
||||
log.debug("[LiveCondition:scan] market={} strategy={} tf={} buy={} sell={}",
|
||||
market, strategyId, primaryTf, buyHits, sellHits);
|
||||
}
|
||||
return signals;
|
||||
} catch (Exception e) {
|
||||
log.warn("[LiveCondition:scan] fail market={} strategy={}: {}",
|
||||
|
||||
@@ -393,6 +393,10 @@ public class StrategyDslToTa4jAdapter {
|
||||
if (children.isArray()) {
|
||||
for (JsonNode child : children) list.add(toRule(child, ctx));
|
||||
}
|
||||
JsonNode single = node.path("child");
|
||||
if (list.isEmpty() && !single.isMissingNode() && !single.isNull()) {
|
||||
list.add(toRule(single, ctx));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -810,7 +814,7 @@ public class StrategyDslToTa4jAdapter {
|
||||
int period = parseTrailingInt(field, "CCI_VALUE_", intP(p, "length", 13));
|
||||
return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)), period);
|
||||
}
|
||||
if (field.equals("CCI_VALUE") || indType.equals("CCI"))
|
||||
if (field.equals("CCI_VALUE"))
|
||||
return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)),
|
||||
effectivePeriod(periodOverride, p, "length", 13));
|
||||
if (field.equals("CCI_SIGNAL")) {
|
||||
@@ -894,7 +898,7 @@ public class StrategyDslToTa4jAdapter {
|
||||
int len = parseTrailingInt(field, "TRIX_VALUE_", intP(p, "length", 12));
|
||||
return buildTrixLine(close, len);
|
||||
}
|
||||
if (field.equals("TRIX_VALUE") || indType.equals("TRIX")) {
|
||||
if (field.equals("TRIX_VALUE")) {
|
||||
return buildTrixLine(close, effectivePeriod(periodOverride, p, "length", 12));
|
||||
}
|
||||
if (field.equals("TRIX_SIGNAL")) {
|
||||
@@ -1561,14 +1565,16 @@ public class StrategyDslToTa4jAdapter {
|
||||
int periodOverride, String lengthKey, int defaultLen) {
|
||||
if (cond != null && !cond.isNull()) {
|
||||
String leftField = cond.path("leftField").asText("");
|
||||
if (leftField.startsWith(valuePrefix + "_")) {
|
||||
int fromField = parseTrailingInt(leftField, valuePrefix + "_", defaultLen);
|
||||
if (fromField > 0) return fromField;
|
||||
if (leftField.equals(valuePrefix) || leftField.startsWith(valuePrefix + "_")) {
|
||||
if (leftField.startsWith(valuePrefix + "_")) {
|
||||
int fromField = parseTrailingInt(leftField, valuePrefix + "_", defaultLen);
|
||||
if (fromField > 0) return fromField;
|
||||
}
|
||||
int leftPeriod = cond.path("leftPeriod").asInt(-1);
|
||||
if (leftPeriod > 0) return leftPeriod;
|
||||
int condPeriod = cond.path("period").asInt(-1);
|
||||
if (condPeriod > 0) return condPeriod;
|
||||
}
|
||||
int leftPeriod = cond.path("leftPeriod").asInt(-1);
|
||||
if (leftPeriod > 0) return leftPeriod;
|
||||
int condPeriod = cond.path("period").asInt(-1);
|
||||
if (condPeriod > 0) return condPeriod;
|
||||
}
|
||||
return effectivePeriod(periodOverride, p, lengthKey, defaultLen);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,165 @@ class CciChartScanIntegrationTest {
|
||||
assertTrue(hits > 0, "candleTypes[0]=3m must win over stale candleType=1m, got " + hits);
|
||||
}
|
||||
|
||||
@Test
|
||||
void chartScan_5mStrategy_remappedTo3mChart_firesSignals() throws Exception {
|
||||
String chartTf = "3m";
|
||||
List<OhlcvBar> bars = buildOscillatingOhlcvBars(150, chartTf);
|
||||
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(bars, chartTf);
|
||||
|
||||
JsonNode buyDslRaw = MAPPER.readTree("""
|
||||
{
|
||||
"type": "TIMEFRAME",
|
||||
"candleTypes": ["5m"],
|
||||
"candleType": "5m",
|
||||
"children": [{
|
||||
"type": "CONDITION",
|
||||
"condition": {
|
||||
"indicatorType": "CCI",
|
||||
"conditionType": "CROSS_UP",
|
||||
"leftField": "CCI_VALUE_20",
|
||||
"rightField": "CCI_SIGNAL",
|
||||
"period": 20,
|
||||
"valuePeriodOverride": true,
|
||||
"candleRange": 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
""");
|
||||
JsonNode buyDsl = normalizer.remapForChartTimeframe(buyDslRaw, chartTf);
|
||||
assertEquals("3m", buyDsl.path("candleType").asText());
|
||||
assertEquals("3m", buyDsl.path("candleTypes").get(0).asText());
|
||||
Map<String, BarSeries> seriesOverrides = OhlcvBarSeriesSupport.buildSeriesOverrides(
|
||||
primarySeries, chartTf, buyDsl, null);
|
||||
|
||||
Map<String, Map<String, Object>> params = Map.of(
|
||||
"CCI", Map.of("length", 20, "maLength", 10, "maType", "SMA", "src", "hlc3"));
|
||||
|
||||
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
||||
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||
primarySeries, params, Map.of(), "KRW-BTC", null, false, seriesOverrides);
|
||||
Rule rule = adapter.toRule(buyDsl, ctx);
|
||||
|
||||
int hits = 0;
|
||||
for (int i = primarySeries.getBeginIndex(); i <= primarySeries.getEndIndex(); i++) {
|
||||
if (rule.isSatisfied(i, null)) hits++;
|
||||
}
|
||||
assertTrue(hits > 0, "5m strategy on 3m chart should detect CCI crosses after remap, got " + hits);
|
||||
}
|
||||
|
||||
@Test
|
||||
void chartScan_5mNativeOn3mChart_aggregatedSeries_firesSignals() throws Exception {
|
||||
String chartTf = "3m";
|
||||
List<OhlcvBar> bars = buildOscillatingOhlcvBars(150, chartTf);
|
||||
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(bars, chartTf);
|
||||
|
||||
// remap 없음 — 5m TIMEFRAME + 3m 차트 (CrossSeriesRule 집계 경로)
|
||||
JsonNode buyDsl = MAPPER.readTree("""
|
||||
{
|
||||
"type": "TIMEFRAME",
|
||||
"candleTypes": ["5m"],
|
||||
"candleType": "5m",
|
||||
"children": [{
|
||||
"type": "CONDITION",
|
||||
"condition": {
|
||||
"indicatorType": "CCI",
|
||||
"conditionType": "CROSS_UP",
|
||||
"leftField": "CCI_VALUE_20",
|
||||
"rightField": "CCI_SIGNAL",
|
||||
"period": 20,
|
||||
"valuePeriodOverride": true
|
||||
}
|
||||
}]
|
||||
}
|
||||
""");
|
||||
Map<String, BarSeries> seriesOverrides = OhlcvBarSeriesSupport.buildSeriesOverrides(
|
||||
primarySeries, chartTf, buyDsl, null);
|
||||
|
||||
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
||||
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||
primarySeries, Map.of("CCI", Map.of("length", 20, "maLength", 10, "maType", "SMA")),
|
||||
Map.of(), "KRW-BTC", null, false, seriesOverrides);
|
||||
Rule rule = adapter.toRule(buyDsl, ctx);
|
||||
|
||||
int hits = 0;
|
||||
for (int i = primarySeries.getBeginIndex(); i <= primarySeries.getEndIndex(); i++) {
|
||||
if (rule.isSatisfied(i, null)) hits++;
|
||||
}
|
||||
assertTrue(hits > 0, "5m native on 3m chart via aggregation should fire, got " + hits);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cciValueField_period20_matchesValue20Suffix() throws Exception {
|
||||
BarSeries series = buildOscillatingOhlcvBars(150, "3m").stream()
|
||||
.collect(java.util.stream.Collectors.collectingAndThen(
|
||||
java.util.stream.Collectors.toList(),
|
||||
list -> OhlcvBarSeriesSupport.buildSeries(list, "3m")));
|
||||
Map<String, Object> p = Map.of("length", 13, "maLength", 10, "maType", "SMA", "src", "hlc3");
|
||||
|
||||
JsonNode condPlain = MAPPER.readTree("""
|
||||
{"period":20,"leftField":"CCI_VALUE","rightField":"CCI_SIGNAL","valuePeriodOverride":true}
|
||||
""");
|
||||
JsonNode condSuffix = MAPPER.readTree("""
|
||||
{"period":20,"leftField":"CCI_VALUE_20","rightField":"CCI_SIGNAL","valuePeriodOverride":true}
|
||||
""");
|
||||
|
||||
var plainValue = adapter.resolveIndicatorFieldForTest(
|
||||
"CCI_VALUE", "CCI", p, series, 20, -1, condPlain);
|
||||
var suffixValue = adapter.resolveIndicatorFieldForTest(
|
||||
"CCI_VALUE_20", "CCI", p, series, 20, -1, condSuffix);
|
||||
var plainSignal = adapter.resolveIndicatorFieldForTest(
|
||||
"CCI_SIGNAL", "CCI", p, series, 20, -1, condPlain);
|
||||
var suffixSignal = adapter.resolveIndicatorFieldForTest(
|
||||
"CCI_SIGNAL", "CCI", p, series, 20, -1, condSuffix);
|
||||
|
||||
int idx = 80;
|
||||
assertEquals(suffixValue.getValue(idx).doubleValue(), plainValue.getValue(idx).doubleValue(), 1e-6,
|
||||
"CCI_VALUE+period must match CCI_VALUE_20");
|
||||
assertEquals(suffixSignal.getValue(idx).doubleValue(), plainSignal.getValue(idx).doubleValue(), 1e-6,
|
||||
"CCI_SIGNAL must match for both left field forms");
|
||||
assertNotEquals(suffixValue.getValue(idx).doubleValue(), suffixSignal.getValue(idx).doubleValue(), 1e-6,
|
||||
"CCI_SIGNAL must differ from raw CCI at index " + idx);
|
||||
}
|
||||
|
||||
@Test
|
||||
void chartScan_cciValueFieldWithPeriod20_firesSignals() throws Exception {
|
||||
String chartTf = "3m";
|
||||
List<OhlcvBar> bars = buildOscillatingOhlcvBars(150, chartTf);
|
||||
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(bars, chartTf);
|
||||
|
||||
JsonNode buyDsl = MAPPER.readTree("""
|
||||
{
|
||||
"type": "TIMEFRAME",
|
||||
"candleTypes": ["3m"],
|
||||
"candleType": "3m",
|
||||
"children": [{
|
||||
"type": "CONDITION",
|
||||
"condition": {
|
||||
"indicatorType": "CCI",
|
||||
"conditionType": "CROSS_UP",
|
||||
"leftField": "CCI_VALUE",
|
||||
"rightField": "CCI_SIGNAL",
|
||||
"period": 20,
|
||||
"valuePeriodOverride": true
|
||||
}
|
||||
}]
|
||||
}
|
||||
""");
|
||||
Map<String, BarSeries> seriesOverrides = OhlcvBarSeriesSupport.buildSeriesOverrides(
|
||||
primarySeries, chartTf, buyDsl, null);
|
||||
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
||||
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||
primarySeries, Map.of("CCI", Map.of("length", 13, "maLength", 10, "maType", "SMA")),
|
||||
Map.of(), null, null, false, seriesOverrides);
|
||||
Rule rule = adapter.toRule(buyDsl, ctx);
|
||||
|
||||
int hits = 0;
|
||||
for (int i = primarySeries.getBeginIndex(); i <= primarySeries.getEndIndex(); i++) {
|
||||
if (rule.isSatisfied(i, null)) hits++;
|
||||
}
|
||||
assertTrue(hits > 0, "CCI_VALUE + period=20 must use period not global length=13, got " + hits);
|
||||
}
|
||||
|
||||
private static List<OhlcvBar> buildOscillatingOhlcvBars(int count, String tf) {
|
||||
long tfSec = switch (OhlcvBarSeriesSupport.normalizeTf(tf)) {
|
||||
case "3m" -> 180;
|
||||
|
||||
Reference in New Issue
Block a user