diff --git a/KissMe/Sources/Domestic/DomesticStockPrice.swift b/KissMe/Sources/Domestic/DomesticStockPrice.swift index 4d1f5f9..45f6414 100644 --- a/KissMe/Sources/Domestic/DomesticStockPrice.swift +++ b/KissMe/Sources/Domestic/DomesticStockPrice.swift @@ -140,8 +140,7 @@ extension Domestic { public var result: KResult? = nil public let credential: Credential public var responseDataLoggable: Bool { true } - public var traceable: Bool { true } - + private var trId: String { "FHKST03010100" diff --git a/KissMe/Sources/Domestic/DomesticStockPriceResult.swift b/KissMe/Sources/Domestic/DomesticStockPriceResult.swift index df1709d..0b54f18 100644 --- a/KissMe/Sources/Domestic/DomesticStockPriceResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockPriceResult.swift @@ -575,7 +575,7 @@ public struct PeriodPriceResult: Codable { public let messageCode: String public let message: String public let output1: OutputSummary? - public let output2: [OutputPrice]? + public let output2: [OutputPriceOptional]? private enum CodingKeys: String, CodingKey { case resultCode = "rt_cd" @@ -711,6 +711,84 @@ public struct PeriodPriceResult: Codable { } } + + public struct OutputPriceOptional: Codable { + /// 주식 영업 일자 + public let stockBusinessDate: String? + + /// 주식 종가 + public let stockClosingPrice: String? + + /// 주식 시가 + public let stockOpenningPrice: String? + + /// 주식 최고가 + public let highestStockPrice: String? + + /// 주식 최저가 + public let lowestStockPrice: String? + + /// 누적 거래량 + public let accumulatedVolume: String? + + /// 누적 거래 대금 + public let accumulatedTradingAmount: String? + + /// 락 구분 코드 + public let exDivision: ExDivision? + + /// 분할 비율 + public let partitionRate: String? + + /// 분할변경여부 + public let partitionModifiable: YesNo? + + /// 전일 대비 부호 + public let previousDayVariableRatioSign: String? + + /// 전일 대비 + public let previousDayVariableRatio: String? + + /// 재평가사유코드 + public let revaluationIssueReason: String? + + private enum CodingKeys: String, CodingKey { + case stockBusinessDate = "stck_bsop_date" + case stockClosingPrice = "stck_clpr" + case stockOpenningPrice = "stck_oprc" + case highestStockPrice = "stck_hgpr" + case lowestStockPrice = "stck_lwpr" + case accumulatedVolume = "acml_vol" + case accumulatedTradingAmount = "acml_tr_pbmn" + case exDivision = "flng_cls_code" + case partitionRate = "prtt_rate" + case partitionModifiable = "mod_yn" + case previousDayVariableRatioSign = "prdy_vrss_sign" + case previousDayVariableRatio = "prdy_vrss" + case revaluationIssueReason = "revl_issu_reas" + } + + public var validObject: OutputPrice? { + guard let stockBusinessDate = stockBusinessDate, + let stockClosingPrice = stockClosingPrice, + let stockOpenningPrice = stockOpenningPrice, + let highestStockPrice = highestStockPrice, + let lowestStockPrice = lowestStockPrice, + let accumulatedVolume = accumulatedVolume, + let accumulatedTradingAmount = accumulatedTradingAmount, + let exDivision = exDivision, + let partitionRate = partitionRate, + let partitionModifiable = partitionModifiable, + let previousDayVariableRatioSign = previousDayVariableRatioSign, + let previousDayVariableRatio = previousDayVariableRatio, + let revaluationIssueReason = revaluationIssueReason else { + return nil + } + return OutputPrice(stockBusinessDate: stockBusinessDate, stockClosingPrice: stockClosingPrice, stockOpenningPrice: stockOpenningPrice, highestStockPrice: highestStockPrice, lowestStockPrice: lowestStockPrice, accumulatedVolume: accumulatedVolume, accumulatedTradingAmount: accumulatedTradingAmount, exDivision: exDivision, partitionRate: partitionRate, partitionModifiable: partitionModifiable, previousDayVariableRatioSign: previousDayVariableRatioSign, previousDayVariableRatio: previousDayVariableRatio, revaluationIssueReason: revaluationIssueReason) + } + } + + public struct OutputPrice: Codable { /// 주식 영업 일자 public let stockBusinessDate: String @@ -782,5 +860,21 @@ public struct PeriodPriceResult: Codable { self.previousDayVariableRatio = array[11] self.revaluationIssueReason = array[12] } + + init(stockBusinessDate: String, stockClosingPrice: String, stockOpenningPrice: String, highestStockPrice: String, lowestStockPrice: String, accumulatedVolume: String, accumulatedTradingAmount: String, exDivision: ExDivision, partitionRate: String, partitionModifiable: YesNo, previousDayVariableRatioSign: String, previousDayVariableRatio: String, revaluationIssueReason: String) { + self.stockBusinessDate = stockBusinessDate + self.stockClosingPrice = stockClosingPrice + self.stockOpenningPrice = stockOpenningPrice + self.highestStockPrice = highestStockPrice + self.lowestStockPrice = lowestStockPrice + self.accumulatedVolume = accumulatedVolume + self.accumulatedTradingAmount = accumulatedTradingAmount + self.exDivision = exDivision + self.partitionRate = partitionRate + self.partitionModifiable = partitionModifiable + self.previousDayVariableRatioSign = previousDayVariableRatioSign + self.previousDayVariableRatio = previousDayVariableRatio + self.revaluationIssueReason = revaluationIssueReason + } } } diff --git a/KissMeConsole/Sources/KissConsole+Candle.swift b/KissMeConsole/Sources/KissConsole+Candle.swift index 4d3b272..1bdde55 100644 --- a/KissMeConsole/Sources/KissConsole+Candle.swift +++ b/KissMeConsole/Sources/KissConsole+Candle.swift @@ -18,33 +18,6 @@ let SecondsForOneDay: TimeInterval = 60 * 60 * 24 extension KissConsole { - var last250Days: (startDate: Date, endDate: Date) { - var curDate = Date() - if let (hour, _, _) = curDate.HHmmss_split { - if hour < 9 { - curDate.addTimeInterval(-SecondsForOneDay) - } - } - let endDate = curDate.changing(hour: 0, min: 0, sec: 0)! - let startDate = endDate.addingTimeInterval(-250 * SecondsForOneDay) - return (startDate, endDate) - } - - - var last52Weeks: (startDate: Date, endDate: Date) { - // TODO: 주당 가격을 얻기 위해, 거래 시작일을 마지막 장 개설일로 보정할 필요가 있을까? - var curDate = Date() - if let (hour, _, _) = curDate.HHmmss_split { - if hour < 9 { - curDate.addTimeInterval(-SecondsForOneDay) - } - } - let endDate = curDate.changing(hour: 0, min: 0, sec: 0)! - let startDate = endDate.addingTimeInterval(-52 * 7 * SecondsForOneDay) - return (startDate, endDate) - } - - enum CandleFilePeriod: String { case minute = "min" case day = "day" @@ -55,14 +28,23 @@ extension KissConsole { func candleFileUrl(productNo: String, period: CandleFilePeriod, day: String) -> URL { assert(day.count == 8) let subPath = "data/\(productNo)/\(period.rawValue)" - let subFile = "\(subPath)/candle-\(day).csv" + let subFile: String + + switch period { + case .minute: + subFile = "\(subPath)/candle-\(day).csv" + case .day: + subFile = "\(subPath)/candle-day-250.csv" + case .weak: + subFile = "\(subPath)/candle-week-42.csv" + } let fileUrl = URL.currentDirectory().appending(path: subFile) createSubpath(subPath) return fileUrl } - func getCandle(productNo: String, period: PeriodDivision, startDate: Date, endDate: Date) async -> Bool { + func getRecentCandle(productNo: String, period: PeriodDivision, count: Int) async -> Bool { do { guard currentCandleShortCode == nil else { print("Already candle collecting") @@ -73,38 +55,44 @@ extension KissConsole { currentCandleShortCode = nil } - var reqStartDate = startDate - var reqEndDate = reqStartDate.addingTimeInterval(99 * SecondsForOneDay) - if reqEndDate > endDate { - reqEndDate = endDate - } - + var reqEndDate = Date() + var reqStartDate = reqEndDate.addingTimeInterval(-period.secondsForPeriodRequest) + var reqCount = 0 + var candles = [Domestic.CandlePeriod]() - var count = 0 - + while true { - count += 1 + reqCount += 1 print("\(period) price \(productNo) from \(reqStartDate.yyyyMMdd_HHmmss_forTime) to \(reqEndDate.yyyyMMdd_HHmmss_forTime)") let result = try await account!.getPeriodPrice(productNo: productNo, startDate: reqStartDate, endDate: reqEndDate, period: period) - - if let prices = result.output2, prices.isEmpty == false { + + if let prices = result.output2?.compactMap({ $0.validObject }), prices.isEmpty == false { candles.append(contentsOf: prices) + if candles.count >= count { + print("\(period) price finished") + break + } if let last = prices.last { if let (yyyy, mm, dd) = last.stockBusinessDate.yyyyMMdd { print("next: \(last.stockBusinessDate)") - reqStartDate.change(year: yyyy, month: mm, day: dd+1) - reqEndDate = reqStartDate.addingTimeInterval(99 * SecondsForOneDay) - if reqEndDate > endDate { - reqEndDate = endDate - } + reqEndDate.change(year: yyyy, month: mm, day: dd-1) + reqStartDate = reqEndDate.addingTimeInterval(-period.secondsForPeriodRequest) } } try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredCandleTPS) } else { - print("\(period) price finished") - break + /// 만약에, 기간내에 데이터를 하나도 없을 경우 (ex. 거래정지 등), + /// 이전 날짜로 몇번 더 올라가서 데이터를 다시 긁어본다. + if reqCount < 5 { + reqEndDate = reqStartDate.addingTimeInterval(-SecondsForOneDay) + reqStartDate = reqEndDate.addingTimeInterval(-period.secondsForPeriodRequest) + } + else { + print("\(period) price finished") + break + } } } @@ -199,4 +187,13 @@ extension PeriodDivision { } return .minute } + + var secondsForPeriodRequest: TimeInterval { + switch self { + case .daily: return 99 * SecondsForOneDay /// 한번에 요청으로 100건씩 가져옴. 시작날 ~ 종료날 포함이므로, 99 + case .weekly: return 99 * 7 * SecondsForOneDay + case .monthly: return 99 * 28 * SecondsForOneDay + case .yearly: return 99 * 365 * SecondsForOneDay + } + } } diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index f9639eb..fa050b3 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -583,13 +583,9 @@ extension KissConsole { return } - let (startDate, endDate) = last250Days - //let startDate = Date().changing(year: 2023, month: 5, day: 1)! - //let endDate = Date().changing(year: 2023, month: 6, day: 1)! - let semaphore = DispatchSemaphore(value: 0) Task { - let success = await getCandle(productNo: productNo, period: .daily, startDate: startDate, endDate: endDate) + let success = await getRecentCandle(productNo: productNo, period: .daily, count: 250) print("DONE \(success) \(productNo)") semaphore.signal() } @@ -598,14 +594,18 @@ extension KissConsole { private func onCandleDayAll() { - let (startDate, endDate) = last250Days let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) Task { - let success = await getCandle(productNo: item.shortCode, period: .daily, startDate: startDate, endDate: endDate) + let success = await getRecentCandle(productNo: item.shortCode, period: .daily, count: 250) print("DONE \(success) \(item.shortCode)") semaphore.signal() + #if DEBUG + if !success { + exit(99) + } + #endif } semaphore.wait() } @@ -614,7 +614,7 @@ extension KissConsole { private func onCandleWeek(_ args: [String]) { if args.count == 1, args[0] == "all" { - onCandleDayAll() + onCandleWeekAll() return } @@ -624,10 +624,9 @@ extension KissConsole { return } - let (startDate, endDate) = last52Weeks let semaphore = DispatchSemaphore(value: 0) Task { - let success = await getCandle(productNo: productNo, period: .weekly, startDate: startDate, endDate: endDate) + let success = await getRecentCandle(productNo: productNo, period: .weekly, count: 52) print("DONE \(success) \(productNo)") semaphore.signal() } @@ -636,14 +635,18 @@ extension KissConsole { private func onCandleWeekAll() { - let (startDate, endDate) = last52Weeks let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) Task { - let success = await getCandle(productNo: item.shortCode, period: .weekly, startDate: startDate, endDate: endDate) + let success = await getRecentCandle(productNo: item.shortCode, period: .weekly, count: 52) print("DONE \(success) \(item.shortCode)") semaphore.signal() + #if DEBUG + if !success { + exit(99) + } + #endif } semaphore.wait() } diff --git a/bin/data b/bin/data index 08024e3..3605948 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit 08024e32ce3235a4df32f6337609c025df1347ba +Subproject commit 3605948d543a8c410fcb2b7fbcfe11f90cd39b5c