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