Implement subscribe/unsubscribe for ContractPriceWebSocket
This commit is contained in:
@@ -13,6 +13,8 @@ public enum GeneralError: Error {
|
||||
case invalidInstance
|
||||
case invalidAccessToken
|
||||
case invalidAccountNo
|
||||
case invalidWebSocket
|
||||
case webSocketError(_ code: String, _ messageCode: String, _ message: String)
|
||||
case unsupportedQueryAtMockServer
|
||||
case emptyData(String)
|
||||
case cannotReadFile
|
||||
|
||||
@@ -9,19 +9,15 @@ import Foundation
|
||||
|
||||
|
||||
public protocol WebSocket {
|
||||
associatedtype KResult: Decodable
|
||||
|
||||
var isMockAvailable: Bool { get }
|
||||
var domain: String { get }
|
||||
var url: String { get }
|
||||
var userAgent: String { get }
|
||||
|
||||
var header: [String: String?] { get }
|
||||
var body: [String: Any] { get }
|
||||
var result: KResult? { get set }
|
||||
var timeout: TimeInterval { get }
|
||||
var session: URLSession { get }
|
||||
var socket: URLSessionWebSocketTask? { get set }
|
||||
var credential: WebSocketCredential { get }
|
||||
}
|
||||
|
||||
|
||||
@@ -45,25 +41,27 @@ extension WebSocket {
|
||||
URL(string: domain + url)
|
||||
}
|
||||
|
||||
public mutating func connect() async throws {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
/*
|
||||
mutating func connect() async throws -> URLSessionWebSocketTask {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
guard let queryUrl = queryUrl else {
|
||||
continuation.resume(throwing: QueryError.invalidUrl)
|
||||
return
|
||||
}
|
||||
|
||||
socket = session.webSocketTask(with: queryUrl)
|
||||
socket?.resume()
|
||||
let socket = session.webSocketTask(with: queryUrl)
|
||||
socket.resume()
|
||||
self.socket = socket
|
||||
|
||||
continuation.resume(returning: ())
|
||||
continuation.resume(returning: socket)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func disconnect() {
|
||||
mutating func disconnect() {
|
||||
socket?.cancel(with: .normalClosure, reason: nil)
|
||||
socket = nil
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +81,41 @@ extension AuthWebSocket {
|
||||
"ws://ops.koreainvestment.com:31000":
|
||||
"ws://ops.koreainvestment.com:21000"
|
||||
}
|
||||
|
||||
public mutating func connect() async throws {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
guard let queryUrl = queryUrl else {
|
||||
continuation.resume(throwing: QueryError.invalidUrl)
|
||||
return
|
||||
}
|
||||
|
||||
let socket = session.webSocketTask(with: queryUrl)
|
||||
socket.resume()
|
||||
self.socket = socket
|
||||
|
||||
continuation.resume(returning: ())
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func disconnect() {
|
||||
socket?.cancel(with: .normalClosure, reason: nil)
|
||||
socket = nil
|
||||
}
|
||||
|
||||
func receive<T>() async throws -> T where T: Decodable {
|
||||
guard let socket = socket else {
|
||||
throw GeneralError.invalidWebSocket
|
||||
}
|
||||
|
||||
let message = try await socket.receive()
|
||||
guard let data = message.data else {
|
||||
throw GeneralError.emptyData("subscribe()")
|
||||
}
|
||||
print(message)
|
||||
|
||||
let r = try JSONDecoder().decode(T.self, from: data)
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,3 +137,17 @@ public struct KissWebSocketCredential: WebSocketCredential, Codable {
|
||||
self.approvalKey = approvalKey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension URLSessionWebSocketTask.Message {
|
||||
var data: Data? {
|
||||
switch self {
|
||||
case .string(let str):
|
||||
return Data(str.utf8)
|
||||
case .data(let data):
|
||||
return data
|
||||
@unknown default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ extension Domestic {
|
||||
|
||||
/// 국내주식 실시간호가[실시간-004]
|
||||
///
|
||||
public struct AskingPriceWebSocket: AuthWebSocket {
|
||||
public typealias KResult = String
|
||||
|
||||
public class AskingPriceWebSocket: AuthWebSocket {
|
||||
public var url: String {
|
||||
"/tryitout/H0STASP0"
|
||||
}
|
||||
@@ -36,7 +34,6 @@ extension Domestic {
|
||||
}
|
||||
|
||||
public var socket: URLSessionWebSocketTask?
|
||||
public var result: KResult? = nil
|
||||
public var credential: WebSocketCredential
|
||||
var event: Event!
|
||||
let productCode: String
|
||||
|
||||
@@ -15,8 +15,6 @@ extension Domestic {
|
||||
/// 국내주식 실시간체결통보[실시간-005]
|
||||
///
|
||||
public struct ContractNoticeWebSocket: AuthWebSocket {
|
||||
public typealias KResult = String
|
||||
|
||||
public var url: String {
|
||||
"/tryitout/H0STCNI0"
|
||||
}
|
||||
@@ -36,7 +34,6 @@ extension Domestic {
|
||||
}
|
||||
|
||||
public var socket: URLSessionWebSocketTask?
|
||||
public var result: KResult? = nil
|
||||
public var credential: WebSocketCredential
|
||||
var event: Event!
|
||||
let htsID: String
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
enum KissWebSocketSubscription: String {
|
||||
enum KissWebSocketSubscription: String, Codable {
|
||||
case subscribed = "1"
|
||||
case unsubscribed = "2"
|
||||
}
|
||||
@@ -26,28 +26,11 @@ extension Domestic {
|
||||
/// 국내주식 실시간체결가[실시간-003]
|
||||
///
|
||||
public class ContractPriceWebSocket: AuthWebSocket {
|
||||
public typealias KResult = String
|
||||
|
||||
public var url: String {
|
||||
"/tryitout/H0STCNT0"
|
||||
}
|
||||
public var header: [String: String?] {
|
||||
[
|
||||
"approval_key": credential.approvalKey,
|
||||
"custtype": "P",
|
||||
"tr_type": KissWebSocketSubscription.subscribed.rawValue,
|
||||
"content-type": "utf-8"
|
||||
]
|
||||
}
|
||||
public var body: [String: Any] {
|
||||
[
|
||||
"tr_id": "H0STASP0",
|
||||
"tr_key": productCode,
|
||||
]
|
||||
}
|
||||
|
||||
public var socket: URLSessionWebSocketTask?
|
||||
public var result: KResult? = nil
|
||||
public var credential: WebSocketCredential
|
||||
var event: Event!
|
||||
let productCode: String
|
||||
@@ -59,25 +42,41 @@ extension Domestic {
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct Message: KissWebSocketMessage {
|
||||
|
||||
let subscription: KissWebSocketSubscription
|
||||
|
||||
init(subscription: KissWebSocketSubscription) {
|
||||
self.subscription = subscription
|
||||
public func subscribe() async throws -> Bool {
|
||||
guard let socket = socket else {
|
||||
throw GeneralError.invalidWebSocket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func subscribe() {
|
||||
// json 포맷으로 데이터를 송수신
|
||||
|
||||
let request = SubscriptionRequest(approvalKey: credential.approvalKey, type: .subscribed, productCode: productCode)
|
||||
let requestData = try JSONEncoder().encode(request)
|
||||
let requestJson = String(data: requestData, encoding: .utf8)!
|
||||
print(requestJson)
|
||||
|
||||
try await socket.send(.string(requestJson))
|
||||
let r: SubscriptionResult = try await receive()
|
||||
guard r.body.output != nil, r.body.resultCode == "0" else {
|
||||
throw GeneralError.webSocketError(r.body.resultCode, r.body.messageCode, r.body.message)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
// json 포맷으로 데이터를 송수신
|
||||
|
||||
|
||||
public func unsubscribe() async throws -> Bool {
|
||||
guard let socket = socket else {
|
||||
throw GeneralError.invalidWebSocket
|
||||
}
|
||||
|
||||
let request = SubscriptionRequest(approvalKey: credential.approvalKey, type: .unsubscribed, productCode: productCode)
|
||||
let requestData = try JSONEncoder().encode(request)
|
||||
let requestJson = String(data: requestData, encoding: .utf8)!
|
||||
print(requestJson)
|
||||
|
||||
try await socket.send(.string(requestJson))
|
||||
let r: SubscriptionResult = try await receive()
|
||||
guard r.body.output != nil, r.body.resultCode == "0" else {
|
||||
throw GeneralError.webSocketError(r.body.resultCode, r.body.messageCode, r.body.message)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +91,83 @@ extension Domestic.ContractPriceWebSocket {
|
||||
self.socket = socket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SubscriptionRequest: Codable {
|
||||
let header: Header
|
||||
let body: Body
|
||||
|
||||
struct Header: Codable {
|
||||
let approvalKey: String
|
||||
let customerType: CustomerType
|
||||
let trType: KissWebSocketSubscription
|
||||
let contentType: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case approvalKey = "approval_key"
|
||||
case customerType = "custtype"
|
||||
case trType = "tr_type"
|
||||
case contentType = "content-type"
|
||||
}
|
||||
}
|
||||
|
||||
struct Body: Codable {
|
||||
let input: Input
|
||||
}
|
||||
|
||||
struct Input: Codable {
|
||||
let trId: String
|
||||
let trKey: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case trId = "tr_id"
|
||||
case trKey = "tr_key"
|
||||
}
|
||||
}
|
||||
|
||||
init(approvalKey: String, type: KissWebSocketSubscription, productCode: String) {
|
||||
|
||||
header = Header(approvalKey: approvalKey, customerType: .personal, trType: type, contentType: "utf-8")
|
||||
body = Body(input: Input(trId: "H0STASP0", trKey: productCode))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SubscriptionResult: Codable {
|
||||
let header: Header
|
||||
let body: Body
|
||||
|
||||
struct Header: Codable {
|
||||
let trId: String
|
||||
let trKey: String
|
||||
let encrypted: YesNo
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case trId = "tr_id"
|
||||
case trKey = "tr_key"
|
||||
case encrypted = "encrypt"
|
||||
}
|
||||
}
|
||||
|
||||
struct Body: Codable {
|
||||
let resultCode: String
|
||||
let messageCode: String
|
||||
let message: String
|
||||
let output: OutputDetail?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case resultCode = "rt_cd"
|
||||
case messageCode = "msg_cd"
|
||||
case message = "msg1"
|
||||
case output
|
||||
}
|
||||
}
|
||||
|
||||
struct OutputDetail: Codable {
|
||||
let iv: String
|
||||
let key: String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum CustomerType: String {
|
||||
public enum CustomerType: String, Codable {
|
||||
case corporation = "B"
|
||||
case personal = "P"
|
||||
}
|
||||
|
||||
@@ -37,13 +37,35 @@ public class KissProfile {
|
||||
return profile.recent?.isMock
|
||||
}
|
||||
|
||||
public var approvalKey: String? {
|
||||
profileLock.lock()
|
||||
defer {
|
||||
profileLock.unlock()
|
||||
}
|
||||
guard let expired = profile.recent?.approvalKeyExpired, expired > Date() else {
|
||||
return nil
|
||||
}
|
||||
return profile.recent?.approvalKey
|
||||
}
|
||||
|
||||
public init() {
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
func setAccessToken(_ accessToken: String, expired: Date, isMock: Bool) {
|
||||
profileLock.lock()
|
||||
profile.recent = Recent(isMock: isMock, accessToken: accessToken, accessTokenExpired: expired)
|
||||
let approvalKey = profile.recent?.approvalKey
|
||||
let approvalKeyExpired = profile.recent?.approvalKeyExpired
|
||||
profile.recent = Recent(isMock: isMock, accessToken: accessToken, accessTokenExpired: expired, approvalKey: approvalKey, approvalKeyExpired: approvalKeyExpired)
|
||||
profileLock.unlock()
|
||||
|
||||
saveProfile()
|
||||
}
|
||||
|
||||
func setApprovalKey(_ approvalKey: String, expired: Date) {
|
||||
profileLock.lock()
|
||||
let recent = profile.recent?.setted(approvalKey: approvalKey, approvalKeyExpired: expired)
|
||||
profile.recent = recent
|
||||
profileLock.unlock()
|
||||
|
||||
saveProfile()
|
||||
@@ -75,6 +97,12 @@ extension KissProfile {
|
||||
public let isMock: Bool
|
||||
public let accessToken: String
|
||||
public let accessTokenExpired: Date
|
||||
public let approvalKey: String?
|
||||
public let approvalKeyExpired: Date?
|
||||
|
||||
func setted(approvalKey: String, approvalKeyExpired: Date) -> Recent {
|
||||
return Recent(isMock: self.isMock, accessToken: self.accessToken, accessTokenExpired: self.accessTokenExpired, approvalKey: approvalKey, approvalKeyExpired: approvalKeyExpired)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Love: Codable {
|
||||
|
||||
@@ -108,6 +108,10 @@ public struct ApprovalKeyAuthRequest: AuthRequest {
|
||||
public var result: KResult? = nil
|
||||
public let credential: Credential
|
||||
|
||||
// TEMP: write logging temporarily
|
||||
public var responseDataLoggable: Bool { return true }
|
||||
public var traceable: Bool { return true }
|
||||
|
||||
public init(credential: Credential) {
|
||||
self.credential = credential
|
||||
}
|
||||
@@ -178,13 +182,19 @@ extension KissAccount {
|
||||
}
|
||||
|
||||
|
||||
public func approvalKey() async throws -> String {
|
||||
public func getApprovalKey() async throws -> String {
|
||||
if let approvalKey = approvalKey {
|
||||
return approvalKey
|
||||
}
|
||||
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
let SecondsForOneDay: TimeInterval = 60 * 60 * 24
|
||||
let request = ApprovalKeyAuthRequest(credential: credential)
|
||||
request.query { result in
|
||||
switch result {
|
||||
case .success(let result):
|
||||
self.setApprovalKey(result.approvalKey, expired: Date().addingTimeInterval(SecondsForOneDay - 60))
|
||||
continuation.resume(returning: result.approvalKey)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
|
||||
@@ -25,11 +25,16 @@ func test_get_websocket_key_and_request_simple() {
|
||||
|
||||
let webSocketCredential = KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey)
|
||||
|
||||
var socket = Domestic.ContractPriceWebSocket(credential: webSocketCredential, productCode: KissConsole.defaultProductNo)
|
||||
let socket = Domestic.ContractPriceWebSocket(credential: webSocketCredential, productCode: KissConsole.defaultProductNo)
|
||||
|
||||
do {
|
||||
try await socket.connect()
|
||||
let result = try await socket.subscribe()
|
||||
print(result)
|
||||
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000 * 3)
|
||||
let result2 = try await socket.unsubscribe()
|
||||
print(result2)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
@@ -56,8 +61,13 @@ func test_get_websocket_key(isMock: Bool) async -> (KissAccount, String)? {
|
||||
|
||||
let account = KissAccount(credential: credential)
|
||||
do {
|
||||
/// Return existing valid key
|
||||
if let approvalKey = account.approvalKey {
|
||||
return (account, approvalKey)
|
||||
}
|
||||
|
||||
if try await account.login() {
|
||||
let approvalKey = try await account.approvalKey()
|
||||
let approvalKey = try await account.getApprovalKey()
|
||||
print("approvalKey : \(approvalKey)")
|
||||
return (account, approvalKey)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user