311 lines
12 KiB
Swift
311 lines
12 KiB
Swift
//
|
|
// KissConsole+Candle.swift
|
|
// KissMeConsole
|
|
//
|
|
// Created by ened-book-m1 on 2023/06/01.
|
|
//
|
|
|
|
import Foundation
|
|
import KissMe
|
|
|
|
|
|
/// Limit to request a candle query
|
|
let PreferredCandleTPS: UInt64 = 19
|
|
|
|
/// Limit to request a current price query
|
|
let PreferredNowTPS: UInt64 = 10
|
|
|
|
/// Limit to request a short query
|
|
let PreferredShortsTPS: UInt64 = 5
|
|
|
|
/// Limit to request a top query
|
|
let PreferredTopTPS: UInt64 = 5
|
|
|
|
/// Limit to reqeust a index query
|
|
let PreferredIndexTPS: UInt64 = 5
|
|
|
|
/// Limit to request a investor query
|
|
let PreferredInvestorTPS: UInt64 = 19
|
|
|
|
/// Limit to request a naver news query
|
|
let PreferredNaverNewsTPS: UInt64 = 1
|
|
|
|
/// How many seconds does 1 day have?
|
|
let SecondsForOneDay: TimeInterval = 60 * 60 * 24
|
|
|
|
|
|
extension KissConsole {
|
|
|
|
enum CandleFilePeriod: String {
|
|
case minute = "min"
|
|
case day = "day"
|
|
case weak = "week"
|
|
}
|
|
|
|
|
|
func getCandle(productNo: String, period: PeriodDivision, count: Int, startDate: Date?) async -> Bool {
|
|
do {
|
|
guard currentCandleShortCode == nil else {
|
|
print("Already candle collecting")
|
|
return false
|
|
}
|
|
currentCandleShortCode = productNo
|
|
defer {
|
|
currentCandleShortCode = nil
|
|
}
|
|
|
|
var reqEndDate = Date()
|
|
var reqStartDate: Date
|
|
if let startDate = startDate {
|
|
reqStartDate = startDate
|
|
}
|
|
else {
|
|
reqStartDate = reqEndDate.addingTimeInterval(-period.secondsForPeriodRequest)
|
|
}
|
|
|
|
var reqCount = 0
|
|
var candles = [Domestic.CandlePeriod]()
|
|
|
|
while true {
|
|
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?.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)")
|
|
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 {
|
|
/// 만약에, 기간내에 데이터를 하나도 없을 경우 (ex. 거래정지 등),
|
|
/// 이전 날짜로 몇번 더 올라가서 데이터를 다시 긁어본다.
|
|
if reqCount < 5 {
|
|
reqEndDate = reqStartDate.addingTimeInterval(-SecondsForOneDay)
|
|
reqStartDate = reqEndDate.addingTimeInterval(-period.secondsForPeriodRequest)
|
|
}
|
|
else {
|
|
print("\(period) price finished")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredCandleTPS)
|
|
|
|
candles.sort(by: { $0.stockBusinessDate > $1.stockBusinessDate })
|
|
guard let recentDay = candles.first?.stockBusinessDate else {
|
|
print("No price items")
|
|
return false
|
|
}
|
|
|
|
let fileUrl = KissConsole.candleFileUrl(productNo: productNo, period: period.filePeriod, day: recentDay)
|
|
try candles.mergeCsv(toFile: fileUrl, merging: { this, file in
|
|
var merged = this
|
|
for old in file {
|
|
if nil == this.first(where: { $0.stockBusinessDate == old.stockBusinessDate }) {
|
|
merged.append(old)
|
|
}
|
|
}
|
|
merged.sort(by: { $0.stockBusinessDate > $1.stockBusinessDate })
|
|
return merged
|
|
}, localized: false)
|
|
|
|
print("wrote \(fileUrl.lastPathComponent) with \(candles.count)")
|
|
return true
|
|
} catch {
|
|
print(error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
func getCandle(productNo: String) async -> Bool {
|
|
do {
|
|
guard currentCandleShortCode == nil else {
|
|
print("Already candle collecting")
|
|
return false
|
|
}
|
|
currentCandleShortCode = productNo
|
|
defer {
|
|
currentCandleShortCode = nil
|
|
}
|
|
|
|
var nextTime = Date()
|
|
var candles = [Domestic.Candle]()
|
|
var count = 0
|
|
|
|
while true {
|
|
let more = (count > 0)
|
|
count += 1
|
|
print("minute price \(productNo) from \(nextTime.yyyyMMdd_HHmmss_forTime) \(more)")
|
|
|
|
let result = try await account!.getMinutePrice(productNo: productNo, startTodayTime: nextTime, more: more)
|
|
|
|
if let prices = result.output2, prices.isEmpty == false {
|
|
candles.append(contentsOf: prices)
|
|
if let last = prices.last {
|
|
if nextTime.yyyyMMdd != last.stockBusinessDate {
|
|
if let (yyyy, mm, dd) = last.stockBusinessDate.yyyyMMdd {
|
|
print("next: \(last.stockBusinessDate)")
|
|
nextTime.change(year: yyyy, month: mm, day: dd)
|
|
}
|
|
}
|
|
if let (hh, mm, ss) = last.stockConclusionTime.HHmmss {
|
|
print("next: \(last.stockConclusionTime) / \(hh) \(mm) \(ss)")
|
|
nextTime.change(hour: hh, min: mm-1, sec: ss)
|
|
if hh == 9, mm == 0, ss == 0 {
|
|
print("minute price finished")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredCandleTPS)
|
|
}
|
|
else {
|
|
print("minute price finished")
|
|
break
|
|
}
|
|
}
|
|
try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredCandleTPS)
|
|
|
|
candles.sort(by: { $0.stockFullDate > $1.stockFullDate })
|
|
guard let maxTime = candles.first?.stockBusinessDate else {
|
|
print("No price items")
|
|
return false
|
|
}
|
|
|
|
let fileUrl = KissConsole.candleFileUrl(productNo: productNo, period: .minute, day: maxTime)
|
|
try candles.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("wrote \(fileUrl.lastPathComponent) with \(candles.count)")
|
|
return true
|
|
} catch {
|
|
print(error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
func checkHoliday(_ date: Date) async throws -> Bool {
|
|
let day = date.yyyyMMdd
|
|
guard await KissContext.shared.targetDay != day else {
|
|
return await KissContext.shared.isHoliday
|
|
}
|
|
|
|
do {
|
|
let holidays = try [HolidyResult.OutputDetail].readCsv(fromFile: KissConsole.holidayUrl)
|
|
let isHoliday = try holidays.isHoliday(day)
|
|
await KissContext.shared.updateHoliday(isHoliday, targetDay: day)
|
|
return isHoliday
|
|
} catch {
|
|
print(error)
|
|
}
|
|
|
|
let result = try await account!.getHolyday(baseDate: day)
|
|
do {
|
|
if let output = result.output {
|
|
try output.mergeCsv(toFile: KissConsole.holidayUrl, merging: { this, file in
|
|
var merged = this
|
|
for old in file {
|
|
if nil == this.first(where: { $0.baseDate == old.baseDate }) {
|
|
merged.append(old)
|
|
}
|
|
}
|
|
merged.sort(by: { $0.baseDate < $1.baseDate })
|
|
return merged
|
|
}, localized: localized)
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
|
|
let isHoliday = try result.isHoliday(day)
|
|
await KissContext.shared.updateHoliday(isHoliday, targetDay: day)
|
|
return isHoliday
|
|
}
|
|
|
|
|
|
func validateAllCsvs(filePriod: CandleFilePeriod) throws {
|
|
let urls = try FileManager.collectCsv(period: filePriod, candleDate: nil)
|
|
|
|
var lastTime = Date.appTime
|
|
for (index, url) in urls.enumerated() {
|
|
|
|
let r = validateCsv(filePriod: filePriod, url: url)
|
|
switch r {
|
|
case .ok, .invalidFileName:
|
|
break
|
|
default:
|
|
print("csv invalid: \(r) at \(url)")
|
|
throw GeneralError.invalidCandleCsvFile(r.description)
|
|
}
|
|
|
|
let curTime = Date.appTime
|
|
if (curTime - lastTime) > 5 {
|
|
lastTime = curTime
|
|
print("checking... \(index+1)/\(urls.count)")
|
|
}
|
|
}
|
|
print("DONE csv valid \(urls.count)")
|
|
}
|
|
|
|
|
|
func validateCsv(filePriod: CandleFilePeriod, url: URL) -> CandleValidation {
|
|
switch filePriod {
|
|
case .minute: return KissConsole.validateCandleMinute(url)
|
|
case .day: return KissConsole.validateCandleDay(url)
|
|
case .weak: return KissConsole.validateCandleWeek(url)
|
|
}
|
|
}
|
|
|
|
|
|
func getLatestDateFromCandle(_ productNo: String, period: PeriodDivision) throws -> Date? {
|
|
let curDate = Date()
|
|
let fileUrl = KissConsole.candleFileUrl(productNo: productNo, period: period.filePeriod, day: curDate.yyyyMMdd)
|
|
|
|
let candles = try [Domestic.CandlePeriod].readCsvTop(fromFile: fileUrl, by: 1)
|
|
if candles.count == 1 {
|
|
let latestCandleDate = candles[0].stockBusinessDate.yyyyMMdd_toDate
|
|
|
|
/// API 호출 결과에서 <누적 거래 대금>, <누적 거래량> 의 값이 0으로 유효하지 않는 값을 제공해주는 경우가 있다.
|
|
/// 이럴 경우, 차후에 다시 데이터를 업데이트할 필요가 있다.
|
|
if candles[0].accumulatedVolume == "0", candles[0].accumulatedTradingAmount == "0" {
|
|
return latestCandleDate?.addingTimeInterval(-SecondsForOneDay)
|
|
}
|
|
return latestCandleDate
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
extension PeriodDivision {
|
|
var filePeriod: KissConsole.CandleFilePeriod {
|
|
switch self {
|
|
case .daily: return .day
|
|
case .weekly: return .weak
|
|
case .monthly: assertionFailure()
|
|
case .yearly: assertionFailure()
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|