2026-01-13 16:04:25 +09:00
|
|
|
// src/main/kotlin/ui/DashboardScreen.kt
|
2026-01-10 18:16:50 +09:00
|
|
|
package ui
|
|
|
|
|
|
2026-01-14 15:42:26 +09:00
|
|
|
import AutoTradeItem
|
2026-01-10 18:16:50 +09:00
|
|
|
import androidx.compose.foundation.background
|
|
|
|
|
import androidx.compose.foundation.layout.*
|
|
|
|
|
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.unit.dp
|
2026-01-14 15:42:26 +09:00
|
|
|
import kotlinx.coroutines.launch
|
2026-01-13 16:04:25 +09:00
|
|
|
import model.KisSession
|
2026-01-10 18:16:50 +09:00
|
|
|
import network.KisTradeService
|
|
|
|
|
import network.KisWebSocketManager
|
|
|
|
|
|
|
|
|
|
@Composable
|
2026-01-13 16:04:25 +09:00
|
|
|
fun DashboardScreen() {
|
|
|
|
|
val tradeService = remember { KisTradeService() }
|
|
|
|
|
val wsManager = remember { KisWebSocketManager() }
|
2026-01-14 15:42:26 +09:00
|
|
|
val config = KisSession.config
|
|
|
|
|
val scope = rememberCoroutineScope()
|
|
|
|
|
// 데이터 갱신을 위한 트리거 상태
|
|
|
|
|
var refreshTrigger by remember { mutableStateOf(0) }
|
2026-01-13 16:04:25 +09:00
|
|
|
// 전역 상태: 현재 선택된 종목 정보
|
2026-01-10 18:16:50 +09:00
|
|
|
var selectedStockCode by remember { mutableStateOf("") }
|
|
|
|
|
var selectedStockName by remember { mutableStateOf("") }
|
2026-01-13 16:04:25 +09:00
|
|
|
var isDomestic by remember { mutableStateOf(true) }
|
2026-01-10 18:16:50 +09:00
|
|
|
|
|
|
|
|
LaunchedEffect(Unit) {
|
2026-01-14 15:42:26 +09:00
|
|
|
// 1. 웹소켓 연결
|
2026-01-13 16:04:25 +09:00
|
|
|
wsManager.connect()
|
2026-01-14 15:42:26 +09:00
|
|
|
|
|
|
|
|
// 2. 체결 통보 콜백 설정 (매수 성공 시 감시 시작)
|
|
|
|
|
wsManager.onExecutionReceived = { orderNo, code, price, qty, isBuy ->
|
|
|
|
|
if (isBuy) {
|
|
|
|
|
// [매수 체결 시] DB에 감시 데이터 저장
|
|
|
|
|
// 주의: targetPrice와 stopLossPrice는 이전에 설정된 값을 가져오거나
|
|
|
|
|
// 임시 상태값에서 가져와야 함 (여기선 예시로 현재가의 +5%, -3% 설정)
|
|
|
|
|
val execPrice = price.toDoubleOrNull() ?: 0.0
|
|
|
|
|
DatabaseFactory.saveAutoTrade(
|
|
|
|
|
AutoTradeItem(
|
|
|
|
|
code = code,
|
|
|
|
|
name = "", // 필요 시 종목명 매핑
|
|
|
|
|
targetPrice = execPrice * 1.05,
|
|
|
|
|
stopLossPrice = execPrice * 0.97,
|
|
|
|
|
status = "MONITORING",
|
|
|
|
|
isDomestic = true
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
println("📝 매수 체결로 인한 자동 감시 등록: $code")
|
|
|
|
|
} else {
|
|
|
|
|
// [매도 체결 시] 감시 종료 및 DB 삭제
|
|
|
|
|
DatabaseFactory.deleteAutoTrade(code)
|
|
|
|
|
println("✅ 매도 체결로 인한 감시 종료: $code")
|
|
|
|
|
}
|
|
|
|
|
refreshTrigger++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 목표가 도달 콜백 설정 (자동 매도 실행)
|
|
|
|
|
wsManager.onTargetReached = { code, price, isProfit ->
|
|
|
|
|
scope.launch {
|
|
|
|
|
println("🚀 목표가 도달! 자동 매도 주문 실행: $code (이유: ${if(isProfit) "익절" else "손절"})")
|
|
|
|
|
|
|
|
|
|
// 실제 매도 주문 API 호출
|
|
|
|
|
tradeService.postOrder(
|
|
|
|
|
stockCode = code,
|
|
|
|
|
qty = "1", // 실제론 보유 수량을 가져와야 함
|
|
|
|
|
price = "0", // 시장가 매도
|
|
|
|
|
isBuy = false
|
|
|
|
|
).onSuccess {
|
|
|
|
|
// 매도 주문 성공 시 로그 기록
|
|
|
|
|
DatabaseFactory.saveTradeLog(
|
|
|
|
|
code, "", "매도", price, 1,
|
|
|
|
|
if(isProfit) "AI 익절 조건 달성" else "AI 손절 조건 달성"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
refreshTrigger++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.htsId.isNotEmpty()) {
|
|
|
|
|
wsManager.subscribeExecution(config.htsId)
|
|
|
|
|
println("📡 HTS ID(${config.htsId})로 체결 통보 구독을 시작합니다.")
|
|
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
2026-01-13 16:04:25 +09:00
|
|
|
// [좌측 25%] 내 자산 및 통합 잔고
|
2026-01-14 15:42:26 +09:00
|
|
|
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
|
2026-01-13 16:04:25 +09:00
|
|
|
BalanceSection(tradeService) { code, name, isDom ->
|
2026-01-10 18:16:50 +09:00
|
|
|
selectedStockCode = code
|
|
|
|
|
selectedStockName = name
|
2026-01-13 16:04:25 +09:00
|
|
|
isDomestic = isDom
|
|
|
|
|
println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VerticalDivider()
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// [중앙 45%] 실시간 정보 및 주문
|
|
|
|
|
Column(modifier = Modifier.weight(0.45f).fillMaxHeight().background(Color.White)) {
|
2026-01-10 18:16:50 +09:00
|
|
|
if (selectedStockCode.isNotEmpty()) {
|
2026-01-13 16:04:25 +09:00
|
|
|
StockDetailSection(
|
|
|
|
|
stockCode = selectedStockCode,
|
|
|
|
|
stockName = selectedStockName,
|
|
|
|
|
isDomestic = isDomestic,
|
|
|
|
|
tradeService = tradeService,
|
|
|
|
|
wsManager = wsManager
|
|
|
|
|
)
|
2026-01-10 18:16:50 +09:00
|
|
|
} else {
|
|
|
|
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
2026-01-13 16:04:25 +09:00
|
|
|
Text("분석할 종목을 선택하세요", color = Color.Gray)
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VerticalDivider()
|
2026-01-14 15:42:26 +09:00
|
|
|
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
|
|
|
|
|
AutoTradeSection(
|
|
|
|
|
tradeService = tradeService,
|
|
|
|
|
onRefresh = { refreshTrigger++ },
|
|
|
|
|
refreshTrigger = refreshTrigger // 트리거 전달
|
|
|
|
|
) { item ->
|
|
|
|
|
selectedStockCode = item.code
|
|
|
|
|
selectedStockName = item.name
|
|
|
|
|
isDomestic = item.isDomestic
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
VerticalDivider()
|
2026-01-13 16:04:25 +09:00
|
|
|
// [우측 30%] 시장 추천 TOP 20 (실전 데이터)
|
2026-01-14 15:42:26 +09:00
|
|
|
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
|
2026-01-13 16:04:25 +09:00
|
|
|
MarketSection(tradeService) { code, name, isDom ->
|
2026-01-10 18:16:50 +09:00
|
|
|
selectedStockCode = code
|
|
|
|
|
selectedStockName = name
|
2026-01-13 16:04:25 +09:00
|
|
|
isDomestic = isDom
|
|
|
|
|
println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Composable
|
2026-01-13 16:04:25 +09:00
|
|
|
fun VerticalDivider() {
|
|
|
|
|
Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|