....
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user