.
This commit is contained in:
parent
3d4f4d7412
commit
eee4f1bab7
@ -13,6 +13,7 @@ import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -296,10 +297,13 @@ object RagService {
|
||||
// 문자열 치환 대신 안전한 JSON 객체 빌더 사용
|
||||
val requestBodyJson = buildJsonObject {
|
||||
put("model", "local-model")
|
||||
put("temperature", 0.0) // 0.1 유지 (결정론적 응답)
|
||||
put("top_p", 0.9)
|
||||
put("max_tokens", 500)
|
||||
put("temperature", 0.1) // 💡 루프 탈출을 위해 더 과감하게 설정
|
||||
put("top_p", 0.85) // 💡 추가
|
||||
put("top_k", 40) // 💡 추가 (서버에서 지원할 경우)
|
||||
// put("frequency_penalty", 0.7) // 💡 반복 단어 억제 강화
|
||||
// put("presence_penalty", 0.5)
|
||||
|
||||
put("max_tokens", 400)
|
||||
putJsonArray("messages") {
|
||||
addJsonObject {
|
||||
put("role", "system")
|
||||
@ -339,94 +343,124 @@ object RagService {
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
suspend fun decideTrading(
|
||||
stockName: String,
|
||||
scores: InvestmentScores, // 직접 계산한 점수 객체
|
||||
financialStmt: FinancialStatement, // 매핑된 재무 수치 객체
|
||||
scores: InvestmentScores,
|
||||
financialStmt: FinancialStatement,
|
||||
tempDecision: TradingDecision
|
||||
): TradingDecision? {
|
||||
// 💡 1. 뉴스 데이터가 유효한지(100자 이상인지) 확인
|
||||
val validNews = tempDecision.newsContext?.takeIf { it.trim().length >= 100 }
|
||||
|
||||
// 💡 2. 동적 데이터 섹션 구성
|
||||
var retryCount = 0
|
||||
val maxRetries = 2
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
|
||||
// 1. 뉴스 데이터가 100자 이상일 때만 유효한 것으로 판단
|
||||
val validNews = tempDecision.newsContext?.takeIf { it.trim().length >= 100 }?.take(400)
|
||||
|
||||
// 2. 뉴스 유무에 따른 동적 데이터 섹션 구성
|
||||
val newsDataSection = if (validNews != null) {
|
||||
"3. News Context: $validNews"
|
||||
} else {
|
||||
"3. News Context: None available. Base your decision ONLY on System Scores and Financials."
|
||||
"3. News Context: No significant news available. Rely on financials."
|
||||
}
|
||||
|
||||
// 💡 3. 동적 제약 조건 구성
|
||||
val newsConstraint = if (validNews != null) {
|
||||
"- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD)."
|
||||
} else {
|
||||
"- No news data is available. Rely strictly on Financials and System Scores for your 'decision' and 'reason'."
|
||||
}
|
||||
|
||||
val prompt = """
|
||||
# Task: Senior AI Investment Analyst
|
||||
Analyze the stock '$stockName' and determine the final trading decision based on the data below.
|
||||
|
||||
# Data
|
||||
Your goal is to provide a final trading decision based on STRICT data analysis.
|
||||
|
||||
|
||||
# Data (SOURCE OF TRUTH)
|
||||
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)}%
|
||||
|
||||
$newsDataSection
|
||||
|
||||
# Constraints
|
||||
- Copy the exact 'System Scores' from the Data section into the output JSON.
|
||||
- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD).
|
||||
- The "reason" field MUST be written in KOREAN and MUST NOT exceed 50 characters. Keep it concise.
|
||||
- Output ONLY a valid JSON object matching the exact structure below. DO NOT output placeholder text like '(integer)'.
|
||||
|
||||
# Example Output JSON Format
|
||||
# Step-by-Step Analysis Logic
|
||||
|
||||
1. Financial Review: First, evaluate the 'Financials' section for long-term stability.
|
||||
|
||||
2. News Verification: Second, check 'News Context' (if available) for immediate market sentiment or specific issues.
|
||||
|
||||
3. Synthesis: Finalize the 'decision' (BUY, SELL, HOLD) by combining the Financials and News analysis.
|
||||
|
||||
4. Confidence: Assign a confidence score (0-100) based on how clearly the data points to the decision.
|
||||
|
||||
|
||||
# Strict Constraints
|
||||
|
||||
- SCORE INTEGRITY: You MUST copy the 'System Scores' into the output JSON exactly as provided. NO TRANSFORMATION.
|
||||
|
||||
- REASON LENGTH: The "reason" field MUST be written in KOREAN and MUST be between 10 to 50 characters.
|
||||
|
||||
- JSON ONLY: Output ONLY a valid JSON object. No markdown, no pre-text, no post-text.
|
||||
|
||||
|
||||
# Output JSON Structure (STRICT NAMES)
|
||||
|
||||
{
|
||||
"ultraShortScore": 0,
|
||||
"shortTermScore": 0,
|
||||
"midTermScore": 0,
|
||||
"longTermScore": 0,
|
||||
"ultraShortScore": ${scores.ultraShort},
|
||||
"shortTermScore": ${scores.shortTerm},
|
||||
"midTermScore": ${scores.midTerm},
|
||||
"longTermScore": ${scores.longTerm},
|
||||
"decision": "HOLD",
|
||||
"reason": "적자 지속 및 네수파립 임상 대기 중으로 관망 필요",
|
||||
"confidence": 50
|
||||
"reason": "10자 이상 50자 이내의 한국어 분석 결과",
|
||||
"confidence": 0
|
||||
}
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
return try {
|
||||
try {
|
||||
val rawResponse = callLlamaWithSchema(prompt)
|
||||
println("📥 [AI Strict JSON]:\n$rawResponse")
|
||||
// 환각 및 루프 검사
|
||||
println("rawResponse $rawResponse")
|
||||
|
||||
// 엄격한 스키마가 적용되었으므로 JsonSanitizer 없이 바로 파싱 가능
|
||||
val lenientJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
val decision = lenientJson.decodeFromString<TradingDecision>(rawResponse)
|
||||
val sanitized = rawResponse.trim().removeSurrounding("```json", "```").trim()
|
||||
|
||||
// 데이터 매핑
|
||||
decision.apply {
|
||||
financialData = tempDecision.financialData
|
||||
newsContext = tempDecision.newsContext
|
||||
techSummary = tempDecision.techSummary
|
||||
stockCode = tempDecision.stockCode
|
||||
corpName = tempDecision.corpName
|
||||
val decision = Json { ignoreUnknownKeys = true; isLenient = true }.decodeFromString<TradingDecision>(sanitized)
|
||||
|
||||
// 2. 사유 길이 및 데이터 정합성 검증 (사용자 요청 반영)
|
||||
val reasonLen = decision.reason?.length ?: 0
|
||||
val isReasonValid = reasonLen in 5..60 // 약간의 마진 허용
|
||||
|
||||
// 점수가 보존되었는지 확인 (Scalping 점수 대조)
|
||||
val isScorePreserved = decision.ultraShortScore == scores.ultraShort.toDouble()
|
||||
|
||||
if (isReasonValid && isScorePreserved) {
|
||||
return decision.apply {
|
||||
this.stockCode = tempDecision.stockCode
|
||||
this.stockName = tempDecision.stockName
|
||||
this.corpName = tempDecision.corpName
|
||||
this.financialData = tempDecision.financialData
|
||||
this.newsContext = tempDecision.newsContext
|
||||
}
|
||||
} else {
|
||||
println("⚠️ [검증 실패] 사유길이($reasonLen) 또는 점수보존($isScorePreserved) 실패. 재시도 합니다.")
|
||||
retryCount++
|
||||
}
|
||||
|
||||
decision
|
||||
} catch (e: InternalServerException) {
|
||||
// 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
|
||||
println("🚨 [AI Server Error] ${e.message}")
|
||||
if (e.message?.contains("Context size") == true) {
|
||||
println("⚠️ 데이터가 너무 많습니다. 요약 로직을 점검하세요.")
|
||||
}
|
||||
tempDecision
|
||||
null
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("❌ [General Error] ${e.message}")
|
||||
null
|
||||
println("❌ [파싱 오류] ${e.message} - 재시도 시도 중... (${retryCount + 1})")
|
||||
retryCount++
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
// 💡 [최종 탈출] 모든 재시도 실패 시 무한 루프를 돌지 않고 null 반환
|
||||
println("🚨 [시스템] $stockName 분석 재시도 횟수 초과. 분석을 스킵합니다.")
|
||||
return TradingDecision().apply {
|
||||
this.stockName = stockName
|
||||
this.decision = "HOLD"
|
||||
this.reason = "AI 분석 지연으로 인한 자동 관망 처리"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -462,7 +462,7 @@ object AutoTradingManager {
|
||||
}
|
||||
reanalysisList.clear()
|
||||
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList}
|
||||
.distinctBy { it.code }.shuffled())
|
||||
.distinctBy { it.code })
|
||||
} else {
|
||||
println("미확인 데이터 ${remainingCandidates.size}")
|
||||
// remainingCandidates.removeIf { it.code in myHoldings || it.code in pendingStocks || it.code in executionCache.values.map { it.code } || it.code in failList}
|
||||
|
||||
@ -15,11 +15,37 @@ object LlamaServerManager {
|
||||
private val processes = ConcurrentHashMap<Int, Process>()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
init {
|
||||
killZombieProcesses()
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
stopAll()
|
||||
})
|
||||
}
|
||||
|
||||
// OS 시스템 명령어를 이용해 찌꺼기 프로세스를 이름으로 찾아 강제 종료하는 함수
|
||||
private fun killZombieProcesses() {
|
||||
try {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
if (os.contains("win")) {
|
||||
// 윈도우: taskkill 명령어로 llama-server.exe 강제 종료 (/F: 강제, /T: 트리거된 자식까지)
|
||||
ProcessBuilder("cmd", "/c", "taskkill /F /IM llama-server.exe /T")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
.waitFor()
|
||||
println("🧹 [System] 이전 llama-server 좀비 프로세스 정리 완료 (Windows)")
|
||||
} else {
|
||||
// 맥/리눅스: pkill 사용
|
||||
ProcessBuilder("pkill", "-f", "llama-server")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
.waitFor()
|
||||
println("🧹 [System] 이전 llama-server 좀비 프로세스 정리 완료 (Mac/Linux)")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 실행 중인 프로세스가 없어서 에러가 나도 조용히 무시합니다.
|
||||
}
|
||||
}
|
||||
|
||||
fun startServer(binPath: String, modelPath: String, port: Int, nGpuLayers: Int = 99) {
|
||||
// 이미 해당 포트에서 실행 중이거나 모델 경로가 비었으면 무시합니다.
|
||||
if (processes.containsKey(port) || modelPath.isBlank()) return
|
||||
@ -28,7 +54,7 @@ object LlamaServerManager {
|
||||
val isWin = os.contains("win")
|
||||
val (nGpuLayers, threads) = when {
|
||||
os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64")) -> 99 to 8
|
||||
isWin -> 16 to 6 // NUC Core Ultra 7: GPU 레이어 40 내외, 스레드 12 권장
|
||||
isWin -> 4 to 12 // NUC Core Ultra 7: GPU 레이어 40 내외, 스레드 12 권장
|
||||
else -> 0 to 4 // 인텔 맥 2017 등
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user