diff --git a/KissMe/Sources/Common/Credential.swift b/KissMe/Sources/Common/Credential.swift index a4612de..743a93b 100644 --- a/KissMe/Sources/Common/Credential.swift +++ b/KissMe/Sources/Common/Credential.swift @@ -15,6 +15,7 @@ public protocol Credential { var appSecret: String { get } } + extension String { var digitsOnly: String { filter { ("0"..."9").contains($0) } } } diff --git a/KissMe/Sources/Common/Request.swift b/KissMe/Sources/Common/Request.swift index aabde7a..bbb7d37 100644 --- a/KissMe/Sources/Common/Request.swift +++ b/KissMe/Sources/Common/Request.swift @@ -58,6 +58,7 @@ public protocol Request { var timeout: TimeInterval { get } var responseDataLoggable: Bool { get } var traceable: Bool { get } + var session: URLSession { get } } diff --git a/KissMe/Sources/Common/WebSocket.swift b/KissMe/Sources/Common/WebSocket.swift new file mode 100644 index 0000000..bab782e --- /dev/null +++ b/KissMe/Sources/Common/WebSocket.swift @@ -0,0 +1,106 @@ +// +// WebSocket.swift +// KissMe +// +// Created by ened-book-m1 on 2023/08/11. +// + +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 } +} + + +extension WebSocket { + public var isMockAvailable: Bool { true } + + public var userAgent: String { + //"KissMe/1.0 Matrix/1.0 Golder/1.0" + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + } + + public var header: [String: String?] { [:] } + public var timeout: TimeInterval { 15 } + public var session: URLSession { return URLSession.shared } +} + + +extension WebSocket { + + var queryUrl: URL? { + URL(string: domain + url) + } + + public mutating func connect() async throws { + return try await withUnsafeThrowingContinuation { continuation in + + guard let queryUrl = queryUrl else { + continuation.resume(throwing: QueryError.invalidUrl) + return + } + + socket = session.webSocketTask(with: queryUrl) + socket?.resume() + + continuation.resume(returning: ()) + } + } + + public mutating func disconnect() { + socket?.cancel(with: .normalClosure, reason: nil) + socket = nil + } +} + + +public protocol AuthWebSocket: WebSocket { + var credential: WebSocketCredential { get } +} + + +extension AuthWebSocket { + + // TODO: work later +// public var session: URLSession { +// } + + public var domain: String { + credential.isMock ? + "ws://ops.koreainvestment.com:31000": + "ws://ops.koreainvestment.com:21000" + } +} + + +public protocol WebSocketCredential { + var isMock: Bool { get } + var accountNo: String { get } + var approvalKey: String { get } +} + + +public struct KissWebSocketCredential: WebSocketCredential, Codable { + public let isMock: Bool + public let accountNo: String + public let approvalKey: String + + public init(isMock: Bool, accountNo: String, approvalKey: String) { + self.isMock = isMock + self.accountNo = accountNo + self.approvalKey = approvalKey + } +} diff --git a/KissMe/Sources/Domestic/Realtime/Domestic.AskingPriceWebSocket.swift b/KissMe/Sources/Domestic/Realtime/Domestic.AskingPriceWebSocket.swift new file mode 100644 index 0000000..db827a6 --- /dev/null +++ b/KissMe/Sources/Domestic/Realtime/Domestic.AskingPriceWebSocket.swift @@ -0,0 +1,71 @@ +// +// Do.swift +// KissMe +// +// Created by ened-book-m1 on 2023/08/11. +// + +import Foundation + + +// MARK: AskingPriceWebSocket + +extension Domestic { + + /// 국내주식 실시간호가[실시간-004] + /// + public struct AskingPriceWebSocket: AuthWebSocket { + public typealias KResult = String + + public var url: String { + "/tryitout/H0STASP0" + } + public var header: [String: String?] { + [ + "approval_key": credential.approvalKey, + "custtype": "P", + "tr_type": "1", + "content-type": "utf-8" + ] + } + public var body: [String: Any] { + [ + "tr_id": "H0STCNT0", + "tr_key": productCode, + ] + } + + public var socket: URLSessionWebSocketTask? + public var result: KResult? = nil + public var credential: WebSocketCredential + var event: Event! + let productCode: String + + init(credential: WebSocketCredential, productCode: String) { + self.credential = credential + self.productCode = productCode + self.event = Event(socket: self) + } + } +} + + +extension Domestic.AskingPriceWebSocket { + + class Event: NSObject { + let socket: Domestic.AskingPriceWebSocket + + init(socket: Domestic.AskingPriceWebSocket) { + self.socket = socket + } + } +} + + +extension Domestic.AskingPriceWebSocket.Event: URLSessionWebSocketDelegate { + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + } +} diff --git a/KissMe/Sources/Domestic/Realtime/Domestic.ContractNoticeWebSocket.swift b/KissMe/Sources/Domestic/Realtime/Domestic.ContractNoticeWebSocket.swift new file mode 100644 index 0000000..7af8c1f --- /dev/null +++ b/KissMe/Sources/Domestic/Realtime/Domestic.ContractNoticeWebSocket.swift @@ -0,0 +1,71 @@ +// +// DomesticStockRealtime.swift +// KissMe +// +// Created by ened-book-m1 on 2023/08/11. +// + +import Foundation + + +// MARK: ContractNoticeWebSocket + +extension Domestic { + + /// 국내주식 실시간체결통보[실시간-005] + /// + public struct ContractNoticeWebSocket: AuthWebSocket { + public typealias KResult = String + + public var url: String { + "/tryitout/H0STCNI0" + } + 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": (credential.isMock ? "H0STCNI9": "H0STCNI0"), + "tr_key": htsID, + ] + } + + public var socket: URLSessionWebSocketTask? + public var result: KResult? = nil + public var credential: WebSocketCredential + var event: Event! + let htsID: String + + init(credential: WebSocketCredential, htsID: String) { + self.credential = credential + self.htsID = htsID + self.event = Event(socket: self) + } + } +} + + +extension Domestic.ContractNoticeWebSocket { + + class Event: NSObject { + let socket: Domestic.ContractNoticeWebSocket + + init(socket: Domestic.ContractNoticeWebSocket) { + self.socket = socket + } + } +} + + +extension Domestic.ContractNoticeWebSocket.Event: URLSessionWebSocketDelegate { + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + } +} diff --git a/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift b/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift new file mode 100644 index 0000000..443e2a5 --- /dev/null +++ b/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift @@ -0,0 +1,106 @@ +// +// Domestic.swift +// KissMe +// +// Created by ened-book-m1 on 2023/08/11. +// + +import Foundation + + +enum KissWebSocketSubscription: String { + case subscribed = "1" + case unsubscribed = "2" +} + + +protocol KissWebSocketMessage { + +} + + +// MARK: ContractPriceWebSocket + +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 + + public init(credential: WebSocketCredential, productCode: String) { + self.credential = credential + self.productCode = productCode + self.event = Event(socket: self) + } + + + + struct Message: KissWebSocketMessage { + + let subscription: KissWebSocketSubscription + + init(subscription: KissWebSocketSubscription) { + self.subscription = subscription + } + } + + + func subscribe() { + // json 포맷으로 데이터를 송수신 + + } + + func unsubscribe() { + // json 포맷으로 데이터를 송수신 + + } + } +} + + +extension Domestic.ContractPriceWebSocket { + + class Event: NSObject { + let socket: Domestic.ContractPriceWebSocket + + init(socket: Domestic.ContractPriceWebSocket) { + self.socket = socket + } + } +} + + +extension Domestic.ContractPriceWebSocket.Event: URLSessionWebSocketDelegate { + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + print("connected...") + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + print("disconnected...") + } +} diff --git a/KissMe/Sources/Domestic/Realtime/DomesticStockRealtime.swift b/KissMe/Sources/Domestic/Realtime/DomesticStockRealtime.swift deleted file mode 100644 index 5b3b932..0000000 --- a/KissMe/Sources/Domestic/Realtime/DomesticStockRealtime.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// DomesticStockRealtime.swift -// KissMe -// -// Created by ened-book-m1 on 2023/08/11. -// - -import Foundation - - -extension Domestic { - -} diff --git a/KissMe/Sources/KissAccount.swift b/KissMe/Sources/KissAccount.swift index ccb026d..780ddbf 100644 --- a/KissMe/Sources/KissAccount.swift +++ b/KissMe/Sources/KissAccount.swift @@ -12,6 +12,10 @@ public class KissAccount: KissProfile { let credential: Credential + public var accountNo: String { + credential.accountNo + } + public init(credential: Credential) { self.credential = credential super.init() diff --git a/KissMe/Sources/Login/Login.swift b/KissMe/Sources/Login/Login.swift index e17c31a..8d67d86 100644 --- a/KissMe/Sources/Login/Login.swift +++ b/KissMe/Sources/Login/Login.swift @@ -102,7 +102,7 @@ public struct ApprovalKeyAuthRequest: AuthRequest { [ "grant_type": "client_credentials", "appkey": credential.appKey, - "appsecret": credential.appSecret + "secretkey": credential.appSecret ] } public var result: KResult? = nil diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index 3f6067d..3f71fdf 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -28,6 +28,9 @@ class KissConsole: KissMe.ShopContext { var indexContext: IndexContext let maxCandleDay: Int = 250 + + // 005930: 삼성전자 + static let defaultProductNo: String = "005930" private enum KissCommand: String { @@ -159,8 +162,7 @@ class KissConsole: KissMe.ShopContext { } semaphore.wait() - // 005930: 삼성전자 - setCurrent(productNo: "005930") + setCurrent(productNo: KissConsole.defaultProductNo) } private func getCommand(_ line: String) -> (KissCommand?, [String]) { diff --git a/KissMeConsole/Sources/main.swift b/KissMeConsole/Sources/main.swift index 59b95cf..00e27fd 100644 --- a/KissMeConsole/Sources/main.swift +++ b/KissMeConsole/Sources/main.swift @@ -7,4 +7,63 @@ import Foundation -KissConsole().run() +//KissConsole().run() + +import KissMe + +test_get_websocket_key_and_request_simple() + + +func test_get_websocket_key_and_request_simple() { + let isMock = false + + let semaphore = DispatchSemaphore(value: 0) + Task { + guard let (account, approvalKey) = await test_get_websocket_key(isMock: isMock) else { + return + } + + let webSocketCredential = KissWebSocketCredential(isMock: isMock, accountNo: account.accountNo, approvalKey: approvalKey) + + var socket = Domestic.ContractPriceWebSocket(credential: webSocketCredential, productCode: KissConsole.defaultProductNo) + + do { + try await socket.connect() + try await Task.sleep(nanoseconds: 1_000_000_000 * 3) + } catch { + print(error) + } + + semaphore.signal() + } + semaphore.wait() + + + // 간단한 리퀘스트를 날려보자. + // 응답을 체크해서 정리해보자. +} + + +func test_get_websocket_key(isMock: Bool) async -> (KissAccount, String)? { + let credential: Credential + + do { + credential = try KissCredential(isMock: isMock) + } catch { + print(error) + return nil + } + + let account = KissAccount(credential: credential) + do { + if try await account.login() { + let approvalKey = try await account.approvalKey() + print("approvalKey : \(approvalKey)") + return (account, approvalKey) + } + } catch { + print(error) + return nil + } + return nil +} diff --git a/projects/macos/KissMe.xcodeproj/project.pbxproj b/projects/macos/KissMe.xcodeproj/project.pbxproj index 470fab1..caff0d7 100644 --- a/projects/macos/KissMe.xcodeproj/project.pbxproj +++ b/projects/macos/KissMe.xcodeproj/project.pbxproj @@ -40,7 +40,10 @@ 3435A7F72A35D82000D604F1 /* DomesticShortSelling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3435A7F62A35D82000D604F1 /* DomesticShortSelling.swift */; }; 349C26AB2A1EAE2400F3EC91 /* KissProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C26AA2A1EAE2400F3EC91 /* KissProfile.swift */; }; 349F5D142A6BC8D3009A0526 /* String+Html.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349F5D132A6BC8D3009A0526 /* String+Html.swift */; }; - 34BC44762A8656570052D8EB /* DomesticStockRealtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BC44752A8656570052D8EB /* DomesticStockRealtime.swift */; }; + 34BC44762A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BC44752A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift */; }; + 34BC44792A8657D50052D8EB /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BC44782A8657D50052D8EB /* WebSocket.swift */; }; + 34BC447B2A8663430052D8EB /* Domestic.AskingPriceWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BC447A2A8663430052D8EB /* Domestic.AskingPriceWebSocket.swift */; }; + 34BC447D2A86635A0052D8EB /* Domestic.ContractPriceWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BC447C2A86635A0052D8EB /* Domestic.ContractPriceWebSocket.swift */; }; 34C1BA4D2A59CD3400423D64 /* DomesticDartNotice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C1BA4C2A59CD3400423D64 /* DomesticDartNotice.swift */; }; 34C1BA4F2A5A603F00423D64 /* DomesticExtra.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C1BA4E2A5A603F00423D64 /* DomesticExtra.swift */; }; 34C1BA512A5A607D00423D64 /* DomesticDart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C1BA502A5A607D00423D64 /* DomesticDart.swift */; }; @@ -179,7 +182,10 @@ 3435A7F62A35D82000D604F1 /* DomesticShortSelling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticShortSelling.swift; sourceTree = ""; }; 349C26AA2A1EAE2400F3EC91 /* KissProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KissProfile.swift; sourceTree = ""; }; 349F5D132A6BC8D3009A0526 /* String+Html.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Html.swift"; sourceTree = ""; }; - 34BC44752A8656570052D8EB /* DomesticStockRealtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticStockRealtime.swift; sourceTree = ""; }; + 34BC44752A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Domestic.ContractNoticeWebSocket.swift; sourceTree = ""; }; + 34BC44782A8657D50052D8EB /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = ""; }; + 34BC447A2A8663430052D8EB /* Domestic.AskingPriceWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Domestic.AskingPriceWebSocket.swift; sourceTree = ""; }; + 34BC447C2A86635A0052D8EB /* Domestic.ContractPriceWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Domestic.ContractPriceWebSocket.swift; sourceTree = ""; }; 34C1BA4C2A59CD3400423D64 /* DomesticDartNotice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticDartNotice.swift; sourceTree = ""; }; 34C1BA4E2A5A603F00423D64 /* DomesticExtra.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticExtra.swift; sourceTree = ""; }; 34C1BA502A5A607D00423D64 /* DomesticDart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticDart.swift; sourceTree = ""; }; @@ -386,6 +392,7 @@ 341F5F062A14634F00962D48 /* Foundation+Extensions.swift */, 34D3680E2A2AA0BE005E6756 /* PropertyIterable.swift */, 349F5D132A6BC8D3009A0526 /* String+Html.swift */, + 34BC44782A8657D50052D8EB /* WebSocket.swift */, ); path = Common; sourceTree = ""; @@ -401,7 +408,9 @@ 34BC44742A8656250052D8EB /* Realtime */ = { isa = PBXGroup; children = ( - 34BC44752A8656570052D8EB /* DomesticStockRealtime.swift */, + 34BC447C2A86635A0052D8EB /* Domestic.ContractPriceWebSocket.swift */, + 34BC447A2A8663430052D8EB /* Domestic.AskingPriceWebSocket.swift */, + 34BC44752A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift */, ); path = Realtime; sourceTree = ""; @@ -789,6 +798,7 @@ buildActionMask = 2147483647; files = ( 341F5EFB2A10909D00962D48 /* LoginResult.swift in Sources */, + 34BC447D2A86635A0052D8EB /* Domestic.ContractPriceWebSocket.swift in Sources */, 34C1BA882A5D9A4A00423D64 /* DomesticDartDisclosureInterests.swift in Sources */, 340A4DC42A4E4345005A1FBA /* ArrayDecodable.swift in Sources */, 34C1BA532A5A683D00423D64 /* DomesticDartBusinessReport.swift in Sources */, @@ -801,6 +811,7 @@ 34C1BA552A5B033E00423D64 /* DomesticDartListedCompany.swift in Sources */, 341F5F072A14634F00962D48 /* Foundation+Extensions.swift in Sources */, 341F5F032A11A2BC00962D48 /* Credential.swift in Sources */, + 34BC44792A8657D50052D8EB /* WebSocket.swift in Sources */, 341F5EB02A0A80EC00962D48 /* KissMe.docc in Sources */, 341F5EFD2A10931B00962D48 /* DomesticStockSearch.swift in Sources */, 349F5D142A6BC8D3009A0526 /* String+Html.swift in Sources */, @@ -830,9 +841,10 @@ 341F5EE12A0F373B00962D48 /* Login.swift in Sources */, 341F5EF52A0F891200962D48 /* KissAccount.swift in Sources */, 340A4DBD2A4C34BE005A1FBA /* IndexContext.swift in Sources */, - 34BC44762A8656570052D8EB /* DomesticStockRealtime.swift in Sources */, + 34BC44762A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift in Sources */, 34E7B9112A49BD2800B3AB9F /* DomesticIndex.swift in Sources */, 341F5F0D2A15222E00962D48 /* AuthRequest.swift in Sources */, + 34BC447B2A8663430052D8EB /* Domestic.AskingPriceWebSocket.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };