...
This commit is contained in:
@@ -72,7 +72,7 @@ fun ActiveTradeRow(
|
||||
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}"
|
||||
else -> "주문번호: ${item.orderNo} ${item.orderedPrice} ${item.quantity}"
|
||||
}
|
||||
|
||||
Text(
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.ExecutionData
|
||||
import model.KisSession
|
||||
import model.StockBasicInfo
|
||||
import network.KisTradeService
|
||||
@@ -22,7 +23,6 @@ fun DashboardScreen() {
|
||||
val tradeService = remember { KisTradeService() }
|
||||
val wsManager = remember { KisWebSocketManager() }
|
||||
val scope = rememberCoroutineScope()
|
||||
var refreshTrigger by remember { mutableStateOf(0) }
|
||||
var selectedStockCode by remember { mutableStateOf("") }
|
||||
var selectedStockName by remember { mutableStateOf("") }
|
||||
var isDomestic by remember { mutableStateOf(true) }
|
||||
@@ -31,7 +31,53 @@ fun DashboardScreen() {
|
||||
var selectedItem by remember { mutableStateOf<AutoTradeItem?>(null) } // 감시/미체결 아이템 선택 시
|
||||
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
||||
|
||||
// 중앙 관리용 상태들
|
||||
var refreshTrigger by remember { mutableStateOf(0) }
|
||||
// [핵심] 아직 DB에 등록되기 전에 도착한 체결 데이터를 임시 보관하는 버퍼
|
||||
val executionCache = remember { mutableMapOf<String, ExecutionData>() }
|
||||
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||
suspend fun syncAndExecute(orderNo: String) {
|
||||
// 이미 다른 코루틴에서 이 주문을 처리 중이라면 즉시 종료 (중복 방지)
|
||||
if (processingIds.contains(orderNo)) return
|
||||
processingIds.add(orderNo)
|
||||
|
||||
try {
|
||||
// DB 아이템과 체결 데이터(캐시)를 모두 가져옴
|
||||
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
||||
val execData = executionCache[orderNo]
|
||||
|
||||
// 둘 다 존재할 때만 로직 실행
|
||||
if (dbItem != null && execData != null && execData.isFilled) {
|
||||
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
||||
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} (${orderNo})")
|
||||
|
||||
val sellPrice = dbItem.targetPrice.toLong().toString()
|
||||
tradeService.postOrder(
|
||||
stockCode = dbItem.code,
|
||||
qty = dbItem.quantity.toString(),
|
||||
price = sellPrice,
|
||||
isBuy = false
|
||||
).onSuccess { newSellOrderNo ->
|
||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
||||
// 처리가 완료된 체결 데이터는 캐시에서 삭제
|
||||
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. 웹소켓 연결
|
||||
@@ -55,39 +101,12 @@ fun DashboardScreen() {
|
||||
}
|
||||
|
||||
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
|
||||
wsManager.onExecutionReceived = { orderNo, code, price, qty, isBuy ->
|
||||
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
|
||||
scope.launch {
|
||||
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
||||
if (dbItem != null) {
|
||||
when (dbItem.status) {
|
||||
TradeStatus.PENDING_BUY -> {
|
||||
// 1. 매수 주문 체결 확인됨 -> 즉시 익절 매도 주문 발주
|
||||
println("✅ 매수 체결 확인 [${dbItem.name}]: 익절가 ${dbItem.targetPrice}로 매도 주문을 생성합니다.")
|
||||
tradeService.postOrder(
|
||||
stockCode = dbItem.code,
|
||||
qty = dbItem.quantity.toString(),
|
||||
price = dbItem.targetPrice.toLong().toString(), // 가격은 정수형 문자열로 전달
|
||||
isBuy = false
|
||||
).onSuccess { newSellOrderNo ->
|
||||
// 2. 매도 주문 성공 시 DB 상태를 SELLING으로 변경하고 새로운 주문번호로 갱신
|
||||
DatabaseFactory.updateStatusAndOrderNo(
|
||||
id = dbItem.id!!,
|
||||
newStatus = TradeStatus.SELLING,
|
||||
newOrderNo = newSellOrderNo
|
||||
)
|
||||
println("🚀 익절 매도 주문 완료: 주문번호 $newSellOrderNo")
|
||||
refreshTrigger++ // UI 갱신
|
||||
}.onFailure {
|
||||
println("❌ 매수 체결 후 익절 주문 발주 실패: ${it.message}")
|
||||
}
|
||||
}
|
||||
TradeStatus.SELLING -> {
|
||||
// 매도(손절/익절) 주문 체결 -> COMPLETED
|
||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.COMPLETED)
|
||||
}
|
||||
}
|
||||
refreshTrigger++
|
||||
}
|
||||
println("$orderNo, $code, $price, $qty, $isBuy")
|
||||
val exec = ExecutionData(orderNo, code, price, qty, isBuy)
|
||||
executionCache[orderNo] = exec
|
||||
syncAndExecute(orderNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +136,12 @@ fun DashboardScreen() {
|
||||
holdingQuantity = selectedStockQuantity,
|
||||
isDomestic = isDomestic,
|
||||
tradeService = tradeService,
|
||||
wsManager = wsManager
|
||||
wsManager = wsManager,
|
||||
onOrderSaved = { orderNo ->
|
||||
scope.launch {
|
||||
syncAndExecute(orderNo) // 매칭 시도
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import util.MarketUtil
|
||||
|
||||
/**
|
||||
* 통합 주문 및 자동매매 설정 섹션
|
||||
@@ -31,6 +33,7 @@ fun IntegratedOrderSection(
|
||||
currentPrice: String,
|
||||
holdingQuantity: String,
|
||||
tradeService: KisTradeService,
|
||||
onOrderSaved: (String) -> Unit,
|
||||
onOrderResult: (String, Boolean) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -40,7 +43,16 @@ fun IntegratedOrderSection(
|
||||
mutableStateOf(DatabaseFactory.findConfigByCode(stockCode))
|
||||
}
|
||||
|
||||
val isAutoSellEnabled = monitoringItem != null
|
||||
var activeMonitoringItem by remember(stockCode) {
|
||||
mutableStateOf(DatabaseFactory.findConfigByCode(stockCode))
|
||||
}
|
||||
|
||||
// 2. 체크박스의 '의도' 상태 (신규 매수 시 자동감시를 켤 것인지 여부)
|
||||
// 감시 중인 아이템이 있으면 true, 없으면 사용자 선택에 따름
|
||||
var willEnableAutoSell by remember(stockCode) {
|
||||
mutableStateOf(activeMonitoringItem != null)
|
||||
}
|
||||
|
||||
|
||||
// UI 입력 상태
|
||||
var orderPrice by remember { mutableStateOf("") } // 빈 값이면 시장가
|
||||
@@ -94,8 +106,9 @@ fun IntegratedOrderSection(
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = isAutoSellEnabled,
|
||||
checked = willEnableAutoSell,
|
||||
onCheckedChange = { checked ->
|
||||
willEnableAutoSell = checked
|
||||
if (!checked) {
|
||||
// [감시 해제] DB ID를 사용하여 정확한 항목 삭제 (데이터 꼬임 방지)
|
||||
monitoringItem?.id?.let { dbId ->
|
||||
@@ -105,27 +118,27 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
} else {
|
||||
// [즉시 감시 등록] 보유 종목에 대해 가상의 주문번호로 감시 시작
|
||||
if (curPriceNum > 0) {
|
||||
val pRate = profitRate.toDoubleOrNull() ?: 0.0
|
||||
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
|
||||
val target = curPriceNum * (1 + pRate / 100.0)
|
||||
val stop = curPriceNum * (1 + sRate / 100.0)
|
||||
|
||||
val newItem = AutoTradeItem(
|
||||
orderNo = "EXISTING_${stockCode}_${System.currentTimeMillis()}",
|
||||
code = stockCode,
|
||||
name = stockName,
|
||||
quantity = inputQty,
|
||||
profitRate = pRate,
|
||||
stopLossRate = sRate,
|
||||
targetPrice = target,
|
||||
stopLossPrice = stop,
|
||||
status = "MONITORING",
|
||||
isDomestic = isDomestic
|
||||
)
|
||||
DatabaseFactory.saveAutoTrade(newItem)
|
||||
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
||||
}
|
||||
// if (curPriceNum > 0) {
|
||||
// val pRate = profitRate.toDoubleOrNull() ?: 0.0
|
||||
// val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
|
||||
// val target = curPriceNum * (1 + pRate / 100.0)
|
||||
// val stop = curPriceNum * (1 + sRate / 100.0)
|
||||
//
|
||||
// val newItem = AutoTradeItem(
|
||||
// orderNo = "EXISTING_${stockCode}_${System.currentTimeMillis()}",
|
||||
// code = stockCode,
|
||||
// name = stockName,
|
||||
// quantity = inputQty,
|
||||
// profitRate = pRate,
|
||||
// stopLossRate = sRate,
|
||||
// targetPrice = target,
|
||||
// stopLossPrice = stop,
|
||||
// status = "MONITORING",
|
||||
// isDomestic = isDomestic
|
||||
// )
|
||||
// DatabaseFactory.saveAutoTrade(newItem)
|
||||
// monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
||||
// }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -136,12 +149,12 @@ fun IntegratedOrderSection(
|
||||
OutlinedTextField(
|
||||
value = profitRate, onValueChange = { profitRate = it },
|
||||
label = { Text("익절 %") }, modifier = Modifier.weight(1f).padding(end = 4.dp),
|
||||
enabled = !isAutoSellEnabled
|
||||
enabled = !willEnableAutoSell
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = stopLossRate, onValueChange = { stopLossRate = it },
|
||||
label = { Text("손절 %") }, modifier = Modifier.weight(1f),
|
||||
enabled = !isAutoSellEnabled
|
||||
enabled = !willEnableAutoSell
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -159,9 +172,12 @@ fun IntegratedOrderSection(
|
||||
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
|
||||
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
|
||||
onOrderResult("주문 성공: $realOrderNo", true)
|
||||
if (isAutoSellEnabled) {
|
||||
if (willEnableAutoSell) {
|
||||
val pRate = profitRate.toDoubleOrNull() ?: 0.0
|
||||
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
|
||||
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + pRate / 100.0))
|
||||
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
||||
|
||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
||||
orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙)
|
||||
code = stockCode,
|
||||
@@ -169,12 +185,14 @@ fun IntegratedOrderSection(
|
||||
quantity = inputQty,
|
||||
profitRate = pRate,
|
||||
stopLossRate = sRate,
|
||||
targetPrice = basePrice * (1 + pRate / 100.0),
|
||||
stopLossPrice = basePrice * (1 + sRate / 100.0),
|
||||
targetPrice = calculatedTarget,
|
||||
stopLossPrice = calculatedStop,
|
||||
status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태
|
||||
isDomestic = isDomestic
|
||||
))
|
||||
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
||||
onOrderSaved(realOrderNo)
|
||||
onOrderResult("매수 및 즉시 체결 확인: $realOrderNo", true)
|
||||
}
|
||||
}
|
||||
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
|
||||
|
||||
@@ -41,7 +41,8 @@ fun StockDetailSection(
|
||||
holdingQuantity : String,
|
||||
isDomestic: Boolean,
|
||||
tradeService: KisTradeService,
|
||||
wsManager: KisWebSocketManager
|
||||
wsManager: KisWebSocketManager,
|
||||
onOrderSaved: (String) -> Unit
|
||||
) {
|
||||
|
||||
var openPrice by remember { mutableStateOf("0") }
|
||||
@@ -220,6 +221,7 @@ fun StockDetailSection(
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
holdingQuantity = holdingQuantity,
|
||||
tradeService = tradeService,
|
||||
onOrderSaved = onOrderSaved,
|
||||
onOrderResult = { msg, success ->
|
||||
resultMessage = msg
|
||||
isSuccess = success
|
||||
|
||||
Reference in New Issue
Block a user