분석후 분할 매수, 보유주식 분할 매도
This commit is contained in:
@@ -247,7 +247,7 @@ class TradeConfig {
|
||||
var ONE_STOCK_ALYSIS_TIME = 180000L
|
||||
var isLowPerformanceMonitoring: Boolean = false
|
||||
var useGradeShare : List<String> = listOf("LEVEL_4","LEVEL_5")
|
||||
var useTagsShare : List<String> = listOf("NOTICE", "WATCH")
|
||||
var useTagsShare : List<String> = listOf("NOTICE", "WATCH","매도 완료")
|
||||
var useLogKeywordsShare : List<String> = listOf("재분석")
|
||||
var useAutoRepost : Boolean = false
|
||||
var minusFilter : Double = 15.0
|
||||
|
||||
@@ -274,14 +274,15 @@ object AutoTradingManager {
|
||||
hasCode: Boolean
|
||||
) {
|
||||
scope.launch {
|
||||
var basePrice = decision.currentPrice
|
||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||
val oneTickLowerPrice =
|
||||
basePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide)
|
||||
.toInt())
|
||||
val initialBasePrice = decision.currentPrice
|
||||
val tickSize = MarketUtil.getTickSize(initialBasePrice)
|
||||
|
||||
// 시스템 가이드에 따른 1차 기준 단가 계산
|
||||
val oneTickLowerPrice = initialBasePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt())
|
||||
val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice)
|
||||
|
||||
var stockCode = decision.stockCode
|
||||
var stockName = decision.stockName
|
||||
val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice.toDouble())
|
||||
val maxStocks = KisSession.config.getValues(ConfigIndex.MAX_HOLDING_COUNT).toInt()
|
||||
|
||||
if (!canAddNewPosition(maxStocks)) {
|
||||
@@ -293,60 +294,68 @@ object AutoTradingManager {
|
||||
"매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가"
|
||||
)
|
||||
} else if (KisSession.isAvailBuyTime(LocalTime.now()) || hasCode) {
|
||||
println(
|
||||
"basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${
|
||||
stockCode.contains(
|
||||
stockCode
|
||||
)
|
||||
}"
|
||||
)
|
||||
var realOrderQty = orderQty
|
||||
KisTradeService.postOrder(
|
||||
stockCode,
|
||||
realOrderQty,
|
||||
finalPrice.toLong().toString(),
|
||||
isBuy = true
|
||||
)
|
||||
.onSuccess { realOrderNo ->
|
||||
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
|
||||
|
||||
val totalQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||
if (totalQty <= 0) return@launch
|
||||
|
||||
// 🌟 [핵심] 매수 2분할 로직 (1주는 1차에 전량, 그 이상은 절반씩)
|
||||
val qty1 = (totalQty + 1) / 2
|
||||
val qty2 = totalQty - qty1
|
||||
|
||||
val price1 = finalPrice
|
||||
// 2차 매수는 1차 매수가보다 3호가 아래에 대기 (더 싼 가격에 줍기)
|
||||
val price2 = MarketUtil.roundToTickSize(price1 - (tickSize * 3))
|
||||
|
||||
println("🔄 [분할 매수 진입] $stockName 1차(${qty1}주 / $price1) | 2차(${qty2}주 / $price2)")
|
||||
|
||||
// 분할 주문 리스트 생성
|
||||
val orderList = mutableListOf<Pair<Int, Double>>()
|
||||
if (qty1 > 0) orderList.add(qty1 to price1)
|
||||
if (qty2 > 0) orderList.add(qty2 to price2)
|
||||
|
||||
// 순차적으로 주문 전송
|
||||
orderList.forEachIndexed { index, (qty, price) ->
|
||||
val orderStep = index + 1 // 1차, 2차 표시용
|
||||
|
||||
KisTradeService.postOrder(
|
||||
stockCode,
|
||||
qty.toString(),
|
||||
price.toLong().toString(),
|
||||
isBuy = true
|
||||
).onSuccess { realOrderNo ->
|
||||
println("[${investmentGrade.displayName}] ${orderStep}차 주문 성공: $realOrderNo $stockCode ${qty}주 $price")
|
||||
TradingLogStore.addLog(
|
||||
decision,
|
||||
"BUY",
|
||||
"[${investmentGrade.displayName}] 주문 성공: $realOrderNo"
|
||||
"[${investmentGrade.displayName}] ${orderStep}차 분할 주문 성공: $realOrderNo"
|
||||
)
|
||||
|
||||
val sRate = -1.5
|
||||
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||
val effectiveProfitRate =
|
||||
(profitRate1
|
||||
?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
|
||||
val tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||
val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
|
||||
|
||||
var targetBasePrice = initialBasePrice
|
||||
try {
|
||||
var oldTarget = currentBalance?.getHoldings()
|
||||
?.first { it.availOrderCount.toInt() > 0 && it.code.equals(decision.stockCode) }
|
||||
val oldTarget = currentBalance?.getHoldings()?.firstOrNull { it.availOrderCount.toInt() > 0 && it.code == decision.stockCode }
|
||||
if (KisSession.tradeConfig.lowerAveragePrice && hasCode && oldTarget != null) {
|
||||
var avgPrive = oldTarget.avgPrice.toDouble()
|
||||
var qty = oldTarget.quantity.toDouble()
|
||||
basePrice =
|
||||
avgPrive * 1.5//((avgPrive * qty) + (decision.currentPrice * orderQty.toInt())).div(qty!!.toInt() + (orderQty.toInt()))
|
||||
println("물타기 ${avgPrive}, ${qty} ${basePrice}")
|
||||
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))
|
||||
|
||||
val calculatedTarget =
|
||||
MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
||||
val calculatedStop =
|
||||
MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
||||
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||
if (!hasCode) {
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = realOrderNo,
|
||||
code = stockCode,
|
||||
name = stockName,
|
||||
quantity = inputQty,
|
||||
quantity = qty,
|
||||
profitRate = effectiveProfitRate,
|
||||
stopLossRate = sRate,
|
||||
targetPrice = calculatedTarget,
|
||||
@@ -360,75 +369,48 @@ object AutoTradingManager {
|
||||
stockCode = stockCode,
|
||||
stockName = stockName,
|
||||
isBuy = true,
|
||||
orderQty = inputQty,
|
||||
reason = decision.reason ?: "", // AI 이유
|
||||
decision = decision // AI 객체 통째로 전달
|
||||
orderQty = qty,
|
||||
reason = "${decision.reason ?: ""} (${orderStep}차 분할 진입)",
|
||||
decision = decision
|
||||
)
|
||||
syncAndExecute(realOrderNo)
|
||||
}
|
||||
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
|
||||
|
||||
TradingLogStore.addLog(
|
||||
decision,
|
||||
"BUY",
|
||||
"[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${
|
||||
String.format(
|
||||
"%.4f",
|
||||
effectiveProfitRate
|
||||
)
|
||||
}%): $realOrderNo"
|
||||
"[${investmentGrade.displayName}] ${orderStep}차 감시 설정 완료 (예상 목표가: $calculatedTarget): $realOrderNo"
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
|
||||
}.onFailure {
|
||||
println("매수 실패: ${it.message} $stockCode $qty $price")
|
||||
|
||||
if (it.message?.contains("주문가능금액을 초과") == true) {
|
||||
AutoTradingManager.addToReanalysis(
|
||||
RankingStock(
|
||||
mksc_shrn_iscd = stockCode,
|
||||
hts_kor_isnm = stockName
|
||||
)
|
||||
)
|
||||
TradingLogStore.addWatchLog(
|
||||
decision,
|
||||
"WATCH",
|
||||
"${it.message ?: " 매수 실패"} => 재분석 대기열에 추가"
|
||||
)
|
||||
// 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 ?: "매수 실패"} => 재분석 대기열에 추가")
|
||||
} else {
|
||||
TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
|
||||
TradingLogStore.addLog(decision, "BUY", "${orderStep}차 매수 실패: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 🌟 [안전장치] KIS API 초당 호출 제한 방어
|
||||
delay(250)
|
||||
}
|
||||
} else if (!hasCode && KisSession.isAvailBuyTime(LocalTime.now()) == false &&
|
||||
(decision.investmentGrade?.displayName?.contains("4") == true || decision.investmentGrade?.displayName?.contains("5") == true)
|
||||
) {
|
||||
AutoTradingManager.addToReanalysis(
|
||||
RankingStock(
|
||||
mksc_shrn_iscd = stockCode,
|
||||
hts_kor_isnm = stockName
|
||||
)
|
||||
)
|
||||
TradingLogStore.addWatchLog(
|
||||
decision,
|
||||
"WATCH",
|
||||
"매수 시간 외 분석 => 재분석 대기열에 추가"
|
||||
)
|
||||
) {
|
||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = stockName))
|
||||
TradingLogStore.addWatchLog(decision, "WATCH", "매수 시간 외 분석 => 재분석 대기열에 추가")
|
||||
} else if (KisSession.isMarketOpenTime(LocalTime.now()) == false){
|
||||
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
||||
unfilledResult.onSuccess { response ->
|
||||
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
|
||||
TradingLogStore.addNotice(
|
||||
order.prdt_name,
|
||||
order.pdno,
|
||||
"[주문 취소] 매수시간 종료 후 모든 매수 취소"
|
||||
)
|
||||
KisTradeService.cancelOrder(
|
||||
order.ord_no, // 원주문번호
|
||||
order.pdno
|
||||
)
|
||||
TradingLogStore.addNotice(order.prdt_name, order.pdno, "[주문 취소] 매수시간 종료 후 모든 매수 취소")
|
||||
KisTradeService.cancelOrder(order.ord_no, order.pdno)
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,50 +565,105 @@ object AutoTradingManager {
|
||||
KisSession.config.SELL_PROFIT
|
||||
}
|
||||
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > targetProfitLimit) {
|
||||
var targetPrice = holding.currentPrice.toDouble()
|
||||
|
||||
val totalQty = holding.availOrderCount.toInt()
|
||||
val currentPrice = holding.currentPrice.toDouble()
|
||||
|
||||
// 🌟 [수정] 1주 남았을 때는 현재가(안전)로, 그 이상은 절반씩 분할
|
||||
val qty1 = (totalQty + 1) / 2 // 절반 (홀수면 현재가 비중을 높임)
|
||||
val qty2 = totalQty - qty1 // 나머지 (4호가 위)
|
||||
|
||||
val price1 = currentPrice
|
||||
val price2 = MarketUtil.roundToTickSize(currentPrice + (MarketUtil.getTickSize(currentPrice) * 4))
|
||||
|
||||
TradingLogStore.addAfterMarketLog(
|
||||
holding.name,
|
||||
holding.code,
|
||||
"${if ("Y".equals(marketCode)) "시간외 단일가" else "대체거래소"} 시세로 ${holding.profitRate} 수익 예상"
|
||||
"${if ("Y" == marketCode) "시간외 단일가" else "대체거래소"} 시세로 ${holding.profitRate} 수익 예상 (분할매도 가동)"
|
||||
)
|
||||
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = holding.availOrderCount,
|
||||
price = targetPrice.toInt().toString(),
|
||||
isBuy = false,
|
||||
orderDivision = if (marketCode.equals("Y")) "41" else "",
|
||||
marketCode = if (marketCode.equals("Y")) "KRX" else "NXT"
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [${if (marketCode.equals("Y")) "시간외 단일가" else "대체거래소"} 주문 완료] ${holding.name}: $newOrderNo")
|
||||
TradingLogStore.addSellLog(
|
||||
"${holding.name}[${holding.code}]",
|
||||
targetPrice.toString(),
|
||||
"SELL",
|
||||
"🎊 ${if (marketCode.equals("Y")) "시간외 단일가" else "대체거래소"} 주식 재고털이 주문 완료"
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = newOrderNo,
|
||||
code = holding.code,
|
||||
name = holding.name,
|
||||
quantity = holding.quantity.toInt(),
|
||||
profitRate = 0.0,
|
||||
stopLossRate = 0.0,
|
||||
targetPrice = targetPrice.toDouble(),
|
||||
stopLossPrice = 0.0,
|
||||
status = "SELLING",
|
||||
isDomestic = true
|
||||
// 1차 매도: 현재가
|
||||
if (qty1 > 0) {
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = qty1.toString(),
|
||||
price = price1.toInt().toString(),
|
||||
isBuy = false,
|
||||
orderDivision = if (marketCode == "Y") "41" else "",
|
||||
marketCode = if (marketCode == "Y") "KRX" else "NXT"
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [1차 분할주문 완료] ${holding.name}: $newOrderNo ($price1)")
|
||||
TradingLogStore.addSellLog(
|
||||
"${holding.name}[${holding.code}]",
|
||||
price1.toString(),
|
||||
"SELL",
|
||||
"🎊 ${if (marketCode == "Y") "시간외 단일가" else "대체거래소"} 1차 분할 매도(현재가) ${qty1}주 매도 완료"
|
||||
)
|
||||
)
|
||||
syncAndExecute(newOrderNo)
|
||||
}.onFailure {
|
||||
TradingLogStore.addSellLog(
|
||||
"${holding.name}[${holding.code}]",
|
||||
targetPrice.toString(),
|
||||
"SELL",
|
||||
"🎊 ${if (marketCode.equals("Y")) "시간외 단일가" else "대체거래소"} 주식 재고털이 주문 실패[${it.message}] "
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = newOrderNo,
|
||||
code = holding.code,
|
||||
name = holding.name,
|
||||
quantity = qty1,
|
||||
profitRate = 0.0,
|
||||
stopLossRate = 0.0,
|
||||
targetPrice = price1,
|
||||
stopLossPrice = 0.0,
|
||||
status = "SELLING",
|
||||
isDomestic = true
|
||||
)
|
||||
)
|
||||
syncAndExecute(newOrderNo)
|
||||
}.onFailure {
|
||||
TradingLogStore.addSellLog(
|
||||
"${holding.name}[${holding.code}]",
|
||||
price1.toString(),
|
||||
"SELL",
|
||||
"❌ 1차 분할매도 주문 실패[${it.message}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 2차 매도: 4호가 위
|
||||
if (qty2 > 0) {
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = qty2.toString(),
|
||||
price = price2.toInt().toString(),
|
||||
isBuy = false,
|
||||
orderDivision = if (marketCode == "Y") "41" else "",
|
||||
marketCode = if (marketCode == "Y") "KRX" else "NXT"
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [2차 분할주문 완료] ${holding.name}: $newOrderNo ($price2)")
|
||||
TradingLogStore.addSellLog(
|
||||
"${holding.name}[${holding.code}]",
|
||||
price2.toString(),
|
||||
"SELL",
|
||||
"🎊 ${if (marketCode == "Y") "시간외 단일가" else "대체거래소"} 2차 분할매도(4호가 위) ${qty2}주 매도 완료"
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = newOrderNo,
|
||||
code = holding.code,
|
||||
name = holding.name,
|
||||
quantity = qty2,
|
||||
profitRate = 0.0,
|
||||
stopLossRate = 0.0,
|
||||
targetPrice = price2,
|
||||
stopLossPrice = 0.0,
|
||||
status = "SELLING",
|
||||
isDomestic = true
|
||||
)
|
||||
)
|
||||
syncAndExecute(newOrderNo)
|
||||
}.onFailure {
|
||||
TradingLogStore.addSellLog(
|
||||
"${holding.name}[${holding.code}]",
|
||||
price2.toString(),
|
||||
"SELL",
|
||||
"❌ 2차 분할매도 주문 실패[${it.message}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ("Y".equals(marketCode)) {
|
||||
@@ -660,6 +697,39 @@ object AutoTradingManager {
|
||||
holding.code,
|
||||
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함."
|
||||
)
|
||||
}else {
|
||||
var errMsg = ""
|
||||
var isSuccess = false
|
||||
if (KisSession.tradeConfig.autoSellOrder
|
||||
&& holding != null && holding.quantity.toInt() > 0
|
||||
&& holding.availOrderCount.toInt() > 0
|
||||
&& holding.profitRate.toDouble() <= KisSession.tradeConfig.autoSellOrderMin
|
||||
&& holding.profitRate.toDouble() >= KisSession.tradeConfig.autoSellOrderMax
|
||||
&& holding.avgPrice.toDouble() > holding.currentPrice.toDouble()
|
||||
) {
|
||||
var targetPrice = holding.avgPrice.toDouble()
|
||||
targetPrice = MarketUtil.roundToTickSize(
|
||||
targetPrice + (MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend)
|
||||
)
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = holding.availOrderCount,
|
||||
price = targetPrice.toInt().toString(),
|
||||
isBuy = false,
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [보유 주식 손절 처리] ${holding.name} 매수가 기준 (${holding.avgPrice.toDouble()} 3호가 위[${targetPrice}] 매도 주문")
|
||||
isSuccess = true
|
||||
}.onFailure { err ->
|
||||
println("✅ [보유 주식 손절 처리] ${holding.name} 실패 ${targetPrice} ${err.message}")
|
||||
errMsg = err.message.toString()
|
||||
}
|
||||
|
||||
TradingLogStore.addNotice(
|
||||
"보유주식[${holding.name}]",
|
||||
holding.code,
|
||||
"매수가 기준 (${holding.avgPrice.toDouble()} ${KisSession.tradeConfig.autoSellOrderAppend}호가 위[${targetPrice}] 매도 주문 ${if (isSuccess) "성공" else "실패[${errMsg}]"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
analyzeDeepLossHoldingsAfterMarket(holding)
|
||||
}
|
||||
@@ -687,54 +757,97 @@ object AutoTradingManager {
|
||||
)
|
||||
} else {
|
||||
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) {
|
||||
var targetPrice = holding.currentPrice.toDouble()
|
||||
val now = LocalTime.now()
|
||||
val currentMinute = now.minute
|
||||
var isBefore930 = false
|
||||
if (now.hour == 9 && currentMinute < 30) {
|
||||
targetPrice = targetPrice
|
||||
isBefore930 = true
|
||||
} else {
|
||||
targetPrice = MarketUtil.roundToTickSize(
|
||||
targetPrice + MarketUtil.getTickSize(targetPrice)
|
||||
)
|
||||
}
|
||||
println("🔄 [보유 주식 주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = holding.availOrderCount,
|
||||
price = targetPrice.toInt().toString(),
|
||||
isBuy = false,
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [보유 주식 주문 완료] ${holding.name}: $newOrderNo")
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
targetPrice.toString(),
|
||||
"SELL",
|
||||
"🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료"
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = newOrderNo,
|
||||
code = holding.code,
|
||||
name = holding.name,
|
||||
quantity = holding.quantity.toInt(),
|
||||
profitRate = 0.0,
|
||||
stopLossRate = 0.0,
|
||||
targetPrice = targetPrice.toDouble(),
|
||||
stopLossPrice = 0.0,
|
||||
status = "SELLING",
|
||||
isDomestic = true
|
||||
|
||||
val totalQty = holding.availOrderCount.toInt()
|
||||
val currentPrice = holding.currentPrice.toDouble()
|
||||
|
||||
// 🌟 [수정] 1주 남았을 때는 현재가(안전)로, 그 이상은 절반씩 분할
|
||||
val qty1 = (totalQty + 1) / 2
|
||||
val qty2 = totalQty - qty1
|
||||
|
||||
val price1 = currentPrice
|
||||
val price2 = MarketUtil.roundToTickSize(currentPrice + (MarketUtil.getTickSize(currentPrice) * 4))
|
||||
|
||||
println("🔄 [보유 주식 주문] ${holding.name} (${holding.code}) 분할 매도: 현재가($price1) ${qty1}주, 4호가 위($price2) ${qty2}주")
|
||||
|
||||
// 1차 매도: 현재가
|
||||
if (qty1 > 0) {
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = qty1.toString(),
|
||||
price = price1.toInt().toString(),
|
||||
isBuy = false,
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [1차 분할주문 완료] ${holding.name}: $newOrderNo")
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
price1.toString(),
|
||||
"SELL",
|
||||
"🎊 보유 주식[수익률: ${holding.profitRate}] 1차 분할매도(현재가) ${qty1}주 완료"
|
||||
)
|
||||
)
|
||||
syncAndExecute(newOrderNo)
|
||||
}.onFailure {
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
targetPrice.toString(),
|
||||
"SELL",
|
||||
"🎊 보유 주식 매도 주문 실패[${it.message}] "
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = newOrderNo,
|
||||
code = holding.code,
|
||||
name = holding.name,
|
||||
quantity = qty1,
|
||||
profitRate = 0.0,
|
||||
stopLossRate = 0.0,
|
||||
targetPrice = price1,
|
||||
stopLossPrice = 0.0,
|
||||
status = "SELLING",
|
||||
isDomestic = true
|
||||
)
|
||||
)
|
||||
syncAndExecute(newOrderNo)
|
||||
}.onFailure {
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
price1.toString(),
|
||||
"SELL",
|
||||
"❌ 보유 주식 1차 매도 주문 실패[${it.message}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 2차 매도: 4호가 위
|
||||
if (qty2 > 0) {
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = qty2.toString(),
|
||||
price = price2.toInt().toString(),
|
||||
isBuy = false,
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [2차 분할주문 완료] ${holding.name}: $newOrderNo")
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
price2.toString(),
|
||||
"SELL",
|
||||
"🎊 보유 주식[수익률: ${holding.profitRate}] 2차 분할매도(4호가 위) ${qty2}주 완료"
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = newOrderNo,
|
||||
code = holding.code,
|
||||
name = holding.name,
|
||||
quantity = qty2,
|
||||
profitRate = 0.0,
|
||||
stopLossRate = 0.0,
|
||||
targetPrice = price2,
|
||||
stopLossPrice = 0.0,
|
||||
status = "SELLING",
|
||||
isDomestic = true
|
||||
)
|
||||
)
|
||||
syncAndExecute(newOrderNo)
|
||||
}.onFailure {
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
price2.toString(),
|
||||
"SELL",
|
||||
"❌ 보유 주식 2차 매도 주문 실패[${it.message}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var errMsg = ""
|
||||
@@ -1397,7 +1510,7 @@ object AutoTradingManager {
|
||||
|
||||
// 2. 재무 안정성 체크 (물타기라도 상폐/자본잠식 위험주는 추가 매수 금지)
|
||||
if (isSafetyBeltStockCodes.contains(stock.code)) {
|
||||
print("-> [${stock.name}] 재무 건전성 미달 제외 | ")
|
||||
println("-> [${stock.name}] 재무 건전성 미달 제외 | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
@@ -1445,7 +1558,7 @@ object AutoTradingManager {
|
||||
}
|
||||
|
||||
if (!isOk || !budgetCheck || currentPrice > maxPrice || currentPrice < minPrice) {
|
||||
print("-> [${stock.name}] 조건/가격 제외 (물타기여부:$isWatering) | ")
|
||||
println("-> [${stock.name}] 조건/가격 제외 (물타기여부:$isWatering) | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
@@ -1486,7 +1599,7 @@ object AutoTradingManager {
|
||||
|
||||
val currentAtr = tempAnalyzer.calculateATR(dailyData)
|
||||
if (!isProfitable || !isValidEntryTiming) {
|
||||
print("-> [${stock.name}] 목표 수익 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | 기대 수익율 : ${expectedProfitRate} || ${dailyStats.avgReboundAmplitude}")
|
||||
println("-> [${stock.name}] 목표 수익 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | 기대 수익율 : ${expectedProfitRate} || ${dailyStats.avgReboundAmplitude}")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
@@ -1497,7 +1610,7 @@ object AutoTradingManager {
|
||||
val allowedMargin = if (isWatering) 4.0 else 2.0 // 물타기는 하락 여력 마진을 4%까지 관대하게 인정
|
||||
|
||||
if (distanceToBottomPct > allowedMargin && !dropPrediction.isBottomZone) {
|
||||
print("-> [${stock.name}] 지하실 주의 (추가하락여력: ${"%.1f".format(distanceToBottomPct)}%) | ")
|
||||
println("-> [${stock.name}] 지하실 주의 (추가하락여력: ${"%.1f".format(distanceToBottomPct)}%) | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
@@ -1505,7 +1618,7 @@ object AutoTradingManager {
|
||||
if (dropPrediction.isBottomZone || isWatering) {
|
||||
val hasBrake = tempAnalyzer.checkBrakeAndReversal(dailyData)
|
||||
if (!hasBrake) {
|
||||
print("-> [${stock.name}] 지지/브레이크 미확인 (칼날 회피) | ")
|
||||
println("-> [${stock.name}] 지지/브레이크 미확인 (칼날 회피) | ")
|
||||
return@withTimeout
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user