...
This commit is contained in:
@@ -40,7 +40,7 @@ object AutoTradingManager {
|
||||
try {
|
||||
// 1. [체크] 현재 잔고 및 보유 종목 조회
|
||||
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||
val myHoldings = balanceResult?.holdings?.map { it.code }?.toSet() ?: emptySet()
|
||||
val myHoldings = balanceResult?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
||||
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||
|
||||
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
|
||||
@@ -49,31 +49,42 @@ object AutoTradingManager {
|
||||
// 1. 랭킹 데이터 가져오기 (비동기)
|
||||
val volRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) }
|
||||
val riseRankDeferred = async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) }
|
||||
|
||||
// 거래대금(Amount) 상위 추가
|
||||
val amountRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VALUE, true).getOrDefault(emptyList()) }
|
||||
val volumePowerDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME_POWER, true).getOrDefault(emptyList()) }
|
||||
val volList = volRankDeferred.await()
|
||||
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 // 너무 과열되지 않은 주도주
|
||||
}
|
||||
|
||||
// [수정] 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% 이하
|
||||
rate in 1.0..18.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% 이하는 안전 구간
|
||||
rate in 2.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
||||
}
|
||||
.take(10)
|
||||
|
||||
// 3. 두 리스트 합치기 (중복 제거)
|
||||
val candidates = (volCandidates + riseCandidates).distinctBy { it.code }
|
||||
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 }
|
||||
|
||||
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
||||
|
||||
@@ -84,14 +95,19 @@ object AutoTradingManager {
|
||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
|
||||
// [조건 2] 최소 1주 매수 가능 여부
|
||||
if (currentPrice > myCash) return@forEach
|
||||
|
||||
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()
|
||||
|
||||
if (dailyData != null && todayCandle != null) {
|
||||
|
||||
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
|
||||
@@ -113,14 +129,14 @@ object AutoTradingManager {
|
||||
val monthlyData = monthDef.await()
|
||||
|
||||
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
||||
TechnicalAnalyzer.clear()
|
||||
TechnicalAnalyzer.daily = dailyData
|
||||
TechnicalAnalyzer.weekly = weeklyData
|
||||
TechnicalAnalyzer.monthly = monthlyData
|
||||
TechnicalAnalyzer.min30 = min30Data
|
||||
val t = TechnicalAnalyzer()
|
||||
t.daily = dailyData
|
||||
t.weekly = weeklyData
|
||||
t.monthly = monthlyData
|
||||
t.min30 = min30Data
|
||||
|
||||
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
||||
RagService.processStock(stock.name, stock.code) { decision, isSuccess ->
|
||||
RagService.processStock(t,stock.name, stock.code) { decision, isSuccess ->
|
||||
if (decision != null) {
|
||||
decision.stockName = stock.name
|
||||
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
|
||||
@@ -133,13 +149,31 @@ object AutoTradingManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(100) // 종목 간 API 호출 간격
|
||||
delay(300) // 종목 간 API 호출 간격
|
||||
}
|
||||
println("💤 사이클 종료. 5분 대기...")
|
||||
|
||||
|
||||
// --- 10초 주기 로그 대기 로직 시작 ---
|
||||
val waitMinutes = 3
|
||||
val totalWaitMillis = waitMinutes * 60 * 1000L
|
||||
val tickMillis = 10 * 1000L
|
||||
var currentWait = 0L
|
||||
println("💤 사이클 종료. ${waitMinutes}분 대기...")
|
||||
println("✅ 이번 사이클 분석 완료.")
|
||||
while (currentWait < totalWaitMillis && discoveryJob?.isActive == true) {
|
||||
delay(tickMillis)
|
||||
currentWait += tickMillis
|
||||
val leftSec = (totalWaitMillis - currentWait) / 1000
|
||||
// 1분 단위 혹은 10초 단위로 자유롭게 로그 조절 가능
|
||||
if (leftSec % 30 == 0L || leftSec <= 30) {
|
||||
println("📡 [AutoTrading] 시스템 정상 작동 중... (다음 분석 ${leftSec}초 전)")
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ 루프 오류: ${e.message}")
|
||||
delay(10000) // 오류 발생 시 10초 후 재시도
|
||||
}
|
||||
delay(5 * 60 * 1000) // 5분
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,9 +186,9 @@ object AutoTradingManager {
|
||||
}
|
||||
|
||||
// 기존 단일 종목 추가 로직 (유지)
|
||||
fun addStock(stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
||||
fun addStock(technicalAnalyzer : TechnicalAnalyzer,stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
||||
scope.launch {
|
||||
RagService.processStock(stockName, stockCode, result)
|
||||
RagService.processStock(technicalAnalyzer,stockName, stockCode, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +200,7 @@ object AutoTradingManager {
|
||||
}
|
||||
}
|
||||
|
||||
object TechnicalAnalyzer {
|
||||
class TechnicalAnalyzer {
|
||||
var monthly: List<CandleData> = emptyList()
|
||||
var weekly: List<CandleData> = emptyList()
|
||||
var daily: List<CandleData> = emptyList()
|
||||
@@ -184,21 +218,21 @@ object TechnicalAnalyzer {
|
||||
): InvestmentScores {
|
||||
|
||||
// 1. 초단기 (분봉 + 에너지 지표 위주)
|
||||
val ultra = (TechnicalAnalyzer.calculateMFI(min30, 14) * 0.4 +
|
||||
TechnicalAnalyzer.calculateStochastic(min30) * 0.3 +
|
||||
(if(TechnicalAnalyzer.calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt()
|
||||
val ultra = (calculateMFI(min30, 14) * 0.4 +
|
||||
calculateStochastic(min30) * 0.3 +
|
||||
(if(calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt()
|
||||
|
||||
// 2. 단기 (일봉 추세 + OBV 에너지)
|
||||
val short = (TechnicalAnalyzer.calculateRSI(daily) * 0.3 +
|
||||
(if(TechnicalAnalyzer.calculateOBV(daily) > 0) 40 else 10) +
|
||||
(if(TechnicalAnalyzer.calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt()
|
||||
val short = (calculateRSI(daily) * 0.3 +
|
||||
(if(calculateOBV(daily) > 0) 40 else 10) +
|
||||
(if(calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt()
|
||||
|
||||
// 3. 중기 (주봉 + 재무 점수 혼합)
|
||||
val mid = (if(TechnicalAnalyzer.calculateChange(weekly) > 0) 40 else 10) +
|
||||
val mid = (if(calculateChange(weekly) > 0) 40 else 10) +
|
||||
(financialScore * 0.6).toInt()
|
||||
|
||||
// 4. 장기 (월봉 + 섹터/기업 펀더멘털)
|
||||
val long = (if(TechnicalAnalyzer.calculateChange(monthly) > 0) 50 else 0) +
|
||||
val long = (if(calculateChange(monthly) > 0) 50 else 0) +
|
||||
(financialScore * 0.5).toInt()
|
||||
|
||||
return InvestmentScores(
|
||||
@@ -390,8 +424,8 @@ class ScalpingAnalyzer {
|
||||
private const val BB_LOWER_POS = 0.2
|
||||
private const val BB_UPPER_POS = 0.8
|
||||
private const val ATR_WINDOW = 14
|
||||
private const val DEFAULT_SL_PCT = -0.5
|
||||
private const val DEFAULT_TP_PCT = 1.0
|
||||
private const val DEFAULT_SL_PCT = -1.5
|
||||
private const val DEFAULT_TP_PCT = 1.5
|
||||
private const val HIGH_SCORE_THRESHOLD = 80
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import com.microsoft.playwright.Playwright
|
||||
import com.microsoft.playwright.BrowserType
|
||||
import com.microsoft.playwright.Page
|
||||
import com.microsoft.playwright.options.LoadState
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@@ -92,33 +93,58 @@ object DynamicNewsScraper {
|
||||
}
|
||||
|
||||
suspend fun fetchFullContent(url: String): String {
|
||||
// browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다.
|
||||
val context = browser.newContext()
|
||||
val page = context.newPage()
|
||||
delay(Random.nextInt(1000).toLong())
|
||||
return try {
|
||||
// 1. 페이지 이동 및 네트워크 유휴 상태까지 대기
|
||||
blockUnnecessaryResources(page)
|
||||
page.navigate(url)
|
||||
// println(url)
|
||||
page.waitForLoadState()
|
||||
context.use { ctx ->
|
||||
ctx.newPage().use { page ->
|
||||
delay(Random.nextInt(1000).toLong())
|
||||
|
||||
// 1. 리스너 설정 시 예외 처리 강화
|
||||
blockUnnecessaryResources(page)
|
||||
|
||||
var finded = cleanText(extractSmartContentWithLineFilter(page))
|
||||
println("finded : $finded")
|
||||
finded
|
||||
// 2. 타임아웃을 설정하여 무한 대기 방지
|
||||
val options = Page.NavigateOptions().setTimeout(30000.0)
|
||||
page.navigate(url, options)
|
||||
|
||||
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
|
||||
page.waitForLoadState(LoadState.LOAD)
|
||||
|
||||
val content = cleanText(extractSmartContentWithLineFilter(page))
|
||||
|
||||
// 4. 명시적으로 route를 해제하여 close 시 발생할 수 있는 리스너 충돌 방지
|
||||
page.unroute("**/*")
|
||||
|
||||
content
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("❌ [Playwright] 스크래핑 실패: ${e.message}")
|
||||
println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}")
|
||||
""
|
||||
} finally {
|
||||
page.close()
|
||||
context.close()
|
||||
// use 블록이 자원을 닫으려 할 때 발생하는 오류는 내부적으로 처리되거나 무시되도록 유도
|
||||
}
|
||||
}
|
||||
|
||||
private fun blockUnnecessaryResources(page: Page) {
|
||||
// 이미지, 폰트, CSS 등 불필요한 요청 가로채서 중단
|
||||
page.route("**/*.{png,jpg,jpeg,gif,webp,svg,css,woff,woff2}") { route ->
|
||||
route.abort()
|
||||
page.route("**/*") { route ->
|
||||
try {
|
||||
val req = route.request()
|
||||
if (req != null) {
|
||||
val type = req.resourceType()
|
||||
if (type == "image" || type == "font" || type == "stylesheet") {
|
||||
route.abort()
|
||||
} else {
|
||||
route.resume()
|
||||
}
|
||||
} else {
|
||||
// request가 이미 null이면 처리를 포기
|
||||
route.resume()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,16 +164,25 @@ object SafeScraper {
|
||||
urls.map { item ->
|
||||
async {
|
||||
if (UrlCacheManager.isAlreadyProcessed(item.originallink) == false) {
|
||||
semaphore.withPermit {
|
||||
RagService.ingestWithChunking(
|
||||
text = DynamicNewsScraper.fetchFullContent(item.originallink),
|
||||
newsLink = item.originallink,
|
||||
pubDate = item.pubDate,
|
||||
stockCode = corpInfo.stockCode,
|
||||
corpName = corpInfo.cName,
|
||||
corpCode = corpInfo.cCode,
|
||||
stcokName = corpInfo.stockName
|
||||
)
|
||||
try {
|
||||
semaphore.withPermit {
|
||||
try {
|
||||
RagService.ingestWithChunking(
|
||||
text = DynamicNewsScraper.fetchFullContent(item.originallink),
|
||||
newsLink = item.originallink,
|
||||
pubDate = item.pubDate,
|
||||
stockCode = corpInfo.stockCode,
|
||||
corpName = corpInfo.cName,
|
||||
corpCode = corpInfo.cCode,
|
||||
stcokName = corpInfo.stockName
|
||||
)
|
||||
}catch (e: Exception) {
|
||||
println("${e.message}")
|
||||
}
|
||||
|
||||
}
|
||||
}catch (e: Exception) {
|
||||
println("${e.message}")
|
||||
}
|
||||
println("📰 '${query}' 관련 뉴스 새로운 학습 데이터 게더링")
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package service
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object SystemSleepPreventer {
|
||||
private var process: Process? = null
|
||||
|
||||
/**
|
||||
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||
*/
|
||||
fun start() {
|
||||
if (process?.isAlive == true) return
|
||||
|
||||
try {
|
||||
// -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
||||
val command = listOf("caffeinate", "-i", "-d", "-m")
|
||||
process = ProcessBuilder(command).start()
|
||||
println("☕ [System] caffeinate 실행됨: 앱이 켜져 있는 동안 절전 모드가 방지됩니다.")
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 앱 종료 시 프로세스 함께 종료
|
||||
*/
|
||||
fun stop() {
|
||||
process?.destroy()
|
||||
// 프로세스가 강제 종료되지 않을 경우를 대비해 0.5초 대기 후 강제 종료
|
||||
if (process?.waitFor(500, TimeUnit.MILLISECONDS) == false) {
|
||||
process?.destroyForcibly()
|
||||
}
|
||||
println("🛑 [System] caffeinate 종료됨: 시스템 절전 설정이 정상화됩니다.")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user