diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 204a556..5084f45 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -252,6 +252,8 @@ class TradeConfig { var useAutoRepost : Boolean = false var minusFilter : Double = 15.0 var plusFilter : Double = 15.0 + var excuteCountOnMin : Int = 2 + var autoSellOrder : Boolean = false } diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index 3faad38..bafb182 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -331,7 +331,7 @@ object KisTradeService { stockCode: String, qty: String, price: String, - isBuy: Boolean, + isBuy: Boolean = false, orderDivision: String = "", marketCode : String = "KRX" ): Result { diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 02bb3cc..cac36ba 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -217,7 +217,7 @@ object AutoTradingManager { TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단") AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") - } else { + } else if (KisSession.isAvailBuyTime(LocalTime.now())){ println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice") KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true) @@ -271,6 +271,17 @@ object AutoTradingManager { TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") } } + } else { + val unfilledResult = KisTradeService.fetchUnfilledOrders() + unfilledResult.onSuccess { response -> + response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order -> + TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 매수시간 종료 후 모든 매수 취소") + KisTradeService.cancelOrder( + order.ord_no, // 원주문번호 + order.pdno + ) + } + } } } } @@ -528,8 +539,9 @@ object AutoTradingManager { && holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) { 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)) + var targetPrice = if (KisSession.tradeConfig.autoSellOrder ) holding.avgPrice.toDouble() else holding.currentPrice.toDouble() + targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0) + tradeService.postOrder( stockCode = holding.code, @@ -764,15 +776,6 @@ 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, "" -// ) -// } -// } - if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) } if (KisSession.tradeConfig.auto_cancel_pending_buy) { checkAndCancelPendingBuyOrders() } } else { @@ -791,14 +794,10 @@ object AutoTradingManager { val orderTimeMillis = parseOrderTime(order.ord_tmd) val elapsedMillis = currentTime - orderTimeMillis - - // 조건 A: 설정된 대기 시간 경과 여부 - if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) { - + if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time ) { // 2. 현재가 조회 (가격을 비교하기 위해) val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno).getOrNull()?.stck_prpr?.toDouble() ?: 0.0 val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0 - // 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때) val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}초 ${priceGap}% 차이") @@ -916,53 +915,62 @@ object AutoTradingManager { } println("⏱️ [Cycle End] ${LocalTime.now()}") } - private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장 + // private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장 + private val executionCountMap = mutableMapOf() suspend fun sellSchedule() { - if (KisSession.config.take_profit == false) { + if (KisSession.config.take_profit == false) return + val now = LocalTime.now() + val timeKey = String.format("%02d:%02d", now.hour, now.minute) // 예: "09:05" + val currentCount = executionCountMap.getOrDefault(timeKey, 0) + if (currentCount >= KisSession.tradeConfig.excuteCountOnMin) { return } - } else { - val now = LocalTime.now() - val currentMinute = now.minute - 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( - " - ", - " - ", - "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", - true + var isExecuted = false + val currentMinute = now.minute + if (now.isBefore(LocalTime.of(8,50)) && now.isAfter(LocalTime.of(8,45))) { + cancelAllPendingSellOrders() + isExecuted = true + } else if ( (now.isBefore(LocalTime.of(16,0)) && now.isAfter(KisSession.endBuyTime())) ) { + val unfilledResult = KisTradeService.fetchUnfilledOrders() + unfilledResult.onSuccess { response -> + response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order -> + TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 정규장 후 모든 매수 취소") + KisTradeService.cancelOrder( + order.ord_no, // 원주문번호 + order.pdno ) - println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") - checkBalance() - lastForceCheckMinute = currentMinute // 실행 완료 기록 - } - } else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) { - if (lastForceCheckMinute != currentMinute) { - TradingLogStore.addAnalyzer( - " - ", - " - ", - "⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.", - true - ) - var list = mutableListOf("X") - if (now.hour != 8 && now.hour < 18) { - list.add("Y") - } - list.forEach { code -> - KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let { - sellingAfterMarketOnePrice(KisTradeService, it, code) - } - } - lastForceCheckMinute = currentMinute // 실행 완료 기록 } } + isExecuted = true + } else if (now.hour == 9) { + TradingLogStore.addAnalyzer( + " - ", + " - ", + "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", + true + ) + println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") + checkBalance() + isExecuted = true + } else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) { + TradingLogStore.addAnalyzer( + " - ", + " - ", + "⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.", + true + ) + var list = mutableListOf("X") + if (now.hour != 8 && now.hour < 18) { + list.add("Y") + } + list.forEach { code -> + KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let { + sellingAfterMarketOnePrice(KisTradeService, it, code) + } + } + isExecuted = true } + if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 } + if (now.hour >= 20) { executionCountMap.clear() } } diff --git a/stocks_universe.json b/stocks_universe.json index 385b905..d2a095b 100644 --- a/stocks_universe.json +++ b/stocks_universe.json @@ -11238,5 +11238,145 @@ { "code": "238490", "name": "힘스" + }, + { + "code": "000980", + "name": "교보19호스팩" + }, + { + "code": "001320", + "name": "교보20호스팩" + }, + { + "code": "478340", + "name": "나라스페이스테크놀로지" + }, + { + "code": "403850", + "name": "더핑크퐁컴퍼니" + }, + { + "code": "000010", + "name": "덕양에너젠" + }, + { + "code": "491000", + "name": "리브스메드" + }, + { + "code": "394420", + "name": "리센스메디컬" + }, + { + "code": "000930", + "name": "미래에셋비전스팩8호" + }, + { + "code": "000960", + "name": "미래에셋비전스팩9호" + }, + { + "code": "488900", + "name": "비츠로넥스텍" + }, + { + "code": "001150", + "name": "삼성스팩13호" + }, + { + "code": "000130", + "name": "삼진식품" + }, + { + "code": "061090", + "name": "세나테크놀로지" + }, + { + "code": "490470", + "name": "세미파이브" + }, + { + "code": "001300", + "name": "신한제17호스팩" + }, + { + "code": "388210", + "name": "씨엠티엑스" + }, + { + "code": "493280", + "name": "아이엠바이오로직스" + }, + { + "code": "476830", + "name": "알지노믹스" + }, + { + "code": "459550", + "name": "알트" + }, + { + "code": "000110", + "name": "액스비스" + }, + { + "code": "458350", + "name": "에스팀" + }, + { + "code": "000090", + "name": "에임드바이오" + }, + { + "code": "001050", + "name": "유진스팩12호" + }, + { + "code": "469610", + "name": "이노테크" + }, + { + "code": "261520", + "name": "이지스" + }, + { + "code": "493330", + "name": "지에프아이" + }, + { + "code": "000820", + "name": "카나프테라퓨틱스" + }, + { + "code": "439960", + "name": "코스모로보틱스" + }, + { + "code": "464490", + "name": "쿼드메디슨" + }, + { + "code": "494120", + "name": "큐리오시스" + }, + { + "code": "466690", + "name": "키움히어로제1호스팩" + }, + { + "code": "001310", + "name": "키움히어로제2호스팩" + }, + { + "code": "487580", + "name": "폴레드" + }, + { + "code": "001010", + "name": "하나36호스팩" + }, + { + "code": "408470", + "name": "한패스" } ] \ No newline at end of file