diff --git a/src/main/kotlin/analyzer/TechnicalAnalyzer.kt b/src/main/kotlin/analyzer/TechnicalAnalyzer.kt index ee973aa..6a40189 100644 --- a/src/main/kotlin/analyzer/TechnicalAnalyzer.kt +++ b/src/main/kotlin/analyzer/TechnicalAnalyzer.kt @@ -254,31 +254,61 @@ class TechnicalAnalyzer { } /** - * 억울한 HOLD를 막아주는 유연한 과열 판별 로직 + * 억울한 HOLD를 막아주는 유연한 과열 판별 로직 (주가 및 변동성 기반 세분화) */ fun isOverheatedStock(): Boolean { if (daily.size < 20) return false val currentPrice = daily.last().stck_prpr.toDouble() - // 1. 일봉 20일선 이격도 + // 1. 기본 지표 계산 val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average() val disparityDaily = (currentPrice / ma20Daily) * 100 - - // 2. 일봉 RSI (단순 이격도 외의 과열 여부 확인) val rsiDaily = calculateRSI(daily) - // 3. 초단기(최근 10분) 순간 급등 꼭지 확인 (min30 재활용) + // 2. 가격대별(시가총액 및 호가단위 대용) 동적 임계값 세팅 + val maxDisparity: Double // 절대 진입 금지 (초과열) + val warningDisparity: Double // 경고 수준 (보조지표 결합 시 차단) + val microPumpThreshold: Double // 10분 내 단기 펌핑 기준 + + when { + currentPrice >= 50000.0 -> { + // [대형주/우량주] 무거워서 20일선 대비 15%만 떠도 역사적 과열 + maxDisparity = 115.0 + warningDisparity = 110.0 + microPumpThreshold = 103.0 // 10분 만에 3% 급등하면 꼭지 + } + currentPrice in 5000.0..49999.0 -> { + // [일반 중소형주] 표준적인 변동성 + maxDisparity = 125.0 + warningDisparity = 115.0 + microPumpThreshold = 105.0 // 10분 만에 5% 급등 + } + else -> { + // [소형주/동전주] 가벼워서 상한가 한 방에 130% 쉽게 도달 + maxDisparity = 135.0 + warningDisparity = 120.0 + microPumpThreshold = 107.0 // 10분 만에 7% 급등 + } + } + + // 3. 종목 고유 변동성(ATR)을 통한 임계값 미세 보정 (Smart Adjustment) + // 평소 하루에 2~3% 움직이는 얌전한 주식이 갑자기 튀면 임계값을 더 빡빡하게 죔 + val atrPct = (calculateATR(daily) / currentPrice) * 100.0 + val adjustedMaxDisparity = if (atrPct < 3.0) maxDisparity * 0.96 else maxDisparity + val adjustedWarnDisparity = if (atrPct < 3.0) warningDisparity * 0.97 else warningDisparity + + // 4. 초단기(최근 10분) 순간 급등 꼭지 확인 (min30 재활용) var isMicroOverheated = false if (min30.size >= 10) { val ma10Min = min30.takeLast(10).map { it.stck_prpr.toDouble() }.average() val disparityMin = (currentPrice / ma10Min) * 100 - isMicroOverheated = disparityMin > 105.0 // 10분 평균가 대비 순간적으로 5% 이상 폭등 시 + isMicroOverheated = disparityMin > microPumpThreshold } - // 단순히 이격도 115%라고 막는 것이 아니라 3가지 깐깐한 조건 중 하나라도 충족될 때만 과열 판정 - return disparityDaily > 130.0 || // (A) 역대급 폭등 상태 - (disparityDaily > 115.0 && rsiDaily > 75.0) || // (B) 급등 중이면서 일봉 과매수 - (disparityDaily > 115.0 && isMicroOverheated) // (C) 급등 중이면서 10분 내 순간 펌핑(꼭지) + // 5. 최종 세분화 판정 로직 + return disparityDaily > adjustedMaxDisparity || // (A) 종목 체급 대비 역대급 폭등 상태 + (disparityDaily > adjustedWarnDisparity && rsiDaily > 75.0) || // (B) 체급별 경고 수준 + 일봉 과매수 + (disparityDaily > adjustedWarnDisparity && isMicroOverheated) // (C) 체급별 경고 수준 + 초단기 펌핑(꼭지) } fun calculateScores(financialScore100: Int): InvestmentScores {