...
This commit is contained in:
@@ -46,16 +46,20 @@ import service.TradingDecisionCallback
|
||||
import service.UrlCacheManager
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface TradingAnalyst {
|
||||
@SystemMessage("""
|
||||
You are a Senior Stock Analyst.
|
||||
Analyze the data and provide a decision in JSON format.
|
||||
You must respond ONLY with a valid JSON object.
|
||||
""")
|
||||
fun analyzeStock(@dev.langchain4j.service.UserMessage prompt: String): TradingDecision
|
||||
}
|
||||
//interface TradingAnalyst {
|
||||
// @SystemMessage("""
|
||||
// You are a Senior Stock Analyst.
|
||||
// Analyze the data and provide a decision in JSON format.
|
||||
// You must respond ONLY with a valid JSON object.
|
||||
// """)
|
||||
// fun analyzeStock(@dev.langchain4j.service.UserMessage prompt: String): TradingDecision
|
||||
//}
|
||||
|
||||
object RagService {
|
||||
|
||||
@@ -76,9 +80,9 @@ object RagService {
|
||||
.responseFormat("json_object")
|
||||
.build()
|
||||
|
||||
private val analyst = AiServices.builder(TradingAnalyst::class.java)
|
||||
.chatModel(chatModel)
|
||||
.build()
|
||||
// private val analyst = AiServices.builder(TradingAnalyst::class.java)
|
||||
// .chatModel(chatModel)
|
||||
// .build()
|
||||
|
||||
private val embeddingStore: LuceneEmbeddingStore by lazy {
|
||||
val path = Paths.get("db/lucene_idx")
|
||||
@@ -180,6 +184,28 @@ object RagService {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRecentNews(dateStr: String?, maxDays: Long = 3): Boolean {
|
||||
if (dateStr.isNullOrBlank()) return false
|
||||
return try {
|
||||
// 네이버 뉴스 OpenAPI 기본 포맷: "Mon, 06 Apr 2026 12:00:00 +0900"
|
||||
val formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH)
|
||||
val pubDate = ZonedDateTime.parse(dateStr, formatter)
|
||||
val now = ZonedDateTime.now()
|
||||
|
||||
// 뉴스가 미래로 표기된 경우도 대비하여 절대값 처리
|
||||
Math.abs(ChronoUnit.DAYS.between(pubDate, now)) <= maxDays
|
||||
} catch (e: Exception) {
|
||||
// 다른 날짜 포맷(예: "yyyy.MM.dd")으로 들어오는 경우를 위한 Fallback
|
||||
try {
|
||||
val fallbackFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm", Locale.ENGLISH)
|
||||
val pubDate = ZonedDateTime.parse("$dateStr 00:00 +0900", fallbackFormatter)
|
||||
Math.abs(ChronoUnit.DAYS.between(pubDate, ZonedDateTime.now())) <= maxDays
|
||||
} catch (e2: Exception) {
|
||||
false // 날짜 파싱 실패 시 보수적으로 '오래된 뉴스'로 취급하여 스크래핑 유도
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun processStock(currentPrice: Double, technicalAnalyzer: TechnicalAnalyzer, stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
||||
val totalStartTime = System.currentTimeMillis() // 전체 시작 시간
|
||||
|
||||
@@ -211,32 +237,68 @@ object RagService {
|
||||
val techDuration = System.currentTimeMillis() - techStartTime
|
||||
println("⏱️ [$stockName] 기술적 지표 계산 소요: ${techDuration}ms")
|
||||
val guideLine = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
|
||||
if (scores.avg() > (guideLine.times(0.85))) {
|
||||
if (scores.avg() > (guideLine.times(0.50))) {
|
||||
// 2. 뉴스 스크래핑 및 학습 시간 측정
|
||||
val ragStartTime = System.currentTimeMillis()
|
||||
val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
|
||||
val questionEmbedding = embeddingModel.embed(question).content()
|
||||
|
||||
// --- 💡 [수정됨] 2. 해당 주식의 최신 뉴스 존재 여부 확인 (최대 10개) ---
|
||||
val preSearchResult = embeddingStore.search(
|
||||
EmbeddingSearchRequest.builder()
|
||||
.queryEmbedding(questionEmbedding)
|
||||
.filter(MetadataFilterBuilder.metadataKey("stockCode").isEqualTo(stockCode)) // 해당 종목만 필터링
|
||||
.maxResults(10)
|
||||
.minScore(0.3) // 👈 0.70은 너무 엄격할 수 있으니 0.65로 하향 조정
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
|
||||
// 검색된 청크들 중 최근 3일 이내의 날짜를 가진 데이터가 하나라도 있는지 확인
|
||||
val hasRecentData = preSearchResult.matches().any { match ->
|
||||
val pubDate = match.embedded().metadata().getString("date")
|
||||
isRecentNews(pubDate, maxDays = 1)
|
||||
}
|
||||
|
||||
// --- 💡 [수정됨] 3. 최신 데이터가 없을 때만 브라우저 스크래핑(Playwright) 실행 ---
|
||||
val newsIngestStartTime = System.currentTimeMillis()
|
||||
corpInfo?.let {
|
||||
try {
|
||||
NewsService.fetchAndIngestNews(it)
|
||||
} catch (e: Exception) {}
|
||||
if (!hasRecentData) {
|
||||
println("🌐 [$stockName] 최근 3일 내 뉴스가 없습니다. 새 뉴스를 스크래핑합니다.")
|
||||
corpInfo?.let {
|
||||
try {
|
||||
NewsService.fetchAndIngestNews(it)
|
||||
} catch (e: Exception) {
|
||||
println("❌ [$stockName] 뉴스 스크래핑 실패: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("✅ [$stockName] 최근 3일 내 뉴스가 DB에 존재하여 브라우저 스크래핑을 생략합니다.")
|
||||
}
|
||||
val newsIngestDuration = System.currentTimeMillis() - newsIngestStartTime
|
||||
println("⏱️ [$stockName] 뉴스 수집/인덱싱 소요: ${newsIngestDuration}ms")
|
||||
println("⏱️ [$stockName] 뉴스 수집/인덱싱 판단 소요: ${newsIngestDuration}ms")
|
||||
|
||||
result(tradingDecision, false)
|
||||
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
|
||||
result(tradingDecision, false)
|
||||
|
||||
// 4. RAG 뉴스 검색 및 임베딩 시간 측정
|
||||
val ragStartTime = System.currentTimeMillis()
|
||||
val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
|
||||
val questionEmbedding = embeddingModel.embed(question).content()
|
||||
val searchResult = embeddingStore.search(
|
||||
// --- 💡 [수정됨] 4. 최종 문맥(Context) 추출 ---
|
||||
// (만약 위에서 스크래핑을 새로 했다면 최신 데이터가 포함되어 검색됩니다)
|
||||
val finalSearchResult = embeddingStore.search(
|
||||
EmbeddingSearchRequest.builder()
|
||||
.queryEmbedding(questionEmbedding)
|
||||
.filter(MetadataFilterBuilder.metadataKey("stockCode").isEqualTo(stockCode)) // 교차 오염 방지를 위해 필터 필수
|
||||
.maxResults(3)
|
||||
.minScore(0.3) // 👈 0.70은 너무 엄격할 수 있으니 0.65로 하향 조정
|
||||
.build()
|
||||
)
|
||||
tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
||||
|
||||
println("🔎 [$stockName] RAG 검색된 문서 개수: ${finalSearchResult.matches().size}개")
|
||||
finalSearchResult.matches().forEach { match ->
|
||||
println("📊 [RAG Score: ${match.score()}] 본문: ${match.embedded().text().replace("\n", " ").take(50)}...")
|
||||
}
|
||||
|
||||
tradingDecision.newsContext = finalSearchResult.matches().joinToString("\n") { it.embedded().text() }
|
||||
val ragDuration = System.currentTimeMillis() - ragStartTime
|
||||
println("⏱️ [$stockName] RAG 뉴스 검색 소요: ${ragDuration}ms")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user