diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 2ea0a59..ca1beab 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -88,7 +88,12 @@ fun getLlamaBinPath(): String { } // Windows NUC os.contains("win") -> { - "$basePath/win-x64-n/llama-server.exe" + if (KisSession.tradeConfig.isLowPerformanceMonitoring) { + "$basePath/win-x64/llama-server.exe" + } + else { + "$basePath/win-x64-n/llama-server.exe" + } } else -> "$basePath/llama-server" } @@ -111,6 +116,7 @@ private var isAppStarted = false fun main() = application { if (!isAppStarted) { initLogger(DETAILLOG) + KisSession.tradeConfig = KisSession.loadTradeConfig() try { val (port1, port2) = PortFinder.findAvailablePortPair(18080, false) if (port1 > 18000 && port2 > port1) { diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index fc67e2f..542ac7d 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import model.AppConfig +import model.KisSession import model.TradingDecision import network.NewsService import org.jetbrains.exposed.sql.* @@ -490,7 +491,18 @@ object TradingLogStore { decision = decision.decision ?: "HOLD", confidence = decision.confidence, reason = decision.reason ?: "" - )) + ).apply { + if (KisSession.tradeConfig.useTagsShare.contains(this.decision) && KisSession.tradeConfig.useLogKeywordsShare.any { + reason.contains( + it + ) + }) { + CoroutineScope(Dispatchers.Default).launch { + NewsService.sendTelegramMessage("${this@apply.decision}$stockName ${reason}") + } + } + } + ) } } @@ -504,11 +516,25 @@ object TradingLogStore { decision = decision, confidence = 100.0, reason = log - ) + ).apply { + if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any { + log.contains( + it + ) + }) { + CoroutineScope(Dispatchers.Default).launch { + println("CALLED sendTelegramMessage") + NewsService.sendTelegramMessage("${this@apply.decision}$stockName ${log}") + } + } + } ) } } + + + fun addLog(tradingDecision: TradingDecision, decision: String, log: String) { synchronized(this) { if (decisionLogs.size > 1000) decisionLogs.removeAt(0) @@ -521,11 +547,8 @@ object TradingLogStore { reason = log ).apply { CoroutineScope(Dispatchers.Default).launch { - println("CALLED sendTelegramMessage -1") - if (decision.contains("WATCH") || ((tradingDecision.investmentGrade?.ordinal - ?: 0) < 2) + if (((tradingDecision.investmentGrade?.name?.length ?: 0) > 0 && KisSession.tradeConfig.useGradeShare.contains(tradingDecision.investmentGrade?.name)) ) { - println("CALLED sendTelegramMessage OK") NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}") } } @@ -534,6 +557,32 @@ object TradingLogStore { } } + fun addWatchLog(tradingDecision: TradingDecision, decision: String, log: String) { + synchronized(this) { + if (decisionLogs.size > 1000) decisionLogs.removeAt(0) + decisionLogs.add( + LogEntry( + time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")), + stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}]", + decision = decision, + confidence = tradingDecision.confidence, + reason = log + ).apply { + CoroutineScope(Dispatchers.Default).launch { + if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any { + log.contains( + it + ) + }) { + NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}") + } + } + } + ) + } + } + + fun addAnalyzer(name : String, code : String, log: String, positive : Boolean = false) { synchronized(this) { if (decisionLogs.size > 1000) decisionLogs.removeAt(0) @@ -545,7 +594,17 @@ object TradingLogStore { decision = if (positive) "ANALYZER" else "PASS", confidence = 100.0, reason = log - ) + ).apply { + if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any { + log.contains( + it + ) + }) { + CoroutineScope(Dispatchers.Default).launch { + NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}") + } + } + } ) } } @@ -562,9 +621,19 @@ object TradingLogStore { confidence = 100.0, reason = log ).apply { - CoroutineScope(Dispatchers.Default).launch { - println("CALLED sendTelegramMessage") - NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}") + if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any { + log.contains( + it + ) + }) { + var current = System.currentTimeMillis() + var sendable = noticeFilter.filter { it.key.equals(code, true) && ((current - it.value) > 1000 * 60 * 30L)}.isNotEmpty() + if (sendable) { + CoroutineScope(Dispatchers.Default).launch { + NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}") + + }} + noticeFilter[code] = current } } ) @@ -572,6 +641,8 @@ object TradingLogStore { } } + + var noticeFilter = hashMapOf() fun addNotice(name : String, code : String, log: String, qty: Int? = null) { synchronized(this) { if (decisionLogs.size > 1000) decisionLogs.removeAt(0) diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 4d15174..204a556 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -241,6 +241,17 @@ class TradeConfig { var end_buy_time : String = "15:10" var enableOverSea : Boolean = false var tlg_id : String = "" + var CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분 + var WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인 + var STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 + var ONE_STOCK_ALYSIS_TIME = 180000L + var isLowPerformanceMonitoring: Boolean = false + var useGradeShare : List = listOf("LEVEL_4","LEVEL_5") + var useTagsShare : List = listOf("NOTICE", "WATCH") + var useLogKeywordsShare : List = listOf("재분석") + var useAutoRepost : Boolean = false + var minusFilter : Double = 15.0 + var plusFilter : Double = 15.0 } diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 6ea11bb..8445142 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -671,9 +671,6 @@ object RagService { val alignmentBonus = if (s.ultraShort > s.shortTerm && s.shortTerm > s.midTerm) 3.0 else 0.0 return (base + alignmentBonus).coerceIn(0.0, 25.0) } - - - } diff --git a/src/main/kotlin/report/TradingReportManager.kt b/src/main/kotlin/report/TradingReportManager.kt index 7d87826..6a4db09 100644 --- a/src/main/kotlin/report/TradingReportManager.kt +++ b/src/main/kotlin/report/TradingReportManager.kt @@ -59,6 +59,9 @@ object TradingReportManager : TradingReportService { private val activePositions = mutableMapOf() override fun recordAssetSnapshot(type: SnapshotType, balance: UnifiedBalance, remark: String?) { + if (!KisSession.tradeConfig.useAutoRepost) { + return + } CoroutineScope(Dispatchers.IO).launch { val todayDate = LocalDate.now().toString() diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index dec4bbc..45432b0 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -61,10 +61,10 @@ object AutoTradingManager { private val lastTickTime = AtomicLong(System.currentTimeMillis()) private var watchdogJob: Job? = null - private const val CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분 - private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인 - private const val STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 - private const val ONE_STOCK_ALYSIS_TIME = 180000L + var CYCLE_TIMEOUT = KisSession.tradeConfig.CYCLE_TIMEOUT + var WATCHDOG_CHECK_INTERVAL = KisSession.tradeConfig.WATCHDOG_CHECK_INTERVAL + var STUCK_THRESHOLD = KisSession.tradeConfig.STUCK_THRESHOLD + var ONE_STOCK_ALYSIS_TIME = KisSession.tradeConfig.ONE_STOCK_ALYSIS_TIME fun isRunning(): Boolean = discoveryJob?.isActive == true private var remainingCandidates = mutableListOf() // private val processedCodes = mutableSetOf() // 중복 처리 방지용 (선택 사항) @@ -216,7 +216,7 @@ object AutoTradingManager { println("🚫 [안전 장치 작동] 현재 포지션이 가득 찼습니다. (최대 ${myOredsAndBalanceCodes.size}/${maxStocks}종목). 신규 매수를 일시 중단하고 매도에 집중합니다.") TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단") AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) - TradingLogStore.addLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") + TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") } else { println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice") @@ -266,7 +266,7 @@ object AutoTradingManager { if (it.message?.contains("주문가능금액을 초과") == true) { AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) - TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") + TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") } else { TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") } @@ -559,7 +559,9 @@ object AutoTradingManager { private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석 val now = LocalTime.now() val currentMinute = now.minute - if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0))) { + if ((holding.availOrderCount.toInt() + ?: 0) > 0 && ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0))) + ) { val profit = holding.profitRate.toDouble() val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다) if (profit <= lossThreshold) { @@ -762,14 +764,14 @@ object AutoTradingManager { suspend fun checkBalance(isMorning: Boolean = true) { if (isMorning) { currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull() - currentBalance?.let { currentBalance -> - if (LocalTime.now().isBefore(LocalTime.of(18,1))) { - TradingReportManager.recordAssetSnapshot( - if (LocalTime.now().isAfter(LocalTime.of(18, 0)) - ) SnapshotType.END else SnapshotType.MIDDLE, currentBalance, "" - ) - } - } +// currentBalance?.let { currentBalance -> +// if (LocalTime.now().isBefore(LocalTime.of(18,1))) { +// TradingReportManager.recordAssetSnapshot( +// if (LocalTime.now().isAfter(LocalTime.of(18, 0)) +// ) SnapshotType.END else SnapshotType.MIDDLE, currentBalance, "" +// ) +// } +// } if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) } if (KisSession.tradeConfig.auto_cancel_pending_buy) { checkAndCancelPendingBuyOrders() } @@ -801,7 +803,7 @@ object AutoTradingManager { val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}초 ${priceGap}% 차이") if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) { - TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap * 100)}%)로 취소 시도") + TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap)}%)로 취소 시도") KisTradeService.cancelOrder( order.ord_no, // 원주문번호 order.pdno @@ -853,7 +855,7 @@ object AutoTradingManager { }.filter { val rate = it.prdy_ctrt.toDouble() val corpInfo = DartCodeManager.getCorpCode(it.code) - val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15) + val isOk = (rate > 0 && rate < KisSession.tradeConfig.plusFilter) || (rate < 0 && rate > (KisSession.tradeConfig.minusFilter * -1)) if (corpInfo?.cName.isNullOrEmpty()) { false } else { diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index b80badc..5d15962 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -64,7 +64,6 @@ fun TradingDecisionLog() { val filterOptions = listOf("전체", "BUY", "SELL", "SETTING","ANALYZER","WATCH","AFTER","NOTICE")//"PASS",,"RETRY""HOLD", var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) } val tradeConfig by remember { - KisSession.tradeConfig = KisSession.loadTradeConfig() CoroutineScope(Dispatchers.Default).launch { println("CALLED sendTelegramMessage -1") val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul"))