This commit is contained in:
2026-10-06 17:18:49 +09:00
parent 4b1805b9b8
commit dab3ab7993
2 changed files with 38 additions and 46 deletions
+21 -21
View File
@@ -196,7 +196,7 @@ object AutoTradingManager {
ts: TradingDecision, ts: TradingDecision,
totalScore: Double, totalScore: Double,
confidence: Double, confidence: Double,
finScore100: Double // 💡 [수정1] 컴파일 에러 방지용 파라미터 추가 finScore100: Double
): InvestmentGrade { ): InvestmentGrade {
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
val minConfidence = minScore val minConfidence = minScore
@@ -209,51 +209,52 @@ object AutoTradingManager {
val midLongAvg = (ts.midTermScore + ts.longTermScore) / 2.0 val midLongAvg = (ts.midTermScore + ts.longTermScore) / 2.0
val isOverheated = ts.analyzer?.isOverheatedStock() ?: true val isOverheated = ts.analyzer?.isOverheatedStock() ?: true
// 1. 기본 등급 산정 // 🌟 1. 기본 등급 산정 (현실적인 통계 범위로 컷오프 하향 조정)
var rawGrade = when { var rawGrade = when {
midLongAvg >= 70.0 -> { midLongAvg >= 65.0 -> { // 우량한 중장기 흐름 (기존 70 -> 65 완화)
if (shortAvg >= 75.0) InvestmentGrade.LEVEL_5_STRONG_RECOMMEND if (shortAvg >= 65.0) InvestmentGrade.LEVEL_5_STRONG_RECOMMEND // 강한 턴어라운드/수급 (기존 75 -> 65 완화)
else if (shortAvg >= 65.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND else if (shortAvg >= 50.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
else InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND else InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND // 눌림목 진입 초기
} }
midLongAvg >= 60.0 -> { midLongAvg >= 55.0 -> { // 무난한 흐름
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND if (shortAvg >= 65.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
else if (shortAvg >= 60.0) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND else if (shortAvg >= 50.0) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
else InvestmentGrade.LEVEL_2_HIGH_RISK else InvestmentGrade.LEVEL_2_HIGH_RISK
} }
else -> { else -> { // 역배열 또는 재무 하위
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_2_HIGH_RISK if (shortAvg >= 65.0) InvestmentGrade.LEVEL_2_HIGH_RISK
else InvestmentGrade.LEVEL_1_SPECULATIVE else InvestmentGrade.LEVEL_1_SPECULATIVE
} }
} }
// 💡 [수정2] 누락되었던 우량주 눌림목 프리미엄 & 잡주 투매 회피 로직 추가 // 🌟 2. [핵심] 우량주 황금 눌림목 프리미엄 (Premium)
val isHealthy = finScore100 >= 70.0 // 재무가 훌륭(75점 이상)하고 중장기 추세가 살아있는데, 단기 지표가 바닥을 찍고 막 고개를 드는(30~55점) 타점
val isPullback = midLongAvg >= 75.0 && shortAvg <= 45.0 val isHealthy = finScore100 >= 75.0
val isPullback = midLongAvg >= 70.0 && shortAvg in 30.0..55.0
if (isHealthy && isPullback) { if (isHealthy && isPullback) {
rawGrade = when (rawGrade) { rawGrade = when (rawGrade) {
InvestmentGrade.LEVEL_1_SPECULATIVE, // 💡 우량주가 완벽한 눌림목에서 반등을 시작하면 최고 등급(5등급)으로 승격!
InvestmentGrade.LEVEL_2_HIGH_RISK -> InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> InvestmentGrade.LEVEL_5_STRONG_RECOMMEND
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
InvestmentGrade.LEVEL_2_HIGH_RISK -> InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
else -> rawGrade else -> rawGrade
} }
} else if (!isHealthy && isPullback) { } else if (!isHealthy && isPullback) {
// 잡주인데 차트가 눌러앉은 경우 (지하실 위험으로 강등)
rawGrade = when (rawGrade) { rawGrade = when (rawGrade) {
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND, InvestmentGrade.LEVEL_5_STRONG_RECOMMEND,
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND, InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND,
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_1_SPECULATIVE InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_1_SPECULATIVE
InvestmentGrade.LEVEL_2_HIGH_RISK, InvestmentGrade.LEVEL_2_HIGH_RISK,
InvestmentGrade.LEVEL_1_SPECULATIVE -> InvestmentGrade.LEVEL_0_SPECULATIVE InvestmentGrade.LEVEL_1_SPECULATIVE -> InvestmentGrade.LEVEL_0_SPECULATIVE
else -> InvestmentGrade.LEVEL_0_SPECULATIVE else -> InvestmentGrade.LEVEL_0_SPECULATIVE
} }
} }
// 3. 과열 강등 로직 (급등주는 한 단계씩 낮춤)
val finalGrade = if (isOverheated) { val finalGrade = if (isOverheated) {
when (rawGrade) { when (rawGrade) {
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
@@ -265,7 +266,7 @@ object AutoTradingManager {
rawGrade rawGrade
} }
// 🌟 [추가] 사용자가 비활성화한 등급인지 체크하여 매수 원천 차단 // 4. 사용자가 UI에서 스위치로 꺼둔(비활성화) 등급인지 체크하여 매수 차단
val isEnabled = when (finalGrade) { val isEnabled = when (finalGrade) {
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> KisSession.tradeConfig.useGrade5 InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> KisSession.tradeConfig.useGrade5
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> KisSession.tradeConfig.useGrade4 InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> KisSession.tradeConfig.useGrade4
@@ -275,7 +276,6 @@ object AutoTradingManager {
else -> false else -> false
} }
// 비활성화된 등급이라면 LEVEL_0_SPECULATIVE(관망) 반환
return if (isEnabled) finalGrade else InvestmentGrade.LEVEL_0_SPECULATIVE return if (isEnabled) finalGrade else InvestmentGrade.LEVEL_0_SPECULATIVE
} }
+17 -25
View File
@@ -288,7 +288,7 @@ fun TradingDecisionLog() {
modifier = Modifier.fillMaxSize().background(Color.White) // fillMaxWidth() 대신 전체 채우기 modifier = Modifier.fillMaxSize().background(Color.White) // fillMaxWidth() 대신 전체 채우기
) { ) {
// ========================================== // ==========================================
// 1️⃣ 첫 번째 섹션: 2열 배치 구간 (span = 3) // 1️⃣ 첫 번째 섹션: 3열 배치 구간 (span = 2로 수정)
// ========================================== // ==========================================
item(span = { GridItemSpan(maxLineSpan) }) { // 6칸 모두 차지 item(span = { GridItemSpan(maxLineSpan) }) { // 6칸 모두 차지
@@ -298,16 +298,15 @@ fun TradingDecisionLog() {
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp) modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
) )
} }
// ConfigIndex.BUY_WEIGHT_INDEX,
var defaults = arrayOf( var defaults = arrayOf(
ConfigIndex.TAX_INDEX, ConfigIndex.PROFIT_INDEX, ConfigIndex.TAX_INDEX, ConfigIndex.PROFIT_INDEX,ConfigIndex.MIN_PURCHASE_SCORE_INDEX,
ConfigIndex.MAX_BUDGET_INDEX, ConfigIndex.MAX_PRICE_INDEX, ConfigIndex.MIN_PRICE_INDEX, ConfigIndex.MAX_BUDGET_INDEX, ConfigIndex.MAX_PRICE_INDEX, ConfigIndex.MIN_PRICE_INDEX,
ConfigIndex.MIN_PURCHASE_SCORE_INDEX, ConfigIndex.SELL_PROFIT, ConfigIndex.SELL_PROFIT, ConfigIndex.MAX_COUNT_INDEX, ConfigIndex.MAX_HOLDING_COUNT,
ConfigIndex.MAX_COUNT_INDEX, ConfigIndex.MAX_HOLDING_COUNT,
) )
// 💡 items에 span을 주어 3칸씩 차지하게 만듦 (결과적으로 2열 배치) // 🌟 [수정] GridItemSpan(3) -> GridItemSpan(2)로 변경하여 3열 배치
items(defaults.size, span = { GridItemSpan(3) }) { index -> items(defaults.size, span = { GridItemSpan(2) }) { index ->
val configKey = defaults.get(index) val configKey = defaults.get(index)
var localText by remember(configKey) { mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "") } var localText by remember(configKey) { mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "") }
@@ -355,28 +354,28 @@ fun TradingDecisionLog() {
} }
} }
// 💡 2열 배치니까 각각 span = 3 // 🌟 [수정] 모든 리스크 관리 아이템들도 GridItemSpan(3) 및 maxLineSpan -> GridItemSpan(2)로 통일
item(span = { GridItemSpan(3) }) { item(span = { GridItemSpan(2) }) {
SettingSwitchField( SettingSwitchField(
label = "보유 주식 자동 매도 활성화", label = "보유 주식 자동 매도 활성화",
initialChecked = KisSession.config.getValues(ConfigIndex.TAKE_PROFIT) > 0.0, initialChecked = KisSession.config.getValues(ConfigIndex.TAKE_PROFIT) > 0.0,
onCheckedChange = { onCheckedChange = {
KisSession.config.setValues(ConfigIndex.TAKE_PROFIT, if (it) 1.0 else 0.0) KisSession.config.setValues(ConfigIndex.TAKE_PROFIT, if (it) 1.0 else 0.0)
}, },
helperText = "목표 수익률 ${KisSession.config.SELL_PROFIT} 도달 시 기계적 매도" helperText = "수익 ${KisSession.config.SELL_PROFIT} 도달 매도" // 텍스트 간소화
) )
} }
item(span = { GridItemSpan(3) }) { item(span = { GridItemSpan(2) }) {
SettingSwitchField( SettingSwitchField(
label = "자동 손절 활성화", label = "자동 손절 활성화",
initialChecked = KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0, initialChecked = KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0,
onCheckedChange = { KisSession.config.setValues(ConfigIndex.STOP_LOSS, if (it) 1.0 else 0.0) }, onCheckedChange = { KisSession.config.setValues(ConfigIndex.STOP_LOSS, if (it) 1.0 else 0.0) },
helperText = "손실 방어선 도달 시 기계적 손절" helperText = "방어선 도달 시 기계적 손절"
) )
} }
item(span = { GridItemSpan(3) }) { item(span = { GridItemSpan(2) }) {
SettingInputField( SettingInputField(
label = "최소 손절 라인 (%)", label = "최소 손절 라인 (%)",
initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MINRATE).toString(), initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MINRATE).toString(),
@@ -386,7 +385,7 @@ fun TradingDecisionLog() {
) )
} }
item(span = { GridItemSpan(3) }) { item(span = { GridItemSpan(2) }) {
SettingInputField( SettingInputField(
label = "최대 허용 손절률 (%)", label = "최대 허용 손절률 (%)",
initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE).toString(), initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE).toString(),
@@ -396,27 +395,20 @@ fun TradingDecisionLog() {
) )
} }
// 💡 금액은 길게 뻗어야 하니 maxLineSpan // 🌟 금액 입력칸도 한 줄에 3개 나란히 배치되도록 변경
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(2) }) {
SettingInputField( SettingInputField(
label = "최대 허용 손실 금액 (원)", label = "최대 허용 손실 금액 (원)",
initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY).toLong().toString(), initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY).toLong().toString(),
placeholder = "50000", placeholder = "50000",
onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MAX_MONEY, it.toDoubleOrNull() ?: 0.0) }, onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MAX_MONEY, it.toDoubleOrNull() ?: 0.0) },
helperText = "1종목당 허용할 수 있는 최대 손실액 (원 단위)" helperText = "1종목당 최대 손실액" // 텍스트 간소화
) )
} }
item(span = { GridItemSpan(maxLineSpan) }) {
Column {
Spacer(modifier = Modifier.height(10.dp))
Divider(color = Color.LightGray, thickness = 1.dp)
Spacer(modifier = Modifier.height(20.dp))
}
}
// ========================================== // ==========================================
// 2️⃣ 두 번째 섹션: 3열 배치 구간 (span = 2) // 2️⃣ 두 번째 섹션: 3열 배치 구간 (기존 코드 그대로 유지)
// ========================================== // ==========================================
item(span = { GridItemSpan(maxLineSpan) }) { // 6칸 모두 차지 item(span = { GridItemSpan(maxLineSpan) }) { // 6칸 모두 차지
Text( Text(