This commit is contained in:
2026-09-22 17:18:21 +09:00
parent 40c70577ca
commit daf74fbe83
2 changed files with 48 additions and 20 deletions
+1
View File
@@ -204,6 +204,7 @@ data class UnfilledOrder(
val ord_dvsn_name: String, val ord_dvsn_name: String,
val rvse_cncl_dvsn_name: String, val rvse_cncl_dvsn_name: String,
val ord_tmd : String, val ord_tmd : String,
val ord_dvsn_cd : String
) { ) {
fun isBuyOrder() : Boolean { fun isBuyOrder() : Boolean {
return true return true
+47 -20
View File
@@ -1187,24 +1187,29 @@ object AutoTradingManager {
suspend fun checkAndCancelPendingBuyOrders( suspend fun checkAndCancelPendingBuyOrders(
) { ) {
println("checkAndCancelPendingBuyOrders 주문 조회 및 가격 괴리 매수 체크")
// 1. 미체결 내역 조회 // 1. 미체결 내역 조회
val unfilledResult = KisTradeService.fetchUnfilledOrders() val unfilledResult = KisTradeService.fetchUnfilledOrders()
unfilledResult.onSuccess { response -> unfilledResult.onSuccess { response ->
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
// 매수 주문('02')만 필터링 // 매수 주문('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 orderTimeMillis = parseOrderTime(order.ord_tmd)
val elapsedMillis = currentTime - orderTimeMillis val elapsedMillis = currentTime - orderTimeMillis
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) { 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) val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno)
.getOrNull()?.stck_prpr?.toDouble() ?: 0.0 .getOrNull()?.stck_prpr?.toDouble() ?: 0.0
val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0 val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0
// 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때) // 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때)
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice 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) { if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {
TradingLogStore.addNotice( TradingLogStore.addNotice(
order.prdt_name, order.prdt_name,
@@ -1505,6 +1510,9 @@ object AutoTradingManager {
if (isWatering) { if (isWatering) {
println("💧 [물타기 분석 모드 가동] ${stock.name} (현재 수익률: ${targetHolding?.profitRate}%)") println("💧 [물타기 분석 모드 가동] ${stock.name} (현재 수익률: ${targetHolding?.profitRate}%)")
} else if (targetHolding != null ){
println("💧 [분석 제외 대상] ${stock.name} 보유주식 이나 물타기 대상은 아님 (현재 수익률: ${targetHolding?.profitRate}%)")
return@withTimeout
} }
// 2. 재무 안정성 체크 (물타기라도 상폐/자본잠식 위험주는 추가 매수 금지) // 2. 재무 안정성 체크 (물타기라도 상폐/자본잠식 위험주는 추가 매수 금지)
@@ -1530,6 +1538,10 @@ object AutoTradingManager {
if (dailyData.size < 30) return@withTimeout 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 today = dailyData.lastOrNull() ?: return@withTimeout
val rate = today.getFluctuationRate() val rate = today.getFluctuationRate()
@@ -1561,20 +1573,25 @@ object AutoTradingManager {
return@withTimeout 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 volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0 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 isProfitable: Boolean
val isValidEntryTiming: Boolean val isValidEntryTiming: Boolean
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData) var isSteadyUptrend = false
if (isWatering) { if (isWatering) {
// 물타기: 평단 낮추기 목적이므로 기대수익률 1.0% 이상이면 충분 // 물타기 로직 (기존과 동일하게 유지)
isProfitable = expectedProfitRate >= 1.0 || dailyStats.avgReboundAmplitude >= 1.5 isProfitable = expectedProfitRate >= 1.0 || dailyStats.avgReboundAmplitude >= 1.5
// 물타기 진입 타이밍: 반등 주기 도달 OR (일봉 RSI 35 이하 과매도) OR (볼린저 하단 터치)
val rsi = tempAnalyzer.calculateRSI(dailyData) val rsi = tempAnalyzer.calculateRSI(dailyData)
val lowerBand = AdvancedTradeAssistant.calculateBollingerLowerBand(dailyData) val lowerBand = AdvancedTradeAssistant.calculateBollingerLowerBand(dailyData)
val isOversoldZone = rsi <= 38.0 || (lowerBand > 0 && currentPrice <= lowerBand * 1.03) val isOversoldZone = rsi <= 38.0 || (lowerBand > 0 && currentPrice <= lowerBand * 1.03)
@@ -1583,22 +1600,33 @@ object AutoTradingManager {
dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance
)) ))
} else { } else {
// 신규 진입: 기존 엄격 기준 유지 // 신규 진입 로직
isProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate ||
(dailyStats.avgReboundAmplitude.times(0.8)) >= KisSession.tradeConfig.minExpectedProfitRate
val isApproaching = tempAnalyzer.checkReboundApproaching( val isApproaching = tempAnalyzer.checkReboundApproaching(
dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance 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.maxExpectedReboundDays &&
dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays)
isSteadyUptrend
// 일봉 타점이 맞으면서 && 주봉상 2주 내 탈출 가능한 종목만 승인 (단, 완벽한 우상향 중이면 통과)
isValidEntryTiming = (isTimingValid && isFastSwing) || isSteadyUptrend
} }
val currentAtr = tempAnalyzer.calculateATR(dailyData) val currentAtr = tempAnalyzer.calculateATR(dailyData)
if (!isProfitable || !isValidEntryTiming) { 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 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 분석 진입 // 멀티 타임프레임 및 AI 분석 진입
val analyzer = coroutineScope { val analyzer = coroutineScope {
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } 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()) } val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
TechnicalAnalyzer().apply { TechnicalAnalyzer().apply {
this.daily = dailyData this.daily = dailyData
this.weekly = weeklyData
this.min30 = min30.await() this.min30 = min30.await()
this.weekly = weekly.await()
this.monthly = monthly.await() this.monthly = monthly.await()
} }
} }