From acd26abe1e62b1adfe58a53ae069183f0ae9328d Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Mon, 3 Aug 2026 11:53:29 +0900 Subject: [PATCH] .... --- src/main/kotlin/analyzer/TechnicalAnalyzer.kt | 4 +- src/main/kotlin/model/AppConfig.kt | 1 + src/main/kotlin/model/StockModels.kt | 10 +++ src/main/kotlin/network/KisTradeService.kt | 72 +++++++++++++++++++ src/main/kotlin/network/RagService.kt | 4 +- src/main/kotlin/service/AutoTradingManager.kt | 21 +++++- src/main/kotlin/ui/TradingDecisionLog.kt | 51 ++++++++----- 7 files changed, 138 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/analyzer/TechnicalAnalyzer.kt b/src/main/kotlin/analyzer/TechnicalAnalyzer.kt index a5984dd..a6c9a62 100644 --- a/src/main/kotlin/analyzer/TechnicalAnalyzer.kt +++ b/src/main/kotlin/analyzer/TechnicalAnalyzer.kt @@ -627,7 +627,7 @@ $standardizedScores if (candles.size < 20 || !reboundStats.isValid) return null // 1. 최근 20일 내 단기 고점 파악 (현재 진행 중인 하락 파동의 시작점) - val recentCandles = candles.takeLast(20) + val recentCandles = candles.takeLast((candles.size.times(0.8).toInt())) var recentPeakPrice = 0.0 for (i in recentCandles.indices.reversed()) { @@ -652,7 +652,7 @@ $standardizedScores val remainingDropRate = reboundStats.avgDropRate - currentDropRate // 5. 바닥권 진입 판별 (예상 바닥가의 +2% 이내로 들어왔거나, 통계적 마지노선(extremeLow) 근처일 때) - val isBottomZone = currentPrice <= (expectedBottomPrice * 1.02) || currentPrice <= (volatility.extremeLow * 1.02) + val isBottomZone = currentPrice <= (expectedBottomPrice * 1.015) || currentPrice <= (volatility.extremeLow * 1.015) return DropPrediction( recentPeakPrice = recentPeakPrice, diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index c8497fa..028925f 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -267,6 +267,7 @@ class TradeConfig { var minExpectedProfitRate: Double = 2.0 // 필터링 기준 최소 기대 수익률 (%) var maxExpectedReboundDays: Double = 10.0 // 필터링 기준 최대 허용 반등 주기 (일) var minExpectedReboundDays: Double = 1.5 + var isUpcomingDividend : Boolean = false } diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index 48f2b48..cfc0e6b 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -3,6 +3,8 @@ package model import AutoTradeItem import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.math.BigDecimal + @Serializable data class StockBalanceResponse( val rt_cd: String = "", @@ -106,6 +108,14 @@ enum class RankingType( HTS_TOP20("HTS조회상위", "HHMCM000100C0", "20175", "/uapi/domestic-stock/v1/ranking/hts-top-view", emptyMap()) } +data class UpcomingDividend( + val hasDividend: Boolean, + val stockCode: String, + val stockName: String, + val exDividendDate: String?, + val dividendAmount: BigDecimal? +) + @Serializable data class RankingStock( val hts_kor_isnm: String = "", // 종목명 diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index e783fe9..c3b5994 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -227,6 +227,78 @@ object KisTradeService { } } + + suspend fun fetchUpcomingDividend( + stockCode: String + ): Result { + + val config = KisSession.config + + val formatter = DateTimeFormatter.ofPattern("yyyyMMdd") + + val fromDate = LocalDate.now().format(formatter) + val toDate = LocalDate.now().plusMonths(6).format(formatter) + + return try { + val response = client.get("$prodUrl/uapi/domestic-stock/v1/ksdinfo/dividend") { + header("authorization", "Bearer ${config.marketToken}") + header("appkey", config.realAppKey) + header("appsecret", config.realSecretKey) + header("tr_id", "HHKDB669102C0") + header("custtype", "P") + + // 실제 API 문서의 파라미터명으로 변경 + parameter("CTS", "") + parameter("GB1", "0") + parameter("SHT_CD", stockCode) + parameter("HIGH_GB", "") + parameter("F_DT", fromDate) + parameter("T_DT", toDate) + } + + val body = response.body() + + if (body["rt_cd"]?.jsonPrimitive?.content != "0") { + return Result.failure( + Exception(body["msg1"]?.jsonPrimitive?.content ?: "배당 조회 실패") + ) + } + + val output = body["output1"]?.jsonArray.orEmpty() + + val item = output + .map { it.jsonObject } + .firstOrNull { + it["sht_cd"]?.jsonPrimitive?.content == stockCode + } + println("fetchUpcomingDividend 배당 body >>> ${item}") + if (item == null) { + return Result.success( + UpcomingDividend( + hasDividend = false, + stockCode = stockCode, + stockName = "", + exDividendDate = null, + dividendAmount = null + ) + ) + } + + Result.success( + UpcomingDividend( + hasDividend = true, + stockCode = item["sht_cd"]?.jsonPrimitive?.content.orEmpty(), + stockName = item["isin_name"]?.jsonPrimitive?.content.orEmpty(), + exDividendDate = item["record_date"]?.jsonPrimitive?.content, + dividendAmount = item["per_sto_divi_amt"]?.jsonPrimitive?.content?.toBigDecimalOrNull() + ) + ) + + } catch (e: Exception) { + Result.failure(e) + } + } + /** * [추가] 기간별(일/주/월) 차트 데이터 조회 * @param periodCode "D"(일), "W"(주), "M"(월) diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 9be0f1d..24252eb 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -278,8 +278,8 @@ object RagService { return@coroutineScope } - if ((tradingDecision.signalModel?.compositeScore ?: 0) < 50) { - logTime(stockName, "기술 점수 미달 조기 종료", techDuration, System.currentTimeMillis() - totalStartTime) + if ((tradingDecision.signalModel?.compositeScore ?: 0) < 40) { + logTime(stockName, "기술 점수 미달 조기 종료 ${tradingDecision.signalModel?.compositeScore} , ${tradingDecision.signalModel?.successProbPct} ", techDuration, System.currentTimeMillis() - totalStartTime) if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) { TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true) result(tradingDecision.apply { diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index f3a1c0b..cb975c6 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -39,6 +39,7 @@ import network.KisWebSocketManager import network.RagService import network.RagService.isSafetyBeltStockCodes import network.StockUniverseLoader +import okhttp3.internal.wait import report.TradingReportManager import util.MarketUtil import java.time.LocalDate @@ -1161,7 +1162,8 @@ object AutoTradingManager { val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData } // 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?) - val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 40) + println("(dailyData.size * 0.8).toInt() ${(dailyData.size * 0.8).toInt()}") + val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, (dailyData.size * 0.8).toInt()) val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0 // 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?) @@ -1173,7 +1175,7 @@ object AutoTradingManager { timeTolerance = dailyStats.timeTolerance ) print("-> [${stock.name}] 필터링 ${dailyStats.avgReboundPeriod} ${dailyStats.avgDropRate} ${dailyStats.timeTolerance}") - val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData) + val isSteadyUptrend = false //tempAnalyzer.checkSteadyUptrend(dailyData) // 🌟 [수정] 조건 통합 (OR 조건) @@ -1192,13 +1194,26 @@ object AutoTradingManager { if (dropPrediction != null) { // 💡 [방어 로직] 아직 바닥까지 한참 남았는데 섣불리 들어가는 것을 방지! // 과거 평균 10% 빠지는 종목인데, 지금 겨우 -3% 빠진 상태라면 (남은 하락폭 -7%) - if (dropPrediction.remainingDropRate < -2.0 && !dropPrediction.isBottomZone) { + if (dropPrediction.remainingDropRate < -1.5 && !dropPrediction.isBottomZone) { print("-> [${stock.name}] 지하실 주의 (현재 ${"%.1f".format(dropPrediction.currentDropRate)}% 하락, 바닥까지 ${"%.1f".format(dropPrediction.remainingDropRate)}% 추가 하락 위험) | ") return@withTimeout // 매수 후보에서 과감히 제외! } // 반대로 완벽한 바닥권(isBottomZone = true)에 들어왔다면 매수 타점으로 인정하여 다음 단계로 넘김 } + + if (KisSession.tradeConfig.isUpcomingDividend) { + var dividend = KisTradeService.fetchUpcomingDividend(stock.code).getOrNull() + if(dividend?.hasDividend == true){ + println("[${stock.name}] 배당락일 ${dividend.exDividendDate} : ${dividend.dividendAmount}") + } else { + println("[${stock.name}] 배당 정보 없어서 분석 종료") + return@withTimeout + } + } else { + println("[${stock.name}] 배당 정보 무관 함.") + } + println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)") if (!isSafetyBeltStockCodes.contains(stock.code)) { val analyzer = coroutineScope { diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index bb1708a..1710c2d 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -564,23 +564,37 @@ fun TradingDecisionLog() { helperText = "현재: ${tradeConfig.auto_cancel_pending_time / 1000}초 후 취소" ) + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + SettingSwitchField ( + modifier = Modifier.weight(1.0f, true), + label = "장 전 대체마켓 매도", + initialChecked = tradeConfig.before_nxt, + onCheckedChange = { + tradeConfig.before_nxt = it + KisSession.saveTradeConfig() + } + ) + SettingSwitchField( + modifier = Modifier.weight(1.0f, true), + label = "장 후 대체 마켓 매도", + initialChecked = tradeConfig.after_nxt, + onCheckedChange = { + tradeConfig.after_nxt = it + KisSession.saveTradeConfig() + } + ) + } +// SettingSwitchField( +// label = "해외 주식", +// initialChecked = tradeConfig.enableOverSea, +// onCheckedChange = { tradeConfig.enableOverSea = it +// KisSession.saveTradeConfig() } +// ) SettingSwitchField( - label = "장 전 대체마켓 매도", - initialChecked = tradeConfig.before_nxt, - onCheckedChange = { tradeConfig.before_nxt = it - KisSession.saveTradeConfig() } - ) - SettingSwitchField( - label = "장 후 대체 마켓 매도", - initialChecked = tradeConfig.after_nxt, - onCheckedChange = { tradeConfig.after_nxt = it - KisSession.saveTradeConfig() } - ) - SettingSwitchField( - label = "해외 주식", - initialChecked = tradeConfig.enableOverSea, - onCheckedChange = { tradeConfig.enableOverSea = it + label = "배당 주만 거래", + initialChecked = tradeConfig.isUpcomingDividend, + onCheckedChange = { tradeConfig.isUpcomingDividend = it KisSession.saveTradeConfig() } ) @@ -965,6 +979,9 @@ fun SettingInputField( @Composable fun SettingSwitchField( + modifier :Modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 4.dp), label: String, initialChecked: Boolean, helperText: String = "", @@ -974,9 +991,7 @@ fun SettingSwitchField( var localChecked by remember { mutableStateOf(initialChecked) } Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 4.dp) + modifier = modifier ) { Row( modifier = Modifier.fillMaxWidth(),