가격괴리 취소 버그 수정

This commit is contained in:
2026-10-06 13:07:56 +09:00
parent daf74fbe83
commit 25c1485b1e
+42 -58
View File
@@ -279,7 +279,7 @@ object AutoTradingManager {
// 시스템 가이드에 따른 1차 기준 단가 계산
val oneTickLowerPrice = initialBasePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt())
val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice)
val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice.toDouble())
var stockCode = decision.stockCode
var stockName = decision.stockName
@@ -298,24 +298,45 @@ object AutoTradingManager {
val totalQty = orderQty.replace(",", "").toIntOrNull() ?: 0
if (totalQty <= 0) return@launch
// 🌟 [핵심] 매수 2분할 로직 (1주는 1차에 전량, 그 이상은 절반씩)
// 🌟 1. 매도 기준(목표가/손절가)을 루프 밖에서 '최초 1회'만 고정으로 계산
val sRate = -1.5
val tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
var targetBasePrice = initialBasePrice // AI가 분석한 최초 기준가
// 물타기 시 평단가 보정 로직
if (KisSession.tradeConfig.lowerAveragePrice && hasCode) {
try {
val oldTarget = currentBalance?.getHoldings()?.firstOrNull { it.availOrderCount.toInt() > 0 && it.code == decision.stockCode }
if (oldTarget != null) {
targetBasePrice = oldTarget.avgPrice.toDouble() * 1.5 // (기존 보정 로직 유지)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// 🌟 통일된 최종 매도/손절 목표가 (1차든 2차든 이 가격으로 매도됨)
val unifiedTargetPrice = MarketUtil.roundToTickSize(targetBasePrice * (1 + effectiveProfitRate / 100.0))
val unifiedStopPrice = MarketUtil.roundToTickSize(targetBasePrice * (1 + sRate / 100.0))
// 🌟 2. 분할 매수 수량 및 가격 설정
val qty1 = (totalQty + 1) / 2
val qty2 = totalQty - qty1
val price1 = finalPrice
// 2차 매수는 1차 매수가보다 3호가 아래에 대기 (더 싼 가격에 줍기)
val price2 = MarketUtil.roundToTickSize(price1 - (tickSize * KisSession.tradeConfig.buyTickOffset))
println("🔄 [분할 매수 진입] $stockName 1차(${qty1}주 / $price1) | 2차(${qty2}주 / $price2)")
println("🔄 [분할 매수 진입] $stockName 1차(${qty1}주 / $price1) | 2차(${qty2}주 / $price2) -> 🎯통일된 목표가: $unifiedTargetPrice")
// 분할 주문 리스트 생성
val orderList = mutableListOf<Pair<Int, Double>>()
if (qty1 > 0) orderList.add(qty1 to price1)
if (qty2 > 0) orderList.add(qty2 to price2)
// 순차적으로 주문 전송
// 🌟 3. 순차적 주문 전송 (DB 저장 시 unifiedTargetPrice 공통 사용)
orderList.forEachIndexed { index, (qty, price) ->
val orderStep = index + 1 // 1차, 2차 표시용
val orderStep = index + 1
KisTradeService.postOrder(
stockCode,
@@ -324,30 +345,6 @@ object AutoTradingManager {
isBuy = true
).onSuccess { realOrderNo ->
println("[${investmentGrade.displayName}] ${orderStep}차 주문 성공: $realOrderNo $stockCode ${qty}주 $price")
TradingLogStore.addLog(
decision,
"BUY",
"[${investmentGrade.displayName}] ${orderStep}차 분할 주문 성공: $realOrderNo"
)
val sRate = -1.5
val tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
var targetBasePrice = initialBasePrice
try {
val oldTarget = currentBalance?.getHoldings()?.firstOrNull { it.availOrderCount.toInt() > 0 && it.code == decision.stockCode }
if (KisSession.tradeConfig.lowerAveragePrice && hasCode && oldTarget != null) {
val avgPrive = oldTarget.avgPrice.toDouble()
// 대표님이 작성하신 기존 물타기 basePrice 보정 로직 유지
targetBasePrice = avgPrive * 1.5
}
} catch (e: Exception) {
e.printStackTrace()
}
val calculatedTarget = MarketUtil.roundToTickSize(targetBasePrice * (1 + effectiveProfitRate / 100.0))
val calculatedStop = MarketUtil.roundToTickSize(targetBasePrice * (1 + sRate / 100.0))
if (!hasCode) {
DatabaseFactory.saveAutoTrade(
@@ -358,8 +355,8 @@ object AutoTradingManager {
quantity = qty,
profitRate = effectiveProfitRate,
stopLossRate = sRate,
targetPrice = calculatedTarget,
stopLossPrice = calculatedStop,
targetPrice = unifiedTargetPrice, // 👈 통일된 목표가 주입
stopLossPrice = unifiedStopPrice, // 👈 통일된 손절가 주입
status = "PENDING_BUY",
isDomestic = true
)
@@ -379,12 +376,10 @@ object AutoTradingManager {
TradingLogStore.addLog(
decision,
"BUY",
"[${investmentGrade.displayName}] ${orderStep}차 감시 설정 완료 (예상 목표가: $calculatedTarget): $realOrderNo"
"[${investmentGrade.displayName}] ${orderStep}차 감시 설정 완료 (예상 목표가: $unifiedTargetPrice): $realOrderNo"
)
}.onFailure {
println("매수 실패: ${it.message} $stockCode $qty $price")
// 1차 주문에서 이미 예수금이 부족하다면 2차는 무의미하므로 재분석 대기열 처리
if (it.message?.contains("주문가능금액을 초과") == true && index == 0) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = stockName))
TradingLogStore.addWatchLog(decision, "WATCH", "${it.message ?: "매수 실패"} => 재분석 대기열에 추가")
@@ -393,7 +388,6 @@ object AutoTradingManager {
}
}
// 🌟 [안전장치] KIS API 초당 호출 제한 방어
delay(250)
}
} else if (!hasCode && KisSession.isAvailBuyTime(LocalTime.now()) == false &&
@@ -438,22 +432,17 @@ object AutoTradingManager {
if (dbItem.status == TradeStatus.PENDING_BUY) {
// ✅ 1. 진짜 사온 가격 (실제 매수 체결가)
var actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
// 💡 [수정] 매수 주문(orderNo)에 대해 '진짜 산 가격'을 기록해야 합니다.
// 기존에는 여기에 finalTargetPrice를 넣으셨는데, 그러면 매수 단가가 오염됩니다.
TradingReportManager.updateExecution(orderNo, actualBuyPrice, dbItem.quantity)
var hasCodes =
KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()
?.any { it.code.equals(dbItem.code) && it.quantity.toInt() > 2 && dbItem.quantity == KisSession.tradeConfig.lowerAverageStockCount } ?: false
if (hasCodes) {
actualBuyPrice = actualBuyPrice * 1.1
}
val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
val finalProfitRate = maxOf(dbItem.profitRate, absoluteMinRate)
val finalTargetPrice =
MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
println("🎯 [매수 확정] ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가 설정: ${finalTargetPrice.toInt()}")
// 물타기 체크 로직 유지
var hasCodes = KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()
?.any { it.code == dbItem.code && it.quantity.toInt() > 2 && dbItem.quantity == KisSession.tradeConfig.lowerAverageStockCount } == true
if (hasCodes) { actualBuyPrice = actualBuyPrice * 1.1 }
// 🌟 [수정] actualBuyPrice로 재계산하지 않고, excuteTrade에서 최초로 결정해 DB에 넣은 목표가를 그대로 가져옵니다.
val finalTargetPrice = dbItem.targetPrice
println("🎯 [매수 확정] ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 통일된 목표가 세팅: ${finalTargetPrice.toInt()}")
KisTradeService.postOrder(
stockCode = dbItem.code,
@@ -468,12 +457,7 @@ object AutoTradingManager {
stockName = dbItem.name,
isBuy = false,
orderQty = dbItem.quantity,
reason = "🎯 목표 수익률 ${
String.format(
"%.2f",
finalProfitRate
)
}% 도달을 위한 익절 주문",
reason = "🎯 목표 매도가 ${finalTargetPrice} 도달을 위한 익절 주문",
holdingAvgPrice = actualBuyPrice, // 👈 여기서 매수단가를 넘겨줘야 매도 리포트가 정확해집니다!
decision = null
)
@@ -1207,7 +1191,7 @@ object AutoTradingManager {
.getOrNull()?.stck_prpr?.toDouble() ?: 0.0
val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0
// 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때)
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice
val priceGap = (Math.abs(currentPrice - orderedPrice) / orderedPrice) * 100
println("checkAndCancelPendingBuyOrders order 주문가 :$order | 현제가 :$currentPrice ${elapsedMillis / 1000L}초 ${priceGap}% 차이 || 괴리 기준 ${KisSession.tradeConfig.auto_cancel_pending_rate}%")
if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {