...
This commit is contained in:
@@ -30,7 +30,7 @@ class KisAuthService {
|
||||
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL // 상세한 디버깅을 위해 ALL로 변경
|
||||
level = LogLevel.NONE // 상세한 디버깅을 위해 ALL로 변경
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class KisTradeService {
|
||||
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL // 상세한 디버깅을 위해 ALL로 변경
|
||||
level = LogLevel.BODY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,18 +337,7 @@ class KisTradeService {
|
||||
} catch (e: Exception) { Result.failure(e) }
|
||||
}
|
||||
|
||||
fun UnfilledOrder.toAutoTradeItem(isDomestic: Boolean): AutoTradeItem {
|
||||
return AutoTradeItem(
|
||||
orderNo = this.ord_no,
|
||||
code = this.pdno,
|
||||
name = this.prdt_name,
|
||||
orderedPrice = this.ord_unpr.toDoubleOrNull() ?: 0.0,
|
||||
quantity = 0, // 미체결 내역에서는 원 주문 수량을 알기 어려우므로 0 또는 별도 처리
|
||||
remainedQuantity = this.rmnd_qty.toIntOrNull() ?: 0,
|
||||
status = "PENDING_BUY", // 기본적으로 미체결은 매수/매도 대기 상태
|
||||
isDomestic = isDomestic
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [추가] 국내 미체결 내역 조회
|
||||
@@ -381,14 +370,9 @@ class KisTradeService {
|
||||
parameter("INQR_DVSN_1", "0")
|
||||
parameter("INQR_DVSN_2", "0")
|
||||
}
|
||||
println("result >> ${response.status}")
|
||||
val body = response.body<UnfilledResponse>()
|
||||
println("result >> ${body.msg1}")
|
||||
println("result >> ${body.rt_cd}")
|
||||
if (body.rt_cd == "0") {
|
||||
|
||||
var result = body
|
||||
println("result >> ${result.output.size}")
|
||||
Result.success(result.output)
|
||||
}
|
||||
else Result.failure(Exception(body.msg1))
|
||||
|
||||
@@ -6,11 +6,19 @@ import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import model.KisSession
|
||||
import util.AesCrypto
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class KisWebSocketManager {
|
||||
private val client = HttpClient { install(WebSockets) }
|
||||
private val client = HttpClient {
|
||||
install(WebSockets) {
|
||||
pingInterval = 20_000 // 20초마다 표준 웹소켓 핑 전송 (서버-클라이언트 연결 유지 도움)
|
||||
}
|
||||
}
|
||||
private var session: DefaultClientWebSocketSession? = null
|
||||
private val isConnected = AtomicBoolean(false)
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
@@ -21,27 +29,42 @@ class KisWebSocketManager {
|
||||
|
||||
suspend fun connect() {
|
||||
if (isConnected.get()) return
|
||||
val url = if (KisSession.config.isSimulation) "ops.koreainvestment.com:21000" else "ops.koreainvestment.com:31000"
|
||||
val url = if (KisSession.config.isSimulation) "ops.koreainvestment.com:31000" else "ops.koreainvestment.com:21000"
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
client.webSocket(method = HttpMethod.Get, host = url.split(":")[0], port = url.split(":")[1].toInt(), path = "/last_price") {
|
||||
session = this
|
||||
isConnected.set(true)
|
||||
println("✅ 웹소켓 연결 성공")
|
||||
while (isActive) { // 재연결을 위한 루프 추가
|
||||
try {
|
||||
client.webSocket(method = HttpMethod.Get, host = url.split(":")[0], port = url.split(":")[1].toInt(), path = "/last_price") {
|
||||
session = this
|
||||
isConnected.set(true)
|
||||
println("✅ 웹소켓 연결 성공")
|
||||
|
||||
// 연결 직후 HTS ID 기반 체결 통보 자동 구독
|
||||
val htsId = KisSession.config.htsId
|
||||
if (htsId.isNotEmpty()) sendRequest("1", "H0STT084R", htsId)
|
||||
// 기존 구독 신청 로직 (H0STCNI0 등)
|
||||
val htsId = KisSession.config.htsId
|
||||
if (htsId.isNotEmpty()) sendRequest("1", "H0STCNI0", htsId)
|
||||
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) handleMessage(frame.readText())
|
||||
// 메시지 수신 루프
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
// [핵심] PINGPONG 처리: 수신된 텍스트 그대로 다시 전송
|
||||
if (text.contains("PINGPONG")) {
|
||||
send(Frame.Text(text))
|
||||
// println("🔄 PINGPONG 응답 완료")
|
||||
} else {
|
||||
handleMessage(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("❌ 웹소켓 연결 끊김: ${e.message}")
|
||||
} finally {
|
||||
isConnected.set(false)
|
||||
session = null
|
||||
println("⏳ 5초 후 재연결 시도...")
|
||||
delay(5000) // 5초 후 재연결 시도
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("❌ 웹소켓 에러: ${e.message}")
|
||||
} finally {
|
||||
isConnected.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,38 +79,76 @@ class KisWebSocketManager {
|
||||
subscribeStock(code, isSubscribe = false)
|
||||
}
|
||||
|
||||
|
||||
// 체결 통보 복호화를 위한 키 저장소
|
||||
private var aesKey: String = ""
|
||||
private var aesIv: String = ""
|
||||
|
||||
private fun handleMessage(message: String) {
|
||||
if (message.startsWith("{")) {
|
||||
val json = Json.parseToJsonElement(message).jsonObject
|
||||
val trId = json["header"]?.jsonObject?.get("tr_id")?.jsonPrimitive?.content
|
||||
|
||||
if (trId == "H0STCNI0" || trId == "H0STCNI9") {
|
||||
val output = json["body"]?.jsonObject?.get("output")?.jsonObject
|
||||
aesKey = output?.get("key")?.jsonPrimitive?.content ?: ""
|
||||
aesIv = output?.get("iv")?.jsonPrimitive?.content ?: ""
|
||||
println("🔑 복호화 키 획득 완료: KEY[$aesKey]")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!message.startsWith("0") && !message.startsWith("1")) return
|
||||
// 2. 실시간 데이터 처리
|
||||
val parts = message.split("|")
|
||||
if (parts.size < 4) return
|
||||
|
||||
val leadingChar = message[0] // '0' 또는 '1'
|
||||
val trId = parts[1]
|
||||
val dataRows = parts[3].split("^")
|
||||
|
||||
when (trId) {
|
||||
"H0STCNT0" -> {
|
||||
val price = dataRows[2]
|
||||
_currentPrice.value = price // 상태 업데이트
|
||||
onPriceUpdate?.invoke(dataRows[0], price.toDoubleOrNull() ?: 0.0)
|
||||
when (leadingChar) {
|
||||
'0' -> { // 일반 시세 (암호화 안됨)
|
||||
if (trId == "H0STCNT0") {
|
||||
val dataRows = parts[3].split("^")
|
||||
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()
|
||||
// 로그 추가 (예시)
|
||||
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()
|
||||
}
|
||||
}
|
||||
"H0STT084R" -> {
|
||||
println("채결 데이터")
|
||||
onExecutionReceived?.invoke(dataRows[5], dataRows[9], dataRows[12], dataRows[13], dataRows[15] == "02")
|
||||
}
|
||||
else -> {
|
||||
println("쓰레기? ${trId}")
|
||||
|
||||
'1' -> { // 체결 통보 (암호화 됨)
|
||||
if ((trId == "H0STCNI0" || trId == "H0STCNI9") && aesKey.isNotEmpty()) {
|
||||
// AES 복호화 실행
|
||||
val decryptedData = AesCrypto.decrypt(parts[3], aesKey, aesIv)
|
||||
val dataRows = decryptedData.split("^")
|
||||
|
||||
println("🔔 복호화된 체결 통보: ${dataRows[8]} ${dataRows[9]}주 ${dataRows[13]} 체결")
|
||||
|
||||
// UI 콜백 호출 (종목코드, 체결량, 체결가, 주문번호, 체결여부)
|
||||
onExecutionReceived?.invoke(
|
||||
dataRows[8], // 주식단축종목코드
|
||||
dataRows[9], // 체결수량
|
||||
dataRows[10], // 체결단가
|
||||
dataRows[2], // 주문번호
|
||||
dataRows[13] == "2" // 체결여부 (02: 체결)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
tradeLogs.clear()
|
||||
_currentPrice.value = "0"
|
||||
|
||||
Reference in New Issue
Block a user