...
This commit is contained in:
parent
2d577300c3
commit
a3a0338cc5
@ -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,
|
||||||
|
|||||||
@ -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,35 +843,42 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
|
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
|
||||||
suspend fun sellSchedule() {
|
suspend fun sellSchedule() {
|
||||||
|
if (KisSession.config.take_profit == false) {
|
||||||
|
|
||||||
val now = LocalTime.now()
|
} else {
|
||||||
val currentMinute = now.minute
|
val now = LocalTime.now()
|
||||||
if (now.hour == 9 && (currentMinute % 10 == 1 || currentMinute % 10 == 7)) {
|
val currentMinute = now.minute
|
||||||
if (lastForceCheckMinute != currentMinute) {
|
if (now.hour == 9 && (currentMinute % 10 == 1 || currentMinute % 10 == 7)) {
|
||||||
TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true)
|
if (lastForceCheckMinute != currentMinute) {
|
||||||
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
|
TradingLogStore.addAnalyzer(
|
||||||
checkBalance()
|
" - ",
|
||||||
lastForceCheckMinute = currentMinute // 실행 완료 기록
|
" - ",
|
||||||
}
|
"⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.",
|
||||||
}
|
true
|
||||||
else if((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) {
|
)
|
||||||
if (lastForceCheckMinute != currentMinute) {
|
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
|
||||||
TradingLogStore.addAnalyzer(
|
checkBalance()
|
||||||
" - ",
|
lastForceCheckMinute = currentMinute // 실행 완료 기록
|
||||||
" - ",
|
|
||||||
"⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
var list = mutableListOf<String>("X")
|
|
||||||
if (now.hour != 8 && now.hour < 18) {
|
|
||||||
list.add("Y")
|
|
||||||
}
|
}
|
||||||
list.forEach { code ->
|
} else if ((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) {
|
||||||
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
|
if (lastForceCheckMinute != currentMinute) {
|
||||||
sellingAfterMarketOnePrice(KisTradeService, it, code)
|
TradingLogStore.addAnalyzer(
|
||||||
|
" - ",
|
||||||
|
" - ",
|
||||||
|
"⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
var list = mutableListOf<String>("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 // 실행 완료 기록
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -116,11 +125,11 @@ fun TradingDecisionLog() {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.horizontalScroll(scrollState),
|
.horizontalScroll(scrollState),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,7 +312,7 @@ fun TradingDecisionLog() {
|
|||||||
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 = "목표 수익률 도달 시 기계적 익절"
|
helperText = "목표 수익률 도달 시 기계적 익절"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user