This commit is contained in:
2026-04-07 18:07:18 +09:00
parent 9172cca791
commit 537aec9302
65 changed files with 132 additions and 94 deletions
@@ -1,5 +1,6 @@
package analyzer
import kotlinx.serialization.Serializable
import model.CandleData
import service.Candle
import java.time.LocalDateTime
@@ -7,6 +8,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.math.*
@Serializable
data class ScalpingSignalModel(
val currentPrice: Double,
val buySignal: Boolean,
+9 -6
View File
@@ -7,6 +7,7 @@ import analyzer.FinancialAnalyzer
import analyzer.FinancialMapper
import analyzer.FinancialStatement
import analyzer.InvestmentScores
import analyzer.ScalpingSignalModel
import analyzer.TechnicalAnalyzer
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
import dev.langchain4j.data.document.Metadata
@@ -248,9 +249,9 @@ object RagService {
val techStartTime = System.currentTimeMillis()
val financialScore = FinancialAnalyzer.calculateScore(financialStmt)
val scores = technicalAnalyzer.calculateScores(financialScore)
val techSignal = technicalAnalyzer.generateComprehensiveSignal()
tradingDecision.signalModel = technicalAnalyzer.generateComprehensiveSignal()
val techDuration = System.currentTimeMillis() - techStartTime
println("techSignal.compositeScore ${techSignal.compositeScore}")
println("techSignal.compositeScore ${tradingDecision.signalModel}")
if (!FinancialAnalyzer.isSafetyBeltMet(financialStmt)) {
logTime(stockName, "재무 미달 조기 종료", finDuration, System.currentTimeMillis() - totalStartTime)
@@ -258,7 +259,7 @@ object RagService {
return@coroutineScope
}
if (techSignal.compositeScore < 50) {
if ((tradingDecision.signalModel?.compositeScore ?: 0) < 50) {
logTime(stockName, "기술 점수 미달 조기 종료", techDuration, System.currentTimeMillis() - totalStartTime)
result(tradingDecision.apply { decision = "HOLD"; reason = "매수 타점 미도달" }, false)
return@coroutineScope
@@ -282,7 +283,9 @@ object RagService {
)
// 3. 검색된 내용을 하나의 문자열로 합쳐서 전달
tradingDecision.newsContext = finalSearchResult.matches().joinToString("\n\n") {
tradingDecision.newsContext = finalSearchResult.matches().distinct() // 중복 제거
.take(4) // 10개에서 4개로 축소
.joinToString("\n\n") {
it.embedded().text()
}
@@ -431,8 +434,7 @@ object RagService {
// 1-2. 기술 분석 및 리포트 생성 시간 측정
val techStartTime = System.currentTimeMillis()
val techSignal = tempDecision.analyzer?.generateComprehensiveSignal()
val techScore100 = techSignal?.compositeScore?.toDouble() ?: 0.0
val techScore100 = tempDecision.signalModel?.compositeScore?.toDouble() ?: 0.0
val isOverheated = tempDecision.analyzer?.isOverheatedStock() ?: false
tempDecision.techSummary = tempDecision.analyzer?.generateComprehensiveReport(finScore100.toInt())
val techDuration = System.currentTimeMillis() - techStartTime
@@ -611,6 +613,7 @@ class TradingDecision {
var newsContext : String? = null
var financialData : String? = null
var analyzer : TechnicalAnalyzer? = null
var signalModel : ScalpingSignalModel? = null
fun shortPossible() =
listOf<Double>(ultraShortScore,
+107 -81
View File
@@ -102,93 +102,119 @@ object AutoTradingManager {
}
}
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
if (isSuccess && completeTradingDecision != null) {
// 1. 로그 저장소에 기록 (UI에서 이걸 읽음)
TradingLogStore.addLog(completeTradingDecision)
// 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) {
val decision = 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)
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (decision.decision == "BUY") {
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
fun resultCheck(completeTradingDecision :TradingDecision) {
val weights = mapOf(
"short" to 0.2, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
"profit" to 0.4,
"safe" to 0.4 // 중장기 점수 비중 강화
)
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
val totalScore =
((completeTradingDecision.shortPossible() + append) * weights["short"]!!) +
((completeTradingDecision.profitPossible() + append) * weights["profit"]!!) +
((completeTradingDecision.safePossible() + append) * weights["safe"]!!)
// 2. 최종 매수 실행
val gradeRate = KisSession.config.getValues(grade.allocationRate)
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
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(
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}")
}
}
}
excuteTrade(
decision = decision,
orderQty = calculatedQty.toString(),
profitRate1 = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide),
investmentGrade = grade
)
} else if (decision.confidence >= 60.0) { // 아까운 종목만 재분석
addToReanalysis(RankingStock(decision.stockCode, decision.stockName))
}
}
}
val MIN_CONFIDENCE = 60.0 // 최소 신뢰도
var append = 0.0
+14 -7
View File
@@ -83,12 +83,12 @@ object LlamaServerManager {
val cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일
val hasGpu = HardwareDetector.hasNvidiaGpu()
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
if(HardwareDetector.getCpuName().contains("i7")) {
optimalGpuLayers = 0
}
var optimalGpuLayers = 99 //if ((isWin && hasGpu) || isMacArm) 99 else 4
// if(HardwareDetector.getCpuName().contains("i7")) {
// optimalGpuLayers = 0
// }
println("🖥️ OS: $os / Arch: $arch")
println("⚙️ 할당 스레드: $optimalThreads (Core: $cpuCores, Ratio: $ratio)")
println("🚀 GPU 레이어: $optimalGpuLayers (NVIDIA/MacArm: ${if(optimalGpuLayers == 99) "YES" else "NO"})")
@@ -97,12 +97,14 @@ object LlamaServerManager {
binPath,
"-m", modelPath,
"--port", port.toString(),
"-c", if (port == EMBEDDING_PORT) "512" else "8192",
"-c", if (port == EMBEDDING_PORT) "2048" else "8192",
"-ngl", optimalGpuLayers.toString(),
"-t", optimalThreads.toString(),
"--embedding",
"--mlock", // RAM 고정으로 스왑 방지
"--no-mmap"
"--no-mmap",
)
if (port != EMBEDDING_PORT) { // 텍스트 생성용 모델에만 적용
command.addAll(listOf(
@@ -112,6 +114,11 @@ object LlamaServerManager {
"--cont-batching"
))
}
if (isWin) {
command.addAll(listOf(
"--numa", "distribute"
))
}
scope.launch {
try {
val pb = ProcessBuilder(command)