빨라져라
This commit is contained in:
@@ -1,117 +1,117 @@
|
||||
// src/main/kotlin/ui/ActiveTradeRow.kt
|
||||
package ui
|
||||
|
||||
import AutoTradeItem
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun ActiveTradeRow(
|
||||
item: AutoTradeItem, // UI 모델 대신 통합 데이터 모델 사용
|
||||
onCancelClick: () -> Unit, // 미체결 취소 시 주문번호(orderNo) 전달
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// 상태에 따른 UI 구성 요소 정의
|
||||
val (statusText, statusColor, backgroundColor) = when (item.status) {
|
||||
"PENDING_BUY" -> Triple("매수중", Color(0xFFFBC02D), Color(0xFFFFF9C4)) // 노랑
|
||||
"MONITORING" -> Triple("감시중", Color(0xFF0E62CF), Color.White) // 파랑
|
||||
"SELLING" -> Triple("매도중", Color(0xFFE03E2D), Color(0xFFFFF4F4)) // 빨강
|
||||
"COMPLETED" -> Triple("완료", Color.Gray, Color(0xFFF5F5F5)) // 회색
|
||||
else -> Triple("알 수 없음", Color.Black, Color.White)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp, horizontal = 2.dp)
|
||||
.clickable { onClick() },
|
||||
elevation = 2.dp,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
backgroundColor = backgroundColor
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 좌측 정보 영역
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// 상태 배지 표시
|
||||
Surface(
|
||||
color = statusColor,
|
||||
shape = RoundedCornerShape(2.dp),
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = statusText,
|
||||
color = Color.White,
|
||||
fontSize = 9.sp,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
// 상세 가격 정보 (상태에 따라 비율 또는 목표가 표시)
|
||||
val detailText = when (item.status) {
|
||||
"PENDING_BUY" -> "설정 비율: 익절 ${item.profitRate}% / 손절 ${item.stopLossRate}%"
|
||||
"MONITORING" -> "목표가: ${String.format("%,.0f", item.targetPrice)} / 손절가: ${String.format("%,.0f", item.stopLossPrice)}"
|
||||
else -> "주문번호: ${item.orderNo} ${item.orderedPrice} ${item.quantity}"
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${item.code} | $detailText",
|
||||
fontSize = 11.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
// 우측 액션 및 수량 영역
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
if (item.status == "PENDING_BUY" || item.status == "SELLING") {
|
||||
// 진행 중인 주문인 경우 취소 버튼 노출
|
||||
Button(
|
||||
onClick = { onCancelClick() },
|
||||
contentPadding = PaddingValues(horizontal = 8.dp),
|
||||
modifier = Modifier.height(28.dp),
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)
|
||||
) {
|
||||
Text("취소", fontSize = 11.sp)
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = { onCancelClick() },
|
||||
contentPadding = PaddingValues(horizontal = 8.dp),
|
||||
modifier = Modifier.height(28.dp),
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)
|
||||
) {
|
||||
Text("취소", fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${item.quantity}주",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = statusColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/ActiveTradeRow.kt
|
||||
//package ui
|
||||
//
|
||||
//import AutoTradeItem
|
||||
//import androidx.compose.foundation.background
|
||||
//import androidx.compose.foundation.clickable
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//
|
||||
//@Composable
|
||||
//fun ActiveTradeRow(
|
||||
// item: AutoTradeItem, // UI 모델 대신 통합 데이터 모델 사용
|
||||
// onCancelClick: () -> Unit, // 미체결 취소 시 주문번호(orderNo) 전달
|
||||
// onClick: () -> Unit
|
||||
//) {
|
||||
// // 상태에 따른 UI 구성 요소 정의
|
||||
// val (statusText, statusColor, backgroundColor) = when (item.status) {
|
||||
// "PENDING_BUY" -> Triple("매수중", Color(0xFFFBC02D), Color(0xFFFFF9C4)) // 노랑
|
||||
// "MONITORING" -> Triple("감시중", Color(0xFF0E62CF), Color.White) // 파랑
|
||||
// "SELLING" -> Triple("매도중", Color(0xFFE03E2D), Color(0xFFFFF4F4)) // 빨강
|
||||
// "COMPLETED" -> Triple("완료", Color.Gray, Color(0xFFF5F5F5)) // 회색
|
||||
// else -> Triple("알 수 없음", Color.Black, Color.White)
|
||||
// }
|
||||
//
|
||||
// Card(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = 4.dp, horizontal = 2.dp)
|
||||
// .clickable { onClick() },
|
||||
// elevation = 2.dp,
|
||||
// shape = RoundedCornerShape(4.dp),
|
||||
// backgroundColor = backgroundColor
|
||||
// ) {
|
||||
// Row(
|
||||
// modifier = Modifier.padding(12.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// // 좌측 정보 영역
|
||||
// Column(modifier = Modifier.weight(1f)) {
|
||||
// Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// // 상태 배지 표시
|
||||
// Surface(
|
||||
// color = statusColor,
|
||||
// shape = RoundedCornerShape(2.dp),
|
||||
// modifier = Modifier.padding(end = 6.dp)
|
||||
// ) {
|
||||
// Text(
|
||||
// text = statusText,
|
||||
// color = Color.White,
|
||||
// fontSize = 9.sp,
|
||||
// modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
// fontWeight = FontWeight.Bold
|
||||
// )
|
||||
// }
|
||||
// Text(
|
||||
// text = item.name,
|
||||
// style = MaterialTheme.typography.body2,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// maxLines = 1
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // 상세 가격 정보 (상태에 따라 비율 또는 목표가 표시)
|
||||
// val detailText = when (item.status) {
|
||||
// "PENDING_BUY" -> "설정 비율: 익절 ${item.profitRate}% / 손절 ${item.stopLossRate}%"
|
||||
// "MONITORING" -> "목표가: ${String.format("%,.0f", item.targetPrice)} / 손절가: ${String.format("%,.0f", item.stopLossPrice)}"
|
||||
// else -> "주문번호: ${item.orderNo} ${item.orderedPrice} ${item.quantity}"
|
||||
// }
|
||||
//
|
||||
// Text(
|
||||
// text = "${item.code} | $detailText",
|
||||
// fontSize = 11.sp,
|
||||
// color = Color.Gray
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // 우측 액션 및 수량 영역
|
||||
// Column(horizontalAlignment = Alignment.End) {
|
||||
// if (item.status == "PENDING_BUY" || item.status == "SELLING") {
|
||||
// // 진행 중인 주문인 경우 취소 버튼 노출
|
||||
// Button(
|
||||
// onClick = { onCancelClick() },
|
||||
// contentPadding = PaddingValues(horizontal = 8.dp),
|
||||
// modifier = Modifier.height(28.dp),
|
||||
// colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)
|
||||
// ) {
|
||||
// Text("취소", fontSize = 11.sp)
|
||||
// }
|
||||
// } else {
|
||||
// Button(
|
||||
// onClick = { onCancelClick() },
|
||||
// contentPadding = PaddingValues(horizontal = 8.dp),
|
||||
// modifier = Modifier.height(28.dp),
|
||||
// colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)
|
||||
// ) {
|
||||
// Text("취소", fontSize = 11.sp)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Text(
|
||||
// text = "${item.quantity}주",
|
||||
// fontSize = 12.sp,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// color = statusColor
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,100 +1,99 @@
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.KisSession
|
||||
import service.AutoTradingManager
|
||||
import service.TechnicalAnalyzer
|
||||
import service.TradingDecisionCallback
|
||||
|
||||
@Composable
|
||||
fun AiAnalysisView(technicalAnalyzer: TechnicalAnalyzer,stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
|
||||
var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
|
||||
var code by remember(stockCode) {
|
||||
aiOpinion = ""
|
||||
mutableStateOf(stockCode.isNotEmpty())
|
||||
}
|
||||
var isAnalyzing by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// KisSession의 전역 설정을 참조
|
||||
val isModelConfigured = remember(KisSession.config.modelPath) {
|
||||
val path = KisSession.config.modelPath
|
||||
path.isNotEmpty() && java.io.File(path).exists()
|
||||
}
|
||||
|
||||
Card(
|
||||
elevation = 2.dp,
|
||||
backgroundColor = if (isModelConfigured) Color(0xFFF1F3F4) else Color(0xFFFFEBEE),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.15F)
|
||||
.verticalScroll(rememberScrollState()) // 스크롤 활성화
|
||||
.padding(16.dp)
|
||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = if (isModelConfigured) "${stockName} AI 투자 전략" else "⚠️ AI 설정 필요",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (isModelConfigured) Color(0xFF1A73E8) else Color.Red
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isAnalyzing = true
|
||||
try {
|
||||
AutoTradingManager.addStock(currentPrice.replace(",","").toDouble(),technicalAnalyzer,stockName,stockCode) { decision,success ->
|
||||
aiOpinion = decision.toString()
|
||||
isAnalyzing = !success
|
||||
tradingDecisionCallback.invoke(decision,success)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
aiOpinion = "분석 중 오류 발생: ${e.message}"
|
||||
println(aiOpinion)
|
||||
isAnalyzing = false
|
||||
} finally {
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.background
|
||||
//import androidx.compose.foundation.layout.Column
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.rememberScrollState
|
||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
//import androidx.compose.foundation.verticalScroll
|
||||
//import androidx.compose.material.Button
|
||||
//import androidx.compose.material.Card
|
||||
//import androidx.compose.material.CircularProgressIndicator
|
||||
//import androidx.compose.material.Divider
|
||||
//import androidx.compose.material.MaterialTheme
|
||||
//import androidx.compose.material.Text
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.mutableStateOf
|
||||
//import androidx.compose.runtime.remember
|
||||
//import androidx.compose.runtime.rememberCoroutineScope
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import androidx.compose.ui.*
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import kotlinx.coroutines.launch
|
||||
//import model.KisSession
|
||||
//import service.AutoTradingManager
|
||||
//import service.TradingDecisionCallback
|
||||
//
|
||||
//@Composable
|
||||
//fun AiAnalysisView(technicalAnalyzer: TechnicalAnalyzer,stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
|
||||
// var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
|
||||
// var code by remember(stockCode) {
|
||||
// aiOpinion = ""
|
||||
// mutableStateOf(stockCode.isNotEmpty())
|
||||
// }
|
||||
// var isAnalyzing by remember { mutableStateOf(false) }
|
||||
// val scope = rememberCoroutineScope()
|
||||
//
|
||||
// // KisSession의 전역 설정을 참조
|
||||
// val isModelConfigured = remember(KisSession.config.modelPath) {
|
||||
// val path = KisSession.config.modelPath
|
||||
// path.isNotEmpty() && java.io.File(path).exists()
|
||||
// }
|
||||
//
|
||||
// Card(
|
||||
// elevation = 2.dp,
|
||||
// backgroundColor = if (isModelConfigured) Color(0xFFF1F3F4) else Color(0xFFFFEBEE),
|
||||
// modifier = Modifier.fillMaxWidth()
|
||||
// ) {
|
||||
// Column(modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .fillMaxHeight(0.15F)
|
||||
// .verticalScroll(rememberScrollState()) // 스크롤 활성화
|
||||
// .padding(16.dp)
|
||||
// .background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))) {
|
||||
// Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Text(
|
||||
// text = if (isModelConfigured) "${stockName} AI 투자 전략" else "⚠️ AI 설정 필요",
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// color = if (isModelConfigured) Color(0xFF1A73E8) else Color.Red
|
||||
// )
|
||||
// Spacer(Modifier.weight(1f))
|
||||
// Button(
|
||||
// onClick = {
|
||||
// scope.launch {
|
||||
// isAnalyzing = true
|
||||
// try {
|
||||
// AutoTradingManager.addStock(currentPrice.replace(",","").toDouble(),technicalAnalyzer,stockName,stockCode) { decision,success ->
|
||||
// aiOpinion = decision.toString()
|
||||
// isAnalyzing = !success
|
||||
// tradingDecisionCallback.invoke(decision,success)
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// aiOpinion = "분석 중 오류 발생: ${e.message}"
|
||||
// println(aiOpinion)
|
||||
// isAnalyzing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isAnalyzing && code
|
||||
) {
|
||||
if (isAnalyzing) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("뉴스 분석 중...")
|
||||
} else {
|
||||
Text("분석 요청")
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(Modifier.padding(vertical = 8.dp))
|
||||
Text(text = aiOpinion, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
}
|
||||
}
|
||||
// } finally {
|
||||
//// isAnalyzing = false
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// enabled = !isAnalyzing && code
|
||||
// ) {
|
||||
// if (isAnalyzing) {
|
||||
// CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)
|
||||
// Spacer(Modifier.width(8.dp))
|
||||
// Text("뉴스 분석 중...")
|
||||
// } else {
|
||||
// Text("분석 요청")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Divider(Modifier.padding(vertical = 8.dp))
|
||||
// Text(text = aiOpinion, style = MaterialTheme.typography.body2)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,95 +1,95 @@
|
||||
// src/main/kotlin/ui/AutoTradeSection.kt (신규 파일)
|
||||
package ui
|
||||
|
||||
import AutoTradeItem
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.toAutoTradeItem
|
||||
import network.KisTradeService
|
||||
|
||||
// src/main/kotlin/ui/AutoTradeSection.kt
|
||||
|
||||
@Composable
|
||||
fun AutoTradeSection(
|
||||
isDomestic: Boolean,
|
||||
tradeService: KisTradeService,
|
||||
refreshTrigger: Int, // 갱신 트리거 추가
|
||||
onRefresh: () -> Unit,
|
||||
onItemSelect: (AutoTradeItem) -> Unit,
|
||||
onItemCancel: (AutoTradeItem) -> Unit
|
||||
) {
|
||||
// 통합 리스트 상태 (ActiveTradeItem은 이전에 정의한 통합 모델)
|
||||
var tradeList by remember { mutableStateOf(emptyList<AutoTradeItem>()) }
|
||||
// refreshTrigger가 바뀔 때마다 실행됨
|
||||
LaunchedEffect(refreshTrigger) {
|
||||
// 1. 서버에서 실제 미체결 내역 가져오기
|
||||
val serverUnfilled = tradeService.fetchUnfilledOrders().getOrNull()?.map { it.toAutoTradeItem(isDomestic) } ?: emptyList()
|
||||
|
||||
// 2. DB에서 로컬 감시 데이터 가져오기
|
||||
val localTrades = DatabaseFactory.getActiveAutoTrades()
|
||||
|
||||
// 3. 리스트 병합 및 동기화
|
||||
val mergedList = mutableListOf<AutoTradeItem>()
|
||||
|
||||
// (A) DB에 있는 항목 처리
|
||||
localTrades.forEach { local ->
|
||||
val serverMatch = serverUnfilled.find { it.orderNo == local.orderNo }
|
||||
if (local.status != "COMPLETED" && serverMatch == null) {
|
||||
// 서버에 없으면 만료 처리
|
||||
mergedList.add(local.copy(status = "EXPIRED"))
|
||||
} else {
|
||||
// 서버에 있으면 그대로 표시 (필요시 잔량 등 업데이트)
|
||||
mergedList.add(local.copy(remainedQuantity = serverMatch?.remainedQuantity ?: 0))
|
||||
}
|
||||
}
|
||||
|
||||
// (B) 서버에는 있지만 DB에는 없는 항목(수동 주문 등) 추가
|
||||
val manualOrders = serverUnfilled.filter { server -> localTrades.none { it.orderNo == server.orderNo } }
|
||||
mergedList.addAll(manualOrders.map { it.copy(status = "MANUAL_ORDER") }) // 수동 주문 상태 등으로 표시
|
||||
|
||||
tradeList = mergedList
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("진행 중인 거래", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold)
|
||||
|
||||
// 강제 갱신 버튼
|
||||
IconButton(
|
||||
onClick = onRefresh,
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
|
||||
contentDescription = "새로고침",
|
||||
tint = Color(0xFF0E62CF),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn {
|
||||
items(tradeList) { item ->
|
||||
ActiveTradeRow(
|
||||
item = item,
|
||||
onCancelClick = { onItemCancel(item) }, // 이미 스코프에 있는 item을 그대로 사용
|
||||
onClick = { onItemSelect(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/AutoTradeSection.kt (신규 파일)
|
||||
//package ui
|
||||
//
|
||||
//import AutoTradeItem
|
||||
//import androidx.compose.foundation.clickable
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.material.icons.filled.Refresh
|
||||
//import androidx.compose.runtime.*
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import model.toAutoTradeItem
|
||||
//import network.KisTradeService
|
||||
//
|
||||
//// src/main/kotlin/ui/AutoTradeSection.kt
|
||||
//
|
||||
//@Composable
|
||||
//fun AutoTradeSection(
|
||||
// isDomestic: Boolean,
|
||||
// tradeService: KisTradeService,
|
||||
// refreshTrigger: Int, // 갱신 트리거 추가
|
||||
// onRefresh: () -> Unit,
|
||||
// onItemSelect: (AutoTradeItem) -> Unit,
|
||||
// onItemCancel: (AutoTradeItem) -> Unit
|
||||
//) {
|
||||
// // 통합 리스트 상태 (ActiveTradeItem은 이전에 정의한 통합 모델)
|
||||
// var tradeList by remember { mutableStateOf(emptyList<AutoTradeItem>()) }
|
||||
// // refreshTrigger가 바뀔 때마다 실행됨
|
||||
// LaunchedEffect(refreshTrigger) {
|
||||
// // 1. 서버에서 실제 미체결 내역 가져오기
|
||||
// val serverUnfilled = tradeService.fetchUnfilledOrders().getOrNull()?.map { it.toAutoTradeItem(isDomestic) } ?: emptyList()
|
||||
//
|
||||
// // 2. DB에서 로컬 감시 데이터 가져오기
|
||||
// val localTrades = DatabaseFactory.getActiveAutoTrades()
|
||||
//
|
||||
// // 3. 리스트 병합 및 동기화
|
||||
// val mergedList = mutableListOf<AutoTradeItem>()
|
||||
//
|
||||
// // (A) DB에 있는 항목 처리
|
||||
// localTrades.forEach { local ->
|
||||
// val serverMatch = serverUnfilled.find { it.orderNo == local.orderNo }
|
||||
// if (local.status != "COMPLETED" && serverMatch == null) {
|
||||
// // 서버에 없으면 만료 처리
|
||||
// mergedList.add(local.copy(status = "EXPIRED"))
|
||||
// } else {
|
||||
// // 서버에 있으면 그대로 표시 (필요시 잔량 등 업데이트)
|
||||
// mergedList.add(local.copy(remainedQuantity = serverMatch?.remainedQuantity ?: 0))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // (B) 서버에는 있지만 DB에는 없는 항목(수동 주문 등) 추가
|
||||
// val manualOrders = serverUnfilled.filter { server -> localTrades.none { it.orderNo == server.orderNo } }
|
||||
// mergedList.addAll(manualOrders.map { it.copy(status = "MANUAL_ORDER") }) // 수동 주문 상태 등으로 표시
|
||||
//
|
||||
// tradeList = mergedList
|
||||
// }
|
||||
//
|
||||
// Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
|
||||
// Row(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// horizontalArrangement = Arrangement.SpaceBetween,
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// Text("진행 중인 거래", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold)
|
||||
//
|
||||
// // 강제 갱신 버튼
|
||||
// IconButton(
|
||||
// onClick = onRefresh,
|
||||
// modifier = Modifier.size(24.dp)
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
|
||||
// contentDescription = "새로고침",
|
||||
// tint = Color(0xFF0E62CF),
|
||||
// modifier = Modifier.size(18.dp)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// LazyColumn {
|
||||
// items(tradeList) { item ->
|
||||
// ActiveTradeRow(
|
||||
// item = item,
|
||||
// onCancelClick = { onItemCancel(item) }, // 이미 스코프에 있는 item을 그대로 사용
|
||||
// onClick = { onItemSelect(item) }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,180 +1,180 @@
|
||||
// src/main/kotlin/ui/BalanceSection.kt
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.UnifiedBalance
|
||||
import network.KisTradeService
|
||||
|
||||
@Composable
|
||||
fun BalanceSection(
|
||||
tradeService: KisTradeService,
|
||||
refreshTrigger: Int, // 갱신 트리거 추가
|
||||
onRefresh: () -> Unit,
|
||||
onStockSelect: (code: String, name: String, isDomestic: Boolean,quantity: String) -> Unit
|
||||
) {
|
||||
var balanceData by remember { mutableStateOf<UnifiedBalance?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
// 화면 진입 시 및 갱신 시 데이터 로드
|
||||
LaunchedEffect(refreshTrigger) {
|
||||
isLoading = true
|
||||
tradeService.fetchIntegratedBalance().onSuccess {
|
||||
balanceData = it
|
||||
}.onFailure {
|
||||
println("❌ 잔고 로드 실패: ${it.localizedMessage}")
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "나의 자산",
|
||||
style = MaterialTheme.typography.h6,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
// 강제 갱신 버튼
|
||||
IconButton(
|
||||
onClick = onRefresh,
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
|
||||
contentDescription = "새로고침",
|
||||
tint = Color(0xFF0E62CF),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
// 1. 자산 요약 카드
|
||||
BalanceSummaryCard(balanceData)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 2. 통합 보유 종목 리스트
|
||||
Text(
|
||||
text = "보유 종목",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.weight(1f, true)) {
|
||||
items(balanceData?.holdings ?: emptyList()) { holding ->
|
||||
UnifiedStockItemRow(holding) {
|
||||
onStockSelect(holding.code, holding.name, holding.isDomestic, holding.quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BalanceSummaryCard(summary: UnifiedBalance?) {
|
||||
Card(
|
||||
elevation = 2.dp,
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = androidx.compose.ui.graphics.Color.White
|
||||
) {
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("총 평가 자산", style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray)
|
||||
Text(
|
||||
text = "${summary?.totalAsset ?: "0"} 원",
|
||||
style = MaterialTheme.typography.h5,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
val rate = summary?.totalProfitRate?.toDoubleOrNull() ?: 0.0
|
||||
val color = if (rate > 0) androidx.compose.ui.graphics.Color.Red
|
||||
else if (rate < 0) androidx.compose.ui.graphics.Color.Blue
|
||||
else androidx.compose.ui.graphics.Color.DarkGray
|
||||
|
||||
Text(
|
||||
text = "수익률: ${if (rate > 0) "+" else ""}$rate%",
|
||||
color = color,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnifiedStockItemRow(holding: model.UnifiedStockHolding, onClick: () -> Unit) {
|
||||
val avgPrice = holding.avgPrice.toDoubleOrNull() ?: 0.0
|
||||
val breakEvenPrice = if (avgPrice > 0) avgPrice / 0.9978 else 0.0
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable { onClick() },
|
||||
elevation = 1.dp
|
||||
) {
|
||||
Row(modifier = Modifier.padding(12.dp), verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
// 국내/해외 구분 배지
|
||||
Surface(
|
||||
color = if (holding.isDomestic) androidx.compose.ui.graphics.Color(0xFFE3F2FD)
|
||||
else androidx.compose.ui.graphics.Color(0xFFF3E5F5),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (holding.isDomestic) "국내" else "해외",
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
fontSize = 10.sp,
|
||||
color = if (holding.isDomestic) androidx.compose.ui.graphics.Color.Blue
|
||||
else androidx.compose.ui.graphics.Color(0xFF7B1FA2)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(holding.name, fontWeight = FontWeight.Bold, maxLines = 1)
|
||||
}
|
||||
Text(holding.code, style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text("${holding.currentPrice} 원", fontWeight = FontWeight.Bold)
|
||||
// 손익분기점 표시 추가
|
||||
Text(
|
||||
"손익분기: ${String.format("%,.0f", breakEvenPrice)}원",
|
||||
fontSize = 10.sp, color = Color(0xFF666666)
|
||||
)
|
||||
val rate = holding.profitRate.toDoubleOrNull() ?: 0.0
|
||||
Text(
|
||||
text = "${if (rate > 0) "+" else ""}${holding.profitRate}%",
|
||||
color = if (rate > 0) Color.Red else if (rate < 0) Color.Blue else Color.DarkGray,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"매수: ${String.format("%,.0f", avgPrice)}원 ${holding.quantity}",
|
||||
fontSize = 11.sp, color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/BalanceSection.kt
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.clickable
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.material.icons.filled.Refresh
|
||||
//import androidx.compose.runtime.*
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import model.UnifiedBalance
|
||||
//import network.KisTradeService
|
||||
//
|
||||
//@Composable
|
||||
//fun BalanceSection(
|
||||
// tradeService: KisTradeService,
|
||||
// refreshTrigger: Int, // 갱신 트리거 추가
|
||||
// onRefresh: () -> Unit,
|
||||
// onStockSelect: (code: String, name: String, isDomestic: Boolean,quantity: String) -> Unit
|
||||
//) {
|
||||
// var balanceData by remember { mutableStateOf<UnifiedBalance?>(null) }
|
||||
// var isLoading by remember { mutableStateOf(false) }
|
||||
//
|
||||
// // 화면 진입 시 및 갱신 시 데이터 로드
|
||||
// LaunchedEffect(refreshTrigger) {
|
||||
// isLoading = true
|
||||
// tradeService.fetchIntegratedBalance().onSuccess {
|
||||
// balanceData = it
|
||||
// }.onFailure {
|
||||
// println("❌ 잔고 로드 실패: ${it.localizedMessage}")
|
||||
// }
|
||||
// isLoading = false
|
||||
// }
|
||||
//
|
||||
// Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Row(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// horizontalArrangement = Arrangement.SpaceBetween,
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// Text(
|
||||
// text = "나의 자산",
|
||||
// style = MaterialTheme.typography.h6,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// modifier = Modifier.padding(bottom = 8.dp)
|
||||
// )
|
||||
// // 강제 갱신 버튼
|
||||
// IconButton(
|
||||
// onClick = onRefresh,
|
||||
// modifier = Modifier.size(24.dp)
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
|
||||
// contentDescription = "새로고침",
|
||||
// tint = Color(0xFF0E62CF),
|
||||
// modifier = Modifier.size(18.dp)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// // 1. 자산 요약 카드
|
||||
// BalanceSummaryCard(balanceData)
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
//
|
||||
// // 2. 통합 보유 종목 리스트
|
||||
// Text(
|
||||
// text = "보유 종목",
|
||||
// style = MaterialTheme.typography.subtitle1,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// modifier = Modifier.padding(vertical = 8.dp)
|
||||
// )
|
||||
//
|
||||
// if (isLoading) {
|
||||
// Box(Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
|
||||
// CircularProgressIndicator()
|
||||
// }
|
||||
// } else {
|
||||
// LazyColumn(modifier = Modifier.weight(1f, true)) {
|
||||
// items(balanceData?.holdings ?: emptyList()) { holding ->
|
||||
// UnifiedStockItemRow(holding) {
|
||||
// onStockSelect(holding.code, holding.name, holding.isDomestic, holding.quantity)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@Composable
|
||||
//fun BalanceSummaryCard(summary: UnifiedBalance?) {
|
||||
// Card(
|
||||
// elevation = 2.dp,
|
||||
// shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// backgroundColor = androidx.compose.ui.graphics.Color.White
|
||||
// ) {
|
||||
//
|
||||
// Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Text("총 평가 자산", style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray)
|
||||
// Text(
|
||||
// text = "${summary?.totalAsset ?: "0"} 원",
|
||||
// style = MaterialTheme.typography.h5,
|
||||
// fontWeight = FontWeight.Bold
|
||||
// )
|
||||
//
|
||||
// val rate = summary?.totalProfitRate?.toDoubleOrNull() ?: 0.0
|
||||
// val color = if (rate > 0) androidx.compose.ui.graphics.Color.Red
|
||||
// else if (rate < 0) androidx.compose.ui.graphics.Color.Blue
|
||||
// else androidx.compose.ui.graphics.Color.DarkGray
|
||||
//
|
||||
// Text(
|
||||
// text = "수익률: ${if (rate > 0) "+" else ""}$rate%",
|
||||
// color = color,
|
||||
// style = MaterialTheme.typography.body2,
|
||||
// fontWeight = FontWeight.Medium
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@Composable
|
||||
//fun UnifiedStockItemRow(holding: model.UnifiedStockHolding, onClick: () -> Unit) {
|
||||
// val avgPrice = holding.avgPrice.toDoubleOrNull() ?: 0.0
|
||||
// val breakEvenPrice = if (avgPrice > 0) avgPrice / 0.9978 else 0.0
|
||||
//
|
||||
// Card(
|
||||
// modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable { onClick() },
|
||||
// elevation = 1.dp
|
||||
// ) {
|
||||
// Row(modifier = Modifier.padding(12.dp), verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
// Column(modifier = Modifier.weight(1f)) {
|
||||
// Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
// // 국내/해외 구분 배지
|
||||
// Surface(
|
||||
// color = if (holding.isDomestic) androidx.compose.ui.graphics.Color(0xFFE3F2FD)
|
||||
// else androidx.compose.ui.graphics.Color(0xFFF3E5F5),
|
||||
// shape = androidx.compose.foundation.shape.RoundedCornerShape(4.dp)
|
||||
// ) {
|
||||
// Text(
|
||||
// text = if (holding.isDomestic) "국내" else "해외",
|
||||
// modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
// fontSize = 10.sp,
|
||||
// color = if (holding.isDomestic) androidx.compose.ui.graphics.Color.Blue
|
||||
// else androidx.compose.ui.graphics.Color(0xFF7B1FA2)
|
||||
// )
|
||||
// }
|
||||
// Spacer(Modifier.width(4.dp))
|
||||
// Text(holding.name, fontWeight = FontWeight.Bold, maxLines = 1)
|
||||
// }
|
||||
// Text(holding.code, style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray)
|
||||
// }
|
||||
//
|
||||
// Column(horizontalAlignment = Alignment.End) {
|
||||
// Text("${holding.currentPrice} 원", fontWeight = FontWeight.Bold)
|
||||
// // 손익분기점 표시 추가
|
||||
// Text(
|
||||
// "손익분기: ${String.format("%,.0f", breakEvenPrice)}원",
|
||||
// fontSize = 10.sp, color = Color(0xFF666666)
|
||||
// )
|
||||
// val rate = holding.profitRate.toDoubleOrNull() ?: 0.0
|
||||
// Text(
|
||||
// text = "${if (rate > 0) "+" else ""}${holding.profitRate}%",
|
||||
// color = if (rate > 0) Color.Red else if (rate < 0) Color.Blue else Color.DarkGray,
|
||||
// fontSize = 12.sp,
|
||||
// fontWeight = FontWeight.Bold
|
||||
// )
|
||||
// Text(
|
||||
// "매수: ${String.format("%,.0f", avgPrice)}원 ${holding.quantity}",
|
||||
// fontSize = 11.sp, color = Color.Gray
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,66 +1,66 @@
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import model.CandleData
|
||||
|
||||
@Composable
|
||||
fun CandleChart(data: List<CandleData>, modifier: Modifier = Modifier) {
|
||||
if (data.isEmpty()) return
|
||||
|
||||
Canvas(modifier = modifier.fillMaxSize()) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
|
||||
// 데이터가 적을 때도 일정한 너비를 유지하도록 개선
|
||||
val maxDisplayCount = 50
|
||||
val candleWidth = width / maxOf(data.size, maxDisplayCount)
|
||||
val spacing = candleWidth * 0.2f
|
||||
|
||||
// 가격 범위 계산 (여백 추가)
|
||||
val maxPrice = data.maxOf { it.stck_hgpr.toDoubleOrNull() ?: 0.0 }
|
||||
val minPrice = data.minOf { it.stck_lwpr.toDoubleOrNull() ?: 0.0 }
|
||||
val priceRange = (maxPrice - minPrice).let { if (it == 0.0) 1.0 else it * 1.1 }
|
||||
val basePrice = minPrice - (priceRange * 0.05) // 아래쪽 여백
|
||||
|
||||
fun getY(price: Double): Float = (height - ((price - basePrice) / priceRange * height)).toFloat()
|
||||
|
||||
data.forEachIndexed { index, candle ->
|
||||
val open = candle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val close = candle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
val high = candle.stck_hgpr.toDoubleOrNull() ?: 0.0
|
||||
val low = candle.stck_lwpr.toDoubleOrNull() ?: 0.0
|
||||
|
||||
val isRising = close >= open
|
||||
val color = if (isRising) Color(0xFFE03E2D) else Color(0xFF0E62CF)
|
||||
|
||||
val x = index * candleWidth + spacing / 2
|
||||
val currentCandleWidth = candleWidth - spacing
|
||||
|
||||
// 2. 꼬리 그리기 (High-Low Line)
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x + currentCandleWidth / 2, getY(high)),
|
||||
end = Offset(x + currentCandleWidth / 2, getY(low)),
|
||||
strokeWidth = 2f
|
||||
)
|
||||
|
||||
// 3. 몸통 그리기 (Open-Close Rect)
|
||||
val bodyTop = getY(maxOf(open, close))
|
||||
val bodyBottom = getY(minOf(open, close))
|
||||
val bodyHeight = maxOf(bodyBottom - bodyTop, 1f) // 최소 1픽셀 보장
|
||||
|
||||
drawRect(
|
||||
color = color,
|
||||
topLeft = Offset(x, bodyTop),
|
||||
size = Size(currentCandleWidth, bodyHeight)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.Canvas
|
||||
//import androidx.compose.foundation.layout.fillMaxSize
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.geometry.Offset
|
||||
//import androidx.compose.ui.geometry.Size
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
//import model.CandleData
|
||||
//
|
||||
//@Composable
|
||||
//fun CandleChart(data: List<CandleData>, modifier: Modifier = Modifier) {
|
||||
// if (data.isEmpty()) return
|
||||
//
|
||||
// Canvas(modifier = modifier.fillMaxSize()) {
|
||||
// val width = size.width
|
||||
// val height = size.height
|
||||
//
|
||||
// // 데이터가 적을 때도 일정한 너비를 유지하도록 개선
|
||||
// val maxDisplayCount = 50
|
||||
// val candleWidth = width / maxOf(data.size, maxDisplayCount)
|
||||
// val spacing = candleWidth * 0.2f
|
||||
//
|
||||
// // 가격 범위 계산 (여백 추가)
|
||||
// val maxPrice = data.maxOf { it.stck_hgpr.toDoubleOrNull() ?: 0.0 }
|
||||
// val minPrice = data.minOf { it.stck_lwpr.toDoubleOrNull() ?: 0.0 }
|
||||
// val priceRange = (maxPrice - minPrice).let { if (it == 0.0) 1.0 else it * 1.1 }
|
||||
// val basePrice = minPrice - (priceRange * 0.05) // 아래쪽 여백
|
||||
//
|
||||
// fun getY(price: Double): Float = (height - ((price - basePrice) / priceRange * height)).toFloat()
|
||||
//
|
||||
// data.forEachIndexed { index, candle ->
|
||||
// val open = candle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
// val close = candle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
// val high = candle.stck_hgpr.toDoubleOrNull() ?: 0.0
|
||||
// val low = candle.stck_lwpr.toDoubleOrNull() ?: 0.0
|
||||
//
|
||||
// val isRising = close >= open
|
||||
// val color = if (isRising) Color(0xFFE03E2D) else Color(0xFF0E62CF)
|
||||
//
|
||||
// val x = index * candleWidth + spacing / 2
|
||||
// val currentCandleWidth = candleWidth - spacing
|
||||
//
|
||||
// // 2. 꼬리 그리기 (High-Low Line)
|
||||
// drawLine(
|
||||
// color = color,
|
||||
// start = Offset(x + currentCandleWidth / 2, getY(high)),
|
||||
// end = Offset(x + currentCandleWidth / 2, getY(low)),
|
||||
// strokeWidth = 2f
|
||||
// )
|
||||
//
|
||||
// // 3. 몸통 그리기 (Open-Close Rect)
|
||||
// val bodyTop = getY(maxOf(open, close))
|
||||
// val bodyBottom = getY(minOf(open, close))
|
||||
// val bodyHeight = maxOf(bodyBottom - bodyTop, 1f) // 최소 1픽셀 보장
|
||||
//
|
||||
// drawRect(
|
||||
// color = color,
|
||||
// topLeft = Offset(x, bodyTop),
|
||||
// size = Size(currentCandleWidth, bodyHeight)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,495 +1,495 @@
|
||||
// src/main/kotlin/ui/DashboardScreen.kt
|
||||
package ui
|
||||
|
||||
import AutoTradeItem
|
||||
import network.TradingDecision
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.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.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.CandleData
|
||||
import model.ConfigIndex
|
||||
import model.ExecutionData
|
||||
import model.KisSession
|
||||
import model.StockBasicInfo
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import service.AutoTradingManager
|
||||
import service.TechnicalAnalyzer
|
||||
import service.TradingDecisionCallback
|
||||
import util.MarketUtil
|
||||
import kotlin.collections.mutableListOf
|
||||
|
||||
@Composable
|
||||
fun DashboardScreen() {
|
||||
val tradeService = remember { KisTradeService }
|
||||
val wsManager = remember { KisWebSocketManager }
|
||||
val scope = rememberCoroutineScope()
|
||||
var selectedStockCode by remember { mutableStateOf("") }
|
||||
var selectedStockName by remember { mutableStateOf("") }
|
||||
var isDomestic by remember { mutableStateOf(true) }
|
||||
var selectedStockQuantity by remember { mutableStateOf("0") }
|
||||
|
||||
var selectedItem by remember { mutableStateOf<AutoTradeItem?>(null) } // 감시/미체결 아이템 선택 시
|
||||
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
||||
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
||||
|
||||
|
||||
var min30 by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var daySummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var weekSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var monthSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var yearSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
|
||||
|
||||
|
||||
var callback = object : TradingDecisionCallback {
|
||||
override fun invoke(decision: TradingDecision?, isSuccess: Boolean) {
|
||||
if (!isSuccess && decision?.confidence ?: 0.0 < 0.0) {
|
||||
decision?.stockCode?.let { stockCode ->
|
||||
decision?.stockName?.let { stockName ->
|
||||
selectedStockCode = stockCode
|
||||
selectedStockName = stockName
|
||||
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||
}
|
||||
}
|
||||
|
||||
}else if (isSuccess && decision != null) {
|
||||
if (!selectedStockCode.equals(decision.stockCode) && selectedStockName.equals(decision.stockName)) {
|
||||
selectedStockCode = decision.stockCode
|
||||
selectedStockName = decision.stockName
|
||||
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||
}
|
||||
// 2. 결정 객체 업데이트 -> IntegratedOrderSection의 LaunchedEffect 트리거
|
||||
completeTradingDecision = decision
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 리소스 정리는 여전히 DisposableEffect에서 수행
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
AutoTradingManager.stopDiscovery()
|
||||
}
|
||||
}
|
||||
|
||||
// 중앙 관리용 상태들
|
||||
var refreshTrigger by remember { mutableStateOf(0) }
|
||||
// [핵심] 아직 DB에 등록되기 전에 도착한 체결 데이터를 임시 보관하는 버퍼
|
||||
val executionCache = remember { mutableMapOf<String, ExecutionData>() }
|
||||
|
||||
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||
|
||||
LaunchedEffect(refreshTrigger) {
|
||||
// setupAutoTradingWatchdog(tradeService,callback)
|
||||
}
|
||||
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||
suspend fun syncAndExecute(orderNo: String) {
|
||||
if (processingIds.contains(orderNo)) return
|
||||
processingIds.add(orderNo)
|
||||
|
||||
try {
|
||||
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
||||
val execData = executionCache[orderNo]
|
||||
|
||||
if (dbItem != null && execData != null && execData.isFilled) {
|
||||
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
||||
// 1. 실제 매수 체결가 가져오기 (문자열인 경우 숫자로 변환)
|
||||
val actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
|
||||
|
||||
// 2. 최소 마진 설정 (수수료/세금 0.3% + 순수익 1.5% = 1.8%)
|
||||
|
||||
val minEffectiveRate = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||
|
||||
// 3. DB에 설정된 목표 수익률과 최소 보장 수익률 중 큰 값 선택
|
||||
val finalProfitRate = maxOf(dbItem.profitRate, minEffectiveRate)
|
||||
|
||||
// 4. 실제 체결가 기준 익절 가격 재계산 및 틱 사이즈 보정
|
||||
val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
|
||||
|
||||
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% 적용)")
|
||||
|
||||
tradeService.postOrder(
|
||||
stockCode = dbItem.code,
|
||||
qty = dbItem.quantity.toString(),
|
||||
price = finalTargetPrice.toLong().toString(),
|
||||
isBuy = false
|
||||
).onSuccess { newSellOrderNo ->
|
||||
// 익절가 업데이트 및 상태 변경
|
||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
||||
// (선택 사항) 실제 계산된 익절가를 DB에 기록하고 싶다면 별도 update 로직 추가 가능
|
||||
|
||||
executionCache.remove(orderNo)
|
||||
refreshTrigger++
|
||||
}.onFailure {
|
||||
println("❌ 익절 주문 실패: ${it.message}")
|
||||
}
|
||||
} else if (dbItem.status == TradeStatus.SELLING) {
|
||||
println("🎊 [매칭 성공] 매도 완료 처리: ${dbItem.name}")
|
||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.COMPLETED)
|
||||
executionCache.remove(orderNo)
|
||||
refreshTrigger++
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
processingIds.remove(orderNo)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// 1. 웹소켓 연결
|
||||
wsManager.connect()
|
||||
|
||||
// 2. [기동 시 동기화 시나리오]
|
||||
scope.launch {
|
||||
// (1) 서버 미체결 내역 로드
|
||||
val serverOrders = tradeService.fetchUnfilledOrders().getOrDefault(emptyList())
|
||||
val serverOrderNos = serverOrders.map { it.ord_no }
|
||||
|
||||
// (2) DB 상태 대조 및 EXPIRED 전환
|
||||
DatabaseFactory.syncWithServer(serverOrderNos)
|
||||
|
||||
// (3) 활성 감시 종목 구독 재개
|
||||
val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY))
|
||||
val monitoringCodes = monitoringTrades.map { it.code }.toSet()
|
||||
wsManager.updateSubscriptions(monitoringCodes)
|
||||
|
||||
refreshTrigger++
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
|
||||
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
|
||||
scope.launch {
|
||||
val exec = ExecutionData(orderNo, code, price, qty, isBuy)
|
||||
executionCache[orderNo] = exec
|
||||
syncAndExecute(orderNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||
// [좌측 25%] 내 자산 및 통합 잔고
|
||||
Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
BalanceSection(tradeService,
|
||||
onRefresh = { refreshTrigger++ },
|
||||
refreshTrigger = refreshTrigger) { code, name, isDom,qty ->
|
||||
selectedStockCode = code
|
||||
selectedStockName = name
|
||||
isDomestic = isDom
|
||||
selectedStockQuantity = qty
|
||||
println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
// [중앙 45%] 실시간 정보 및 주문
|
||||
Column(modifier = Modifier.weight(0.40f).fillMaxHeight().background(Color.White)) {
|
||||
if (selectedStockCode.isNotEmpty()) {
|
||||
StockDetailSection(
|
||||
min30 = min30,
|
||||
daySummary = daySummary,
|
||||
monthSummary = monthSummary,
|
||||
weekSummary = weekSummary,
|
||||
yearSummary = yearSummary,
|
||||
stockCode = selectedStockCode,
|
||||
stockName = selectedStockName,
|
||||
holdingQuantity = selectedStockQuantity,
|
||||
isDomestic = isDomestic,
|
||||
tradeService = tradeService,
|
||||
wsManager = wsManager,
|
||||
onOrderSaved = { orderNo ->
|
||||
scope.launch {
|
||||
syncAndExecute(orderNo) // 매칭 시도
|
||||
}
|
||||
},
|
||||
completeTradingDecision = completeTradingDecision,
|
||||
)
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("분석할 종목을 선택하세요", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
Column(modifier = Modifier.weight(0.25f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||
AiAnalysisView(
|
||||
technicalAnalyzer = TechnicalAnalyzer().apply {
|
||||
this.min30 = min30
|
||||
this.daily = daySummary
|
||||
this.weekly = weekSummary
|
||||
this.monthly = monthSummary
|
||||
this.weekly = weekSummary
|
||||
},
|
||||
stockCode = selectedStockCode,
|
||||
stockName = selectedStockName,
|
||||
currentPrice = "0",
|
||||
trades = wsManager.tradeLogs,
|
||||
tradingDecisionCallback = { decision,bool ->
|
||||
if (bool && decision != null && KisSession.config.isSimulation) {
|
||||
completeTradingDecision = decision
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("설정값 관리", style = MaterialTheme.typography.subtitle2, modifier = Modifier.padding(bottom = 4.dp))
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2), // 2열 병렬 배치
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().weight(0.3f)
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
"💰 거래 기본 설정",
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(top = 16.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.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.h6,
|
||||
modifier = Modifier.padding(top = 16.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.h6,
|
||||
modifier = Modifier.padding(top = 16.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
|
||||
//// src/main/kotlin/ui/DashboardScreen.kt
|
||||
//package ui
|
||||
//
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalDivider()
|
||||
Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
AutoTradeSection(
|
||||
isDomestic = isDomestic,
|
||||
tradeService = tradeService,
|
||||
onRefresh = { refreshTrigger++ },
|
||||
refreshTrigger = refreshTrigger , // 트리거 전달
|
||||
onItemCancel = { item ->
|
||||
scope.launch {
|
||||
tradeService.cancelOrder(item.orderNo,item.code).onSuccess {
|
||||
refreshTrigger++
|
||||
}
|
||||
}
|
||||
},
|
||||
onItemSelect = { item ->
|
||||
selectedStockCode = item.code
|
||||
selectedStockName = item.name
|
||||
isDomestic = item.isDomestic
|
||||
})
|
||||
}
|
||||
VerticalDivider()
|
||||
// [우측 30%] 시장 추천 TOP 20 (실전 데이터)
|
||||
Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
MarketSection(tradeService) { code, name, isDom ->
|
||||
val info = StockBasicInfo(
|
||||
code = code,
|
||||
name = name,
|
||||
isDomestic = isDom
|
||||
)
|
||||
selectedStockInfo = info
|
||||
selectedStockCode = code
|
||||
selectedStockName = name
|
||||
isDomestic = isDom
|
||||
println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun VerticalDivider() {
|
||||
Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray))
|
||||
}
|
||||
|
||||
fun findLongestCommonSubstring(s1: String, s2: String): String {
|
||||
if (s1.isEmpty() || s2.isEmpty()) return ""
|
||||
|
||||
var longest = ""
|
||||
// 더 짧은 문자열을 기준으로 삼아 반복 횟수를 줄임
|
||||
val reference = if (s1.length <= s2.length) s1 else s2
|
||||
val target = if (s1.length <= s2.length) s2 else s1
|
||||
|
||||
for (i in reference.indices) {
|
||||
for (j in (i + longest.length + 1)..reference.length) {
|
||||
val sub = reference.substring(i, j)
|
||||
if (target.contains(sub)) {
|
||||
if (sub.length > longest.length) {
|
||||
longest = sub
|
||||
}
|
||||
} else {
|
||||
// target에 포함되지 않으면 더 긴 substring은 존재할 수 없으므로 탈출
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return longest
|
||||
}
|
||||
|
||||
fun getRemaining(original: String, common: String): String {
|
||||
if (common.isEmpty()) return original
|
||||
// 가장 처음 발견되는 공통 문자열을 한 번만 제거
|
||||
return original.replaceFirst(common, "").trim()
|
||||
}
|
||||
//import AutoTradeItem
|
||||
//import network.TradingDecision
|
||||
//import androidx.compose.foundation.background
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//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.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.input.ImeAction
|
||||
//import androidx.compose.ui.text.input.KeyboardType
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import kotlinx.coroutines.launch
|
||||
//import model.CandleData
|
||||
//import model.ConfigIndex
|
||||
//import model.ExecutionData
|
||||
//import model.KisSession
|
||||
//import model.StockBasicInfo
|
||||
//import network.KisTradeService
|
||||
//import network.KisWebSocketManager
|
||||
//import service.AutoTradingManager
|
||||
//import service.TechnicalAnalyzer
|
||||
//import service.TradingDecisionCallback
|
||||
//import util.MarketUtil
|
||||
//import kotlin.collections.mutableListOf
|
||||
//
|
||||
//@Composable
|
||||
//fun DashboardScreen() {
|
||||
// val tradeService = remember { KisTradeService }
|
||||
// val wsManager = remember { KisWebSocketManager }
|
||||
// val scope = rememberCoroutineScope()
|
||||
// var selectedStockCode by remember { mutableStateOf("") }
|
||||
// var selectedStockName by remember { mutableStateOf("") }
|
||||
// var isDomestic by remember { mutableStateOf(true) }
|
||||
// var selectedStockQuantity by remember { mutableStateOf("0") }
|
||||
//
|
||||
// var selectedItem by remember { mutableStateOf<AutoTradeItem?>(null) } // 감시/미체결 아이템 선택 시
|
||||
// var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
||||
// var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
||||
//
|
||||
//
|
||||
// var min30 by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
// var daySummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
// var weekSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
// var monthSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
// var yearSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
//
|
||||
//
|
||||
//
|
||||
// var callback = object : TradingDecisionCallback {
|
||||
// override fun invoke(decision: TradingDecision?, isSuccess: Boolean) {
|
||||
// if (!isSuccess && decision?.confidence ?: 0.0 < 0.0) {
|
||||
// decision?.stockCode?.let { stockCode ->
|
||||
// decision?.stockName?.let { stockName ->
|
||||
// selectedStockCode = stockCode
|
||||
// selectedStockName = stockName
|
||||
// isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }else if (isSuccess && decision != null) {
|
||||
// if (!selectedStockCode.equals(decision.stockCode) && selectedStockName.equals(decision.stockName)) {
|
||||
// selectedStockCode = decision.stockCode
|
||||
// selectedStockName = decision.stockName
|
||||
// isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||
// }
|
||||
// // 2. 결정 객체 업데이트 -> IntegratedOrderSection의 LaunchedEffect 트리거
|
||||
// completeTradingDecision = decision
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//// 리소스 정리는 여전히 DisposableEffect에서 수행
|
||||
// DisposableEffect(Unit) {
|
||||
// onDispose {
|
||||
// AutoTradingManager.stopDiscovery()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//// 중앙 관리용 상태들
|
||||
// var refreshTrigger by remember { mutableStateOf(0) }
|
||||
// // [핵심] 아직 DB에 등록되기 전에 도착한 체결 데이터를 임시 보관하는 버퍼
|
||||
// val executionCache = remember { mutableMapOf<String, ExecutionData>() }
|
||||
//
|
||||
// // [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||
//
|
||||
// LaunchedEffect(refreshTrigger) {
|
||||
//// setupAutoTradingWatchdog(tradeService,callback)
|
||||
// }
|
||||
// val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||
// suspend fun syncAndExecute(orderNo: String) {
|
||||
// if (processingIds.contains(orderNo)) return
|
||||
// processingIds.add(orderNo)
|
||||
//
|
||||
// try {
|
||||
// val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
||||
// val execData = executionCache[orderNo]
|
||||
//
|
||||
// if (dbItem != null && execData != null && execData.isFilled) {
|
||||
// if (dbItem.status == TradeStatus.PENDING_BUY) {
|
||||
// // 1. 실제 매수 체결가 가져오기 (문자열인 경우 숫자로 변환)
|
||||
// val actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
|
||||
//
|
||||
// // 2. 최소 마진 설정 (수수료/세금 0.3% + 순수익 1.5% = 1.8%)
|
||||
//
|
||||
// val minEffectiveRate = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||
//
|
||||
// // 3. DB에 설정된 목표 수익률과 최소 보장 수익률 중 큰 값 선택
|
||||
// val finalProfitRate = maxOf(dbItem.profitRate, minEffectiveRate)
|
||||
//
|
||||
// // 4. 실제 체결가 기준 익절 가격 재계산 및 틱 사이즈 보정
|
||||
// val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
|
||||
//
|
||||
// println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% 적용)")
|
||||
//
|
||||
// tradeService.postOrder(
|
||||
// stockCode = dbItem.code,
|
||||
// qty = dbItem.quantity.toString(),
|
||||
// price = finalTargetPrice.toLong().toString(),
|
||||
// isBuy = false
|
||||
// ).onSuccess { newSellOrderNo ->
|
||||
// // 익절가 업데이트 및 상태 변경
|
||||
// DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
||||
// // (선택 사항) 실제 계산된 익절가를 DB에 기록하고 싶다면 별도 update 로직 추가 가능
|
||||
//
|
||||
// executionCache.remove(orderNo)
|
||||
// refreshTrigger++
|
||||
// }.onFailure {
|
||||
// println("❌ 익절 주문 실패: ${it.message}")
|
||||
// }
|
||||
// } else if (dbItem.status == TradeStatus.SELLING) {
|
||||
// println("🎊 [매칭 성공] 매도 완료 처리: ${dbItem.name}")
|
||||
// DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.COMPLETED)
|
||||
// executionCache.remove(orderNo)
|
||||
// refreshTrigger++
|
||||
// }
|
||||
// }
|
||||
// } finally {
|
||||
// processingIds.remove(orderNo)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// LaunchedEffect(Unit) {
|
||||
// // 1. 웹소켓 연결
|
||||
// wsManager.connect()
|
||||
//
|
||||
// // 2. [기동 시 동기화 시나리오]
|
||||
// scope.launch {
|
||||
// // (1) 서버 미체결 내역 로드
|
||||
// val serverOrders = tradeService.fetchUnfilledOrders().getOrDefault(emptyList())
|
||||
// val serverOrderNos = serverOrders.map { it.ord_no }
|
||||
//
|
||||
// // (2) DB 상태 대조 및 EXPIRED 전환
|
||||
// DatabaseFactory.syncWithServer(serverOrderNos)
|
||||
//
|
||||
// // (3) 활성 감시 종목 구독 재개
|
||||
// val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY))
|
||||
// val monitoringCodes = monitoringTrades.map { it.code }.toSet()
|
||||
// wsManager.updateSubscriptions(monitoringCodes)
|
||||
//
|
||||
// refreshTrigger++
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// // 3. 실시간 체결 통보 핸들러 (주문번호 중심)
|
||||
// wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
|
||||
// scope.launch {
|
||||
// val exec = ExecutionData(orderNo, code, price, qty, isBuy)
|
||||
// executionCache[orderNo] = exec
|
||||
// syncAndExecute(orderNo)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||
// // [좌측 25%] 내 자산 및 통합 잔고
|
||||
// Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
// BalanceSection(tradeService,
|
||||
// onRefresh = { refreshTrigger++ },
|
||||
// refreshTrigger = refreshTrigger) { code, name, isDom,qty ->
|
||||
// selectedStockCode = code
|
||||
// selectedStockName = name
|
||||
// isDomestic = isDom
|
||||
// selectedStockQuantity = qty
|
||||
// println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VerticalDivider()
|
||||
//
|
||||
// // [중앙 45%] 실시간 정보 및 주문
|
||||
// Column(modifier = Modifier.weight(0.40f).fillMaxHeight().background(Color.White)) {
|
||||
// if (selectedStockCode.isNotEmpty()) {
|
||||
// StockDetailSection(
|
||||
// min30 = min30,
|
||||
// daySummary = daySummary,
|
||||
// monthSummary = monthSummary,
|
||||
// weekSummary = weekSummary,
|
||||
// yearSummary = yearSummary,
|
||||
// stockCode = selectedStockCode,
|
||||
// stockName = selectedStockName,
|
||||
// holdingQuantity = selectedStockQuantity,
|
||||
// isDomestic = isDomestic,
|
||||
// tradeService = tradeService,
|
||||
// wsManager = wsManager,
|
||||
// onOrderSaved = { orderNo ->
|
||||
// scope.launch {
|
||||
// syncAndExecute(orderNo) // 매칭 시도
|
||||
// }
|
||||
// },
|
||||
// completeTradingDecision = completeTradingDecision,
|
||||
// )
|
||||
// } else {
|
||||
// Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
// Text("분석할 종목을 선택하세요", color = Color.Gray)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VerticalDivider()
|
||||
//
|
||||
// Column(modifier = Modifier.weight(0.25f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||
// AiAnalysisView(
|
||||
// technicalAnalyzer = TechnicalAnalyzer().apply {
|
||||
// this.min30 = min30
|
||||
// this.daily = daySummary
|
||||
// this.weekly = weekSummary
|
||||
// this.monthly = monthSummary
|
||||
// this.weekly = weekSummary
|
||||
// },
|
||||
// stockCode = selectedStockCode,
|
||||
// stockName = selectedStockName,
|
||||
// currentPrice = "0",
|
||||
// trades = wsManager.tradeLogs,
|
||||
// tradingDecisionCallback = { decision,bool ->
|
||||
// if (bool && decision != null && KisSession.config.isSimulation) {
|
||||
// completeTradingDecision = decision
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
// Text("설정값 관리", style = MaterialTheme.typography.subtitle2, modifier = Modifier.padding(bottom = 4.dp))
|
||||
// LazyVerticalGrid(
|
||||
// columns = GridCells.Fixed(2), // 2열 병렬 배치
|
||||
// horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
// verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
// modifier = Modifier.fillMaxWidth().weight(0.3f)
|
||||
// ) {
|
||||
// item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
// Text(
|
||||
// "💰 거래 기본 설정",
|
||||
// style = MaterialTheme.typography.h6,
|
||||
// modifier = Modifier.padding(top = 16.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.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.h6,
|
||||
// modifier = Modifier.padding(top = 16.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.h6,
|
||||
// modifier = Modifier.padding(top = 16.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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// VerticalDivider()
|
||||
// Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
// AutoTradeSection(
|
||||
// isDomestic = isDomestic,
|
||||
// tradeService = tradeService,
|
||||
// onRefresh = { refreshTrigger++ },
|
||||
// refreshTrigger = refreshTrigger , // 트리거 전달
|
||||
// onItemCancel = { item ->
|
||||
// scope.launch {
|
||||
// tradeService.cancelOrder(item.orderNo,item.code).onSuccess {
|
||||
// refreshTrigger++
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// onItemSelect = { item ->
|
||||
// selectedStockCode = item.code
|
||||
// selectedStockName = item.name
|
||||
// isDomestic = item.isDomestic
|
||||
// })
|
||||
// }
|
||||
// VerticalDivider()
|
||||
// // [우측 30%] 시장 추천 TOP 20 (실전 데이터)
|
||||
// Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
// MarketSection(tradeService) { code, name, isDom ->
|
||||
// val info = StockBasicInfo(
|
||||
// code = code,
|
||||
// name = name,
|
||||
// isDomestic = isDom
|
||||
// )
|
||||
// selectedStockInfo = info
|
||||
// selectedStockCode = code
|
||||
// selectedStockName = name
|
||||
// isDomestic = isDom
|
||||
// println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
//
|
||||
//
|
||||
//
|
||||
//@Composable
|
||||
//fun VerticalDivider() {
|
||||
// Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray))
|
||||
//}
|
||||
//
|
||||
//fun findLongestCommonSubstring(s1: String, s2: String): String {
|
||||
// if (s1.isEmpty() || s2.isEmpty()) return ""
|
||||
//
|
||||
// var longest = ""
|
||||
// // 더 짧은 문자열을 기준으로 삼아 반복 횟수를 줄임
|
||||
// val reference = if (s1.length <= s2.length) s1 else s2
|
||||
// val target = if (s1.length <= s2.length) s2 else s1
|
||||
//
|
||||
// for (i in reference.indices) {
|
||||
// for (j in (i + longest.length + 1)..reference.length) {
|
||||
// val sub = reference.substring(i, j)
|
||||
// if (target.contains(sub)) {
|
||||
// if (sub.length > longest.length) {
|
||||
// longest = sub
|
||||
// }
|
||||
// } else {
|
||||
// // target에 포함되지 않으면 더 긴 substring은 존재할 수 없으므로 탈출
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return longest
|
||||
//}
|
||||
//
|
||||
//fun getRemaining(original: String, common: String): String {
|
||||
// if (common.isEmpty()) return original
|
||||
// // 가장 처음 발견되는 공통 문자열을 한 번만 제거
|
||||
// return original.replaceFirst(common, "").trim()
|
||||
//}
|
||||
File diff suppressed because it is too large
Load Diff
+101
-101
@@ -1,101 +1,101 @@
|
||||
// src/main/kotlin/ui/MarketSection.kt
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.RankingStock
|
||||
import model.RankingType
|
||||
import network.KisTradeService
|
||||
|
||||
@Composable
|
||||
fun MarketSection(
|
||||
tradeService: KisTradeService,
|
||||
onStockSelect: (code: String, name: String, isDomestic: Boolean) -> Unit
|
||||
) {
|
||||
var selectedTab by remember { mutableStateOf(RankingType.VOLUME) }
|
||||
var isDomestic by remember { mutableStateOf(true) }
|
||||
var rankingList by remember { mutableStateOf<List<RankingStock>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
// 탭 또는 국가 변경 시 데이터 로드
|
||||
LaunchedEffect(selectedTab, isDomestic) {
|
||||
isLoading = true
|
||||
tradeService.fetchMarketRanking(selectedTab, isDomestic).onSuccess {
|
||||
rankingList = it
|
||||
}.onFailure {
|
||||
rankingList = emptyList()
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// [1] 상단 타이틀 및 국내/해외 토글
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("시장 랭킹 (TOP 20)", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold)
|
||||
|
||||
// 국내/해외 전환 스위치 방식
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(if (isDomestic) "국내" else "해외", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
Switch(
|
||||
checked = !isDomestic,
|
||||
onCheckedChange = { isDomestic = !it },
|
||||
colors = SwitchDefaults.colors(checkedThumbColor = Color(0xFF0E62CF))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// [2] 랭킹 타입 탭 (상승, 하락, 거래량 등)
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = selectedTab.ordinal,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = Color.Black,
|
||||
edgePadding = 0.dp,
|
||||
indicator = { tabPositions ->
|
||||
TabRowDefaults.Indicator(
|
||||
Modifier.tabIndicatorOffset(tabPositions[selectedTab.ordinal]),
|
||||
color = Color(0xFFE03E2D)
|
||||
)
|
||||
}
|
||||
) {
|
||||
RankingType.values().forEach { type ->
|
||||
Tab(
|
||||
selected = selectedTab == type,
|
||||
onClick = { selectedTab = type },
|
||||
text = { Text(type.title, fontSize = 12.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// [3] 랭킹 리스트
|
||||
if (isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(rankingList.withIndex().toList()) { (index, stock) ->
|
||||
MarketStockItemRow(index + 1, stock) {
|
||||
onStockSelect(stock.code, stock.name, isDomestic)
|
||||
}
|
||||
Divider(color = Color(0xFFF5F5F5), thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/MarketSection.kt
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
||||
//import androidx.compose.runtime.*
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import model.RankingStock
|
||||
//import model.RankingType
|
||||
//import network.KisTradeService
|
||||
//
|
||||
//@Composable
|
||||
//fun MarketSection(
|
||||
// tradeService: KisTradeService,
|
||||
// onStockSelect: (code: String, name: String, isDomestic: Boolean) -> Unit
|
||||
//) {
|
||||
// var selectedTab by remember { mutableStateOf(RankingType.VOLUME) }
|
||||
// var isDomestic by remember { mutableStateOf(true) }
|
||||
// var rankingList by remember { mutableStateOf<List<RankingStock>>(emptyList()) }
|
||||
// var isLoading by remember { mutableStateOf(false) }
|
||||
//
|
||||
// // 탭 또는 국가 변경 시 데이터 로드
|
||||
// LaunchedEffect(selectedTab, isDomestic) {
|
||||
// isLoading = true
|
||||
// tradeService.fetchMarketRanking(selectedTab, isDomestic).onSuccess {
|
||||
// rankingList = it
|
||||
// }.onFailure {
|
||||
// rankingList = emptyList()
|
||||
// }
|
||||
// isLoading = false
|
||||
// }
|
||||
//
|
||||
// Column(modifier = Modifier.fillMaxSize()) {
|
||||
// // [1] 상단 타이틀 및 국내/해외 토글
|
||||
// Row(
|
||||
// modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// horizontalArrangement = Arrangement.SpaceBetween
|
||||
// ) {
|
||||
// Text("시장 랭킹 (TOP 20)", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold)
|
||||
//
|
||||
// // 국내/해외 전환 스위치 방식
|
||||
// Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Text(if (isDomestic) "국내" else "해외", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
// Switch(
|
||||
// checked = !isDomestic,
|
||||
// onCheckedChange = { isDomestic = !it },
|
||||
// colors = SwitchDefaults.colors(checkedThumbColor = Color(0xFF0E62CF))
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // [2] 랭킹 타입 탭 (상승, 하락, 거래량 등)
|
||||
// ScrollableTabRow(
|
||||
// selectedTabIndex = selectedTab.ordinal,
|
||||
// backgroundColor = Color.Transparent,
|
||||
// contentColor = Color.Black,
|
||||
// edgePadding = 0.dp,
|
||||
// indicator = { tabPositions ->
|
||||
// TabRowDefaults.Indicator(
|
||||
// Modifier.tabIndicatorOffset(tabPositions[selectedTab.ordinal]),
|
||||
// color = Color(0xFFE03E2D)
|
||||
// )
|
||||
// }
|
||||
// ) {
|
||||
// RankingType.values().forEach { type ->
|
||||
// Tab(
|
||||
// selected = selectedTab == type,
|
||||
// onClick = { selectedTab = type },
|
||||
// text = { Text(type.title, fontSize = 12.sp) }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// // [3] 랭킹 리스트
|
||||
// if (isLoading) {
|
||||
// Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
// CircularProgressIndicator(strokeWidth = 2.dp)
|
||||
// }
|
||||
// } else {
|
||||
// LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
// items(rankingList.withIndex().toList()) { (index, stock) ->
|
||||
// MarketStockItemRow(index + 1, stock) {
|
||||
// onStockSelect(stock.code, stock.name, isDomestic)
|
||||
// }
|
||||
// Divider(color = Color(0xFFF5F5F5), thickness = 0.5.dp)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,62 +1,62 @@
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.RankingStock
|
||||
import model.RankingType
|
||||
import network.KisTradeService
|
||||
|
||||
|
||||
// src/main/kotlin/ui/MarketStockItemRow.kt
|
||||
@Composable
|
||||
fun MarketStockItemRow(
|
||||
rank: Int,
|
||||
stock: RankingStock,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 순위 표시
|
||||
Text(
|
||||
text = rank.toString(),
|
||||
modifier = Modifier.width(24.dp),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (rank <= 3) Color(0xFFE03E2D) else Color.Gray
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stock.name, fontSize = 13.sp, fontWeight = FontWeight.Medium, maxLines = 1)
|
||||
Text(stock.code, fontSize = 10.sp, color = Color.Gray)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = String.format("%,d", stock.stck_prpr.toLongOrNull() ?: 0L),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
Text(
|
||||
text = "${if (rate > 0) "+" else ""}${stock.prdy_ctrt}%",
|
||||
fontSize = 11.sp,
|
||||
color = if (rate > 0) Color(0xFFE03E2D) else if (rate < 0) Color(0xFF0E62CF) else Color.DarkGray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.clickable
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.runtime.*
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import model.RankingStock
|
||||
//import model.RankingType
|
||||
//import network.KisTradeService
|
||||
//
|
||||
//
|
||||
//// src/main/kotlin/ui/MarketStockItemRow.kt
|
||||
//@Composable
|
||||
//fun MarketStockItemRow(
|
||||
// rank: Int,
|
||||
// stock: RankingStock,
|
||||
// onClick: () -> Unit
|
||||
//) {
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .clickable { onClick() }
|
||||
// .padding(vertical = 10.dp, horizontal = 4.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// // 순위 표시
|
||||
// Text(
|
||||
// text = rank.toString(),
|
||||
// modifier = Modifier.width(24.dp),
|
||||
// fontSize = 14.sp,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// color = if (rank <= 3) Color(0xFFE03E2D) else Color.Gray
|
||||
// )
|
||||
//
|
||||
// Column(modifier = Modifier.weight(1f)) {
|
||||
// Text(stock.name, fontSize = 13.sp, fontWeight = FontWeight.Medium, maxLines = 1)
|
||||
// Text(stock.code, fontSize = 10.sp, color = Color.Gray)
|
||||
// }
|
||||
//
|
||||
// Column(horizontalAlignment = Alignment.End) {
|
||||
// Text(
|
||||
// text = String.format("%,d", stock.stck_prpr.toLongOrNull() ?: 0L),
|
||||
// fontSize = 13.sp,
|
||||
// fontWeight = FontWeight.Bold
|
||||
// )
|
||||
// val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||
// Text(
|
||||
// text = "${if (rate > 0) "+" else ""}${stock.prdy_ctrt}%",
|
||||
// fontSize = 11.sp,
|
||||
// color = if (rate > 0) Color(0xFFE03E2D) else if (rate < 0) Color(0xFF0E62CF) else Color.DarkGray
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,57 +1,57 @@
|
||||
// src/main/kotlin/ui/PeriodTrendCard.kt (신규/통합)
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.CandleData
|
||||
|
||||
@Composable
|
||||
fun PeriodTrendCard(label: String, data: List<CandleData>, modifier: Modifier = Modifier) {
|
||||
val avgPrice = if (data.isEmpty()) "0"
|
||||
else String.format("%,d", data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }.average().toLong())
|
||||
|
||||
Card(modifier = modifier.height(80.dp), elevation = 2.dp, backgroundColor = Color.White) {
|
||||
Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
// [좌측] 라벨 및 평균가
|
||||
Column(modifier = Modifier.weight(0.4f)) {
|
||||
Text(label, fontSize = 10.sp, color = Color.Gray)
|
||||
Text(text = "${avgPrice}원", fontSize = 12.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
// [우측] 간소화된 그래프 (Sparkline)
|
||||
Box(modifier = Modifier.weight(0.6f).fillMaxHeight()) {
|
||||
if (data.isNotEmpty()) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val prices = data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }
|
||||
val max = prices.maxOrNull() ?: 1.0
|
||||
val min = prices.minOrNull() ?: 0.0
|
||||
val range = if (max == min) 1.0 else max - min
|
||||
|
||||
val stepX = size.width / (prices.size - 1).coerceAtLeast(1)
|
||||
val points = prices.mapIndexed { i, p ->
|
||||
Offset(i * stepX, (size.height - ((p - min) / range * size.height)).toFloat())
|
||||
}
|
||||
|
||||
for (i in 0 until points.size - 1) {
|
||||
drawLine(
|
||||
color = if (prices.last() >= prices.first()) Color(0xFFE03E2D) else Color(0xFF0E62CF),
|
||||
start = points[i],
|
||||
end = points[i + 1],
|
||||
strokeWidth = 2f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/PeriodTrendCard.kt (신규/통합)
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.Canvas
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.geometry.Offset
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import model.CandleData
|
||||
//
|
||||
//@Composable
|
||||
//fun PeriodTrendCard(label: String, data: List<CandleData>, modifier: Modifier = Modifier) {
|
||||
// val avgPrice = if (data.isEmpty()) "0"
|
||||
// else String.format("%,d", data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }.average().toLong())
|
||||
//
|
||||
// Card(modifier = modifier.height(80.dp), elevation = 2.dp, backgroundColor = Color.White) {
|
||||
// Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
// // [좌측] 라벨 및 평균가
|
||||
// Column(modifier = Modifier.weight(0.4f)) {
|
||||
// Text(label, fontSize = 10.sp, color = Color.Gray)
|
||||
// Text(text = "${avgPrice}원", fontSize = 12.sp, fontWeight = FontWeight.Bold)
|
||||
// }
|
||||
//
|
||||
// // [우측] 간소화된 그래프 (Sparkline)
|
||||
// Box(modifier = Modifier.weight(0.6f).fillMaxHeight()) {
|
||||
// if (data.isNotEmpty()) {
|
||||
// Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
// val prices = data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }
|
||||
// val max = prices.maxOrNull() ?: 1.0
|
||||
// val min = prices.minOrNull() ?: 0.0
|
||||
// val range = if (max == min) 1.0 else max - min
|
||||
//
|
||||
// val stepX = size.width / (prices.size - 1).coerceAtLeast(1)
|
||||
// val points = prices.mapIndexed { i, p ->
|
||||
// Offset(i * stepX, (size.height - ((p - min) / range * size.height)).toFloat())
|
||||
// }
|
||||
//
|
||||
// for (i in 0 until points.size - 1) {
|
||||
// drawLine(
|
||||
// color = if (prices.last() >= prices.first()) Color(0xFFE03E2D) else Color(0xFF0E62CF),
|
||||
// start = points[i],
|
||||
// end = points[i + 1],
|
||||
// strokeWidth = 2f
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,43 +1,43 @@
|
||||
// src/main/kotlin/ui/RealTimeTradeList.kt
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import model.RealTimeTrade
|
||||
|
||||
@Composable
|
||||
fun RealTimeTradeList(tradeLogs: List<RealTimeTrade>) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// [1] 리스트 헤더 영역
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFFEEEEEE)) // 연한 회색 배경
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text("시간", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
Text("체결가", modifier = Modifier.weight(1.5f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
Text("대비", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
Text("체결량", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
}
|
||||
|
||||
// [2] 실제 데이터 리스트
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
// 최신 데이터가 위로 오도록 표시 (이미 tradeLogs에 add(0, new)로 들어옴)
|
||||
items(tradeLogs) { trade ->
|
||||
TradeLogRow(trade) // 기존에 만드신 행(Row) 컴포넌트 재사용
|
||||
Divider(color = Color(0xFFF5F5F5), thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/RealTimeTradeList.kt
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.background
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items
|
||||
//import androidx.compose.material.Divider
|
||||
//import androidx.compose.material.Text
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.style.TextAlign
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import model.RealTimeTrade
|
||||
//
|
||||
//@Composable
|
||||
//fun RealTimeTradeList(tradeLogs: List<RealTimeTrade>) {
|
||||
// Column(modifier = Modifier.fillMaxSize()) {
|
||||
// // [1] 리스트 헤더 영역
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .background(Color(0xFFEEEEEE)) // 연한 회색 배경
|
||||
// .padding(vertical = 4.dp)
|
||||
// ) {
|
||||
// Text("시간", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
// Text("체결가", modifier = Modifier.weight(1.5f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
// Text("대비", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
// Text("체결량", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp, color = Color.Gray)
|
||||
// }
|
||||
//
|
||||
// // [2] 실제 데이터 리스트
|
||||
// LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
// // 최신 데이터가 위로 오도록 표시 (이미 tradeLogs에 add(0, new)로 들어옴)
|
||||
// items(tradeLogs) { trade ->
|
||||
// TradeLogRow(trade) // 기존에 만드신 행(Row) 컴포넌트 재사용
|
||||
// Divider(color = Color(0xFFF5F5F5), thickness = 0.5.dp)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,302 +1,302 @@
|
||||
package ui
|
||||
|
||||
|
||||
|
||||
import network.TradingDecision
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
// 아래 두 import가 'delegate' 에러를 해결합니다.
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import model.CandleData
|
||||
import network.DartCodeManager
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.collections.isNotEmpty
|
||||
|
||||
@Composable
|
||||
fun StockDetailSection(
|
||||
stockCode: String,
|
||||
stockName: String,
|
||||
holdingQuantity: String,
|
||||
isDomestic: Boolean,
|
||||
tradeService: KisTradeService,
|
||||
wsManager: KisWebSocketManager,
|
||||
onOrderSaved: (String) -> Unit,
|
||||
completeTradingDecision: TradingDecision?,
|
||||
min30 : MutableList<CandleData>,
|
||||
daySummary : MutableList<CandleData>,
|
||||
weekSummary : MutableList<CandleData>,
|
||||
monthSummary : MutableList<CandleData>,
|
||||
yearSummary : MutableList<CandleData>
|
||||
) {
|
||||
|
||||
// var openPrice by remember { mutableStateOf("0") }
|
||||
var chartData by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var resultMessage by remember { mutableStateOf("") }
|
||||
var isSuccess by remember { mutableStateOf(true) }
|
||||
|
||||
|
||||
val todayOpen = remember(daySummary) {
|
||||
daySummary.lastOrNull()?.stck_oprc ?: "0"
|
||||
}
|
||||
val previousClose = remember(daySummary) {
|
||||
if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_prpr else "0"
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 이전 종목 코드를 기억하기 위한 상태
|
||||
var previousCode by remember { mutableStateOf("") }
|
||||
var lastPrice by remember { mutableStateOf("0") }
|
||||
|
||||
|
||||
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
||||
LaunchedEffect(stockCode) {
|
||||
if (stockCode.isEmpty()) return@LaunchedEffect
|
||||
|
||||
isLoading = true
|
||||
|
||||
// 1. 웹소켓 구독 관리: 이전 종목 해제 -> 새 종목 구독
|
||||
if (previousCode.isNotEmpty()) {
|
||||
wsManager.unsubscribeStock(previousCode)
|
||||
}
|
||||
wsManager.clearData()
|
||||
wsManager.subscribeStock(stockCode)
|
||||
previousCode = stockCode
|
||||
|
||||
|
||||
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||
|
||||
coroutineScope {
|
||||
launch {
|
||||
wsManager.onPriceUpdate = {tradeLog ->
|
||||
|
||||
|
||||
if (tradeLog.code.equals(stockCode)) {
|
||||
val code = tradeLog.code
|
||||
val price = tradeLog.price
|
||||
wsManager.tradeLogs.add(tradeLog)
|
||||
if (wsManager.tradeLogs.size > 50) wsManager.tradeLogs.removeLast()
|
||||
// println("code $code ,price $price")
|
||||
val currentPrice = price
|
||||
if (chartData.isNotEmpty() && currentPrice != "0") {
|
||||
val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
val lastCandle = chartData.last()
|
||||
|
||||
// 현재 시간(분 단위) 확인
|
||||
val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
|
||||
if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
val newCandle = CandleData(
|
||||
stck_bsop_date = currentMinute,
|
||||
stck_oprc = currentPrice,
|
||||
stck_hgpr = currentPrice,
|
||||
stck_lwpr = currentPrice,
|
||||
stck_prpr = currentPrice,
|
||||
stck_cntg_hour = currentMinute,
|
||||
cntg_vol = "1",
|
||||
acml_tr_pbmn = "1",
|
||||
)
|
||||
// 최대 100개까지만 유지하여 성능 최적화
|
||||
chartData = (chartData + newCandle).takeLast(100)
|
||||
} else {
|
||||
// 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
||||
val updatedCandle = lastCandle.copy(
|
||||
stck_prpr = currentPrice,
|
||||
stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) currentPrice else lastCandle.stck_hgpr,
|
||||
stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) currentPrice else lastCandle.stck_lwpr
|
||||
)
|
||||
chartData = chartData.dropLast(1) + updatedCandle
|
||||
}
|
||||
}
|
||||
lastPrice = currentPrice
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
launch {tradeService.fetchChartData(stockCode, isDomestic)
|
||||
.onSuccess { data ->
|
||||
// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
chartData = data
|
||||
min30.clear()
|
||||
min30.addAll(chartData)
|
||||
}
|
||||
.onFailure { error ->
|
||||
// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
chartData = emptyList()
|
||||
}
|
||||
}
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
||||
daySummary.clear()
|
||||
daySummary.addAll(it)
|
||||
}
|
||||
} // 최근 7일
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
||||
weekSummary.clear()
|
||||
weekSummary.addAll(it.takeLast(4))
|
||||
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||
}
|
||||
} // 최근 4주
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
||||
monthSummary.clear()
|
||||
monthSummary.addAll(it.takeLast(6))
|
||||
yearSummary.clear()
|
||||
yearSummary.addAll(it.takeLast(36))
|
||||
}
|
||||
}
|
||||
launch {
|
||||
DartCodeManager.getCorpCode(stockCode)?.let {
|
||||
it.stockName = stockName
|
||||
// NewsService.fetchAndIngestNews(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// LaunchedEffect(latestPrice) {
|
||||
// println("latestPrice >>> $latestPrice")
|
||||
// if (chartData.isNotEmpty() && latestPrice != "0") {
|
||||
// val latestPrice = latestPrice ?: "0"
|
||||
// val priceDouble = latestPrice?.replace(",", "")?.toDoubleOrNull() ?: return@LaunchedEffect
|
||||
// val lastCandle = chartData.last()
|
||||
//package ui
|
||||
//
|
||||
// // 현재 시간(분 단위) 확인
|
||||
// val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
//
|
||||
// if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
// val newCandle = CandleData(
|
||||
// stck_bsop_date = currentMinute,
|
||||
// stck_oprc = latestPrice,
|
||||
// stck_hgpr = latestPrice,
|
||||
// stck_lwpr = latestPrice,
|
||||
// stck_prpr = latestPrice,
|
||||
// stck_cntg_hour = currentMinute,
|
||||
// cntg_vol = "1",
|
||||
// acml_tr_pbmn = "1",
|
||||
//
|
||||
//import network.TradingDecision
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.runtime.*
|
||||
//// 아래 두 import가 'delegate' 에러를 해결합니다.
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import kotlinx.coroutines.coroutineScope
|
||||
//import kotlinx.coroutines.launch
|
||||
//import model.CandleData
|
||||
//import network.DartCodeManager
|
||||
//import network.KisTradeService
|
||||
//import network.KisWebSocketManager
|
||||
//import java.time.LocalTime
|
||||
//import java.time.format.DateTimeFormatter
|
||||
//import kotlin.collections.isNotEmpty
|
||||
//
|
||||
//@Composable
|
||||
//fun StockDetailSection(
|
||||
// stockCode: String,
|
||||
// stockName: String,
|
||||
// holdingQuantity: String,
|
||||
// isDomestic: Boolean,
|
||||
// tradeService: KisTradeService,
|
||||
// wsManager: KisWebSocketManager,
|
||||
// onOrderSaved: (String) -> Unit,
|
||||
// completeTradingDecision: TradingDecision?,
|
||||
// min30 : MutableList<CandleData>,
|
||||
// daySummary : MutableList<CandleData>,
|
||||
// weekSummary : MutableList<CandleData>,
|
||||
// monthSummary : MutableList<CandleData>,
|
||||
// yearSummary : MutableList<CandleData>
|
||||
//) {
|
||||
//
|
||||
//// var openPrice by remember { mutableStateOf("0") }
|
||||
// var chartData by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
// var isLoading by remember { mutableStateOf(false) }
|
||||
// var resultMessage by remember { mutableStateOf("") }
|
||||
// var isSuccess by remember { mutableStateOf(true) }
|
||||
//
|
||||
//
|
||||
// val todayOpen = remember(daySummary) {
|
||||
// daySummary.lastOrNull()?.stck_oprc ?: "0"
|
||||
// }
|
||||
// val previousClose = remember(daySummary) {
|
||||
// if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_prpr else "0"
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// // 이전 종목 코드를 기억하기 위한 상태
|
||||
// var previousCode by remember { mutableStateOf("") }
|
||||
// var lastPrice by remember { mutableStateOf("0") }
|
||||
//
|
||||
//
|
||||
// // 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
||||
// LaunchedEffect(stockCode) {
|
||||
// if (stockCode.isEmpty()) return@LaunchedEffect
|
||||
//
|
||||
// isLoading = true
|
||||
//
|
||||
// // 1. 웹소켓 구독 관리: 이전 종목 해제 -> 새 종목 구독
|
||||
// if (previousCode.isNotEmpty()) {
|
||||
// wsManager.unsubscribeStock(previousCode)
|
||||
// }
|
||||
// wsManager.clearData()
|
||||
// wsManager.subscribeStock(stockCode)
|
||||
// previousCode = stockCode
|
||||
//
|
||||
//
|
||||
// // 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||
//
|
||||
// coroutineScope {
|
||||
// launch {
|
||||
// wsManager.onPriceUpdate = {tradeLog ->
|
||||
//
|
||||
//
|
||||
// if (tradeLog.code.equals(stockCode)) {
|
||||
// val code = tradeLog.code
|
||||
// val price = tradeLog.price
|
||||
// wsManager.tradeLogs.add(tradeLog)
|
||||
// if (wsManager.tradeLogs.size > 50) wsManager.tradeLogs.removeLast()
|
||||
//// println("code $code ,price $price")
|
||||
// val currentPrice = price
|
||||
// if (chartData.isNotEmpty() && currentPrice != "0") {
|
||||
// val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
// val lastCandle = chartData.last()
|
||||
//
|
||||
// // 현재 시간(분 단위) 확인
|
||||
// val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
//
|
||||
// if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
// val newCandle = CandleData(
|
||||
// stck_bsop_date = currentMinute,
|
||||
// stck_oprc = currentPrice,
|
||||
// stck_hgpr = currentPrice,
|
||||
// stck_lwpr = currentPrice,
|
||||
// stck_prpr = currentPrice,
|
||||
// stck_cntg_hour = currentMinute,
|
||||
// cntg_vol = "1",
|
||||
// acml_tr_pbmn = "1",
|
||||
// )
|
||||
// // 최대 100개까지만 유지하여 성능 최적화
|
||||
// chartData = (chartData + newCandle).takeLast(100)
|
||||
// } else {
|
||||
// // 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
||||
// val updatedCandle = lastCandle.copy(
|
||||
// stck_prpr = currentPrice,
|
||||
// stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) currentPrice else lastCandle.stck_hgpr,
|
||||
// stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) currentPrice else lastCandle.stck_lwpr
|
||||
// )
|
||||
// chartData = chartData.dropLast(1) + updatedCandle
|
||||
// }
|
||||
// }
|
||||
// lastPrice = currentPrice
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// launch {tradeService.fetchChartData(stockCode, isDomestic)
|
||||
// .onSuccess { data ->
|
||||
//// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
// chartData = data
|
||||
// min30.clear()
|
||||
// min30.addAll(chartData)
|
||||
// }
|
||||
// .onFailure { error ->
|
||||
//// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
// chartData = emptyList()
|
||||
// }
|
||||
// }
|
||||
// launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
||||
// daySummary.clear()
|
||||
// daySummary.addAll(it)
|
||||
// }
|
||||
// } // 최근 7일
|
||||
// launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
||||
// weekSummary.clear()
|
||||
// weekSummary.addAll(it.takeLast(4))
|
||||
//// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||
// }
|
||||
// } // 최근 4주
|
||||
// launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
||||
// monthSummary.clear()
|
||||
// monthSummary.addAll(it.takeLast(6))
|
||||
// yearSummary.clear()
|
||||
// yearSummary.addAll(it.takeLast(36))
|
||||
// }
|
||||
// }
|
||||
// launch {
|
||||
// DartCodeManager.getCorpCode(stockCode)?.let {
|
||||
// it.stockName = stockName
|
||||
//// NewsService.fetchAndIngestNews(it)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// isLoading = false
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//// LaunchedEffect(latestPrice) {
|
||||
//// println("latestPrice >>> $latestPrice")
|
||||
//// if (chartData.isNotEmpty() && latestPrice != "0") {
|
||||
//// val latestPrice = latestPrice ?: "0"
|
||||
//// val priceDouble = latestPrice?.replace(",", "")?.toDoubleOrNull() ?: return@LaunchedEffect
|
||||
//// val lastCandle = chartData.last()
|
||||
////
|
||||
//// // 현재 시간(분 단위) 확인
|
||||
//// val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
////
|
||||
//// if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
//// // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
//// val newCandle = CandleData(
|
||||
//// stck_bsop_date = currentMinute,
|
||||
//// stck_oprc = latestPrice,
|
||||
//// stck_hgpr = latestPrice,
|
||||
//// stck_lwpr = latestPrice,
|
||||
//// stck_prpr = latestPrice,
|
||||
//// stck_cntg_hour = currentMinute,
|
||||
//// cntg_vol = "1",
|
||||
//// acml_tr_pbmn = "1",
|
||||
//// )
|
||||
//// // 최대 100개까지만 유지하여 성능 최적화
|
||||
//// chartData = (chartData + newCandle).takeLast(100)
|
||||
//// } else {
|
||||
//// // 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
||||
//// val updatedCandle = lastCandle.copy(
|
||||
//// stck_prpr = latestPrice,
|
||||
//// stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) latestPrice else lastCandle.stck_hgpr,
|
||||
//// stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) latestPrice else lastCandle.stck_lwpr
|
||||
//// )
|
||||
//// chartData = chartData.dropLast(1) + updatedCandle
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
// // [상단] 종목명 및 상태 메시지
|
||||
// Row(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// horizontalArrangement = Arrangement.SpaceBetween,
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// StockHeader(
|
||||
// name = stockName,
|
||||
// code = stockCode,
|
||||
// isDomestic = isDomestic,
|
||||
// previousClose = previousClose,
|
||||
// openPrice = lastPrice,
|
||||
// resultMessage = resultMessage,
|
||||
// resultMessageClear = {resultMessage = ""},
|
||||
// isSuccess = isSuccess
|
||||
// )
|
||||
//
|
||||
// // 실시간 가격 표시 (WebSocket 데이터)
|
||||
// Column(horizontalAlignment = Alignment.End) {
|
||||
// Text(
|
||||
// text = "${lastPrice} 원",
|
||||
// style = MaterialTheme.typography.h4,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// color = if (lastPrice?.contains("-") ?: false) Color.Blue else Color.Red
|
||||
// )
|
||||
// // 최대 100개까지만 유지하여 성능 최적화
|
||||
// chartData = (chartData + newCandle).takeLast(100)
|
||||
// Text("실시간 체결가", style = MaterialTheme.typography.caption, color = Color.Gray)
|
||||
// }
|
||||
// }
|
||||
// // 통합된 트렌드 카드 배치
|
||||
// Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// PeriodTrendCard("7일", daySummary, Modifier.weight(1f))
|
||||
// PeriodTrendCard("4주", weekSummary, Modifier.weight(1f))
|
||||
// PeriodTrendCard("6개월", monthSummary, Modifier.weight(1f))
|
||||
// PeriodTrendCard("3년", yearSummary, Modifier.weight(1f))
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(4.dp))
|
||||
// // [중앙] 캔들 차트 (Card 내부)
|
||||
// Card(
|
||||
// modifier = Modifier.fillMaxWidth().height(320.dp),
|
||||
// backgroundColor = Color(0xFF121212)
|
||||
// ) {
|
||||
// if (isLoading) {
|
||||
// Box(contentAlignment = Alignment.Center) { CircularProgressIndicator(color = Color.White) }
|
||||
// } else {
|
||||
// // 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
||||
// val updatedCandle = lastCandle.copy(
|
||||
// stck_prpr = latestPrice,
|
||||
// stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) latestPrice else lastCandle.stck_hgpr,
|
||||
// stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) latestPrice else lastCandle.stck_lwpr
|
||||
// CandleChart(data = chartData, modifier = Modifier.padding(16.dp))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(4.dp))
|
||||
//
|
||||
//
|
||||
//
|
||||
// // [하단] 실시간 체결 내역 및 주문 섹션
|
||||
// Row(modifier = Modifier.weight(1f)) {
|
||||
// // 실시간 체결 리스트
|
||||
// Column(modifier = Modifier.weight(1f)) {
|
||||
// Text("실시간 체결", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold)
|
||||
// RealTimeTradeList(wsManager.tradeLogs)
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.width(12.dp))
|
||||
//
|
||||
// // 주문 섹션 (인자 간소화)
|
||||
// Column(modifier = Modifier.weight(0.6f)) {
|
||||
// IntegratedOrderSection(
|
||||
// stockCode = stockCode,
|
||||
// stockName = stockName,
|
||||
// isDomestic = isDomestic,
|
||||
// currentPrice = lastPrice,
|
||||
// holdingQuantity = holdingQuantity,
|
||||
// tradeService = tradeService,
|
||||
// onOrderSaved = onOrderSaved,
|
||||
// onOrderResult = { msg, success ->
|
||||
// resultMessage = msg
|
||||
// isSuccess = success
|
||||
// },
|
||||
// completeTradingDecision
|
||||
// )
|
||||
// chartData = chartData.dropLast(1) + updatedCandle
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
// [상단] 종목명 및 상태 메시지
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StockHeader(
|
||||
name = stockName,
|
||||
code = stockCode,
|
||||
isDomestic = isDomestic,
|
||||
previousClose = previousClose,
|
||||
openPrice = lastPrice,
|
||||
resultMessage = resultMessage,
|
||||
resultMessageClear = {resultMessage = ""},
|
||||
isSuccess = isSuccess
|
||||
)
|
||||
|
||||
// 실시간 가격 표시 (WebSocket 데이터)
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = "${lastPrice} 원",
|
||||
style = MaterialTheme.typography.h4,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (lastPrice?.contains("-") ?: false) Color.Blue else Color.Red
|
||||
)
|
||||
Text("실시간 체결가", style = MaterialTheme.typography.caption, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
// 통합된 트렌드 카드 배치
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PeriodTrendCard("7일", daySummary, Modifier.weight(1f))
|
||||
PeriodTrendCard("4주", weekSummary, Modifier.weight(1f))
|
||||
PeriodTrendCard("6개월", monthSummary, Modifier.weight(1f))
|
||||
PeriodTrendCard("3년", yearSummary, Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// [중앙] 캔들 차트 (Card 내부)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().height(320.dp),
|
||||
backgroundColor = Color(0xFF121212)
|
||||
) {
|
||||
if (isLoading) {
|
||||
Box(contentAlignment = Alignment.Center) { CircularProgressIndicator(color = Color.White) }
|
||||
} else {
|
||||
CandleChart(data = chartData, modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
|
||||
|
||||
// [하단] 실시간 체결 내역 및 주문 섹션
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// 실시간 체결 리스트
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("실시간 체결", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold)
|
||||
RealTimeTradeList(wsManager.tradeLogs)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// 주문 섹션 (인자 간소화)
|
||||
Column(modifier = Modifier.weight(0.6f)) {
|
||||
IntegratedOrderSection(
|
||||
stockCode = stockCode,
|
||||
stockName = stockName,
|
||||
isDomestic = isDomestic,
|
||||
currentPrice = lastPrice,
|
||||
holdingQuantity = holdingQuantity,
|
||||
tradeService = tradeService,
|
||||
onOrderSaved = onOrderSaved,
|
||||
onOrderResult = { msg, success ->
|
||||
resultMessage = msg
|
||||
isSuccess = success
|
||||
},
|
||||
completeTradingDecision
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PeriodSummaryCard(label: String, avgPrice: String, modifier: Modifier = Modifier) {
|
||||
Card(modifier = modifier, elevation = 2.dp, backgroundColor = Color.White) {
|
||||
Column(modifier = Modifier.padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(label, fontSize = 10.sp, color = Color.Gray)
|
||||
Text(text = "${avgPrice}원", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color.Black)
|
||||
}
|
||||
}
|
||||
}
|
||||
//}
|
||||
//
|
||||
//@Composable
|
||||
//fun PeriodSummaryCard(label: String, avgPrice: String, modifier: Modifier = Modifier) {
|
||||
// Card(modifier = modifier, elevation = 2.dp, backgroundColor = Color.White) {
|
||||
// Column(modifier = Modifier.padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
// Text(label, fontSize = 10.sp, color = Color.Gray)
|
||||
// Text(text = "${avgPrice}원", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color.Black)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,74 +1,74 @@
|
||||
// src/main/kotlin/ui/StockHeader.kt
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun StockHeader(
|
||||
name: String,
|
||||
code: String,
|
||||
isDomestic: Boolean,
|
||||
previousClose: String, // 추가: 전일 종가
|
||||
openPrice: String, // 추가: 금일 시가
|
||||
resultMessage: String,
|
||||
resultMessageClear : ()->Unit,
|
||||
isSuccess: Boolean
|
||||
) {
|
||||
Column(modifier = Modifier.wrapContentWidth()) {
|
||||
// [1] 알림 메시지 영역 (기존 동일)
|
||||
if (resultMessage.isNotEmpty()) {
|
||||
Surface(
|
||||
color = if (isSuccess) Color(0xFF4CAF50) else Color(0xFFF44336),
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
onClick = {
|
||||
resultMessageClear.invoke()
|
||||
}
|
||||
) {
|
||||
Text(text = resultMessage, color = Color.White, modifier = Modifier.padding(8.dp), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
// [2] 종목명 및 정보
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Surface(
|
||||
color = if (isDomestic) Color(0xFFE03E2D) else Color(0xFF0E62CF),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(text = if (isDomestic) "국내" else "해외", color = Color.White, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = name, style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = "($code)", color = Color.Gray)
|
||||
}
|
||||
|
||||
// [3] 전일 종가 및 시가 정보 행 추가
|
||||
Row(modifier = Modifier.padding(top = 4.dp)) {
|
||||
PriceSummaryItem("전일 종가", previousClose)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
PriceSummaryItem("금일 시가", openPrice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriceSummaryItem(label: String, price: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = label, fontSize = 11.sp, color = Color.Gray)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
val formattedPrice = price.toLongOrNull()?.let { String.format("%,d", it) } ?: price
|
||||
Text(text = "${formattedPrice}원", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
//// src/main/kotlin/ui/StockHeader.kt
|
||||
//package ui
|
||||
//
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.text.style.TextAlign
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//
|
||||
//@OptIn(ExperimentalMaterialApi::class)
|
||||
//@Composable
|
||||
//fun StockHeader(
|
||||
// name: String,
|
||||
// code: String,
|
||||
// isDomestic: Boolean,
|
||||
// previousClose: String, // 추가: 전일 종가
|
||||
// openPrice: String, // 추가: 금일 시가
|
||||
// resultMessage: String,
|
||||
// resultMessageClear : ()->Unit,
|
||||
// isSuccess: Boolean
|
||||
//) {
|
||||
// Column(modifier = Modifier.wrapContentWidth()) {
|
||||
// // [1] 알림 메시지 영역 (기존 동일)
|
||||
// if (resultMessage.isNotEmpty()) {
|
||||
// Surface(
|
||||
// color = if (isSuccess) Color(0xFF4CAF50) else Color(0xFFF44336),
|
||||
// modifier = Modifier.padding(bottom = 8.dp),
|
||||
// shape = RoundedCornerShape(4.dp),
|
||||
// onClick = {
|
||||
// resultMessageClear.invoke()
|
||||
// }
|
||||
// ) {
|
||||
// Text(text = resultMessage, color = Color.White, modifier = Modifier.padding(8.dp), fontWeight = FontWeight.Bold)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // [2] 종목명 및 정보
|
||||
// Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Surface(
|
||||
// color = if (isDomestic) Color(0xFFE03E2D) else Color(0xFF0E62CF),
|
||||
// shape = RoundedCornerShape(4.dp)
|
||||
// ) {
|
||||
// Text(text = if (isDomestic) "국내" else "해외", color = Color.White, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp))
|
||||
// }
|
||||
// Spacer(modifier = Modifier.width(8.dp))
|
||||
// Text(text = name, style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold)
|
||||
// Spacer(modifier = Modifier.width(6.dp))
|
||||
// Text(text = "($code)", color = Color.Gray)
|
||||
// }
|
||||
//
|
||||
// // [3] 전일 종가 및 시가 정보 행 추가
|
||||
// Row(modifier = Modifier.padding(top = 4.dp)) {
|
||||
// PriceSummaryItem("전일 종가", previousClose)
|
||||
// Spacer(modifier = Modifier.width(16.dp))
|
||||
// PriceSummaryItem("금일 시가", openPrice)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@Composable
|
||||
//fun PriceSummaryItem(label: String, price: String) {
|
||||
// Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Text(text = label, fontSize = 11.sp, color = Color.Gray)
|
||||
// Spacer(modifier = Modifier.width(4.dp))
|
||||
// val formattedPrice = price.toLongOrNull()?.let { String.format("%,d", it) } ?: price
|
||||
// Text(text = "${formattedPrice}원", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
// }
|
||||
//}
|
||||
@@ -1,63 +1,63 @@
|
||||
package ui
|
||||
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items // 반드시 수동 import 확인
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
// 아래 두 import가 'delegate' 에러를 해결합니다.
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.AppConfig
|
||||
import model.BalanceSummary
|
||||
import model.RankingStock
|
||||
import model.RealTimeTrade
|
||||
import model.StockHolding
|
||||
import model.TradeType
|
||||
import network.KisTradeService
|
||||
import util.MarketUtil
|
||||
|
||||
@Composable
|
||||
fun TradeLogRow(trade: RealTimeTrade) {
|
||||
val color = when (trade.type) {
|
||||
TradeType.BUY -> Color(0xFFE03E2D)
|
||||
TradeType.SELL -> Color(0xFF0E62CF)
|
||||
else -> Color.DarkGray
|
||||
}
|
||||
|
||||
// 대량 체결(예: 1000주 이상) 시 연한 배경색 강조
|
||||
val isLargeTrade = (trade.volume.replace(",", "").toIntOrNull() ?: 0) >= 1000
|
||||
val rowBgColor = if (isLargeTrade) color.copy(alpha = 0.05f) else Color.Transparent
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(rowBgColor).padding(vertical = 6.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(trade.time, modifier = Modifier.weight(1f), fontSize = 12.sp, color = Color.Gray, textAlign = TextAlign.Center)
|
||||
Text(trade.price, modifier = Modifier.weight(1.5f), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = color, textAlign = TextAlign.End)
|
||||
Text(trade.change, modifier = Modifier.weight(1f), fontSize = 11.sp, color = color, textAlign = TextAlign.End)
|
||||
Text(
|
||||
text = trade.volume,
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (isLargeTrade) FontWeight.ExtraBold else FontWeight.Normal,
|
||||
color = color,
|
||||
textAlign = TextAlign.End
|
||||
)
|
||||
}
|
||||
}
|
||||
//package ui
|
||||
//
|
||||
//
|
||||
//import androidx.compose.foundation.background
|
||||
//import androidx.compose.foundation.clickable
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items // 반드시 수동 import 확인
|
||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
//import androidx.compose.material.*
|
||||
//import androidx.compose.runtime.*
|
||||
//
|
||||
//import io.ktor.client.engine.cio.CIO
|
||||
//// 아래 두 import가 'delegate' 에러를 해결합니다.
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.text.font.FontWeight
|
||||
//import androidx.compose.ui.text.style.TextAlign
|
||||
//import androidx.compose.ui.text.style.TextOverflow
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import kotlinx.coroutines.launch
|
||||
//import model.AppConfig
|
||||
//import model.BalanceSummary
|
||||
//import model.RankingStock
|
||||
//import model.RealTimeTrade
|
||||
//import model.StockHolding
|
||||
//import model.TradeType
|
||||
//import network.KisTradeService
|
||||
//import util.MarketUtil
|
||||
//
|
||||
//@Composable
|
||||
//fun TradeLogRow(trade: RealTimeTrade) {
|
||||
// val color = when (trade.type) {
|
||||
// TradeType.BUY -> Color(0xFFE03E2D)
|
||||
// TradeType.SELL -> Color(0xFF0E62CF)
|
||||
// else -> Color.DarkGray
|
||||
// }
|
||||
//
|
||||
// // 대량 체결(예: 1000주 이상) 시 연한 배경색 강조
|
||||
// val isLargeTrade = (trade.volume.replace(",", "").toIntOrNull() ?: 0) >= 1000
|
||||
// val rowBgColor = if (isLargeTrade) color.copy(alpha = 0.05f) else Color.Transparent
|
||||
//
|
||||
// Row(
|
||||
// modifier = Modifier.fillMaxWidth().background(rowBgColor).padding(vertical = 6.dp, horizontal = 4.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// Text(trade.time, modifier = Modifier.weight(1f), fontSize = 12.sp, color = Color.Gray, textAlign = TextAlign.Center)
|
||||
// Text(trade.price, modifier = Modifier.weight(1.5f), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = color, textAlign = TextAlign.End)
|
||||
// Text(trade.change, modifier = Modifier.weight(1f), fontSize = 11.sp, color = color, textAlign = TextAlign.End)
|
||||
// Text(
|
||||
// text = trade.volume,
|
||||
// modifier = Modifier.weight(1f),
|
||||
// fontSize = 12.sp,
|
||||
// fontWeight = if (isLargeTrade) FontWeight.ExtraBold else FontWeight.Normal,
|
||||
// color = color,
|
||||
// textAlign = TextAlign.End
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
@@ -395,3 +395,33 @@ fun StatusIndicator(label: String, isActive: Boolean, onRestart: (() -> Unit)? =
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLongestCommonSubstring(s1: String, s2: String): String {
|
||||
if (s1.isEmpty() || s2.isEmpty()) return ""
|
||||
|
||||
var longest = ""
|
||||
// 더 짧은 문자열을 기준으로 삼아 반복 횟수를 줄임
|
||||
val reference = if (s1.length <= s2.length) s1 else s2
|
||||
val target = if (s1.length <= s2.length) s2 else s1
|
||||
|
||||
for (i in reference.indices) {
|
||||
for (j in (i + longest.length + 1)..reference.length) {
|
||||
val sub = reference.substring(i, j)
|
||||
if (target.contains(sub)) {
|
||||
if (sub.length > longest.length) {
|
||||
longest = sub
|
||||
}
|
||||
} else {
|
||||
// target에 포함되지 않으면 더 긴 substring은 존재할 수 없으므로 탈출
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return longest
|
||||
}
|
||||
|
||||
fun getRemaining(original: String, common: String): String {
|
||||
if (common.isEmpty()) return original
|
||||
// 가장 처음 발견되는 공통 문자열을 한 번만 제거
|
||||
return original.replaceFirst(common, "").trim()
|
||||
}
|
||||
Reference in New Issue
Block a user