...
This commit is contained in:
@@ -4,9 +4,11 @@ import TradingDecision
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import model.CandleData
|
||||
import model.RankingType
|
||||
import network.KisTradeService
|
||||
@@ -26,7 +28,8 @@ object AutoTradingManager {
|
||||
private var discoveryJob: Job? = null
|
||||
|
||||
|
||||
|
||||
val MIN = 0.1
|
||||
val MAX = 15.0
|
||||
fun startAutoDiscoveryLoop(
|
||||
tradeService: KisTradeService,
|
||||
callback: TradingDecisionCallback
|
||||
@@ -56,100 +59,113 @@ object AutoTradingManager {
|
||||
val riseList = riseRankDeferred.await()
|
||||
val amountList = amountRankDeferred.await()
|
||||
val volumeList = volumePowerDeferred.await()
|
||||
// (C) 거래대금 상위 종목 필터링 (시장의 주도주)
|
||||
val amountCandidates = amountList
|
||||
.filter { stock ->
|
||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
rate in 1.0..15.0 // 너무 과열되지 않은 주도주
|
||||
}
|
||||
|
||||
val volCandidates = volList
|
||||
.filter { stock ->
|
||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
rate in 1.0..18.0 // 0% 초과 20% 이하
|
||||
}
|
||||
|
||||
|
||||
// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개
|
||||
// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함
|
||||
val riseCandidates = riseList
|
||||
.filter { stock ->
|
||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
rate in 2.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
||||
}
|
||||
|
||||
val volumeCandidates = volumeList .filter { stock ->
|
||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
rate in 1.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
||||
}
|
||||
|
||||
|
||||
// 3. 리스트 합치기 (중복 제거)
|
||||
val candidates = (volCandidates + riseCandidates + amountCandidates + volumeCandidates).distinctBy { it.code }
|
||||
val candidates = (volList + riseList + amountList + volumeList + async { tradeService.fetchMarketRanking(RankingType.FOREIGNER_BUY, true).getOrDefault(emptyList()) + async { tradeService.fetchMarketRanking(RankingType.INSTITUTION_BUY, true).getOrDefault(emptyList()) }.await()}.await()).filter {stock ->
|
||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
rate in MIN..MAX // 너무 과열되지 않은 주도주
|
||||
}.distinctBy { it.code }
|
||||
|
||||
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
||||
|
||||
candidates.forEach { stock ->
|
||||
// [조건 1] 이미 보유한 종목 제외
|
||||
if (myHoldings.contains(stock.code)) return@forEach
|
||||
try {
|
||||
|
||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
|
||||
// [조건 2] 최소 1주 매수 가능 여부
|
||||
if (currentPrice > myCash || currentPrice > 5000) return@forEach
|
||||
callback(TradingDecision().apply {
|
||||
this.stockCode = stock.code
|
||||
this.confidence = -1.0
|
||||
this.stockName = stock.name
|
||||
}, false)
|
||||
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
|
||||
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
|
||||
val dailyData = dailyResult.getOrNull()
|
||||
val todayCandle = dailyData?.lastOrNull()
|
||||
// [조건 1] 이미 보유한 종목 제외
|
||||
if (myHoldings.contains(stock.code)) return@forEach
|
||||
|
||||
if (dailyData != null && todayCandle != null) {
|
||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
|
||||
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
// [조건 2] 최소 1주 매수 가능 여부
|
||||
if (currentPrice > myCash || currentPrice > 10000) {
|
||||
println("⏭️ [제외] ${stock.name}: 주가($currentPrice)가 예산 초과")
|
||||
return@forEach
|
||||
}
|
||||
callback(TradingDecision().apply {
|
||||
this.stockCode = stock.code
|
||||
this.confidence = -1.0
|
||||
this.stockName = stock.name
|
||||
}, false)
|
||||
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
|
||||
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
|
||||
val dailyData = dailyResult.getOrNull()
|
||||
val todayCandle = dailyData?.lastOrNull()
|
||||
|
||||
if (open > 0) {
|
||||
val riseRate = (current - open) / open * 100
|
||||
if (dailyData != null && todayCandle != null) {
|
||||
|
||||
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승
|
||||
if (riseRate > 0 && riseRate <= 20.0) {
|
||||
println("✨ [발굴] ${stock.name} (+${String.format("%.1f", riseRate)}%) -> 데이터 수집 및 분석")
|
||||
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
|
||||
// [핵심 수정] 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()) }
|
||||
if (open > 0) {
|
||||
val riseRate = (current - open) / open * 100
|
||||
|
||||
val min30Data = min30Def.await()
|
||||
val weeklyData = weekDef.await()
|
||||
val monthlyData = monthDef.await()
|
||||
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승
|
||||
if (riseRate > 0 && riseRate <= 20.0) {
|
||||
println(
|
||||
"✨ [발굴] ${stock.name} (+${
|
||||
String.format(
|
||||
"%.1f",
|
||||
riseRate
|
||||
)
|
||||
}%) -> 데이터 수집 및 분석"
|
||||
)
|
||||
|
||||
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
||||
val t = TechnicalAnalyzer()
|
||||
t.daily = dailyData
|
||||
t.weekly = weeklyData
|
||||
t.monthly = monthlyData
|
||||
t.min30 = min30Data
|
||||
|
||||
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
||||
RagService.processStock(t,stock.name, stock.code) { decision, isSuccess ->
|
||||
if (decision != null) {
|
||||
decision.stockName = stock.name
|
||||
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
|
||||
// [핵심 수정] 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())
|
||||
}
|
||||
callback(decision, isSuccess) // DashboardScreen으로 전달
|
||||
}
|
||||
|
||||
// 분석 후 잠시 대기 (서버 부하 조절)
|
||||
delay(2000)
|
||||
val min30Data = min30Def.await()
|
||||
val weeklyData = weekDef.await()
|
||||
val monthlyData = monthDef.await()
|
||||
|
||||
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
||||
val t = TechnicalAnalyzer()
|
||||
t.daily = dailyData
|
||||
t.weekly = weeklyData
|
||||
t.monthly = monthlyData
|
||||
t.min30 = min30Data
|
||||
|
||||
try {
|
||||
withTimeout(60000L) { // 60초 타임아웃 설정
|
||||
RagService.processStock(t, stock.name, stock.code) { decision, isSuccess ->
|
||||
if (decision != null) {
|
||||
decision.stockName = stock.name
|
||||
decision.currentPrice = current
|
||||
}
|
||||
callback(decision, isSuccess)
|
||||
}
|
||||
}
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
println("⏳ [Timeout] ${stock.name} AI 분석 시간 초과로 스킵합니다.")
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [Error] AI 분석 중 오류: ${e.message}")
|
||||
}
|
||||
|
||||
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
||||
// 분석 후 잠시 대기 (서버 부하 조절)
|
||||
delay(2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(300) // 종목 간 API 호출 간격
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [오류] ${stock.name} 분석 중 예외 발생: ${e.message}")
|
||||
}
|
||||
delay(300) // 종목 간 API 호출 간격
|
||||
}
|
||||
|
||||
|
||||
@@ -164,9 +180,10 @@ object AutoTradingManager {
|
||||
delay(tickMillis)
|
||||
currentWait += tickMillis
|
||||
val leftSec = (totalWaitMillis - currentWait) / 1000
|
||||
// 1분 단위 혹은 10초 단위로 자유롭게 로그 조절 가능
|
||||
if (leftSec % 30 == 0L || leftSec <= 30) {
|
||||
println("📡 [AutoTrading] 시스템 정상 작동 중... (다음 분석 ${leftSec}초 전)")
|
||||
if (leftSec % 60 == 0L) {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024
|
||||
println("📡 [System] 정상 작동 중... (남은 시간: ${leftSec}초 | 메모리 사용: ${usedMem}MB)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,19 +216,19 @@ object AutoTradingManager {
|
||||
println("🔥 [주문 집행] $code $type 완료")
|
||||
}
|
||||
}
|
||||
|
||||
data class InvestmentScores(
|
||||
val ultraShort: Int, // 초단기 (분봉/에너지)
|
||||
val shortTerm: Int, // 단기 (일봉/뉴스)
|
||||
val midTerm: Int, // 중기 (주봉/재무)
|
||||
val longTerm: Int // 장기 (월봉/펀더멘털)
|
||||
)
|
||||
class TechnicalAnalyzer {
|
||||
var monthly: List<CandleData> = emptyList()
|
||||
var weekly: List<CandleData> = emptyList()
|
||||
var daily: List<CandleData> = emptyList()
|
||||
var min30: List<CandleData> = emptyList()
|
||||
|
||||
data class InvestmentScores(
|
||||
val ultraShort: Int, // 초단기 (분봉/에너지)
|
||||
val shortTerm: Int, // 단기 (일봉/뉴스)
|
||||
val midTerm: Int, // 중기 (주봉/재무)
|
||||
val longTerm: Int // 장기 (월봉/펀더멘털)
|
||||
)
|
||||
|
||||
|
||||
fun calculateScores(
|
||||
financialScore: Int // 재무제표 점수 (성장률 등 기반)
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import model.NewsItem
|
||||
import network.CorpInfo
|
||||
import java.net.URL
|
||||
import kotlin.random.Random
|
||||
|
||||
object DynamicNewsScraper {
|
||||
@@ -91,20 +92,25 @@ object DynamicNewsScraper {
|
||||
|
||||
return page.evaluate(script) as String
|
||||
}
|
||||
|
||||
var failDomainList = arrayListOf<String>()
|
||||
suspend fun fetchFullContent(url: String): String {
|
||||
// browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다.
|
||||
val domain = URL(url).host
|
||||
val context = browser.newContext()
|
||||
if(failDomainList.contains(domain)) {
|
||||
println("실패한 도메인 스크래핑 종료 $domain ")
|
||||
return ""
|
||||
}
|
||||
return try {
|
||||
context.use { ctx ->
|
||||
ctx.newPage().use { page ->
|
||||
delay(Random.nextInt(1000).toLong())
|
||||
delay(Random.nextInt(2000).toLong())
|
||||
|
||||
// 1. 리스너 설정 시 예외 처리 강화
|
||||
blockUnnecessaryResources(page)
|
||||
|
||||
// 2. 타임아웃을 설정하여 무한 대기 방지
|
||||
val options = Page.NavigateOptions().setTimeout(30000.0)
|
||||
val options = Page.NavigateOptions().setTimeout(8000.0)
|
||||
page.navigate(url, options)
|
||||
|
||||
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
|
||||
@@ -119,6 +125,7 @@ object DynamicNewsScraper {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failDomainList.add(domain)
|
||||
println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}")
|
||||
""
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package service
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.slf4j.LoggerFactory
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.Logger
|
||||
|
||||
object SystemSleepPreventer {
|
||||
private var process: Process? = null
|
||||
@@ -9,6 +12,9 @@ object SystemSleepPreventer {
|
||||
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||
*/
|
||||
fun start() {
|
||||
val root = LoggerFactory.getLogger("Exposed") as Logger
|
||||
root.level = Level.ERROR
|
||||
|
||||
if (process?.isAlive == true) return
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user