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.LocalTime // 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() 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() // [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 return """ [์ข…ํ•ฉ ์‹œ๊ณ„์—ด ๋ฐ ์—๋„ˆ์ง€ ๋ถ„์„ ๋ณด๊ณ ์„œ] 1. ๊ฐ€๊ฒฉ ๋ฐ ์ถ”์„ธ ํ˜„ํ™ฉ - ์›”๋ด‰/์ฃผ๋ด‰ ์œ„์น˜: ${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 "ํ•˜๋‹จ ์œ„์น˜"} 2. ์ž๊ธˆ ํ๋ฆ„ ๋ฐ ์—๋„ˆ์ง€ ์ง€ํ‘œ - OBV (๋ˆ„์  ๊ฑฐ๋ž˜๋Ÿ‰ ์—๋„ˆ์ง€): ${ "%.0f".format(obv) } (${if(obv > 0) "๋ˆ„์  ๋งค์ˆ˜ ์šฐ์œ„" else "๋ˆ„์  ๋งค๋„ ์šฐ์œ„"}) - MFI (์ž๊ธˆ ์œ ์ž… ์ง€์ˆ˜): ${ "%.1f".format(mfi) } (๊ณผ๋งค์ˆ˜ ๊ธฐ์ค€: 80 / ๊ณผ๋งค๋„ ๊ธฐ์ค€: 20) - A/D (๋ˆ„์  ๋ถ„์‚ฐ ๋ผ์ธ): ${ "%.0f".format(adLine) } (์ข…๊ฐ€ ํ˜•์„ฑ ์œ„์น˜์™€ ๊ฑฐ๋ž˜๋Ÿ‰ ๊ฒฐํ•ฉ ์ˆ˜์น˜) - ๊ฑฐ๋ž˜๋Ÿ‰ ๊ฐ•๋„: ์ตœ๊ทผ 5๋ถ„ ํ‰๊ท ์ด 30๋ถ„ ํ‰๊ท ์˜ ${ "%.1f".format(volStrength) }๋ฐฐ ์ˆ˜์ค€ 3. ๊ฐ€๊ฒฉ ๋ณ€๋™ ๋ฒ”์œ„ - 30๋ถ„๋ด‰ ์ตœ๊ณ ๊ฐ€: ${min30.maxOf { it.stck_hgpr.toInt() }} - 30๋ถ„๋ด‰ ์ตœ์ €๊ฐ€: ${min30.minOf { it.stck_lwpr.toInt() }} - RSI(14): ${ "%.1f".format(calculateRSI(min30)) } """.trimIndent() } 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 } }