....
This commit is contained in:
@@ -58,14 +58,6 @@ fun DashboardScreen() {
|
||||
var monthSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var yearSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
|
||||
fun setupAutoTradingWatchdog(tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
// while (true) {
|
||||
// delay(60000) // 1분마다 체크
|
||||
AutoTradingManager.checkAndRestart(tradeService, callback)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var callback = object : TradingDecisionCallback {
|
||||
@@ -91,11 +83,6 @@ fun DashboardScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// 화면이 완전히 그려지고 안정화될 때까지 1초 대기
|
||||
delay(1000)
|
||||
AutoTradingManager.startAutoDiscoveryLoop(tradeService, callback)
|
||||
}
|
||||
|
||||
// 리소스 정리는 여전히 DisposableEffect에서 수행
|
||||
DisposableEffect(Unit) {
|
||||
@@ -108,13 +95,13 @@ fun DashboardScreen() {
|
||||
var refreshTrigger by remember { mutableStateOf(0) }
|
||||
// [핵심] 아직 DB에 등록되기 전에 도착한 체결 데이터를 임시 보관하는 버퍼
|
||||
val executionCache = remember { mutableMapOf<String, ExecutionData>() }
|
||||
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||
|
||||
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||
|
||||
LaunchedEffect(refreshTrigger) {
|
||||
// setupAutoTradingWatchdog(tradeService,callback)
|
||||
}
|
||||
|
||||
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||
suspend fun syncAndExecute(orderNo: String) {
|
||||
if (processingIds.contains(orderNo)) return
|
||||
processingIds.add(orderNo)
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.min
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.ConfigIndex
|
||||
import model.InvestmentGrade
|
||||
import model.KisSession
|
||||
import model.RankingStock
|
||||
import network.KisTradeService
|
||||
@@ -30,61 +31,7 @@ import util.MarketUtil
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class InvestmentGrade(
|
||||
val displayName: String,
|
||||
val description: String,
|
||||
val shortWeight: Double = 0.0,
|
||||
val midWeight: Double = 0.0,
|
||||
val longWeight: Double = 0.0,
|
||||
val profitGuide: ConfigIndex,
|
||||
val buyGuide: ConfigIndex,
|
||||
) {
|
||||
LEVEL_5_STRONG_RECOMMEND(
|
||||
displayName = "최상급 추천",
|
||||
description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천",
|
||||
shortWeight = 1.0,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = ConfigIndex.GRADE_5_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_5_BUY,
|
||||
),
|
||||
LEVEL_4_BALANCED_RECOMMEND(
|
||||
displayName = "균형 추천",
|
||||
description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천",
|
||||
shortWeight = 0.8,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = ConfigIndex.GRADE_4_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_4_BUY,
|
||||
),
|
||||
LEVEL_3_CAUTIOUS_RECOMMEND(
|
||||
displayName = "보수적 추천",
|
||||
description = "중기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함",
|
||||
shortWeight = 0.6,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = ConfigIndex.GRADE_3_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_3_BUY,
|
||||
),
|
||||
LEVEL_2_HIGH_RISK(
|
||||
displayName = "고위험 추천",
|
||||
description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자",
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.4,
|
||||
longWeight = 0.4,
|
||||
profitGuide = ConfigIndex.GRADE_2_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_2_BUY,
|
||||
),
|
||||
LEVEL_1_SPECULATIVE(
|
||||
displayName = "순수 공격적 선택",
|
||||
description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자",
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.2,
|
||||
longWeight = 0.2,
|
||||
profitGuide = ConfigIndex.GRADE_1_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_1_BUY,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -316,7 +263,7 @@ fun IntegratedOrderSection(
|
||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
|
||||
println("✋ [관망] 토탈 스코어 또는 신뢰도 미달 이나 약간의 오차로 재분석 대기열에 추가")
|
||||
} else {
|
||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)}) 또는 신뢰도 ${completeTradingDecision.confidence} 미달")
|
||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달")
|
||||
}
|
||||
}
|
||||
when (completeTradingDecision?.decision) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import model.AppConfig
|
||||
@@ -53,17 +54,22 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(24.dp)) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(20.dp)) {
|
||||
item {
|
||||
Text("거래 방식 선택", style = MaterialTheme.typography.h6)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
||||
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
||||
){
|
||||
Text("투자 방식", style = MaterialTheme.typography.subtitle1)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false,) })
|
||||
Text("실전투자")
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text("실전")
|
||||
Spacer(Modifier.width(10.dp))
|
||||
RadioButton(selected = config.isSimulation, onClick = { config = config.copy() })
|
||||
Text("모의투자")
|
||||
Text("모의")
|
||||
}
|
||||
Divider(Modifier.padding(vertical = 12.dp))
|
||||
|
||||
Divider(Modifier.padding(vertical = 10.dp))
|
||||
OutlinedTextField(
|
||||
value = config.htsId,
|
||||
onValueChange = { config = config.copy(htsId = it,) },
|
||||
@@ -73,94 +79,126 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
)
|
||||
// 실전 3종 입력
|
||||
Text("실전투자 정보 (시세 조회 필수)", fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(value = config.realAccountNo, onValueChange = {
|
||||
config = config.copy(realAccountNo = it,)
|
||||
if(it.length >= 8) checkAndLoadConfig(it, true)
|
||||
}, label = { Text("실전 계좌번호") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.realAppKey, onValueChange = { config = config.copy(realAppKey = it,) }, label = { Text("실전 App Key") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
||||
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = config.realAccountNo, onValueChange = {
|
||||
config = config.copy(realAccountNo = it,)
|
||||
if (it.length >= 8) checkAndLoadConfig(it, true)
|
||||
}, label = { Text("실전 계좌번호") }, modifier = Modifier.weight(0.5f))
|
||||
OutlinedTextField(
|
||||
value = config.realAppKey,
|
||||
onValueChange = { config = config.copy(realAppKey = it,) },
|
||||
label = { Text("실전 App Key") },
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
}
|
||||
OutlinedTextField(value = config.realSecretKey, onValueChange = { config = config.copy(realSecretKey = it,) }, label = { Text("실전 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
// 모의 3종 입력
|
||||
Text("모의투자 정보", fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(value = config.vtsAccountNo, onValueChange = {
|
||||
config = config.copy(vtsAccountNo = it,)
|
||||
if(it.length >= 8) checkAndLoadConfig(it, false)
|
||||
}, label = { Text("모의 계좌번호") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.vtsAppKey, onValueChange = { config = config.copy(vtsAppKey = it,) }, label = { Text("모의 App Key") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
||||
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
||||
) {
|
||||
OutlinedTextField(value = config.vtsAccountNo, onValueChange = {
|
||||
config = config.copy(vtsAccountNo = it,)
|
||||
if (it.length >= 8) checkAndLoadConfig(it, false)
|
||||
}, label = { Text("모의 계좌번호") }, modifier = Modifier.weight(0.5f))
|
||||
OutlinedTextField(
|
||||
value = config.vtsAppKey,
|
||||
onValueChange = { config = config.copy(vtsAppKey = it,) },
|
||||
label = { Text("모의 App Key") },
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
}
|
||||
OutlinedTextField(value = config.vtsSecretKey, onValueChange = { config = config.copy(vtsSecretKey = it,) }, label = { Text("모의 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("정보 조회 Api Keyz", fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(value = config.nAppKey, onValueChange = { config = config.copy(nAppKey = it,) }, label = { Text("NAVER Client ID") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.nSecretKey, onValueChange = { config = config.copy(nSecretKey = it,) }, label = { Text("NAVER Client Secret") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
OutlinedTextField(value = config.dAppKey, onValueChange = { config = config.copy(dAppKey = it,) }, label = { Text("Dart ApiKey") }, modifier = Modifier.fillMaxWidth())
|
||||
|
||||
// AI 모델 경로 및 드래그 앤 드롭
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("AI 모델 설정", fontWeight = FontWeight.Bold)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
.onExternalDrag(onDrop = { state ->
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val path = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path,)
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(if(config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, fontSize = 12.sp)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
||||
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
||||
){
|
||||
Box(
|
||||
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
.onExternalDrag(onDrop = { state ->
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val path = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path,)
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(if(config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, fontSize = 12.sp)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
.onExternalDrag(onDrop = { state ->
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val embedModelPath = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath,)
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(if(config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
.onExternalDrag(onDrop = { state ->
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val embedModelPath = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath,)
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(if(config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath, fontSize = 12.sp)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth().height(50.dp),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
// isLoading = true
|
||||
// 1. KisSession.config 업데이트 및 DB 저장
|
||||
KisSession.config = config
|
||||
DatabaseFactory.saveConfig(config)
|
||||
DartCodeManager.updateCorpCodes(HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true // 기본값이 포함된 요청 바디를 정확히 전송하기 위해 필요
|
||||
})
|
||||
}
|
||||
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.BODY
|
||||
}
|
||||
})
|
||||
val authService = KisAuthService
|
||||
val tradeService = KisTradeService
|
||||
val authSuccess = authService.refreshAllTokens()
|
||||
val wsKeySuccess = tradeService.refreshWebsocketKey()
|
||||
var retryCount = 0
|
||||
val maxRetries = 3
|
||||
val retryDelay = 90_000L // 1분 30초
|
||||
var isAuthCompleted = false
|
||||
|
||||
if (authSuccess && wsKeySuccess) {
|
||||
statusMessage = "✅ 인증 성공! LLM 시작 중..."
|
||||
onAuthSuccess()
|
||||
} else {
|
||||
statusMessage = "❌ 인증 실패. 키 정보를 확인하세요."
|
||||
while (retryCount <= maxRetries && !isAuthCompleted) {
|
||||
if (retryCount > 0) {
|
||||
statusMessage = "⏳ 인증 재시도 중... (${retryCount}/${maxRetries}) - 1분 30초 후 재시작"
|
||||
delay(retryDelay)
|
||||
}
|
||||
|
||||
// 1. 설정값 저장
|
||||
KisSession.config = config
|
||||
DatabaseFactory.saveConfig(config)
|
||||
|
||||
// 2. 법인코드 업데이트
|
||||
DartCodeManager.updateCorpCodes(HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
|
||||
})
|
||||
|
||||
// 3. 토큰 및 웹소켓 키 갱신
|
||||
val authSuccess = KisAuthService.refreshAllTokens()
|
||||
val wsKeySuccess = KisTradeService.refreshWebsocketKey()
|
||||
|
||||
if (authSuccess && wsKeySuccess) {
|
||||
statusMessage = "✅ 인증 성공! LLM 시작 중..."
|
||||
isAuthCompleted = true
|
||||
onAuthSuccess()
|
||||
} else {
|
||||
retryCount++
|
||||
if (retryCount > maxRetries) {
|
||||
statusMessage = "❌ 인증 실패. 3회 재시도 후 중단되었습니다. 키 정보를 확인하세요."
|
||||
} else {
|
||||
statusMessage = "⚠️ 인증 실패. 잠시 후 자동으로 다시 시도합니다. (시도 $retryCount)"
|
||||
}
|
||||
}
|
||||
}
|
||||
// isLoading = false
|
||||
}
|
||||
}
|
||||
) { Text("설정 저장 및 실행") }
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package ui
|
||||
|
||||
|
||||
import AutoTradeItem
|
||||
import TradingDecision
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.ConfigIndex
|
||||
import model.KisSession
|
||||
import service.TechnicalAnalyzer
|
||||
|
||||
@Composable
|
||||
fun TradingDecisionLog() {
|
||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||
Column(modifier = Modifier.weight(0.5f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||
Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6)
|
||||
Divider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
LazyColumn(reverseLayout = true) { // 최신 로그가 위로 오게 함
|
||||
items(TradingLogStore.decisionLogs) { log ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
elevation = 2.dp
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("${log.time} - ${log.stockName}", fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
text = log.decision,
|
||||
color = if (log.decision == "BUY") Color.Red else Color.Gray,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
}
|
||||
Text("신뢰도: ${log.confidence}%", fontSize = 11.sp)
|
||||
Text("이유: ${log.reason}", fontSize = 12.sp, color = Color.DarkGray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(0.5f).padding(6.dp).fillMaxHeight().background(Color.White)) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2), // 2열 병렬 배치
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight().background(Color.White)
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
"💰 거래 기본 설정",
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
var defaults = arrayOf(
|
||||
ConfigIndex.TAX_INDEX,
|
||||
ConfigIndex.PROFIT_INDEX,
|
||||
ConfigIndex.BUY_WEIGHT_INDEX,
|
||||
ConfigIndex.MAX_BUDGET_INDEX,
|
||||
ConfigIndex.MAX_PRICE_INDEX,
|
||||
ConfigIndex.MIN_PRICE_INDEX,
|
||||
ConfigIndex.MIN_PURCHASE_SCORE_INDEX,
|
||||
ConfigIndex.SELL_PROFIT,
|
||||
ConfigIndex.MAX_COUNT_INDEX,
|
||||
)
|
||||
items(defaults.size) { index ->
|
||||
val configKey = defaults.get(index)
|
||||
|
||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
||||
var localText by remember(configKey) {
|
||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||
}
|
||||
|
||||
// 저장 로직을 공통 함수로 분리
|
||||
val saveAction = {
|
||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||
if (configKey.label.contains("PROFIT")) {
|
||||
newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||
}
|
||||
KisSession.config.setValues(configKey, newValue)
|
||||
DatabaseFactory.saveConfig(KisSession.config)
|
||||
println("💾 저장됨: ${configKey.label} = $newValue")
|
||||
}
|
||||
|
||||
var text = 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 ->
|
||||
// 2. 포커스를 잃었을 때 저장
|
||||
if (!focusState.isFocused) {
|
||||
saveAction()
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Decimal
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
||||
onDone = {
|
||||
saveAction()
|
||||
}
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
"💰매수 정책 및 기대 수익률",
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
var defaults2 = arrayOf(
|
||||
arrayOf(ConfigIndex.GRADE_5_BUY,
|
||||
ConfigIndex.GRADE_5_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_4_BUY,
|
||||
ConfigIndex.GRADE_4_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_3_BUY,
|
||||
ConfigIndex.GRADE_3_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_2_BUY,
|
||||
ConfigIndex.GRADE_2_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_1_BUY,
|
||||
ConfigIndex.GRADE_1_PROFIT,),
|
||||
)
|
||||
for (items in defaults2) {
|
||||
val common = findLongestCommonSubstring(items.first().label,items.last().label)
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
common,
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
items(items.size) { index ->
|
||||
val configKey = items.get(index)
|
||||
|
||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
||||
var localText by remember(configKey) {
|
||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||
}
|
||||
|
||||
var labelText by remember(configKey) {
|
||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||
}
|
||||
|
||||
val saveAction = {
|
||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||
//
|
||||
KisSession.config.setValues(configKey, newValue)
|
||||
DatabaseFactory.saveConfig(KisSession.config)
|
||||
println("💾 저장됨: ${configKey.label} = $newValue")
|
||||
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.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
|
||||
ConfigIndex.TAX_INDEX)}"
|
||||
} else {
|
||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||
}
|
||||
}
|
||||
|
||||
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.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
|
||||
ConfigIndex.TAX_INDEX)} "
|
||||
} else {
|
||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = localText,
|
||||
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
||||
label = { Text(labelText) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focusState ->
|
||||
// 2. 포커스를 잃었을 때 저장
|
||||
if (!focusState.isFocused) {
|
||||
saveAction()
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Decimal
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
||||
onDone = {
|
||||
saveAction()
|
||||
}
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user