atrade/src/main/kotlin/ui/StockDetailArea.kt

302 lines
12 KiB
Kotlin
Raw Normal View History

2026-01-10 18:16:50 +09:00
package ui
2026-03-17 10:50:13 +09:00
import network.TradingDecision
2026-01-10 18:16:50 +09:00
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
2026-01-14 15:42:26 +09:00
import kotlinx.coroutines.coroutineScope
2026-01-10 18:16:50 +09:00
import kotlinx.coroutines.launch
import model.CandleData
2026-01-23 17:05:09 +09:00
import network.DartCodeManager
2026-01-10 18:16:50 +09:00
import network.KisTradeService
import network.KisWebSocketManager
2026-01-22 16:21:18 +09:00
import java.time.LocalTime
import java.time.format.DateTimeFormatter
2026-01-10 18:16:50 +09:00
import kotlin.collections.isNotEmpty
@Composable
2026-01-13 16:04:25 +09:00
fun StockDetailSection(
stockCode: String,
stockName: String,
2026-01-23 17:05:09 +09:00
holdingQuantity: String,
2026-01-13 16:04:25 +09:00
isDomestic: Boolean,
tradeService: KisTradeService,
2026-01-21 11:49:30 +09:00
wsManager: KisWebSocketManager,
2026-01-23 17:05:09 +09:00
onOrderSaved: (String) -> Unit,
2026-02-04 14:52:09 +09:00
completeTradingDecision: TradingDecision?,
min30 : MutableList<CandleData>,
daySummary : MutableList<CandleData>,
weekSummary : MutableList<CandleData>,
monthSummary : MutableList<CandleData>,
yearSummary : MutableList<CandleData>
2026-01-10 18:16:50 +09:00
) {
2026-01-14 15:42:26 +09:00
2026-02-12 15:31:34 +09:00
// var openPrice by remember { mutableStateOf("0") }
2026-01-10 18:16:50 +09:00
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) }
2026-02-04 14:52:09 +09:00
2026-01-14 15:42:26 +09:00
val todayOpen = remember(daySummary) {
daySummary.lastOrNull()?.stck_oprc ?: "0"
}
val previousClose = remember(daySummary) {
2026-01-22 16:21:18 +09:00
if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_prpr else "0"
2026-01-14 15:42:26 +09:00
}
2026-01-23 17:05:09 +09:00
2026-01-10 18:16:50 +09:00
2026-01-13 16:04:25 +09:00
// 이전 종목 코드를 기억하기 위한 상태
var previousCode by remember { mutableStateOf("") }
2026-02-13 13:49:40 +09:00
var lastPrice by remember { mutableStateOf("0") }
2026-02-12 15:31:34 +09:00
2026-01-13 16:04:25 +09:00
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
LaunchedEffect(stockCode) {
if (stockCode.isEmpty()) return@LaunchedEffect
2026-01-10 18:16:50 +09:00
isLoading = true
2026-01-13 16:04:25 +09:00
// 1. 웹소켓 구독 관리: 이전 종목 해제 -> 새 종목 구독
if (previousCode.isNotEmpty()) {
wsManager.unsubscribeStock(previousCode)
}
2026-01-14 15:42:26 +09:00
wsManager.clearData()
2026-01-13 16:04:25 +09:00
wsManager.subscribeStock(stockCode)
previousCode = stockCode
2026-01-23 17:05:09 +09:00
2026-01-13 16:04:25 +09:00
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
2026-01-10 18:16:50 +09:00
2026-01-14 15:42:26 +09:00
coroutineScope {
2026-02-12 15:31:34 +09:00
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()
2026-02-13 13:49:40 +09:00
// println("code $code ,price $price")
2026-02-12 15:31:34 +09:00
val currentPrice = price
if (chartData.isNotEmpty() && currentPrice != "0") {
val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
val lastCandle = chartData.last()
2026-02-04 14:52:09 +09:00
2026-02-12 15:31:34 +09:00
// 현재 시간(분 단위) 확인
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
}
}
2026-02-13 13:49:40 +09:00
lastPrice = currentPrice
2026-02-12 15:31:34 +09:00
}
}
}
2026-01-14 15:42:26 +09:00
launch {tradeService.fetchChartData(stockCode, isDomestic)
.onSuccess { data ->
2026-02-12 15:31:34 +09:00
// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
2026-01-14 15:42:26 +09:00
chartData = data
2026-02-04 14:52:09 +09:00
min30.clear()
min30.addAll(chartData)
2026-01-14 15:42:26 +09:00
}
.onFailure { error ->
2026-02-12 15:31:34 +09:00
// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
2026-01-14 15:42:26 +09:00
chartData = emptyList()
2026-01-23 17:05:09 +09:00
}
}
2026-01-22 16:21:18 +09:00
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
2026-02-04 14:52:09 +09:00
daySummary.clear()
daySummary.addAll(it)
2026-01-23 17:05:09 +09:00
}
2026-01-22 16:21:18 +09:00
} // 최근 7일
2026-01-23 17:05:09 +09:00
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
2026-02-04 14:52:09 +09:00
weekSummary.clear()
weekSummary.addAll(it.takeLast(4))
2026-02-03 18:07:18 +09:00
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
2026-01-23 17:05:09 +09:00
}
} // 최근 4주
2026-01-14 15:42:26 +09:00
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
2026-02-04 14:52:09 +09:00
monthSummary.clear()
monthSummary.addAll(it.takeLast(6))
yearSummary.clear()
yearSummary.addAll(it.takeLast(36))
2026-01-23 17:05:09 +09:00
}
}
launch {
DartCodeManager.getCorpCode(stockCode)?.let {
it.stockName = stockName
2026-01-26 15:32:03 +09:00
// NewsService.fetchAndIngestNews(it)
2026-01-23 17:05:09 +09:00
}
}
2026-01-14 15:42:26 +09:00
}
2026-01-10 18:16:50 +09:00
isLoading = false
}
2026-02-12 15:31:34 +09:00
// 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
// }
// }
// }
2026-01-10 18:16:50 +09:00
2026-01-13 16:04:25 +09:00
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// [상단] 종목명 및 상태 메시지
2026-01-14 15:42:26 +09:00
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
StockHeader(
name = stockName,
code = stockCode,
isDomestic = isDomestic,
previousClose = previousClose,
2026-02-13 13:49:40 +09:00
openPrice = lastPrice,
2026-01-14 15:42:26 +09:00
resultMessage = resultMessage,
2026-01-19 17:09:37 +09:00
resultMessageClear = {resultMessage = ""},
2026-01-14 15:42:26 +09:00
isSuccess = isSuccess
)
// 실시간 가격 표시 (WebSocket 데이터)
Column(horizontalAlignment = Alignment.End) {
Text(
2026-02-13 13:49:40 +09:00
text = "${lastPrice}",
2026-01-14 15:42:26 +09:00
style = MaterialTheme.typography.h4,
fontWeight = FontWeight.Bold,
2026-02-13 13:49:40 +09:00
color = if (lastPrice?.contains("-") ?: false) Color.Blue else Color.Red
2026-01-14 15:42:26 +09:00
)
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))
}
2026-01-10 18:16:50 +09:00
2026-01-22 16:21:18 +09:00
Spacer(modifier = Modifier.height(4.dp))
2026-01-13 16:04:25 +09:00
// [중앙] 캔들 차트 (Card 내부)
2026-01-10 18:16:50 +09:00
Card(
2026-01-22 16:21:18 +09:00
modifier = Modifier.fillMaxWidth().height(320.dp),
2026-01-10 18:16:50 +09:00
backgroundColor = Color(0xFF121212)
) {
if (isLoading) {
Box(contentAlignment = Alignment.Center) { CircularProgressIndicator(color = Color.White) }
} else {
2026-01-13 16:04:25 +09:00
CandleChart(data = chartData, modifier = Modifier.padding(16.dp))
2026-01-10 18:16:50 +09:00
}
}
2026-01-13 16:04:25 +09:00
2026-01-22 16:21:18 +09:00
Spacer(modifier = Modifier.height(4.dp))
2026-01-13 16:04:25 +09:00
// [하단] 실시간 체결 내역 및 주문 섹션
Row(modifier = Modifier.weight(1f)) {
// 실시간 체결 리스트
Column(modifier = Modifier.weight(1f)) {
Text("실시간 체결", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold)
RealTimeTradeList(wsManager.tradeLogs)
2026-01-10 18:16:50 +09:00
}
2026-01-13 16:04:25 +09:00
Spacer(modifier = Modifier.width(12.dp))
// 주문 섹션 (인자 간소화)
2026-01-14 15:42:26 +09:00
Column(modifier = Modifier.weight(0.6f)) {
IntegratedOrderSection(
stockCode = stockCode,
2026-01-19 17:09:37 +09:00
stockName = stockName,
isDomestic = isDomestic,
2026-02-13 13:49:40 +09:00
currentPrice = lastPrice,
2026-01-19 17:09:37 +09:00
holdingQuantity = holdingQuantity,
2026-01-14 15:42:26 +09:00
tradeService = tradeService,
2026-01-21 11:49:30 +09:00
onOrderSaved = onOrderSaved,
2026-01-14 15:42:26 +09:00
onOrderResult = { msg, success ->
resultMessage = msg
isSuccess = success
2026-01-23 17:05:09 +09:00
},
completeTradingDecision
2026-01-14 15:42:26 +09:00
)
}
}
}
}
@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)
2026-01-10 18:16:50 +09:00
}
}
}