This commit is contained in:
2026-04-08 14:18:09 +09:00
parent b95c1d5f72
commit 6494784bbc
7 changed files with 471 additions and 274 deletions
+96 -12
View File
@@ -3,6 +3,7 @@ package network// src/main/kotlin/network/RagService.kt
import Defines.EMBEDDING_PORT
import Defines.LLM_PORT
import TradingLogStore
import analyzer.AdvancedTradeAssistant
import analyzer.FinancialAnalyzer
import analyzer.FinancialMapper
import analyzer.FinancialStatement
@@ -55,6 +56,7 @@ import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
//interface TradingAnalyst {
@@ -67,6 +69,13 @@ import java.util.concurrent.TimeUnit
//}
object RagService {
val isSafetyBeltStockCodes = ConcurrentHashMap.newKeySet<String>()
// (매일 아침 8시 30분 시스템 초기화 시 호출해주어야 함)
fun clearDailyCache() {
isSafetyBeltStockCodes.clear()
println("🧹 [System] 일일 재무 미달 캐시 초기화 완료")
}
// 임베딩 모델 (8081) 및 채팅 모델 (8080) 설정
private val embeddingModel = OpenAiEmbeddingModel.builder()
@@ -237,6 +246,13 @@ object RagService {
this.currentPrice = currentPrice
}
if (isSafetyBeltStockCodes.contains(stockCode)) {
// 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스
// logTime(stockName, "재무 미달 (캐시) 조기 종료", 0, System.currentTimeMillis() - totalStartTime)
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false)
return@coroutineScope
}
// [1단계] 재무 분석 및 필터링 (가장 빠름)
val finStartTime = System.currentTimeMillis()
val financialData = NewsService.fetchFinancialGrowth(DartCodeManager.getCorpCode(stockCode)?.cCode)
@@ -256,12 +272,22 @@ object RagService {
if (!FinancialAnalyzer.isSafetyBeltMet(financialStmt)) {
logTime(stockName, "재무 미달 조기 종료", finDuration, System.currentTimeMillis() - totalStartTime)
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족" }, false)
isSafetyBeltStockCodes.add(stockCode)
return@coroutineScope
}
if ((tradingDecision.signalModel?.compositeScore ?: 0) < 50) {
logTime(stockName, "기술 점수 미달 조기 종료", techDuration, System.currentTimeMillis() - totalStartTime)
result(tradingDecision.apply { decision = "HOLD"; reason = "매수 타점 미도달" }, false)
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) {
TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true)
result(tradingDecision.apply {
decision = "RETRY" // 콜백에서 "BUY"가 아니므로 HOLD와 동일하게 취급됨
reason = "매수 타점 미도달 (재무 우량주로 감시 지속)"
confidence = 65.0 // AutoTradingManager의 재분석 기준(60.0)을 넘기기 위해 부여
}, true) // isSuccess를 true로 주어야 콜백이 무시하지 않음
} else {
result(tradingDecision.apply { decision = "HOLD"; reason = "매수 타점 미도달" }, false)
}
return@coroutineScope
}
@@ -286,8 +312,8 @@ object RagService {
tradingDecision.newsContext = finalSearchResult.matches().distinct() // 중복 제거
.take(4) // 10개에서 4개로 축소
.joinToString("\n\n") {
it.embedded().text()
}
it.embedded().text()
}
val finalDecision = decideTrading(stockName, scores, financialStmt, tradingDecision)
val ragAiDuration = System.currentTimeMillis() - ragStartTime
@@ -442,7 +468,7 @@ object RagService {
// 1-3. 뉴스 AI 분석 시간 측정 (가장 병목이 예상되는 구간)
val newsStartTime = System.currentTimeMillis()
val (newsScore100, newsReason) = tempDecision.newsContext?.let {
getAiNewsScore(it, tempDecision.techSummary ?: "")
getAiNewsScore(stockName,it, tempDecision.techSummary ?: "")
} ?: (50.0 to "참조 뉴스 없음")
val newsDuration = System.currentTimeMillis() - newsStartTime
@@ -460,14 +486,41 @@ object RagService {
if (isOverheated) finalConfidence *= 0.85
val totalScore = (scores.ultraShort + scores.shortTerm + scores.midTerm + scores.longTerm) / 4.0
val grade = AutoTradingManager.getInvestmentGrade(tempDecision, totalScore, finalConfidence)
val synthDuration = System.currentTimeMillis() - synthStartTime
tempDecision.ultraShortScore = scores.ultraShort.toDouble()
tempDecision.shortTermScore = scores.shortTerm.toDouble()
tempDecision.midTermScore = scores.midTerm.toDouble()
tempDecision.longTermScore = scores.longTerm.toDouble()
// 5. 최종 결정 및 사유 정리
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
var finalDecision = "HOLD"
var finalReason = ""
var grade = AutoTradingManager.getInvestmentGrade(tempDecision, totalScore, finalConfidence, finScore100)
var assistantReason = ""
if (grade != InvestmentGrade.LEVEL_0_SPECULATIVE) {
val advice = AdvancedTradeAssistant.confirmTrade(
currentGrade = grade,
currentPrice = tempDecision.currentPrice,
min30 = tempDecision.analyzer?.min30 ?: emptyList(),
daily = tempDecision.analyzer?.daily ?: emptyList()
)
finalConfidence += advice.confidenceBonus
if (!advice.isConfirmed) {
grade = InvestmentGrade.LEVEL_0_SPECULATIVE
assistantReason = " 🚫 [어시스턴트 차단] ${advice.reason}"
} else if (advice.reason.isNotEmpty()) { // 💡 조건 변경
assistantReason = " [확인됨: ${advice.reason}]"
}
}
val synthDuration = System.currentTimeMillis() - synthStartTime
when {
newsScore100 < 30.0 -> {
finalDecision = "HOLD"
@@ -477,9 +530,9 @@ object RagService {
finalDecision = "HOLD"
finalReason = "🔥 단기 과열 구간(이격도 높음)으로 인한 매수 제한"
}
finalConfidence >= minScore && newsScore100 >= 50.0 && grade != InvestmentGrade.LEVEL_1_SPECULATIVE -> {
finalConfidence >= minScore && newsScore100 >= 50.0 && grade != InvestmentGrade.LEVEL_0_SPECULATIVE -> {
finalDecision = "BUY"
finalReason = "✅ [${grade.displayName}] $newsReason | 종합 지표 우수"
finalReason = "✅ [${grade.displayName}] $newsReason | 종합 지표 우수 | $assistantReason"
}
finalConfidence < 40.0 -> {
finalDecision = "SELL"
@@ -497,6 +550,9 @@ object RagService {
println("⏱️ [$stockName] 처리 성능 리포트: 전체 ${totalDuration}ms | 재무 ${finDuration}ms | 기술 ${techDuration}ms | 뉴스AI ${newsDuration}ms | 합성 ${synthDuration}ms")
return TradingDecision().apply {
this.technicalScore = techScore100
this.financialScore = finScore100
this.systemScore = sysScore100
this.stockCode = tempDecision.stockCode
this.stockName = stockName
this.currentPrice = tempDecision.currentPrice
@@ -520,9 +576,11 @@ object RagService {
}
private suspend fun getAiNewsScore(news: String,techSummary : String): Pair<Double, String> {
private suspend fun getAiNewsScore(stockName:String , news: String,techSummary : String): Pair<Double, String> {
val prompt = """
# Role: Expert Quantitative & Sentiment Analyst
# Target Stock: [$stockName]
# Task: Evaluate [News Text] by correlating it with [Market Context].
# Input 1: [Market Context] (Standardized Scores & Price History)
@@ -543,7 +601,11 @@ object RagService {
- If 'Base Position' is near 100% (at 120MA), consider it a 'Safe Entry' for long-term holding.
# Constraints:
- Reason: KOREAN only, max 50 chars. Explain the "Synergy" between scores and news.
- Reason: KOREAN only, max 50 chars. Explain the "Synergy" between scores and news.
1. Target Isolation: You MUST ONLY extract facts related EXACTLY to [$stockName].
2. No Mix-up: Do NOT attribute actions of other companies (e.g., unrelated capital increases or earnings of competitors) to [$stockName].
3. Verify Numbers: Check if [$stockName]'s YoY profit is Positive (+) or Negative (-). If Negative, you MUST reflect it as a penalty in the score.
- Output: Strictly JSON format.
# JSON Output:
@@ -608,6 +670,9 @@ class TradingDecision {
var reason: String? = null
var confidence: Double = 0.0
var newsScore : Double = 0.0
var systemScore : Double = 0.0
var financialScore : Double = 0.0
var technicalScore : Double = 0.0
var investmentGrade : InvestmentGrade? = null
var techSummary : String? = null
var newsContext : String? = null
@@ -630,6 +695,26 @@ class TradingDecision {
midTermScore,
longTermScore).average()
fun summary() : String{
return """
$corpName[$stockName]
수익실현 가능성 : ${profitPossible()}
investmentGrade:${investmentGrade!!.name}
ultraShortScore :$ultraShortScore
shortTermScore :$shortTermScore
midTermScore :$midTermScore
longTermScore :$longTermScore
systemScore :$systemScore
technicalScore: $technicalScore
financialScore: $financialScore
newsScore: $newsScore
decision: $decision
reason: $reason
""".trimIndent()
}
override fun toString(): String {
return """
$corpName($stockName)
@@ -639,12 +724,11 @@ shortTermScore :$shortTermScore
midTermScore :$midTermScore
longTermScore :$longTermScore
decision: $decision
investmentGrade:${investmentGrade!!.name}
reason: $reason
confidence: $confidence
기술 분석: $techSummary
뉴스 점수: $newsScore
""".trimIndent()
}
}