This commit is contained in:
2026-03-16 17:07:25 +09:00
parent 1552020d31
commit 44e14dd207
4 changed files with 9241 additions and 55 deletions
+39 -43
View File
@@ -26,6 +26,15 @@ import service.UrlCacheManager
import java.nio.file.Paths
import java.time.Duration
interface TradingAnalyst {
@dev.langchain4j.service.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 {
// 임베딩 모델 (8081) 및 채팅 모델 (8080) 설정
@@ -39,8 +48,15 @@ object RagService {
.apiKey("unused")
.temperature(0.0) // [중요] 0.0으로 설정하여 결정론적 응답 유도
.timeout(Duration.ofSeconds(60))
.frequencyPenalty(1.1)
.maxTokens(500) // 👈 루프 방지를 위해 반드시 짧게 제한!
// 1.x 버전에서는 responseFormat이 아래처럼 바뀔 수 있으니 체크하세요
.responseFormat("json_object")
.build()
private val analyst = dev.langchain4j.service.AiServices.builder(TradingAnalyst::class.java)
.chatModel(chatModel)
.build()
private val embeddingStore: LuceneEmbeddingStore by lazy {
val path = Paths.get("db/lucene_idx")
@@ -63,6 +79,7 @@ object RagService {
}
/**
* 텍스트를 임베딩하여 H2 DB에 저장합니다.
*/
@@ -255,52 +272,31 @@ object RagService {
tempDecision: TradingDecision
): TradingDecision? {
val prompt = """
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
당신은 정량적 수치와 정성적 뉴스를 통합 분석하는 'AI 수석 애널리스트'입니다.
시스템이 계산한 지표 점수와 실제 재무제표 요약본을 바탕으로 최종 매매 전략을 수립하십시오.
# Task: Senior AI Investment Analyst
Analyze the stock '$stockName' and determine the final trading decision based on the data below.
[종목 정보]
- 종목명: $stockName
# Data
1. System Scores: Scalping(${scores.ultraShort}), Short(${scores.shortTerm}), Mid(${scores.midTerm}), Long(${scores.longTerm})
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. News Context: ${tempDecision.newsContext?.take(400)} // 👈 뉴스 길이를 물리적으로 제한
[1. 시스템 산출 스코어 (0-100)]
- 초단기(Scalping): ${scores.ultraShort}
- 단기(Daily): ${scores.shortTerm}
- 중기(Weekly): ${scores.midTerm}
- 장기(Monthly): ${scores.longTerm}
# Constraints
1. 모든 점수와 confidence는 0에서 100 사이의 **정수(Integer)**로만 작성하십시오.
- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD).
- Synchronization: High scalping score + positive news momentum = Higher BUY confidence.
- Output: Response ONLY in valid JSON format. No extra text.
[2. 핵심 재무제표 요약]
- 영업이익: ${if(financialStmt.isOperatingProfitPositive) "흑자" else "적자"} (성장률: ${"%.2f".format(financialStmt.operatingProfitGrowth)}%)
- 당기순이익: ${if(financialStmt.isNetIncomePositive) "흑자" else "적자"} (성장률: ${"%.2f".format(financialStmt.netIncomeGrowth)}%)
- 수익성(ROE): ${"%.2f".format(financialStmt.roe)}%
- 안정성(부채비율): ${"%.2f".format(financialStmt.debtRatio)}%
- 유동성(당좌비율): ${"%.2f".format(financialStmt.quickRatio)}%
[3. 시장 심리 및 뉴스 컨텍스트]
${tempDecision.newsContext}
[분석 지침]
1. **재무-뉴스 정합성**: 재무제표상 영업이익이 적자임에도 뉴스가 장기적 장밋빛 전망만 내놓는다면 '신중(HOLD)' 의견을 제시하십시오.
2. **기술-심리 동기화**: 초단기 점수가 높고 뉴스에서 수급 급증 키워드가 포착되면 'BUY' 신뢰도를 높이십시오.
3. **종합 결정**: 모든 수치와 컨텍스트를 고려하여 최종 Decision을 내리고, 그 근거를 핵심만 기술하십시오.
[응답 지침]
- JSON 데이터만 출력하십시오. 설명이나 서론은 생략합니다.
- 반드시 아래 형식을 엄격히 준수하십시오.
{
"ultraShortScore": ${scores.ultraShort},
"shortTermScore": ${scores.shortTerm},
"midTermScore": ${scores.midTerm},
"longTermScore": ${scores.longTerm},
"decision": "BUY" | "SELL" | "HOLD",
"reason": "재무 수치와 뉴스 심리를 대조한 최종 결론 한 줄",
"confidence": 0~100
}
<|eot_id|>
<|start_header_id|>user<|end_header_id|>
상기 데이터를 통합 분석하여 최종 리포트를 생성하십시오.
<|eot_id|><|start_header_id|>assistant<|end_header_id|>
""".trimIndent()
# Output JSON Format (Reason must be in Korean)
{
"ultraShortScore": ${scores.ultraShort},
"shortTermScore": ${scores.shortTerm},
"midTermScore": ${scores.midTerm},
"longTermScore": ${scores.longTerm},
"decision": "BUY/SELL/HOLD",
"reason": "재무와 뉴스를 대조한 분석 결과 (한국어)",
"confidence": 0~100
}
""".trimIndent()
val response = chatModel.chat(UserMessage.from(prompt))
val rawResponse = response.aiMessage().text()