This commit is contained in:
2026-04-29 15:32:09 +09:00
parent a3a0338cc5
commit 2c736e687c
13 changed files with 857 additions and 104 deletions
+78 -23
View File
@@ -87,7 +87,7 @@ object AutoTradingManager {
val nowDate = LocalDate.now(seoulZone)
var checkTime = 60_000 * 3L
val isTradingDay = nowDate.dayOfWeek.value in 1..5
if (isTradingDay && now.isAfter(H07M50) && now.isBefore(H18) && !shouldShowFullWindow) {
if (isTradingDay && now.isAfter(KisSession.startTime()) && now.isBefore(KisSession.endTime()) && !shouldShowFullWindow) {
shouldShowFullWindow = true
SystemSleepPreventer.wakeDisplay()
} else if (now.isAfter(LocalTime.of(23, 50)) && now.isBefore(LocalTime.of(8, 0))) {
@@ -104,7 +104,7 @@ object AutoTradingManager {
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
val seoulZone = ZoneId.of("Asia/Seoul")
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
if (now.isBefore(H15M30) && now.isAfter(H08M45) && isSuccess && completeTradingDecision != null) {
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
val decision = completeTradingDecision
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
@@ -520,7 +520,7 @@ object AutoTradingManager {
)
}
} else {
if (KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0
if (KisSession.config.stop_Loss
&& holding != null && holding.quantity.toInt() > 0
&& holding.availOrderCount.toInt() > 0
&& holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)
@@ -528,10 +528,19 @@ object AutoTradingManager {
&& 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)}")
val profit = holding.profitRate.toDouble()
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
price = "0",
isBuy = false,
).onSuccess { newOrderNo ->
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
}.onFailure {
}
TradingLogStore.addNotice(
"보유주식[${holding.name}]",
holding.code,
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함."
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
)
}
analyzeDeepLossHoldingsAfterMarket(holding , true)
@@ -545,13 +554,12 @@ object AutoTradingManager {
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
val now = LocalTime.now()
val currentMinute = now.minute
if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute == 0 ))) {
if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0))) {
val profit = holding.profitRate.toDouble()
val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다)
if (profit <= lossThreshold) {
println("🔍 [손실 종목 분석] ${holding.name} (수익률: $profit%) - 가이드 산출 중...")
// 차트 데이터 빠르게 가져오기 (일봉 위주로 큰 추세만 확인)
val dailyData = KisTradeService.fetchPeriodChartData(holding.code, "D", true).getOrNull()
if (!dailyData.isNullOrEmpty()) {
@@ -583,6 +591,7 @@ object AutoTradingManager {
"보유주식[${holding.name}]",
holding.code,
"수익률 심각($profit%) -> $advice",
holding.quantity.toInt()
)
}
// 🟡 [관망] 어정쩡하게 물려있는 상태
@@ -594,9 +603,6 @@ object AutoTradingManager {
"수익률($profit%) -> $advice", false
)
}
// 분석 결과를 UI 로그에 띄워 대표님이 확인할 수 있게 함
}
} else {
// -5% 이내의 자잘한 손실은 별도 분석 없이 조용히 넘기거나 약식 로그만 남김
@@ -658,12 +664,8 @@ object AutoTradingManager {
var currentTimeMillis = System.currentTimeMillis()
var waitTime = 0.2
val H15M30 = LocalTime.of(15, 30)
val H16 = LocalTime.of(16, 0)
val H18 = LocalTime.of(18, 0)
val H20 = LocalTime.of(20, 0)
val H08M00 = LocalTime.of(8, 0)
val H08M45 = LocalTime.of(8, 45)
val H07M50 = LocalTime.of(7, 50)
private fun runDiscoveryLoop(callback: TradingDecisionCallback) {
discoveryJob = scope.launch {
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
@@ -673,10 +675,10 @@ object AutoTradingManager {
currentTimeMillis = System.currentTimeMillis()
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
when {
now.isAfter(H20) || now.isBefore(H07M50) -> {
now.isAfter(KisSession.endTime()) || now.isBefore(KisSession.startTime()) -> {
prepareMarketOpen(now)
}
now.isBefore(H20) && now.isAfter(H08M00) -> {
now.isBefore(KisSession.endTime()) && now.isAfter(KisSession.startTime()) -> {
waitTime = 0.2
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
@@ -690,7 +692,7 @@ object AutoTradingManager {
}
withTimeout(CYCLE_TIMEOUT) {
println("⏱️ [Cycle Start] ${LocalTime.now()}")
if (now.isAfter(H20)) {
if (now.isAfter(KisSession.endTime())) {
executeClosingLiquidation(KisTradeService)
} else {
executeMarketLoop()
@@ -713,7 +715,7 @@ object AutoTradingManager {
}
suspend fun prepareMarketOpen(now : LocalTime) {
if (now.isAfter(H20) || now.isBefore(H07M50)) {
if (now.isAfter(KisSession.endTime()) || now.isBefore(KisSession.startTime())) {
println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.")
onMarketClosed?.invoke()
RagService.clearDailyCache()
@@ -724,7 +726,7 @@ object AutoTradingManager {
isSystemReadyToday = false
shouldShowFullWindow = false
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
} else if (now.isAfter(H07M50) && now.isBefore(H08M00) && !isSystemReadyToday) {
} else if (now.isAfter(KisSession.startTime().minusMinutes(10)) && now.isBefore(KisSession.startTime()) && !isSystemReadyToday) {
if (MarketUtil.canTradeToday()) {
SystemSleepPreventer.wakeDisplay()
shouldShowFullWindow = true
@@ -756,19 +758,72 @@ object AutoTradingManager {
if (isMorning) {
currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull()
currentBalance?.let { currentBalance ->
if (LocalTime.now().isBefore(LocalTime.of(16,2))) {
if (LocalTime.now().isBefore(LocalTime.of(18,1))) {
TradingReportManager.recordAssetSnapshot(
if (LocalTime.now().isAfter(LocalTime.of(17, 59))
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.tradeConfig.auto_cancel_pending_buy) {
checkAndCancelPendingBuyOrders()
}
} else {
}
}
suspend fun checkAndCancelPendingBuyOrders(
) {
// 1. 미체결 내역 조회
val unfilledResult = KisTradeService.fetchUnfilledOrders()
unfilledResult.onSuccess { response ->
val currentTime = System.currentTimeMillis()
// 매수 주문('02')만 필터링
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
val orderTimeMillis = parseOrderTime(order.ord_tmd)
val elapsedMillis = currentTime - orderTimeMillis
// 조건 A: 설정된 대기 시간 경과 여부
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) {
// 2. 현재가 조회 (가격을 비교하기 위해)
val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno).getOrNull()?.stck_prpr?.toDouble() ?: 0.0
val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0
// 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때)
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice
println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}${priceGap}% 차이")
if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap * 100)}%)로 취소 시도")
KisTradeService.cancelOrder(
order.ord_no, // 원주문번호
order.pdno
)
}
}
}
}
}
// 주문 시간 문자열을 Millis로 변환하는 유틸리티 (당일 주문 기준)
fun parseOrderTime(ordTmd: String): Long {
return try {
val now = java.time.LocalDateTime.now()
val hour = ordTmd.substring(0, 2).toInt()
val min = ordTmd.substring(2, 4).toInt()
val sec = ordTmd.substring(4, 6).toInt()
val orderDateTime = now.withHour(hour).withMinute(min).withSecond(sec)
orderDateTime.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()
} catch (e: Exception) {
System.currentTimeMillis()
}
}
suspend fun executeMarketLoop() {
myOredsAndBalanceCodes.clear()
checkBalance()
@@ -822,7 +877,7 @@ object AutoTradingManager {
while (iterator.hasNext()) {
totalCount--
val stock = iterator.next()
if (now.isBefore(H15M30) && now.isAfter(H08M45)) {
if (KisSession.isAvailBuyTime(now)) {
if (BLACKLISTEDSTOCKCODES.contains(stock.code)) {
println("❌ 차단 처리된 주식 : ${stock.name}")
} else {
@@ -860,7 +915,7 @@ object AutoTradingManager {
checkBalance()
lastForceCheckMinute = currentMinute // 실행 완료 기록
}
} else if ((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) {
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) {
if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(
" - ",