This commit is contained in:
lunaticbum 2026-04-07 18:07:18 +09:00
parent 9172cca791
commit 537aec9302
65 changed files with 132 additions and 94 deletions

View File

@ -1,5 +1,6 @@
package analyzer package analyzer
import kotlinx.serialization.Serializable
import model.CandleData import model.CandleData
import service.Candle import service.Candle
import java.time.LocalDateTime import java.time.LocalDateTime
@ -7,6 +8,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.* import kotlin.math.*
@Serializable
data class ScalpingSignalModel( data class ScalpingSignalModel(
val currentPrice: Double, val currentPrice: Double,
val buySignal: Boolean, val buySignal: Boolean,

View File

@ -7,6 +7,7 @@ import analyzer.FinancialAnalyzer
import analyzer.FinancialMapper import analyzer.FinancialMapper
import analyzer.FinancialStatement import analyzer.FinancialStatement
import analyzer.InvestmentScores import analyzer.InvestmentScores
import analyzer.ScalpingSignalModel
import analyzer.TechnicalAnalyzer import analyzer.TechnicalAnalyzer
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
import dev.langchain4j.data.document.Metadata import dev.langchain4j.data.document.Metadata
@ -248,9 +249,9 @@ object RagService {
val techStartTime = System.currentTimeMillis() val techStartTime = System.currentTimeMillis()
val financialScore = FinancialAnalyzer.calculateScore(financialStmt) val financialScore = FinancialAnalyzer.calculateScore(financialStmt)
val scores = technicalAnalyzer.calculateScores(financialScore) val scores = technicalAnalyzer.calculateScores(financialScore)
val techSignal = technicalAnalyzer.generateComprehensiveSignal() tradingDecision.signalModel = technicalAnalyzer.generateComprehensiveSignal()
val techDuration = System.currentTimeMillis() - techStartTime val techDuration = System.currentTimeMillis() - techStartTime
println("techSignal.compositeScore ${techSignal.compositeScore}") println("techSignal.compositeScore ${tradingDecision.signalModel}")
if (!FinancialAnalyzer.isSafetyBeltMet(financialStmt)) { if (!FinancialAnalyzer.isSafetyBeltMet(financialStmt)) {
logTime(stockName, "재무 미달 조기 종료", finDuration, System.currentTimeMillis() - totalStartTime) logTime(stockName, "재무 미달 조기 종료", finDuration, System.currentTimeMillis() - totalStartTime)
@ -258,7 +259,7 @@ object RagService {
return@coroutineScope return@coroutineScope
} }
if (techSignal.compositeScore < 50) { if ((tradingDecision.signalModel?.compositeScore ?: 0) < 50) {
logTime(stockName, "기술 점수 미달 조기 종료", techDuration, System.currentTimeMillis() - totalStartTime) logTime(stockName, "기술 점수 미달 조기 종료", techDuration, System.currentTimeMillis() - totalStartTime)
result(tradingDecision.apply { decision = "HOLD"; reason = "매수 타점 미도달" }, false) result(tradingDecision.apply { decision = "HOLD"; reason = "매수 타점 미도달" }, false)
return@coroutineScope return@coroutineScope
@ -282,7 +283,9 @@ object RagService {
) )
// 3. 검색된 내용을 하나의 문자열로 합쳐서 전달 // 3. 검색된 내용을 하나의 문자열로 합쳐서 전달
tradingDecision.newsContext = finalSearchResult.matches().joinToString("\n\n") { tradingDecision.newsContext = finalSearchResult.matches().distinct() // 중복 제거
.take(4) // 10개에서 4개로 축소
.joinToString("\n\n") {
it.embedded().text() it.embedded().text()
} }
@ -431,8 +434,7 @@ object RagService {
// 1-2. 기술 분석 및 리포트 생성 시간 측정 // 1-2. 기술 분석 및 리포트 생성 시간 측정
val techStartTime = System.currentTimeMillis() val techStartTime = System.currentTimeMillis()
val techSignal = tempDecision.analyzer?.generateComprehensiveSignal() val techScore100 = tempDecision.signalModel?.compositeScore?.toDouble() ?: 0.0
val techScore100 = techSignal?.compositeScore?.toDouble() ?: 0.0
val isOverheated = tempDecision.analyzer?.isOverheatedStock() ?: false val isOverheated = tempDecision.analyzer?.isOverheatedStock() ?: false
tempDecision.techSummary = tempDecision.analyzer?.generateComprehensiveReport(finScore100.toInt()) tempDecision.techSummary = tempDecision.analyzer?.generateComprehensiveReport(finScore100.toInt())
val techDuration = System.currentTimeMillis() - techStartTime val techDuration = System.currentTimeMillis() - techStartTime
@ -611,6 +613,7 @@ class TradingDecision {
var newsContext : String? = null var newsContext : String? = null
var financialData : String? = null var financialData : String? = null
var analyzer : TechnicalAnalyzer? = null var analyzer : TechnicalAnalyzer? = null
var signalModel : ScalpingSignalModel? = null
fun shortPossible() = fun shortPossible() =
listOf<Double>(ultraShortScore, listOf<Double>(ultraShortScore,

View File

@ -102,93 +102,119 @@ object AutoTradingManager {
} }
} }
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean -> // val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
// if (isSuccess && completeTradingDecision != null) {
// // 1. 로그 저장소에 기록 (UI에서 이걸 읽음)
// TradingLogStore.addLog(completeTradingDecision)
//
// println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}")
// if (completeTradingDecision.confidence < 10) {
// addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
// TradingLogStore.addLog(completeTradingDecision,"RETRY","분석 신뢰도 오류 인지로 재분석 대기열에 추가")
// }else if (completeTradingDecision != null && !completeTradingDecision.stockCode.isNullOrEmpty()) {
// var basePrice = completeTradingDecision.currentPrice
// var stockCode = completeTradingDecision.stockCode
// println("basePrice $basePrice")
// val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
// var maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
// val buyWeight = KisSession.config.getValues(ConfigIndex.BUY_WEIGHT_INDEX)
// val baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
//
// fun resultCheck(completeTradingDecision :TradingDecision) {
// val weights = mapOf(
// "short" to 0.2, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
// "profit" to 0.4,
// "safe" to 0.4 // 중장기 점수 비중 강화
// )
//
// val totalScore =
// ((completeTradingDecision.shortPossible() + append) * weights["short"]!!) +
// ((completeTradingDecision.profitPossible() + append) * weights["profit"]!!) +
// ((completeTradingDecision.safePossible() + append) * weights["safe"]!!)
//
// if (totalScore >= minScore && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
// var investmentGrade = completeTradingDecision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
//
// val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide)
// println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
//
// // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
// val gradeRate = KisSession.config.getValues(investmentGrade.allocationRate)
// val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt()
// maxBudget = maxBudget * gradeRate
// val calculatedQty = if (basePrice > 0) {
// (maxBudget / basePrice).toInt().coerceAtLeast(1)
// } else {
// 1
// }
// // 5. 매수 실행 (계산된 finalMargin 전달)
// excuteTrade(
// decision = completeTradingDecision,
// orderQty = min(calculatedQty, maxQty).toString(),
// profitRate1 = finalMargin,
// investmentGrade = investmentGrade,
// )
//
// } else if(totalScore >= (minScore * 0.9) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.9)) {
// addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
// TradingLogStore.addLog(completeTradingDecision,"RETRY","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
// } else {
// TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달")
// }
// }
// if (completeTradingDecision?.decision?.contains("매수") == true) {
// completeTradingDecision.decision = "BUY"
// }
// when (completeTradingDecision?.decision) {
// "BUY","매수" -> {
// append = buyWeight
// TradingLogStore.addLog(completeTradingDecision,"BUY","[$stockCode] 매수 추천 : ${completeTradingDecision?.reason}")
// resultCheck(completeTradingDecision)
// }
// "SELL" -> {
// TradingLogStore.addLog(completeTradingDecision,"SELL","[$stockCode] 매도 추천 : ${completeTradingDecision?.reason}")
// println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
// }
// "HOLD" -> {
// append = 0.0
// TradingLogStore.addLog(completeTradingDecision,"HOLD","[$stockCode] 관망 유지 : ${completeTradingDecision?.reason}")
// resultCheck(completeTradingDecision)
// }
// else -> {
// append = 0.0
// println("[$stockCode] ${completeTradingDecision?.decision} : ${completeTradingDecision?.reason}")
// }
// }
// }
// }
// }
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
if (isSuccess && completeTradingDecision != null) { if (isSuccess && completeTradingDecision != null) {
// 1. 로그 저장소에 기록 (UI에서 이걸 읽음) val decision = completeTradingDecision
TradingLogStore.addLog(completeTradingDecision)
println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}") // 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (completeTradingDecision.confidence < 10) { if (decision.decision == "BUY") {
addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
TradingLogStore.addLog(completeTradingDecision,"RETRY","분석 신뢰도 오류 인지로 재분석 대기열에 추가")
}else if (completeTradingDecision != null && !completeTradingDecision.stockCode.isNullOrEmpty()) {
var basePrice = completeTradingDecision.currentPrice
var stockCode = completeTradingDecision.stockCode
println("basePrice $basePrice")
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
var maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
val buyWeight = KisSession.config.getValues(ConfigIndex.BUY_WEIGHT_INDEX)
val baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
fun resultCheck(completeTradingDecision :TradingDecision) { // AI가 이미 검증한 등급을 사용 (재계산 불필요)
val weights = mapOf( val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
"short" to 0.2, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
"profit" to 0.4,
"safe" to 0.4 // 중장기 점수 비중 강화
)
val totalScore = // 2. 최종 매수 실행
((completeTradingDecision.shortPossible() + append) * weights["short"]!!) + val gradeRate = KisSession.config.getValues(grade.allocationRate)
((completeTradingDecision.profitPossible() + append) * weights["profit"]!!) + val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
((completeTradingDecision.safePossible() + append) * weights["safe"]!!) val calculatedQty = (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
if (totalScore >= minScore && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
var investmentGrade = completeTradingDecision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide)
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
val gradeRate = KisSession.config.getValues(investmentGrade.allocationRate)
val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt()
maxBudget = maxBudget * gradeRate
val calculatedQty = if (basePrice > 0) {
(maxBudget / basePrice).toInt().coerceAtLeast(1)
} else {
1
}
// 5. 매수 실행 (계산된 finalMargin 전달)
excuteTrade( excuteTrade(
decision = completeTradingDecision, decision = decision,
orderQty = min(calculatedQty, maxQty).toString(), orderQty = calculatedQty.toString(),
profitRate1 = finalMargin, profitRate1 = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide),
investmentGrade = investmentGrade, investmentGrade = grade
) )
} else if (decision.confidence >= 60.0) { // 아까운 종목만 재분석
} else if(totalScore >= (minScore * 0.9) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.9)) { addToReanalysis(RankingStock(decision.stockCode, decision.stockName))
addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
TradingLogStore.addLog(completeTradingDecision,"RETRY","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
} else {
TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달")
} }
} }
if (completeTradingDecision?.decision?.contains("매수") == true) { }
completeTradingDecision.decision = "BUY"
}
when (completeTradingDecision?.decision) {
"BUY","매수" -> {
append = buyWeight
TradingLogStore.addLog(completeTradingDecision,"BUY","[$stockCode] 매수 추천 : ${completeTradingDecision?.reason}")
resultCheck(completeTradingDecision)
}
"SELL" -> {
TradingLogStore.addLog(completeTradingDecision,"SELL","[$stockCode] 매도 추천 : ${completeTradingDecision?.reason}")
println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
}
"HOLD" -> {
append = 0.0
TradingLogStore.addLog(completeTradingDecision,"HOLD","[$stockCode] 관망 유지 : ${completeTradingDecision?.reason}")
resultCheck(completeTradingDecision)
}
else -> {
append = 0.0
println("[$stockCode] ${completeTradingDecision?.decision} : ${completeTradingDecision?.reason}")
}
}
}
}
}
val MIN_CONFIDENCE = 60.0 // 최소 신뢰도 val MIN_CONFIDENCE = 60.0 // 최소 신뢰도
var append = 0.0 var append = 0.0

View File

@ -83,12 +83,12 @@ object LlamaServerManager {
val cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일 val cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일
val hasGpu = HardwareDetector.hasNvidiaGpu() val hasGpu = HardwareDetector.hasNvidiaGpu()
val ratio = if (isWin) 0.5 else 0.7 val ratio = if (isWin) 0.5 else 0.7
val optimalThreads = (cpuCores * ratio).toInt().coerceIn(4, 16) val optimalThreads = if(isWin)(cpuCores * ratio).toInt().coerceIn(4, 12) else (cpuCores * ratio).toInt().coerceIn(4, 16)
var optimalGpuLayers = if ((isWin && hasGpu) || isMacArm) 99 else 4 var optimalGpuLayers = 99 //if ((isWin && hasGpu) || isMacArm) 99 else 4
if(HardwareDetector.getCpuName().contains("i7")) { // if(HardwareDetector.getCpuName().contains("i7")) {
optimalGpuLayers = 0 // optimalGpuLayers = 0
} // }
println("🖥️ OS: $os / Arch: $arch") println("🖥️ OS: $os / Arch: $arch")
println("⚙️ 할당 스레드: $optimalThreads (Core: $cpuCores, Ratio: $ratio)") println("⚙️ 할당 스레드: $optimalThreads (Core: $cpuCores, Ratio: $ratio)")
println("🚀 GPU 레이어: $optimalGpuLayers (NVIDIA/MacArm: ${if(optimalGpuLayers == 99) "YES" else "NO"})") println("🚀 GPU 레이어: $optimalGpuLayers (NVIDIA/MacArm: ${if(optimalGpuLayers == 99) "YES" else "NO"})")
@ -97,12 +97,14 @@ object LlamaServerManager {
binPath, binPath,
"-m", modelPath, "-m", modelPath,
"--port", port.toString(), "--port", port.toString(),
"-c", if (port == EMBEDDING_PORT) "512" else "8192", "-c", if (port == EMBEDDING_PORT) "2048" else "8192",
"-ngl", optimalGpuLayers.toString(), "-ngl", optimalGpuLayers.toString(),
"-t", optimalThreads.toString(), "-t", optimalThreads.toString(),
"--embedding", "--embedding",
"--mlock", // RAM 고정으로 스왑 방지 "--mlock", // RAM 고정으로 스왑 방지
"--no-mmap" "--no-mmap",
) )
if (port != EMBEDDING_PORT) { // 텍스트 생성용 모델에만 적용 if (port != EMBEDDING_PORT) { // 텍스트 생성용 모델에만 적용
command.addAll(listOf( command.addAll(listOf(
@ -112,6 +114,11 @@ object LlamaServerManager {
"--cont-batching" "--cont-batching"
)) ))
} }
if (isWin) {
command.addAll(listOf(
"--numa", "distribute"
))
}
scope.launch { scope.launch {
try { try {
val pb = ProcessBuilder(command) val pb = ProcessBuilder(command)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.