...
This commit is contained in:
parent
5ba71f37c0
commit
05a9d01bba
@ -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초 후 재연결 시도
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -201,16 +201,6 @@ object RagService {
|
|||||||
println("⏱️ [$stockName] 재무 분석 소요: ${financialDuration}ms")
|
println("⏱️ [$stockName] 재무 분석 소요: ${financialDuration}ms")
|
||||||
|
|
||||||
if (FinancialAnalyzer.isSafetyBeltMet(financialStmt)) {
|
if (FinancialAnalyzer.isSafetyBeltMet(financialStmt)) {
|
||||||
// 2. 뉴스 스크래핑 및 학습 시간 측정
|
|
||||||
val newsIngestStartTime = System.currentTimeMillis()
|
|
||||||
corpInfo?.let {
|
|
||||||
try {
|
|
||||||
NewsService.fetchAndIngestNews(it)
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
}
|
|
||||||
val newsIngestDuration = System.currentTimeMillis() - newsIngestStartTime
|
|
||||||
println("⏱️ [$stockName] 뉴스 수집/인덱싱 소요: ${newsIngestDuration}ms")
|
|
||||||
|
|
||||||
// 3. 기술적 지표 계산 시간 측정
|
// 3. 기술적 지표 계산 시간 측정
|
||||||
val techStartTime = System.currentTimeMillis()
|
val techStartTime = System.currentTimeMillis()
|
||||||
val financialScore = FinancialAnalyzer.calculateScore(financialStmt)
|
val financialScore = FinancialAnalyzer.calculateScore(financialStmt)
|
||||||
@ -218,6 +208,16 @@ object RagService {
|
|||||||
val techDuration = System.currentTimeMillis() - techStartTime
|
val techDuration = System.currentTimeMillis() - techStartTime
|
||||||
println("⏱️ [$stockName] 기술적 지표 계산 소요: ${techDuration}ms")
|
println("⏱️ [$stockName] 기술적 지표 계산 소요: ${techDuration}ms")
|
||||||
if (scores.avg() > 50) {
|
if (scores.avg() > 50) {
|
||||||
|
// 2. 뉴스 스크래핑 및 학습 시간 측정
|
||||||
|
val newsIngestStartTime = System.currentTimeMillis()
|
||||||
|
corpInfo?.let {
|
||||||
|
try {
|
||||||
|
NewsService.fetchAndIngestNews(it)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
val newsIngestDuration = System.currentTimeMillis() - newsIngestStartTime
|
||||||
|
println("⏱️ [$stockName] 뉴스 수집/인덱싱 소요: ${newsIngestDuration}ms")
|
||||||
|
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
|
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
@ -566,4 +566,84 @@ confidence: $confidence
|
|||||||
재무재표: $financialData
|
재무재표: $financialData
|
||||||
""".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
|
||||||
|
)
|
||||||
@ -282,7 +282,13 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
|
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
|
||||||
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
|
|
||||||
|
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 ?: "매수 실패")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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/메모리 점유율이 매우 높습니다.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user