This commit is contained in:
2026-04-08 14:18:09 +09:00
parent b95c1d5f72
commit 6494784bbc
7 changed files with 471 additions and 274 deletions
+207 -141
View File
@@ -7,10 +7,10 @@ import Defines.EMBEDDING_PORT
import Defines.LLM_PORT
import network.TradingDecision
import TradingLogStore
import analyzer.AdvancedTradeAssistant
import analyzer.TechnicalAnalyzer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import getLlamaBinPath
import kotlinx.coroutines.CoroutineScope
@@ -25,31 +25,27 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import model.CandleData
import model.ConfigIndex
import model.ExecutionData
import model.KisSession
import model.RankingStock
import model.RankingType
import model.UnifiedBalance
import model.UnifiedStockHolding
import network.DartCodeManager
import network.KisAuthService
import network.KisTradeService
import network.KisWebSocketManager
import network.RagService
import network.StockUniverseLoader
import org.jetbrains.skia.ImageFilter
import util.MarketUtil
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.atomic.AtomicLong
import kotlin.collections.List
import kotlin.math.*
// service/AutoTradingManager.kt
typealias TradingDecisionCallback = (TradingDecision?, Boolean)->Unit
object AutoTradingManager {
@@ -88,7 +84,7 @@ object AutoTradingManager {
val nowDate = LocalDate.now(seoulZone)
var checkTime = 60_000 * 3L
val isTradingDay = nowDate.dayOfWeek.value in 1..5
if (isTradingDay && now.isAfter(H08M30) && now.isBefore(H18) && !shouldShowFullWindow) {
if (isTradingDay && now.isAfter(H07M50) && now.isBefore(H18) && !shouldShowFullWindow) {
shouldShowFullWindow = true
SystemSleepPreventer.wakeDisplay()
} else if (now.isAfter(LocalTime.of(23, 50)) && now.isBefore(LocalTime.of(8, 0))) {
@@ -102,7 +98,7 @@ object AutoTradingManager {
}
}
// val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
// val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
// if (isSuccess && completeTradingDecision != null) {
// // 1. 로그 저장소에 기록 (UI에서 이걸 읽음)
// TradingLogStore.addLog(completeTradingDecision)
@@ -188,79 +184,91 @@ object AutoTradingManager {
// }
// }
// }
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
if (isSuccess && completeTradingDecision != null) {
val decision = completeTradingDecision
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
if (isSuccess && completeTradingDecision != null) {
val decision = completeTradingDecision
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (decision.decision == "BUY") {
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (decision.decision == "BUY") {
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
// 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)
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))
// 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)
TradingLogStore.addLog(decision,"BUY",decision.summary())
excuteTrade(
decision = decision,
orderQty = calculatedQty.toString(),
profitRate1 = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide),
investmentGrade = grade
)
} else if (decision.decision.equals("RETRY") || decision.confidence >= 60.0) { // 아까운 종목만 재분석
addToReanalysis(RankingStock(decision.stockCode, decision.stockName))
}
}
}
}
val MIN_CONFIDENCE = 60.0 // 최소 신뢰도
var append = 0.0
fun getInvestmentGrade(
ts: TradingDecision,
totalScore: Double,
confidence: Double
confidence: Double,
finScore100: Double // 💡 [수정1] 컴파일 에러 방지용 파라미터 추가
): InvestmentGrade {
// [개선] 하드코딩된 60/70 대신 사용자 설정 최소 점수를 기준으로 사용
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
val minConfidence = minScore // 신뢰도 하한선도 매수 기준 점수와 동기화
val minConfidence = minScore
// 1. 최소 기준 미달 시 (관망 대상)
if (totalScore < (minScore * 0.8) || confidence < minConfidence) {
return InvestmentGrade.LEVEL_1_SPECULATIVE
return InvestmentGrade.LEVEL_0_SPECULATIVE
}
// 2. 패턴 점수 추출
val shortAvg = (ts.ultraShortScore + ts.shortTermScore) / 2.0
val midLongAvg = (ts.midTermScore + ts.longTermScore) / 2.0
val isOverheated = ts.analyzer?.isOverheatedStock() ?: true
// 3. [개선] 점수 구간을 5~10점씩 하향 조정하여 실제 '추천' 등급이 나오도록 보
val rawGrade = when {
// [A그룹] 중장기 추세가 강한 상태
midLongAvg >= 70.0 -> { // 75 -> 70 하향
if (shortAvg >= 75.0) InvestmentGrade.LEVEL_5_STRONG_RECOMMEND // 80 -> 75
else if (shortAvg >= 65.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND // 70 -> 65
// 1. 기본 등급 산
var rawGrade = when {
midLongAvg >= 70.0 -> {
if (shortAvg >= 75.0) InvestmentGrade.LEVEL_5_STRONG_RECOMMEND
else if (shortAvg >= 65.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
else InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
}
// [B그룹] 중장기 추세가 보통인 상태
midLongAvg >= 60.0 -> { // 65 -> 60 하향
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND // 75 -> 70
else if (shortAvg >= 60.0) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND // 65 -> 60
midLongAvg >= 60.0 -> {
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
else if (shortAvg >= 60.0) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
else InvestmentGrade.LEVEL_2_HIGH_RISK
}
// [C그룹] 중장기는 약하지만 단기 에너지가 폭발적인 상태
else -> {
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_2_HIGH_RISK
else InvestmentGrade.LEVEL_1_SPECULATIVE
}
}
// 4. 단기 과열 패널티 (일괄 1단계 강등)
// 💡 [수정2] 누락되었던 우량주 눌림목 프리미엄 & 잡주 투매 회피 로직 추가
val isHealthy = finScore100 >= 70.0
val isPullback = midLongAvg >= 75.0 && shortAvg <= 45.0
if (isHealthy && isPullback) {
rawGrade = when (rawGrade) {
InvestmentGrade.LEVEL_1_SPECULATIVE,
InvestmentGrade.LEVEL_2_HIGH_RISK -> InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
else -> rawGrade
}
} else if (!isHealthy && isPullback) {
rawGrade = when (rawGrade) {
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND,
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND,
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_1_SPECULATIVE
InvestmentGrade.LEVEL_2_HIGH_RISK,
InvestmentGrade.LEVEL_1_SPECULATIVE -> InvestmentGrade.LEVEL_0_SPECULATIVE
else -> InvestmentGrade.LEVEL_0_SPECULATIVE
}
}
return if (isOverheated) {
when (rawGrade) {
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
@@ -273,10 +281,11 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
}
}
fun excuteTrade(decision: TradingDecision,orderQty: String, profitRate1: Double?,investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
scope.launch {
var basePrice = decision.currentPrice
val tickSize = MarketUtil.getTickSize(basePrice)
// 등급별 가이드에 따라 매수 호가 설정
val oneTickLowerPrice = basePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt())
var stockCode = decision.stockCode
var stockName = decision.stockName
@@ -284,15 +293,16 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
println("주문 성공: $realOrderNo ${stockCode} $orderQty $finalPrice")
TradingLogStore.addLog(decision,"BUY","주문 성공: $realOrderNo")
val pRate = 0.4
val sRate = -1.5
.onSuccess { realOrderNo ->
// 💡 [개선 1] 첫 번째 성공 로그에 등급 이름 추가
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 주문 성공: $realOrderNo")
// 손절 라인 하드코딩 (필요시 Config로 빼는 것 권장)
val sRate = -1.5
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
val effectiveProfitRate = maxOf(((profitRate1 ?: pRate) + tax), (KisSession.config.getValues(
ConfigIndex.PROFIT_INDEX) + tax))
// 최소 보장 수익률(전역 설정)과 요청 수익률 중 큰 값 선택 후 세금 더하기
val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
@@ -303,7 +313,7 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
code = stockCode,
name = stockName,
quantity = inputQty,
profitRate = effectiveProfitRate, // 보정된 수익률 저장
profitRate = effectiveProfitRate,
stopLossRate = sRate,
targetPrice = calculatedTarget,
stopLossPrice = calculatedStop,
@@ -311,7 +321,9 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
isDomestic = true
))
syncAndExecute(realOrderNo)
TradingLogStore.addLog(decision,"BUY","매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo")
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo")
}
.onFailure {
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
@@ -347,16 +359,13 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
// 1. 실제 매수 체결가 가져오기 (문자열인 경우 숫자로 변환)
val actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
// 2. 최소 마진 설정 (수수료/세금 0.3% + 순수익 1.5% = 1.8%)
val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX) + 0.05
val finalProfitRate = maxOf(dbItem.profitRate, absoluteMinRate)
val minEffectiveRate = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(ConfigIndex.TAX_INDEX)
// 3. DB에 설정된 목표 수익률과 최소 보장 수익률 중 큰 값 선택
val finalProfitRate = maxOf(dbItem.profitRate, minEffectiveRate)
// 4. 실제 체결가 기준 익절 가격 재계산 및 틱 사이즈 보정
// 3. 실제 체결가 기준 익절 가격 재계산 및 틱 사이즈 보정
val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% 적용)")
KisTradeService.postOrder(
@@ -422,10 +431,10 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) {
var targetPrice = holding.currentPrice.toDouble()
TradingLogStore.addAfterMarketLog(
holding.name,
holding.code,
"${if ("Y".equals(marketCode)) "시간외 단일가" else "대체거래소"} 시세로 ${holding.profitRate} 수익 예상"
)
holding.name,
holding.code,
"${if ("Y".equals(marketCode)) "시간외 단일가" else "대체거래소"} 시세로 ${holding.profitRate} 수익 예상"
)
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
@@ -452,6 +461,8 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
"🎊 시간외 단일가 주식 재고털이 주문 실패[${it.message}] "
)
}
} else {
analyzeDeepLossHoldingsAfterMarket(holding)
}
delay(300) // API 호출 부하 방지
}
@@ -460,68 +471,113 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) {
// if (isRunning()) return
val now = LocalTime.now()
val currentMinute = now.minute
// if (now.isBefore(H16) && now.isAfter(H08M35)) {
println("resumePendingSellOrders")
balance.holdings.forEach { holding ->
if (BLACKLISTEDSTOCKCODES.contains(holding.code)){
println("❌ 차단 처리된 주식 : ${holding.name}")
TradingLogStore.addAnalyzer(
holding.name,
holding.code,
"거랙 차단 대상 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
)
} else {
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) {
// println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ")
// 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송
var targetPrice = holding.currentPrice.toDouble()
val now = LocalTime.now()
val currentMinute = now.minute
var isBefore930 = false
if (now.hour == 9 && currentMinute < 30) {
targetPrice = targetPrice
isBefore930 = true
} else {
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
}
println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
price = targetPrice.toInt().toString(),
isBuy = false,
).onSuccess { newOrderNo ->
println("✅ [재주문 완료] ${holding.name}: $newOrderNo")
TradingLogStore.addSellLog(
holding.code,
targetPrice.toString(),
"SELL",
"🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료"
)
}.onFailure {
TradingLogStore.addSellLog(
holding.code,
targetPrice.toString(),
"SELL",
"🎊 보유 주식 매도 주문 실패[${it.message}] "
)
}
println("resumePendingSellOrders")
balance.holdings.forEach { holding ->
if (BLACKLISTEDSTOCKCODES.contains(holding.code)){
println("❌ 차단 처리된 주식 : ${holding.name}")
TradingLogStore.addAnalyzer(
holding.name,
holding.code,
"거랙 차단 대상 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
)
} else {
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) {
var targetPrice = holding.currentPrice.toDouble()
val now = LocalTime.now()
val currentMinute = now.minute
var isBefore930 = false
if (now.hour == 9 && currentMinute < 30) {
targetPrice = targetPrice
isBefore930 = true
} else {
TradingLogStore.addAnalyzer(
"보유주식[${holding.name}]",
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
}
println("🔄 [보유 주식 주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
price = targetPrice.toInt().toString(),
isBuy = false,
).onSuccess { newOrderNo ->
println("✅ [보유 주식 주문 완료] ${holding.name}: $newOrderNo")
TradingLogStore.addSellLog(
holding.code,
"수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
targetPrice.toString(),
"SELL",
"🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료"
)
}.onFailure {
TradingLogStore.addSellLog(
holding.code,
targetPrice.toString(),
"SELL",
"🎊 보유 주식 매도 주문 실패[${it.message}] "
)
}
delay(200) // API 호출 부하 방지
} else {
analyzeDeepLossHoldingsAfterMarket(holding)
}
delay(200) // API 호출 부하 방지
}
}
// }
}
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
val now = LocalTime.now()
val currentMinute = now.minute
if ((now.hour == 8 || now.hour == 16 || now.hour == 17)) {
val profit = holding.profitRate.toDouble()
val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다)
if (profit <= lossThreshold) {
println("🔍 [손실 종목 분석] ${holding.name} (수익률: $profit%) - 가이드 산출 중...")
// 차트 데이터 빠르게 가져오기 (일봉 위주로 큰 추세만 확인)
val dailyData = KisTradeService.fetchPeriodChartData(holding.code, "D", true).getOrNull()
if (!dailyData.isNullOrEmpty()) {
val analyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
val currentPrice = holding.currentPrice.toDouble()
// 1. 볼린저 밴드 하단선 (통계적 바닥) 확인
val lowerBand = AdvancedTradeAssistant.calculateBollingerLowerBand(dailyData)
// 2. RSI 확인 (과매도 투매 상태인지)
val rsiDaily = analyzer.calculateRSI(dailyData)
// 3. 중기 추세 확인 (최근 20일 기준 10% 이상 하락했는지)
val isTrendBroken = analyzer.calculateChange(dailyData.takeLast(20)) < -10.0
var advice = ""
// 🟢 [추매 타점] 볼린저 하단 터치(1.05배 이내) + RSI 과매도(35 이하) 구간
if (lowerBand > 0 && currentPrice <= lowerBand * 1.05 && rsiDaily < 35.0) {
advice = "📉 [추매 권장] 볼린저 밴드 하단 터치 및 RSI 과매도(${"%.1f".format(rsiDaily)}). 기술적 반등 확률이 매우 높은 통계적 바닥권입니다. (물타기 고려)"
}
// 🔴 [손절 타점] 추세가 완전히 깨졌는데, 바닥(볼린저 하단)까지 한참 남았을 때
else if (isTrendBroken && currentPrice > lowerBand * 1.1) {
advice = "🚨 [손절 경고] 20일 추세가 완전히 무너졌으며, 아직 바닥(하단 밴드)도 확인되지 않았습니다. 추가 하락(지하실) 위험이 크므로 리스크 관리(손절)가 필요합니다."
}
// 🟡 [관망] 어정쩡하게 물려있는 상태
else {
advice = "⏳ [관망 유지] 뚜렷한 반등 시그널(바닥)이나 치명적 투매 시그널이 없습니다. 조금 더 지켜봅니다."
}
// 분석 결과를 UI 로그에 띄워 대표님이 확인할 수 있게 함
TradingLogStore.addNotice(
"보유주식[${holding.name}]",
holding.code,
"수익률 심각($profit%) -> $advice",
)
}
} else {
// -5% 이내의 자잘한 손실은 별도 분석 없이 조용히 넘기거나 약식 로그만 남김
// TradingLogStore.addAnalyzer("보유주식[${holding.name}]", holding.code, "수익률 미달 대기중 (${profit}%)")
}
}
}
var isSystemReadyToday = false
var isSystemCleanedUpToday = false
private var lastRetryTime = 0L
@@ -567,9 +623,9 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
var waitTime = 0.2
val H16 = LocalTime.of(16, 0)
val H18 = LocalTime.of(18, 0)
val H08M35 = LocalTime.of(8, 0)
val H08M00 = LocalTime.of(8, 0)
val H08M45 = LocalTime.of(8, 45)
val H08M30 = LocalTime.of(7, 50)
val H07M50 = LocalTime.of(7, 50)
private fun runDiscoveryLoop(callback: TradingDecisionCallback) {
discoveryJob = scope.launch {
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
@@ -579,10 +635,10 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
currentTimeMillis = System.currentTimeMillis()
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
when {
now.isAfter(H18) || now.isBefore(H08M35) -> {
now.isAfter(H18) || now.isBefore(H08M00) -> {
prepareMarketOpen(now)
}
now.isBefore(H18) && now.isAfter(H08M35) -> {
now.isBefore(H18) && now.isAfter(H08M00) -> {
waitTime = 0.2
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
@@ -624,9 +680,10 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
}
suspend fun prepareMarketOpen(now : LocalTime) {
if (now.isAfter(H18) || now.isBefore(H08M30)) {
if (now.isAfter(H18) || now.isBefore(H07M50)) {
println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.")
onMarketClosed?.invoke()
RagService.clearDailyCache()
KisWebSocketManager.disconnect()
BrowserManager.closeIfIdle(0)
LlamaServerManager.stopAll() // AI 서버 완전 종료
@@ -634,7 +691,7 @@ val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boo
isSystemReadyToday = false
shouldShowFullWindow = false
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
} else if (now.isAfter(H08M30) && now.isBefore(H08M35) && !isSystemReadyToday) {
} else if (now.isAfter(H07M50) && now.isBefore(H08M00) && !isSystemReadyToday) {
if (MarketUtil.canTradeToday()) {
SystemSleepPreventer.wakeDisplay()
shouldShowFullWindow = true
@@ -1008,8 +1065,8 @@ enum class InvestmentGrade(
val allocationRate: ConfigIndex,
) {
LEVEL_5_STRONG_RECOMMEND(
displayName = "최상급 추천",
description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천",
displayName = "최상급 스윙/가치형",
description = "중장기 추세가 완벽하며 단기 파동까지 일치하는 매우 안정적인 매수 추천",
shortWeight = 1.0,
midWeight = 1.0,
longWeight = 1.0,
@@ -1018,8 +1075,8 @@ enum class InvestmentGrade(
allocationRate = ConfigIndex.GRADE_5_ALLOCATIONRATE,
),
LEVEL_4_BALANCED_RECOMMEND(
displayName = "균형 추천",
description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천",
displayName = "우량 균형형",
description = "기본적인 펀더멘털과 중장기 추세가 양호하여 꾸준한 우상향이 기대되는 종목",
shortWeight = 0.8,
midWeight = 1.0,
longWeight = 1.0,
@@ -1028,8 +1085,8 @@ enum class InvestmentGrade(
allocationRate = ConfigIndex.GRADE_4_ALLOCATIONRATE,
),
LEVEL_3_CAUTIOUS_RECOMMEND(
displayName = "보수적 추천",
description = "기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함",
displayName = "보수적 혼합형",
description = "중장기 지표는 양호하 단기 변동성이 있거나, 반대로 단기 수급만 몰린 팽팽한 상태",
shortWeight = 0.6,
midWeight = 1.0,
longWeight = 1.0,
@@ -1038,8 +1095,8 @@ enum class InvestmentGrade(
allocationRate = ConfigIndex.GRADE_3_ALLOCATIONRATE,
),
LEVEL_2_HIGH_RISK(
displayName = "고위험 추천",
description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자",
displayName = "고위험 단기 모멘텀",
description = "중장기 추세는 약하지만, 뉴스나 테마로 인해 단기 수급이 강력하게 붙은 스캘핑 대상",
shortWeight = 1.0,
midWeight = 0.4,
longWeight = 0.4,
@@ -1048,14 +1105,23 @@ enum class InvestmentGrade(
allocationRate = ConfigIndex.GRADE_2_ALLOCATIONRATE,
),
LEVEL_1_SPECULATIVE(
displayName = "순수 공격적 선택",
description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자",
displayName = "순수 투기/초단타",
description = "재무 및 중장기 지표 무관, 오직 초단기 분봉과 에너지만 살아있는 극도의 투기적 진입",
shortWeight = 1.0,
midWeight = 0.2,
longWeight = 0.2,
profitGuide = ConfigIndex.GRADE_1_PROFIT,
buyGuide = ConfigIndex.GRADE_1_BUY,
allocationRate = ConfigIndex.GRADE_1_ALLOCATIONRATE,
),
LEVEL_0_SPECULATIVE(
displayName = "매수 금지 (관망)",
description = "최소 신뢰도(Confidence) 미달로 시스템 통과 실패",
shortWeight = 0.1,
midWeight = 0.1,
longWeight = 0.1,
profitGuide = ConfigIndex.GRADE_1_PROFIT, // 더미 데이터
buyGuide = ConfigIndex.GRADE_1_BUY,
allocationRate = ConfigIndex.GRADE_1_ALLOCATIONRATE,
)
}