diff --git a/src/main/kotlin/network/NewsService.kt b/src/main/kotlin/network/NewsService.kt index e44e971..94fde6f 100644 --- a/src/main/kotlin/network/NewsService.kt +++ b/src/main/kotlin/network/NewsService.kt @@ -21,6 +21,7 @@ import model.DartFinancialResponse import model.KisSession import model.NaverNewsResponse import service.DynamicNewsScraper +import service.FinancialAnalyzer import service.SafeScraper import service.UrlCacheManager import kotlin.Double @@ -94,7 +95,7 @@ object NewsService { var buffer : StringBuffer = StringBuffer() buffer.append("[재무 분석 데이터]").append("\n") response.list.forEach { it - buffer.append("${it.account_nm} (당기)${it?.thstrm_amount}, (전기)${it?.frmtrm_amount}").append("\n") + buffer.append("${it.account_nm} (당기)${it.thstrm_amount} (전기)${it.frmtrm_amount}").append("\n") } return buffer.toString() } catch (e: Exception) { @@ -114,6 +115,7 @@ object FinancialMapper { if (rawText.isBlank()) { return FinancialStatement() } +// println(rawText) val currentValues = extractYearlyValues(rawText, "당기") val previousValues = extractYearlyValues(rawText, "전기") @@ -148,17 +150,23 @@ object FinancialMapper { quickRatio = quickRatio, isOperatingProfitPositive = opCurrent > 0, isNetIncomePositive = niCurrent > 0 - ) + ).apply { + println("당기순이익: ${niCurrent} , isSafetyBeltMet ${FinancialAnalyzer.isSafetyBeltMet(this)}") + } } private fun extractYearlyValues(text: String, type: String): Map { val result = mutableMapOf() - // 정규식 설명: 항목명 뒤의 (당기/전기) 괄호 안의 숫자와 콤마를 찾아 숫자로 변환 - val regex = Regex("""([가-힣\s()]+)\s\(?$type\)?([-0-9,.]+)""") + + // 핵심 수정: 항목명 뒤에 (당기) 또는 (전기)가 오고, 그 직후의 숫자(마이너스, 쉼표 포함)를 캡처 + // 쉼표나 공백으로 끝나는 지점까지 찾습니다. + val regex = Regex("""([가-힣\s()]+)\s\($type\)([-0-9,.]+)""") + regex.findAll(text).forEach { match -> val key = match.groupValues[1].trim() - val value = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0 - result[key] = value + // 숫자 내 쉼표 제거 후 Double 변환 + val rawValue = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0 + result[key] = rawValue } return result } diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 707c636..a5d602f 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -201,7 +201,6 @@ object RagService { val scores = technicalAnalyzer.calculateScores(financialScore) if (scores.avg() > 50) { result(tradingDecision, false) - tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport() result(tradingDecision, false) @@ -217,9 +216,12 @@ object RagService { result(tradingDecision, false) result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true) } else { + println("${corpInfo?.cName} : ${scores.toString()}") + tradingDecision.confidence = 1.0 result(tradingDecision, false) } } else { + tradingDecision.confidence = 1.0 result(tradingDecision, false) } }catch (e: Exception) { diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index dcd20f3..c1d988a 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -3,7 +3,6 @@ package service import AutoTradeItem import network.TradingDecision import TradingLogStore -import TradingLogStore.decisionLogs import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -358,7 +357,7 @@ object AutoTradingManager { } // 2. 메인 루프 실행 - runDiscoveryLoop(KisTradeService, globalCallback) + runDiscoveryLoop(globalCallback) } @@ -375,6 +374,7 @@ object AutoTradingManager { targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice)) println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도") +// TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문[예상수익 : ${holding.profitRate}] ") tradeService.postOrder( stockCode = holding.code, qty = holding.availOrderCount, @@ -384,8 +384,10 @@ object AutoTradingManager { // 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지 // DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo) println("✅ [재주문 완료] ${holding.name}: $newOrderNo") + TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문 완료[예상수익 : ${holding.profitRate}] ") }.onFailure { - println("❌ [재주문 실패] ${holding.name}: ${it.message}") + TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문 실패[${it.message}] ") +// println("❌ [재주문 실패] ${holding.name}: ${it.message}") } } else { @@ -437,9 +439,9 @@ object AutoTradingManager { var currentTimeMillis = System.currentTimeMillis() var waitTime = 0.2 val H16 = LocalTime.of(16, 0) - val H08M50 = LocalTime.of(8, 50) + val H08M35 = LocalTime.of(8, 35) val H08M30 = LocalTime.of(8, 30) - private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) { + private fun runDiscoveryLoop(callback: TradingDecisionCallback) { discoveryJob = scope.launch { println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}") while (isActive) { @@ -447,34 +449,11 @@ object AutoTradingManager { now = LocalTime.now(ZoneId.of("Asia/Seoul")) currentTimeMillis = System.currentTimeMillis() lastTickTime.set(System.currentTimeMillis()) // 생존 신고 - // [수정] 16시 이후이거나 8시 30분 이전이면 모든 로직 중단 및 초기화 - if (now.isAfter(H16) || now.isBefore(H08M30)) { - println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.") - onMarketClosed?.invoke() - - - KisWebSocketManager.disconnect() - BrowserManager.closeIfIdle(0) - LlamaServerManager.stopAll() // AI 서버 완전 종료 - TradingLogStore.clear() - // Main.kt에 설정 화면으로 가라고 신호 전송 - stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐) - return@launch - } else if (now.isAfter(H08M30) && now.isBefore(H08M50) && !isSystemReadyToday) { - if (MarketUtil.canTradeToday()) { - println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.") - tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true) - } else { - println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.") - isSystemReadyToday = false - delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여 - continue - } - } when { - - //장중 - now.isBefore(H16) && now.isAfter(H08M50) -> { + now.isAfter(H16) || now.isBefore(H08M35) -> { + prepareMarketOpen(now) + } + now.isBefore(H16) && now.isAfter(H08M35) -> { waitTime = 0.2 if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) { if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) { @@ -488,122 +467,18 @@ object AutoTradingManager { } withTimeout(CYCLE_TIMEOUT) { println("⏱️ [Cycle Start] ${LocalTime.now()}") - - val now = LocalTime.now(ZoneId.of("Asia/Seoul")) - if (now.isAfter(LocalTime.of(15, 20))) { - executeClosingLiquidation(tradeService) - return@withTimeout - } - - val balance = tradeService.fetchIntegratedBalance().getOrNull() - balance?.let { resumePendingSellOrders(tradeService, it) } - val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L - val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet() - val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code } - if (remainingCandidates.isEmpty()) { - val stocks = StockUniverseLoader.loadUniverse().shuffled().take(100) - println("✅ 총 ${stocks.size}개의 종목을 로드했습니다.") - stocks.forEach { (code, name) -> -// println("📌 로드됨: [$code] $name") - addToReanalysis(RankingStock(mksc_shrn_iscd = code, hts_kor_isnm = name)) - } - val candidates: MutableList = fetchCandidates(tradeService).apply { - println("후보군 총 개수 : $size") - }.filter { - val rate = it.prdy_ctrt.toDouble() - val corpInfo = DartCodeManager.getCorpCode(it.code) - val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15) - if (corpInfo?.cName.isNullOrEmpty()) { - false - } else { - isOk - } - } - .filter { !it.name.contains("호스팩", true) } - .sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) } - .toMutableList() - - if (reanalysisList.isNotEmpty()) { - candidates.addAll(reanalysisList.asReversed()) - } - reanalysisList.clear() - remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList} - .distinctBy { it.code }) + if (now.isAfter(H16)) { + executeClosingLiquidation(KisTradeService) } else { - println("미확인 데이터 ${remainingCandidates.size}") -// remainingCandidates.removeIf { it.code in myHoldings || it.code in pendingStocks || it.code in executionCache.values.map { it.code } || it.code in failList} - } - - - var totalCount = remainingCandidates.size - println("후보군 조건 충족 총 개수 : ${totalCount}") - val iterator = remainingCandidates.iterator() - - while (iterator.hasNext()) { - totalCount-- - val stock = iterator.next() - try { - processSingleStock(stock, myCash, tradeService, callback) - } catch (e: Exception) { - println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}") - } finally { - iterator.remove() - } - println("남은 후보군 개수 : ${totalCount}") - delay(100) - } - println("⏱️ [Cycle End] ${LocalTime.now()}") - } - } - - //장외 - now.isAfter(H16) || now.isBefore(H08M50) -> { - when { - (now.hour == 0 && now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> { - waitTime = 10.0 - isSystemReadyToday = false - isSystemCleanedUpToday = false - } - - (now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> { - waitTime = 3.0 - if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) { - KisWebSocketManager.disconnect() - tryRefreshToken() - } - } - - (now.isAfter(LocalTime.of(18, 0))) -> { - try { - waitTime = 5.0 - println("current SystemCleanedUpToday is $isSystemCleanedUpToday") - if (!isSystemCleanedUpToday) { - println("🌙 [System] 업무 종료 및 자원 정리 시작...") - SystemSleepPreventer.sleepDisplay() // 모니터 끄기 - KisWebSocketManager.disconnect() - BrowserManager.closeIfIdle(0) // 즉시 닫기 - if (LlamaServerManager.stopAll()) { - isSystemCleanedUpToday = true - } - - } - println("✅ [System] 오늘의 모든 정리가 완료되었습니다.") - } catch (e: Exception) { - } - } - - (now.isAfter(LocalTime.of(18, 15)) && now.minute % 15 == 0) -> { - try { - waitTime = 5.0 - SystemSleepPreventer.sleepDisplay() // 모니터 끄기 - } catch (e: Exception) { - } - } - else -> { - waitTime = 5.0 + executeMarketLoop() } } } +// +// //장외 +// now.isAfter(H16) || now.isBefore(H08M35) -> { +// finalizeMarketClose(now) +// } else ->{ waitTime = 3.0 } @@ -619,6 +494,150 @@ object AutoTradingManager { } } + suspend fun prepareMarketOpen(now : LocalTime) { + if (now.isAfter(H16) || now.isBefore(H08M30)) { + println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.") + onMarketClosed?.invoke() + KisWebSocketManager.disconnect() + BrowserManager.closeIfIdle(0) + LlamaServerManager.stopAll() // AI 서버 완전 종료 + TradingLogStore.clear() + // Main.kt에 설정 화면으로 가라고 신호 전송 + stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐) + } else if (now.isAfter(H08M30) && now.isBefore(H08M35) && !isSystemReadyToday) { + if (MarketUtil.canTradeToday()) { + println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.") + tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true) + } else { + println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.") + isSystemReadyToday = false + delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여 + } + } + } + var loadedTops = mutableListOf>() + + fun poll100Stocks(): List> { + val count = minOf(loadedTops.size, 100) + if (count == 0) return emptyList() + + // 앞의 100개를 복사 + val batch = loadedTops.subList(0, count).toList() + + // 원본에서 삭제 (이 작업이 큐의 pop/remove 역할을 합니다) + loadedTops.subList(0, count).clear() + + return batch + } + + suspend fun executeMarketLoop() { + val balance = KisTradeService.fetchIntegratedBalance().getOrNull() + balance?.let { resumePendingSellOrders(KisTradeService, it) } + val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L + val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet() + val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code } + if (remainingCandidates.isEmpty()) { + if (loadedTops.size < 100) { + loadedTops.addAll(StockUniverseLoader.loadUniverse()) + loadedTops.shuffle() + println("✅ 총 ${loadedTops.size}개의 종목이 로드되있음.") + } + poll100Stocks().forEach { (code, name) -> + addToReanalysis(RankingStock(mksc_shrn_iscd = code, hts_kor_isnm = name)) + } + + val candidates: MutableList = fetchCandidates(KisTradeService).apply { + }.filter { + val rate = it.prdy_ctrt.toDouble() + val corpInfo = DartCodeManager.getCorpCode(it.code) + val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15) + if (corpInfo?.cName.isNullOrEmpty()) { + false + } else { + isOk + } + } + .filter { !it.name.contains("호스팩", true) } + .sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) } + .toMutableList() + + if (reanalysisList.isNotEmpty()) { + candidates.addAll(reanalysisList) + } + reanalysisList.clear() + remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList} + .distinctBy { it.code }) + } else { + println("미확인 데이터 ${remainingCandidates.size}") + } + + var totalCount = remainingCandidates.size + println("후보군 조건 충족 총 개수 : ${totalCount}") + val iterator = remainingCandidates.iterator() + while (iterator.hasNext()) { + totalCount-- + val stock = iterator.next() + try { + processSingleStock(stock, myCash, KisTradeService, globalCallback) + } catch (e: Exception) { + println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}") + } finally { + iterator.remove() + } + println("남은 후보군 개수 : ${totalCount}") + delay(100) + } + println("⏱️ [Cycle End] ${LocalTime.now()}") + } + + suspend fun finalizeMarketClose(now: LocalTime) { + when { + (AutoTradingManager.now.hour == 0 && AutoTradingManager.now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> { + waitTime = 10.0 + isSystemReadyToday = false + isSystemCleanedUpToday = false + } + + (AutoTradingManager.now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> { + waitTime = 3.0 + if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) { + KisWebSocketManager.disconnect() + tryRefreshToken() + } + } + + (AutoTradingManager.now.isAfter(LocalTime.of(18, 0))) -> { + try { + waitTime = 5.0 + println("current SystemCleanedUpToday is $isSystemCleanedUpToday") + if (!isSystemCleanedUpToday) { + println("🌙 [System] 업무 종료 및 자원 정리 시작...") + SystemSleepPreventer.sleepDisplay() // 모니터 끄기 + KisWebSocketManager.disconnect() + BrowserManager.closeIfIdle(0) // 즉시 닫기 + if (LlamaServerManager.stopAll()) { + isSystemCleanedUpToday = true + } + + } + println("✅ [System] 오늘의 모든 정리가 완료되었습니다.") + } catch (e: Exception) { + } + } + + (AutoTradingManager.now.isAfter(LocalTime.of(18, 15)) && AutoTradingManager.now.minute % 15 == 0) -> { + try { + waitTime = 5.0 + SystemSleepPreventer.sleepDisplay() // 모니터 끄기 + } catch (e: Exception) { + } + } + else -> { + waitTime = 5.0 + } + } + } + fun addToReanalysis(stock: RankingStock) { val count = retryCountMap.getOrDefault(stock.code, 0) if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지 @@ -862,14 +881,16 @@ data class InvestmentScores( ) { override fun toString(): String { return """ - ultraShort $ultraShort - shortTerm $shortTerm - midTerm $midTerm - longTerm $longTerm + AVG : ${avg()} + ultraShort : $ultraShort + shortTerm : $shortTerm + midTerm : $midTerm + longTerm : $longTerm """.trimIndent() } fun avg() = listOf(ultraShort, shortTerm, midTerm, longTerm).average() + } @Serializable diff --git a/src/main/kotlin/ui/SettingsScreen.kt b/src/main/kotlin/ui/SettingsScreen.kt index 2d3d226..6338d67 100644 --- a/src/main/kotlin/ui/SettingsScreen.kt +++ b/src/main/kotlin/ui/SettingsScreen.kt @@ -96,7 +96,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { break // 성공하면 루프 탈출 } } - delay(60_000 * 2) // 1분마다 시간 체크 + delay(30_000) // 1분마다 시간 체크 } }