diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index 542ac7d..f5f0e2f 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -547,8 +547,11 @@ object TradingLogStore { reason = log ).apply { CoroutineScope(Dispatchers.Default).launch { - if (((tradingDecision.investmentGrade?.name?.length ?: 0) > 0 && KisSession.tradeConfig.useGradeShare.contains(tradingDecision.investmentGrade?.name)) - ) { + if (((tradingDecision.investmentGrade?.name?.length ?: 0) > 0 && KisSession.tradeConfig.useGradeShare.any { + tradingDecision.investmentGrade?.name?.contains( + it + ) ?: false + })) { NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}") } } diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index 46dabab..3faad38 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -766,4 +766,64 @@ object KisTradeService { } + suspend fun reserveSell(stockCode: String, + qty: String, + price: String, + targetDate : String) :Result { + + val config = KisSession.config + val isDomestic = stockCode.length == 6 && stockCode.all { it.isDigit() } + val baseUrl = if (config.isSimulation) vtsUrl else prodUrl + + // 계좌번호 처리: 8자리면 01 자동 추가 + var pureAccount = config.accountNo.replace("-", "").trim() + if (pureAccount.length == 8) pureAccount += "01" + + val cano = pureAccount.take(8) + val acntPrdtCd = pureAccount.takeLast(2) + + val trId = "CTSC0008U" + + + + return try { + val response = client.post("$baseUrl/uapi/${if(isDomestic) "domestic" else "overseas"}-stock/v1/trading/order-resv") { + header("authorization", "Bearer ${config.tradeToken}") + header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey) + header("appsecret", if (config.isSimulation) config.vtsSecretKey else config.realSecretKey) + header("tr_id", trId) + header("custtype", "P") // [해결] 필수 헤더 추가 + header("Content-Type", "application/json") + + setBody(mapOf( + "CANO" to cano, + "ACNT_PRDT_CD" to acntPrdtCd, + "PDNO" to stockCode, + "ORD_DVSN_CD" to "00", + "SLL_BUY_DVSN_CD" to "01", + "ORD_QTY" to qty, + "ORD_UNPR" to price, + "ORD_OBJT_CBLC_DVSN_CD" to "10", + "RSVN_ORD_END_DT" to targetDate + )) + } + + val body = response.body() // [해결] Polymorphic 직렬화 에러 방지 + val rtCd = body["rt_cd"]?.jsonPrimitive?.content + val msg = body["msg1"]?.jsonPrimitive?.content ?: "메시지 없음" + + if (rtCd == "0") { + // 응답의 output 객체에서 주문 번호(ODNO) 추출 + val orderNo = body["output"]?.jsonObject?.get("ODNO")?.jsonPrimitive?.content + ?: body["output"]?.jsonObject?.get("odno")?.jsonPrimitive?.content // API마다 대소문자가 다를 수 있음 + ?: "" + Result.success(orderNo) // 성공 시 주문 번호 반환 + } else { + val msg = body["msg1"]?.jsonPrimitive?.content ?: "메시지 없음" + Result.failure(Exception("❌ 오류 ($rtCd): $msg")) + } + } catch (e: Exception) { Result.failure(e) } + + } + } \ No newline at end of file diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 45432b0..6ae4af5 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -529,7 +529,7 @@ object AutoTradingManager { println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}") val profit = holding.profitRate.toDouble() var targetPrice = holding.currentPrice.toDouble() - targetPrice = MarketUtil.roundToTickSize(targetPrice + (MarketUtil.getTickSize(targetPrice) * 3.0)) + targetPrice = MarketUtil.roundToTickSize(targetPrice + (MarketUtil.getTickSize(targetPrice) * 3.0)) tradeService.postOrder( stockCode = holding.code, @@ -814,6 +814,21 @@ object AutoTradingManager { } } + suspend fun cancelAllPendingSellOrders( + ) { + // 1. 미체결 내역 조회 + val unfilledResult = KisTradeService.fetchUnfilledOrders() + unfilledResult.onSuccess { response -> + response.filter { it.sll_buy_dvsn_cd == "01" }.forEach { order -> + TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 정규장 시작전 모든 매도 주문 취소") + KisTradeService.cancelOrder( + order.ord_no, // 원주문번호 + order.pdno + ) + } + } + } + // 주문 시간 문자열을 Millis로 변환하는 유틸리티 (당일 주문 기준) fun parseOrderTime(ordTmd: String): Long { return try { @@ -908,8 +923,14 @@ object AutoTradingManager { } else { val now = LocalTime.now() val currentMinute = now.minute - if (now.hour == 9 && currentMinute % 2 == 1 - ) { + if (now.hour == 8 && currentMinute < 50 && currentMinute > 45) { + if (lastForceCheckMinute != currentMinute) { + cancelAllPendingSellOrders() + } + } else if (now.hour == 8 && currentMinute > 55 && currentMinute > 50) { + + } else if (now.hour == 9 && currentMinute % 2 == 1 + ) { if (lastForceCheckMinute != currentMinute) { TradingLogStore.addAnalyzer( " - ",