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