From ac50737ea8bb28becfdae1406e842a8e04ae795c Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 22 Jan 2026 17:56:31 +0900 Subject: [PATCH] .. --- src/main/kotlin/network/DartCodeManager.kt | 33 +- src/main/kotlin/network/KisTradeService.kt | 2 +- src/main/kotlin/network/RagService.kt | 40 ++- src/main/kotlin/service/AutoTradingManager.kt | 303 ++++++++++++++++-- src/main/kotlin/ui/AiAnalysisView.kt | 1 + 5 files changed, 346 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/network/DartCodeManager.kt b/src/main/kotlin/network/DartCodeManager.kt index 92a78c5..6a04f4f 100644 --- a/src/main/kotlin/network/DartCodeManager.kt +++ b/src/main/kotlin/network/DartCodeManager.kt @@ -4,6 +4,7 @@ import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* import java.io.ByteArrayInputStream +import java.io.File import java.util.zip.ZipInputStream import javax.xml.parsers.DocumentBuilderFactory @@ -11,6 +12,17 @@ object DartCodeManager { private val corpCodeMap = mutableMapOf() private const val DART_API_KEY = "61143d2af0759f6c28ce372d9e339d1e01687abc" // 지범님의 API 키 입력 + private fun saveXmlDebugFile(xmlBytes: ByteArray) { + try { + val debugFile = java.io.File("debug_CORPCODE.xml") + debugFile.writeBytes(xmlBytes) + println("💾 [디버그] XML 파일 저장 완료: ${debugFile.absolutePath}") + // M3 Pro 환경에서는 파일 쓰기가 거의 즉시 완료됩니다. + } catch (e: Exception) { + println("⚠️ [디버그] 파일 저장 실패: ${e.message}") + } + } + /** * 앱 실행 시 호출하여 매핑 테이블 업데이트 */ @@ -21,11 +33,14 @@ object DartCodeManager { val url = "https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key=$DART_API_KEY" val response: HttpResponse = client.get(url) val zipBytes = response.readBytes() - + val zipFile = File("dart_corp_codes.zip") + zipFile.writeBytes(zipBytes) + println("💾 [디버그] 원본 ZIP 저장 완료: ${zipFile.absolutePath} (${zipBytes.size} bytes)") ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis -> var entry = zis.nextEntry while (entry != null) { if (entry.name == "CORPCODE.xml") { + saveXmlDebugFile(zipBytes) parseXml(zis.readAllBytes()) break } @@ -48,7 +63,7 @@ object DartCodeManager { val element = nodeList.item(i) as org.w3c.dom.Element val stockCode = element.getElementsByTagName("stock_code").item(0)?.textContent?.trim() ?: "" val corpCode = element.getElementsByTagName("corp_code").item(0)?.textContent ?: "" - + println("stockCode: $stockCode , corpCode: $corpCode") // 종목코드(stock_code)가 있는 상장사만 매핑에 추가 if (stockCode.isNotEmpty()) { corpCodeMap[stockCode] = corpCode @@ -60,6 +75,18 @@ object DartCodeManager { * 6자리 종목코드로 8자리 법인코드 반환 */ fun getCorpCode(stockCode: String): String? { - return corpCodeMap[stockCode] + // 1. 직접 매칭 시도 + corpCodeMap[stockCode]?.let { return it } + + // 2. 우선주 규칙 적용 (마지막 자리가 5, 7, 9인 경우 0으로 변경) + if (stockCode.length == 6 && stockCode.last() in listOf('5', '7', '9')) { + val commonStockCode = stockCode.substring(0, 5) + "0" + corpCodeMap[commonStockCode]?.let { + println("ℹ️ [DART] 우선주($stockCode)를 보통주($commonStockCode) 코드로 매핑 성공") + return it + } + } + + return null } } \ No newline at end of file diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index 279510a..da4a165 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -452,7 +452,7 @@ object KisTradeService { else "/uapi/overseas-stock/v1/quotations/inquire-time-itemchartprice" val now = LocalTime.now().minusMinutes(30) val searchTime = if (now.isAfter(LocalTime.of(15, 30))) { - "153000" + "150000" } else { now.format(DateTimeFormatter.ofPattern("HHmmss")) } diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 203c58f..72cee77 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -69,7 +69,8 @@ object RagService { // 1. 10분간의 데이터 가져오기 (API 호출) coroutineScope { var tradingDecision : TradingDecision = TradingDecision() - val financialDataDeferred = async { NewsService.fetchFinancialGrowth(DartCodeManager.getCorpCode(stockCode)) } + val corpCode = DartCodeManager.getCorpCode(stockCode) + val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpCode) } tradingDecision.financialData = financialDataDeferred.await() result(tradingDecision.toString(),false) @@ -148,19 +149,38 @@ object RagService { financialData: String ): TradingDecision? { val prompt = """ - 당신은 단기 데이트레이딩 전문가입니다. 아래 데이터를 분석하여 '매수', '매도', '관망' 중 하나를 결정하세요. + <|begin_of_text|><|start_header_id|>system<|end_header_id|> + 당신은 수치 기반의 '정량 분석(Quantitative Analysis)' 단기 데이트레이딩 전문가이자 전문 애널리스트입니다. + 제공된 데이터를 바탕으로 투자 기간별 스코어를 산출하고 최종 매매 결정을 내리십시오. + 아래 데이터를 분석하여 '매수', '매도', '관망' 중 하나를 결정하세요. - [종목]: $stockName + [데이터 요약] + - 종목: $stockName $techSummary - [관련 뉴스]: $newsContext - [재무 기초]: $financialData + - 기업/재무: $financialData + - 시장 심리: $newsContext - 반드시 아래 JSON 형식으로만 답변하세요: + [스코어 산출 가이드 (0-100)] + 1. 초단기: 30분봉 추세, MFI, OBV 에너지가 일치하면 80점 이상. + 2. 단기: 일봉 이평선 정배열 및 3일 변동률 양수일 때 70점 이상. + 3. 중기: 주봉 추세와 재무 성장성(매출/영익)이 동반 상승 시 75점 이상. + 4. 장기: 월봉 위치와 기업의 근본적인 시장 지배력 기반 판단. + + [응답 형식] + 반드시 아래 JSON 형식으로만 답변하십시오: { + "ultraShortScore": (숫자), + "shortTermScore": (숫자), + "midTermScore": (숫자), + "longTermScore": (숫자), "decision": "BUY" | "SELL" | "HOLD", "reason": "결정적 근거 한 줄", "confidence": 0~100 } + <|eot_id|> + <|start_header_id|>user<|end_header_id|> + 모든 데이터를 종합하여 스코어링 리포트를 작성하십시오. + <|eot_id|><|start_header_id|>assistant<|end_header_id|> """.trimIndent() val response = chatModel.chat(UserMessage.from(prompt)) @@ -183,6 +203,10 @@ object RagService { } @Serializable class TradingDecision { + val ultraShortScore: Int = 0 // 초단기 (분봉/에너지) + val shortTermScore: Int = 0 // 단기 (일봉/뉴스) + val midTermScore: Int = 0 // 중기 (주봉/재무) + val longTermScore: Int = 0 var decision: String? = null var reason: String? = null var confidence: Int = 0 @@ -191,6 +215,10 @@ class TradingDecision { var financialData : String? = null override fun toString(): String { return """ +ultraShortScore :$ultraShortScore +shortTermScore :$shortTermScore +midTermScore :$midTermScore +longTermScore :$longTermScore decision: $decision reason: $reason confidence: $confidence diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 14f2d0d..d9c7034 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -10,9 +10,13 @@ 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) @@ -56,7 +60,43 @@ object TechnicalAnalyzer { 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) @@ -72,35 +112,71 @@ object TechnicalAnalyzer { // [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 """ - [종합 시계열 및 에너지 분석 보고서] - - 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)) } + [초단기 기술적 스켈핑 분석] + - 종합 스코어: ${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() @@ -170,4 +246,185 @@ object TechnicalAnalyzer { min30 = emptyList() } -} \ No newline at end of file +} + + +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() } +} diff --git a/src/main/kotlin/ui/AiAnalysisView.kt b/src/main/kotlin/ui/AiAnalysisView.kt index 2e818e2..ba65ac8 100644 --- a/src/main/kotlin/ui/AiAnalysisView.kt +++ b/src/main/kotlin/ui/AiAnalysisView.kt @@ -31,6 +31,7 @@ import service.AutoTradingManager fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, trades: List) { var aiOpinion by remember { mutableStateOf("분석 대기 중...") } var code by remember(stockCode) { + aiOpinion = "" mutableStateOf(stockCode.isNotEmpty()) } var isAnalyzing by remember { mutableStateOf(false) }