diff --git a/src/main/kotlin/network/DartCodeManager.kt b/src/main/kotlin/network/DartCodeManager.kt index 81cf488..3a5a4d9 100644 --- a/src/main/kotlin/network/DartCodeManager.kt +++ b/src/main/kotlin/network/DartCodeManager.kt @@ -3,6 +3,8 @@ package network import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* +import model.RankingStock +import service.AutoTradingManager import java.io.ByteArrayInputStream import java.io.File import java.util.zip.ZipInputStream @@ -75,6 +77,7 @@ object DartCodeManager { // 종목코드(stock_code)가 있는 상장사만 매핑에 추가 if (stockCode.isNotEmpty()) { corpCodeMap[stockCode] = CorpInfo(corpCode, corpName, stockCode) +// AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = corpName)) } } } diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index c46e877..b72edf1 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -48,8 +48,8 @@ object AutoTradingManager { private const val MAX_RISE_RATE = 21.0 private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분 private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인 - private const val STUCK_THRESHOLD = 5 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 - + private const val STUCK_THRESHOLD = 3 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 + private const val ONE_STOCK_ALYSIS_TIME = 90000L fun isRunning(): Boolean = discoveryJob?.isActive == true private var remainingCandidates = mutableListOf() // private val processedCodes = mutableSetOf() // 중복 처리 방지용 (선택 사항) @@ -82,32 +82,36 @@ object AutoTradingManager { suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) { // 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다. println("resumePendingSellOrders") - val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED)) - println("pendingSells >>> ${pendingSells.size}") - pendingSells.forEach { item -> +// val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED)) +// println("pendingSells >>> ${pendingSells.size}") + balance.holdings.forEach { holding -> // 2. 실제로 잔고에 해당 종목이 있는지 확인 (안전장치) // val balance = tradeService.fetchIntegratedBalance().getOrNull() - val holding = balance?.holdings?.find { it.code == item.code } +// val holding = balance?.holdings?.find { it.code == item.code } + + if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > 0.8) { + println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ") - 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)로 다시 매도 주문 전송 + var targetPrice = holding.currentPrice.toDouble() + targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice)) + + println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도") tradeService.postOrder( - stockCode = item.code, - qty = min(item.quantity,holding.availOrderCount.toInt()).toString(), - price = final.toLong().toString(), + stockCode = holding.code, + qty = holding.availOrderCount, + price = targetPrice.toInt().toString(), isBuy = false ).onSuccess { newOrderNo -> // 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지 - DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo) - println("✅ [재주문 완료] ${item.name}: $newOrderNo") +// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo) + println("✅ [재주문 완료] ${holding.name}: $newOrderNo") }.onFailure { - println("❌ [재주문 실패] ${item.name}: ${it.message}") + println("❌ [재주문 실패] ${holding.name}: ${it.message}") } } else { // 잔고에 없다면 이미 매도된 것으로 간주하고 완료 처리 - DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED) +// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED) } delay(200) // API 호출 부하 방지 } @@ -127,11 +131,11 @@ object AutoTradingManager { // [프로세스 1] 장 마감 및 잔고 체크 val now = LocalTime.now(ZoneId.of("Asia/Seoul")) //&& now.isBefore(LocalTime.of(15, 30)) - if (now.isAfter(LocalTime.of(15, 15)) ) { + if (now.isAfter(LocalTime.of(15, 20)) ) { executeClosingLiquidation(tradeService) return@withTimeout } - +// addToReanalysis(RankingStock(mksc_shrn_iscd = ,hts_kor_isnm = )) val balance = tradeService.fetchIntegratedBalance().getOrNull() balance?.let { resumePendingSellOrders(tradeService,it) } @@ -158,7 +162,7 @@ object AutoTradingManager { .toMutableList() if (reanalysisList.isNotEmpty()) { - candidates.addAll(reanalysisList) + candidates.addAll(reanalysisList.asReversed()) } reanalysisList.clear() remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }.distinctBy { it.code }) @@ -186,26 +190,25 @@ object AutoTradingManager { iterator.remove() } println("남은 후보군 개수 : ${totalCount}") - delay(250) + delay(100) } - println("⏱️ [Cycle End] ${LocalTime.now()}") } } catch (e: TimeoutCancellationException) { println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.") } catch (e: Exception) { println("⚠️ [Loop Error] ${e.message}") - delay(3000) + delay(1500) } - waitForNextCycle(0.3) + waitForNextCycle(0.2) } } } fun addToReanalysis(stock: RankingStock) { val count = retryCountMap.getOrDefault(stock.code, 0) - if (count < 2) { // 최대 2회까지만 재시도하여 무한 루프 방지 + if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지 retryCountMap[stock.code] = count + 1 reanalysisList.add(stock) println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록") @@ -218,7 +221,7 @@ object AutoTradingManager { val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX) val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX) // 개별 종목 분석은 최대 2분으로 제한 - withTimeout(120000L) { + withTimeout(ONE_STOCK_ALYSIS_TIME) { val corpInfo = DartCodeManager.getCorpCode(stock.code) if (corpInfo?.cName.isNullOrEmpty()) { print("-> 기업명을 못찾아서 제외 | ") @@ -359,8 +362,8 @@ object FinancialAnalyzer { val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만 val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상 val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함 - - return isDebtSafe && isLiquiditySafe && isNotDeficit +//&& isNotDeficit + return isDebtSafe && isLiquiditySafe } /** diff --git a/src/main/kotlin/ui/DashboardScreen.kt b/src/main/kotlin/ui/DashboardScreen.kt index 9f39276..588073c 100644 --- a/src/main/kotlin/ui/DashboardScreen.kt +++ b/src/main/kotlin/ui/DashboardScreen.kt @@ -384,15 +384,13 @@ fun DashboardScreen() { val saveAction = { var newValue = localText.toDoubleOrNull() ?: 0.0 -// if (configKey.name.contains("PROFIT")) { -// newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) -// } +// KisSession.config.setValues(configKey, newValue) DatabaseFactory.saveConfig(KisSession.config) println("💾 저장됨: ${configKey.label} = $newValue") labelText = if (configKey.name.contains("PROFIT")) { 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(configKey) + KisSession.config.getValues( + ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues( ConfigIndex.TAX_INDEX)}" } else { getRemaining(configKey.label,common) + ": -${localText} 호가 매수}" @@ -401,7 +399,7 @@ fun DashboardScreen() { labelText = if (configKey.name.contains("PROFIT")) { 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(configKey) + KisSession.config.getValues( + ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues( ConfigIndex.TAX_INDEX)} " } else { getRemaining(configKey.label,common) + ": -${localText} 호가 매수}" diff --git a/src/main/kotlin/ui/IntegratedOrderSection.kt b/src/main/kotlin/ui/IntegratedOrderSection.kt index 07612d3..473542f 100644 --- a/src/main/kotlin/ui/IntegratedOrderSection.kt +++ b/src/main/kotlin/ui/IntegratedOrderSection.kt @@ -266,8 +266,8 @@ fun IntegratedOrderSection( fun resultCheck(completeTradingDecision :TradingDecision) { val weights = mapOf( - "short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소 - "profit" to 0.3, + "short" to 0.2, // 초단기 점수가 낮아도 전체에 미치는 영향 감소 + "profit" to 0.4, "safe" to 0.4 // 중장기 점수 비중 강화 ) @@ -322,11 +322,15 @@ fun IntegratedOrderSection( resultCheck(completeTradingDecision) } "SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}") - else -> { + "HOLD" -> { append = 0.0 resultCheck(completeTradingDecision) println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}") } + else -> { + append = 0.0 + println("[$stockCode] ${completeTradingDecision?.decision} resultCheck: ${completeTradingDecision?.reason}") + } } } } diff --git a/src/main/kotlin/util/MarketUtil.kt b/src/main/kotlin/util/MarketUtil.kt index f3d27a1..6e89c2f 100644 --- a/src/main/kotlin/util/MarketUtil.kt +++ b/src/main/kotlin/util/MarketUtil.kt @@ -40,4 +40,5 @@ object MarketUtil { return Math.round(price / tick) * tick } + } \ No newline at end of file