가격괴리 취소 버그 수정
This commit is contained in:
@@ -279,7 +279,7 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
// 시스템 가이드에 따른 1차 기준 단가 계산
|
// 시스템 가이드에 따른 1차 기준 단가 계산
|
||||||
val oneTickLowerPrice = initialBasePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt())
|
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 stockCode = decision.stockCode
|
||||||
var stockName = decision.stockName
|
var stockName = decision.stockName
|
||||||
@@ -298,24 +298,45 @@ object AutoTradingManager {
|
|||||||
val totalQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
val totalQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||||
if (totalQty <= 0) return@launch
|
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 qty1 = (totalQty + 1) / 2
|
||||||
val qty2 = totalQty - qty1
|
val qty2 = totalQty - qty1
|
||||||
|
|
||||||
val price1 = finalPrice
|
val price1 = finalPrice
|
||||||
// 2차 매수는 1차 매수가보다 3호가 아래에 대기 (더 싼 가격에 줍기)
|
|
||||||
val price2 = MarketUtil.roundToTickSize(price1 - (tickSize * KisSession.tradeConfig.buyTickOffset))
|
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>>()
|
val orderList = mutableListOf<Pair<Int, Double>>()
|
||||||
if (qty1 > 0) orderList.add(qty1 to price1)
|
if (qty1 > 0) orderList.add(qty1 to price1)
|
||||||
if (qty2 > 0) orderList.add(qty2 to price2)
|
if (qty2 > 0) orderList.add(qty2 to price2)
|
||||||
|
|
||||||
// 순차적으로 주문 전송
|
// 🌟 3. 순차적 주문 전송 (DB 저장 시 unifiedTargetPrice 공통 사용)
|
||||||
orderList.forEachIndexed { index, (qty, price) ->
|
orderList.forEachIndexed { index, (qty, price) ->
|
||||||
val orderStep = index + 1 // 1차, 2차 표시용
|
val orderStep = index + 1
|
||||||
|
|
||||||
KisTradeService.postOrder(
|
KisTradeService.postOrder(
|
||||||
stockCode,
|
stockCode,
|
||||||
@@ -324,30 +345,6 @@ object AutoTradingManager {
|
|||||||
isBuy = true
|
isBuy = true
|
||||||
).onSuccess { realOrderNo ->
|
).onSuccess { realOrderNo ->
|
||||||
println("[${investmentGrade.displayName}] ${orderStep}차 주문 성공: $realOrderNo $stockCode ${qty}주 $price")
|
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) {
|
if (!hasCode) {
|
||||||
DatabaseFactory.saveAutoTrade(
|
DatabaseFactory.saveAutoTrade(
|
||||||
@@ -358,8 +355,8 @@ object AutoTradingManager {
|
|||||||
quantity = qty,
|
quantity = qty,
|
||||||
profitRate = effectiveProfitRate,
|
profitRate = effectiveProfitRate,
|
||||||
stopLossRate = sRate,
|
stopLossRate = sRate,
|
||||||
targetPrice = calculatedTarget,
|
targetPrice = unifiedTargetPrice, // 👈 통일된 목표가 주입
|
||||||
stopLossPrice = calculatedStop,
|
stopLossPrice = unifiedStopPrice, // 👈 통일된 손절가 주입
|
||||||
status = "PENDING_BUY",
|
status = "PENDING_BUY",
|
||||||
isDomestic = true
|
isDomestic = true
|
||||||
)
|
)
|
||||||
@@ -379,12 +376,10 @@ object AutoTradingManager {
|
|||||||
TradingLogStore.addLog(
|
TradingLogStore.addLog(
|
||||||
decision,
|
decision,
|
||||||
"BUY",
|
"BUY",
|
||||||
"[${investmentGrade.displayName}] ${orderStep}차 감시 설정 완료 (예상 목표가: $calculatedTarget): $realOrderNo"
|
"[${investmentGrade.displayName}] ${orderStep}차 감시 설정 완료 (예상 목표가: $unifiedTargetPrice): $realOrderNo"
|
||||||
)
|
)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
println("매수 실패: ${it.message} $stockCode $qty $price")
|
println("매수 실패: ${it.message} $stockCode $qty $price")
|
||||||
|
|
||||||
// 1차 주문에서 이미 예수금이 부족하다면 2차는 무의미하므로 재분석 대기열 처리
|
|
||||||
if (it.message?.contains("주문가능금액을 초과") == true && index == 0) {
|
if (it.message?.contains("주문가능금액을 초과") == true && index == 0) {
|
||||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = stockName))
|
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = stockName))
|
||||||
TradingLogStore.addWatchLog(decision, "WATCH", "${it.message ?: "매수 실패"} => 재분석 대기열에 추가")
|
TradingLogStore.addWatchLog(decision, "WATCH", "${it.message ?: "매수 실패"} => 재분석 대기열에 추가")
|
||||||
@@ -393,7 +388,6 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 [안전장치] KIS API 초당 호출 제한 방어
|
|
||||||
delay(250)
|
delay(250)
|
||||||
}
|
}
|
||||||
} else if (!hasCode && KisSession.isAvailBuyTime(LocalTime.now()) == false &&
|
} else if (!hasCode && KisSession.isAvailBuyTime(LocalTime.now()) == false &&
|
||||||
@@ -438,22 +432,17 @@ object AutoTradingManager {
|
|||||||
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
||||||
// ✅ 1. 진짜 사온 가격 (실제 매수 체결가)
|
// ✅ 1. 진짜 사온 가격 (실제 매수 체결가)
|
||||||
var actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
|
var actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
|
||||||
|
|
||||||
// 💡 [수정] 매수 주문(orderNo)에 대해 '진짜 산 가격'을 기록해야 합니다.
|
|
||||||
// 기존에는 여기에 finalTargetPrice를 넣으셨는데, 그러면 매수 단가가 오염됩니다.
|
|
||||||
TradingReportManager.updateExecution(orderNo, actualBuyPrice, dbItem.quantity)
|
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(
|
KisTradeService.postOrder(
|
||||||
stockCode = dbItem.code,
|
stockCode = dbItem.code,
|
||||||
@@ -468,12 +457,7 @@ object AutoTradingManager {
|
|||||||
stockName = dbItem.name,
|
stockName = dbItem.name,
|
||||||
isBuy = false,
|
isBuy = false,
|
||||||
orderQty = dbItem.quantity,
|
orderQty = dbItem.quantity,
|
||||||
reason = "🎯 목표 수익률 ${
|
reason = "🎯 목표 매도가 ${finalTargetPrice} 도달을 위한 익절 주문",
|
||||||
String.format(
|
|
||||||
"%.2f",
|
|
||||||
finalProfitRate
|
|
||||||
)
|
|
||||||
}% 도달을 위한 익절 주문",
|
|
||||||
holdingAvgPrice = actualBuyPrice, // 👈 여기서 매수단가를 넘겨줘야 매도 리포트가 정확해집니다!
|
holdingAvgPrice = actualBuyPrice, // 👈 여기서 매수단가를 넘겨줘야 매도 리포트가 정확해집니다!
|
||||||
decision = null
|
decision = null
|
||||||
)
|
)
|
||||||
@@ -1207,7 +1191,7 @@ object AutoTradingManager {
|
|||||||
.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) * 100
|
||||||
println("checkAndCancelPendingBuyOrders order 주문가 :$order | 현제가 :$currentPrice ${elapsedMillis / 1000L}초 ${priceGap}% 차이 || 괴리 기준 ${KisSession.tradeConfig.auto_cancel_pending_rate}%")
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user