This commit is contained in:
2026-02-12 15:31:34 +09:00
parent da4ab01d5f
commit ad5e7e0ccb
8 changed files with 189 additions and 111 deletions
+41 -22
View File
@@ -42,7 +42,7 @@ object AutoTradingManager {
// 설정 상수
private const val MIN_RISE_RATE = 0.1
private const val MAX_RISE_RATE = 19.0
private const val MAX_RISE_RATE = 21.0
private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
private const val STUCK_THRESHOLD = 5 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
@@ -50,7 +50,8 @@ object AutoTradingManager {
fun isRunning(): Boolean = discoveryJob?.isActive == true
private var remainingCandidates = mutableListOf<RankingStock>()
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
private val reanalysisList = mutableListOf<RankingStock>()
private val retryCountMap = mutableMapOf<String, Int>()
/**
* 자동 발굴 루프 시작 및 Watchdog 실행
*/
@@ -88,7 +89,7 @@ object AutoTradingManager {
// [프로세스 1] 장 마감 및 잔고 체크
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
//&& now.isBefore(LocalTime.of(15, 30))
if (now.isAfter(LocalTime.of(15, 0)) ) {
if (now.isAfter(LocalTime.of(15, 30)) ) {
executeClosingLiquidation(tradeService)
return@withTimeout
}
@@ -99,25 +100,31 @@ object AutoTradingManager {
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
// [프로세스 2] 후보군 수집
if (remainingCandidates.isEmpty()) {
val candidates = fetchCandidates(tradeService).apply {
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
println("후보군 총 개수 : $size")
}
.filter { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) in MIN_RISE_RATE..MAX_RISE_RATE }
.filter { it.code !in myHoldings && it.code !in pendingStocks }
.distinctBy { it.code }
.filter { !it.name.contains("호스팩", true) }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.apply {
println("후보군 조건 충족 총 개수 : $size")
}
remainingCandidates.addAll(candidates)
.toMutableList()
if (reanalysisList.isNotEmpty()) {
candidates.addAll(reanalysisList)
}
remainingCandidates.addAll(candidates.distinctBy { it.code })
} else {
println("미확인 데이터 ${remainingCandidates.size}")
}
// [프로세스 3] 종목별 순회 분석
val iterator = remainingCandidates.iterator()
while (iterator.hasNext()) {
val stock = iterator.next()
// [프로세스 3] 종목별 순회 분석
var totalCount = remainingCandidates.size
println("후보군 조건 충족 총 개수 : ${totalCount}")
val iterator = remainingCandidates.iterator()
while (iterator.hasNext()) {
totalCount--
val stock = iterator.next()
try {
processSingleStock(stock, myCash, tradeService, callback)
// 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거
@@ -128,6 +135,7 @@ object AutoTradingManager {
} finally {
iterator.remove()
}
println("남은 후보군 개수 : ${totalCount}")
delay(300)
}
@@ -145,29 +153,40 @@ object AutoTradingManager {
}
}
fun addToReanalysis(stock: RankingStock) {
val count = retryCountMap.getOrDefault(stock.code, 0)
if (count < 2) { // 최대 2회까지만 재시도하여 무한 루프 방지
retryCountMap[stock.code] = count + 1
reanalysisList.add(stock)
println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
}
}
private suspend fun processSingleStock(stock: RankingStock, myCash: Long, tradeService: KisTradeService, callback: TradingDecisionCallback) {
try {
// 개별 종목 분석은 최대 2분으로 제한
withTimeout(120000L) {
val corpInfo = DartCodeManager.getCorpCode(stock.code)
if (corpInfo?.cName.isNullOrEmpty()) {
// println("⏭️ [제외] ${stock.name}: 법인명 정보를 찾을 수 없음")
return@withTimeout
}
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
val today = dailyData.lastOrNull() ?: return@withTimeout
val currentPrice = today.stck_prpr.toDouble()
if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) return@withTimeout
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
callback(TradingDecision().apply {
this.stockCode = stock.code
this.confidence = -1.0
this.stockName = stock.name
}, false)
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
val today = dailyData.lastOrNull() ?: return@withTimeout
val currentPrice = today.stck_prpr.toDouble()
if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) {
return@withTimeout
}
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
val analyzer = coroutineScope {
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }