diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index 1020849..9b4de6e 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -70,7 +70,7 @@ object ConfigTable : Table("app_config") { val grade_2_allocationrate = double("grade_2_allocationrate").default(0.4) val grade_1_allocationrate = double("grade_1_allocationrate").default(0.3) - val take_profit = bool("take_profit").default(false) + val take_profit = bool("take_profit").default(true) val stop_Loss = bool("stop_Loss").default(false) val loss_minrate = double("loss_minrate").default(3.5) @@ -452,7 +452,7 @@ data class AutoTradeItem( var stopLossPrice: Double = 0.0, // 손절 목표가 // 수량 정보 - val quantity: Int = 0, // 총 주문 수량 + var quantity: Int = 0, // 총 주문 수량 var remainedQuantity: Int = 0, // 미체결 잔량 (서버 동기화용) val isDomestic: Boolean = true, diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 28f4658..af495bf 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -545,7 +545,7 @@ object AutoTradingManager { private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석 val now = LocalTime.now() val currentMinute = now.minute - if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 15 == 0 ))) { + if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute == 0 ))) { val profit = holding.profitRate.toDouble() val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다) if (profit <= lossThreshold) { @@ -764,7 +764,7 @@ object AutoTradingManager { } } - if (AUTOSELL) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) } + if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) } } else { } } @@ -843,35 +843,42 @@ object AutoTradingManager { } private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장 suspend fun sellSchedule() { + if (KisSession.config.take_profit == false) { - val now = LocalTime.now() - val currentMinute = now.minute - if (now.hour == 9 && (currentMinute % 10 == 1 || currentMinute % 10 == 7)) { - if (lastForceCheckMinute != currentMinute) { - TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true) - println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") - checkBalance() - lastForceCheckMinute = currentMinute // 실행 완료 기록 - } - } - else if((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) { - if (lastForceCheckMinute != currentMinute) { - TradingLogStore.addAnalyzer( - " - ", - " - ", - "⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.", - true - ) - var list = mutableListOf("X") - if (now.hour != 8 && now.hour < 18) { - list.add("Y") + } else { + val now = LocalTime.now() + val currentMinute = now.minute + if (now.hour == 9 && (currentMinute % 10 == 1 || currentMinute % 10 == 7)) { + if (lastForceCheckMinute != currentMinute) { + TradingLogStore.addAnalyzer( + " - ", + " - ", + "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", + true + ) + println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") + checkBalance() + lastForceCheckMinute = currentMinute // 실행 완료 기록 } - list.forEach { code -> - KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let { - sellingAfterMarketOnePrice(KisTradeService, it, code) + } else if ((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) { + if (lastForceCheckMinute != currentMinute) { + TradingLogStore.addAnalyzer( + " - ", + " - ", + "⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.", + true + ) + var list = mutableListOf("X") + if (now.hour != 8 && now.hour < 18) { + list.add("Y") } + list.forEach { code -> + KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let { + sellingAfterMarketOnePrice(KisTradeService, it, code) + } + } + lastForceCheckMinute = currentMinute // 실행 완료 기록 } - lastForceCheckMinute = currentMinute // 실행 완료 기록 } } } diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index f040bd3..c868673 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -1,8 +1,14 @@ package ui +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -25,6 +31,9 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.onExternalDrag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -116,11 +125,11 @@ fun TradingDecisionLog() { .fillMaxWidth() .horizontalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { + ) { filterOptions.forEach { option -> val isSelected = selectedFilters.contains(option) - FilterChip( - selected = isSelected, + FilterChipWithRightClick( + isSelected = isSelected, onClick = { val newFilters = selectedFilters.toMutableSet() if (option == "전체") { @@ -143,13 +152,25 @@ fun TradingDecisionLog() { } selectedFilters = newFilters }, - colors = ChipDefaults.filterChipColors( - selectedBackgroundColor = Color(0xFF0E62CF), - selectedContentColor = Color.White - ) - ) { - Text(option, fontSize = 11.sp) - } + onClear = { + if (option != "전체") { + // 예: 로그의 등급(investmentGrade)이나 결정(decision)이 필터명과 일치할 때 삭제 + TradingLogStore.decisionLogs.removeIf { log -> + log.decision.contains(option) + } + } else { + // "전체" 필터에서 우클릭 시 모든 로그 삭제 + TradingLogStore.decisionLogs.clear() + } + + // 💡 2. UI 동기화 (선택 해제) + val newFilters = selectedFilters.toMutableSet() + newFilters.remove(option) + if (newFilters.isEmpty()) newFilters.add("전체") + selectedFilters = newFilters + }, + label = option + ) } } } @@ -291,7 +312,7 @@ fun TradingDecisionLog() { initialChecked = KisSession.config.getValues(ConfigIndex.TAKE_PROFIT) > 0.0, onCheckedChange = { KisSession.config.setValues(ConfigIndex.TAKE_PROFIT, if (it) 1.0 else 0.0) - }, + }, helperText = "목표 수익률 도달 시 기계적 익절" ) } @@ -416,7 +437,7 @@ fun TradingDecisionLog() { @Composable fun StatusIndicator(label: String, isActive: Boolean, onRestart: (() -> Unit)? = null) { Row( - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp) ) { Text(text = label, style = MaterialTheme.typography.body2) @@ -621,7 +642,7 @@ fun SettingSwitchField( ) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically ) { Text( text = label, @@ -647,3 +668,66 @@ fun SettingSwitchField( } } } +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) +@Composable +fun FilterChipWithRightClick( + label: String, + isSelected: Boolean, + onClick: () -> Unit, + onClear: () -> Unit +) { + var showMenu by remember { mutableStateOf(false) } + + Box { + Surface( + modifier = Modifier + .padding(end = 6.dp) + .pointerInput(label) { + awaitEachGesture { + val down = awaitFirstDown() + // 💡 마우스 우클릭 검사 + if (down.type == PointerType.Mouse && currentEvent.buttons.isSecondaryPressed) { + down.consume() + showMenu = true + } else { + // 💡 터치/좌클릭 시 롱클릭 판별 + val up = withTimeoutOrNull(viewConfiguration.longPressTimeoutMillis) { + waitForUpOrCancellation() + } + if (up == null) { + showMenu = true // 롱클릭 + } else { + up.consume() + onClick() // 일반 클릭 + } + } + } + }, + shape = RoundedCornerShape(16.dp), + color = if (isSelected) Color(0xFF0E62CF) else Color(0xFFF0F2F5), + border = BorderStroke(1.dp, if (isSelected) Color(0xFF0E62CF) else Color.LightGray) + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + fontSize = 11.sp, + color = if (isSelected) Color.White else Color.Black, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal + ) + } + + // 💡 우클릭 시 나타나는 메뉴 + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem(onClick = { + onClear() // 실제 삭제 수행 + showMenu = false + }) { + val menuText = if (label == "전체") "전체 로그 초기화" else "'$label' 관련 로그 삭제" + Text(menuText, color = Color.Red, fontSize = 12.sp) + } + } + } +} \ No newline at end of file