diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index dcfff27..74f7130 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -321,7 +321,8 @@ object KisTradeService { stockCode: String, qty: String, price: String, - isBuy: Boolean + isBuy: Boolean, + orderDivision: String = "" ): Result { val config = KisSession.config val isDomestic = stockCode.length == 6 && stockCode.all { it.isDigit() } @@ -339,6 +340,12 @@ object KisTradeService { isDomestic && !config.isSimulation -> if (isBuy) "TTTC0802U" else "TTTC0801U" else -> if (isBuy) "TTTS3002U" else "TTTS3001U" } + val finalOrderDivision = when { + orderDivision.isNotEmpty() -> orderDivision // 외부에서 넘겨준 코드 우선 (02, 03, 34 등) + price == "0" || price.isEmpty() -> "01" // 시장가 + else -> "00" // 지정가 + } + return try { val response = client.post("$baseUrl/uapi/${if(isDomestic) "domestic" else "overseas"}-stock/v1/trading/order-cash") { @@ -353,7 +360,7 @@ object KisTradeService { "CANO" to cano, "ACNT_PRDT_CD" to acntPrdtCd, "PDNO" to stockCode, - "ORD_DVSN" to if (price == "0" || price.isEmpty()) "01" else "00", + "ORD_DVSN" to finalOrderDivision, "ORD_QTY" to qty, "ORD_UNPR" to if (price.isEmpty() || price == "0") "0" else price )) diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 8e7d5a3..1ffadb6 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -365,10 +365,8 @@ object AutoTradingManager { runDiscoveryLoop(globalCallback) } - - suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) { - // 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다. - println("resumePendingSellOrders") + suspend fun sellingAfterMarketOnePrice(tradeService: KisTradeService,balance : UnifiedBalance) { + println("sellingAfterMarketOnePrice") balance.holdings.forEach { holding -> if (BLACKLISTEDSTOCKCODES.contains(holding.code)){ println("❌ 차단 처리된 주식 : ${holding.name}") @@ -378,46 +376,100 @@ object AutoTradingManager { "거랙 차단 대상 : ${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} , 주문 가능 : ${holding.availOrderCount}, 수익율 : ${holding.profitRate}") + if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > 0.5) { println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ") - - // 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송 var targetPrice = holding.currentPrice.toDouble() - targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice)) - - println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도") -// TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문[예상수익 : ${holding.profitRate}] ") + targetPrice = MarketUtil.roundToTickSize(targetPrice) tradeService.postOrder( stockCode = holding.code, qty = holding.availOrderCount, price = targetPrice.toInt().toString(), - isBuy = false + isBuy = false, + "34" ).onSuccess { newOrderNo -> - // 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지 -// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo) println("✅ [재주문 완료] ${holding.name}: $newOrderNo") TradingLogStore.addSellLog( holding.code, targetPrice.toString(), "SELL", - "🎊 보유 주식 매도 주문 완료[예상수익 : ${holding.profitRate}] " + "🎊 시간외 단일가 주식 재고털이 주문 완료" ) }.onFailure { TradingLogStore.addSellLog( holding.code, targetPrice.toString(), "SELL", - "🎊 보유 주식 매도 주문 실패[${it.message}] " + "🎊 시간외 단일가 주식 재고털이 주문 실패[${it.message}] " ) } - } else { - TradingLogStore.addAnalyzer( - "보유주식[${holding.name}]", - holding.code, - "수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})" - ) } - delay(200) // API 호출 부하 방지 + delay(300) // API 호출 부하 방지 + } + } + } + + + suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) { + if (isRunning()) return + val now = LocalTime.now() + val currentMinute = now.minute + if (now.isBefore(H16) && now.isAfter(H08M35)) { + println("resumePendingSellOrders") + 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} ") + + // 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송 + var targetPrice = holding.currentPrice.toDouble() + val now = LocalTime.now() + val currentMinute = now.minute + var isBefore930 = false + if (now.hour == 9 && currentMinute < 30) { + targetPrice = targetPrice + isBefore930 = true + } else { + targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice)) + } + println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도") + tradeService.postOrder( + stockCode = holding.code, + qty = holding.availOrderCount, + price = targetPrice.toInt().toString(), + isBuy = false, + ).onSuccess { newOrderNo -> + println("✅ [재주문 완료] ${holding.name}: $newOrderNo") + TradingLogStore.addSellLog( + holding.code, + targetPrice.toString(), + "SELL", + "🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료" + ) + }.onFailure { + TradingLogStore.addSellLog( + holding.code, + targetPrice.toString(), + "SELL", + "🎊 보유 주식 매도 주문 실패[${it.message}] " + ) + } + } else { + TradingLogStore.addAnalyzer( + "보유주식[${holding.name}]", + holding.code, + "수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})" + ) + } + delay(200) // API 호출 부하 방지 + } } } } @@ -464,6 +516,7 @@ object AutoTradingManager { var now = LocalTime.now(ZoneId.of("Asia/Seoul")) var currentTimeMillis = System.currentTimeMillis() var waitTime = 0.2 + val H16 = LocalTime.of(16, 0) val H18 = LocalTime.of(18, 0) val H08M35 = LocalTime.of(8, 35) val H08M30 = LocalTime.of(8, 30) @@ -558,9 +611,13 @@ object AutoTradingManager { return batch } - suspend fun checkBalance() : UnifiedBalance? { + suspend fun checkBalance(isMorning: Boolean = true) : UnifiedBalance? { val balance = KisTradeService.fetchIntegratedBalance().getOrNull() - if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) } + if (isMorning) { + if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) } + } else { + balance?.let { sellingAfterMarketOnePrice(KisTradeService, it) } + } return balance } @@ -612,18 +669,20 @@ object AutoTradingManager { while (iterator.hasNext()) { totalCount-- val stock = iterator.next() - if (BLACKLISTEDSTOCKCODES.contains(stock.code)){ - println("❌ 차단 처리된 주식 : ${stock.name}") - } else { - try { - processSingleStock(stock, myCash, KisTradeService, globalCallback) - } catch (e: Exception) { - println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}") - } finally { - iterator.remove() + if (now.isBefore(H16) && now.isAfter(H08M35)) { + if (BLACKLISTEDSTOCKCODES.contains(stock.code)) { + println("❌ 차단 처리된 주식 : ${stock.name}") + } else { + try { + processSingleStock(stock, myCash, KisTradeService, globalCallback) + } catch (e: Exception) { + println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}") + } finally { + iterator.remove() + } + println("남은 후보군 개수 : ${totalCount}") + delay(100) } - println("남은 후보군 개수 : ${totalCount}") - delay(100) } sellSchedule() } @@ -643,12 +702,19 @@ object AutoTradingManager { lastForceCheckMinute = currentMinute // 실행 완료 기록 } } -// else if(now.hour % 2 == 1 && (currentMinute == 43)) { -// TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true) -// println("⏰ [강제 스케줄 실행] 오전 ${now.hour}시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") -// checkBalance() -// lastForceCheckMinute = currentMinute // 실행 완료 기록 -// } + else if((now.hour == 16 || now.hour == 17) && (currentMinute == 5 || currentMinute == 55)) { + if (lastForceCheckMinute != currentMinute) { + TradingLogStore.addAnalyzer( + " - ", + " - ", + "⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 매도 체크를 시작합니다.", + true + ) + println("⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 매도 체크를 시작합니다.") + checkBalance(false) + lastForceCheckMinute = currentMinute // 실행 완료 기록 + } + } } suspend fun finalizeMarketClose(now: LocalTime) { diff --git a/src/main/kotlin/ui/SettingsScreen.kt b/src/main/kotlin/ui/SettingsScreen.kt index 84d53bb..6338d67 100644 --- a/src/main/kotlin/ui/SettingsScreen.kt +++ b/src/main/kotlin/ui/SettingsScreen.kt @@ -63,7 +63,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { KisSession.config = config DatabaseFactory.saveConfig(config) - + DartCodeManager.updateCorpCodes(HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } })