...
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user