From d6cfcbd5796b6c37316bc6ac3596e6a0991b5261 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 26 Mar 2026 14:42:39 +0900 Subject: [PATCH] ... --- src/main/kotlin/database/DatabaseFactory.kt | 15 ++++ src/main/kotlin/network/KisAuthService.kt | 3 + .../kotlin/network/KisWebSocketManager.kt | 4 + src/main/kotlin/network/RagService.kt | 8 +- src/main/kotlin/service/AutoTradingManager.kt | 75 +++++++++++++++---- src/main/kotlin/service/LlamaServerManager.kt | 6 ++ src/main/kotlin/ui/TradingDecisionLog.kt | 59 ++++++++++++++- 7 files changed, 151 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index e6ab279..006cce8 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -448,6 +448,21 @@ object TradingLogStore { } } + fun addAnalyzer(name : String, code : String, log: String) { + synchronized(this) { + if (decisionLogs.size > 1000) decisionLogs.removeAt(0) + decisionLogs.add( + LogEntry( + time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")), + stockName = "$name[$code] 분석", + decision = "ANALYZER", + confidence = 100.0, + reason = log + ) + ) + } + } + fun addSettingLog(settingDesc : String, old : String, new : String, log: String) { synchronized(this) { if (decisionLogs.size > 1000) decisionLogs.removeAt(0) diff --git a/src/main/kotlin/network/KisAuthService.kt b/src/main/kotlin/network/KisAuthService.kt index b6c1ba9..781f779 100644 --- a/src/main/kotlin/network/KisAuthService.kt +++ b/src/main/kotlin/network/KisAuthService.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.coroutineScope import model.KisSession import model.TokenRequest import model.TokenResponse +import service.AutoTradingManager import java.time.LocalDateTime object KisAuthService { @@ -66,8 +67,10 @@ object KisAuthService { tradeToken = tData.access_token, tradeTokenExpiredAt = LocalDateTime.now().plusSeconds(tData.expires_in), ) + AutoTradingManager.tradeToken = true true } else { + AutoTradingManager.tradeToken = false false } } diff --git a/src/main/kotlin/network/KisWebSocketManager.kt b/src/main/kotlin/network/KisWebSocketManager.kt index e8e74de..81292ef 100644 --- a/src/main/kotlin/network/KisWebSocketManager.kt +++ b/src/main/kotlin/network/KisWebSocketManager.kt @@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import model.KisSession import model.RealTimeTrade +import service.AutoTradingManager import util.AesCrypto import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -90,6 +91,7 @@ object KisWebSocketManager { println("✅ 웹소켓 세션 진입 성공") session = this isConnected.set(true) + AutoTradingManager.webSocketConnect = true println("✅ 웹소켓 연결 성공") // 기존 구독 신청 로직 (H0STCNI0 등) @@ -111,12 +113,14 @@ object KisWebSocketManager { } } } catch (e: Exception) { + AutoTradingManager.webSocketConnect = false println("❌ 웹소켓 연결 끊김: ${e.message}") e.printStackTrace() } finally { println("🏁 웹소켓 finally 블록 진입 (연결 시도 종료)") isConnected.set(false) session = null + AutoTradingManager.webSocketConnect = false println("⏳ 5초 후 재연결 시도...") delay(5000) // 5초 후 재연결 시도 } diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index a5d602f..ecd42de 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -1,5 +1,6 @@ package network// src/main/kotlin/network/RagService.kt +import TradingLogStore import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore import dev.langchain4j.data.document.Metadata import dev.langchain4j.data.segment.TextSegment @@ -214,13 +215,18 @@ object RagService { ) tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() } result(tradingDecision, false) + TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") + println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true) } else { - println("${corpInfo?.cName} : ${scores.toString()}") + println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") + TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") tradingDecision.confidence = 1.0 result(tradingDecision, false) } } else { + TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}") + println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}") tradingDecision.confidence = 1.0 result(tradingDecision, false) } diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index c1d988a..27b8d58 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -5,6 +5,7 @@ import network.TradingDecision import TradingLogStore import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import getLlamaBinPath import kotlinx.coroutines.CoroutineScope @@ -35,6 +36,7 @@ import network.KisTradeService import network.KisWebSocketManager import network.RagService import network.StockUniverseLoader +import org.jetbrains.skia.ImageFilter import util.MarketUtil import java.time.LocalDateTime import java.time.LocalTime @@ -64,6 +66,12 @@ object AutoTradingManager { private val reanalysisList = mutableListOf() private val retryCountMap = mutableMapOf() var shouldShowFullWindow by mutableStateOf(false) + + var llmAnalyser by mutableStateOf(false) + var llmNews by mutableStateOf(false) + var tradeToken by mutableStateOf(false) + var webSocketConnect by mutableStateOf(false) + fun startBackgroundScheduler() { // scope.launch { // while (isActive) { @@ -123,15 +131,15 @@ object AutoTradingManager { var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence) val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide) - println(""" - 사명 : ${completeTradingDecision.corpName} - 신뢰도 : ${completeTradingDecision.confidence + append} - 단기성 : ${completeTradingDecision.shortPossible() + append} - 수익성 : ${completeTradingDecision.profitPossible()+ append} - 안전성 : ${completeTradingDecision.safePossible()+ append} - ${investmentGrade.displayName} : ${investmentGrade.description} - 총점 : ${totalScore} - """.trimIndent()) +// 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주 보장) @@ -167,7 +175,10 @@ object AutoTradingManager { TradingLogStore.addLog(completeTradingDecision,"BUY","[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}") resultCheck(completeTradingDecision) } - "SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}") + "SELL" -> { + TradingLogStore.addLog(completeTradingDecision,"SELL","[$stockCode] 매도 추천 resultCheck: ${completeTradingDecision?.reason}") + println("[$stockCode] 매도: ${completeTradingDecision?.reason}") + } "HOLD" -> { append = 0.0 TradingLogStore.addLog(completeTradingDecision,"HOLD","[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}") @@ -830,6 +841,40 @@ object FinancialAnalyzer { 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 // 당기순이익은 일단 흑자여야 함 + if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) { + if (isDebtSafe)buffer.appendLine( "부채비율 200% 이상") + if (isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만") + if (isNotDeficit)buffer.appendLine( "당기순이익 적자") + 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 또는 로그용) */ @@ -881,11 +926,11 @@ data class InvestmentScores( ) { override fun toString(): String { return """ - AVG : ${avg()} - ultraShort : $ultraShort - shortTerm : $shortTerm - midTerm : $midTerm - longTerm : $longTerm + 평점 : ${avg()} + 초단 : $ultraShort + 단기 : $shortTerm + 중기 : $midTerm + 장기 : $longTerm """.trimIndent() } diff --git a/src/main/kotlin/service/LlamaServerManager.kt b/src/main/kotlin/service/LlamaServerManager.kt index c0e27f5..89f88eb 100644 --- a/src/main/kotlin/service/LlamaServerManager.kt +++ b/src/main/kotlin/service/LlamaServerManager.kt @@ -106,6 +106,12 @@ object LlamaServerManager { // println("[Server $port] $line") if (line?.contains("server is listening") == true) { println("🚀 AI 서버 준비 완료 (Port: $port)") + if (port == 8080){ + AutoTradingManager.llmAnalyser = true + } + if (port == 8081){ + AutoTradingManager.llmNews = true + } if (processes.size > 1) { println("[Cache] ${processes.size}") RagService.active() diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index 92f9676..5a62c43 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -24,13 +24,31 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import model.ConfigIndex import model.KisSession +import service.AutoTradingManager @OptIn(ExperimentalMaterialApi::class) @Composable fun TradingDecisionLog() { var searchQuery by remember { mutableStateOf("") } var selectedFilter by remember { mutableStateOf("전체") } - val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING") + val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING","ANALYZER") + var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) } + LaunchedEffect(AutoTradingManager.llmAnalyser) { + llmAnalyser = AutoTradingManager.llmAnalyser + } + var llmNews by remember { mutableStateOf(AutoTradingManager.llmNews) } + LaunchedEffect(AutoTradingManager.llmNews) { + llmNews = AutoTradingManager.llmNews + } + var tradeToken by remember { mutableStateOf(AutoTradingManager.tradeToken) } + LaunchedEffect(AutoTradingManager.tradeToken) { + tradeToken = AutoTradingManager.tradeToken + } + var webSocketConnect by remember { mutableStateOf(AutoTradingManager.webSocketConnect) } + LaunchedEffect(AutoTradingManager.webSocketConnect) { + webSocketConnect = AutoTradingManager.webSocketConnect + } + // [핵심] 원본 로그에서 필터 조건에 맞는 리스트만 산출 val filteredLogs = TradingLogStore.decisionLogs.filter { log -> @@ -42,8 +60,17 @@ fun TradingDecisionLog() { Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) { Column(modifier = Modifier.weight(0.5f).padding(8.dp).fillMaxHeight().background(Color.White)) { - Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6) + Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6) + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Start + ) { + StatusIndicator("주식 분석기",llmAnalyser) + StatusIndicator("뉴스 처리기",llmNews) + StatusIndicator("한투 인증서",tradeToken) + StatusIndicator("실시간 감시",webSocketConnect) + } // [추가] 상단 검색 및 필터 UI Column(modifier = Modifier.padding(vertical = 8.dp)) { // 1. 검색창 @@ -95,6 +122,7 @@ fun TradingDecisionLog() { "SETTING" -> Color(0xFFFFA500) "SELL" -> Color(0xFF800080) "HOLD" -> Color.Gray + "ANALYZER" -> Color.Green else -> Color.Gray }, fontWeight = FontWeight.ExtraBold @@ -288,4 +316,29 @@ fun TradingDecisionLog() { } } } -} \ No newline at end of file +} + +@Composable +fun StatusIndicator(label: String, isActive: Boolean, onRestart: (() -> Unit)? = null) { + Row( + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + modifier = Modifier.padding(end = 12.dp) + ) { + Text(text = label, style = MaterialTheme.typography.body2) + Spacer(Modifier.width(4.dp)) + // 상태 아이콘 (초록 O / 빨간 X) + Text( + text = if (isActive) "●" else "×", + color = if (isActive) Color.Green else Color.Red, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + + // 문제가 있을 때(false)만 재구동 유도 버튼 표시 +// if (!isActive && onRestart != null) { +// TextButton(onClick = onRestart, contentPadding = PaddingValues(0.dp)) { +// Text("재구동", color = Color.Blue, fontSize = 11.sp, textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline) +// } +// } + } +}