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