빨라져라

This commit is contained in:
2026-04-07 17:32:21 +09:00
parent a700d54dfe
commit 9172cca791
24 changed files with 3337 additions and 3253 deletions
@@ -0,0 +1,206 @@
package analyzer
import kotlin.compareTo
object FinancialAnalyzer {
/**
* [매수 고려] 우량 기업 요건 확인
* 모든 조건 충족 시 적극적인 분석(AI/차트) 단계로 진입합니다.
*/
fun isSafetyBeltMet(fs: FinancialStatement): Boolean {
// 1. 유동성 위기 체크 (이건 유지하는 것이 좋습니다)
val isDebtSafe = fs.debtRatio < 300.0 // 200% -> 300%로 완화
val isLiquiditySafe = fs.quickRatio > 60.0 // 80% -> 60%로 완화 (급전이 필요한 수준만 차단)
// 2. 턴어라운드 허용 (적자여도 개선 중이면 통과)
val isTurningAround = !fs.isNetIncomePositive && fs.operatingProfitGrowth > 50.0
val isNotFatalDeficit = fs.isNetIncomePositive || isTurningAround
// 3. 상장폐지 요건 중심 필터
val isNotCapitalImpaired = fs.capitalImpairmentRate < 50.0 // 자본잠식 50% 이상 차단
val isNotLossExploding = fs.lossToSalesRatio < 150.0 // 매출보다 손실이 너무 크면 차단
// 최종: 정말 위험한 경우가 아니면 분석 단계(AI/뉴스)로 보냄
return isDebtSafe && isLiquiditySafe && isNotFatalDeficit && isNotCapitalImpaired
}
fun toString(fs : FinancialStatement): String {
var buffer = StringBuffer()
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
val isNotCrashing = fs.netIncomeGrowth > -40.0
if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) {
if (!isDebtSafe)buffer.appendLine( "부채비율 200% 이상")
if (!isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만")
if (!isNotDeficit)buffer.appendLine( "당기순이익 적자")
if (!isNotCrashing) { buffer.appendLine("당기순이익 급감(${String.format("%.1f", fs.netIncomeGrowth)}%)") }
buffer.appendLine("최소 기준 미달")
} else {
buffer.appendLine("최소 기준 충족")
}
val highProfitability = fs.roe >= 10.0 // ROE 10% 이상
val strongGrowth = fs.netIncomeGrowth >= 15.0 // 이익 성장률 15% 이상
val verySafeDebt = fs.debtRatio <= 100.0 // 부채비율 100% 이하 (안전)
val goodLiquidity = fs.quickRatio >= 120.0 // 당좌비율 120% 이상 (여유)
val businessHealthy = fs.isOperatingProfitPositive // 본업(영업이익)이 흑자
if ((highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy) == false) {
if(!highProfitability) buffer.appendLine( "ROE 10% 미만")
if(!strongGrowth) buffer.appendLine( "이익 성장률 15% 미만")
if(!verySafeDebt) buffer.appendLine( "부채비율 100% 이상 (안전성 미달)")
if(!goodLiquidity) buffer.appendLine( "당좌비율 120% 이하 (여유 없음)")
if(!businessHealthy) buffer.appendLine( "본업(영업이익)이 적자")
buffer.appendLine("재무 건전성 및 성장성 미달")
} else {
buffer.appendLine("재무 건전성 및 성장성 충족")
}
return buffer.toString()
}
fun calculateScore(fs: FinancialStatement): Int {
var score = 50.0 // 중립 시작
// 1. 수익성 및 성장 추세 (Max 50)
score += when {
fs.isOperatingProfitPositive && fs.operatingProfitGrowth > 20.0 -> 30.0 // 우량 성장
fs.isOperatingProfitPositive && fs.operatingProfitGrowth < -30.0 -> -20.0 // 쇠퇴 위험
!fs.isOperatingProfitPositive && fs.operatingProfitGrowth > 50.0 -> 20.0 // 턴어라운드 신호
!fs.isOperatingProfitPositive && fs.operatingProfitGrowth < -20.0 -> -30.0 // 적자 심화
else -> 0.0
}
// 2. 수익 효율 ROE (Max 30)
score += (fs.roe.coerceIn(-20.0, 20.0) * 1.5)
// 3. 안정성 (Max 20)
if (fs.debtRatio <= 100.0) score += 20.0
else if (fs.debtRatio <= 150.0) score += 10.0
// 4. 감점 페널티 (위험 징후 시 최대 -50)
if (fs.capitalImpairmentRate > 20.0) score -= 30.0
if (fs.debtAccelerationRate > 100.0) score -= 20.0
return score.coerceIn(0.0, 100.0).toInt()
}
/**
* 상황별 상태 메시지 정교화
*/
fun getInvestmentStatus(fs: FinancialStatement): String {
return when {
fs.isOperatingProfitPositive && fs.operatingProfitGrowth > 10.0 -> "🚀 [성장중] 실적 개선세 뚜렷"
!fs.isOperatingProfitPositive && fs.operatingProfitGrowth > 40.0 -> "☀️ [회복중] 적자폭 급감, 턴어라운드 가시화"
fs.isOperatingProfitPositive && fs.operatingProfitGrowth < -40.0 -> "⚠️ [쇠퇴중] 이익 급감, 적자전환 유의"
else -> "🚨 [부실] 재무 구조 악화 지속"
}
}
}
object FinancialAnalyzer2 {
fun isSafetyBeltMet(fs: FinancialStatement): Boolean {
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
val isNotCrashing = fs.netIncomeGrowth > -40.0
return isDebtSafe && isLiquiditySafe && isNotDeficit && isNotCrashing
}
/**
* [매수 고려] 우량 기업 요건 확인
* 모든 조건 충족 시 적극적인 분석(AI/차트) 단계로 진입합니다.
*/
fun isBuyConsiderationMet(fs: FinancialStatement): Boolean {
val highProfitability = fs.roe >= 10.0 // ROE 10% 이상
val strongGrowth = fs.netIncomeGrowth >= 15.0 // 이익 성장률 15% 이상
val verySafeDebt = fs.debtRatio <= 100.0 // 부채비율 100% 이하 (안전)
val goodLiquidity = fs.quickRatio >= 120.0 // 당좌비율 120% 이상 (여유)
val businessHealthy = fs.isOperatingProfitPositive // 본업(영업이익)이 흑자
return highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy
}
fun toString(fs : FinancialStatement): String {
var buffer = StringBuffer()
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
val isNotCrashing = fs.netIncomeGrowth > -40.0
if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) {
if (!isDebtSafe)buffer.appendLine( "부채비율 200% 이상")
if (!isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만")
if (!isNotDeficit)buffer.appendLine( "당기순이익 적자")
if (!isNotCrashing) { buffer.appendLine("당기순이익 급감(${String.format("%.1f", fs.netIncomeGrowth)}%)") }
buffer.appendLine("최소 기준 미달")
} else {
buffer.appendLine("최소 기준 충족")
}
val highProfitability = fs.roe >= 10.0 // ROE 10% 이상
val strongGrowth = fs.netIncomeGrowth >= 15.0 // 이익 성장률 15% 이상
val verySafeDebt = fs.debtRatio <= 100.0 // 부채비율 100% 이하 (안전)
val goodLiquidity = fs.quickRatio >= 120.0 // 당좌비율 120% 이상 (여유)
val businessHealthy = fs.isOperatingProfitPositive // 본업(영업이익)이 흑자
if ((highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy) == false) {
if(!highProfitability) buffer.appendLine( "ROE 10% 미만")
if(!strongGrowth) buffer.appendLine( "이익 성장률 15% 미만")
if(!verySafeDebt) buffer.appendLine( "부채비율 100% 이상 (안전성 미달)")
if(!goodLiquidity) buffer.appendLine( "당좌비율 120% 이하 (여유 없음)")
if(!businessHealthy) buffer.appendLine( "본업(영업이익)이 적자")
buffer.appendLine("재무 건전성 및 성장성 미달")
} else {
buffer.appendLine("재무 건전성 및 성장성 충족")
}
return buffer.toString()
}
/**
* 종합 상태 반환 (UI 또는 로그용)
*/
fun getInvestmentStatus(fs: FinancialStatement): String {
return when {
isBuyConsiderationMet(fs) -> "🚀 [매수 검토 권장] 재무 건전성 및 성장성 우수"
isSafetyBeltMet(fs) -> "⚖️ [관망/보류] 생존 요건은 충족하나 성장성 부족"
else -> "🚨 [위험/제외] 재무 안정성 미달 또는 적자 기업"
}
}
fun calculateScore(fs: FinancialStatement): Int {
var score = 50.0 // 기본 점수
// 성장성 (영업이익 증가율)
score += when {
fs.operatingProfitGrowth > 20 -> 20
fs.operatingProfitGrowth > 0 -> 10
else -> -10 // 역성장 시 감점
}
// 수익성 (ROE)
score += when {
fs.roe > 15 -> 15
fs.roe > 5 -> 5
fs.roe < 0 -> -15 // 적자 시 큰 감점
else -> 0
}
// 안정성 (부채비율)
score += when {
fs.debtRatio < 100 -> 15
fs.debtRatio < 200 -> 5
else -> -10
}
// 유동성 (당좌비율)
if (fs.quickRatio < 100) score -= 10 // 단기 채무 지급 능력 부족 시 감점
return score.coerceIn(0.0, 100.0).toInt()
}
}
@@ -0,0 +1,75 @@
package analyzer
import kotlinx.serialization.Serializable
import kotlin.Double
import kotlin.math.abs
@Serializable
data class FinancialStatement(
val revenueGrowth: Double = 0.0, // 매출액 증가율
val operatingProfitGrowth: Double = 0.0, // 영업이익 증가율
val netIncomeGrowth: Double = 0.0, // 당기순이익 증가율
val roe: Double = 0.0, // ROE
val debtRatio: Double = 0.0, // 부채비율
val quickRatio: Double = 0.0, // 당좌비율
val isOperatingProfitPositive: Boolean = false, // 당기 영업이익 흑자 여부
val isNetIncomePositive: Boolean = false,
val capitalImpairmentRate: Double = 0.0,
val debtAccelerationRate: Double = 0.0,
val lossToSalesRatio: Double = 0.0
)
object FinancialMapper {
fun mapRawTextToStatement(rawText: String): FinancialStatement {
if (rawText.isBlank()) return FinancialStatement()
val current = extractYearlyValues(rawText, "당기")
val previous = extractYearlyValues(rawText, "전기")
// 기본 수치 추출
val opCurrent = current["영업이익"] ?: 0.0
val opPrevious = previous["영업이익"] ?: 0.0
val salesCurrent = current["매출액"] ?: 1.0
val niCurrent = current["당기순이익(손실)"] ?: 0.0
val equityCurrent = current["자본총계"] ?: 1.0
val capitalStock = current["자본금"] ?: 1.0
val debtCurrent = current["부채총계"] ?: 0.0
val debtPrevious = previous["부채총계"] ?: 1.0
val currentAssets = current["유동자산"] ?: 0.0
val currentLiabilities = current["유동부채"] ?: 1.0
// [강화] 자본잠식률: (자본금 - 자본총계) / 자본금
val capitalImpairment = (capitalStock - equityCurrent) / capitalStock * 100
// [강화] 부채 가속도: 전년 대비 부채 증가율
val debtAcceleration = ((debtCurrent - debtPrevious) / debtPrevious) * 100
// [강화] 매출 대비 영업손실률
val lossToSalesRatio = if (opCurrent < 0) (abs(opCurrent) / salesCurrent) * 100 else 0.0
return FinancialStatement(
operatingProfitGrowth = if (opPrevious != 0.0) ((opCurrent - opPrevious) / abs(opPrevious)) * 100 else 0.0,
roe = (niCurrent / equityCurrent) * 100,
debtRatio = (debtCurrent / equityCurrent) * 100,
quickRatio = (currentAssets / currentLiabilities) * 100,
isOperatingProfitPositive = opCurrent > 0,
isNetIncomePositive = niCurrent > 0,
// 추가된 정교화 지표
capitalImpairmentRate = capitalImpairment,
debtAccelerationRate = debtAcceleration,
lossToSalesRatio = lossToSalesRatio
)
}
private fun extractYearlyValues(text: String, type: String): Map<String, Double> {
val result = mutableMapOf<String, Double>()
val regex = Regex("""([가-힣\s()]+)\s\($type\)([-0-9,.]+)""")
regex.findAll(text).forEach { match ->
val key = match.groupValues[1].trim()
val rawValue = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
result[key] = rawValue
}
return result
}
}
@@ -0,0 +1,173 @@
package analyzer
import model.CandleData
import service.Candle
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.math.*
data class ScalpingSignalModel(
val currentPrice: Double,
val buySignal: Boolean,
val compositeScore: Int, // 0-100: 종합 매수 추천도 (80+ 강매수)
val successProbPct: Double, // 성공 확률 추정 %
val riskLevel: String, // "Low", "Medium", "High"
val rsi: Double,
val volRatio: Double,
val suggestedSlPrice: Double, // 손절 가격
val suggestedTpPrice: Double, // 익절 가격
val riskRewardRatio: Double
)
class ScalpingAnalyzer {
companion object {
private const val SMA_SHORT = 10 // 단기 이평선 (10봉)
private const val SMA_LONG = 20 // 장기 이평선 (20봉)
private const val RSI_WINDOW = 14 // RSI 기간
private const val VOL_WINDOW = 20 // 거래량 평균 기간
private const val VOL_SURGE_THRESHOLD = 1.5 // 평소 대비 1.5배 거래량
private const val RSI_THRESHOLD = 50.0 // RSI 매수 우위 기준
private const val DEFAULT_SL_PCT = -1.5 // 기본 손절 라인 (-1.5%)
private const val DEFAULT_TP_PCT = 1.5 // 기본 익절 라인 (+1.5%)
}
/**
* 실시간 분봉 데이터를 분석하여 종합 신호 모델을 반환합니다.
*/
fun analyze(candles: List<Candle>, isDailyBullish: Boolean): ScalpingSignalModel {
if (candles.size < SMA_LONG) throw IllegalArgumentException("최소 20봉 이상의 데이터가 필요합니다.")
val closes = candles.map { it.close }
val volumes = candles.map { it.volume }
// 1. 보조 지표 계산
val sma10 = simpleMovingAverage(closes, SMA_SHORT)
val sma20 = simpleMovingAverage(closes, SMA_LONG)
val rsiList = computeRSI(closes)
val volAvg = simpleMovingAverage(volumes, VOL_WINDOW)
val (bbUpper, _, bbLower) = bollingerBands(closes)
// 2. 현재 시점 데이터 추출
val current = candles.last()
val currentClose = current.close
val sma10Now = sma10.lastOrNull() ?: 0.0
val sma20Now = sma20.lastOrNull() ?: 0.0
val rsiNow = rsiList.lastOrNull() ?: 0.0
val volRatioNow = if (volAvg.isNotEmpty()) current.volume / volAvg.last() else 1.0
// 3. 정교화된 상태 판별 로직
// [볼린저 밴드 위치] 0.0(하단) ~ 1.0(상단)
val bbPos = if (bbUpper.isNotEmpty() && bbLower.isNotEmpty()) {
(currentClose - bbLower.last()) / (bbUpper.last() - bbLower.last())
} else 0.5
// [전고점 돌파 확인] 최근 6봉 이내의 최고점을 거래량과 함께 돌파하는지 확인
val nearHigh = candles.takeLast(6).dropLast(1).maxOf { it.high }
val isBreakout = currentClose > nearHigh && volRatioNow > 2.0 // 거래량 2배 동반 돌파
// [일봉 이격도 과열 체크] 5일 이동평균선 대비 이격도 계산 (상위 호출부에서 계산 권장)
// 여기서는 기술적 지표 조합으로만 판단
val ma5 = if (candles.size >= 5) candles.takeLast(5).map { it.close }.average() else currentClose
val isOverheated = (currentClose / ma5) * 100 > 112.0 // 12% 이상 이격 시 과열
// 4. 매수 신호 확정 조건
val maBull = currentClose > sma10Now && sma10Now > sma20Now // 정배열
val rsiBull = rsiNow > RSI_THRESHOLD // 매수 강도
val volSurge = volRatioNow > VOL_SURGE_THRESHOLD // 수급 폭발
val bbValid = bbPos in 0.2..0.9 // 밴드 내 안정적 위치
val buySignal = maBull && rsiBull && volSurge && isBreakout && !isOverheated && isDailyBullish
// 5. 종합 점수(0~100) 산출 가중치
val score = (if (maBull) 25 else 0) +
(if (rsiBull) 15 else 0) +
(if (isBreakout) 20 else 0) +
(minOf((volRatioNow - 1.0) * 15, 20.0)).toInt() +
(if (bbValid) 10 else 0) +
(if (isDailyBullish) 10 else 0)
// 6. 성공 확률 및 위험도 계산
val successProb = if (buySignal) 75.0 + (score / 10.0) else 30.0 + (score / 2.0)
return ScalpingSignalModel(
currentPrice = currentClose,
buySignal = buySignal,
compositeScore = score.coerceIn(0, 100),
successProbPct = successProb.coerceAtMost(98.0),
riskLevel = if (volRatioNow > 5.0) "High" else "Medium",
rsi = rsiNow,
volRatio = volRatioNow,
suggestedSlPrice = currentClose * (1 + DEFAULT_SL_PCT / 100),
suggestedTpPrice = currentClose * (1 + DEFAULT_TP_PCT / 100),
riskRewardRatio = abs(DEFAULT_TP_PCT / DEFAULT_SL_PCT)
)
}
// --- 내부 계산 유틸리티 ---
private fun simpleMovingAverage(values: List<Double>, window: Int): List<Double> {
return values.windowed(window).map { it.average() }
}
private fun computeRSI(closes: List<Double>, window: Int = RSI_WINDOW): List<Double> {
val rsi = mutableListOf<Double>()
if (closes.size < window + 1) return rsi
val changes = closes.zipWithNext { a, b -> b - a }
for (i in window..changes.size) {
val windowChanges = changes.subList(i - window, i)
val gains = windowChanges.filter { it > 0 }.sum()
val losses = windowChanges.filter { it < 0 }.map { abs(it) }.sum()
val rs = if (losses > 0) gains / losses else Double.POSITIVE_INFINITY
rsi.add(100.0 - (100.0 / (1.0 + rs)))
}
return rsi
}
private fun bollingerBands(closes: List<Double>, window: Int = SMA_LONG): Triple<List<Double>, List<Double>, List<Double>> {
val upper = mutableListOf<Double>()
val sma = mutableListOf<Double>()
val lower = mutableListOf<Double>()
for (i in window..closes.size) {
val slice = closes.subList(i - window, i)
val mean = slice.average()
val std = sqrt(slice.map { (it - mean).pow(2) }.average())
sma.add(mean)
upper.add(mean + (std * 2))
lower.add(mean - (std * 2))
}
return Triple(upper, sma, lower)
}
}
fun CandleData.toScalpingCandle(): Candle {
// 1. 날짜(YYYYMMDD)와 시간(HHMMSS) 문자열 결합
val dateTimeStr = "${this.stck_bsop_date}${this.stck_cntg_hour}"
val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
// 2. 타임스탬프(Epoch Milliseconds) 계산
val timestamp = try {
val ldt = LocalDateTime.parse(dateTimeStr, formatter)
ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
} catch (e: Exception) {
// 시간 파싱 실패 시 현재 시스템 시간 사용
System.currentTimeMillis()
}
// 3. String 필드들을 Double로 변환하여 Candle 객체 생성
return Candle(
timestamp = timestamp,
open = this.stck_oprc.toDoubleOrNull() ?: 0.0,
high = this.stck_hgpr.toDoubleOrNull() ?: 0.0,
low = this.stck_lwpr.toDoubleOrNull() ?: 0.0,
close = this.stck_prpr.toDoubleOrNull() ?: 0.0, // stck_prpr가 종가 역할
volume = this.cntg_vol.toDoubleOrNull() ?: 0.0
)
}
/**
* 리스트 전체를 변환하는 유틸리티
*/
fun List<CandleData>.toScalpingList(): List<Candle> {
return this.map { it.toScalpingCandle() }
}
@@ -0,0 +1,202 @@
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<CandleData> = emptyList()
var weekly: List<CandleData> = emptyList()
var daily: List<CandleData> = emptyList()
var min30: List<CandleData> = 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<CandleData>, period: Int = 14): Double {
if (candles.size < period + 1) return 0.0
val sub = candles.takeLast(period + 1)
val trList = mutableListOf<Double>()
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<CandleData>, 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<CandleData>): 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<CandleData>): 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<CandleData>): 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()
}
}