This commit is contained in:
2026-10-06 17:55:03 +09:00
parent dab3ab7993
commit 7a7de487a4
3 changed files with 188 additions and 198 deletions
+1 -1
View File
@@ -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 = ""
@@ -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(
+133 -145
View File
@@ -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,14 +480,10 @@ 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),
@@ -544,11 +525,9 @@ fun TradingDecisionLog() {
}
}
}
}
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),
@@ -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,24 +1008,19 @@ 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,
text = if (helperText.isNotEmpty()) helperText else " ",
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.padding(start = 4.dp)
)
}
}
}
@Composable
@@ -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()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = helperText,
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)
)
}
}