Files
KissMe/KissMeConsole/Sources/KissConsole+Candle.swift
2023-07-29 19:48:34 +09:00

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
}
}
}