diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index d5ac022..b3cf2b4 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -2,8 +2,9 @@ package model import java.time.LocalDateTime -const val feesAndTaxRate = 0.3 -const val minimumNetProfit = 0.8 +const val feesAndTaxRate = 0.33 +const val minimumNetProfit = 0.4 +const val buyWeight = 2.0 data class AppConfig( // [DB 저장 데이터] diff --git a/src/main/kotlin/ui/IntegratedOrderSection.kt b/src/main/kotlin/ui/IntegratedOrderSection.kt index ce86c3e..1fdd362 100644 --- a/src/main/kotlin/ui/IntegratedOrderSection.kt +++ b/src/main/kotlin/ui/IntegratedOrderSection.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch +import model.buyWeight import model.feesAndTaxRate import model.minimumNetProfit import network.KisTradeService @@ -72,7 +73,7 @@ fun IntegratedOrderSection( } var profitRate by remember(monitoringItem) { - mutableStateOf(monitoringItem?.profitRate?.toString() ?: "0.8") + mutableStateOf(monitoringItem?.profitRate?.toString() ?: minimumNetProfit.toString()) } var stopLossRate by remember(monitoringItem) { mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5") @@ -83,7 +84,7 @@ fun IntegratedOrderSection( val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0) val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 - fun excuteTrade(willEnableAutoSell: Boolean,orderQty: String) { + fun excuteTrade(willEnableAutoSell: Boolean, orderQty: String, profitRate1: Double?) { scope.launch { val tickSize = MarketUtil.getTickSize(basePrice) val oneTickLowerPrice = basePrice - tickSize @@ -108,7 +109,7 @@ fun IntegratedOrderSection( // 3. 실질 목표 수익률 계산 // 사용자가 입력한 pRate와 (최소 순수익 + 제반 비용) 중 큰 값을 선택합니다. - val effectiveProfitRate = maxOf(pRate, minimumNetProfit + feesAndTaxRate) + val effectiveProfitRate = maxOf((profitRate1 ?: pRate) + feesAndTaxRate, minimumNetProfit + feesAndTaxRate) // 4. 보정된 수익률을 적용하여 목표가 계산 val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0)) @@ -152,25 +153,50 @@ fun IntegratedOrderSection( profitPossible : ${completeTradingDecision.profitPossible()+ append} safePossible : ${completeTradingDecision.safePossible()+ append} """.trimIndent()) - // 2. 조건 검사: 신뢰도 80 이상 AND 중기 점수 70 이상 - if (completeTradingDecision.confidence + append >= MIN_CONFIDENCE && - completeTradingDecision.shortPossible() + append >= MIN_SHORT_SCORE && - completeTradingDecision.profitPossible() + append >= MIN_POSSIBLE_SCORE && - completeTradingDecision.safePossible() + append >= MIN_SAFE_SCORE - ) { - println("🚀 [조건 만족] 강력 매수 시그널 포착 -> 자동 매수 진행 (1주) ${completeTradingDecision.stockCode}") - // 3. 매수 실행 (자동 감시 켜기: true, 수량: 1주) - // 수량은 필요에 따라 로직으로 계산하여 변경 가능 (예: 자산의 10% 등) - excuteTrade(willEnableAutoSell = true, orderQty = "1") + val weights = mapOf( + "short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소 + "profit" to 0.3, + "safe" to 0.4 // 중장기 점수 비중 강화 + ) + +// 2. 토탈 스코어 계산 + val totalScore = + (completeTradingDecision.shortPossible() * weights["short"]!!) + + (completeTradingDecision.profitPossible() * weights["profit"]!!) + + (completeTradingDecision.safePossible() * weights["safe"]!!) + +// 3. 매수 결정 문턱값 (예: 70점 이상이면 매수 가능) + val MIN_PURCHASE_SCORE = 70.0 + val HIGH_QUALITY_SCORE = 85.0 // 강력 추천 기준 + + if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence > MIN_CONFIDENCE) { + + // 4. 점수에 따른 가변 마진 적용 + // 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용 + val finalMargin = if (totalScore >= HIGH_QUALITY_SCORE) { + println("💎 [우량주 포착] 토탈 스코어($totalScore)가 매우 높아 목표 마진을 3.0%로 상향합니다.") + minimumNetProfit + (append * 1.5) + } else { + minimumNetProfit + append + } + + println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") + + // 5. 매수 실행 (계산된 finalMargin 전달) + excuteTrade( + willEnableAutoSell = true, + orderQty = "1", + profitRate1 = finalMargin + ) } else { - println("✋ [조건 미달] 매수 의견이나 점수 부족으로 관망") + println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달") } } when (completeTradingDecision?.decision) { "BUY" -> { - append = 3.0 + append = buyWeight println("[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}") resultCheck(completeTradingDecision) } @@ -274,7 +300,7 @@ fun IntegratedOrderSection( // 매수 버튼 Button( onClick = { - excuteTrade(willEnableAutoSell,orderQty) + excuteTrade(willEnableAutoSell, orderQty, profitRate.toDouble()) }, modifier = Modifier.weight(1f).padding(end = 4.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFFE03E2D))