...
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user