9일 신고가 매수 조건 오류
This commit is contained in:
@@ -20,6 +20,12 @@ public final class IndicatorHlineResolver {
|
|||||||
|
|
||||||
if (field == null || field.isBlank()) return null;
|
if (field == null || field.isBlank()) return null;
|
||||||
|
|
||||||
|
// 지표 시리즈 필드 — 접미 숫자를 임계값 상수로 오인하지 않음 (NH_PRIOR_9 → 9.0 버그 방지)
|
||||||
|
if (field.startsWith("NH_PRIOR_") || field.startsWith("NL_PRIOR_")
|
||||||
|
|| field.startsWith("DC_UPPER_") || field.startsWith("DC_LOWER_") || field.startsWith("DC_MIDDLE_")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false);
|
boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false);
|
||||||
|
|
||||||
// K_* = 직접입력 숫자 스냅샷 — hline 역할로 치환하지 않음 (K_55 → 55)
|
// K_* = 직접입력 숫자 스냅샷 — hline 역할로 치환하지 않음 (K_55 → 55)
|
||||||
@@ -54,6 +60,10 @@ public final class IndicatorHlineResolver {
|
|||||||
private static boolean isThresholdRoleField(String field) {
|
private static boolean isThresholdRoleField(String field) {
|
||||||
if (field == null || field.isBlank()) return false;
|
if (field == null || field.isBlank()) return false;
|
||||||
if (field.startsWith("K_")) return true;
|
if (field.startsWith("K_")) return true;
|
||||||
|
if (field.startsWith("NH_PRIOR_") || field.startsWith("NL_PRIOR_")
|
||||||
|
|| field.startsWith("DC_UPPER_") || field.startsWith("DC_LOWER_") || field.startsWith("DC_MIDDLE_")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if ("HL_OVER".equals(field) || "HL_MID".equals(field) || "HL_UNDER".equals(field)) {
|
if ("HL_OVER".equals(field) || "HL_MID".equals(field) || "HL_UNDER".equals(field)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,4 +68,14 @@ class IndicatorHlineResolverTest {
|
|||||||
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
||||||
null, "", "RSI", Map.of()));
|
null, "", "RSI", Map.of()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nhPriorField_notParsedAsNumericConstant() {
|
||||||
|
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
null, "NH_PRIOR_9", "NEW_HIGH", Map.of()));
|
||||||
|
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
null, "NL_PRIOR_20", "NEW_LOW", Map.of()));
|
||||||
|
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
null, "DC_UPPER_9", "DONCHIAN", Map.of()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,8 +173,56 @@ class PriceExtremeRuleTest {
|
|||||||
assertFalse(hits.isEmpty(), "하락 추세에서 9·20일 신저가 복합 매도 조건이 최소 1회 충족되어야 함");
|
assertFalse(hits.isEmpty(), "하락 추세에서 9·20일 신저가 복합 매도 조건이 최소 1회 충족되어야 함");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nhPrior_crossUp_firesOnRealPriceBreakout_notConstantNine() throws Exception {
|
||||||
|
BarSeries series = buildFlatThenBreakoutSeries(97_000_000.0, 97_500_000.0, 15);
|
||||||
|
Rule buy = rule(series, """
|
||||||
|
{ "type": "CONDITION", "condition": {
|
||||||
|
"indicatorType": "NEW_HIGH",
|
||||||
|
"conditionType": "CROSS_UP",
|
||||||
|
"leftField": "CLOSE_PRICE",
|
||||||
|
"rightField": "NH_PRIOR_9",
|
||||||
|
"period": 9
|
||||||
|
}}
|
||||||
|
""");
|
||||||
|
|
||||||
|
List<Integer> hits = satisfiedIndices(buy, series);
|
||||||
|
assertFalse(hits.isEmpty(),
|
||||||
|
"97M대 종가가 9일 신고가선 상향 돌파 시 CROSS_UP — NH_PRIOR_9를 상수 9로 해석하면 영원히 false");
|
||||||
|
assertTrue(hits.stream().anyMatch(i -> i >= 9),
|
||||||
|
"9봉 워밍업 이후 돌파 봉에서만 시그널");
|
||||||
|
}
|
||||||
|
|
||||||
// ── helpers ─────────────────────────────────────────────────────────────
|
// ── helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** 횡보 후 한 봉에 종가 돌파 — NH_PRIOR CROSS_UP 검증용 */
|
||||||
|
private static BarSeries buildFlatThenBreakoutSeries(double flatPrice, double breakoutClose, int flatBars) {
|
||||||
|
Duration period = Duration.ofMinutes(3);
|
||||||
|
BarSeries series = new BaseBarSeriesBuilder()
|
||||||
|
.withName("flat-breakout")
|
||||||
|
.withNumFactory(DoubleNumFactory.getInstance())
|
||||||
|
.withBarBuilderFactory(new TimeBarBuilderFactory())
|
||||||
|
.build();
|
||||||
|
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
|
||||||
|
Instant base = Instant.parse("2024-01-01T00:00:00Z");
|
||||||
|
for (int i = 0; i < flatBars; i++) {
|
||||||
|
factory.createBarBuilder(series)
|
||||||
|
.timePeriod(period)
|
||||||
|
.endTime(base.plus(period.multipliedBy(i + 1L)))
|
||||||
|
.openPrice(flatPrice).highPrice(flatPrice + 1000).lowPrice(flatPrice - 1000).closePrice(flatPrice)
|
||||||
|
.volume(1000)
|
||||||
|
.add();
|
||||||
|
}
|
||||||
|
int i = flatBars;
|
||||||
|
factory.createBarBuilder(series)
|
||||||
|
.timePeriod(period)
|
||||||
|
.endTime(base.plus(period.multipliedBy(i + 1L)))
|
||||||
|
.openPrice(flatPrice).highPrice(breakoutClose + 500).lowPrice(flatPrice - 500).closePrice(breakoutClose)
|
||||||
|
.volume(5000)
|
||||||
|
.add();
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
private Rule rule(BarSeries series, String json) throws Exception {
|
private Rule rule(BarSeries series, String json) throws Exception {
|
||||||
JsonNode node = MAPPER.readTree(json);
|
JsonNode node = MAPPER.readTree(json);
|
||||||
return adapter.toRule(node, series, Map.of());
|
return adapter.toRule(node, series, Map.of());
|
||||||
|
|||||||
Reference in New Issue
Block a user