From aababdae8e7bb9627c27e2c2b4ffb2e45ba57726 Mon Sep 17 00:00:00 2001 From: ened Date: Sat, 26 Aug 2023 15:46:30 +0900 Subject: [PATCH] Parsing websocket data --- KissMeConsole/Sources/main.swift | 319 ++++++++++++++++++++++++++----- 1 file changed, 269 insertions(+), 50 deletions(-) diff --git a/KissMeConsole/Sources/main.swift b/KissMeConsole/Sources/main.swift index 3eac3e2..73c4c1f 100644 --- a/KissMeConsole/Sources/main.swift +++ b/KissMeConsole/Sources/main.swift @@ -12,7 +12,8 @@ import Foundation import KissMe //test_get_websocket_key_and_contact_price() -test_get_websocket_key_and_asking_price() +//test_get_websocket_key_and_asking_price() +test_parse_contact_price_response() enum VariableRatioSignType: UInt8 { @@ -53,24 +54,41 @@ enum HourClassCode: Character { } -enum MarketOperationCode: UInt8 { - // TODO: work -// (1) 첫 번째 비트 -// 1 : 장개시전 -// 2 : 장중 -// 3 : 장종료후 -// 4 : 시간외단일가 -// 7 : 일반Buy-in -// 8 : 당일Buy-in -// -// (2) 두 번째 비트 -// 0 : 보통 -// 1 : 종가 -// 2 : 대량 -// 3 : 바스켓 -// 7 : 정리매매 -// 8 : Buy-in - case aa = 1 +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)] } } @@ -167,6 +185,62 @@ struct ContractPrice { 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 + } } @@ -217,6 +291,48 @@ struct AskingPrice { 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]) + } } @@ -339,12 +455,62 @@ struct ContractNotice { 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) + for data in dataArray { + switch data { + case .json(let str): + print("json: \(str)") + case .contractPrice(let price): + print("contractPrice: \(price)") + case .askingPrice(let price): + print("askingPrice: \(price)") + case .contractNotice(let notice): + print("contractNotice: \(notice)") + } + } + } catch { + print(error) + } } @@ -358,16 +524,21 @@ enum JsonTrDataType { func parseJsonTrData(_ str: String) throws -> [JsonTrDataType] { var dataArray = [JsonTrDataType]() - var startAt = str.startIndex - - while startAt < str.endIndex { - switch str.first { + var nextAt = str.startIndex + let charset = CharacterSet(charactersIn: "{}") + + while nextAt < str.endIndex { + print("nextAt... \(nextAt.utf16Offset(in: str))") + + switch str[nextAt] { case "{": - var openedCount = 0 - let charset = CharacterSet(charactersIn: "{}") + /// Scan end of json data + var openedCount = 1 + let startAt = nextAt + nextAt = str.index(nextAt, offsetBy: 1) - repeat { - guard let r = str.rangeOfCharacter(from: charset, options: [], range: startAt ..< str.endIndex) else { + while openedCount > 0 { + guard let r = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { break } @@ -378,25 +549,29 @@ func parseJsonTrData(_ str: String) throws -> [JsonTrDataType] { openedCount -= 1 if openedCount == 0 { /// end of json data - let jsonString = String(str[str.startIndex ..< r.upperBound]) + let jsonString = String(str[startAt ..< r.upperBound]) dataArray.append(.json(jsonString)) - startAt = r.upperBound + nextAt = r.upperBound + break } default: throw GeneralError.impossibleJsonCharacter } - startAt = r.upperBound - } while true + nextAt = r.upperBound + } case "0": - let data = try parseTrData(false, str: str, startAt: startAt) + let (data, endAt) = try parseTrData(false, str: str, startAt: nextAt) dataArray.append(contentsOf: data) + nextAt = endAt case "1": - let data = try parseTrData(true, str: str, startAt: startAt) + 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 } } @@ -405,52 +580,96 @@ func parseJsonTrData(_ str: String) throws -> [JsonTrDataType] { } -private func parseTrData(_ encrypted: Bool, str: String, startAt: String.Index) throws -> [JsonTrDataType] { +private func parseTrData(_ encrypted: Bool, str: String, startAt: String.Index) throws -> ([JsonTrDataType], String.Index) { var dataArray = [JsonTrDataType]() - var startAt = str.startIndex + var nextAt = startAt let charset = CharacterSet(charactersIn: "|") - guard let i1 = str.rangeOfCharacter(from: charset, options: [], range: startAt ..< str.endIndex) else { + guard let i1 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { throw GeneralError.notEnoughWebSocketData } - - let encryption = String(str[startAt ..< i1.upperBound]) + /// Get encryption field + let encryption = String(str[nextAt ..< i1.lowerBound]) guard encryption == "0" || encryption == "1" else { throw GeneralError.invalidWebSocketData_EncryptionField } - startAt = i1.upperBound - guard let i2 = str.rangeOfCharacter(from: charset, options: [], range: startAt ..< str.endIndex) else { + nextAt = i1.upperBound + guard let i2 = str.rangeOfCharacter(from: charset, options: [], range: nextAt ..< str.endIndex) else { throw GeneralError.notEnoughWebSocketData } - - let trId = String(str[startAt ..< i2.upperBound]) - startAt = i2.upperBound - guard let i3 = str.rangeOfCharacter(from: charset, options: [], range: startAt ..< str.endIndex) else { + /// 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 } - - let dataCountString = String(str[startAt ..< i3.upperBound]) + /// 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": - break + 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": - break + 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 + 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) + } }