This commit is contained in:
lunaticbum 2026-04-02 11:16:53 +09:00
parent 6e7f485988
commit 921e6c7caf
2 changed files with 52 additions and 63 deletions

View File

@ -256,14 +256,14 @@ object RagService {
result(finalDecision, true) result(finalDecision, true)
} else { } else {
println("✋ [$stockName] 기술 점수 미달로 분석 중단") println("✋ [$stockName] 기술 점수 미달로 분석 중단 ${scores.toString()}")
TradingLogStore.addAnalyzer(stockName, stockCode, "기술 점수 미달로 분석 중단") TradingLogStore.addAnalyzer(stockName, stockCode, "기술 점수 미달로 분석 중단")
tradingDecision.confidence = 1.0 tradingDecision.confidence = 1.0
result(tradingDecision, false) result(tradingDecision, false)
} }
} else { } else {
println("🚨 [$stockName] 재무 안전벨트 미달") println("🚨 [$stockName] ${FinancialAnalyzer.getInvestmentStatus(financialStmt)} 재무 안전벨트 미달")
TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단") TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.getInvestmentStatus(financialStmt)}")
tradingDecision.confidence = 1.0 tradingDecision.confidence = 1.0
result(tradingDecision, false) result(tradingDecision, false)
} }
@ -290,53 +290,6 @@ object RagService {
return result.matches().isNotEmpty() return result.matches().isNotEmpty()
} }
/**
* 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다.
*/
// fun askWithContext(question: String,
// corpInfo: String,
// financialData: String,
// days : List<CandleData>,
// weeks : List<CandleData>,
// monthly : List<CandleData>): 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 fun LLM_API_URL() = "http://127.0.0.1:$LLM_PORT/v1/chat/completions"
private suspend fun callLlamaWithSchema(prompt: String): String { private suspend fun callLlamaWithSchema(prompt: String): String {
@ -409,9 +362,9 @@ object RagService {
// 2. 뉴스 유무에 따른 동적 데이터 섹션 구성 // 2. 뉴스 유무에 따른 동적 데이터 섹션 구성
val newsDataSection = if (validNews != null) { val newsDataSection = if (validNews != null) {
"3. News Context: $validNews" "4. News Context: $validNews"
} else { } 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)}% 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 $newsDataSection
# Step-by-Step Analysis Logic # Step-by-Step Analysis Logic
1. Financial Review: First, evaluate the 'Financials' section for long-term stability. 1. Financial Review: First, evaluate the 'Financials' section for long-term stability.

View File

@ -852,8 +852,8 @@ object FinancialAnalyzer {
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만 val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상 val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함 val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
val isNotCrashing = fs.netIncomeGrowth > -40.0
return isDebtSafe && isLiquiditySafe && isNotDeficit return isDebtSafe && isLiquiditySafe && isNotDeficit && isNotCrashing
} }
/** /**
@ -876,10 +876,12 @@ object FinancialAnalyzer {
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만 val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상 val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함 val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
val isNotCrashing = fs.netIncomeGrowth > -40.0
if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) { if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) {
if (isDebtSafe)buffer.appendLine( "부채비율 200% 이상") if (!isDebtSafe)buffer.appendLine( "부채비율 200% 이상")
if (isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만") if (!isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만")
if (isNotDeficit)buffer.appendLine( "당기순이익 적자") if (!isNotDeficit)buffer.appendLine( "당기순이익 적자")
if (!isNotCrashing) { buffer.appendLine("당기순이익 급감(${String.format("%.1f", fs.netIncomeGrowth)}%)") }
buffer.appendLine("최소 기준 미달") buffer.appendLine("최소 기준 미달")
} else { } else {
buffer.appendLine("최소 기준 충족") buffer.appendLine("최소 기준 충족")
@ -1009,23 +1011,47 @@ class TechnicalAnalyzer {
): InvestmentScores { ): InvestmentScores {
// 1. 초단기 (분봉 + 에너지 지표 위주) // 1. 초단기 (분봉 + 에너지 지표 위주)
val ultra = (calculateMFI(min30, 14) * 0.4 + var ultra = (calculateMFI(min30, 14) * 0.4 +
calculateStochastic(min30) * 0.3 + calculateStochastic(min30) * 0.3 +
(if(calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt() (if(calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt()
// 2. 단기 (일봉 추세 + OBV 에너지) // 2. 단기 (일봉 추세 + OBV 에너지)
val short = (calculateRSI(daily) * 0.3 + var short = (calculateRSI(daily) * 0.3 +
(if(calculateOBV(daily) > 0) 40 else 10) + (if(calculateOBV(daily) > 0) 40 else 10) +
(if(calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt() (if(calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt()
// 3. 중기 (주봉 + 재무 점수 혼합) // 3. 중기 (주봉 + 재무 점수 혼합)
val mid = (if(calculateChange(weekly) > 0) 40 else 10) + var mid = (if(calculateChange(weekly) > 0) 40 else 10) +
(financialScore * 0.6).toInt() (financialScore * 0.6).toInt()
// 4. 장기 (월봉 + 섹터/기업 펀더멘털) // 4. 장기 (월봉 + 섹터/기업 펀더멘털)
val long = (if(calculateChange(monthly) > 0) 50 else 0) + var long = (if(calculateChange(monthly) > 0) 50 else 0) +
(financialScore * 0.5).toInt() (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( return InvestmentScores(
ultraShort = ultra.coerceIn(0, 100), ultraShort = ultra.coerceIn(0, 100),
shortTerm = short.coerceIn(0, 100), shortTerm = short.coerceIn(0, 100),
@ -1296,8 +1322,15 @@ class ScalpingAnalyzer {
val volSurge = volRatioNow > VOL_SURGE_THRESHOLD val volSurge = volRatioNow > VOL_SURGE_THRESHOLD
val bbGood = bbPos > BB_LOWER_POS && bbPos < BB_UPPER_POS val bbGood = bbPos > BB_LOWER_POS && bbPos < BB_UPPER_POS
val maBull = currentClose > sma10Now && sma10Now > sma20Now val maBull = currentClose > sma10Now && sma10Now > sma20Now
val buySignal = maBull && rsiBull && volSurge && bbGood && isBreakout // val buySignal = maBull && rsiBull && volSurge && bbGood && isBreakout
// val buySignal = maBull && rsiBull && volSurge && bbGood 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) + val score = (if (maBull) 25 else 0) +