This commit is contained in:
lunaticbum 2026-04-01 14:35:56 +09:00
parent 5ba71f37c0
commit 05a9d01bba
5 changed files with 102 additions and 115 deletions

View File

@ -120,9 +120,10 @@ object KisWebSocketManager {
println("🏁 웹소켓 finally 블록 진입 (연결 시도 종료)") println("🏁 웹소켓 finally 블록 진입 (연결 시도 종료)")
isConnected.set(false) isConnected.set(false)
session = null session = null
val retry = 10000L
AutoTradingManager.webSocketConnect = false AutoTradingManager.webSocketConnect = false
println("5초 후 재연결 시도...") println("${(retry / 1000).toInt()}초 후 재연결 시도...")
delay(5000) // 5초 후 재연결 시도 delay(retry) // 5초 후 재연결 시도
} }
} }
} }

View File

@ -71,18 +71,6 @@ object NewsService {
} }
// suspend fun fetchCorpInfo(corpCode: String): String {
// val apiKey = "61143d2af0759f6c28ce372d9e339d1e01687abc"
// val url = "https://opendart.fss.or.kr/api/company.json?crtfc_key=$apiKey&corp_code=$corpCode"
//
// return try {
// val response = client.get(url).body<CorpInfo>()
// "기업명: ${response.corp_name}, 주요사업: ${response.main_business}"
// } catch (e: Exception) {
// "기업 정보 로드 실패"
// }
// }
suspend fun fetchFinancialGrowth(corpCode: String?): String { suspend fun fetchFinancialGrowth(corpCode: String?): String {
if (corpCode != null) { if (corpCode != null) {
val apiKey = KisSession.config.dAppKey val apiKey = KisSession.config.dAppKey
@ -106,81 +94,3 @@ object NewsService {
} }
} }
} }
object FinancialMapper {
/**
* 제공된 텍스트 데이터를 파싱하여 FinancialStatement 객체로 변환
*/
fun mapRawTextToStatement(rawText: String): FinancialStatement {
if (rawText.isBlank()) {
return FinancialStatement()
}
// println(rawText)
val currentValues = extractYearlyValues(rawText, "당기")
val previousValues = extractYearlyValues(rawText, "전기")
// 1. 영업이익 증가율: (당기 - 전기) / |전기| * 100
val opCurrent = currentValues["영업이익"] ?: 0.0
val opPrevious = previousValues["영업이익"] ?: 0.0
val opGrowth = if (opPrevious != 0.0) ((opCurrent - opPrevious) / Math.abs(opPrevious)) * 100 else 0.0
// 2. 당기순이익 증가율
val niCurrent = currentValues["당기순이익(손실)"] ?: 0.0
val niPrevious = previousValues["당기순이익(손실)"] ?: 0.0
val niGrowth = if (niPrevious != 0.0) ((niCurrent - niPrevious) / Math.abs(niPrevious)) * 100 else 0.0
// 3. ROE: 당기순이익 / 당기 자본총계 * 100
val equityCurrent = currentValues["자본총계"] ?: 1.0
val roe = (niCurrent / equityCurrent) * 100
// 4. 부채비율: 당기 부채총계 / 당기 자본총계 * 100
val debtCurrent = currentValues["부채총계"] ?: 0.0
val debtRatio = (debtCurrent / equityCurrent) * 100
// 5. 당좌비율(유동성): 당기 유동자산 / 당기 유동부채 * 100
val currentAssets = currentValues["유동자산"] ?: 0.0
val currentLiabilities = currentValues["유동부채"] ?: 1.0
val quickRatio = (currentAssets / currentLiabilities) * 100
return FinancialStatement(
operatingProfitGrowth = opGrowth,
netIncomeGrowth = niGrowth,
roe = roe,
debtRatio = debtRatio,
quickRatio = quickRatio,
isOperatingProfitPositive = opCurrent > 0,
isNetIncomePositive = niCurrent > 0
).apply {
println("당기순이익: ${niCurrent} , isSafetyBeltMet ${FinancialAnalyzer.isSafetyBeltMet(this)}")
}
}
private fun extractYearlyValues(text: String, type: String): Map<String, Double> {
val result = mutableMapOf<String, Double>()
// 핵심 수정: 항목명 뒤에 (당기) 또는 (전기)가 오고, 그 직후의 숫자(마이너스, 쉼표 포함)를 캡처
// 쉼표나 공백으로 끝나는 지점까지 찾습니다.
val regex = Regex("""([가-힣\s()]+)\s\($type\)([-0-9,.]+)""")
regex.findAll(text).forEach { match ->
val key = match.groupValues[1].trim()
// 숫자 내 쉼표 제거 후 Double 변환
val rawValue = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
result[key] = rawValue
}
return result
}
}
@Serializable
data class FinancialStatement(
val revenueGrowth: Double = 0.0, // 매출액 증가율
val operatingProfitGrowth: Double = 0.0, // 영업이익 증가율
val netIncomeGrowth: Double = 0.0, // 당기순이익 증가율
val roe: Double = 0.0, // ROE
val debtRatio: Double = 0.0, // 부채비율
val quickRatio: Double = 0.0, // 당좌비율
val isOperatingProfitPositive: Boolean = false, // 당기 영업이익 흑자 여부
val isNetIncomePositive: Boolean = false
)

View File

@ -201,6 +201,13 @@ object RagService {
println("⏱️ [$stockName] 재무 분석 소요: ${financialDuration}ms") println("⏱️ [$stockName] 재무 분석 소요: ${financialDuration}ms")
if (FinancialAnalyzer.isSafetyBeltMet(financialStmt)) { if (FinancialAnalyzer.isSafetyBeltMet(financialStmt)) {
// 3. 기술적 지표 계산 시간 측정
val techStartTime = System.currentTimeMillis()
val financialScore = FinancialAnalyzer.calculateScore(financialStmt)
val scores = technicalAnalyzer.calculateScores(financialScore)
val techDuration = System.currentTimeMillis() - techStartTime
println("⏱️ [$stockName] 기술적 지표 계산 소요: ${techDuration}ms")
if (scores.avg() > 50) {
// 2. 뉴스 스크래핑 및 학습 시간 측정 // 2. 뉴스 스크래핑 및 학습 시간 측정
val newsIngestStartTime = System.currentTimeMillis() val newsIngestStartTime = System.currentTimeMillis()
corpInfo?.let { corpInfo?.let {
@ -211,13 +218,6 @@ object RagService {
val newsIngestDuration = System.currentTimeMillis() - newsIngestStartTime val newsIngestDuration = System.currentTimeMillis() - newsIngestStartTime
println("⏱️ [$stockName] 뉴스 수집/인덱싱 소요: ${newsIngestDuration}ms") println("⏱️ [$stockName] 뉴스 수집/인덱싱 소요: ${newsIngestDuration}ms")
// 3. 기술적 지표 계산 시간 측정
val techStartTime = System.currentTimeMillis()
val financialScore = FinancialAnalyzer.calculateScore(financialStmt)
val scores = technicalAnalyzer.calculateScores(financialScore)
val techDuration = System.currentTimeMillis() - techStartTime
println("⏱️ [$stockName] 기술적 지표 계산 소요: ${techDuration}ms")
if (scores.avg() > 50) {
result(tradingDecision, false) result(tradingDecision, false)
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport() tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
result(tradingDecision, false) result(tradingDecision, false)
@ -567,3 +567,83 @@ confidence: $confidence
""".trimIndent() """.trimIndent()
} }
} }
object FinancialMapper {
/**
* 제공된 텍스트 데이터를 파싱하여 FinancialStatement 객체로 변환
*/
fun mapRawTextToStatement(rawText: String): FinancialStatement {
if (rawText.isBlank()) {
return FinancialStatement()
}
// println(rawText)
val currentValues = extractYearlyValues(rawText, "당기")
val previousValues = extractYearlyValues(rawText, "전기")
// 1. 영업이익 증가율: (당기 - 전기) / |전기| * 100
val opCurrent = currentValues["영업이익"] ?: 0.0
val opPrevious = previousValues["영업이익"] ?: 0.0
val opGrowth = if (opPrevious != 0.0) ((opCurrent - opPrevious) / Math.abs(opPrevious)) * 100 else 0.0
// 2. 당기순이익 증가율
val niCurrent = currentValues["당기순이익(손실)"] ?: 0.0
val niPrevious = previousValues["당기순이익(손실)"] ?: 0.0
val niGrowth = if (niPrevious != 0.0) ((niCurrent - niPrevious) / Math.abs(niPrevious)) * 100 else 0.0
// 3. ROE: 당기순이익 / 당기 자본총계 * 100
val equityCurrent = currentValues["자본총계"] ?: 1.0
val roe = (niCurrent / equityCurrent) * 100
// 4. 부채비율: 당기 부채총계 / 당기 자본총계 * 100
val debtCurrent = currentValues["부채총계"] ?: 0.0
val debtRatio = (debtCurrent / equityCurrent) * 100
// 5. 당좌비율(유동성): 당기 유동자산 / 당기 유동부채 * 100
val currentAssets = currentValues["유동자산"] ?: 0.0
val currentLiabilities = currentValues["유동부채"] ?: 1.0
val quickRatio = (currentAssets / currentLiabilities) * 100
return FinancialStatement(
operatingProfitGrowth = opGrowth,
netIncomeGrowth = niGrowth,
roe = roe,
debtRatio = debtRatio,
quickRatio = quickRatio,
isOperatingProfitPositive = opCurrent > 0,
isNetIncomePositive = niCurrent > 0
).apply {
println("당기순이익: ${niCurrent} , isSafetyBeltMet ${FinancialAnalyzer.isSafetyBeltMet(this)}")
}
}
private fun extractYearlyValues(text: String, type: String): Map<String, Double> {
val result = mutableMapOf<String, Double>()
// 핵심 수정: 항목명 뒤에 (당기) 또는 (전기)가 오고, 그 직후의 숫자(마이너스, 쉼표 포함)를 캡처
// 쉼표나 공백으로 끝나는 지점까지 찾습니다.
val regex = Regex("""([가-힣\s()]+)\s\($type\)([-0-9,.]+)""")
regex.findAll(text).forEach { match ->
val key = match.groupValues[1].trim()
// 숫자 내 쉼표 제거 후 Double 변환
val rawValue = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
result[key] = rawValue
}
return result
}
}
@Serializable
data class FinancialStatement(
val revenueGrowth: Double = 0.0, // 매출액 증가율
val operatingProfitGrowth: Double = 0.0, // 영업이익 증가율
val netIncomeGrowth: Double = 0.0, // 당기순이익 증가율
val roe: Double = 0.0, // ROE
val debtRatio: Double = 0.0, // 부채비율
val quickRatio: Double = 0.0, // 당좌비율
val isOperatingProfitPositive: Boolean = false, // 당기 영업이익 흑자 여부
val isNetIncomePositive: Boolean = false
)

View File

@ -282,10 +282,16 @@ object AutoTradingManager {
} }
.onFailure { .onFailure {
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice") println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"BUY","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
} else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
} }
} }
} }
}
var onExecutionReceived : ((String, String, String, String, Boolean) -> Unit)? = {code, qty, price,orderNo, isBuy -> var onExecutionReceived : ((String, String, String, String, Boolean) -> Unit)? = {code, qty, price,orderNo, isBuy ->
scope.launch { scope.launch {
val exec = ExecutionData(orderNo, code, price, qty, isBuy) val exec = ExecutionData(orderNo, code, price, qty, isBuy)

View File

@ -107,10 +107,6 @@ object BrowserManager {
} }
object DynamicNewsScraper { object DynamicNewsScraper {
// private val playwright by lazy { Playwright.create() }
// private val browser by lazy {
// playwright.chromium().launch(BrowserType.LaunchOptions().setHeadless(true))
// }
fun extractSmartContentWithLineFilter(page: Page): String { fun extractSmartContentWithLineFilter(page: Page): String {
val script = """ val script = """
@ -273,13 +269,7 @@ object SafeScraper {
private val totalRam = HardwareDetector.getTotalRamGb() private val totalRam = HardwareDetector.getTotalRamGb()
// RAM 8GB당 1개 수준으로 설정하되, 최대 10~12개로 제한 (CPU 부하 방지) // RAM 8GB당 1개 수준으로 설정하되, 최대 10~12개로 제한 (CPU 부하 방지)
private val maxParallel = when { private val maxParallel = totalRam.div(6).toInt()
totalRam >= 128 -> 8
totalRam >= 64 -> 6
totalRam >= 32 -> 4
totalRam >= 16 -> 2
else -> 1
}
// 동시 처리를 1개로 줄여서 안정성을 극대화 (추천) // 동시 처리를 1개로 줄여서 안정성을 극대화 (추천)
// Playwright는 여러 페이지를 띄울 때 CPU/메모리 점유율이 매우 높습니다. // Playwright는 여러 페이지를 띄울 때 CPU/메모리 점유율이 매우 높습니다.