This commit is contained in:
lunaticbum 2026-04-20 17:09:54 +09:00
parent 2d577300c3
commit a3a0338cc5
3 changed files with 133 additions and 42 deletions

View File

@ -70,7 +70,7 @@ object ConfigTable : Table("app_config") {
val grade_2_allocationrate = double("grade_2_allocationrate").default(0.4) val grade_2_allocationrate = double("grade_2_allocationrate").default(0.4)
val grade_1_allocationrate = double("grade_1_allocationrate").default(0.3) 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 stop_Loss = bool("stop_Loss").default(false)
val loss_minrate = double("loss_minrate").default(3.5) val loss_minrate = double("loss_minrate").default(3.5)
@ -452,7 +452,7 @@ data class AutoTradeItem(
var stopLossPrice: Double = 0.0, // 손절 목표가 var stopLossPrice: Double = 0.0, // 손절 목표가
// 수량 정보 // 수량 정보
val quantity: Int = 0, // 총 주문 수량 var quantity: Int = 0, // 총 주문 수량
var remainedQuantity: Int = 0, // 미체결 잔량 (서버 동기화용) var remainedQuantity: Int = 0, // 미체결 잔량 (서버 동기화용)
val isDomestic: Boolean = true, val isDomestic: Boolean = true,

View File

@ -545,7 +545,7 @@ object AutoTradingManager {
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석 private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
val now = LocalTime.now() val now = LocalTime.now()
val currentMinute = now.minute 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 profit = holding.profitRate.toDouble()
val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다) val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다)
if (profit <= lossThreshold) { 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 { } else {
} }
} }
@ -843,18 +843,24 @@ object AutoTradingManager {
} }
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장 private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
suspend fun sellSchedule() { suspend fun sellSchedule() {
if (KisSession.config.take_profit == false) {
} else {
val now = LocalTime.now() val now = LocalTime.now()
val currentMinute = now.minute val currentMinute = now.minute
if (now.hour == 9 && (currentMinute % 10 == 1 || currentMinute % 10 == 7)) { if (now.hour == 9 && (currentMinute % 10 == 1 || currentMinute % 10 == 7)) {
if (lastForceCheckMinute != currentMinute) { if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true) TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.",
true
)
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.") println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance() checkBalance()
lastForceCheckMinute = currentMinute // 실행 완료 기록 lastForceCheckMinute = currentMinute // 실행 완료 기록
} }
} } else if ((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) {
else if((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) {
if (lastForceCheckMinute != currentMinute) { if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer( TradingLogStore.addAnalyzer(
" - ", " - ",
@ -875,6 +881,7 @@ object AutoTradingManager {
} }
} }
} }
}
fun addToReanalysis(stock: RankingStock) { fun addToReanalysis(stock: RankingStock) {

View File

@ -1,8 +1,14 @@
package ui package ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border 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.horizontalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -25,6 +31,9 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color 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.onExternalDrag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@ -119,8 +128,8 @@ fun TradingDecisionLog() {
) { ) {
filterOptions.forEach { option -> filterOptions.forEach { option ->
val isSelected = selectedFilters.contains(option) val isSelected = selectedFilters.contains(option)
FilterChip( FilterChipWithRightClick(
selected = isSelected, isSelected = isSelected,
onClick = { onClick = {
val newFilters = selectedFilters.toMutableSet() val newFilters = selectedFilters.toMutableSet()
if (option == "전체") { if (option == "전체") {
@ -143,13 +152,25 @@ fun TradingDecisionLog() {
} }
selectedFilters = newFilters selectedFilters = newFilters
}, },
colors = ChipDefaults.filterChipColors( onClear = {
selectedBackgroundColor = Color(0xFF0E62CF), if (option != "전체") {
selectedContentColor = Color.White // 예: 로그의 등급(investmentGrade)이나 결정(decision)이 필터명과 일치할 때 삭제
) TradingLogStore.decisionLogs.removeIf { log ->
) { log.decision.contains(option)
Text(option, fontSize = 11.sp)
} }
} else {
// "전체" 필터에서 우클릭 시 모든 로그 삭제
TradingLogStore.decisionLogs.clear()
}
// 💡 2. UI 동기화 (선택 해제)
val newFilters = selectedFilters.toMutableSet()
newFilters.remove(option)
if (newFilters.isEmpty()) newFilters.add("전체")
selectedFilters = newFilters
},
label = option
)
} }
} }
} }
@ -416,7 +437,7 @@ fun TradingDecisionLog() {
@Composable @Composable
fun StatusIndicator(label: String, isActive: Boolean, onRestart: (() -> Unit)? = null) { fun StatusIndicator(label: String, isActive: Boolean, onRestart: (() -> Unit)? = null) {
Row( Row(
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 12.dp) modifier = Modifier.padding(end = 12.dp)
) { ) {
Text(text = label, style = MaterialTheme.typography.body2) Text(text = label, style = MaterialTheme.typography.body2)
@ -621,7 +642,7 @@ fun SettingSwitchField(
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = label, 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)
}
}
}
}