From ef1847f1159d49c974402583a05cef3cc69451f1 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 2 Apr 2026 14:05:14 +0900 Subject: [PATCH] ... --- src/main/kotlin/Main.kt | 10 ++ src/main/kotlin/database/DatabaseFactory.kt | 38 ++++- src/main/kotlin/model/AppConfig.kt | 33 ++++- src/main/kotlin/model/StockModels.kt | 56 -------- src/main/kotlin/network/RagService.kt | 7 +- src/main/kotlin/service/AutoTradingManager.kt | 131 +++++++++++++++--- src/main/kotlin/ui/IntegratedOrderSection.kt | 2 +- src/main/kotlin/ui/TradingDecisionLog.kt | 34 +++-- 8 files changed, 215 insertions(+), 96 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 8a4f787..faa3086 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,11 +1,16 @@ +import ConfigTable.grade_1_allocationrate import ConfigTable.grade_1_buy import ConfigTable.grade_1_profit +import ConfigTable.grade_2_allocationrate import ConfigTable.grade_2_buy import ConfigTable.grade_2_profit +import ConfigTable.grade_3_allocationrate import ConfigTable.grade_3_buy import ConfigTable.grade_3_profit +import ConfigTable.grade_4_allocationrate import ConfigTable.grade_4_buy import ConfigTable.grade_4_profit +import ConfigTable.grade_5_allocationrate import ConfigTable.grade_5_buy import ConfigTable.grade_5_profit import ConfigTable.max_count @@ -195,6 +200,11 @@ fun main() = application { GRADE_2_PROFIT = it[grade_2_profit], GRADE_1_BUY = it[grade_1_buy], GRADE_1_PROFIT = it[grade_1_profit], + GRADE_1_ALLOCATIONRATE = it[grade_1_allocationrate], + GRADE_2_ALLOCATIONRATE = it[grade_2_allocationrate], + GRADE_3_ALLOCATIONRATE = it[grade_3_allocationrate], + GRADE_4_ALLOCATIONRATE = it[grade_4_allocationrate], + GRADE_5_ALLOCATIONRATE = it[grade_5_allocationrate], MAX_COUNT = it[max_count], ) } diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index e80c8e1..000424b 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -47,15 +47,34 @@ object ConfigTable : Table("app_config") { val min_purchase_score = double("min_purchase_score").default( 65.0) val sell_profit = double("sell_profit").default( 1.0) val grade_5_buy = integer("grade_5_buy").default(0) - val grade_5_profit = double("grade_5_profit").default(1.8) val grade_4_buy = integer("grade_4_buy").default(1) - val grade_4_profit = double("grade_4_profit").default(1.3) val grade_3_buy = integer("grade_3_buy").default(1) - val grade_3_profit = double("grade_3_profit").default(0.9) val grade_2_buy = integer("grade_2_buy").default(2) - val grade_2_profit = double("grade_2_profit").default(0.7) val grade_1_buy = integer("grade_1_buy").default(3) + val grade_5_profit = double("grade_5_profit").default(1.8) + val grade_4_profit = double("grade_4_profit").default(1.3) + val grade_3_profit = double("grade_3_profit").default(0.9) + val grade_2_profit = double("grade_2_profit").default(0.7) val grade_1_profit = double("grade_1_profit").default(0.5) + + val grade_5_allocationrate = double("grade_5_allocationrate").default(1.0) + val grade_4_allocationrate = double("grade_4_allocationrate").default(0.8) + val grade_3_allocationrate = double("grade_3_allocationrate").default(0.6) + val grade_2_allocationrate = double("grade_2_allocationrate").default(0.4) + val grade_1_allocationrate = double("grade_1_allocationrate").default(0.3) + + + + + + + + + + + + + val max_count = integer("max_count").default(20) override val primaryKey = PrimaryKey(id) } @@ -260,6 +279,11 @@ object DatabaseFactory { GRADE_2_PROFIT = it[ConfigTable.grade_2_profit], GRADE_1_BUY = it[ConfigTable.grade_1_buy], GRADE_1_PROFIT = it[ConfigTable.grade_1_profit], + GRADE_1_ALLOCATIONRATE = it[ConfigTable.grade_1_allocationrate], + GRADE_2_ALLOCATIONRATE = it[ConfigTable.grade_2_allocationrate], + GRADE_3_ALLOCATIONRATE = it[ConfigTable.grade_3_allocationrate], + GRADE_4_ALLOCATIONRATE = it[ConfigTable.grade_4_allocationrate], + GRADE_5_ALLOCATIONRATE = it[ConfigTable.grade_5_allocationrate], MAX_COUNT = it[ConfigTable.max_count], ) } @@ -300,6 +324,12 @@ object DatabaseFactory { it[grade_2_profit] = config.GRADE_2_PROFIT it[grade_1_buy] = config.GRADE_1_BUY it[grade_1_profit] = config.GRADE_1_PROFIT + it[grade_5_allocationrate] = config.GRADE_5_ALLOCATIONRATE + it[grade_4_allocationrate] = config.GRADE_4_ALLOCATIONRATE + it[grade_3_allocationrate] = config.GRADE_3_ALLOCATIONRATE + it[grade_2_allocationrate] = config.GRADE_2_ALLOCATIONRATE + it[grade_1_allocationrate] = config.GRADE_1_ALLOCATIONRATE + it[max_count] = config.MAX_COUNT } } diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 3e51299..b93ae88 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -25,7 +25,15 @@ enum class ConfigIndex(val index : Int,val label : String) { GRADE_4_PROFIT(GRADE_5_PROFIT.index + 1, "안정적 투자 목표 수익 비율"), GRADE_3_PROFIT(GRADE_4_PROFIT.index + 1, "보수적 투자 목표 수익 비율"), GRADE_2_PROFIT(GRADE_3_PROFIT.index + 1, "하이리스크,리턴 목표 수익 비율"), - GRADE_1_PROFIT(GRADE_2_PROFIT.index + 1, "공격적초단기 목표 수익 비율"); + GRADE_1_PROFIT(GRADE_2_PROFIT.index + 1, "공격적초단기 목표 수익 비율"), + + + GRADE_5_ALLOCATIONRATE(GRADE_1_PROFIT.index + 1, "강력 추천 투자 목표 투자금 비율"), + GRADE_4_ALLOCATIONRATE(GRADE_5_ALLOCATIONRATE.index + 1, "안정적 투자 목표 투자금 비율"), + GRADE_3_ALLOCATIONRATE(GRADE_4_ALLOCATIONRATE.index + 1, "보수적 투자 목표 투자금 비율"), + GRADE_2_ALLOCATIONRATE(GRADE_3_ALLOCATIONRATE.index + 1, "하이리스크,리턴 목표 투자금 비율"), + GRADE_1_ALLOCATIONRATE(GRADE_2_ALLOCATIONRATE.index + 1, "공격적초단기 목표 투자금 비율"); + companion object { fun get(index : Int) = ConfigIndex.entries[index] } @@ -82,6 +90,13 @@ data class AppConfig( var GRADE_3_PROFIT : Double = 0.9, var GRADE_2_PROFIT : Double = 0.7, var GRADE_1_PROFIT : Double = 0.5, + + var GRADE_5_ALLOCATIONRATE : Double = 1.0, + var GRADE_4_ALLOCATIONRATE : Double = 0.8, + var GRADE_3_ALLOCATIONRATE : Double = 0.6, + var GRADE_2_ALLOCATIONRATE : Double = 0.4, + var GRADE_1_ALLOCATIONRATE : Double = 0.3, + var MAX_COUNT : Int = 20, ) { @@ -111,7 +126,15 @@ data class AppConfig( ConfigIndex.GRADE_3_BUY -> {GRADE_3_BUY = value.toInt()} ConfigIndex.GRADE_2_BUY -> {GRADE_2_BUY = value.toInt()} ConfigIndex.GRADE_1_BUY -> {GRADE_1_BUY = value.toInt()} + + ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT = value.toInt()} + + ConfigIndex.GRADE_5_ALLOCATIONRATE -> {GRADE_5_ALLOCATIONRATE = value} + ConfigIndex.GRADE_4_ALLOCATIONRATE -> {GRADE_4_ALLOCATIONRATE = value} + ConfigIndex.GRADE_3_ALLOCATIONRATE -> {GRADE_3_ALLOCATIONRATE = value} + ConfigIndex.GRADE_2_ALLOCATIONRATE -> {GRADE_2_ALLOCATIONRATE = value} + ConfigIndex.GRADE_1_ALLOCATIONRATE -> {GRADE_1_ALLOCATIONRATE = value} } } fun getValues(index :ConfigIndex) : Double { @@ -148,6 +171,14 @@ data class AppConfig( ConfigIndex.GRADE_3_PROFIT -> {GRADE_3_PROFIT} ConfigIndex.GRADE_2_PROFIT -> {GRADE_2_PROFIT} ConfigIndex.GRADE_1_PROFIT -> {GRADE_1_PROFIT} + + ConfigIndex.GRADE_5_ALLOCATIONRATE -> {GRADE_5_ALLOCATIONRATE} + ConfigIndex.GRADE_4_ALLOCATIONRATE -> {GRADE_4_ALLOCATIONRATE} + ConfigIndex.GRADE_3_ALLOCATIONRATE -> {GRADE_3_ALLOCATIONRATE} + ConfigIndex.GRADE_2_ALLOCATIONRATE -> {GRADE_2_ALLOCATIONRATE} + ConfigIndex.GRADE_1_ALLOCATIONRATE -> {GRADE_1_ALLOCATIONRATE} + + ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT.toDouble()} } } diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index 9701738..208cf6d 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -215,59 +215,3 @@ data class ExecutionData( val qty: String, val isFilled: Boolean ) - -enum class InvestmentGrade( - val displayName: String, - val description: String, - val shortWeight: Double = 0.0, - val midWeight: Double = 0.0, - val longWeight: Double = 0.0, - val profitGuide: ConfigIndex, - val buyGuide: ConfigIndex, -) { - LEVEL_5_STRONG_RECOMMEND( - displayName = "최상급 추천", - description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천", - shortWeight = 1.0, - midWeight = 1.0, - longWeight = 1.0, - profitGuide = ConfigIndex.GRADE_5_PROFIT, - buyGuide = ConfigIndex.GRADE_5_BUY, - ), - LEVEL_4_BALANCED_RECOMMEND( - displayName = "균형 추천", - description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천", - shortWeight = 0.8, - midWeight = 1.0, - longWeight = 1.0, - profitGuide = ConfigIndex.GRADE_4_PROFIT, - buyGuide = ConfigIndex.GRADE_4_BUY, - ), - LEVEL_3_CAUTIOUS_RECOMMEND( - displayName = "보수적 추천", - description = "중기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함", - shortWeight = 0.6, - midWeight = 1.0, - longWeight = 1.0, - profitGuide = ConfigIndex.GRADE_3_PROFIT, - buyGuide = ConfigIndex.GRADE_3_BUY, - ), - LEVEL_2_HIGH_RISK( - displayName = "고위험 추천", - description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자", - shortWeight = 1.0, - midWeight = 0.4, - longWeight = 0.4, - profitGuide = ConfigIndex.GRADE_2_PROFIT, - buyGuide = ConfigIndex.GRADE_2_BUY, - ), - LEVEL_1_SPECULATIVE( - displayName = "순수 공격적 선택", - description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자", - shortWeight = 1.0, - midWeight = 0.2, - longWeight = 0.2, - profitGuide = ConfigIndex.GRADE_1_PROFIT, - buyGuide = ConfigIndex.GRADE_1_BUY, - ) -} diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 7540860..a1afa08 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -31,6 +31,7 @@ import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject import model.ConfigIndex import model.KisSession +import model.RankingStock import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -258,12 +259,16 @@ object RagService { } else { println("✋ [$stockName] 기술 점수 미달로 분석 중단 ${scores.toString()}") TradingLogStore.addAnalyzer(stockName, stockCode, "기술 점수 미달로 분석 중단") + if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt)) { + TradingLogStore.addLog(tradingDecision,"WATCH","우량주로 판단되나 거래량 혹은 최근 거래 점수 미달로 재분석 대상에 추가") + AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) + } tradingDecision.confidence = 1.0 result(tradingDecision, false) } } else { println("🚨 [$stockName] ${FinancialAnalyzer.toString(financialStmt)} 재무 안전벨트 미달") - TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.getInvestmentStatus(financialStmt)}") + TradingLogStore.addAnalyzer(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 c04a439..10c6ff2 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -28,7 +28,6 @@ import kotlinx.serialization.Serializable import model.CandleData import model.ConfigIndex import model.ExecutionData -import model.InvestmentGrade import model.KisSession import model.RankingStock import model.RankingType @@ -103,7 +102,7 @@ object AutoTradingManager { println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}") if (completeTradingDecision.confidence < 10) { addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName)) - TradingLogStore.addLog(completeTradingDecision,"HOLD","분석 신뢰도 오류 인지로 재분석 대기열에 추가") + TradingLogStore.addLog(completeTradingDecision,"RETRY","분석 신뢰도 오류 인지로 재분석 대기열에 추가") }else if (completeTradingDecision != null && !completeTradingDecision.stockCode.isNullOrEmpty()) { var basePrice = completeTradingDecision.currentPrice var stockCode = completeTradingDecision.stockCode @@ -129,19 +128,10 @@ 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("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장) - val gradeRate = (1.0 - (investmentGrade.ordinal * 0.1)) + val gradeRate = (1.0 - (investmentGrade.ordinal * 0.12)) val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt() maxBudget = maxBudget * gradeRate val calculatedQty = if (basePrice > 0) { @@ -159,7 +149,7 @@ object AutoTradingManager { } else if(totalScore >= (minScore * 0.85) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.85)) { addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName)) - TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가") + TradingLogStore.addLog(completeTradingDecision,"RETRY","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가") } else { TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달") } @@ -285,7 +275,7 @@ object AutoTradingManager { if (it.message?.contains("주문가능금액을 초과") == true) { AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) - TradingLogStore.addLog(decision,"BUY","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") + TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") } else { TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") } @@ -382,6 +372,11 @@ object AutoTradingManager { balance.holdings.forEach { holding -> if (BLACKLISTEDSTOCKCODES.contains(holding.code)){ println("❌ 차단 처리된 주식 : ${holding.name}") + TradingLogStore.addAnalyzer( + holding.name, + holding.code, + "거랙 차단 대상 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})" + ) } else { if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) { println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ") @@ -414,10 +409,13 @@ object AutoTradingManager { "SELL", "🎊 보유 주식 매도 주문 실패[${it.message}] " ) -// println("❌ [재주문 실패] ${holding.name}: ${it.message}") } } else { - + TradingLogStore.addAnalyzer( + "보유주식[${holding.name}]", + holding.code, + "수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})" + ) } delay(200) // API 호출 부하 방지 } @@ -560,9 +558,14 @@ object AutoTradingManager { return batch } - suspend fun executeMarketLoop() { + suspend fun checkBalance() : UnifiedBalance? { val balance = KisTradeService.fetchIntegratedBalance().getOrNull() + if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) } + return balance + } + suspend fun executeMarketLoop() { + val balance = checkBalance() if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) } val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L @@ -622,9 +625,31 @@ object AutoTradingManager { println("남은 후보군 개수 : ${totalCount}") delay(100) } + sellSchedule() } println("⏱️ [Cycle End] ${LocalTime.now()}") } + private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장 + suspend fun sellSchedule() { + + val now = LocalTime.now() + val currentMinute = now.minute + println("매도 스케줄 체크") + if (now.hour == 9 && (currentMinute == 1 || currentMinute == 15 || currentMinute == 40)) { + if (lastForceCheckMinute != currentMinute) { + TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true) + println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") + checkBalance() + lastForceCheckMinute = currentMinute // 실행 완료 기록 + } + } +// else if(now.hour % 2 == 1 && (currentMinute == 43)) { +// TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true) +// println("⏰ [강제 스케줄 실행] 오전 ${now.hour}시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") +// checkBalance() +// lastForceCheckMinute = currentMinute // 실행 완료 기록 +// } + } suspend fun finalizeMarketClose(now: LocalTime) { when { @@ -1035,7 +1060,7 @@ class TechnicalAnalyzer { val disparityDaily = (currentPrice / ma20Daily) * 100 if (disparityDaily > 115.0) { // 20일선보다 15% 이상 떠 있으면 감점 시작 - val penalty = ((disparityDaily - 115.0) * 1).toInt() // 초과분 1%당 2점 감점 + val penalty = ((disparityDaily - 115.0) * 0.5).toInt() // 초과분 1%당 2점 감점 short -= penalty ultra -= (penalty / 2) // 초단기에도 영향 println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}점") @@ -1046,9 +1071,9 @@ class TechnicalAnalyzer { if (weekly.size >= 3) { val weeklyChange = calculateChange(weekly.takeLast(3)) if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시 - mid -= 15 - short -= 7 - println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -15점") + mid -= 10 + short -= 5 + println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -10점") } } @@ -1436,3 +1461,67 @@ fun CandleData.toScalpingCandle(): Candle { fun List.toScalpingList(): List { return this.map { it.toScalpingCandle() } } + + +enum class InvestmentGrade( + val displayName: String, + val description: String, + val shortWeight: Double = 0.0, + val midWeight: Double = 0.0, + val longWeight: Double = 0.0, + val profitGuide: ConfigIndex, + val buyGuide: ConfigIndex, + val allocationRate: ConfigIndex, +) { + LEVEL_5_STRONG_RECOMMEND( + displayName = "최상급 추천", + description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천", + shortWeight = 1.0, + midWeight = 1.0, + longWeight = 1.0, + profitGuide = ConfigIndex.GRADE_5_PROFIT, + buyGuide = ConfigIndex.GRADE_5_BUY, + allocationRate = ConfigIndex.GRADE_5_ALLOCATIONRATE, + ), + LEVEL_4_BALANCED_RECOMMEND( + displayName = "균형 추천", + description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천", + shortWeight = 0.8, + midWeight = 1.0, + longWeight = 1.0, + profitGuide = ConfigIndex.GRADE_4_PROFIT, + buyGuide = ConfigIndex.GRADE_4_BUY, + allocationRate = ConfigIndex.GRADE_4_ALLOCATIONRATE, + ), + LEVEL_3_CAUTIOUS_RECOMMEND( + displayName = "보수적 추천", + description = "중기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함", + shortWeight = 0.6, + midWeight = 1.0, + longWeight = 1.0, + profitGuide = ConfigIndex.GRADE_3_PROFIT, + buyGuide = ConfigIndex.GRADE_3_BUY, + allocationRate = ConfigIndex.GRADE_3_ALLOCATIONRATE, + ), + LEVEL_2_HIGH_RISK( + displayName = "고위험 추천", + description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자", + shortWeight = 1.0, + midWeight = 0.4, + longWeight = 0.4, + profitGuide = ConfigIndex.GRADE_2_PROFIT, + buyGuide = ConfigIndex.GRADE_2_BUY, + allocationRate = ConfigIndex.GRADE_2_ALLOCATIONRATE, + ), + LEVEL_1_SPECULATIVE( + displayName = "순수 공격적 선택", + description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자", + shortWeight = 1.0, + midWeight = 0.2, + longWeight = 0.2, + profitGuide = ConfigIndex.GRADE_1_PROFIT, + buyGuide = ConfigIndex.GRADE_1_BUY, + allocationRate = ConfigIndex.GRADE_1_ALLOCATIONRATE, + ) +} + diff --git a/src/main/kotlin/ui/IntegratedOrderSection.kt b/src/main/kotlin/ui/IntegratedOrderSection.kt index 59467ce..9e3e29a 100644 --- a/src/main/kotlin/ui/IntegratedOrderSection.kt +++ b/src/main/kotlin/ui/IntegratedOrderSection.kt @@ -21,11 +21,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import model.ConfigIndex -import model.InvestmentGrade import model.KisSession import model.RankingStock import network.KisTradeService import service.AutoTradingManager +import service.InvestmentGrade import util.MarketUtil import kotlin.math.min import kotlin.math.roundToInt diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index 40e0e36..e981625 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -35,7 +35,7 @@ fun TradingDecisionLog() { val coroutineScope = rememberCoroutineScope() var searchQuery by remember { mutableStateOf("") } var selectedFilters by remember { mutableStateOf(setOf("전체")) } - val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING","ANALYZER","PASS") + val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING","ANALYZER","PASS","WATCH","RETRY") var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) } LaunchedEffect(AutoTradingManager.llmAnalyser) { llmAnalyser = AutoTradingManager.llmAnalyser @@ -162,6 +162,8 @@ fun TradingDecisionLog() { "HOLD" -> Color.DarkGray "ANALYZER" -> Color.Green "PASS" -> Color.Yellow + "RETRY" -> Color(0xFF00BCD4) // [추가] 하늘색 (재분석/대기열) + "WATCH" -> Color(0xFF4CAF50) // [추가] 연초록 (관심 종목 감시) else -> Color.DarkGray }, fontWeight = FontWeight.ExtraBold @@ -182,7 +184,7 @@ fun TradingDecisionLog() { columns = GridCells.Fixed(2), // 2열 병렬 배치 horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier.fillMaxWidth().fillMaxHeight().background(Color.White) + modifier = Modifier.fillMaxWidth().weight(0.5f).background(Color.White) ) { var firstSet = mutableSetOf() item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함 @@ -260,6 +262,15 @@ fun TradingDecisionLog() { singleLine = true ) } + + } + LazyVerticalGrid( + columns = GridCells.Fixed(3), // 2열 병렬 배치 + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.fillMaxWidth().weight(0.5f).background(Color.White) + ) { + var firstSet = mutableSetOf() item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함 Text( "💰매수 정책 및 기대 수익률", @@ -268,16 +279,11 @@ fun TradingDecisionLog() { ) } var defaults2 = arrayOf( - arrayOf(ConfigIndex.GRADE_5_BUY, - ConfigIndex.GRADE_5_PROFIT,), - arrayOf(ConfigIndex.GRADE_4_BUY, - ConfigIndex.GRADE_4_PROFIT,), - arrayOf(ConfigIndex.GRADE_3_BUY, - ConfigIndex.GRADE_3_PROFIT,), - arrayOf(ConfigIndex.GRADE_2_BUY, - ConfigIndex.GRADE_2_PROFIT,), - arrayOf(ConfigIndex.GRADE_1_BUY, - ConfigIndex.GRADE_1_PROFIT,), + arrayOf(ConfigIndex.GRADE_5_BUY, ConfigIndex.GRADE_5_PROFIT,ConfigIndex.GRADE_5_ALLOCATIONRATE), + arrayOf(ConfigIndex.GRADE_4_BUY, ConfigIndex.GRADE_4_PROFIT,ConfigIndex.GRADE_4_ALLOCATIONRATE), + arrayOf(ConfigIndex.GRADE_3_BUY, ConfigIndex.GRADE_3_PROFIT,ConfigIndex.GRADE_3_ALLOCATIONRATE), + arrayOf(ConfigIndex.GRADE_2_BUY, ConfigIndex.GRADE_2_PROFIT,ConfigIndex.GRADE_2_ALLOCATIONRATE), + arrayOf(ConfigIndex.GRADE_1_BUY, ConfigIndex.GRADE_1_PROFIT,ConfigIndex.GRADE_1_ALLOCATIONRATE), ) for (items in defaults2) { val common = findLongestCommonSubstring(items.first().label,items.last().label) @@ -316,6 +322,8 @@ fun TradingDecisionLog() { getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues( ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues( ConfigIndex.TAX_INDEX)}" + } else if (configKey.name.contains("ALLOCATIONRATE")) { + getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * newValue}원 투자}" } else { getRemaining(configKey.label,common) + ": -${localText} 호가 매수}" } @@ -325,6 +333,8 @@ fun TradingDecisionLog() { getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues( ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues( ConfigIndex.TAX_INDEX)} " + } else if (configKey.name.contains("ALLOCATIONRATE")) { + getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * localText.toDouble() }원 투자}" } else { getRemaining(configKey.label,common) + ": -${localText} 호가 매수}" }