package service import TradingDecision import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import model.CandleData import network.KisTradeService import network.NewsService import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId import java.time.format.DateTimeFormatter import kotlin.collections.List import kotlin.math.* // service/AutoTradingManager.kt object AutoTradingManager { private val scope = CoroutineScope(Dispatchers.Default) val targetStocks = mutableListOf() fun addStock(stockCode : String, result :(String, Boolean)->Unit) { targetStocks.add(stockCode) startTradingLoop(result) } fun startTradingLoop(result :(String, Boolean)->Unit) { scope.launch { println("๐Ÿš€ 10๋ถ„ ์ฃผ๊ธฐ ์ž๋™ ๋ถ„์„ ๋ฐ ๋งค๋งค ์‹œ์ž‘: ${LocalTime.now()}") targetStocks.forEach { stockCode -> launch { // ์ข…๋ชฉ๋ณ„ ๋ณ‘๋ ฌ ๋ถ„์„ (M3 Pro ํŒŒ์›Œ ํ™œ์šฉ) RagService.processStock(stockCode,result) {code ,decision -> when (decision?.decision) { "BUY" -> if (decision.confidence > 70) executeOrder(stockCode, "๋งค์ˆ˜") "SELL" -> executeOrder(stockCode, "๋งค๋„") else -> println("[$stockCode] ๊ด€๋ง ์œ ์ง€: ${decision?.reason}") } result(decision.toString(),true) } } } delay(10 * 60 * 1000) // 10๋ถ„ ๋Œ€๊ธฐ } } private fun executeOrder(code: String, type: String) { // ์‹ค์ œ ์ฆ๊ถŒ์‚ฌ API ํ˜ธ์ถœ ๋กœ์ง (ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ, ํ‚ค์›€ ๋“ฑ) println("๐Ÿ”ฅ [์ฃผ๋ฌธ ์ง‘ํ–‰] $code $type ์™„๋ฃŒ") } } object TechnicalAnalyzer { var monthly: List = emptyList() var weekly: List = emptyList() var daily: List = emptyList() var min30: List = emptyList() data class InvestmentScores( val ultraShort: Int, // ์ดˆ๋‹จ๊ธฐ (๋ถ„๋ด‰/์—๋„ˆ์ง€) val shortTerm: Int, // ๋‹จ๊ธฐ (์ผ๋ด‰/๋‰ด์Šค) val midTerm: Int, // ์ค‘๊ธฐ (์ฃผ๋ด‰/์žฌ๋ฌด) val longTerm: Int // ์žฅ๊ธฐ (์›”๋ด‰/ํŽ€๋”๋ฉ˜ํ„ธ) ) fun calculateScores( financialScore: Int // ์žฌ๋ฌด์ œํ‘œ ์ ์ˆ˜ (์„ฑ์žฅ๋ฅ  ๋“ฑ ๊ธฐ๋ฐ˜) ): InvestmentScores { // 1. ์ดˆ๋‹จ๊ธฐ (๋ถ„๋ด‰ + ์—๋„ˆ์ง€ ์ง€ํ‘œ ์œ„์ฃผ) val ultra = (TechnicalAnalyzer.calculateMFI(min30, 14) * 0.4 + TechnicalAnalyzer.calculateStochastic(min30) * 0.3 + (if(TechnicalAnalyzer.calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt() // 2. ๋‹จ๊ธฐ (์ผ๋ด‰ ์ถ”์„ธ + OBV ์—๋„ˆ์ง€) val short = (TechnicalAnalyzer.calculateRSI(daily) * 0.3 + (if(TechnicalAnalyzer.calculateOBV(daily) > 0) 40 else 10) + (if(TechnicalAnalyzer.calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt() // 3. ์ค‘๊ธฐ (์ฃผ๋ด‰ + ์žฌ๋ฌด ์ ์ˆ˜ ํ˜ผํ•ฉ) val mid = (if(TechnicalAnalyzer.calculateChange(weekly) > 0) 40 else 10) + (financialScore * 0.6).toInt() // 4. ์žฅ๊ธฐ (์›”๋ด‰ + ์„นํ„ฐ/๊ธฐ์—… ํŽ€๋”๋ฉ˜ํ„ธ) val long = (if(TechnicalAnalyzer.calculateChange(monthly) > 0) 50 else 0) + (financialScore * 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 generateComprehensiveReport(): String { // [1] ๋‹จ๊ธฐ ์—๋„ˆ์ง€ ์ง€ํ‘œ ๊ณ„์‚ฐ (์ตœ๊ทผ 30๋ถ„๋ด‰ ๊ธฐ์ค€) val obv = calculateOBV(min30) val mfi = calculateMFI(min30, 14) val adLine = calculateADLine(min30) // [2] ์‹œ๊ณ„์—ด๋ณ„ ๊ฐ€๊ฒฉ ๋ณ€๋™ ๋ฐ ์ถ”์„ธ ์š”์•ฝ val m10 = min30.takeLast(10) val change10 = calculateChange(m10) val change30 = calculateChange(min30) val changeDaily = calculateChange(daily.takeLast(2)) // ์ „์ผ ๋Œ€๋น„ // [3] ์ดํ‰์„  ๋ฐ ๊ฐ€๊ฒฉ ์œ„์น˜ val ma5 = m10.takeLast(5).map { it.stck_prpr.toDouble() }.average() val currentPrice = min30.last().stck_prpr.toDouble() val signal = ScalpingAnalyzer().analyze(min30.toScalpingList()) // [4] ๊ฑฐ๋ž˜๋Ÿ‰ ๊ฐ•๋„ val avgVol30 = min30.map { it.cntg_vol.toLong() }.average() val recentVol5 = m10.takeLast(5).map { it.cntg_vol.toLong() }.average() val volStrength = if (avgVol30 > 0) recentVol5 / avgVol30 else 1.0 val atr = calculateATR(min30) val stochK = calculateStochastic(min30) val priceRange30 = min30.maxOf { it.stck_hgpr.toDouble() } - min30.minOf { it.stck_lwpr.toDouble() } return """ [์ดˆ๋‹จ๊ธฐ ๊ธฐ์ˆ ์  ์Šค์ผˆํ•‘ ๋ถ„์„] - ์ข…ํ•ฉ ์Šค์ฝ”์–ด: ${signal.compositeScore} / 100 - ๋งค์ˆ˜ ์‹ ํ˜ธ ๋ฐœ์ƒ ์—ฌ๋ถ€: ${if (signal.buySignal) "YES" else "NO"} - ์„ฑ๊ณต ํ™•๋ฅ  ์˜ˆ์ธก: ${signal.successProbPct}% - ์œ„ํ—˜ ๋“ฑ๊ธ‰: ${signal.riskLevel} (ATR ๋ณ€๋™์„ฑ ๊ธฐ๋ฐ˜) - RSI: ${"%.1f".format(signal.rsi)} / ๊ฑฐ๋ž˜๋Ÿ‰ ๋น„์œจ: ${"%.1f".format(signal.volRatio)}๋ฐฐ - ๊ถŒ์žฅ ๊ฐ€๊ฒฉ: ์†์ ˆ๊ฐ€(${signal.suggestedSlPrice.toInt()}์›), ์ต์ ˆ๊ฐ€(${signal.suggestedTpPrice.toInt()}์›) - ์›”๋ด‰/์ฃผ๋ด‰ ์œ„์น˜: ${if(calculateChange(monthly) > 0) "์žฅ๊ธฐ ์ƒ์Šน" else "์žฅ๊ธฐ ํ•˜๋ฝ"} / ${if(calculateChange(weekly) > 0) "์ค‘๊ธฐ ์ƒ์Šน" else "์ค‘๊ธฐ ํ•˜๋ฝ"} - ์ผ๋ด‰ ๋Œ€๋น„: ${ "%.2f".format(changeDaily) }% ๋ณ€๋™ - 30๋ถ„ ๋Œ€๋น„: ${ "%.2f".format(change30) }% ๋ณ€๋™ - 10๋ถ„ ๋Œ€๋น„: ${ "%.2f".format(change10) }% ๋ณ€๋™ - ์ดํ‰์„  ์ƒํƒœ: ํ˜„์žฌ๊ฐ€(${currentPrice.toInt()}) vs MA5(${ma5.toInt()}) -> ${if(currentPrice > ma5) "์ƒ๋‹จ ์œ„์น˜" else "ํ•˜๋‹จ ์œ„์น˜"} - OBV (๋ˆ„์  ๊ฑฐ๋ž˜๋Ÿ‰ ์—๋„ˆ์ง€): ${ "%.0f".format(obv) } (${if(obv > 0) "๋ˆ„์  ๋งค์ˆ˜ ์šฐ์œ„" else "๋ˆ„์  ๋งค๋„ ์šฐ์œ„"}) - MFI (์ž๊ธˆ ์œ ์ž… ์ง€์ˆ˜): ${ "%.1f".format(mfi) } (๊ณผ๋งค์ˆ˜ ๊ธฐ์ค€: 80 / ๊ณผ๋งค๋„ ๊ธฐ์ค€: 20) - A/D (๋ˆ„์  ๋ถ„์‚ฐ ๋ผ์ธ): ${ "%.0f".format(adLine) } (์ข…๊ฐ€ ํ˜•์„ฑ ์œ„์น˜์™€ ๊ฑฐ๋ž˜๋Ÿ‰ ๊ฒฐํ•ฉ ์ˆ˜์น˜) - ๊ฑฐ๋ž˜๋Ÿ‰ ๊ฐ•๋„: ์ตœ๊ทผ 5๋ถ„ ํ‰๊ท ์ด 30๋ถ„ ํ‰๊ท ์˜ ${ "%.1f".format(volStrength) }๋ฐฐ ์ˆ˜์ค€ - ATR (ํ‰๊ท  ๋ณ€๋™ํญ): ${"%.0f".format(atr)}์› (์ตœ๊ทผ ์บ”๋“ค ํ•˜๋‚˜๊ฐ€ ํ‰๊ท ์ ์œผ๋กœ ์›€์ง์ด๋Š” ํฌ๊ธฐ) - 30๋ถ„ ๋‚ด ์ตœ๋Œ€ ์ง„ํญ: ${"%.0f".format(priceRange30)}์› (์ตœ๊ณ ๊ฐ€-์ตœ์ €๊ฐ€ ์ฐจ์ด) - ์Šคํ† ์บ์Šคํ‹ฑ(%K): ${"%.1f".format(stochK)} (100์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์ตœ๊ทผ ํŒŒ๋™์˜ ๊ณ ์ , 0์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์ €์ ) - ๋ณ€๋™์„ฑ ๊ฐ•๋„: ํ˜„์žฌ ์ง„ํญ์ด ATR ๋Œ€๋น„ ${"%.1f".format(priceRange30 / atr)}๋ฐฐ ์ˆ˜์ค€์œผ๋กœ ์ „๊ฐœ ์ค‘ - 30๋ถ„๋ด‰ ์ตœ๊ณ ๊ฐ€: ${min30.maxOf { it.stck_hgpr.toInt() }} - 30๋ถ„๋ด‰ ์ตœ์ €๊ฐ€: ${min30.minOf { it.stck_lwpr.toInt() }} - RSI(14): ${ "%.1f".format(calculateRSI(min30)) } """.trimIndent() } /** * ATR (Average True Range): ์ตœ๊ทผ ๋ณ€๋™ ํญ์˜ ํ‰๊ท . ๊ทธ๋ž˜ํ”„์˜ '์ถœ๋ ์ž„' ํฌ๊ธฐ๋ฅผ ์ธก์ • */ fun calculateATR(candles: List, period: Int = 14): Double { 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() val tr = maxOf(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)) trList.add(tr) } return trList.average() } /** * Stochastic (%K): ์ตœ๊ทผ ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ๋‚ด์—์„œ ํ˜„์žฌ๊ฐ€์˜ ์œ„์น˜ (0~100) * ๋ฐ˜๋ณต๋˜๋Š” ํŒŒ๋™(Ups and Downs)์—์„œ ํ˜„์žฌ๊ฐ€ ๊ณ ์ ์ธ์ง€ ์ €์ ์ธ์ง€ ํŒ๋‹จ */ fun calculateStochastic(candles: List, period: Int = 14): Double { val sub = candles.takeLast(period) val highest = sub.maxOf { it.stck_hgpr.toDouble() } val lowest = sub.minOf { it.stck_lwpr.toDouble() } val current = sub.last().stck_prpr.toDouble() return if (highest != lowest) (current - lowest) / (highest - lowest) * 100 else 50.0 } private fun calculateChange(list: List): Double { 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 } private 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() val currVol = candles[i].cntg_vol.toDouble() when { currClose > prevClose -> obv += currVol currClose < prevClose -> obv -= currVol } } return obv } /** * MFI (Money Flow Index) ๊ณ„์‚ฐ (๊ธฐ๊ฐ„: ๋ณดํ†ต 14์ผ) */ fun calculateMFI(candles: List, period: Int = 14): Double { val subList = candles.takeLast(period + 1) var posFlow = 0.0 var negFlow = 0.0 for (i in 1 until subList.size) { val prevTypical = (subList[i-1].stck_hgpr.toDouble() + subList[i-1].stck_lwpr.toDouble() + subList[i-1].stck_prpr.toDouble()) / 3 val currTypical = (subList[i].stck_hgpr.toDouble() + subList[i].stck_lwpr.toDouble() + subList[i].stck_prpr.toDouble()) / 3 val moneyFlow = currTypical * subList[i].cntg_vol.toDouble() if (currTypical > prevTypical) posFlow += moneyFlow else if (currTypical < prevTypical) negFlow += moneyFlow } return if (negFlow == 0.0) 100.0 else 100 - (100 / (1+ (posFlow / negFlow))) } private fun calculateADLine(candles: List): Double { var ad = 0.0 candles.forEach { val high = it.stck_hgpr.toDouble(); val low = it.stck_lwpr.toDouble(); val close = it.stck_prpr.toDouble() val mfv = if (high != low) ((close - low) - (high - close)) / (high - low) else 0.0 ad += mfv * it.cntg_vol.toDouble() } return ad } fun clear() { monthly = emptyList() weekly = emptyList() daily = emptyList() min30 = emptyList() } } class ScalpingAnalyzer { companion object { private const val SMA_SHORT = 10 private const val SMA_LONG = 20 private const val RSI_WINDOW = 14 private const val VOL_WINDOW = 20 private const val VOL_SURGE_THRESHOLD = 1.5 private const val RSI_THRESHOLD = 50.0 private const val BB_LOWER_POS = 0.2 private const val BB_UPPER_POS = 0.8 private const val ATR_WINDOW = 14 private const val DEFAULT_SL_PCT = -0.5 private const val DEFAULT_TP_PCT = 1.0 private const val HIGH_SCORE_THRESHOLD = 80 } fun computeRSI(closes: List, window: Int = RSI_WINDOW): List { val rsi = mutableListOf() if (closes.size < window + 1) return rsi for (i in window until closes.size) { val gains = mutableListOf() val losses = mutableListOf() for (j in (i - window + 1) until i + 1) { val delta = closes[j] - closes[j - 1] if (delta > 0) gains.add(delta) else losses.add(abs(delta)) } val avgGain = gains.average() val avgLoss = losses.average() val rs = if (avgLoss > 0) avgGain / avgLoss else Double.POSITIVE_INFINITY rsi.add(100.0 - (100.0 / (1.0 + rs))) } return rsi } fun bollingerBands(closes: List, window: Int = SMA_LONG): Triple, List, List> { val sma = mutableListOf() val upper = mutableListOf() val lower = mutableListOf() for (i in window - 1 until closes.size) { val slice = closes.subList(i - window + 1, i + 1) val mean = slice.average() val std = sqrt(slice.map { (it - mean).pow(2.0) }.average()) * 2.0 sma.add(mean) upper.add(mean + std) lower.add(mean - std) } return Triple(upper, sma, lower) } fun analyze(candles: List): ScalpingSignalModel { if (candles.size < SMA_LONG) throw IllegalArgumentException("์ตœ์†Œ 20๋ด‰ ํ•„์š”") val closes = candles.map { it.close } val volumes = candles.map { it.volume } // ์ง€ํ‘œ ๊ณ„์‚ฐ val sma10 = simpleMovingAverage(closes, SMA_SHORT) val sma20 = simpleMovingAverage(closes, SMA_LONG) val rsiList = computeRSI(closes) val volAvg = simpleMovingAverage(volumes, VOL_WINDOW) val volRatioList = volumes.mapIndexed { i, v -> if (i >= VOL_WINDOW) v / volAvg[i - VOL_WINDOW] else 0.0 } val (bbUpper, bbMiddle, bbLower) = bollingerBands(closes) val current = candles.last() val idx = candles.size - 1 val currentClose = current.close val sma10Now = if (sma10.size > 0) sma10.last() else 0.0 val sma20Now = if (sma20.size > 0) sma20.last() else 0.0 val rsiNow = if (rsiList.isNotEmpty()) rsiList.last() else 0.0 val volRatioNow = volRatioList.last() val bbPos = if (bbUpper.isNotEmpty() && bbLower.isNotEmpty()) { (currentClose - bbLower.last()) / (bbUpper.last() - bbLower.last()) } else 0.5 // ์‹ ํ˜ธ ์กฐ๊ฑด val maBull = currentClose > sma10Now && sma10Now > sma20Now val rsiBull = rsiNow > RSI_THRESHOLD val volSurge = volRatioNow > VOL_SURGE_THRESHOLD val bbGood = bbPos > BB_LOWER_POS && bbPos < BB_UPPER_POS val buySignal = maBull && rsiBull && volSurge && bbGood // ์ข…ํ•ฉ ์Šค์ฝ”์–ด (๊ฐ€์ค‘: MA 30%, RSI 20%, Vol 30%, BB 20%) val score = (if (maBull) 30 else 0) + (if (rsiBull) 20 else 0) + (minOf((volRatioNow - 1.0) * 30, 30.0)).toInt() + (if (bbGood) 20 else 0) // ์œ„ํ—˜๋„ (ATR proxy) val returns = closes.mapIndexed { i, c -> if (i > 0) (c - closes[i-1])/closes[i-1] * 100 else 0.0 } val atrProxy = if (returns.size >= ATR_WINDOW) { returns.subList(returns.size - ATR_WINDOW, returns.size).average() } else 1.0 val riskLevel = when { abs(atrProxy) < 1 -> "Low" abs(atrProxy) < 2 -> "Medium" else -> "High" } // ์„ฑ๊ณต ํ™•๋ฅ  & SL/TP val successProb = if (buySignal) 75.0 else 35.0 + (score / 100.0 * 20) val slPrice = currentClose * (1 + DEFAULT_SL_PCT / 100) val tpPrice = currentClose * (1 + DEFAULT_TP_PCT / 100) val rrRatio = abs(DEFAULT_TP_PCT / DEFAULT_SL_PCT) return ScalpingSignalModel( currentPrice = currentClose, buySignal = buySignal, compositeScore = minOf(score.toInt(), 100), successProbPct = successProb, riskLevel = riskLevel, rsi = rsiNow, volRatio = volRatioNow, suggestedSlPrice = slPrice, suggestedTpPrice = tpPrice, riskRewardRatio = rrRatio ) } private fun simpleMovingAverage(values: List, window: Int): List { val sma = mutableListOf() for (i in window - 1 until values.size) { val slice = values.subList(i - window + 1, i + 1) sma.add(slice.average()) } return sma } } data class Candle( val timestamp: Long, val open: Double, val high: Double, val low: Double, val close: Double, val volume: Double ) 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 ) 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.toScalpingList(): List { return this.map { it.toScalpingCandle() } }