9일 신고가 매수 조건 오류

This commit is contained in:
Macbook
2026-06-22 12:49:36 +09:00
parent 2299381403
commit 53747b8773
3 changed files with 68 additions and 0 deletions
@@ -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());