From 55b82c23af4ee31d726bafbfa0f2400a3f3afda6 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 20 Feb 2026 15:21:38 +0900 Subject: [PATCH] .. --- src/main/kotlin/model/StockModels.kt | 7 +- src/main/kotlin/network/KisTradeService.kt | 8 +- src/main/kotlin/network/RagService.kt | 90 ++++++++++--------- src/main/kotlin/service/AutoTradingManager.kt | 67 ++++++++------ src/main/kotlin/ui/IntegratedOrderSection.kt | 9 +- 5 files changed, 101 insertions(+), 80 deletions(-) diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index b404610..fab462e 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -22,7 +22,8 @@ data class StockHolding( val pchs_avg_pric: String = "0", // 매입평균가 val prpr: String = "0", // 현재가 val evlu_pfls_rt: String = "0.0", // 평가손익률 - val evlu_amt: String = "0" // 평가금액 + val evlu_amt: String = "0" , // 평가금액 + val ord_psbl_qty : String = "0", ) @Serializable @@ -148,7 +149,9 @@ data class UnifiedStockHolding( val currentPrice: String, // 현재가 val profitRate: String, // 수익률 val evalAmount: String, // 평가금액 - val isDomestic: Boolean // 국내/해외 구분 + val isDomestic: Boolean, // 국내/해외 구분 + val availOrderCount : String, + ) @Serializable diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index b0e12d9..786be52 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -71,10 +71,11 @@ object KisTradeService { combinedHoldings.add(UnifiedStockHolding( code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty, avgPrice = it.pchs_avg_pric, currentPrice = it.prpr, - profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = true + profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = true, + availOrderCount = it.ord_psbl_qty ).apply { if (it.hldg_qty.toLong() > 0) { - println("보유 종목 : ${it.prdt_name} , 수량 : ${it.hldg_qty}") +// println("보유 종목 : ${it.prdt_name} , 수량 : ${it.hldg_qty}") } }) } @@ -84,7 +85,8 @@ object KisTradeService { combinedHoldings.add(UnifiedStockHolding( code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty, avgPrice = it.pchs_avg_pric, currentPrice = it.prpr, - profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = false + profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = false, + availOrderCount = it.ord_psbl_qty )) } diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index f32ac48..fda21f6 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -203,50 +203,50 @@ object RagService { /** * 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다. */ - fun askWithContext(question: String, - corpInfo: String, - financialData: String, - days : List, - weeks : List, - monthly : List): String { - val questionEmbedding = embeddingModel.embed(question).content() - val searchResult = embeddingStore.search( - EmbeddingSearchRequest.builder() - .queryEmbedding(questionEmbedding) - .maxResults(5) - .build() - ) - val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() } - - // 2. 종합 분석 프롬프트 구성 - val finalPrompt = """ - <|begin_of_text|><|start_header_id|>system<|end_header_id|> - 당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다. - 제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요. - - [데이터 세트] - 1. 기업 기본 정보: $corpInfo - 2. 재무 성장성: $financialData - 3. 기술적 추세: ${monthly}, ${weeks}, ${days} - 4. 최신 이슈(뉴스): $newsContext - - [분석 요청 사항] - 1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요. - 2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요. - 3. **장기/단기 전략**: - - 장기(재무/월봉 기반): 추천 혹은 비추천 사유 - - 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유 - 4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요. - <|eot_id|> - <|start_header_id|>user<|end_header_id|> - 질문: $question - <|eot_id|><|start_header_id|>assistant<|end_header_id|> - """.trimIndent() - - val response = chatModel.chat(UserMessage.from(finalPrompt)) -// println(response) - return response.aiMessage().text() - } +// fun askWithContext(question: String, +// corpInfo: String, +// financialData: String, +// days : List, +// weeks : List, +// monthly : List): String { +// val questionEmbedding = embeddingModel.embed(question).content() +// val searchResult = embeddingStore.search( +// EmbeddingSearchRequest.builder() +// .queryEmbedding(questionEmbedding) +// .maxResults(5) +// .build() +// ) +// val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() } +// +// // 2. 종합 분석 프롬프트 구성 +// val finalPrompt = """ +// <|begin_of_text|><|start_header_id|>system<|end_header_id|> +// 당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다. +// 제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요. +// +// [데이터 세트] +// 1. 기업 기본 정보: $corpInfo +// 2. 재무 성장성: $financialData +// 3. 기술적 추세: ${monthly}, ${weeks}, ${days} +// 4. 최신 이슈(뉴스): $newsContext +// +// [분석 요청 사항] +// 1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요. +// 2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요. +// 3. **장기/단기 전략**: +// - 장기(재무/월봉 기반): 추천 혹은 비추천 사유 +// - 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유 +// 4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요. +// <|eot_id|> +// <|start_header_id|>user<|end_header_id|> +// 질문: $question +// <|eot_id|><|start_header_id|>assistant<|end_header_id|> +// """.trimIndent() +// +// val response = chatModel.chat(UserMessage.from(finalPrompt)) +//// println(response) +// return response.aiMessage().text() +// } suspend fun decideTrading( stockName: String, @@ -342,6 +342,8 @@ object RagService { } + + @Serializable class TradingDecision { var corpName : String = "" diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 5ca59f7..c46e877 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -89,13 +89,13 @@ object AutoTradingManager { // val balance = tradeService.fetchIntegratedBalance().getOrNull() val holding = balance?.holdings?.find { it.code == item.code } - if (holding != null && holding.quantity.toInt() > 0) { + if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0) { var final = MarketUtil.roundToTickSize(item.targetPrice) println("🔄 [재주문] ${item.name} (${item.code}) ${item.orderedPrice} ${final} 전날 미체결 매도 건 재주문 시도") // 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송 tradeService.postOrder( stockCode = item.code, - qty = item.quantity.toString(), + qty = min(item.quantity,holding.availOrderCount.toInt()).toString(), price = final.toLong().toString(), isBuy = false ).onSuccess { newOrderNo -> @@ -125,12 +125,12 @@ object AutoTradingManager { println("⏱️ [Cycle Start] ${LocalTime.now()}") // [프로세스 1] 장 마감 및 잔고 체크 -// val now = LocalTime.now(ZoneId.of("Asia/Seoul")) -// //&& now.isBefore(LocalTime.of(15, 30)) -// if (now.isAfter(LocalTime.of(15, 30)) ) { -//// executeClosingLiquidation(tradeService) -// return@withTimeout -// } + val now = LocalTime.now(ZoneId.of("Asia/Seoul")) + //&& now.isBefore(LocalTime.of(15, 30)) + if (now.isAfter(LocalTime.of(15, 15)) ) { + executeClosingLiquidation(tradeService) + return@withTimeout + } val balance = tradeService.fetchIntegratedBalance().getOrNull() @@ -142,7 +142,18 @@ object AutoTradingManager { if (remainingCandidates.isEmpty()) { val candidates: MutableList = fetchCandidates(tradeService).apply { println("후보군 총 개수 : $size") - }.filter { !it.name.contains("호스팩", true) } + }.filter { + val rate = it.prdy_ctrt.toDouble() + val corpInfo = DartCodeManager.getCorpCode(it.code) + val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15) +// if (isOk) {println("${it.name} : ${it.prdy_ctrt}")} + if (corpInfo?.cName.isNullOrEmpty()) { + false + }else { + isOk + } + } + .filter { !it.name.contains("호스팩", true) } .sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) } .toMutableList() @@ -299,25 +310,25 @@ object AutoTradingManager { -// private suspend fun executeClosingLiquidation(tradeService: KisTradeService) { -// val activeTrades = DatabaseFactory.findAllMonitoringTrades() -// val balanceResult = tradeService.fetchIntegratedBalance().getOrNull() -// val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap() -// -// activeTrades.forEach { trade -> -// try { -// if (!realHoldings.containsKey(trade.code)) { -// DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED) -// return@forEach -// } -// // 마감 정리 로직 (필요 시 주석 해제하여 사용) -// println("📢 [마감 정리 체크] ${trade.name}") -// } catch (e: Exception) { -// println("⚠️ [마감 에러] ${trade.name}: ${e.message}") -// } -// delay(200) -// } -// } + private suspend fun executeClosingLiquidation(tradeService: KisTradeService) { + val activeTrades = DatabaseFactory.findAllMonitoringTrades() + val balanceResult = tradeService.fetchIntegratedBalance().getOrNull() + val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap() + + activeTrades.forEach { trade -> + try { + if (!realHoldings.containsKey(trade.code)) { + DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.EXPIRED) + return@forEach + } + // 마감 정리 로직 (필요 시 주석 해제하여 사용) + println("📢 [마감 정리 체크] ${trade.name}") + } catch (e: Exception) { + println("⚠️ [마감 에러] ${trade.name}: ${e.message}") + } + delay(200) + } + } fun stopDiscovery() { discoveryJob?.cancel() diff --git a/src/main/kotlin/ui/IntegratedOrderSection.kt b/src/main/kotlin/ui/IntegratedOrderSection.kt index cd3a754..07612d3 100644 --- a/src/main/kotlin/ui/IntegratedOrderSection.kt +++ b/src/main/kotlin/ui/IntegratedOrderSection.kt @@ -28,6 +28,7 @@ import network.KisTradeService import service.AutoTradingManager import util.MarketUtil import kotlin.math.min +import kotlin.math.roundToInt enum class InvestmentGrade( val displayName: String, @@ -259,7 +260,7 @@ fun IntegratedOrderSection( basePrice = completeTradingDecision.currentPrice println("basePrice $basePrice") val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) - val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) + var maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) val buyWeight = KisSession.config.getValues(ConfigIndex.BUY_WEIGHT_INDEX) val baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) @@ -291,7 +292,9 @@ fun IntegratedOrderSection( println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장) - + val gradeRate = (1.0 - (investmentGrade.ordinal * 0.1)) + val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt() + maxBudget = maxBudget * gradeRate val calculatedQty = if (basePrice > 0) { (maxBudget / basePrice).toInt().coerceAtLeast(1) } else { @@ -300,7 +303,7 @@ fun IntegratedOrderSection( // 5. 매수 실행 (계산된 finalMargin 전달) excuteTrade( willEnableAutoSell = true, - orderQty = min(calculatedQty, KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX).toInt()).toString(), + orderQty = min(calculatedQty, maxQty).toString(), profitRate1 = finalMargin, investmentGrade = investmentGrade, )