This commit is contained in:
2026-03-13 10:41:10 +09:00
parent eb2dc0efd0
commit 6d340b7dc0
8 changed files with 307 additions and 77 deletions
+179 -64
View File
@@ -1,6 +1,7 @@
package service
import TradingDecision
import getLlamaBinPath
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -23,7 +24,9 @@ import model.UnifiedBalance
import network.DartCodeManager
import network.FinancialMapper
import network.FinancialStatement
import network.KisAuthService
import network.KisTradeService
import network.KisWebSocketManager
import util.MarketUtil
import java.time.LocalDateTime
import java.time.LocalTime
@@ -116,83 +119,196 @@ object AutoTradingManager {
delay(200) // API 호출 부하 방지
}
}
var isSystemReadyToday = false
var isSystemCleanedUpToday = false
private var lastRetryTime = 0L
val binPath = getLlamaBinPath()
suspend fun tryRefreshToken() {
try {
// 2분 간격 재시도 로직 (처음 실행 시에는 lastRetryTime이 0이므로 즉시 실행)
if (currentTimeMillis - lastRetryTime >= 2 * 60 * 1000L) {
lastRetryTime = currentTimeMillis
println("🌅 [System] 오전 8시 업무 시작 준비 시도...")
SystemSleepPreventer.wakeDisplay() // 모니터 깨우기
val authSuccess = KisAuthService.refreshAllTokens()
val wsSuccess = KisTradeService.refreshWebsocketKey()
if (authSuccess && wsSuccess) {
println("✅ [System] 토큰 갱신 성공. AI 서버를 기동합니다.")
// 서버 시작 로직 실행 (Main.kt에 있던 로직 활용)
val config = KisSession.config
// LLM 서버 시작 (설정된 모델 경로 사용)
if (config.modelPath.isNotEmpty()) {
LlamaServerManager.startServer(binPath, config.modelPath,port = 8080)
}
if (config.embedModelPath.isNotEmpty()) {
LlamaServerManager.startServer(binPath, config.embedModelPath, port = 8081)
}
KisWebSocketManager.connect()
isSystemReadyToday = true
} else {
println("❌ [System] 토큰 갱신 실패. 2분 후 재시도합니다.")
}
}
} catch (e: Exception) {}
}
var now = LocalTime.now(ZoneId.of("Asia/Seoul"))
var currentTimeMillis = System.currentTimeMillis()
var waitTime = 0.2
private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) {
discoveryJob = scope.launch {
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
while (isActive) {
try {
now = LocalTime.now(ZoneId.of("Asia/Seoul"))
currentTimeMillis = System.currentTimeMillis()
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
withTimeout(CYCLE_TIMEOUT) {
println("⏱️ [Cycle Start] ${LocalTime.now()}")
// [프로세스 1] 장 마감 및 잔고 체크
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
//&& now.isBefore(LocalTime.of(15, 30))
if (now.isAfter(LocalTime.of(15, 20)) ) {
executeClosingLiquidation(tradeService)
return@withTimeout
}
// addToReanalysis(RankingStock(mksc_shrn_iscd = ,hts_kor_isnm = ))
val balance = tradeService.fetchIntegratedBalance().getOrNull()
balance?.let { resumePendingSellOrders(tradeService,it) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
// [프로세스 2] 후보군 수집
if (remainingCandidates.isEmpty()) {
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
println("후보군 총 개수 : $size")
}.filter {
val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
// if (isOk) {println("${it.name} : ${it.prdy_ctrt}")}
if (corpInfo?.cName.isNullOrEmpty()) {
false
}else {
isOk
// if (now.minute % 5 == 0) {
// SystemSleepPreventer.sleepDisplay()
// } else {
// SystemSleepPreventer.wakeDisplay()
// }
when {
//장중
now.isBefore(LocalTime.of(16, 0)) && now.isAfter(LocalTime.of(8, 50)) -> {
waitTime = 0.2
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
// 토큰 중 하나라도 만료 5분 전이거나 비어있다면 다시 준비 상태로 전환
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
if (isSystemReadyToday) {
println("⚠️ [System] 토큰 만료 감지. 재발급 프로세스를 가동합니다.")
isSystemReadyToday = false
KisWebSocketManager.disconnect()
tryRefreshToken()
}
}
}
.filter { !it.name.contains("호스팩", true) }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.toMutableList()
withTimeout(CYCLE_TIMEOUT) {
println("⏱️ [Cycle Start] ${LocalTime.now()}")
if (reanalysisList.isNotEmpty()) {
candidates.addAll(reanalysisList.asReversed())
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
if (now.isAfter(LocalTime.of(15, 20))) {
executeClosingLiquidation(tradeService)
return@withTimeout
}
val balance = tradeService.fetchIntegratedBalance().getOrNull()
balance?.let { resumePendingSellOrders(tradeService, it) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
val myHoldings =
balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet()
?: emptySet()
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
// [프로세스 2] 후보군 수집
if (remainingCandidates.isEmpty()) {
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
println("후보군 총 개수 : $size")
}.filter {
val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
if (corpInfo?.cName.isNullOrEmpty()) {
false
} else {
isOk
}
}
.filter { !it.name.contains("호스팩", true) }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.toMutableList()
if (reanalysisList.isNotEmpty()) {
candidates.addAll(reanalysisList.asReversed())
}
reanalysisList.clear()
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }
.distinctBy { it.code })
} else {
println("미확인 데이터 ${remainingCandidates.size}")
}
// [프로세스 3] 종목별 순회 분석
var totalCount = remainingCandidates.size
println("후보군 조건 충족 총 개수 : ${totalCount}")
val iterator = remainingCandidates.iterator()
while (iterator.hasNext()) {
if (now.minute % 2 == 0) {
// SystemSleepPreventer.sleepDisplay()
} else {
// SystemSleepPreventer.wakeDisplay()
}
totalCount--
val stock = iterator.next()
try {
processSingleStock(stock, myCash, tradeService, callback)
} catch (e: Exception) {
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
} finally {
iterator.remove()
}
println("남은 후보군 개수 : ${totalCount}")
delay(100)
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
}
reanalysisList.clear()
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }.distinctBy { it.code })
} else {
println("미확인 데이터 ${remainingCandidates.size}")
}
//장외
now.isAfter(LocalTime.of(18, 0)) || now.isBefore(LocalTime.of(8, 50)) -> {
when {
(now.hour == 0 && now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> {
waitTime = 10.0
isSystemReadyToday = false
isSystemCleanedUpToday = false
}
// [프로세스 3] 종목별 순회 분석
var totalCount = remainingCandidates.size
println("후보군 조건 충족 총 개수 : ${totalCount}")
val iterator = remainingCandidates.iterator()
(now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> {
waitTime = 3.0
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
KisWebSocketManager.disconnect()
tryRefreshToken()
}
}
while (iterator.hasNext()) {
totalCount--
val stock = iterator.next()
try {
processSingleStock(stock, myCash, tradeService, callback)
// 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거
} catch (e: Exception) {
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
// 오류 시 리스트에 남겨둘지, 제거할지 결정
// (심각한 에러면 remove하고 다음 루프에서 다시 받는게 안전)
} finally {
iterator.remove()
(now.isAfter(LocalTime.of(18, 0))) -> {
try {
waitTime = 5.0
println("current SystemCleanedUpToday is $isSystemCleanedUpToday")
if (!isSystemCleanedUpToday) {
println("🌙 [System] 업무 종료 및 자원 정리 시작...")
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
KisWebSocketManager.disconnect()
//isSystemReadyToday = false
if (LlamaServerManager.stopAll()) {
isSystemCleanedUpToday = true
}
}
println("✅ [System] 오늘의 모든 정리가 완료되었습니다.")
} catch (e: Exception) {
}
}
(now.isAfter(LocalTime.of(18, 15)) && now.minute % 15 == 0) -> {
try {
waitTime = 5.0
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
} catch (e: Exception) {
}
}
else -> {
waitTime = 5.0
}
}
println("남은 후보군 개수 : ${totalCount}")
delay(100)
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
else ->{
waitTime = 3.0
}
}
} catch (e: TimeoutCancellationException) {
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
@@ -200,8 +316,7 @@ object AutoTradingManager {
println("⚠️ [Loop Error] ${e.message}")
delay(1500)
}
waitForNextCycle(0.2)
waitForNextCycle(waitTime)
}
}
}
@@ -302,12 +417,12 @@ object AutoTradingManager {
}
private suspend fun waitForNextCycle(minutes: Double) {
println("💤 대기 모드 진입...")
println("💤 대기 모드 진입... $minutes")
val endWait = System.currentTimeMillis() + (minutes * 60 * 1000L)
while (System.currentTimeMillis() < endWait && isRunning()) {
lastTickTime.set(System.currentTimeMillis()) // 대기 중에도 Watchdog에 생존 신고
println("💤 대기 모드 상태 확인...")
delay(1000)
delay(if(minutes > 3.0 ) 10000 else 1000)
}
}