diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index 2f82c41..7a68fc7 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -204,6 +204,7 @@ data class UnfilledOrder( val ord_dvsn_name: String, val rvse_cncl_dvsn_name: String, val ord_tmd : String, + val ord_dvsn_cd : String ) { fun isBuyOrder() : Boolean { return true diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index a65dbfe..a5d8108 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -1187,24 +1187,29 @@ object AutoTradingManager { suspend fun checkAndCancelPendingBuyOrders( ) { + println("checkAndCancelPendingBuyOrders 주문 조회 및 가격 괴리 매수 체크") // 1. 미체결 내역 조회 val unfilledResult = KisTradeService.fetchUnfilledOrders() unfilledResult.onSuccess { response -> val currentTime = System.currentTimeMillis() // 매수 주문('02')만 필터링 - response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order -> + response.filter { + println("${it.prdt_name}[${it.pdno}] | 가격 ${it.ord_unpr.toInt()} ${it.ord_dvsn_cd}") + it.sll_buy_dvsn_cd == "02" && it.ord_dvsn_cd == "00" }.forEach { order -> val orderTimeMillis = parseOrderTime(order.ord_tmd) val elapsedMillis = currentTime - orderTimeMillis if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) { - // 2. 현재가 조회 (가격을 비교하기 위해) + println("checkAndCancelPendingBuyOrders ${elapsedMillis / 1000L}초 경과 | ${order.prdt_name}[${order.pdno}] | 가격 ${order.ord_unpr.toInt()} , ${order.rmnd_qty.toInt()}주 매수 대기중" ) + delay(150) 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}% 차이") + println("checkAndCancelPendingBuyOrders order 주문가 :$order | 현제가 :$currentPrice ${elapsedMillis / 1000L}초 ${priceGap}% 차이 || 괴리 기준 ${KisSession.tradeConfig.auto_cancel_pending_rate}%") + if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) { TradingLogStore.addNotice( order.prdt_name, @@ -1505,6 +1510,9 @@ object AutoTradingManager { if (isWatering) { println("💧 [물타기 분석 모드 가동] ${stock.name} (현재 수익률: ${targetHolding?.profitRate}%)") + } else if (targetHolding != null ){ + println("💧 [분석 제외 대상] ${stock.name} 보유주식 이나 물타기 대상은 아님 (현재 수익률: ${targetHolding?.profitRate}%)") + return@withTimeout } // 2. 재무 안정성 체크 (물타기라도 상폐/자본잠식 위험주는 추가 매수 금지) @@ -1530,6 +1538,10 @@ object AutoTradingManager { if (dailyData.size < 30) return@withTimeout + delay(40) // API 속도 제한 방어 + val weeklyData = tradeService.fetchPeriodChartData(stock.code, "W", true).getOrNull() ?: return@withTimeout + if (weeklyData.size < 10) return@withTimeout + val today = dailyData.lastOrNull() ?: return@withTimeout val rate = today.getFluctuationRate() @@ -1561,20 +1573,25 @@ object AutoTradingManager { return@withTimeout } - val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData } + val tempAnalyzer = TechnicalAnalyzer().apply { + this.daily = dailyData + this.weekly = weeklyData + } val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20) val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0 - val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 3.0) - // 🌟 [완화 3] 기대수익률 및 진입 타이밍 이원화 + // 1. 일봉 단기 타점 (오늘이 미세 반등 타이밍인가?) + val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 3.0) + // 🌟 2. 주봉 스윙 타점 (이 주식이 5% 이상 빠졌을 때, 평균 몇 주 만에 회복하는가?) + val weeklyStats = tempAnalyzer.calculateDynamicReboundStats(weeklyData, 5.0) + val isProfitable: Boolean val isValidEntryTiming: Boolean - val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData) + var isSteadyUptrend = false if (isWatering) { - // 물타기: 평단 낮추기 목적이므로 기대수익률 1.0% 이상이면 충분 + // 물타기 로직 (기존과 동일하게 유지) isProfitable = expectedProfitRate >= 1.0 || dailyStats.avgReboundAmplitude >= 1.5 - // 물타기 진입 타이밍: 반등 주기 도달 OR (일봉 RSI 35 이하 과매도) OR (볼린저 하단 터치) val rsi = tempAnalyzer.calculateRSI(dailyData) val lowerBand = AdvancedTradeAssistant.calculateBollingerLowerBand(dailyData) val isOversoldZone = rsi <= 38.0 || (lowerBand > 0 && currentPrice <= lowerBand * 1.03) @@ -1583,22 +1600,33 @@ object AutoTradingManager { dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance )) } else { - // 신규 진입: 기존 엄격 기준 유지 - isProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate || - (dailyStats.avgReboundAmplitude.times(0.8)) >= KisSession.tradeConfig.minExpectedProfitRate - + // 신규 진입 로직 val isApproaching = tempAnalyzer.checkReboundApproaching( dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance ) - isValidEntryTiming = (dailyStats.isValid && isApproaching && + + val baseProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate || + (dailyStats.avgReboundAmplitude * 0.8) >= KisSession.tradeConfig.minExpectedProfitRate + + isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData) + // 우상향 종목은 프리패스 + isProfitable = baseProfitable || isSteadyUptrend + + // 🌟 [핵심 필터] 주간 반등 주기가 2.5(약 2~3주) 이하인지 확인 (죽은 주식 장기투자 방지) + val isFastSwing = weeklyStats.isValid && weeklyStats.avgReboundPeriod <= 2.5 + + // 일봉 타점이 맞는지 확인 + val isTimingValid = (dailyStats.isValid && isApproaching && dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays && - dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || - isSteadyUptrend + dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) + + // 일봉 타점이 맞으면서 && 주봉상 2주 내 탈출 가능한 종목만 승인 (단, 완벽한 우상향 중이면 통과) + isValidEntryTiming = (isTimingValid && isFastSwing) || isSteadyUptrend } val currentAtr = tempAnalyzer.calculateATR(dailyData) if (!isProfitable || !isValidEntryTiming) { - println("-> [${stock.name}] 목표 수익 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming, 우상향:$isSteadyUptrend) | 기대 수익율 : ${expectedProfitRate} || ${dailyStats.avgReboundAmplitude}") + println("-> [${stock.name}] 목표 수익 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming, 우상향:$isSteadyUptrend) | 기대 수익율 : ${expectedProfitRate} || ${dailyStats.avgReboundAmplitude} | 평균 반등일 : ${dailyStats.avgReboundPeriod}") return@withTimeout } @@ -1623,18 +1651,17 @@ object AutoTradingManager { } } - println("\n💧 [검문소 통과 -> ${if (isWatering) "물타기 탈출 분석" else "신규 매수 분석"}] ${stock.name} (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | 기대 수익율 : ${expectedProfitRate} || ${dailyStats.avgReboundAmplitude}") + println("\n💧 [검문소 통과 -> ${if (isWatering) "물타기 탈출 분석" else "신규 매수 분석"}] ${stock.name} (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming, 우상향:$isSteadyUptrend) | 기대 수익율 : ${expectedProfitRate} || ${dailyStats.avgReboundAmplitude}| 평균 반등일 : ${dailyStats.avgReboundPeriod}") // 멀티 타임프레임 및 AI 분석 진입 val analyzer = coroutineScope { val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } - val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) } val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) } TechnicalAnalyzer().apply { this.daily = dailyData + this.weekly = weeklyData this.min30 = min30.await() - this.weekly = weekly.await() this.monthly = monthly.await() } }