From 4890896c0a5c74a03411ec2ce4a124f5a016d402 Mon Sep 17 00:00:00 2001 From: ened Date: Sat, 26 Aug 2023 19:17:29 +0900 Subject: [PATCH] Moving code into kissme framework --- .../Domestic.ContractPriceWebSocket.swift | 101 --- .../Realtime/Domestic.WebSocketData.swift | 751 ++++++++++++++++++ KissMeConsole/Sources/main.swift | 634 +-------------- .../macos/KissMe.xcodeproj/project.pbxproj | 4 + 4 files changed, 756 insertions(+), 734 deletions(-) create mode 100644 KissMe/Sources/Domestic/Realtime/Domestic.WebSocketData.swift diff --git a/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift b/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift index cc21c3f..13da035 100644 --- a/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift +++ b/KissMe/Sources/Domestic/Realtime/Domestic.ContractPriceWebSocket.swift @@ -8,12 +8,6 @@ import Foundation -enum KissWebSocketSubscription: String, Codable { - case subscribed = "1" - case unsubscribed = "2" -} - - // MARK: ContractPriceWebSocket extension Domestic { @@ -66,98 +60,3 @@ extension Domestic.ContractPriceWebSocket.Event: URLSessionWebSocketDelegate { print("disconnected...") } } - - -extension Domestic { - - struct TypeResponse: Codable { - let header: Header - - struct Header: Codable { - let trId: String - /// yyyyMMddHHmmss - let dateTime: String? - - private enum CodingKeys: String, CodingKey { - case trId = "tr_id" - case dateTime = "datetime" - } - } - } - - 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, trId: String, trKey: String) { - - header = Header(approvalKey: approvalKey, customerType: .personal, trType: type, contentType: "utf-8") - body = Body(input: Input(trId: trId, trKey: trKey)) - } - } - - - 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: Output? - - private enum CodingKeys: String, CodingKey { - case resultCode = "rt_cd" - case messageCode = "msg_cd" - case message = "msg1" - case output - } - } - - struct Output: Codable { - let iv: String - let key: String - } - } -} diff --git a/KissMe/Sources/Domestic/Realtime/Domestic.WebSocketData.swift b/KissMe/Sources/Domestic/Realtime/Domestic.WebSocketData.swift new file mode 100644 index 0000000..629da00 --- /dev/null +++ b/KissMe/Sources/Domestic/Realtime/Domestic.WebSocketData.swift @@ -0,0 +1,751 @@ +// +// Domestic.WebSocketResponse.swift +// KissMe +// +// Created by ened-book-m1 on 2023/08/26. +// + +import Foundation + + +extension Domestic { + + enum KissWebSocketSubscription: String, Codable { + case subscribed = "1" + case unsubscribed = "2" + } + + + 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, trId: String, trKey: String) { + + header = Header(approvalKey: approvalKey, customerType: .personal, trType: type, contentType: "utf-8") + body = Body(input: Input(trId: trId, trKey: trKey)) + } + } + + + struct GeneralResult: Codable { + let header: Header + + struct Header: Codable { + let trId: String + /// yyyyMMddHHmmss + let dateTime: String? + + private enum CodingKeys: String, CodingKey { + case trId = "tr_id" + case dateTime = "datetime" + } + } + } + + + 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: Output? + + private enum CodingKeys: String, CodingKey { + case resultCode = "rt_cd" + case messageCode = "msg_cd" + case message = "msg1" + case output + } + } + + struct Output: Codable { + let iv: String + let key: String + } + } +} + + +extension Domestic { + + public enum VariableRatioSignType: UInt8 { + /// 상한 + case upLimit = 1 + /// 상승 + case up = 2 + /// 보합 + case noChange = 3 + /// 하한 + case downLimit = 4 + /// 하락 + case down = 5 + } + + + public enum ConclusionType: UInt8 { + /// 매수(+) + case buying = 1 + /// 장전 + case load = 3 + /// 매도(-) + case selling = 5 + } + + + public enum HourClassCode: Character { + /// 장중 + case onMarket = "0" + /// 장후예상 + case afterMarket = "A" + /// 장전예상 + case beforeMarket = "B" + /// 9시이후의 예상가, VI발동 + case viInvoking = "C" + /// 시간외 단일가 예상 + case overTime_SinglePrice = "D" + } + + + public typealias MarketOperationCode = String + + + public enum ContractClass: String { + /// 매도 + case selling = "01" + /// 매수 + case buying = "02" + } + + + public enum OrderClass: String { + /// 지정가 + case limits = "00" + /// 시장가 + case marketPrice = "01" + /// 조건부지정가 + case conditionalLimits = "02" + /// 최유리지정가 + case mostFavorLimits = "03" + /// 최우선지정가 + case mostPriorLimits = "04" + /// 장전 시간외 + case overTime_BeforeMarket = "05" + /// 장후 시간외 + case overTime_AfterMarket = "06" + /// 시간외 단일가 + case overTime_SinglePrice = "07" + /// 자기주식 + case treasury = "08" + /// 자기주식S-Option + case treasury_SOption = "09" + /// 자기주식금전신탁 + case treasury_MoneyTrust = "10" + /// IOC지정가 (즉시체결,잔량취소) + case iocLimits_Conclusion_CancelResidualQuantity = "11" + /// FOK지정가 (즉시체결,전량취소) + case fokLimits_Conclusion_CancelEntireQuantity = "12" + /// IOC시장가 (즉시체결,잔량취소) + case iocMarketPrice_Conclusion_CancelResidualQuantity = "13" + /// FOK시장가 (즉시체결,전량취소) + case fokMarketPrice_Conclusion_CancelEntireQuantity = "14" + /// IOC최유리 (즉시체결,잔량취소) + case iocMostFavor_Conclusion_CancelResidualQuantity = "15" + /// FOK최유리 (즉시체결,전량취소) + case fokMostFavor_Conclusion_CancelEntireQuantity = "16" + } + + + public enum RefuseYesNo: Int { + /// 승인 + case approval = 0 + /// 거부 + case refuse = 1 + } + + + public enum ConclusionYesNo: Int { + /// 주문,정정,취소,거부, + case unconclused = 1 + /// 체결 (★ 체결만 보실경우 2번만 보시면 됩니다) + case conclused = 2 + } + + + public enum AcceptYesNo: Int { + /// 주문접수 + case orderAccept = 1 + /// 확인 + case confirm = 2 + /// 취소(FOK/IOC) + case cancel = 3 + } + + + public struct ContractPrice { + /// 유가증권 단축 종목코드 + public let shortCode: String + /// 주식 체결 시간 (HHmmss) + public let conclusionTime: String + /// 주식 현재가 + public let currentPrice: Int + /// 전일 대비 부호 + public let previousDayVariableRatioSign: VariableRatioSignType + /// 전일 대비 + public let previousDayVariableRatio: Double + /// 전일 대비율 + public let previousDayDiffRatio: Double + /// 가중 평균 주식 가격 + public let weightedAveragePrice: Int + /// 주식 시가 + public let openningPrice: Int + /// 주식 최고가 + public let highestPrice: Int + /// 주식 최저가 + public let lowestPrice: Int + /// 매도호가1 + public let askingPrice: Int + /// 매수호가1 + public let biddingPrice: Int + /// 체결 거래량 + public let conclusionVolume: Int + /// 누적 거래량 + public let accumulatedVolume: Int + /// 누적 거래 대금 + public let accumulatedTradingAmount: Int + /// 매도 체결 건수 + public let sellingConclusionCount: Int + /// 매수 체결 건수 + public let buyingConclusionCount: Int + /// 순매수 체결 건수 + public let netBuyingConclusionCount: Int + /// 체결강도 + public let conclusionStrength: Double + /// 총 매도 수량 + public let totalSellingQuantity: Int + /// 총 매수 수량 + public let totalBuyingQuantity: Int + /// 체결구분 + public let conclusionType: ConclusionType + /// 매수비율 + public let buyingRatio: Double + /// 전일 거래량 대비 등락율 + public let previousDay_VolumeDiff_FluctuationRate: Double + /// 시가 시간 + public let openningPriceHour: String + /// 시가대비구분 + public let openningPrice_VariableRatioSign: VariableRatioSignType + /// 시가대비 + public let openningPrice_VariableRatio: Int + /// 최고가 시간 + public let highestPriceHour: String + /// 고가대비구분 + public let highPrice_VariableRatioSign: VariableRatioSignType + /// 고가대비 + public let highPrice_VariableRatio: Int + /// 최저가 시간 + public let lowestPriceHour: String + /// 저가대비구분 + public let lowPrice_VariableRatioSign: VariableRatioSignType + /// 저가대비 + public let lowPrice_VariableRatio: Int + /// 영업 일자 + public let businessDate: String + /// 신 장운영 구분 코드 + public let marketOperationCode: MarketOperationCode + /// 거래정지 여부 + public let tradeStopped: Bool + /// 매도호가 잔량1 + public let askingPrice_ResidualQuantity: Int + /// 매수호가 잔량1 + public let biddingPrice_ResidualQuantity: Int + /// 총 매도호가 잔량 + public let askingPrice_TotalResidualQuantity: Int + /// 총 매수호가 잔량 + public let biddingPrice_TotalResidualQuantity: Int + /// 거래량 회전율 + public let volumeTurnoverRate: Double + /// 전일 동시간 누적 거래량 + public let previousDaySameTime_AccumulatedTradingQuantity: Int + /// 전일 동시간 누적 거래량 비율 + public let previousDaySameTime_AccumulatedTradingQuantityRatio: Double + /// 시간 구분 코드 + public let hourClassCode: HourClassCode + /// 임의종료구분코드 + public let marketTerminationCode: String + /// 정적VI발동기준가 + public let viStandardPrice: Int + + + static let propertiesCount = 46 + + public init(array: [Substring], source: Substring) throws { + guard array.count == ContractPrice.propertiesCount else { + throw GeneralError.incorrectArrayItems(String(source), array.count, ContractPrice.propertiesCount) + } + + self.shortCode = String(array[0]) + self.conclusionTime = String(array[1]) + self.currentPrice = Int(array[2]) ?? 0 + self.previousDayVariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[3])!)! + self.previousDayVariableRatio = Double(array[4]) ?? 0 + self.previousDayDiffRatio = Double(array[5]) ?? 0 + self.weightedAveragePrice = Int(array[6]) ?? 0 + self.openningPrice = Int(array[7]) ?? 0 + self.highestPrice = Int(array[8]) ?? 0 + self.lowestPrice = Int(array[9]) ?? 0 + self.askingPrice = Int(array[10]) ?? 0 + self.biddingPrice = Int(array[11]) ?? 0 + self.conclusionVolume = Int(array[12]) ?? 0 + self.accumulatedVolume = Int(array[13]) ?? 0 + self.accumulatedTradingAmount = Int(array[14]) ?? 0 + self.sellingConclusionCount = Int(array[15]) ?? 0 + self.buyingConclusionCount = Int(array[16]) ?? 0 + self.netBuyingConclusionCount = Int(array[17]) ?? 0 + self.conclusionStrength = Double(array[18]) ?? 0 + self.totalSellingQuantity = Int(array[19]) ?? 0 + self.totalBuyingQuantity = Int(array[20]) ?? 0 + self.conclusionType = ConclusionType(rawValue: UInt8(array[21])!)! + self.buyingRatio = Double(array[22]) ?? 0 + self.previousDay_VolumeDiff_FluctuationRate = Double(array[23]) ?? 0 + self.openningPriceHour = String(array[24]) + self.openningPrice_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[25])!)! + self.openningPrice_VariableRatio = Int(array[26]) ?? 0 + self.highestPriceHour = String(array[27]) + self.highPrice_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[28])!)! + self.highPrice_VariableRatio = Int(array[29]) ?? 0 + self.lowestPriceHour = String(array[30]) + self.lowPrice_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[31])!)! + self.lowPrice_VariableRatio = Int(array[32]) ?? 0 + self.businessDate = String(array[33]) + self.marketOperationCode = String(array[34]) + self.tradeStopped = String(array[35]) == "Y" + self.askingPrice_ResidualQuantity = Int(array[36]) ?? 0 + self.biddingPrice_ResidualQuantity = Int(array[37]) ?? 0 + self.askingPrice_TotalResidualQuantity = Int(array[38]) ?? 0 + self.biddingPrice_TotalResidualQuantity = Int(array[39]) ?? 0 + self.volumeTurnoverRate = Double(array[40]) ?? 0 + self.previousDaySameTime_AccumulatedTradingQuantity = Int(array[41]) ?? 0 + self.previousDaySameTime_AccumulatedTradingQuantityRatio = Double(array[42]) ?? 0 + self.hourClassCode = HourClassCode(rawValue: String(array[43]).first!)! + self.marketTerminationCode = String(array[44]) + self.viStandardPrice = Int(array[45]) ?? 0 + } + } + + + public struct AskingPrice { + /// 유가증권 단축 종목코드 + public let shortCode: String + /// 영업 시간 + public let businessTime: String + /// 시간 구분 코드 + public let hourClassCode: HourClassCode + /// 매도호가1 ~ 매도호가10 + public let askingPrices: [Int] + /// 매수호가1 ~ 매수호가10 + public let biddingPrices: [Int] + /// 매도호가 잔량1 ~ 매도호가 잔량4 + public let askingPriceVolumes: [Int] + /// 매수호가 잔량1 ~ 매수호가 잔량10 + public let biddingPriceVolumes: [Int] + /// 총 매도호가 잔량 + public let askingPrice_TotalResidualQuantity: Int + /// 총 매수호가 잔량 + public let biddingPrice_TotalResidualQuantity: Int + /// 시간외 총 매도호가 잔량 + public let askingPrice_Overtime_TotalResidualQuantity: Int + /// 시간외 총 매수호가 잔량 + public let biddingPrice_Overtime_TotalResidualQuantity: Int + /// 예상 체결가 + public let expectedConclusionPrice: Int + /// 예상 체결량 + public let expectedConclusionQuantity: Int + /// 예상 거래량 + public let expectedVolume: Int + /// 예상 체결 대비 + public let expectedConclusion_VariableRatio: Double + /// 예상 체결 대비 부호 + public let expectedConclusion_VariableRatioSign: VariableRatioSignType + /// 예상 체결 전일 대비율 + public let expectedConclusion_PreviousDayDiffRatio: Double + /// 누적 거래량 + public let accumulatedVolume: Int + /// 총 매도호가 잔량 증감 + public let askingPrice_TotalResidualQuantity_IncreaseDecrease: Int + /// 총 매수호가 잔량 증감 + public let biddingPrice_TotalResidualQuantity_IncreaseDecrease: Int + /// 시간외 총 매도호가 증감 + public let askingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease: Int + /// 시간외 총 매수호가 증감 + public let biddingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease: Int + /// 주식 매매 구분 코드 + public let tradeCode: String + + + static let propertiesCount = 59 + + public init(array: [Substring], source: Substring) throws { + guard array.count == AskingPrice.propertiesCount else { + throw GeneralError.incorrectArrayItems(String(source), array.count, AskingPrice.propertiesCount) + } + + func getIntArray(_ fromIndex: Int, _ toIndex: Int) -> [Int] { + var array = [Int]() + for i in fromIndex ..< toIndex { + let value = Int(array[i]) + array.append(value) + } + return array + } + + self.shortCode = String(array[0]) + self.businessTime = String(array[1]) + self.hourClassCode = HourClassCode(rawValue: String(array[2]).first!)! + self.askingPrices = getIntArray(3, 3+10) + self.biddingPrices = getIntArray(13, 13+10) + self.askingPriceVolumes = getIntArray(23, 23+10) + self.biddingPriceVolumes = getIntArray(33, 33+10) + self.askingPrice_TotalResidualQuantity = Int(array[43]) ?? 0 + self.biddingPrice_TotalResidualQuantity = Int(array[44]) ?? 0 + self.askingPrice_Overtime_TotalResidualQuantity = Int(array[45]) ?? 0 + self.biddingPrice_Overtime_TotalResidualQuantity = Int(array[46]) ?? 0 + self.expectedConclusionPrice = Int(array[47]) ?? 0 + self.expectedConclusionQuantity = Int(array[48]) ?? 0 + self.expectedVolume = Int(array[49]) ?? 0 + self.expectedConclusion_VariableRatio = Double(array[50]) ?? 0 + self.expectedConclusion_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[51])!)! + self.expectedConclusion_PreviousDayDiffRatio = Double(array[52]) ?? 0 + self.accumulatedVolume = Int(array[53]) ?? 0 + self.askingPrice_TotalResidualQuantity_IncreaseDecrease = Int(array[54]) ?? 0 + self.biddingPrice_TotalResidualQuantity_IncreaseDecrease = Int(array[55]) ?? 0 + self.askingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease = Int(array[56]) ?? 0 + self.biddingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease = Int(array[57]) ?? 0 + self.tradeCode = String(array[58]) + } + } + + + public struct ContractNotice { + /// 고객 ID + public let customerID: String + /// 계좌번호 + public let accountNo: String + /// 주문번호 + public let orderNo: String + /// 원주문번호 + public let originalOrderNo: String + /// 매도매수구분 + public let contractClass: ContractClass + /// 정정구분 + public let revisionType: String + /// 주문종류 + public let orderClass: OrderClass + /// 주문조건 + public let orderCondition: String + /// 주식 단축 종목코드 + public let shortCode: String + /// 체결 수량 + public let conclusionQuantity: Int + /// 체결단가 + public let conclusionPrice: Int + /// 주식 체결 시간 + public let conclusionTime: String + /// 거부여부 + public let refused: RefuseYesNo + /// 체결여부 + public let conclused: ConclusionYesNo + /// 접수여부 + public let accepted: AcceptYesNo + /// 지점번호 + public let branchNo: String + /// 주문수량 + public let orderQuantity: Int + /// 계좌명 + public let accountName: String + /// 체결종목명 + public let productName: String + /// 신용구분 + public let creditClass: String + /// 신용대출일자 + public let creditLoanDate: String + /// 체결종목명40 + public let productName40: String + /// 주문가격 + public let orderPrice: Int + + + static let propertiesCount = 23 + + public init(array: [Substring], source: Substring) throws { + guard array.count == ContractNotice.propertiesCount else { + throw GeneralError.incorrectArrayItems(String(source), array.count, ContractNotice.propertiesCount) + } + + self.customerID = String(array[0]) + self.accountNo = String(array[1]) + self.orderNo = String(array[2]) + self.originalOrderNo = String(array[3]) + self.contractClass = ContractClass(rawValue: String(array[4]))! + self.revisionType = String(array[5]) + self.orderClass = OrderClass(rawValue: String(array[6]))! + self.orderCondition = String(array[7]) + self.shortCode = String(array[8]) + self.conclusionQuantity = Int(array[9]) ?? 0 + self.conclusionPrice = Int(array[10]) ?? 0 + self.conclusionTime = String(array[11]) + self.refused = RefuseYesNo(rawValue: Int(array[12])!)! + self.conclused = ConclusionYesNo(rawValue: Int(array[13])!)! + self.accepted = AcceptYesNo(rawValue: Int(array[14])!)! + self.branchNo = String(array[15]) + self.orderQuantity = Int(array[16]) ?? 0 + self.accountName = String(array[17]) + self.productName = String(array[18]) + self.creditClass = String(array[19]) + self.creditLoanDate = String(array[20]) + self.productName40 = String(array[21]) + self.orderPrice = Int(array[22]) ?? 0 + } + } + + + public enum WebSocketResult { + case json(String) + case contractPrice(ContractPrice) + case askingPrice(AskingPrice) + case contractNotice(ContractNotice) + } +} + + +extension Domestic.MarketOperationCode { + // MARK: 첫번째 비트 + + /// 장개시전 + public var isBeforeMarket: Bool { first == "1" } + /// 장중 + public var isOnMarket: Bool { first == "2" } + /// 장종료후 + public var isAfterMarket: Bool { first == "3" } + /// 시간외단일가 + public var isOverTime_SinglePrice: Bool { first == "4" } + /// 일반Buy-in + public var isGeneralBuyIn: Bool { first == "5" } + /// 당일Buy-in + public var isTheDayBuyIn: Bool { first == "6" } + + // MARK: 두번째 비트 + + /// 보통 + public var isGeneral: Bool { second == "0" } + /// 종가 + public var isClosingPrice: Bool { second == "1" } + /// 대량 + public var isBulk: Bool { second == "2" } + /// 바스켓 + public var isBasket: Bool { second == "3" } + /// 정리매매 + public var isClearanceSale: Bool { second == "7" } + /// Buy-in + public var isBuyIn: Bool { second == "8" } + + private var first: Character { self[startIndex] } + private var second: Character { self[index(startIndex, offsetBy: 1)] } +} + + +extension Domestic.WebSocketResult { + + static public func parse(_ str: String) throws -> [Domestic.WebSocketResult] { + var dataArray = [Domestic.WebSocketResult]() + var nextAt = str.startIndex + let charset = CharacterSet(charactersIn: "{}") + + while nextAt < str.endIndex { + print("nextAt... \(nextAt.utf16Offset(in: str))") + + switch str[nextAt] { + case "{": + /// Scan end of json data + var openedCount = 1 + let startAt = nextAt + nextAt = str.index(nextAt, offsetBy: 1) + + while openedCount > 0 { + guard let r = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { + break + } + + switch str[r.lowerBound] { + case "{": + openedCount += 1 + case "}": + openedCount -= 1 + if openedCount == 0 { + /// end of json data + let jsonString = String(str[startAt ..< r.upperBound]) + dataArray.append(.json(jsonString)) + nextAt = r.upperBound + break + } + default: + throw GeneralError.impossibleJsonCharacter + } + nextAt = r.upperBound + } + + case "0": + let (data, endAt) = try parseTrData(false, str: str, startAt: nextAt) + dataArray.append(contentsOf: data) + nextAt = endAt + + case "1": + let (data, endAt) = try parseTrData(true, str: str, startAt: nextAt) + dataArray.append(contentsOf: data) + nextAt = endAt + + default: + print(str[nextAt ..< str.endIndex]) + throw GeneralError.invalidWebSocketData + } + } + + return dataArray + } + + + private static func parseTrData(_ encrypted: Bool, str: String, startAt: String.Index) throws -> ([Domestic.WebSocketResult], String.Index) { + var dataArray = [Domestic.WebSocketResult]() + var nextAt = startAt + let charset = CharacterSet(charactersIn: "|") + + guard let i1 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { + throw GeneralError.notEnoughWebSocketData + } + /// Get encryption field + let encryption = String(str[nextAt ..< i1.lowerBound]) + guard encryption == "0" || encryption == "1" else { + throw GeneralError.invalidWebSocketData_EncryptionField + } + nextAt = i1.upperBound + guard let i2 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { + throw GeneralError.notEnoughWebSocketData + } + /// Get trId field + let trId = String(str[nextAt ..< i2.lowerBound]) + nextAt = i2.upperBound + guard let i3 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { + throw GeneralError.notEnoughWebSocketData + } + /// Get data count field + let dataCountString = String(str[nextAt ..< i3.lowerBound]) + guard let dataCount = Int(dataCountString) else { + throw GeneralError.notEnoughWebSocketData + } + nextAt = i3.upperBound + + /// Retrieve multiple data + switch trId { + case "H0STCNT0": + for _ in 0 ..< dataCount { + let (stringArray, string, endAt) = try getTrDataString(str, startAt: nextAt, seperatorCount: Domestic.ContractPrice.propertiesCount-1) + let price = try Domestic.ContractPrice(array: stringArray, source: string) + dataArray.append(.contractPrice(price)) + nextAt = endAt + } + + case "H0STASP0": + for _ in 0 ..< dataCount { + let (stringArray, string, endAt) = try getTrDataString(str, startAt: nextAt, seperatorCount: Domestic.AskingPrice.propertiesCount-1) + let price = try Domestic.ContractPrice(array: stringArray, source: string) + dataArray.append(.contractPrice(price)) + nextAt = endAt + } + + case "H0STCNI0": + for _ in 0 ..< dataCount { + let (stringArray, string, endAt) = try getTrDataString(str, startAt: startAt, seperatorCount: Domestic.ContractNotice.propertiesCount-1) + let notice = try Domestic.ContractNotice(array: stringArray, source: string) + dataArray.append(.contractNotice(notice)) + nextAt = endAt + } + + default: + throw GeneralError.invalidWebSocketData_TrIdField + } + + return (dataArray, nextAt) + } + + + private static func getTrDataString(_ str: String, startAt: String.Index, seperatorCount: Int) throws -> ([Substring], Substring, String.Index) { + var currentSeperator = 0 + var nextAt = startAt + var array = [Substring]() + + /// Find last seperator + while currentSeperator < seperatorCount { + guard let last = str.range(of: "^", options: [], range: nextAt ..< str.endIndex) else { + throw GeneralError.notEnoughWebSocketData + } + currentSeperator += 1 + array.append(str[nextAt ..< last.lowerBound]) + nextAt = last.upperBound + } + + /// Look forward to find final field + if let end = str.rangeOfCharacter(from: .alphanumerics.inverted, options: [], range: nextAt ..< str.endIndex) { + array.append(str[nextAt ..< end.lowerBound]) + let string = str[startAt ..< end.lowerBound] + return (array, string, end.lowerBound) + } + else { + array.append(str[nextAt ..< str.endIndex]) + let string = str[startAt ..< str.endIndex] + return (array, string, str.endIndex) + } + } +} diff --git a/KissMeConsole/Sources/main.swift b/KissMeConsole/Sources/main.swift index 73c4c1f..c7d2356 100644 --- a/KissMeConsole/Sources/main.swift +++ b/KissMeConsole/Sources/main.swift @@ -16,486 +16,13 @@ import KissMe test_parse_contact_price_response() -enum VariableRatioSignType: UInt8 { - /// 상한 - case upLimit = 1 - /// 상승 - case up = 2 - /// 보합 - case noChange = 3 - /// 하한 - case downLimit = 4 - /// 하락 - case down = 5 -} - - -enum ConclusionType: UInt8 { - /// 매수(+) - case buying = 1 - /// 장전 - case load = 3 - /// 매도(-) - case selling = 5 -} - - -enum HourClassCode: Character { - /// 장중 - case onMarket = "0" - /// 장후예상 - case afterMarket = "A" - /// 장전예상 - case beforeMarket = "B" - /// 9시이후의 예상가, VI발동 - case viInvoking = "C" - /// 시간외 단일가 예상 - case overTime_SinglePrice = "D" -} - - -typealias MarketOperationCode = String - -extension MarketOperationCode { - // MARK: 첫번째 비트 - - /// 장개시전 - public var isBeforeMarket: Bool { first == "1" } - /// 장중 - public var isOnMarket: Bool { first == "2" } - /// 장종료후 - public var isAfterMarket: Bool { first == "3" } - /// 시간외단일가 - public var isOverTime_SinglePrice: Bool { first == "4" } - /// 일반Buy-in - public var isGeneralBuyIn: Bool { first == "5" } - /// 당일Buy-in - public var isTheDayBuyIn: Bool { first == "6" } - - // MARK: 두번째 비트 - - /// 보통 - public var isGeneral: Bool { second == "0" } - /// 종가 - public var isClosingPrice: Bool { second == "1" } - /// 대량 - public var isBulk: Bool { second == "2" } - /// 바스켓 - public var isBasket: Bool { second == "3" } - /// 정리매매 - public var isClearanceSale: Bool { second == "7" } - /// Buy-in - public var isBuyIn: Bool { second == "8" } - - var first: Character { self[startIndex] } - var second: Character { self[index(startIndex, offsetBy: 1)] } -} - - -struct ContractPrice { - /// 유가증권 단축 종목코드 - public let shortCode: String - /// 주식 체결 시간 (HHmmss) - public let conclusionTime: String - /// 주식 현재가 - public let currentPrice: Int - /// 전일 대비 부호 - public let previousDayVariableRatioSign: VariableRatioSignType - /// 전일 대비 - public let previousDayVariableRatio: Double - /// 전일 대비율 - public let previousDayDiffRatio: Double - /// 가중 평균 주식 가격 - public let weightedAveragePrice: Int - /// 주식 시가 - public let openningPrice: Int - /// 주식 최고가 - public let highestPrice: Int - /// 주식 최저가 - public let lowestPrice: Int - /// 매도호가1 - public let askingPrice: Int - /// 매수호가1 - public let biddingPrice: Int - /// 체결 거래량 - public let conclusionVolume: Int - /// 누적 거래량 - public let accumulatedVolume: Int - /// 누적 거래 대금 - public let accumulatedTradingAmount: Int - /// 매도 체결 건수 - public let sellingConclusionCount: Int - /// 매수 체결 건수 - public let buyingConclusionCount: Int - /// 순매수 체결 건수 - public let netBuyingConclusionCount: Int - /// 체결강도 - public let conclusionStrength: Double - /// 총 매도 수량 - public let totalSellingQuantity: Int - /// 총 매수 수량 - public let totalBuyingQuantity: Int - /// 체결구분 - public let conclusionType: ConclusionType - /// 매수비율 - public let buyingRatio: Double - /// 전일 거래량 대비 등락율 - public let previousDay_VolumeDiff_FluctuationRate: Double - /// 시가 시간 - public let openningPriceHour: String - /// 시가대비구분 - public let openningPrice_VariableRatioSign: VariableRatioSignType - /// 시가대비 - public let openningPrice_VariableRatio: Int - /// 최고가 시간 - public let highestPriceHour: String - /// 고가대비구분 - public let highPrice_VariableRatioSign: VariableRatioSignType - /// 고가대비 - public let highPrice_VariableRatio: Int - /// 최저가 시간 - public let lowestPriceHour: String - /// 저가대비구분 - public let lowPrice_VariableRatioSign: VariableRatioSignType - /// 저가대비 - public let lowPrice_VariableRatio: Int - /// 영업 일자 - public let businessDate: String - /// 신 장운영 구분 코드 - public let marketOperationCode: MarketOperationCode - /// 거래정지 여부 - public let tradeStopped: Bool - /// 매도호가 잔량1 - public let askingPrice_ResidualQuantity: Int - /// 매수호가 잔량1 - public let biddingPrice_ResidualQuantity: Int - /// 총 매도호가 잔량 - public let askingPrice_TotalResidualQuantity: Int - /// 총 매수호가 잔량 - public let biddingPrice_TotalResidualQuantity: Int - /// 거래량 회전율 - public let volumeTurnoverRate: Double - /// 전일 동시간 누적 거래량 - public let previousDaySameTime_AccumulatedTradingQuantity: Int - /// 전일 동시간 누적 거래량 비율 - public let previousDaySameTime_AccumulatedTradingQuantityRatio: Double - /// 시간 구분 코드 - public let hourClassCode: HourClassCode - /// 임의종료구분코드 - public let marketTerminationCode: String - /// 정적VI발동기준가 - public let viStandardPrice: Int - - - static let propertiesCount = 46 - - public init(array: [Substring], source: Substring) throws { - guard array.count == ContractPrice.propertiesCount else { - throw GeneralError.incorrectArrayItems(String(source), array.count, ContractPrice.propertiesCount) - } - - self.shortCode = String(array[0]) - self.conclusionTime = String(array[1]) - self.currentPrice = Int(array[2]) ?? 0 - self.previousDayVariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[3])!)! - self.previousDayVariableRatio = Double(array[4]) ?? 0 - self.previousDayDiffRatio = Double(array[5]) ?? 0 - self.weightedAveragePrice = Int(array[6]) ?? 0 - self.openningPrice = Int(array[7]) ?? 0 - self.highestPrice = Int(array[8]) ?? 0 - self.lowestPrice = Int(array[9]) ?? 0 - self.askingPrice = Int(array[10]) ?? 0 - self.biddingPrice = Int(array[11]) ?? 0 - self.conclusionVolume = Int(array[12]) ?? 0 - self.accumulatedVolume = Int(array[13]) ?? 0 - self.accumulatedTradingAmount = Int(array[14]) ?? 0 - self.sellingConclusionCount = Int(array[15]) ?? 0 - self.buyingConclusionCount = Int(array[16]) ?? 0 - self.netBuyingConclusionCount = Int(array[17]) ?? 0 - self.conclusionStrength = Double(array[18]) ?? 0 - self.totalSellingQuantity = Int(array[19]) ?? 0 - self.totalBuyingQuantity = Int(array[20]) ?? 0 - self.conclusionType = ConclusionType(rawValue: UInt8(array[21])!)! - self.buyingRatio = Double(array[22]) ?? 0 - self.previousDay_VolumeDiff_FluctuationRate = Double(array[23]) ?? 0 - self.openningPriceHour = String(array[24]) - self.openningPrice_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[25])!)! - self.openningPrice_VariableRatio = Int(array[26]) ?? 0 - self.highestPriceHour = String(array[27]) - self.highPrice_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[28])!)! - self.highPrice_VariableRatio = Int(array[29]) ?? 0 - self.lowestPriceHour = String(array[30]) - self.lowPrice_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[31])!)! - self.lowPrice_VariableRatio = Int(array[32]) ?? 0 - self.businessDate = String(array[33]) - self.marketOperationCode = String(array[34]) - self.tradeStopped = String(array[35]) == "Y" - self.askingPrice_ResidualQuantity = Int(array[36]) ?? 0 - self.biddingPrice_ResidualQuantity = Int(array[37]) ?? 0 - self.askingPrice_TotalResidualQuantity = Int(array[38]) ?? 0 - self.biddingPrice_TotalResidualQuantity = Int(array[39]) ?? 0 - self.volumeTurnoverRate = Double(array[40]) ?? 0 - self.previousDaySameTime_AccumulatedTradingQuantity = Int(array[41]) ?? 0 - self.previousDaySameTime_AccumulatedTradingQuantityRatio = Double(array[42]) ?? 0 - self.hourClassCode = HourClassCode(rawValue: String(array[43]).first!)! - self.marketTerminationCode = String(array[44]) - self.viStandardPrice = Int(array[45]) ?? 0 - } -} - - -struct AskingPrice { - /// 유가증권 단축 종목코드 - public let shortCode: String - /// 영업 시간 - public let businessTime: String - /// 시간 구분 코드 - public let hourClassCode: HourClassCode - /// 매도호가1 ~ 매도호가10 - public let askingPrices: [Int] - /// 매수호가1 ~ 매수호가10 - public let biddingPrices: [Int] - /// 매도호가 잔량1 ~ 매도호가 잔량4 - public let askingPriceVolumes: [Int] - /// 매수호가 잔량1 ~ 매수호가 잔량10 - public let biddingPriceVolumes: [Int] - /// 총 매도호가 잔량 - public let askingPrice_TotalResidualQuantity: Int - /// 총 매수호가 잔량 - public let biddingPrice_TotalResidualQuantity: Int - /// 시간외 총 매도호가 잔량 - public let askingPrice_Overtime_TotalResidualQuantity: Int - /// 시간외 총 매수호가 잔량 - public let biddingPrice_Overtime_TotalResidualQuantity: Int - /// 예상 체결가 - public let expectedConclusionPrice: Int - /// 예상 체결량 - public let expectedConclusionQuantity: Int - /// 예상 거래량 - public let expectedVolume: Int - /// 예상 체결 대비 - public let expectedConclusion_VariableRatio: Double - /// 예상 체결 대비 부호 - public let expectedConclusion_VariableRatioSign: VariableRatioSignType - /// 예상 체결 전일 대비율 - public let expectedConclusion_PreviousDayDiffRatio: Double - /// 누적 거래량 - public let accumulatedVolume: Int - /// 총 매도호가 잔량 증감 - public let askingPrice_TotalResidualQuantity_IncreaseDecrease: Int - /// 총 매수호가 잔량 증감 - public let biddingPrice_TotalResidualQuantity_IncreaseDecrease: Int - /// 시간외 총 매도호가 증감 - public let askingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease: Int - /// 시간외 총 매수호가 증감 - public let biddingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease: Int - /// 주식 매매 구분 코드 - public let tradeCode: String - - - static let propertiesCount = 59 - - public init(array: [Substring], source: Substring) throws { - guard array.count == AskingPrice.propertiesCount else { - throw GeneralError.incorrectArrayItems(String(source), array.count, AskingPrice.propertiesCount) - } - - func getIntArray(_ fromIndex: Int, _ toIndex: Int) -> [Int] { - var array = [Int]() - for i in fromIndex ..< toIndex { - let value = Int(array[i]) - array.append(value) - } - return array - } - - self.shortCode = String(array[0]) - self.businessTime = String(array[1]) - self.hourClassCode = HourClassCode(rawValue: String(array[2]).first!)! - self.askingPrices = getIntArray(3, 3+10) - self.biddingPrices = getIntArray(13, 13+10) - self.askingPriceVolumes = getIntArray(23, 23+10) - self.biddingPriceVolumes = getIntArray(33, 33+10) - self.askingPrice_TotalResidualQuantity = Int(array[43]) ?? 0 - self.biddingPrice_TotalResidualQuantity = Int(array[44]) ?? 0 - self.askingPrice_Overtime_TotalResidualQuantity = Int(array[45]) ?? 0 - self.biddingPrice_Overtime_TotalResidualQuantity = Int(array[46]) ?? 0 - self.expectedConclusionPrice = Int(array[47]) ?? 0 - self.expectedConclusionQuantity = Int(array[48]) ?? 0 - self.expectedVolume = Int(array[49]) ?? 0 - self.expectedConclusion_VariableRatio = Double(array[50]) ?? 0 - self.expectedConclusion_VariableRatioSign = VariableRatioSignType(rawValue: UInt8(array[51])!)! - self.expectedConclusion_PreviousDayDiffRatio = Double(array[52]) ?? 0 - self.accumulatedVolume = Int(array[53]) ?? 0 - self.askingPrice_TotalResidualQuantity_IncreaseDecrease = Int(array[54]) ?? 0 - self.biddingPrice_TotalResidualQuantity_IncreaseDecrease = Int(array[55]) ?? 0 - self.askingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease = Int(array[56]) ?? 0 - self.biddingPrice_Overtime_TotalResidualQuantity_IncreaseDecrease = Int(array[57]) ?? 0 - self.tradeCode = String(array[58]) - } -} - - -enum ContractType: String { - /// 매도 - case selling = "01" - /// 매수 - case buying = "02" -} - - -enum OrderType: String { - /// 지정가 - case limits = "00" - /// 시장가 - case marketPrice = "01" - /// 조건부지정가 - case conditionalLimits = "02" - /// 최유리지정가 - case mostFavorLimits = "03" - /// 최우선지정가 - case mostPriorLimits = "04" - /// 장전 시간외 - case overTime_BeforeMarket = "05" - /// 장후 시간외 - case overTime_AfterMarket = "06" - /// 시간외 단일가 - case overTime_SinglePrice = "07" - /// 자기주식 - case treasury = "08" - /// 자기주식S-Option - case treasury_SOption = "09" - /// 자기주식금전신탁 - case treasury_MoneyTrust = "10" - /// IOC지정가 (즉시체결,잔량취소) - case iocLimits_Conclusion_CancelResidualQuantity = "11" - /// FOK지정가 (즉시체결,전량취소) - case fokLimits_Conclusion_CancelEntireQuantity = "12" - /// IOC시장가 (즉시체결,잔량취소) - case iocMarketPrice_Conclusion_CancelResidualQuantity = "13" - /// FOK시장가 (즉시체결,전량취소) - case fokMarketPrice_Conclusion_CancelEntireQuantity = "14" - /// IOC최유리 (즉시체결,잔량취소) - case iocMostFavor_Conclusion_CancelResidualQuantity = "15" - /// FOK최유리 (즉시체결,전량취소) - case fokMostFavor_Conclusion_CancelEntireQuantity = "16" -} - - -enum RefuseYesNo: Int { - /// 승인 - case approval = 0 - /// 거부 - case refuse = 1 -} - - -enum ConclusionYesNo: Int { - /// 주문,정정,취소,거부, - case unconclused = 1 - /// 체결 (★ 체결만 보실경우 2번만 보시면 됩니다) - case conclused = 2 -} - - -enum AcceptYesNo: Int { - /// 주문접수 - case orderAccept = 1 - /// 확인 - case confirm = 2 - /// 취소(FOK/IOC) - case cancel = 3 -} - - -struct ContractNotice { - /// 고객 ID - public let customerID: String - /// 계좌번호 - public let accountNo: String - /// 주문번호 - public let orderNo: String - /// 원주문번호 - public let originalOrderNo: String - /// 매도매수구분 - public let contractType: ContractType - /// 정정구분 - public let revisionType: String - /// 주문종류 - public let orderType: OrderType - /// 주문조건 - public let orderCondition: String - /// 주식 단축 종목코드 - public let shortCode: String - /// 체결 수량 - public let conclusionQuantity: Int - /// 체결단가 - public let conclusionPrice: Int - /// 주식 체결 시간 - public let conclusionTime: String - /// 거부여부 - public let refused: RefuseYesNo - /// 체결여부 - public let conclused: ConclusionYesNo - /// 접수여부 - public let accepted: AcceptYesNo - /// 지점번호 - public let branchNo: String - /// 주문수량 - public let orderQuantity: Int - /// 계좌명 - public let accountName: String - /// 체결종목명 - public let productName: String - /// 신용구분 - public let creditClass: String - /// 신용대출일자 - public let creditLoanDate: String - /// 체결종목명40 - public let productName40: String - /// 주문가격 - public let orderPrice: Int - - - static let propertiesCount = 23 - - public init(array: [Substring], source: Substring) throws { - guard array.count == ContractNotice.propertiesCount else { - throw GeneralError.incorrectArrayItems(String(source), array.count, ContractNotice.propertiesCount) - } - - self.customerID = String(array[0]) - self.accountNo = String(array[1]) - self.orderNo = String(array[2]) - self.originalOrderNo = String(array[3]) - self.contractType = ContractType(rawValue: String(array[4]))! - self.revisionType = String(array[5]) - self.orderType = OrderType(rawValue: String(array[6]))! - self.orderCondition = String(array[7]) - self.shortCode = String(array[8]) - self.conclusionQuantity = Int(array[9]) ?? 0 - self.conclusionPrice = Int(array[10]) ?? 0 - self.conclusionTime = String(array[11]) - self.refused = RefuseYesNo(rawValue: Int(array[12])!)! - self.conclused = ConclusionYesNo(rawValue: Int(array[13])!)! - self.accepted = AcceptYesNo(rawValue: Int(array[14])!)! - self.branchNo = String(array[15]) - self.orderQuantity = Int(array[16]) ?? 0 - self.accountName = String(array[17]) - self.productName = String(array[18]) - self.creditClass = String(array[19]) - self.creditLoanDate = String(array[20]) - self.productName40 = String(array[21]) - self.orderPrice = Int(array[22]) ?? 0 - } -} func test_parse_contact_price_response() { let str = "{\"header\":{\"tr_id\":\"H0STCNT0\",\"tr_key\":\"005930\",\"encrypt\":\"N\"},\"body\":{\"rt_cd\":\"0\",\"msg_cd\":\"OPSP0000\",\"msg1\":\"SUBSCRIBE SUCCESS\",\"output\":{\"iv\":\"dcc3c442acfb8b9a\",\"key\":\"vcvxscahuklwkiawiuxbsfcmsulqjejf\"}}}0|H0STCNT0|001|005930^134305^68100^2^1000^1.49^68305.67^68300^68700^67900^68100^68000^1^11393808^778261604700^32559^26679^-5880^79.02^6084367^4807987^1^0.43^119.32^090027^5^-200^091809^5^-600^113615^2^200^20230824^20^N^309354^354766^2143494^2041321^0.19^6698642^170.09^0^^68300{\"header\":{\"tr_id\":\"PINGPONG\",\"datetime\":\"20230824212922\"}}" do { - let dataArray = try parseJsonTrData(str) + let dataArray = try Domestic.WebSocketResult.parse(str) for data in dataArray { switch data { case .json(let str): @@ -514,165 +41,6 @@ func test_parse_contact_price_response() { } -enum JsonTrDataType { - case json(String) - case contractPrice(ContractPrice) - case askingPrice(AskingPrice) - case contractNotice(ContractNotice) -} - - -func parseJsonTrData(_ str: String) throws -> [JsonTrDataType] { - var dataArray = [JsonTrDataType]() - var nextAt = str.startIndex - let charset = CharacterSet(charactersIn: "{}") - - while nextAt < str.endIndex { - print("nextAt... \(nextAt.utf16Offset(in: str))") - - switch str[nextAt] { - case "{": - /// Scan end of json data - var openedCount = 1 - let startAt = nextAt - nextAt = str.index(nextAt, offsetBy: 1) - - while openedCount > 0 { - guard let r = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { - break - } - - switch str[r.lowerBound] { - case "{": - openedCount += 1 - case "}": - openedCount -= 1 - if openedCount == 0 { - /// end of json data - let jsonString = String(str[startAt ..< r.upperBound]) - dataArray.append(.json(jsonString)) - nextAt = r.upperBound - break - } - default: - throw GeneralError.impossibleJsonCharacter - } - nextAt = r.upperBound - } - - case "0": - let (data, endAt) = try parseTrData(false, str: str, startAt: nextAt) - dataArray.append(contentsOf: data) - nextAt = endAt - - case "1": - let (data, endAt) = try parseTrData(true, str: str, startAt: nextAt) - dataArray.append(contentsOf: data) - nextAt = endAt - - default: - print(str[nextAt ..< str.endIndex]) - throw GeneralError.invalidWebSocketData - } - } - - return dataArray -} - - -private func parseTrData(_ encrypted: Bool, str: String, startAt: String.Index) throws -> ([JsonTrDataType], String.Index) { - var dataArray = [JsonTrDataType]() - var nextAt = startAt - let charset = CharacterSet(charactersIn: "|") - - guard let i1 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { - throw GeneralError.notEnoughWebSocketData - } - /// Get encryption field - let encryption = String(str[nextAt ..< i1.lowerBound]) - guard encryption == "0" || encryption == "1" else { - throw GeneralError.invalidWebSocketData_EncryptionField - } - nextAt = i1.upperBound - guard let i2 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { - throw GeneralError.notEnoughWebSocketData - } - /// Get trId field - let trId = String(str[nextAt ..< i2.lowerBound]) - nextAt = i2.upperBound - guard let i3 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { - throw GeneralError.notEnoughWebSocketData - } - /// Get data count field - let dataCountString = String(str[nextAt ..< i3.lowerBound]) - guard let dataCount = Int(dataCountString) else { - throw GeneralError.notEnoughWebSocketData - } - nextAt = i3.upperBound - - /// Retrieve multiple data - switch trId { - case "H0STCNT0": - for _ in 0 ..< dataCount { - let (stringArray, string, endAt) = try getTrDataString(str, startAt: nextAt, seperatorCount: ContractPrice.propertiesCount-1) - let price = try ContractPrice(array: stringArray, source: string) - dataArray.append(.contractPrice(price)) - nextAt = endAt - } - - case "H0STASP0": - for _ in 0 ..< dataCount { - let (stringArray, string, endAt) = try getTrDataString(str, startAt: nextAt, seperatorCount: AskingPrice.propertiesCount-1) - let price = try ContractPrice(array: stringArray, source: string) - dataArray.append(.contractPrice(price)) - nextAt = endAt - } - - case "H0STCNI0": - for _ in 0 ..< dataCount { - let (stringArray, string, endAt) = try getTrDataString(str, startAt: startAt, seperatorCount: ContractNotice.propertiesCount-1) - let notice = try ContractNotice(array: stringArray, source: string) - dataArray.append(.contractNotice(notice)) - nextAt = endAt - } - - default: - throw GeneralError.invalidWebSocketData_TrIdField - } - - return (dataArray, nextAt) -} - - -private func getTrDataString(_ str: String, startAt: String.Index, seperatorCount: Int) throws -> ([Substring], Substring, String.Index) { - var currentSeperator = 0 - var nextAt = startAt - var array = [Substring]() - - /// Find last seperator - while currentSeperator < seperatorCount { - guard let last = str.range(of: "^", options: [], range: nextAt ..< str.endIndex) else { - throw GeneralError.notEnoughWebSocketData - } - currentSeperator += 1 - array.append(str[nextAt ..< last.lowerBound]) - nextAt = last.upperBound - } - - /// Look forward to find final field - if let end = str.rangeOfCharacter(from: .alphanumerics.inverted, options: [], range: nextAt ..< str.endIndex) { - array.append(str[nextAt ..< end.lowerBound]) - let string = str[startAt ..< end.lowerBound] - return (array, string, end.lowerBound) - } - else { - array.append(str[nextAt ..< str.endIndex]) - let string = str[startAt ..< str.endIndex] - return (array, string, str.endIndex) - } -} - - func test_get_websocket_key_and_asking_price() { let isMock = false diff --git a/projects/macos/KissMe.xcodeproj/project.pbxproj b/projects/macos/KissMe.xcodeproj/project.pbxproj index caff0d7..b353c05 100644 --- a/projects/macos/KissMe.xcodeproj/project.pbxproj +++ b/projects/macos/KissMe.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 34C1BA8A2A5DA00A00423D64 /* DomesticDartMajorReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C1BA892A5DA00A00423D64 /* DomesticDartMajorReport.swift */; }; 34C97D0B2A602A3D00ED8B33 /* DomesticDartSecuritiesReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C97D0A2A602A3D00ED8B33 /* DomesticDartSecuritiesReport.swift */; }; 34D3680F2A2AA0BE005E6756 /* PropertyIterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D3680E2A2AA0BE005E6756 /* PropertyIterable.swift */; }; + 34DA3E9D2A9A028200BB3439 /* Domestic.WebSocketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DA3E9C2A9A028200BB3439 /* Domestic.WebSocketData.swift */; }; 34E7B9112A49BD2800B3AB9F /* DomesticIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E7B9102A49BD2800B3AB9F /* DomesticIndex.swift */; }; 34EC4D212A7ACB07002F947C /* CompanyPartitionMergerDecisionResult.json in Resources */ = {isa = PBXBuildFile; fileRef = 34EC4D202A7ACB06002F947C /* CompanyPartitionMergerDecisionResult.json */; }; 34EC4D242A7F27A8002F947C /* CompanyPartitionDecisionResult.json in Resources */ = {isa = PBXBuildFile; fileRef = 34EC4D232A7F27A8002F947C /* CompanyPartitionDecisionResult.json */; }; @@ -195,6 +196,7 @@ 34C1BA892A5DA00A00423D64 /* DomesticDartMajorReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticDartMajorReport.swift; sourceTree = ""; }; 34C97D0A2A602A3D00ED8B33 /* DomesticDartSecuritiesReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticDartSecuritiesReport.swift; sourceTree = ""; }; 34D3680E2A2AA0BE005E6756 /* PropertyIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyIterable.swift; sourceTree = ""; }; + 34DA3E9C2A9A028200BB3439 /* Domestic.WebSocketData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Domestic.WebSocketData.swift; sourceTree = ""; }; 34E7B9102A49BD2800B3AB9F /* DomesticIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticIndex.swift; sourceTree = ""; }; 34EC4D202A7ACB06002F947C /* CompanyPartitionMergerDecisionResult.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CompanyPartitionMergerDecisionResult.json; sourceTree = ""; }; 34EC4D232A7F27A8002F947C /* CompanyPartitionDecisionResult.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CompanyPartitionDecisionResult.json; sourceTree = ""; }; @@ -408,6 +410,7 @@ 34BC44742A8656250052D8EB /* Realtime */ = { isa = PBXGroup; children = ( + 34DA3E9C2A9A028200BB3439 /* Domestic.WebSocketData.swift */, 34BC447C2A86635A0052D8EB /* Domestic.ContractPriceWebSocket.swift */, 34BC447A2A8663430052D8EB /* Domestic.AskingPriceWebSocket.swift */, 34BC44752A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift */, @@ -841,6 +844,7 @@ 341F5EE12A0F373B00962D48 /* Login.swift in Sources */, 341F5EF52A0F891200962D48 /* KissAccount.swift in Sources */, 340A4DBD2A4C34BE005A1FBA /* IndexContext.swift in Sources */, + 34DA3E9D2A9A028200BB3439 /* Domestic.WebSocketData.swift in Sources */, 34BC44762A8656570052D8EB /* Domestic.ContractNoticeWebSocket.swift in Sources */, 34E7B9112A49BD2800B3AB9F /* DomesticIndex.swift in Sources */, 341F5F0D2A15222E00962D48 /* AuthRequest.swift in Sources */,