diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 577b18b..6f3c5c3 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -256,14 +256,14 @@ object RagService { result(finalDecision, true) } else { - println("✋ [$stockName] 기술 점수 미달로 분석 중단") + println("✋ [$stockName] 기술 점수 미달로 분석 중단 ${scores.toString()}") TradingLogStore.addAnalyzer(stockName, stockCode, "기술 점수 미달로 분석 중단") tradingDecision.confidence = 1.0 result(tradingDecision, false) } } else { - println("🚨 [$stockName] 재무 안전벨트 미달") - TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단") + println("🚨 [$stockName] ${FinancialAnalyzer.getInvestmentStatus(financialStmt)} 재무 안전벨트 미달") + TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.getInvestmentStatus(financialStmt)}") tradingDecision.confidence = 1.0 result(tradingDecision, false) } @@ -290,53 +290,6 @@ object RagService { return result.matches().isNotEmpty() } - /** - * 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다. - */ -// fun askWithContext(question: String, -// corpInfo: String, -// financialData: String, -// days : List, -// weeks : List, -// monthly : List): String { -// val questionEmbedding = embeddingModel.embed(question).content() -// val searchResult = embeddingStore.search( -// EmbeddingSearchRequest.builder() -// .queryEmbedding(questionEmbedding) -// .maxResults(5) -// .build() -// ) -// val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() } -// -// // 2. 종합 분석 프롬프트 구성 -// val finalPrompt = """ -// <|begin_of_text|><|start_header_id|>system<|end_header_id|> -// 당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다. -// 제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요. -// -// [데이터 세트] -// 1. 기업 기본 정보: $corpInfo -// 2. 재무 성장성: $financialData -// 3. 기술적 추세: ${monthly}, ${weeks}, ${days} -// 4. 최신 이슈(뉴스): $newsContext -// -// [분석 요청 사항] -// 1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요. -// 2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요. -// 3. **장기/단기 전략**: -// - 장기(재무/월봉 기반): 추천 혹은 비추천 사유 -// - 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유 -// 4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요. -// <|eot_id|> -// <|start_header_id|>user<|end_header_id|> -// 질문: $question -// <|eot_id|><|start_header_id|>assistant<|end_header_id|> -// """.trimIndent() -// -// val response = chatModel.chat(UserMessage.from(finalPrompt)) -//// println(response) -// return response.aiMessage().text() -// } private fun LLM_API_URL() = "http://127.0.0.1:$LLM_PORT/v1/chat/completions" private suspend fun callLlamaWithSchema(prompt: String): String { @@ -409,9 +362,9 @@ object RagService { // 2. 뉴스 유무에 따른 동적 데이터 섹션 구성 val newsDataSection = if (validNews != null) { - "3. News Context: $validNews" + "4. News Context: $validNews" } else { - "3. News Context: No significant news available. Rely on financials." + "4. News Context: No significant news available. Rely on financials." } @@ -426,9 +379,12 @@ Your goal is to provide a final trading decision based on STRICT data analysis. 2. Financials: Operating Profit ${if(financialStmt.isOperatingProfitPositive) "PROFIT" else "LOSS"} (Growth: ${"%.2f".format(financialStmt.operatingProfitGrowth)}%), ROE: ${"%.2f".format(financialStmt.roe)}%, Debt: ${"%.2f".format(financialStmt.debtRatio)}% +3. Technical Analysis Summary: ${tempDecision.techSummary ?: "No technical summary available."} + $newsDataSection + # Step-by-Step Analysis Logic 1. Financial Review: First, evaluate the 'Financials' section for long-term stability. diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index d859999..c04a439 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -852,8 +852,8 @@ object FinancialAnalyzer { val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만 val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상 val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함 - - return isDebtSafe && isLiquiditySafe && isNotDeficit + val isNotCrashing = fs.netIncomeGrowth > -40.0 + return isDebtSafe && isLiquiditySafe && isNotDeficit && isNotCrashing } /** @@ -876,10 +876,12 @@ object FinancialAnalyzer { val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만 val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상 val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함 + val isNotCrashing = fs.netIncomeGrowth > -40.0 if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) { - if (isDebtSafe)buffer.appendLine( "부채비율 200% 이상") - if (isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만") - if (isNotDeficit)buffer.appendLine( "당기순이익 적자") + if (!isDebtSafe)buffer.appendLine( "부채비율 200% 이상") + if (!isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만") + if (!isNotDeficit)buffer.appendLine( "당기순이익 적자") + if (!isNotCrashing) { buffer.appendLine("당기순이익 급감(${String.format("%.1f", fs.netIncomeGrowth)}%)") } buffer.appendLine("최소 기준 미달") } else { buffer.appendLine("최소 기준 충족") @@ -1009,23 +1011,47 @@ class TechnicalAnalyzer { ): InvestmentScores { // 1. 초단기 (분봉 + 에너지 지표 위주) - val ultra = (calculateMFI(min30, 14) * 0.4 + + var ultra = (calculateMFI(min30, 14) * 0.4 + calculateStochastic(min30) * 0.3 + (if(calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt() // 2. 단기 (일봉 추세 + OBV 에너지) - val short = (calculateRSI(daily) * 0.3 + + var 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(calculateChange(weekly) > 0) 40 else 10) + + var mid = (if(calculateChange(weekly) > 0) 40 else 10) + (financialScore * 0.6).toInt() // 4. 장기 (월봉 + 섹터/기업 펀더멘털) - val long = (if(calculateChange(monthly) > 0) 50 else 0) + + var long = (if(calculateChange(monthly) > 0) 50 else 0) + (financialScore * 0.5).toInt() + // 1. 일봉 이격도 과열 체크 (20일 이평선 기준) + if (daily.size >= 20) { + val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average() + val currentPrice = daily.last().stck_prpr.toDouble() + val disparityDaily = (currentPrice / ma20Daily) * 100 + + if (disparityDaily > 115.0) { // 20일선보다 15% 이상 떠 있으면 감점 시작 + val penalty = ((disparityDaily - 115.0) * 1).toInt() // 초과분 1%당 2점 감점 + short -= penalty + ultra -= (penalty / 2) // 초단기에도 영향 + println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}점") + } + } + + // 2. 주봉 급등 체크 (최근 3주간의 상승폭) + if (weekly.size >= 3) { + val weeklyChange = calculateChange(weekly.takeLast(3)) + if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시 + mid -= 15 + short -= 7 + println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -15점") + } + } + return InvestmentScores( ultraShort = ultra.coerceIn(0, 100), shortTerm = short.coerceIn(0, 100), @@ -1296,8 +1322,15 @@ class ScalpingAnalyzer { val volSurge = volRatioNow > VOL_SURGE_THRESHOLD val bbGood = bbPos > BB_LOWER_POS && bbPos < BB_UPPER_POS val maBull = currentClose > sma10Now && sma10Now > sma20Now - val buySignal = maBull && rsiBull && volSurge && bbGood && isBreakout -// val buySignal = maBull && rsiBull && volSurge && bbGood +// val buySignal = maBull && rsiBull && volSurge && bbGood && isBreakout + val ma5Daily = if (candles.size >= 5) candles.takeLast(5).map { it.close.toDouble() }.average() else currentClose + val dailyDisparity = (currentClose / ma5Daily) * 100 + +// 과열 기준 정의 + val isOverheated = dailyDisparity > 110.0 // 일봉 5일선 대비 10% 이상 이격 시 과열로 간주 + +// 매수 신호 조건에 과열 방지 추가 + val buySignal = maBull && rsiBull && volSurge && bbGood && isBreakout && !isOverheated val score = (if (maBull) 25 else 0) +