This commit is contained in:
lunaticbum 2026-05-18 17:56:26 +09:00
parent 4c27d71701
commit 9af9f46748
4 changed files with 209 additions and 59 deletions

View File

@ -252,6 +252,8 @@ class TradeConfig {
var useAutoRepost : Boolean = false var useAutoRepost : Boolean = false
var minusFilter : Double = 15.0 var minusFilter : Double = 15.0
var plusFilter : Double = 15.0 var plusFilter : Double = 15.0
var excuteCountOnMin : Int = 2
var autoSellOrder : Boolean = false
} }

View File

@ -331,7 +331,7 @@ object KisTradeService {
stockCode: String, stockCode: String,
qty: String, qty: String,
price: String, price: String,
isBuy: Boolean, isBuy: Boolean = false,
orderDivision: String = "", orderDivision: String = "",
marketCode : String = "KRX" marketCode : String = "KRX"
): Result<String> { ): Result<String> {

View File

@ -217,7 +217,7 @@ object AutoTradingManager {
TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단") TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단")
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","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else { } else if (KisSession.isAvailBuyTime(LocalTime.now())){
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice") println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true) KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
@ -271,6 +271,17 @@ object AutoTradingManager {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
} }
} }
} else {
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
)
}
}
} }
} }
} }
@ -528,8 +539,9 @@ object AutoTradingManager {
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) { && holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}") println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
val profit = holding.profitRate.toDouble() val profit = holding.profitRate.toDouble()
var targetPrice = holding.currentPrice.toDouble() var targetPrice = if (KisSession.tradeConfig.autoSellOrder ) holding.avgPrice.toDouble() else holding.currentPrice.toDouble()
targetPrice = MarketUtil.roundToTickSize(targetPrice + (MarketUtil.getTickSize(targetPrice) * 3.0)) targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0)
tradeService.postOrder( tradeService.postOrder(
stockCode = holding.code, stockCode = holding.code,
@ -764,15 +776,6 @@ object AutoTradingManager {
suspend fun checkBalance(isMorning: Boolean = true) { suspend fun checkBalance(isMorning: Boolean = true) {
if (isMorning) { if (isMorning) {
currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull() currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull()
// currentBalance?.let { currentBalance ->
// if (LocalTime.now().isBefore(LocalTime.of(18,1))) {
// TradingReportManager.recordAssetSnapshot(
// if (LocalTime.now().isAfter(LocalTime.of(18, 0))
// ) SnapshotType.END else SnapshotType.MIDDLE, currentBalance, ""
// )
// }
// }
if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) } if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) }
if (KisSession.tradeConfig.auto_cancel_pending_buy) { checkAndCancelPendingBuyOrders() } if (KisSession.tradeConfig.auto_cancel_pending_buy) { checkAndCancelPendingBuyOrders() }
} else { } else {
@ -791,14 +794,10 @@ object AutoTradingManager {
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 ) {
// 조건 A: 설정된 대기 시간 경과 여부
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) {
// 2. 현재가 조회 (가격을 비교하기 위해) // 2. 현재가 조회 (가격을 비교하기 위해)
val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno).getOrNull()?.stck_prpr?.toDouble() ?: 0.0 val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno).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 ${elapsedMillis / 1000L}${priceGap}% 차이")
@ -916,53 +915,62 @@ object AutoTradingManager {
} }
println("⏱️ [Cycle End] ${LocalTime.now()}") println("⏱️ [Cycle End] ${LocalTime.now()}")
} }
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장 // private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
private val executionCountMap = mutableMapOf<String, Int>()
suspend fun sellSchedule() { suspend fun sellSchedule() {
if (KisSession.config.take_profit == false) { if (KisSession.config.take_profit == false) return
val now = LocalTime.now()
val timeKey = String.format("%02d:%02d", now.hour, now.minute) // 예: "09:05"
val currentCount = executionCountMap.getOrDefault(timeKey, 0)
if (currentCount >= KisSession.tradeConfig.excuteCountOnMin) { return }
} else { var isExecuted = false
val now = LocalTime.now() val currentMinute = now.minute
val currentMinute = now.minute if (now.isBefore(LocalTime.of(8,50)) && now.isAfter(LocalTime.of(8,45))) {
if (now.hour == 8 && currentMinute < 50 && currentMinute > 45) { cancelAllPendingSellOrders()
if (lastForceCheckMinute != currentMinute) { isExecuted = true
cancelAllPendingSellOrders() } else if ( (now.isBefore(LocalTime.of(16,0)) && now.isAfter(KisSession.endBuyTime())) ) {
} val unfilledResult = KisTradeService.fetchUnfilledOrders()
} else if (now.hour == 8 && currentMinute > 55 && currentMinute > 50) { unfilledResult.onSuccess { response ->
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
} else if (now.hour == 9 && currentMinute % 2 == 1 TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 정규장 후 모든 매수 취소")
) { KisTradeService.cancelOrder(
if (lastForceCheckMinute != currentMinute) { order.ord_no, // 원주문번호
TradingLogStore.addAnalyzer( order.pdno
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.",
true
) )
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance()
lastForceCheckMinute = currentMinute // 실행 완료 기록
}
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) {
if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오후 ${now.hour}${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
true
)
var list = mutableListOf<String>("X")
if (now.hour != 8 && now.hour < 18) {
list.add("Y")
}
list.forEach { code ->
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
sellingAfterMarketOnePrice(KisTradeService, it, code)
}
}
lastForceCheckMinute = currentMinute // 실행 완료 기록
} }
} }
isExecuted = true
} else if (now.hour == 9) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.",
true
)
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance()
isExecuted = true
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오후 ${now.hour}${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
true
)
var list = mutableListOf<String>("X")
if (now.hour != 8 && now.hour < 18) {
list.add("Y")
}
list.forEach { code ->
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
sellingAfterMarketOnePrice(KisTradeService, it, code)
}
}
isExecuted = true
} }
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
if (now.hour >= 20) { executionCountMap.clear() }
} }

View File

@ -11238,5 +11238,145 @@
{ {
"code": "238490", "code": "238490",
"name": "힘스" "name": "힘스"
},
{
"code": "000980",
"name": "교보19호스팩"
},
{
"code": "001320",
"name": "교보20호스팩"
},
{
"code": "478340",
"name": "나라스페이스테크놀로지"
},
{
"code": "403850",
"name": "더핑크퐁컴퍼니"
},
{
"code": "000010",
"name": "덕양에너젠"
},
{
"code": "491000",
"name": "리브스메드"
},
{
"code": "394420",
"name": "리센스메디컬"
},
{
"code": "000930",
"name": "미래에셋비전스팩8호"
},
{
"code": "000960",
"name": "미래에셋비전스팩9호"
},
{
"code": "488900",
"name": "비츠로넥스텍"
},
{
"code": "001150",
"name": "삼성스팩13호"
},
{
"code": "000130",
"name": "삼진식품"
},
{
"code": "061090",
"name": "세나테크놀로지"
},
{
"code": "490470",
"name": "세미파이브"
},
{
"code": "001300",
"name": "신한제17호스팩"
},
{
"code": "388210",
"name": "씨엠티엑스"
},
{
"code": "493280",
"name": "아이엠바이오로직스"
},
{
"code": "476830",
"name": "알지노믹스"
},
{
"code": "459550",
"name": "알트"
},
{
"code": "000110",
"name": "액스비스"
},
{
"code": "458350",
"name": "에스팀"
},
{
"code": "000090",
"name": "에임드바이오"
},
{
"code": "001050",
"name": "유진스팩12호"
},
{
"code": "469610",
"name": "이노테크"
},
{
"code": "261520",
"name": "이지스"
},
{
"code": "493330",
"name": "지에프아이"
},
{
"code": "000820",
"name": "카나프테라퓨틱스"
},
{
"code": "439960",
"name": "코스모로보틱스"
},
{
"code": "464490",
"name": "쿼드메디슨"
},
{
"code": "494120",
"name": "큐리오시스"
},
{
"code": "466690",
"name": "키움히어로제1호스팩"
},
{
"code": "001310",
"name": "키움히어로제2호스팩"
},
{
"code": "487580",
"name": "폴레드"
},
{
"code": "001010",
"name": "하나36호스팩"
},
{
"code": "408470",
"name": "한패스"
} }
] ]