atrade/src/main/kotlin/network/KisWebSocketManager.kt

127 lines
4.7 KiB
Kotlin
Raw Normal View History

2026-01-10 18:16:50 +09:00
package network
import androidx.compose.runtime.mutableStateOf
import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.http.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
2026-01-13 16:04:25 +09:00
import model.KisSession
2026-01-19 17:09:37 +09:00
import java.util.concurrent.atomic.AtomicBoolean
2026-01-10 18:16:50 +09:00
2026-01-13 16:04:25 +09:00
class KisWebSocketManager {
2026-01-19 17:09:37 +09:00
private val client = HttpClient { install(WebSockets) }
2026-01-13 16:04:25 +09:00
private var session: DefaultClientWebSocketSession? = null
2026-01-19 17:09:37 +09:00
private val isConnected = AtomicBoolean(false)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
2026-01-14 15:42:26 +09:00
2026-01-19 17:09:37 +09:00
// 콜백 리스너
var onPriceUpdate: ((String, Double) -> Unit)? = null
var onExecutionReceived: ((String, String, String, String, Boolean) -> Unit)? = null
2026-01-14 15:42:26 +09:00
2026-01-13 16:04:25 +09:00
suspend fun connect() {
2026-01-19 17:09:37 +09:00
if (isConnected.get()) return
val url = if (KisSession.config.isSimulation) "ops.koreainvestment.com:21000" else "ops.koreainvestment.com:31000"
2026-01-10 18:16:50 +09:00
scope.launch {
try {
2026-01-19 17:09:37 +09:00
client.webSocket(method = HttpMethod.Get, host = url.split(":")[0], port = url.split(":")[1].toInt(), path = "/last_price") {
2026-01-10 18:16:50 +09:00
session = this
2026-01-19 17:09:37 +09:00
isConnected.set(true)
println("✅ 웹소켓 연결 성공")
// 연결 직후 HTS ID 기반 체결 통보 자동 구독
val htsId = KisSession.config.htsId
if (htsId.isNotEmpty()) sendRequest("1", "H0STT084R", htsId)
2026-01-13 16:04:25 +09:00
2026-01-19 17:09:37 +09:00
for (frame in incoming) {
if (frame is Frame.Text) handleMessage(frame.readText())
2026-01-10 18:16:50 +09:00
}
}
} catch (e: Exception) {
2026-01-19 17:09:37 +09:00
println("❌ 웹소켓 에러: ${e.message}")
} finally {
isConnected.set(false)
2026-01-10 18:16:50 +09:00
}
}
}
2026-01-14 15:42:26 +09:00
2026-01-19 17:09:37 +09:00
private val _currentPrice = mutableStateOf("0")
val currentPrice = _currentPrice
2026-01-10 18:16:50 +09:00
2026-01-19 17:09:37 +09:00
val tradeLogs = androidx.compose.runtime.mutableStateListOf<model.RealTimeTrade>()
2026-01-14 15:42:26 +09:00
2026-01-19 17:09:37 +09:00
suspend fun unsubscribeStock(code: String) {
subscribeStock(code, isSubscribe = false)
2026-01-10 18:16:50 +09:00
}
2026-01-19 17:09:37 +09:00
private fun handleMessage(message: String) {
if (!message.startsWith("0") && !message.startsWith("1")) return
val parts = message.split("|")
if (parts.size < 4) return
val trId = parts[1]
val dataRows = parts[3].split("^")
2026-01-13 16:04:25 +09:00
2026-01-19 17:09:37 +09:00
when (trId) {
"H0STCNT0" -> {
val price = dataRows[2]
_currentPrice.value = price // 상태 업데이트
onPriceUpdate?.invoke(dataRows[0], price.toDoubleOrNull() ?: 0.0)
// 로그 추가 (예시)
tradeLogs.add(0, model.RealTimeTrade(
time = dataRows[1],
price = price,
change = dataRows[4],
volume = dataRows[2],
type = model.TradeType.NEUTRAL
))
if (tradeLogs.size > 50) tradeLogs.removeLast()
2026-01-14 15:42:26 +09:00
}
2026-01-19 17:09:37 +09:00
"H0STT084R" -> onExecutionReceived?.invoke(dataRows[5], dataRows[9], dataRows[12], dataRows[13], dataRows[15] == "02")
2026-01-14 15:42:26 +09:00
}
2026-01-13 16:04:25 +09:00
}
2026-01-19 17:09:37 +09:00
fun clearData() {
tradeLogs.clear()
_currentPrice.value = "0"
2026-01-14 15:42:26 +09:00
}
2026-01-19 17:09:37 +09:00
suspend fun subscribeStock(code: String, isSubscribe: Boolean = true) {
val trType = if (isSubscribe) "1" else "2"
sendRequest(trType, "H0STCNT0", code)
if (isSubscribe) println("📡 구독 등록: $code") else println("📴 구독 해제: $code")
2026-01-14 15:42:26 +09:00
}
2026-01-19 17:09:37 +09:00
private suspend fun sendRequest(trType: String, trId: String, trKey: String) {
val approvalKey = KisSession.getWebSocketKey() ?: return
val json = """
{
"header": {
"approval_key": "$approvalKey",
"custtype": "P",
"tr_type": "$trType",
"content-type": "utf-8"
},
"body": {
"input": { "tr_id": "$trId", "tr_key": "$trKey" }
2026-01-10 18:16:50 +09:00
}
}
2026-01-13 16:04:25 +09:00
""".trimIndent()
2026-01-19 17:09:37 +09:00
session?.send(json)
2026-01-10 18:16:50 +09:00
}
2026-01-19 17:09:37 +09:00
private val activeSubscriptions = mutableSetOf<String>() // 현재 구독 중인 종목 코드 관리
suspend fun updateSubscriptions(requiredCodes: Set<String>) {
// 해지할 종목 (현재 구독 중이나 요구 리스트에 없는 것)
val toUnsubscribe = activeSubscriptions - requiredCodes
toUnsubscribe.forEach { subscribeStock(it, isSubscribe = false) }
// 신규 구독 (요구 리스트에는 있으나 현재 구독 중이 아닌 것)
val toSubscribe = requiredCodes - activeSubscriptions
toSubscribe.forEach { subscribeStock(it, isSubscribe = true) }
activeSubscriptions.clear()
activeSubscriptions.addAll(requiredCodes)
2026-01-14 15:42:26 +09:00
}
2026-01-19 17:09:37 +09:00
2026-01-10 18:16:50 +09:00
}