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
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,

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,

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

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)

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.