From 1cca11edc4fde45c6a1389447f38ec38a0dc6491 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 26 May 2026 11:20:37 +0900 Subject: [PATCH] .... --- src/main/kotlin/database/DatabaseFactory.kt | 2 +- src/main/kotlin/model/AppConfig.kt | 3 + src/main/kotlin/network/RagService.kt | 2 +- src/main/kotlin/service/AutoTradingManager.kt | 202 ++++++++++++------ 4 files changed, 138 insertions(+), 71 deletions(-) diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index b8e0103..8176959 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -624,7 +624,7 @@ object TradingLogStore { val lastSentTime = noticeFilter[code.uppercase()] // 기록이 없거나(처음 보냄), 마지막 발송 기준 30분이 지났다면 - if (lastSentTime == null || (current - lastSentTime > 1000 * 60 * 30L)) { + if (lastSentTime == null || (current - lastSentTime > (1000 * 60) * KisSession.tradeConfig.noticeGapTime)) { isSendable = true noticeFilter[code.uppercase()] = current // 즉시 발송 시간 갱신하여 중복 방지 } diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 547eb9e..874bff7 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -255,6 +255,9 @@ class TradeConfig { var excuteCountOnMin : Int = 2 var autoSellOrder : Boolean = false var excuteMinCheck : Int = 2 + var noticeGapTime : Int = 60 + var lowerAveragePrice : Boolean = false + var lowerAverageStockCount : Int = 1 } diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 8445142..53cef01 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -250,7 +250,7 @@ object RagService { if (isSafetyBeltStockCodes.contains(stockCode)) { // 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스 - // logTime(stockName, "재무 미달 (캐시) 조기 종료", 0, System.currentTimeMillis() - totalStartTime) + println("재무 안정성 부족 (캐시)") result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false) return@coroutineScope } diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index e0d4263..474171d 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -38,6 +38,7 @@ import network.KisAuthService import network.KisTradeService import network.KisWebSocketManager import network.RagService +import network.RagService.isSafetyBeltStockCodes import network.StockUniverseLoader import report.SnapshotType import report.TradingReportManager @@ -108,9 +109,9 @@ object AutoTradingManager { if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) { val decision = completeTradingDecision + println("${decision.stockName} ${decision.decision} ${buysCodes.contains(decision.stockCode) == false}") // 1. 이미 AI가 결정한 decision과 confidence를 신뢰함 - if (decision.decision == "BUY") { - val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) + if (decision.decision == "BUY" && buysCodes.contains(decision.stockCode) == false) { // AI가 이미 검증한 등급을 사용 (재계산 불필요) val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE @@ -118,8 +119,14 @@ object AutoTradingManager { // 2. 최종 매수 실행 val gradeRate = KisSession.config.getValues(grade.allocationRate) val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate - val calculatedQty = (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1) + TradingLogStore.addLog(decision,"BUY",decision.summary()) + var hasCodes = currentBalance?.getHoldings()?.any { it.code.equals(decision.stockCode) && it.quantity.toInt() > 2 && it.isTodayEntry == false} + if (hasCodes == true) { + buysCodes.add(decision.stockCode) + TradingLogStore.addNotice(decision.stockName,decision.stockCode,"물타기 시도 1주 매수") + } + val calculatedQty = if(hasCodes == true) KisSession.tradeConfig.lowerAverageStockCount else (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1) excuteTrade( decision = decision, orderQty = calculatedQty.toString(), @@ -202,7 +209,7 @@ object AutoTradingManager { rawGrade } } - + var buysCodes = arrayListOf() fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) { scope.launch { var basePrice = decision.currentPrice @@ -219,33 +226,41 @@ object AutoTradingManager { AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") } else if (KisSession.isAvailBuyTime(LocalTime.now())){ - println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice") - - KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true) + println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${stockCode.contains(stockCode)}" ) + var realOrderQty = orderQty + KisTradeService.postOrder(stockCode, realOrderQty, finalPrice.toLong().toString(), isBuy = true) .onSuccess { realOrderNo -> println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice") - TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 주문 성공: $realOrderNo") + TradingLogStore.addLog( + decision, + "BUY", + "[${investmentGrade.displayName}] 주문 성공: $realOrderNo" + ) val sRate = -1.5 var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX) - val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax + val effectiveProfitRate = + (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax - val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0)) + val calculatedTarget = + MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0)) val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0)) val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 - DatabaseFactory.saveAutoTrade(AutoTradeItem( - orderNo = realOrderNo, - code = stockCode, - name = stockName, - quantity = inputQty, - profitRate = effectiveProfitRate, - stopLossRate = sRate, - targetPrice = calculatedTarget, - stopLossPrice = calculatedStop, - status = "PENDING_BUY", - isDomestic = true - )) + DatabaseFactory.saveAutoTrade( + AutoTradeItem( + orderNo = realOrderNo, + code = stockCode, + name = stockName, + quantity = inputQty, + profitRate = effectiveProfitRate, + stopLossRate = sRate, + targetPrice = calculatedTarget, + stopLossPrice = calculatedStop, + status = "PENDING_BUY", + isDomestic = true + ) + ) TradingReportManager.recordTradeDecision( orderNo = realOrderNo, @@ -260,16 +275,34 @@ object AutoTradingManager { syncAndExecute(realOrderNo) // 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출 - TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo") + TradingLogStore.addLog( + decision, + "BUY", + "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${ + String.format( + "%.4f", + effectiveProfitRate + ) + }%): $realOrderNo" + ) } .onFailure { println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice") if (it.message?.contains("주문가능금액을 초과") == true) { - AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) - TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") + AutoTradingManager.addToReanalysis( + RankingStock( + mksc_shrn_iscd = stockCode, + hts_kor_isnm = stockName + ) + ) + TradingLogStore.addWatchLog( + decision, + "WATCH", + "${it.message ?: " 매수 실패"} => 재분석 대기열에 추가" + ) } else { - TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") + TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패") } } } else { @@ -342,7 +375,6 @@ object AutoTradingManager { // ✅ 2. 매도 완료 시점 (실제 매도 체결가) val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0 val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity - // 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록 TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty) @@ -534,7 +566,7 @@ object AutoTradingManager { } else { var errMsg = "" var isSuccess = false - if (KisSession.tradeConfig.autoSellOrder + if (KisSession.tradeConfig.autoSellOrder && holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() <= -15.0 @@ -561,34 +593,34 @@ object AutoTradingManager { "매수가 기준 (${holding.avgPrice.toDouble()} 3호가 위[${targetPrice}] 매도 주문 ${if (isSuccess) "성공" else "실패[${errMsg}]"}" ) } else if (KisSession.config.stop_Loss - && holding != null && holding.quantity.toInt() > 0 - && holding.availOrderCount.toInt() > 0 - && holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE) - && holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE) - && holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) { - println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}") - val profit = holding.profitRate.toDouble() - var targetPrice = holding.currentPrice.toDouble() - targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0) + && holding != null && holding.quantity.toInt() > 0 + && holding.availOrderCount.toInt() > 0 + && holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE) + && holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE) + && holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) { + println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}") + val profit = holding.profitRate.toDouble() + var targetPrice = holding.currentPrice.toDouble() + targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0) - tradeService.postOrder( - stockCode = holding.code, - qty = holding.availOrderCount, - price = targetPrice.toInt().toString(), - isBuy = false, - ).onSuccess { newOrderNo -> - println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.") - }.onFailure { err-> - println("✅ [보유 주식 손절 처리] 실패 ${err.message}") - } + tradeService.postOrder( + stockCode = holding.code, + qty = holding.availOrderCount, + price = targetPrice.toInt().toString(), + isBuy = false, + ).onSuccess { newOrderNo -> + println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.") + }.onFailure { err-> + println("✅ [보유 주식 손절 처리] 실패 ${err.message}") + } - TradingLogStore.addNotice( - "보유주식[${holding.name}]", - holding.code, - "수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도." - ) - } + TradingLogStore.addNotice( + "보유주식[${holding.name}]", + holding.code, + "수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도." + ) + } analyzeDeepLossHoldingsAfterMarket(holding , true) } delay(200) // API 호출 부하 방지 @@ -914,7 +946,20 @@ object AutoTradingManager { 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} + if (KisSession.tradeConfig.lowerAveragePrice) { + currentBalance?.getHoldings()?.map { + if(!it.isTodayEntry && it.quantity.toInt() > 2) { + remainingCandidates.add(RankingStock(mksc_shrn_iscd = it.code, hts_kor_isnm = it.name)) + } + } + } + remainingCandidates.addAll(candidates.filter { + if (KisSession.tradeConfig.lowerAveragePrice) { true } else {it.code !in myHoldings} && + it.code !in pendingStocks && + it.code !in executionCache.values.map { it.code } && + it.code !in failList && + it.code !in isSafetyBeltStockCodes + } .distinctBy { it.code }) } else { println("미확인 데이터 ${remainingCandidates.size}") @@ -1001,6 +1046,7 @@ object AutoTradingManager { } if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 } if (now.hour >= 20) { + buysCodes.clear() executionCountMap.clear() noticeFilter.clear() } @@ -1034,7 +1080,8 @@ object AutoTradingManager { this.stockName = stock.name }, false) - val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout + val dailyData = + tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout val today = dailyData.lastOrNull() ?: null if (today == null) { failList.add(stock.code) @@ -1043,29 +1090,46 @@ object AutoTradingManager { } val currentPrice = today.stck_prpr.toDouble() - if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) { + if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) { print("-> 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${minPrice}, 최대 기준:${maxPrice}] | ") return@withTimeout } println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})") + if (!isSafetyBeltStockCodes.contains(stock.code)) { - val analyzer = coroutineScope { - val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } - val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) } - val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) } - TechnicalAnalyzer().apply { - this.daily = dailyData - this.min30 = min30.await() - this.weekly = weekly.await() - this.monthly = monthly.await() + + + val analyzer = coroutineScope { + val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } + delay(20) + val weekly = + async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) } + delay(20) + val monthly = + async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) } + delay(20) + TechnicalAnalyzer().apply { + this.daily = dailyData + delay(50) + this.min30 = min30.await() + delay(50) + this.weekly = weekly.await() + delay(50) + this.monthly = monthly.await() + } } - } - if (analyzer.isValid()) { - RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess -> - callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess) + if (analyzer.isValid()) { + println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})") + RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess -> + callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess) + } + } else { + println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})") } + } else { + println("재무 안정성 부족 (캐시)") } println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})") }