package analyzer import kotlinx.serialization.Serializable import model.CandleData import kotlin.math.abs import kotlin.text.toDouble import kotlin.text.toInt data class InvestmentScores( val ultraShort: Int, // 초단기 (분봉/에너지) val shortTerm: Int, // 단기 (일봉/뉴스) val midTerm: Int, // 중기 (주봉/재무) val longTerm: Int // 장기 (월봉/펀더멘털) ) { override fun toString(): String { return """ 평점 : ${avg()} 초단 : $ultraShort 단기 : $shortTerm 중기 : $midTerm 장기 : $longTerm """.trimIndent() } fun avg() = listOf(ultraShort, shortTerm, midTerm, longTerm).average() } @Serializable class TechnicalAnalyzer { var monthly: List = emptyList() var weekly: List = emptyList() var daily: List = emptyList() var min30: List = emptyList() fun isValid() = listOf(min30, monthly, weekly, daily).all { it.isNotEmpty() } /** * [신규] 기술적 지표와 추세를 결합한 종합 신호 생성 */ fun generateComprehensiveSignal(): ScalpingSignalModel { val scalpingAnalyzer = ScalpingAnalyzer() val dailyBullish = isDailyBullish() // 1. 기본 스캘핑 신호 생성 val baseSignal = scalpingAnalyzer.analyze(min30.toScalpingList(), dailyBullish) // 2. 점수 정교화 (가점/감점 요인) var refinedScore = baseSignal.compositeScore.toDouble() // [보완] 추세 동기화 가점: 월/주/일봉이 모두 상승 추세일 때 if (calculateChange(monthly) > 0 && calculateChange(weekly) > 0 && calculateChange(daily.takeLast(5)) > 0) { refinedScore += 10.0 } // [보완] 자금 유입 강도(MFI) 반영 val mfi = calculateMFI(min30) when { mfi > 80.0 -> refinedScore -= 15.0 // 과매수 권역 감점 mfi < 20.0 -> refinedScore -= 5.0 // 자금 유출 감점 mfi in 45.0..65.0 -> refinedScore += 5.0 // 안정적 수급 구간 } // [보완] 변동성 돌파 확인 (ATR 대비 현재 몸통 크기) val atr = calculateATR(min30) val lastCandle = min30.last() val bodyRange = abs(lastCandle.stck_prpr.toDouble() - lastCandle.stck_oprc.toDouble()) if (bodyRange > atr * 1.2) refinedScore += 7.0 return baseSignal.copy( compositeScore = refinedScore.coerceIn(0.0, 100.0).toInt(), successProbPct = (refinedScore * 0.85).coerceAtMost(98.0) ) } fun isOverheatedStock(): Boolean { if (min30.size < 20 || daily.size < 20) return false val currentPrice = min30.last().stck_prpr.toDouble() val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average() val disparityDaily = (currentPrice / ma20Daily) * 100 // 이격도 115% 이상이면 주의, 125% 이상이면 과열 return disparityDaily > 115.0 } fun calculateScores(financialScore100: Int): InvestmentScores { val signal = generateComprehensiveSignal() // 이미 100점 만점 기반 // 모든 지표를 100점 스케일 내에서 조합 val ultra = signal.compositeScore val short = (calculateRSI(daily) * 0.5 + (if(calculateOBV(daily) > 0) 50 else 0)).toInt() val mid = (if(calculateChange(weekly) > 0) 60 else 20) + (financialScore100 * 0.4).toInt() val long = (if(calculateChange(monthly) > 0) 50 else 10) + (financialScore100 * 0.5).toInt() return InvestmentScores( ultraShort = ultra.coerceIn(0, 100), shortTerm = short.coerceIn(0, 100), midTerm = mid.coerceIn(0, 100), longTerm = long.coerceIn(0, 100) ) } // --- 유틸리티 함수군 (기존 로직 유지 및 보완) --- fun calculateATR(candles: List, period: Int = 14): Double { if (candles.size < period + 1) return 0.0 val sub = candles.takeLast(period + 1) val trList = mutableListOf() for (i in 1 until sub.size) { val high = sub[i].stck_hgpr.toDouble() val low = sub[i].stck_lwpr.toDouble() val prevClose = sub[i - 1].stck_prpr.toDouble() trList.add(maxOf(high - low, abs(high - prevClose), abs(low - prevClose))) } return trList.average() } fun calculateMFI(candles: List, period: Int = 14): Double { if (candles.size < period + 1) return 50.0 val subList = candles.takeLast(period + 1) var posFlow = 0.0 var negFlow = 0.0 for (i in 1 until subList.size) { val prevTyp = (subList[i-1].stck_hgpr.toDouble() + subList[i-1].stck_lwpr.toDouble() + subList[i-1].stck_prpr.toDouble()) / 3 val currTyp = (subList[i].stck_hgpr.toDouble() + subList[i].stck_lwpr.toDouble() + subList[i].stck_prpr.toDouble()) / 3 val flow = currTyp * subList[i].cntg_vol.toDouble() if (currTyp > prevTyp) posFlow += flow else if (currTyp < prevTyp) negFlow += flow } return if (negFlow == 0.0) 100.0 else 100 - (100 / (1 + (posFlow / negFlow))) } fun calculateRSI(list: List): Double { if (list.size < 2) return 50.0 var gains = 0.0 var losses = 0.0 for (i in 1 until list.size) { val diff = list[i].stck_prpr.toDouble() - list[i-1].stck_prpr.toDouble() if (diff > 0) gains += diff else losses -= diff } return if (gains + losses == 0.0) 50.0 else (gains / (gains + losses)) * 100 } fun calculateOBV(candles: List): Double { var obv = 0.0 for (i in 1 until candles.size) { val prevClose = candles[i-1].stck_prpr.toDouble() val currClose = candles[i].stck_prpr.toDouble() if (currClose > prevClose) obv += candles[i].cntg_vol.toDouble() else if (currClose < prevClose) obv -= candles[i].cntg_vol.toDouble() } return obv } fun calculateChange(list: List): Double { if (list.isEmpty()) return 0.0 val start = list.first().stck_oprc.toDouble() val end = list.last().stck_prpr.toDouble() return if (start != 0.0) ((end - start) / start) * 100 else 0.0 } fun isDailyBullish(): Boolean { if (daily.size < 20) return true val currentPrice = daily.last().stck_prpr.toDouble() val ma20 = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average() val ma5 = daily.takeLast(5).map { it.stck_prpr.toDouble() }.average() val prevMa5 = daily.dropLast(1).takeLast(5).map { it.stck_prpr.toDouble() }.average() return currentPrice > ma20 && ma5 > prevMa5 } fun generateComprehensiveReport(finScore100: Int): String { 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}]" } val weeklyRange = weekly.takeLast(4).joinToString(" -> ") { "[H:${it.stck_hgpr}, L:${it.stck_lwpr}]" } 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() } }