This commit is contained in:
2026-02-03 18:07:18 +09:00
parent d4926646f9
commit a51ffb6193
9 changed files with 251 additions and 61 deletions
+134 -20
View File
@@ -3,8 +3,13 @@ package service
import TradingDecision
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import model.CandleData
import model.RankingType
import network.KisTradeService
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
@@ -17,30 +22,139 @@ typealias TradingDecisionCallback = (TradingDecision?, Boolean)->Unit
object AutoTradingManager {
private val scope = CoroutineScope(Dispatchers.Default)
val targetStocks = mutableListOf<Pair<String, String>>()
// 자동 발굴 루프 제어용 Job
private var discoveryJob: Job? = null
fun addStock(stockName : String,stockCode : String, result :TradingDecisionCallback) {
targetStocks.add(Pair(stockName, stockCode))
startTradingLoop(stockName,stockCode,result)
fun startAutoDiscoveryLoop(
tradeService: KisTradeService,
callback: TradingDecisionCallback
) {
if (discoveryJob?.isActive == true) return
discoveryJob = scope.launch {
println("🚀 [AutoTrading] 5분 주기 자동 발굴 시작")
while (discoveryJob?.isActive == true) {
try {
// 1. [체크] 현재 잔고 및 보유 종목 조회
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
val myHoldings = balanceResult?.holdings?.map { it.code }?.toSet() ?: emptySet()
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
// 2. 랭킹 데이터 가져오기
// 1. 랭킹 데이터 가져오기 (비동기)
val volRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) }
val riseRankDeferred = async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) }
val volList = volRankDeferred.await()
val riseList = riseRankDeferred.await()
// [수정] 2. 의미 있는 후보군 선정 (단순 상위 15개가 아님)
// (A) 거래량 상위 종목 중: 현재가 기준 등락률이 0% ~ 20% 사이인 것만 필터링 -> 상위 10개
val volCandidates = volList
.filter { stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in 0.0..20.0 // 0% 초과 20% 이하
}
.take(10)
// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개
// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함
val riseCandidates = riseList
.filter { stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in 3.0..20.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
}
.take(10)
// 3. 두 리스트 합치기 (중복 제거)
val candidates = (volCandidates + riseCandidates).distinctBy { it.code }
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
candidates.forEach { stock ->
// [조건 1] 이미 보유한 종목 제외
if (myHoldings.contains(stock.code)) return@forEach
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
// [조건 2] 최소 1주 매수 가능 여부
if (currentPrice > myCash) return@forEach
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
val dailyData = dailyResult.getOrNull()
val todayCandle = dailyData?.lastOrNull()
if (dailyData != null && todayCandle != null) {
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
if (open > 0) {
val riseRate = (current - open) / open * 100
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승
if (riseRate > 0 && riseRate <= 20.0) {
println("✨ [발굴] ${stock.name} (+${String.format("%.1f", riseRate)}%) -> 데이터 수집 및 분석")
// [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입
// 비동기로 동시에 요청하여 속도 향상
val min30Def = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
val weekDef = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
val monthDef = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
val min30Data = min30Def.await()
val weeklyData = weekDef.await()
val monthlyData = monthDef.await()
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
TechnicalAnalyzer.clear()
TechnicalAnalyzer.daily = dailyData
TechnicalAnalyzer.weekly = weeklyData
TechnicalAnalyzer.monthly = monthlyData
TechnicalAnalyzer.min30 = min30Data
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
RagService.processStock(stock.name, stock.code) { decision, isSuccess ->
if (decision != null) {
decision.stockName = stock.name
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
}
callback(decision, isSuccess) // DashboardScreen으로 전달
}
// 분석 후 잠시 대기 (서버 부하 조절)
delay(2000)
}
}
}
delay(100) // 종목 간 API 호출 간격
}
println("💤 사이클 종료. 5분 대기...")
} catch (e: Exception) {
println("⚠️ 루프 오류: ${e.message}")
}
delay(5 * 60 * 1000) // 5분
}
}
}
fun startTradingLoop(stockName : String, stockCode : String, result :TradingDecisionCallback) {
// 루프 중단 함수
fun stopDiscovery() {
discoveryJob?.cancel()
discoveryJob = null
println("🛑 [AutoTrading] 자동 발굴 중단됨")
}
// 기존 단일 종목 추가 로직 (유지)
fun addStock(stockName: String, stockCode: String, result: TradingDecisionCallback) {
scope.launch {
println("🚀 10분 주기 자동 분석 및 매매 시작: ${LocalTime.now()}")
// targetStocks.forEach { stockCode ->
launch { // 종목별 병렬 분석 (M3 Pro 파워 활용)
RagService.processStock(stockName, stockCode,result)
// {decision,b ->
//// when (decision?.decision) {
//// "BUY" -> if (decision.confidence > 70) executeOrder(stockCode, "매수")
//// "SELL" -> executeOrder(stockCode, "매도")
//// else -> println("[$stockCode] 관망 유지: ${decision?.reason}")
//// }
// result(decision,b)
// }
}
// }
// targetStocks.re
// delay(10 * 60 * 1000) // 10분 대기
RagService.processStock(stockName, stockCode, result)
}
}