diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 2d1aa1b..285a0bf 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -3,10 +3,10 @@ package model import java.time.LocalDateTime const val feesAndTaxRate = 0.33 -const val minimumNetProfit = 0.35 +const val minimumNetProfit = 0.8 const val buyWeight = 2.0 -val MAX_BUDGET = 60000.0 -val MAX_PRICE = 30000 +val MAX_BUDGET = 80000.0 +val MAX_PRICE = 40000 val MIN_PRICE = 1000 val MIN_PURCHASE_SCORE = 65.0 data class AppConfig( diff --git a/src/main/kotlin/model/TradeModels.kt b/src/main/kotlin/model/TradeModels.kt index d7dbbc6..f82f53a 100644 --- a/src/main/kotlin/model/TradeModels.kt +++ b/src/main/kotlin/model/TradeModels.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class RealTimeTrade( + val code: String, val time: String, // 체결 시간 (HH:mm:ss) val price: String, // 체결가 val change: String, // 전일 대비 (대비 기호 포함) diff --git a/src/main/kotlin/network/KisWebSocketManager.kt b/src/main/kotlin/network/KisWebSocketManager.kt index 4d55b32..d64e06c 100644 --- a/src/main/kotlin/network/KisWebSocketManager.kt +++ b/src/main/kotlin/network/KisWebSocketManager.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import model.KisSession +import model.RealTimeTrade import util.AesCrypto import java.util.concurrent.atomic.AtomicBoolean @@ -24,7 +25,8 @@ class KisWebSocketManager { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // 콜백 리스너 - var onPriceUpdate: ((String, Double) -> Unit)? = null + var onPriceUpdate: ((RealTimeTrade) -> Unit)? = null + var onExecutionReceived: ((String, String, String, String, Boolean) -> Unit)? = null suspend fun connect() { @@ -70,8 +72,9 @@ class KisWebSocketManager { } - private val _currentPrice = mutableStateOf("0") - val currentPrice = _currentPrice +// private val _currentPrice = mutableStateOf("0") + private val currentPrice = androidx.compose.runtime.mutableStateMapOf() +// val currentPrice = _currentPrice val tradeLogs = androidx.compose.runtime.mutableStateListOf() @@ -111,20 +114,19 @@ class KisWebSocketManager { if (trId == "H0STCNT0") { val dataRows = parts[3].split("^") val price = dataRows[2] - _currentPrice.value = price // 상태 업데이트 - onPriceUpdate?.invoke(dataRows[0], price.toDoubleOrNull() ?: 0.0) + currentPrice[dataRows[0]] = price // 상태 업데이트 + onPriceUpdate?.invoke(RealTimeTrade( + code = dataRows[0], + time = dataRows[1], + price = price, + change = dataRows[4], + volume = dataRows[12], + type = model.TradeType.NEUTRAL + )) // 로그 추가 (예시) - tradeLogs.add( - 0, model.RealTimeTrade( - time = dataRows[1], - price = price, - change = dataRows[4], - volume = dataRows[12], - type = model.TradeType.NEUTRAL - ) - ) - if (tradeLogs.size > 50) tradeLogs.removeLast() +// if (tradeLogs.isNotEmpty() && tradeLogs.last().code) + } } @@ -133,7 +135,7 @@ class KisWebSocketManager { // AES 복호화 실행 val decryptedData = AesCrypto.decrypt(parts[3], aesKey, aesIv) val dataRows = decryptedData.split("^") - println("🔔 복호화된 체결 통보: ${if (dataRows[4] == "01") {"매도"} else {"매수"}} ${dataRows[8]} ${dataRows[9]}주 ${dataRows[13]} 체결") + println("🔔 복호화된 체결 통보: ${if (dataRows[4] == "01") {"매도"} else {"매수"}} ${dataRows[8]} ${dataRows[9]}주 ${if(dataRows[13] == "01"){"체결"}else{"접수"} }") // UI 콜백 호출 (종목코드, 체결량, 체결가, 주문번호, 체결여부) onExecutionReceived?.invoke( @@ -150,13 +152,13 @@ class KisWebSocketManager { fun clearData() { tradeLogs.clear() - _currentPrice.value = "0" + currentPrice.clear() } suspend fun subscribeStock(code: String, isSubscribe: Boolean = true) { val trType = if (isSubscribe) "1" else "2" sendRequest(trType, "H0STCNT0", code) - if (isSubscribe) println("📡 구독 등록: $code") else println("📴 구독 해제: $code") +// if (isSubscribe) println("📡 구독 등록: $code") else println("📴 구독 해제: $code") } private suspend fun sendRequest(trType: String, trId: String, trKey: String) { diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 759ad1d..03eb2d8 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -42,7 +42,7 @@ object AutoTradingManager { // 설정 상수 private const val MIN_RISE_RATE = 0.1 - private const val MAX_RISE_RATE = 19.0 + private const val MAX_RISE_RATE = 21.0 private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분 private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인 private const val STUCK_THRESHOLD = 5 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 @@ -50,7 +50,8 @@ object AutoTradingManager { fun isRunning(): Boolean = discoveryJob?.isActive == true private var remainingCandidates = mutableListOf() // private val processedCodes = mutableSetOf() // 중복 처리 방지용 (선택 사항) - + private val reanalysisList = mutableListOf() + private val retryCountMap = mutableMapOf() /** * 자동 발굴 루프 시작 및 Watchdog 실행 */ @@ -88,7 +89,7 @@ object AutoTradingManager { // [프로세스 1] 장 마감 및 잔고 체크 val now = LocalTime.now(ZoneId.of("Asia/Seoul")) //&& now.isBefore(LocalTime.of(15, 30)) - if (now.isAfter(LocalTime.of(15, 0)) ) { + if (now.isAfter(LocalTime.of(15, 30)) ) { executeClosingLiquidation(tradeService) return@withTimeout } @@ -99,25 +100,31 @@ object AutoTradingManager { val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code } // [프로세스 2] 후보군 수집 if (remainingCandidates.isEmpty()) { - val candidates = fetchCandidates(tradeService).apply { + val candidates: MutableList = fetchCandidates(tradeService).apply { println("후보군 총 개수 : $size") } .filter { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) in MIN_RISE_RATE..MAX_RISE_RATE } .filter { it.code !in myHoldings && it.code !in pendingStocks } - .distinctBy { it.code } + .filter { !it.name.contains("호스팩", true) } .sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) } - .apply { - println("후보군 조건 충족 총 개수 : $size") - } - remainingCandidates.addAll(candidates) + .toMutableList() + if (reanalysisList.isNotEmpty()) { + candidates.addAll(reanalysisList) + } + remainingCandidates.addAll(candidates.distinctBy { it.code }) } else { println("미확인 데이터 ${remainingCandidates.size}") } - // [프로세스 3] 종목별 순회 분석 - val iterator = remainingCandidates.iterator() - while (iterator.hasNext()) { - val stock = iterator.next() + + // [프로세스 3] 종목별 순회 분석 + var totalCount = remainingCandidates.size + println("후보군 조건 충족 총 개수 : ${totalCount}") + val iterator = remainingCandidates.iterator() + + while (iterator.hasNext()) { + totalCount-- + val stock = iterator.next() try { processSingleStock(stock, myCash, tradeService, callback) // 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거 @@ -128,6 +135,7 @@ object AutoTradingManager { } finally { iterator.remove() } + println("남은 후보군 개수 : ${totalCount}") delay(300) } @@ -145,29 +153,40 @@ object AutoTradingManager { } } + fun addToReanalysis(stock: RankingStock) { + val count = retryCountMap.getOrDefault(stock.code, 0) + if (count < 2) { // 최대 2회까지만 재시도하여 무한 루프 방지 + retryCountMap[stock.code] = count + 1 + reanalysisList.add(stock) + println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록") + } + } + private suspend fun processSingleStock(stock: RankingStock, myCash: Long, tradeService: KisTradeService, callback: TradingDecisionCallback) { try { // 개별 종목 분석은 최대 2분으로 제한 withTimeout(120000L) { val corpInfo = DartCodeManager.getCorpCode(stock.code) if (corpInfo?.cName.isNullOrEmpty()) { - // println("⏭️ [제외] ${stock.name}: 법인명 정보를 찾을 수 없음") return@withTimeout } - - val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout - val today = dailyData.lastOrNull() ?: return@withTimeout - val currentPrice = today.stck_prpr.toDouble() - - if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) return@withTimeout - - println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})") callback(TradingDecision().apply { this.stockCode = stock.code this.confidence = -1.0 this.stockName = stock.name }, false) + val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout + val today = dailyData.lastOrNull() ?: return@withTimeout + val currentPrice = today.stck_prpr.toDouble() + + if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) { + return@withTimeout + } + + println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})") + + val analyzer = coroutineScope { val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) } diff --git a/src/main/kotlin/service/DynamicNewsScraper.kt b/src/main/kotlin/service/DynamicNewsScraper.kt index c256270..2b4d2d9 100644 --- a/src/main/kotlin/service/DynamicNewsScraper.kt +++ b/src/main/kotlin/service/DynamicNewsScraper.kt @@ -282,7 +282,6 @@ object SafeScraper { delay(Random.nextLong(500, 1500)) } } - println("🏁 뉴스 처리 완료") } } diff --git a/src/main/kotlin/ui/DashboardScreen.kt b/src/main/kotlin/ui/DashboardScreen.kt index 3eea217..dd66c24 100644 --- a/src/main/kotlin/ui/DashboardScreen.kt +++ b/src/main/kotlin/ui/DashboardScreen.kt @@ -246,7 +246,7 @@ fun DashboardScreen() { }, stockCode = selectedStockCode, stockName = selectedStockName, - currentPrice = wsManager.currentPrice.value, + currentPrice = "0", trades = wsManager.tradeLogs, tradingDecisionCallback = { decision,bool -> if (bool && decision != null && KisSession.config.isSimulation) { diff --git a/src/main/kotlin/ui/IntegratedOrderSection.kt b/src/main/kotlin/ui/IntegratedOrderSection.kt index fbbaa23..6f08298 100644 --- a/src/main/kotlin/ui/IntegratedOrderSection.kt +++ b/src/main/kotlin/ui/IntegratedOrderSection.kt @@ -22,10 +22,12 @@ import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import model.MAX_BUDGET import model.MIN_PURCHASE_SCORE +import model.RankingStock import model.buyWeight import model.feesAndTaxRate import model.minimumNetProfit import network.KisTradeService +import service.AutoTradingManager import util.MarketUtil enum class InvestmentGrade( @@ -50,7 +52,7 @@ enum class InvestmentGrade( shortWeight = 0.8, midWeight = 1.0, longWeight = 1.0, - profitGuide = 1.4, + profitGuide = 1.3, ), LEVEL_3_CAUTIOUS_RECOMMEND( displayName = "보수적 추천", @@ -58,7 +60,7 @@ enum class InvestmentGrade( shortWeight = 0.6, midWeight = 1.0, longWeight = 1.0, - profitGuide = 1.0, + profitGuide = 0.9, ), LEVEL_2_HIGH_RISK( displayName = "고위험 추천", @@ -66,7 +68,7 @@ enum class InvestmentGrade( shortWeight = 1.0, midWeight = 0.4, longWeight = 0.4, - profitGuide = 0.8, + profitGuide = 0.7, ), LEVEL_1_SPECULATIVE( displayName = "순수 공격적 선택", @@ -74,7 +76,7 @@ enum class InvestmentGrade( shortWeight = 1.0, midWeight = 0.2, longWeight = 0.2, - profitGuide = 0.6, + profitGuide = 0.5, ) } @@ -131,10 +133,14 @@ fun IntegratedOrderSection( var stopLossRate by remember(monitoringItem) { mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5") } - + var basePrice: Double = 0.0 + LaunchedEffect(currentPrice) { + val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0 + basePrice = curPriceNum + } // 계산용 변수 - val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0 - val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0) + + fun getInvestmentGrade( ts: TradingDecision, @@ -187,9 +193,9 @@ fun IntegratedOrderSection( val tickSize = MarketUtil.getTickSize(basePrice) val oneTickLowerPrice = basePrice - (tickSize * when(investmentGrade) { InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> 1 - InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 2 - InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 3 - InvestmentGrade.LEVEL_2_HIGH_RISK -> 3 + InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 1 + InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 2 + InvestmentGrade.LEVEL_2_HIGH_RISK -> 2 InvestmentGrade.LEVEL_1_SPECULATIVE -> 4 else -> 4 }) @@ -249,10 +255,8 @@ fun IntegratedOrderSection( var append = 0.0 if (completeTradingDecision != null && completeTradingDecision.stockCode.equals(stockCode)) { - + println("basePrice $basePrice") fun resultCheck(completeTradingDecision :TradingDecision) { - - val weights = mapOf( "short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소 "profit" to 0.3, @@ -263,23 +267,25 @@ fun IntegratedOrderSection( (completeTradingDecision.shortPossible() * weights["short"]!!) + (completeTradingDecision.profitPossible() * weights["profit"]!!) + (completeTradingDecision.safePossible() * weights["safe"]!!) - println(""" - corpName : ${completeTradingDecision.corpName} - confidence : ${completeTradingDecision.confidence + append} - shortPossible : ${completeTradingDecision.shortPossible() + append} - profitPossible : ${completeTradingDecision.profitPossible()+ append} - safePossible : ${completeTradingDecision.safePossible()+ append} - totalScore : ${totalScore} - """.trimIndent()) + if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence >= MIN_CONFIDENCE) { var investmentGrade : InvestmentGrade = getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence) // 4. 점수에 따른 가변 마진 적용 // 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용 val finalMargin = minimumNetProfit * investmentGrade.profitGuide - + println(""" + 사명 : ${completeTradingDecision.corpName} + 신뢰도 : ${completeTradingDecision.confidence + append} + 단기성 : ${completeTradingDecision.shortPossible() + append} + 수익성 : ${completeTradingDecision.profitPossible()+ append} + 안전성 : ${completeTradingDecision.safePossible()+ append} + ${investmentGrade.displayName} : ${investmentGrade.description} + 총점 : ${totalScore} + """.trimIndent()) println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장) + val calculatedQty = if (basePrice > 0) { (MAX_BUDGET / basePrice).toInt().coerceAtLeast(1) } else { @@ -293,8 +299,11 @@ fun IntegratedOrderSection( investmentGrade = investmentGrade, ) - } else { - println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달 또는 신뢰도 ${completeTradingDecision.confidence}가 기준치 ${MIN_CONFIDENCE} 미달") + } else if(totalScore >= (MIN_PURCHASE_SCORE * 0.85) && completeTradingDecision.confidence >= (MIN_CONFIDENCE * 0.85)) { + AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName)) + println("✋ [관망] 토탈 스코어 또는 신뢰도 미달 이나 약간의 오차로 재분석 대기열에 추가") + } else { + println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)}) 또는 신뢰도 ${completeTradingDecision.confidence} 미달") } } when (completeTradingDecision?.decision) { diff --git a/src/main/kotlin/ui/StockDetailArea.kt b/src/main/kotlin/ui/StockDetailArea.kt index 5148b38..2d47f51 100644 --- a/src/main/kotlin/ui/StockDetailArea.kt +++ b/src/main/kotlin/ui/StockDetailArea.kt @@ -21,8 +21,6 @@ import model.CandleData import network.DartCodeManager import network.KisTradeService import network.KisWebSocketManager -import network.NewsService -import service.TechnicalAnalyzer import java.time.LocalTime import java.time.format.DateTimeFormatter import kotlin.collections.isNotEmpty @@ -44,7 +42,7 @@ fun StockDetailSection( yearSummary : MutableList ) { - var openPrice by remember { mutableStateOf("0") } +// var openPrice by remember { mutableStateOf("0") } var chartData by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(false) } var resultMessage by remember { mutableStateOf("") } @@ -62,6 +60,8 @@ fun StockDetailSection( // 이전 종목 코드를 기억하기 위한 상태 var previousCode by remember { mutableStateOf("") } + var latestPrice by remember { mutableStateOf("0") } + // 종목 변경 시 데이터 로드 및 웹소켓 구독 관리 LaunchedEffect(stockCode) { @@ -81,16 +81,62 @@ fun StockDetailSection( // 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화) coroutineScope { + launch { + wsManager.onPriceUpdate = {tradeLog -> + + if (tradeLog.code.equals(stockCode)) { + val code = tradeLog.code + val price = tradeLog.price + wsManager.tradeLogs.add(tradeLog) + if (wsManager.tradeLogs.size > 50) wsManager.tradeLogs.removeLast() + println("code $code ,price $price") + val currentPrice = price + if (chartData.isNotEmpty() && currentPrice != "0") { + val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0 + val lastCandle = chartData.last() + + // 현재 시간(분 단위) 확인 + val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00")) + + if (lastCandle.stck_bsop_date != currentMinute) { + // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과) + val newCandle = CandleData( + stck_bsop_date = currentMinute, + stck_oprc = currentPrice, + stck_hgpr = currentPrice, + stck_lwpr = currentPrice, + stck_prpr = currentPrice, + stck_cntg_hour = currentMinute, + cntg_vol = "1", + acml_tr_pbmn = "1", + ) + // 최대 100개까지만 유지하여 성능 최적화 + chartData = (chartData + newCandle).takeLast(100) + } else { + // 같은 분 내에서는 기존 마지막 캔들만 업데이트 + val updatedCandle = lastCandle.copy( + stck_prpr = currentPrice, + stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) currentPrice else lastCandle.stck_hgpr, + stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) currentPrice else lastCandle.stck_lwpr + ) + chartData = chartData.dropLast(1) + updatedCandle + } + } + latestPrice = currentPrice + } + + } + } launch {tradeService.fetchChartData(stockCode, isDomestic) .onSuccess { data -> - println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력 +// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력 chartData = data min30.clear() min30.addAll(chartData) } .onFailure { error -> - println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}") +// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}") chartData = emptyList() } } @@ -122,41 +168,43 @@ fun StockDetailSection( isLoading = false } - val latestPrice by wsManager.currentPrice // 웹소켓에서 업데이트되는 현재가 - LaunchedEffect(latestPrice) { - if (chartData.isNotEmpty() && latestPrice != "0") { - val priceDouble = latestPrice.replace(",", "").toDoubleOrNull() ?: return@LaunchedEffect - val lastCandle = chartData.last() - // 현재 시간(분 단위) 확인 - val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00")) - - if (lastCandle.stck_bsop_date != currentMinute) { - // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과) - val newCandle = CandleData( - stck_bsop_date = currentMinute, - stck_oprc = latestPrice, - stck_hgpr = latestPrice, - stck_lwpr = latestPrice, - stck_prpr = latestPrice, - stck_cntg_hour = currentMinute, - cntg_vol = "1", - acml_tr_pbmn = "1", - ) - // 최대 100개까지만 유지하여 성능 최적화 - chartData = (chartData + newCandle).takeLast(100) - } else { - // 같은 분 내에서는 기존 마지막 캔들만 업데이트 - val updatedCandle = lastCandle.copy( - stck_prpr = latestPrice, - stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) latestPrice else lastCandle.stck_hgpr, - stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) latestPrice else lastCandle.stck_lwpr - ) - chartData = chartData.dropLast(1) + updatedCandle - } - } - } +// LaunchedEffect(latestPrice) { +// println("latestPrice >>> $latestPrice") +// if (chartData.isNotEmpty() && latestPrice != "0") { +// val latestPrice = latestPrice ?: "0" +// val priceDouble = latestPrice?.replace(",", "")?.toDoubleOrNull() ?: return@LaunchedEffect +// val lastCandle = chartData.last() +// +// // 현재 시간(분 단위) 확인 +// val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00")) +// +// if (lastCandle.stck_bsop_date != currentMinute) { +// // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과) +// val newCandle = CandleData( +// stck_bsop_date = currentMinute, +// stck_oprc = latestPrice, +// stck_hgpr = latestPrice, +// stck_lwpr = latestPrice, +// stck_prpr = latestPrice, +// stck_cntg_hour = currentMinute, +// cntg_vol = "1", +// acml_tr_pbmn = "1", +// ) +// // 최대 100개까지만 유지하여 성능 최적화 +// chartData = (chartData + newCandle).takeLast(100) +// } else { +// // 같은 분 내에서는 기존 마지막 캔들만 업데이트 +// val updatedCandle = lastCandle.copy( +// stck_prpr = latestPrice, +// stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) latestPrice else lastCandle.stck_hgpr, +// stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) latestPrice else lastCandle.stck_lwpr +// ) +// chartData = chartData.dropLast(1) + updatedCandle +// } +// } +// } Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { // [상단] 종목명 및 상태 메시지 @@ -170,7 +218,7 @@ fun StockDetailSection( code = stockCode, isDomestic = isDomestic, previousClose = previousClose, - openPrice = openPrice, + openPrice = latestPrice, resultMessage = resultMessage, resultMessageClear = {resultMessage = ""}, isSuccess = isSuccess @@ -179,10 +227,10 @@ fun StockDetailSection( // 실시간 가격 표시 (WebSocket 데이터) Column(horizontalAlignment = Alignment.End) { Text( - text = "${wsManager.currentPrice.value} 원", + text = "${latestPrice} 원", style = MaterialTheme.typography.h4, fontWeight = FontWeight.Bold, - color = if (wsManager.currentPrice.value.contains("-")) Color.Blue else Color.Red + color = if (latestPrice?.contains("-") ?: false) Color.Blue else Color.Red ) Text("실시간 체결가", style = MaterialTheme.typography.caption, color = Color.Gray) } @@ -228,7 +276,7 @@ fun StockDetailSection( stockCode = stockCode, stockName = stockName, isDomestic = isDomestic, - currentPrice = wsManager.currentPrice.value, + currentPrice = latestPrice, holdingQuantity = holdingQuantity, tradeService = tradeService, onOrderSaved = onOrderSaved,