...
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user