From 7a7de487a443436758a101eb0b0b25ea9655b0c0 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 6 Oct 2026 17:55:03 +0900 Subject: [PATCH] ... --- src/main/kotlin/network/RagService.kt | 2 +- src/main/kotlin/service/AutoTradingManager.kt | 8 +- src/main/kotlin/ui/TradingDecisionLog.kt | 376 +++++++++--------- 3 files changed, 188 insertions(+), 198 deletions(-) diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index fced423..ef506cd 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -528,7 +528,7 @@ object RagService { var finalDecision = "HOLD" var finalReason = "" - var grade = AutoTradingManager.getInvestmentGrade(tempDecision, totalScore, finalConfidence, finScore100) + var grade = AutoTradingManager.getInvestmentGrade(tempDecision, totalScore, finalConfidence, finScore100, tempDecision.isWatering) var assistantReason = "" diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index ed63674..51cbf60 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -196,7 +196,8 @@ object AutoTradingManager { ts: TradingDecision, totalScore: Double, confidence: Double, - finScore100: Double + finScore100: Double, + isWatering : Boolean = false ): InvestmentGrade { val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) val minConfidence = minScore @@ -266,7 +267,7 @@ object AutoTradingManager { rawGrade } - // 4. 사용자가 UI에서 스위치로 꺼둔(비활성화) 등급인지 체크하여 매수 차단 + // 4. 사용자가 UI에서 스위치로 꺼둔(비활성화) 등급인지 체크 val isEnabled = when (finalGrade) { InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> KisSession.tradeConfig.useGrade5 InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> KisSession.tradeConfig.useGrade4 @@ -276,7 +277,8 @@ object AutoTradingManager { else -> false } - return if (isEnabled) finalGrade else InvestmentGrade.LEVEL_0_SPECULATIVE + // 🌟 [핵심] 물타기 대상(isWatering == true)이면 스위치(isEnabled)가 꺼져 있어도 강제 승인(프리패스) + return if (isEnabled || isWatering) finalGrade else InvestmentGrade.LEVEL_0_SPECULATIVE } fun excuteTrade( diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index cd78704..ad81146 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -280,11 +280,11 @@ fun TradingDecisionLog() { } } } - Column(modifier = Modifier.weight(1f).padding(6.dp).fillMaxHeight().background(Color.White)) { + Column(modifier = Modifier.weight(1f).padding(3.dp).fillMaxHeight().background(Color.White)) { LazyVerticalGrid( columns = GridCells.Fixed(6), // 💡 2와 3의 최소공배수인 6열로 통합! - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalArrangement = Arrangement.spacedBy(3.dp), modifier = Modifier.fillMaxSize().background(Color.White) // fillMaxWidth() 대신 전체 채우기 ) { // ========================================== @@ -295,7 +295,7 @@ fun TradingDecisionLog() { Text( "💰 거래 기본 설정", style = MaterialTheme.typography.subtitle2, - modifier = Modifier.padding(top = 10.dp, bottom = 8.dp) + modifier = Modifier.padding(top = 3.dp, bottom = 3.dp) ) } @@ -305,51 +305,42 @@ fun TradingDecisionLog() { ConfigIndex.SELL_PROFIT, ConfigIndex.MAX_COUNT_INDEX, ConfigIndex.MAX_HOLDING_COUNT, ) - // 🌟 [수정] GridItemSpan(3) -> GridItemSpan(2)로 변경하여 3열 배치 items(defaults.size, span = { GridItemSpan(2) }) { index -> val configKey = defaults.get(index) var localText by remember(configKey) { mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "") } - val saveAction = { - var newValue = localText.toDoubleOrNull() ?: 0.0 - var oldValue = KisSession.config.getValues(configKey) - if (configKey.label.contains("PROFIT")) { - newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) - } - KisSession.config.setValues(configKey, newValue) - } - - var text = if (configKey.label.contains("PROFIT")) { + // 🌟 1. 초기값 계산 + val displayText = if (configKey.label.contains("PROFIT")) { "${(localText.toDoubleOrNull() ?: 1.0) * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}" } else { localText } - OutlinedTextField( - value = text, - onValueChange = { localText = it }, - label = { Text(configKey.label) }, - modifier = Modifier - .fillMaxWidth() - .onFocusChanged { focusState -> if (!focusState.isFocused) saveAction() }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, keyboardType = KeyboardType.Decimal), - keyboardActions = KeyboardActions(onDone = { saveAction() }), - singleLine = true + // 🌟 2. 직접 구현했던 OutlinedTextField를 SettingInputField로 깔끔하게 교체 + SettingInputField( + modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp), // 살짝 여백 추가 + label = configKey.label, + initialValue = displayText, + onSave = { newValueStr -> + localText = newValueStr + var newValue = newValueStr.toDoubleOrNull() ?: 0.0 + if (configKey.label.contains("PROFIT")) { + newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + } + KisSession.config.setValues(configKey, newValue) + } ) } // --- 🛡️ 수익 및 리스크 관리 섹션 --- item(span = { GridItemSpan(maxLineSpan) }) { Column { - Spacer(modifier = Modifier.height(20.dp)) - Divider(color = Color.LightGray, thickness = 1.dp) - Spacer(modifier = Modifier.height(10.dp)) Text( text = "🛡️ 수익 및 리스크 관리", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.ExtraBold, color = Color.DarkGray, - modifier = Modifier.padding(bottom = 12.dp) + modifier = Modifier.padding(bottom = 3.dp) ) } } @@ -375,6 +366,17 @@ fun TradingDecisionLog() { ) } + // 🌟 금액 입력칸도 한 줄에 3개 나란히 배치되도록 변경 + item(span = { GridItemSpan(2) }) { + SettingInputField( + label = "최대 허용 손실 금액 (원)", + initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY).toLong().toString(), + placeholder = "50000", + onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MAX_MONEY, it.toDoubleOrNull() ?: 0.0) }, + helperText = "1종목당 최대 손실액" // 텍스트 간소화 + ) + } + item(span = { GridItemSpan(2) }) { SettingInputField( label = "최소 손절 라인 (%)", @@ -395,16 +397,7 @@ fun TradingDecisionLog() { ) } - // 🌟 금액 입력칸도 한 줄에 3개 나란히 배치되도록 변경 - item(span = { GridItemSpan(2) }) { - SettingInputField( - label = "최대 허용 손실 금액 (원)", - initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY).toLong().toString(), - placeholder = "50000", - onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MAX_MONEY, it.toDoubleOrNull() ?: 0.0) }, - helperText = "1종목당 최대 손실액" // 텍스트 간소화 - ) - } + // ========================================== @@ -414,7 +407,7 @@ fun TradingDecisionLog() { Text( "💰 매수 정책 및 기대 수익률", style = MaterialTheme.typography.subtitle2, - modifier = Modifier.padding(top = 10.dp, bottom = 8.dp) + modifier = Modifier.padding(top = 3.dp, bottom = 3.dp) ) } @@ -434,8 +427,8 @@ fun TradingDecisionLog() { item(span = { GridItemSpan(maxLineSpan) }) { // 🌟 [핵심] 제목 텍스트 대신 사용 여부 스위치 부착 SettingSwitchField( - modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 8.dp), - label = "$common (LEVEL_$levelNum) 자동매수", + modifier = Modifier.fillMaxWidth().padding(vertical = 0.dp), + label = "$common (LEVEL_$levelNum) 자동매수(활성화시 매수 진행)", initialChecked = when (levelNum) { 5 -> tradeConfig.useGrade5 4 -> tradeConfig.useGrade4 @@ -452,8 +445,7 @@ fun TradingDecisionLog() { 1 -> tradeConfig.useGrade1 = isChecked } KisSession.saveTradeConfig() // 즉시 저장 - }, - helperText = "체크 해제 시 해당 등급 판정이 나와도 매수하지 않습니다." + } ) } @@ -461,16 +453,9 @@ fun TradingDecisionLog() { items(items.size, span = { GridItemSpan(2) }) { index -> val configKey = items.get(index) var localText by remember(configKey) { mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "") } - var labelText by remember(configKey) { mutableStateOf("") } - val saveAction = { - var oldValue = KisSession.config.getValues(configKey) - var newValue = localText.toDoubleOrNull() ?: 0.0 - KisSession.config.setValues(configKey, newValue) - } - - // labelText 업데이트 로직 - labelText = if (configKey.name.contains("PROFIT")) { + // 🌟 1. 라벨 텍스트 계산 + val labelText = if (configKey.name.contains("PROFIT")) { getRemaining(configKey.label, common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 비율(${KisSession.config.getValues(configKey)}) + 세금(${KisSession.config.getValues(ConfigIndex.TAX_INDEX)}) = ${(localText.toDoubleOrNull() ?: 0.0) * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(ConfigIndex.TAX_INDEX)}" } else if (configKey.name.contains("ALLOCATIONRATE")) { getRemaining(configKey.label, common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * (localText.toDoubleOrNull() ?: 0.0)}원" @@ -478,16 +463,16 @@ fun TradingDecisionLog() { getRemaining(configKey.label, common) + ": -${localText} 호가 매수" } - OutlinedTextField( - value = localText, - onValueChange = { localText = it }, - label = { Text(labelText) }, - modifier = Modifier - .fillMaxWidth() - .onFocusChanged { focusState -> if (!focusState.isFocused) saveAction() }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, keyboardType = KeyboardType.Decimal), - keyboardActions = KeyboardActions(onDone = { saveAction() }), - singleLine = true + // 🌟 2. 길고 복잡했던 코드를 SettingInputField 하나로 압축 + SettingInputField( + modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 0.dp), + label = labelText, + initialValue = localText, + onSave = { newValueStr -> + localText = newValueStr // 라벨 문구 실시간 업데이트용 + val newValue = newValueStr.toDoubleOrNull() ?: 0.0 + KisSession.config.setValues(configKey, newValue) + } ) } } @@ -495,60 +480,54 @@ fun TradingDecisionLog() { } Column(modifier = Modifier.weight(1f).padding(8.dp).fillMaxHeight().background(Color.White).verticalScroll(rememberScrollState())) { - Text("🕒 시간 및 매매 스케줄", style = MaterialTheme.typography.h6, modifier = Modifier.padding(8.dp)) + Text("🕒 시간 및 매매 스케줄", style = MaterialTheme.typography.subtitle2, modifier = Modifier.padding(3.dp)) - Card(elevation = 2.dp, modifier = Modifier.fillMaxWidth().padding(4.dp)) { - Column(modifier = Modifier.padding(12.dp)) { - Text("자동 매매 시간 설정", fontWeight = FontWeight.Bold, color = Color.Blue) - Spacer(Modifier.height(8.dp)) - // 🌟 [수정] 자동 시작/종료를 한 줄로 - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - TimeInputRow( - modifier = Modifier.weight(1f), - label = "자동 시작", - initialTime = tradeConfig.auto_start_time - ) { - tradeConfig.auto_start_time = it - KisSession.saveTradeConfig() - } - TimeInputRow( - modifier = Modifier.weight(1f), - label = "자동 종료", - initialTime = tradeConfig.auto_end_time - ) { - tradeConfig.auto_end_time = it - KisSession.saveTradeConfig() - } + Column(modifier = Modifier.padding(12.dp)) { + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + TimeInputRow( + modifier = Modifier.weight(1f), + label = "자동 시작", + initialTime = tradeConfig.auto_start_time + ) { + tradeConfig.auto_start_time = it + KisSession.saveTradeConfig() } + TimeInputRow( + modifier = Modifier.weight(1f), + label = "자동 종료", + initialTime = tradeConfig.auto_end_time + ) { + tradeConfig.auto_end_time = it + KisSession.saveTradeConfig() + } + } - Divider(Modifier.padding(vertical = 8.dp)) + Divider(Modifier.padding(vertical = 8.dp)) - // 🌟 [수정] 매수 시작/종료를 한 줄로 - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - TimeInputRow( - modifier = Modifier.weight(1f), - label = "매수 시작", - initialTime = tradeConfig.start_buy_time - ) { - tradeConfig.start_buy_time = it - KisSession.saveTradeConfig() - } - TimeInputRow( - modifier = Modifier.weight(1f), - label = "매수 종료", - initialTime = tradeConfig.end_buy_time - ) { - tradeConfig.end_buy_time = it - KisSession.saveTradeConfig() - } + // 🌟 [수정] 매수 시작/종료를 한 줄로 + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + TimeInputRow( + modifier = Modifier.weight(1f), + label = "매수 시작", + initialTime = tradeConfig.start_buy_time + ) { + tradeConfig.start_buy_time = it + KisSession.saveTradeConfig() + } + TimeInputRow( + modifier = Modifier.weight(1f), + label = "매수 종료", + initialTime = tradeConfig.end_buy_time + ) { + tradeConfig.end_buy_time = it + KisSession.saveTradeConfig() } } } - Spacer(Modifier.height(16.dp)) - Text("⚙️️ 기타 고급 설정", style = MaterialTheme.typography.h6, modifier = Modifier.padding(8.dp)) + Text("⚙️️ 기타 고급 설정", style = MaterialTheme.typography.subtitle2, modifier = Modifier.padding(3.dp)) Row(horizontalArrangement = Arrangement.SpaceEvenly) { SettingInputField( modifier = Modifier.weight(1.0f, true).padding(end = 4.dp), @@ -616,7 +595,7 @@ fun TradingDecisionLog() { Row(horizontalArrangement = Arrangement.SpaceEvenly) { SettingSwitchField ( - modifier = Modifier.weight(1.0f, true), + modifier = Modifier.weight(1.0f, true), label = "장 전 대체마켓 매도", initialChecked = tradeConfig.before_nxt, onCheckedChange = { @@ -655,7 +634,7 @@ fun TradingDecisionLog() { tradeConfig.tlg_id = it KisSession.saveTradeConfig() }, - helperText = "본인의 텔레그램 아뒤" + helperText = "본인의 텔레그램 아뒤" ) SettingSwitchField( label = "물타기", @@ -684,11 +663,9 @@ fun TradingDecisionLog() { }, helperText = "이것보다 작아야 삼" ) - } - Row(horizontalArrangement = Arrangement.SpaceEvenly) { SettingInputField( modifier = Modifier.weight(1.0f, true), - label = "물타기 기준 최소 보유 수량", + label = "물타기 최소 보유", initialValue = (tradeConfig.lowerAverageTargetCount).toString(), onSave = { tradeConfig.lowerAverageTargetCount = it.toInt() @@ -996,30 +973,31 @@ fun CsvDropZone( // } // } //} - @OptIn(ExperimentalMaterialApi::class) @Composable fun SettingInputField( modifier: Modifier? = null, label: String, - initialValue: String, // 💡 value -> initialValue 로 변경 + initialValue: String, placeholder: String = "", helperText: String = "", - onSave: (String) -> Unit // 💡 타자 칠 때마다가 아니라, 완료 시 저장하도록 콜백 변경 + onSave: (String) -> Unit ) { - // 💡 화면에 즉시 글자를 그려주기 위한 로컬 상태 (핵심 해결책) - var localText by remember { mutableStateOf(initialValue) } + // 💡 [수정 2] remember(initialValue)로 키를 지정하여 외부 설정 변경 시 UI 즉각 갱신 + var localText by remember(initialValue) { mutableStateOf(initialValue) } - Column(modifier = modifier ?: Modifier.fillMaxWidth()) { + Column( + modifier = (modifier ?: Modifier.fillMaxWidth()).height(80.dp), // 80.dp 통일 + verticalArrangement = Arrangement.Top + ) { OutlinedTextField( value = localText, - onValueChange = { localText = it }, // 타자 칠 때 화면 즉시 반영 + onValueChange = { localText = it }, label = { Text(label, fontWeight = FontWeight.Bold) }, placeholder = { Text(placeholder) }, modifier = Modifier .fillMaxWidth() .onFocusChanged { focusState -> - // 💡 포커스를 잃었을 때 (다른 칸을 클릭했을 때) 저장 if (!focusState.isFocused) { onSave(localText) } @@ -1030,22 +1008,17 @@ fun SettingInputField( keyboardType = KeyboardType.Decimal ), keyboardActions = KeyboardActions( - // 💡 모바일 키보드나 키보드에서 엔터(Done) 쳤을 때 저장 - onDone = { - onSave(localText) - } + onDone = { onSave(localText) } ) ) - if (helperText.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = helperText, - color = Color.Gray, - fontSize = 11.sp, - modifier = Modifier.padding(start = 4.dp) - ) - } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (helperText.isNotEmpty()) helperText else " ", + color = Color.Gray, + fontSize = 11.sp, + modifier = Modifier.padding(start = 4.dp) + ) } } @@ -1060,40 +1033,102 @@ fun SettingSwitchField( helperText: String = "", onCheckedChange: (Boolean) -> Unit ) { - // 💡 스위치 애니메이션을 즉각 보여주기 위한 로컬 상태 - var localChecked by remember { mutableStateOf(initialChecked) } + // 💡 [수정 2] remember(initialChecked)로 키를 지정하여 외부 변경 시 동기화 + var localChecked by remember(initialChecked) { mutableStateOf(initialChecked) } Column( - modifier = modifier + modifier = modifier.height(80.dp), // 80.dp 통일 + verticalArrangement = Arrangement.Top ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .height(52.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = label, fontWeight = FontWeight.Bold, + // 💡 [수정 3] 글씨가 너무 길면 한 줄로 유지하고 끝을 말줄임표(...)로 처리 + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) Switch( checked = localChecked, onCheckedChange = { isChecked -> - localChecked = isChecked // 화면 스위치 즉시 변경 - onCheckedChange(isChecked) // 실제 DB/설정 저장 트리거 + localChecked = isChecked + onCheckedChange(isChecked) } ) } - if (helperText.isNotEmpty()) { - Text( - text = helperText, - color = Color.Gray, - fontSize = 11.sp, - modifier = Modifier.padding(start = 2.dp) - ) - } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (helperText.isNotEmpty()) helperText else " ", + color = Color.Gray, + fontSize = 11.sp, + modifier = Modifier.padding(start = 2.dp) + ) } } + + +/** + * 시:분 입력을 위한 전용 컴포저블 + */ +@Composable +fun TimeInputRow( + modifier: Modifier = Modifier, + label: String, + initialTime: String, + helperText: String = "HH:mm 형식", + onTimeChange: (String) -> Unit +) { + var localText by remember(initialTime) { mutableStateOf(initialTime) } + + Column( + // 💡 [수정 1] 높이를 90.dp -> 80.dp로 수정하여 다른 필드들과 완벽한 줄맞춤 + modifier = modifier.height(80.dp).padding(horizontal = 2.dp), + verticalArrangement = Arrangement.Top + ) { + OutlinedTextField( + value = localText, + onValueChange = { + if (it.length <= 5) { + localText = it + } + }, + label = { Text(label, fontWeight = FontWeight.Bold) }, + placeholder = { Text("00:00") }, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focusState -> + if (!focusState.isFocused) { + onTimeChange(localText) + } + }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number + ), + keyboardActions = KeyboardActions( + onDone = { onTimeChange(localText) } + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (helperText.isNotEmpty()) helperText else " ", + color = Color.Gray, + fontSize = 11.sp, + modifier = Modifier.padding(start = 4.dp) + ) + } +} + @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable fun FilterChipWithRightClick( @@ -1166,50 +1201,3 @@ fun FilterChipWithRightClick( } } } - -/** - * 시:분 입력을 위한 전용 컴포저블 - */ -@Composable -fun TimeInputRow( - modifier: Modifier = Modifier, // 🌟 modifier 파라미터 추가 - label: String, - initialTime: String, - onTimeChange: (String) -> Unit -) { - var localText by remember(initialTime) { mutableStateOf(initialTime) } - - Row( - modifier = modifier.padding(vertical = 4.dp, horizontal = 4.dp), // 🌟 수정 - verticalAlignment = Alignment.CenterVertically - ) { - // 🌟 글자 크기와 너비를 약간 줄여서 한 줄에 두 개가 들어가도 예쁘게 설정 - Text(label, modifier = Modifier.width(65.dp), fontSize = 12.sp, fontWeight = FontWeight.Medium) - - OutlinedTextField( - value = localText, - onValueChange = { - if (it.length <= 5) { - localText = it - } - }, - placeholder = { Text("00:00") }, - modifier = Modifier - .weight(1f) - .onFocusChanged { focusState -> - if (!focusState.isFocused) { - onTimeChange(localText) - } - }, - singleLine = true, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number - ), - keyboardActions = KeyboardActions( - onDone = { onTimeChange(localText) } - ), - textStyle = androidx.compose.ui.text.TextStyle(fontSize = 13.sp) - ) - } -} \ No newline at end of file