This commit is contained in:
2026-04-08 14:18:09 +09:00
parent b95c1d5f72
commit 6494784bbc
7 changed files with 471 additions and 274 deletions
+59 -17
View File
@@ -12,7 +12,7 @@ data class InvestmentScores(
val ultraShort: Int, // 초단기 (분봉/에너지)
val shortTerm: Int, // 단기 (일봉/뉴스)
val midTerm: Int, // 중기 (주봉/재무)
val longTerm: Int // 장기 (월봉/펀더멘털)
val longTerm: Int // 장기 (월봉/펀더멘털)
) {
override fun toString(): String {
return """
@@ -28,15 +28,16 @@ data class InvestmentScores(
@Serializable
class TechnicalAnalyzer {
var monthly: List<CandleData> = emptyList()
var weekly: List<CandleData> = emptyList()
var daily: List<CandleData> = emptyList()
// 주의: min30은 '30분봉'이 아니라 '1분 단위 캔들 30개'를 의미합니다.
var min30: List<CandleData> = emptyList()
var daily: List<CandleData> = emptyList()
var weekly: List<CandleData> = emptyList()
var monthly: List<CandleData> = emptyList()
fun isValid() = listOf(min30, monthly, weekly, daily).all { it.isNotEmpty() }
/**
* [신규] 기술적 지표와 추세 결합한 종합 신호 생성
* 기술적 지표와 추세, 그리고 초단기(Micro) 흐름을 결합한 종합 신호 생성
*/
fun generateComprehensiveSignal(): ScalpingSignalModel {
val scalpingAnalyzer = ScalpingAnalyzer()
@@ -44,10 +45,9 @@ class TechnicalAnalyzer {
// 1. 기본 스캘핑 신호 생성
val baseSignal = scalpingAnalyzer.analyze(min30.toScalpingList(), dailyBullish)
// 2. 점수 정교화 (가점/감점 요인)
var refinedScore = baseSignal.compositeScore.toDouble()
// 2. 점수 정교화 (가점/감점 요인)
// [보완] 추세 동기화 가점: 월/주/일봉이 모두 상승 추세일 때
if (calculateChange(monthly) > 0 && calculateChange(weekly) > 0 && calculateChange(daily.takeLast(5)) > 0) {
refinedScore += 10.0
@@ -67,20 +67,64 @@ class TechnicalAnalyzer {
val bodyRange = abs(lastCandle.stck_prpr.toDouble() - lastCandle.stck_oprc.toDouble())
if (bodyRange > atr * 1.2) refinedScore += 7.0
// 🚀 [마이크로 분석] 기존 min30 리스트를 재활용하여 최근 5분간의 초단기 흐름 분석
if (min30.size >= 15) {
val last5Candles = min30.takeLast(5) // 최근 5분(5개 캔들)
// ① 초단기 추세 가감점 (최근 5분간 변화율)
val microChange = calculateChange(last5Candles)
if (microChange > 1.5) refinedScore += 6.0 // 순간 급등세 (수급 유입)
else if (microChange < -1.5) refinedScore -= 12.0 // 순간 투매 방어 (강력 감점)
// ② 초단기 거래량 급증 (V-Spike) 확인
val recentVolume = last5Candles.map { it.cntg_vol.toDouble() }.average()
val pastVolume = min30.dropLast(5).takeLast(10).map { it.cntg_vol.toDouble() }.average() // 그 이전 10분 평균
if (pastVolume > 0 && recentVolume > pastVolume * 2.5) {
// 평소보다 거래량이 2.5배 터졌을 때, 양봉이면 매수세 폭발, 음봉이면 쏟아내는 투매 물량
val isBullishMicro = lastCandle.stck_prpr.toDouble() >= lastCandle.stck_oprc.toDouble()
if (isBullishMicro) refinedScore += 8.0
else refinedScore -= 10.0
}
// ③ 초단기 RSI 과열/과매도 필터링
val rsiMicro = calculateRSI(last5Candles)
if (rsiMicro > 75.0) refinedScore -= 8.0 // 초단기 꼭지 (추격매수 방지)
else if (rsiMicro < 25.0) refinedScore += 5.0 // 초단기 투매 과도 (기술적 반등 타점 노림)
}
return baseSignal.copy(
compositeScore = refinedScore.coerceIn(0.0, 100.0).toInt(),
successProbPct = (refinedScore * 0.85).coerceAtMost(98.0)
)
}
/**
* 억울한 HOLD를 막아주는 유연한 과열 판별 로직
*/
fun isOverheatedStock(): Boolean {
if (min30.size < 20 || daily.size < 20) return false
val currentPrice = min30.last().stck_prpr.toDouble()
if (daily.size < 20) return false
val currentPrice = daily.last().stck_prpr.toDouble()
// 1. 일봉 20일선 이격도
val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average()
val disparityDaily = (currentPrice / ma20Daily) * 100
// 이격도 115% 이상이면 주의, 125% 이상이면 과열
return disparityDaily > 115.0
// 2. 일봉 RSI (단순 이격도 외의 과열 여부 확인)
val rsiDaily = calculateRSI(daily)
// 3. 초단기(최근 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% 이상 폭등 시
}
// 단순히 이격도 115%라고 막는 것이 아니라 3가지 깐깐한 조건 중 하나라도 충족될 때만 과열 판정
return disparityDaily > 130.0 || // (A) 역대급 폭등 상태
(disparityDaily > 115.0 && rsiDaily > 75.0) || // (B) 급등 중이면서 일봉 과매수
(disparityDaily > 115.0 && isMicroOverheated) // (C) 급등 중이면서 10분 내 순간 펌핑(꼭지)
}
fun calculateScores(financialScore100: Int): InvestmentScores {
@@ -100,7 +144,7 @@ class TechnicalAnalyzer {
)
}
// --- 유틸리티 함수군 (기존 로직 유지 및 보완) ---
// --- 이하 유틸리티 함수군 (변경 없음) ---
fun calculateATR(candles: List<CandleData>, period: Int = 14): Double {
if (candles.size < period + 1) return 0.0
val sub = candles.takeLast(period + 1)
@@ -170,14 +214,14 @@ class TechnicalAnalyzer {
val signal = generateComprehensiveSignal()
val currentPrice = min30.last().stck_prpr.toDouble()
// [표준화된 분석 점수] - AI에게 가이드라인 제공
// [표준화된 분석 점수]
val standardizedScores = """
- Financial Health Score: $finScore100 / 100
- Technical Momentum Score: ${signal.compositeScore} / 100
- Market Energy (Volume): ${"%.1f".format(signal.volRatio)}x relative to avg
""".trimIndent()
// [시계열 가격 흐름] - AI에게 지지와 저항 맥락 제공
// [시계열 가격 흐름]
val monthlyRange = monthly.takeLast(3).joinToString(" -> ") {
"[H:${it.stck_hgpr}, L:${it.stck_lwpr}]"
}
@@ -188,15 +232,13 @@ class TechnicalAnalyzer {
return """
# [Standardized Analysis Summary]
$standardizedScores
# [Historical Price Range]
- Monthly (Last 3M): $monthlyRange
- Weekly (Last 4W): $weeklyRange
- Current Price: $currentPrice
# [Technical Context]
- Base Position: ${"%.1f".format((currentPrice / daily.takeLast(120).map { it.stck_prpr.toDouble() }.average()) * 100)}% (120MA)
- RSI (Daily): ${"%.1f".format(calculateRSI(daily))}
""".trimIndent()
}
}
}