This commit is contained in:
2026-02-10 15:08:52 +09:00
parent 4dff629861
commit 4bf055fa68
12 changed files with 302 additions and 399 deletions
+119 -24
View File
@@ -14,9 +14,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import model.CandleData
import model.MAX_PRICE
import model.MIN_PRICE
import model.RankingStock
import model.RankingType
import network.DartCodeManager
import network.FinancialMapper
import network.FinancialStatement
import network.KisTradeService
import java.time.LocalDateTime
import java.time.LocalTime
@@ -39,11 +43,13 @@ object AutoTradingManager {
// 설정 상수
private const val MIN_RISE_RATE = 0.1
private const val MAX_RISE_RATE = 15.0
private const val CYCLE_TIMEOUT = 10 * 60 * 1000L // 한 사이클 최대 10분
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'으로 판단
fun isRunning(): Boolean = discoveryJob?.isActive == true
private var remainingCandidates = mutableListOf<RankingStock>()
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
/**
* 자동 발굴 루프 시작 및 Watchdog 실행
@@ -82,36 +88,47 @@ object AutoTradingManager {
// [프로세스 1] 장 마감 및 잔고 체크
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
//&& now.isBefore(LocalTime.of(15, 30))
if (now.isAfter(LocalTime.of(15, 30)) ) {
executeClosingLiquidation(tradeService)
return@withTimeout
}
// if (now.isAfter(LocalTime.of(15, 30)) ) {
// executeClosingLiquidation(tradeService)
// return@withTimeout
// }
val balance = tradeService.fetchIntegratedBalance().getOrNull()
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] 후보군 수집
val candidates = fetchCandidates(tradeService).apply {
println("후보군 총 개수 : $size")
if (remainingCandidates.isEmpty()) {
val candidates = 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 }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.apply {
println("후보군 조건 충족 총 개수 : $size")
}
remainingCandidates.addAll(candidates)
} else {
println("미확인 데이터 ${remainingCandidates.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 }
.apply {
println("후보군 조건 충족 총 개수 : $size")
}
// [프로세스 3] 종목별 순회 분석
candidates.forEach { stock ->
try {
lastTickTime.set(System.currentTimeMillis()) // 종목별로도 생존 신고
processSingleStock(stock, myCash, tradeService, callback)
} catch (e: Exception) {
val iterator = remainingCandidates.iterator()
while (iterator.hasNext()) {
val stock = iterator.next()
}finally {
delay(300)
try {
processSingleStock(stock, myCash, tradeService, callback)
// 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거
} catch (e: Exception) {
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
// 오류 시 리스트에 남겨둘지, 제거할지 결정
// (심각한 에러면 remove하고 다음 루프에서 다시 받는게 안전)
} finally {
iterator.remove()
}
delay(300)
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
@@ -142,7 +159,7 @@ object AutoTradingManager {
val today = dailyData.lastOrNull() ?: return@withTimeout
val currentPrice = today.stck_prpr.toDouble()
if (currentPrice > myCash || currentPrice > 15000 || currentPrice < 900) return@withTimeout
if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) return@withTimeout
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
callback(TradingDecision().apply {
@@ -163,6 +180,7 @@ object AutoTradingManager {
}
}
RagService.processStock(analyzer, stock.name, stock.code) { decision, isSuccess ->
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
}
@@ -186,7 +204,7 @@ object AutoTradingManager {
// async { tradeService.fetchMarketRanking(RankingType.FALL2, true).getOrDefault(emptyList()) },
async { tradeService.fetchMarketRanking(RankingType.VALUE, true).getOrDefault(emptyList()) },
async { tradeService.fetchMarketRanking(RankingType.VOLUME_POWER, true).getOrDefault(emptyList()) },
async { tradeService.fetchMarketRanking(RankingType.NEW_HIGH, true).getOrDefault(emptyList()) },
// async { tradeService.fetchMarketRanking(RankingType.NEW_HIGH, true).getOrDefault(emptyList()) },
async { tradeService.fetchMarketRanking(RankingType.COMPANY_TRADE, true).getOrDefault(emptyList()) },
async { tradeService.fetchMarketRanking(RankingType.FINANCE, true).getOrDefault(emptyList()) },
async { tradeService.fetchMarketRanking(RankingType.MARKET_VALUE, true).getOrDefault(emptyList()) },
@@ -252,12 +270,89 @@ object AutoTradingManager {
}
}
object FinancialAnalyzer {
fun isSafetyBeltMet(fs: FinancialStatement): Boolean {
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
return isDebtSafe && isLiquiditySafe && isNotDeficit
}
/**
* [매수 고려] 우량 기업 요건 확인
* 모든 조건 충족 시 적극적인 분석(AI/차트) 단계로 진입합니다.
*/
fun isBuyConsiderationMet(fs: FinancialStatement): Boolean {
val highProfitability = fs.roe >= 10.0 // ROE 10% 이상
val strongGrowth = fs.netIncomeGrowth >= 15.0 // 이익 성장률 15% 이상
val verySafeDebt = fs.debtRatio <= 100.0 // 부채비율 100% 이하 (안전)
val goodLiquidity = fs.quickRatio >= 120.0 // 당좌비율 120% 이상 (여유)
val businessHealthy = fs.isOperatingProfitPositive // 본업(영업이익)이 흑자
return highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy
}
/**
* 종합 상태 반환 (UI 또는 로그용)
*/
fun getInvestmentStatus(fs: FinancialStatement): String {
return when {
isBuyConsiderationMet(fs) -> "🚀 [매수 검토 권장] 재무 건전성 및 성장성 우수"
isSafetyBeltMet(fs) -> "⚖️ [관망/보류] 생존 요건은 충족하나 성장성 부족"
else -> "🚨 [위험/제외] 재무 안정성 미달 또는 적자 기업"
}
}
fun calculateScore(fs: FinancialStatement): Int {
var score = 50.0 // 기본 점수
// 성장성 (영업이익 증가율)
score += when {
fs.operatingProfitGrowth > 20 -> 20
fs.operatingProfitGrowth > 0 -> 10
else -> -10 // 역성장 시 감점
}
// 수익성 (ROE)
score += when {
fs.roe > 15 -> 15
fs.roe > 5 -> 5
fs.roe < 0 -> -15 // 적자 시 큰 감점
else -> 0
}
// 안정성 (부채비율)
score += when {
fs.debtRatio < 100 -> 15
fs.debtRatio < 200 -> 5
else -> -10
}
// 유동성 (당좌비율)
if (fs.quickRatio < 100) score -= 10 // 단기 채무 지급 능력 부족 시 감점
return score.coerceIn(0.0, 100.0).toInt()
}
}
data class InvestmentScores(
val ultraShort: Int, // 초단기 (분봉/에너지)
val shortTerm: Int, // 단기 (일봉/뉴스)
val midTerm: Int, // 중기 (주봉/재무)
val longTerm: Int // 장기 (월봉/펀더멘털)
)
) {
override fun toString(): String {
return """
ultraShort $ultraShort
shortTerm $shortTerm
midTerm $midTerm
longTerm $longTerm
""".trimIndent()
}
}
class TechnicalAnalyzer {
var monthly: List<CandleData> = emptyList()
var weekly: List<CandleData> = emptyList()
@@ -1,40 +0,0 @@
//package service
//
//import kotlinx.coroutines.async
//import kotlinx.coroutines.coroutineScope
//import model.CandleData
//import model.RealTimeTrade
//import network.NewsService
//
//object StockAnalysisManager {
// var days : List<CandleData> = emptyList()
// var weeks : List<CandleData> = emptyList()
// var monthly : List<CandleData> = emptyList()
// var mins : List<CandleData> = emptyList()
//
// suspend fun analyzeStockWithMultiData(stockCode : String, stockName: String, result : (String)-> Unit) {
// coroutineScope {
// println("🔍 [1/3] '${stockName}' 실시간 뉴스 수집 및 학습 시작...")
//
// val corpInfoDeferred = async { NewsService.fetchCorpInfo(stockCode) }
// val financialDataDeferred = async { NewsService.fetchFinancialGrowth(stockCode) }
//
// val corpInfo = corpInfoDeferred.await()
// val financialData = financialDataDeferred.await()
//
// NewsService.fetchAndIngestNews("$stockName 주가 전망")
//
// println("🧠 [2/3] 관련 컨텍스트 추출 중...")
//
// // 2. 방금 저장된 뉴스를 포함하여 DB에서 관련성 높은 정보 추출
// val question = "$stockCode 종목의 현재 주가 흐름과 뉴스, 재무 실적을 바탕으로 종합 투자 전략을 세워줘."
// val context = RagService.askWithContext(question,corpInfo,financialData,days,weeks,monthly)
//
// println("🤖 [3/3] AI 분석 생성 중 (Chat 서버 8080)...")
//
// // 3. 최종 분석 결과 반환
// result.invoke(context)
// }
// }
//
//}