diff --git a/KissMe/Sources/Common/Foundation+Extensions.swift b/KissMe/Sources/Common/Foundation+Extensions.swift index 669c1a1..fe5ac30 100644 --- a/KissMe/Sources/Common/Foundation+Extensions.swift +++ b/KissMe/Sources/Common/Foundation+Extensions.swift @@ -16,6 +16,10 @@ extension Date { return dateFormatter.string(from: self) } + public var yyyyMM01: String { + yyyyMM + "01" + } + public var yyyyMMdd: String { let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(abbreviation: "KST") @@ -165,8 +169,22 @@ extension FileManager { public func valueToString(_ any: Any) -> String { + + func validateComma(_ s: String) -> String { + let comma: CharacterSet = [","] + if s.rangeOfCharacter(from: comma) != nil { + assertionFailure("There are comma in: \(s)") + } + return s + } + switch any { - case let s as String: return s + case let s as String: + #if DEBUG + return validateComma(s) + #else + return s + #endif case let i as Int8: return String(i) case let i as UInt8: return String(i) case let i as Int16: return String(i) @@ -184,7 +202,12 @@ public func valueToString(_ any: Any) -> String { case let d as Double: return String(d) case let raw as any RawRepresentable: switch raw.rawValue { - case let s as String: return s + case let s as String: + #if DEBUG + return validateComma(s) + #else + return s + #endif case let i as Int8: return String(i) case let i as UInt8: return String(i) case let i as Int16: return String(i) @@ -198,7 +221,8 @@ public func valueToString(_ any: Any) -> String { default: return "" } - case let c as CustomStringConvertible: return c.description + case let c as CustomStringConvertible: + return validateComma(c.description) default: return "" } @@ -279,11 +303,13 @@ extension Array where Element: PropertyIterable { for (index, item) in items.enumerated() { if index == 0 { - headerItems = item.split(separator: ",").map { String($0) } + headerItems = item.split(separator: ",", omittingEmptySubsequences: false).map { String($0) } continue } - let array = item.split(separator: ",").map { String($0) } - let element = try Element(array: array) + let array = item.split(separator: ",", omittingEmptySubsequences: false).map { String($0) } + + //print("index: \(index), \(fromFile.path)") + let element = try Element(array: array, source: item) if index == 1, verifyHeader { // Validate property with header @@ -342,7 +368,7 @@ extension String { public static func readCsvHeader(fromFile: URL) throws -> [String] { let header = try String(firstLineOfFile: fromFile.path) - return header.split(separator: ",").map { String($0) } + return header.split(separator: ",", omittingEmptySubsequences: false).map { String($0) } } public func writeAppending(toFile path: String) throws { diff --git a/KissMe/Sources/Common/PropertyIterable.swift b/KissMe/Sources/Common/PropertyIterable.swift index c812e1e..80175f7 100644 --- a/KissMe/Sources/Common/PropertyIterable.swift +++ b/KissMe/Sources/Common/PropertyIterable.swift @@ -34,5 +34,5 @@ public extension PropertyIterable { public protocol ArrayDecodable { - init(array: [String]) throws + init(array: [String], source: String.SubSequence) throws } diff --git a/KissMe/Sources/Common/Request.swift b/KissMe/Sources/Common/Request.swift index 699d0bd..b5f5114 100644 --- a/KissMe/Sources/Common/Request.swift +++ b/KissMe/Sources/Common/Request.swift @@ -37,7 +37,7 @@ public enum GeneralError: Error { case cannotWriteFile case cannotReadFileLine case cannotReadFileToConvertString - case incorrectArrayItems + case incorrectArrayItems(String, Int, Int) case headerNoFiendName(String) case noCsvFile case invalidCandleCsvFile(String) diff --git a/KissMe/Sources/Common/LocalContext.swift b/KissMe/Sources/Context/LocalContext.swift similarity index 89% rename from KissMe/Sources/Common/LocalContext.swift rename to KissMe/Sources/Context/LocalContext.swift index d5d50bb..84abfce 100644 --- a/KissMe/Sources/Common/LocalContext.swift +++ b/KissMe/Sources/Context/LocalContext.swift @@ -12,9 +12,9 @@ public struct LocalName: Codable, PropertyIterable, ArrayDecodable { public let fieldName: String public let localizedName: String - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 2 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 2) } fieldName = array[0] localizedName = array[1] diff --git a/KissMe/Sources/Context/ShopContext.swift b/KissMe/Sources/Context/ShopContext.swift new file mode 100644 index 0000000..6fab9ea --- /dev/null +++ b/KissMe/Sources/Context/ShopContext.swift @@ -0,0 +1,149 @@ +// +// ShopContext.swift +// KissMe +// +// Created by ened-book-m1 on 2023/06/21. +// + +import Foundation + + +open class ShopContext { + + private var productsLock = NSLock() + /// 전체 종목 정보 + private var products = [String: [DomesticShop.Product]]() + + public var productsCount: Int { + productsLock.lock() + defer { + productsLock.unlock() + } + return products.count + } + + /// 현재 기본으로 선택된 productNo + private var shortCodeLock = NSLock() + private var _currentShortCode: String? + public var currentShortCode: String? { + get { + shortCodeLock.lock() + defer { + shortCodeLock.unlock() + } + return _currentShortCode + } + set { + shortCodeLock.lock() + defer { + shortCodeLock.unlock() + } + _currentShortCode = newValue + } + } + + public init() { + } +} + + +extension ShopContext { + + public func loadShop(url: URL, profile: Bool = false) { + let appTime1 = Date.appTime + guard let stringCsv = try? String(contentsOfFile: url.path) else { + return + } + + let appTime2 = Date.appTime + if profile { + print("\tloading file \(appTime2 - appTime1) elapsed") + } + + var products = [String: [DomesticShop.Product]]() + let rows = stringCsv.split(separator: "\n") + + let appTime3 = Date.appTime + if profile { + print("\trow split \(appTime3 - appTime2) elapsed") + } + + for (i, row) in rows.enumerated() { + let array = row.split(separator: ",", omittingEmptySubsequences: false).map { String($0) } + if i == 0, array[0] == "baseDate" { + continue + } + + let product = try! DomesticShop.Product(array: array, source: "") + if let _ = products[product.itemName] { + products[product.itemName]!.append(product) + } + else { + products[product.itemName] = [product] + } + } + let appTime4 = Date.appTime + if profile { + print("\tparse product \(appTime4 - appTime3) elapsed") + } + + setProducts(products) + let totalCount = products.reduce(0, { $0 + $1.value.count }) + print("load products \(totalCount) with \(products.count) key") + } + + private func setProducts(_ products: [String: [DomesticShop.Product]]) { + productsLock.lock() + self.products = products + productsLock.unlock() + } + + public func getProducts(similarName: String) -> [String: [DomesticShop.Product]]? { + productsLock.lock() + defer { + productsLock.unlock() + } + return products.filter { $0.key.contains(similarName) } + //return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) } + } + + public func getProduct(isin: String) -> DomesticShop.Product? { + productsLock.lock() + defer { + productsLock.unlock() + } + + return products.compactMap { $0.value.first(where: { $0.isinCode == isin }) }.first + } + + public func getProduct(shortCode: String) -> DomesticShop.Product? { + productsLock.lock() + defer { + productsLock.unlock() + } + + return products.compactMap { $0.value.first(where: { $0.shortCode == shortCode }) }.first + } + + public func getAllProducts() -> [DomesticShop.Product] { + productsLock.lock() + defer { + productsLock.unlock() + } + + var all = [DomesticShop.Product]() + for items in products.values { + all.append(contentsOf: items) + } + return all + } + + public func setCurrent(productNo: String) { + productsLock.lock() + currentShortCode = productNo + productsLock.unlock() + + let productName = getProduct(shortCode: productNo)?.itemName ?? "" + print("current product \(productNo) \(productName)") + } +} diff --git a/KissMe/Sources/Domestic/DomesticStockPriceResult.swift b/KissMe/Sources/Domestic/DomesticStockPriceResult.swift index dc0b967..a7c5850 100644 --- a/KissMe/Sources/Domestic/DomesticStockPriceResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockPriceResult.swift @@ -470,9 +470,9 @@ public struct CurrentPriceResult: Codable { case shortOverheated = "short_over_yn" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 80 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 80) } self.itemStateCode = StateClass(rawValue: array[0])! self.marginalRate = array[1] @@ -557,7 +557,7 @@ public struct CurrentPriceResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputDetail(array: Array(repeating: "", count: 80)) + let i = try! OutputDetail(array: Array(repeating: "", count: 80), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -894,7 +894,7 @@ public struct CapturePrice: Codable, PropertyIterable, ArrayDecodable { self.totalOutstandingloanRate = p.totalOutstandingloanRate self.shortSellingAllowable = p.shortSellingAllowable self.shortProductCode = p.shortProductCode - self.facePriceCurrency = p.facePriceCurrency + self.facePriceCurrency = p.facePriceCurrency.trimmingCharacters(in: CharacterSet([","])) self.capitalCurrency = p.capitalCurrency self.approachRate = p.approachRate self.foreignHoldQuantity = p.foreignHoldQuantity @@ -906,9 +906,9 @@ public struct CapturePrice: Codable, PropertyIterable, ArrayDecodable { self.shortOverheated = p.shortOverheated } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 82 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 82) } self.stockBusinessDate = array[0] self.captureTime = array[1] @@ -995,7 +995,7 @@ public struct CapturePrice: Codable, PropertyIterable, ArrayDecodable { } public static func symbols() -> [String] { - let i = try! CapturePrice(array: Array(repeating: "", count: 82)) + let i = try! CapturePrice(array: Array(repeating: "", count: 82), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -1097,9 +1097,9 @@ public struct MinutePriceResult: Codable { return stockBusinessDate + stockConclusionTime } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 8 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 8) } self.stockBusinessDate = array[0] self.stockConclusionTime = array[1] @@ -1112,7 +1112,7 @@ public struct MinutePriceResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputPrice(array: Array(repeating: "", count: 8)) + let i = try! OutputPrice(array: Array(repeating: "", count: 8), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -1398,9 +1398,9 @@ public struct PeriodPriceResult: Codable { case revaluationIssueReason = "revl_issu_reas" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 13 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 13) } self.stockBusinessDate = array[0] self.stockClosingPrice = array[1] @@ -1418,7 +1418,7 @@ public struct PeriodPriceResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputPrice(array: Array(repeating: "", count: 13)) + let i = try! OutputPrice(array: Array(repeating: "", count: 13), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } diff --git a/KissMe/Sources/Domestic/DomesticStockResult.swift b/KissMe/Sources/Domestic/DomesticStockResult.swift index 48aefad..681e02d 100644 --- a/KissMe/Sources/Domestic/DomesticStockResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockResult.swift @@ -195,9 +195,9 @@ public struct BalanceResult: Codable { case stockLoanPrice = "stck_loan_unpr" } - init(array: [String]) throws { + init(array: [String], source: String.SubSequence) throws { guard array.count == 26 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 26) } self.productNo = array[0] self.productName = array[1] @@ -228,7 +228,7 @@ public struct BalanceResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputStock(array: Array(repeating: "", count: 26)) + let i = try! OutputStock(array: Array(repeating: "", count: 26), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -337,9 +337,9 @@ public struct BalanceResult: Codable { case assetFluctuationRate = "asst_icdc_erng_rt" } - init(array: [String]) throws { + init(array: [String], source: String.SubSequence) throws { guard array.count == 24 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 24) } self.depositTotalAmount = array[0] self.nextDayCalcAmount = array[1] @@ -368,7 +368,7 @@ public struct BalanceResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputAmount(array: Array(repeating: "", count: 24)) + let i = try! OutputAmount(array: Array(repeating: "", count: 24), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } diff --git a/KissMe/Sources/Domestic/DomesticStockSearchResult.swift b/KissMe/Sources/Domestic/DomesticStockSearchResult.swift index baaeaa8..ce7c5cb 100644 --- a/KissMe/Sources/Domestic/DomesticStockSearchResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockSearchResult.swift @@ -103,9 +103,9 @@ public struct VolumeRankResult: Codable { case accumulatedTradingAmount = "acml_tr_pbmn" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 19 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 19) } self.htsProductName = array[0] self.shortProductNo = array[1] @@ -129,7 +129,7 @@ public struct VolumeRankResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputDetail(array: Array(repeating: "", count: 19)) + let i = try! OutputDetail(array: Array(repeating: "", count: 19), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -188,9 +188,9 @@ public struct HolidyResult: Codable { case settlementDay = "sttl_day_yn" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 6 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 6) } self.baseDate = array[0] self.weekday = WeekdayDivision(rawValue: array[1])! @@ -201,7 +201,7 @@ public struct HolidyResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputDetail(array: Array(repeating: "", count: 22)) + let i = try! OutputDetail(array: Array(repeating: "", count: 22), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -327,9 +327,9 @@ public struct InvestorVolumeResult: Codable { case organizationSellingTradingAmount = "orgn_seln_tr_pbmn" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 22 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 22) } self.stockBusinessDate = array[0] self.stockClosingPrice = array[1] @@ -356,7 +356,7 @@ public struct InvestorVolumeResult: Codable { } public static func symbols() -> [String] { - let i = try! OutputDetail(array: Array(repeating: "", count: 22)) + let i = try! OutputDetail(array: Array(repeating: "", count: 22), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } @@ -488,9 +488,9 @@ public struct ForeignOrganizationVolumeResult: Codable { case etcCorporationNetBuyingTradingAmount = "etc_corp_ntby_tr_pbmn" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 26 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 26) } self.htsProductName = array[0] self.shortProductNo = array[1] diff --git a/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift b/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift index 24883c6..7552860 100644 --- a/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift +++ b/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift @@ -117,9 +117,9 @@ extension DomesticShop { case corporationNo = "crno" } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 6 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 6) } self.baseDate = array[0] @@ -134,7 +134,7 @@ extension DomesticShop { } public static func symbols() -> [String] { - let i = try! Item(array: Array(repeating: "", count: 6)) + let i = try! Item(array: Array(repeating: "", count: 6), source: #function) return Mirror(reflecting: i).children.compactMap { $0.label } } diff --git a/KissMe/Sources/Domestic/ShortSelling/DomesticShortSelling.swift b/KissMe/Sources/Domestic/ShortSelling/DomesticShortSelling.swift index bbf46ce..fb6c3fa 100644 --- a/KissMe/Sources/Domestic/ShortSelling/DomesticShortSelling.swift +++ b/KissMe/Sources/Domestic/ShortSelling/DomesticShortSelling.swift @@ -155,9 +155,9 @@ extension DomesticExtra { self.shortSellingBalanceRatio = try container.decode(String.self, forKey: DomesticExtra.ShortSellingBalanceResult.OutBlock.CodingKeys.shortSellingBalanceRatio) } - public init(array: [String]) throws { + public init(array: [String], source: String.SubSequence) throws { guard array.count == 6 else { - throw GeneralError.incorrectArrayItems + throw GeneralError.incorrectArrayItems(String(source), array.count, 6) } self.stockBusinessDate = array[0] self.shortSellingBalanceQuantity = array[1] diff --git a/KissMeConsole/Sources/KissConsole+CSV.swift b/KissMeConsole/Sources/KissConsole+CSV.swift index e200d3e..8ebd701 100644 --- a/KissMeConsole/Sources/KissConsole+CSV.swift +++ b/KissMeConsole/Sources/KissConsole+CSV.swift @@ -87,53 +87,6 @@ extension KissConsole { } -extension KissConsole { - - func loadShop(_ profile: Bool = false) { - let appTime1 = Date.appTime - guard let stringCsv = try? String(contentsOfFile: KissConsole.shopProductsUrl.path) else { - return - } - - let appTime2 = Date.appTime - if profile { - print("\tloading file \(appTime2 - appTime1) elapsed") - } - - var products = [String: [DomesticShop.Product]]() - let rows = stringCsv.split(separator: "\n") - - let appTime3 = Date.appTime - if profile { - print("\trow split \(appTime3 - appTime2) elapsed") - } - - for (i, row) in rows.enumerated() { - let array = row.split(separator: ",").map { String($0) } - if i == 0, array[0] == "baseDate" { - continue - } - - let product = try! DomesticShop.Product(array: array) - if let _ = products[product.itemName] { - products[product.itemName]!.append(product) - } - else { - products[product.itemName] = [product] - } - } - let appTime4 = Date.appTime - if profile { - print("\tparse product \(appTime4 - appTime3) elapsed") - } - - setProducts(products) - let totalCount = products.reduce(0, { $0 + $1.value.count }) - print("load products \(totalCount) with \(products.count) key") - } -} - - extension KissConsole { enum CandleValidation: CustomStringConvertible { @@ -187,7 +140,7 @@ extension KissConsole { var candles = [Domestic.Candle]() let rows = stringCsv.split(separator: "\n") for (i, row) in rows.enumerated() { - let array = row.split(separator: ",").map { String($0) } + let array = row.split(separator: ",", omittingEmptySubsequences: false).map { String($0) } if i == 0 { if array.count != 8 { return .invalidCsvHeader @@ -195,7 +148,7 @@ extension KissConsole { continue } - let candle = try! Domestic.Candle(array: array) + let candle = try! Domestic.Candle(array: array, source: row) candles.append(candle) } diff --git a/KissMeConsole/Sources/KissConsole+Price.swift b/KissMeConsole/Sources/KissConsole+Price.swift index 6e58b91..13c6ef6 100644 --- a/KissMeConsole/Sources/KissConsole+Price.swift +++ b/KissMeConsole/Sources/KissConsole+Price.swift @@ -71,7 +71,7 @@ extension KissConsole { /// -29일씩 이전으로 돌아가면서, 마지막으로 csv 로 저장했던 날짜를 찾는다. while startDate < backDate { - let day = backDate.yyyyMM + "01" + let day = backDate.yyyyMM01 let fileUrl = KissConsole.shortsFileUrl(productNo: productNo, day: day) guard let _ = fileUrl.isFileExists else { backDate = backDate.addingTimeInterval(-29 * SecondsForOneDay) diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index 7edbfb8..81c911b 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -9,18 +9,11 @@ import Foundation import KissMe -class KissConsole { +class KissConsole: KissMe.ShopContext { private var credential: Credential? = nil var account: KissAccount? = nil private var shop: KissShop? = nil - private var productsLock = NSLock() - /// 전체 종목 정보 - private var products = [String: [DomesticShop.Product]]() - - /// 현재 기본으로 선택된 productNo - private var currentShortCode: String? - /// 현재 candle 파일로 저장 중인 productNo var currentCandleShortCode: String? @@ -118,12 +111,14 @@ class KissConsole { account != nil } - init() { + override init() { let jsonUrl = URL.currentDirectory().appending(path: "shop-server.json") shop = try? KissShop(jsonUrl: jsonUrl) KissConsole.createSubpath("log") KissConsole.createSubpath("data") + + super.init() lastLogin() loadLocalName() @@ -258,52 +253,6 @@ extension KissConsole { try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true) } - func setProducts(_ products: [String: [DomesticShop.Product]]) { - productsLock.lock() - self.products = products - productsLock.unlock() - } - - private func getProducts(similarName: String) -> [String: [DomesticShop.Product]]? { - productsLock.lock() - defer { - productsLock.unlock() - } - return products.filter { $0.key.contains(similarName) } - //return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) } - } - - private func getProduct(isin: String) -> DomesticShop.Product? { - productsLock.lock() - defer { - productsLock.unlock() - } - - return products.compactMap { $0.value.first(where: { $0.isinCode == isin }) }.first - } - - func getProduct(shortCode: String) -> DomesticShop.Product? { - productsLock.lock() - defer { - productsLock.unlock() - } - - return products.compactMap { $0.value.first(where: { $0.shortCode == shortCode }) }.first - } - - private func getAllProducts() -> [DomesticShop.Product] { - productsLock.lock() - defer { - productsLock.unlock() - } - - var all = [DomesticShop.Product]() - for items in products.values { - all.append(contentsOf: items) - } - return all - } - private func lastLogin() { let profile = KissProfile() guard let isMock = profile.isMock else { @@ -332,15 +281,6 @@ extension KissConsole { private func loadLocalName() { LocalContext.shared.load(KissConsole.localNamesUrl) } - - func setCurrent(productNo: String) { - productsLock.lock() - currentShortCode = productNo - productsLock.unlock() - - let productName = getProduct(shortCode: productNo)?.itemName ?? "" - print("current product \(productNo) \(productName)") - } } @@ -695,8 +635,9 @@ extension KissConsole { private func onCandleDay(_ args: [String]) { - if args.count == 1, args[0] == "all" { - onCandleDayAll() + if args.count >= 1, args[0] == "all" { + let otherArgs = args[1...].map { String($0) } + onCandleDayAll(otherArgs) return } @@ -716,7 +657,9 @@ extension KissConsole { } - private func onCandleDayAll() { + private func onCandleDayAll(_ args: [String]) { + let resume: Bool = (args.count == 1 && args[0] == "resume") + let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) @@ -743,8 +686,9 @@ extension KissConsole { private func onCandleWeek(_ args: [String]) { - if args.count == 1, args[0] == "all" { - onCandleWeekAll() + if args.count >= 1, args[0] == "all" { + let otherArgs = args[1...].map { String($0) } + onCandleWeekAll(otherArgs) return } @@ -764,7 +708,9 @@ extension KissConsole { } - private func onCandleWeekAll() { + private func onCandleWeekAll(_ args: [String]) { + let resume: Bool = (args.count == 1 && args[0] == "resume") + let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) @@ -900,7 +846,7 @@ extension KissConsole { private func onLoadShop() async { return await withUnsafeContinuation { continuation in - self.loadShop() + self.loadShop(url: KissConsole.shopProductsUrl) continuation.resume() } } @@ -1114,7 +1060,7 @@ extension KissConsole { var curNames = try! [LocalName].readCsv(fromFile: nameUrl, verifyHeader: true) for name in newNames { if false == curNames.contains(where: { $0.fieldName == name }) { - let item = try! LocalName(array: [name, ""]) + let item = try! LocalName(array: [name, ""], source: "") curNames.append(item) addedNameCount += 1 } diff --git a/KissMeConsole/Sources/main.swift b/KissMeConsole/Sources/main.swift index 59b95cf..ae95dee 100644 --- a/KissMeConsole/Sources/main.swift +++ b/KissMeConsole/Sources/main.swift @@ -8,3 +8,17 @@ import Foundation KissConsole().run() + + +// 액면가가 1,000원 이상인 종목들 추려보자. (미챠...) +// 액면가가 넘어가면, prices.csv 에 저장된 comma 값을 다시 수정해야 함. +/* +import KissMe + +let path = URL(filePath: "/Users/ened/Kiss/KissMe/bin/data/065350/price/prices.csv") +let data = try [CapturePrice].readCsv(fromFile: path, verifyHeader: true) + +if let last = data.last { + print(last) +} +*/ diff --git a/KissMeIndex/Sources/KissIndex+0002.swift b/KissMeIndex/Sources/KissIndex+0002.swift index 22dd95a..6d6cf0f 100644 --- a/KissMeIndex/Sources/KissIndex+0002.swift +++ b/KissMeIndex/Sources/KissIndex+0002.swift @@ -6,12 +6,131 @@ // import Foundation +import KissMe extension KissIndex { func indexSet_0002(date: Date, config: String?, kmi: KissIndexType) { - // TODO: work + if productsCount == 0 { + loadShop(url: KissIndex.shopProductsUrl) + } + let semaphore = DispatchSemaphore(value: 0) + Task { +// var scoreMap = [String: Int]() + + do { + let shorts = try await collectShorts(date: date) + print(shorts.count) + + let prices = try await collectPrices(date: date) + print(prices.count) + } catch { + print(error) + } + + semaphore.signal() + } + semaphore.wait() + } + + + private func collectShorts(date: Date) async throws -> [DomesticExtra.Shorts] { + let shorts = try await withThrowingTaskGroup(of: DomesticExtra.Shorts?.self, returning: [DomesticExtra.Shorts].self) { taskGroup in + let all = getAllProducts() + let yyyyMMdd = date.yyyyMMdd + + for item in all { + taskGroup.addTask { + let shortsUrl = KissIndex.pickNearShortsUrl(productNo: item.shortCode, date: date) + + let shorts = try [DomesticExtra.Shorts].readCsv(fromFile: shortsUrl) + let targetShorts = shorts.filter { $0.stockBusinessDate == yyyyMMdd } + + /// 공매도 잔고 비중 (1%) 이상 종목 리스트 + if let aShorts = targetShorts.first, let ratio = Double(aShorts.shortSellingBalanceRatio), ratio >= 0.01 { + return aShorts + } + return nil + } + } + + var taskResult = [DomesticExtra.Shorts]() + for try await result in taskGroup.compactMap( { $0 }) { + taskResult.append(result) + } + return taskResult + } + return shorts + } + + + private func collectPrices(date: Date) async throws -> [CapturePrice] { + let prices = try await withThrowingTaskGroup(of: CapturePrice?.self, returning: [CapturePrice].self) { taskGroup in + let all = getAllProducts() + let yyyyMMdd = date.yyyyMMdd + let dateHHmmss = date.HHmmss + + for item in all { + taskGroup.addTask { + let pricesUrl = KissIndex.pickNearPricesUrl(productNo: item.shortCode, date: date) + let prices = try [CapturePrice].readCsv(fromFile: pricesUrl) + let targetPrices = prices.filter { $0.stockBusinessDate == yyyyMMdd && $0.captureTime <= dateHHmmss } + .sorted(by: { dateHHmmss.diffSecondsTwoHHmmss($0.captureTime) < dateHHmmss.diffSecondsTwoHHmmss($1.captureTime) }) + + if let price = targetPrices.first, let quantity = Int(price.lastShortSellingConclusionQuantity), quantity > 0 { + /// 최종 공매도 체결 수량이 잔고량에 비해서 높으면? + /// lastShortSellingConclusionQuantity + return price + } + return nil + } + } + + var taskResult = [CapturePrice]() + for try await result in taskGroup.compactMap( { $0 }) { + taskResult.append(result) + } + return taskResult + } + return prices + } + + + private static var shopProductsUrl: URL { + URL.currentDirectory().appending(path: "data/shop-products.csv") + } + + private static func pickNearShortsUrl(productNo: String, date: Date) -> URL { + let subPath = "data/\(productNo)/shorts" + let monthFile = "shorts-\(date.yyyyMM01).csv" + + return URL.currentDirectory().appending(path: "\(subPath)/\(monthFile)") + } + + private static func pickNearPricesUrl(productNo: String, date: Date) -> URL { + let subPath = "data/\(productNo)/price" + let priceFile = "prices.csv" + + // TODO: work month file + //let monthFile = "prices-\(date.yyyyMM01).csv" + + return URL.currentDirectory().appending(path: "\(subPath)/\(priceFile)") + } +} + + +extension String { + func diffSecondsTwoHHmmss(_ another: String) -> TimeInterval { + guard let (hour, min, sec) = self.HHmmss else { + return Double.greatestFiniteMagnitude + } + guard let (dHour, dMin, dSec) = another.HHmmss else { + return Double.greatestFiniteMagnitude + } + let seconds = (hour * 60 * 60 + min * 60 + sec) + let dSeconds = (dHour * 60 * 60 + dMin * 60 + dSec) + return TimeInterval(seconds - dSeconds) } } diff --git a/KissMeIndex/Sources/KissIndex+0005.swift b/KissMeIndex/Sources/KissIndex+0005.swift index f44e01f..ade24c1 100644 --- a/KissMeIndex/Sources/KissIndex+0005.swift +++ b/KissMeIndex/Sources/KissIndex+0005.swift @@ -12,8 +12,7 @@ import KissMe extension KissIndex { func indexSet_0005(date: Date, config: String?, kmi: KissIndexType) { - //let belongs: [BelongClassCode] = [.averageVolume, .volumeIncreaseRate, .averageVolumeTurnoverRate, .transactionValue, .averageTransactionValueTurnoverRate] - let belongs: [BelongClassCode] = [.averageVolume] + let belongs: [BelongClassCode] = [.averageVolume, .volumeIncreaseRate, .averageVolumeTurnoverRate, .transactionValue, .averageTransactionValueTurnoverRate] do { var scoreMap = [String: Int]() @@ -51,7 +50,7 @@ extension KissIndex { } - static func pickNearTopProductsUrl(_ belong: BelongClassCode, date: Date) throws -> URL { + private static func pickNearTopProductsUrl(_ belong: BelongClassCode, date: Date) throws -> URL { let subPath = "data/top30/\(date.yyyyMMdd)" let dayFile = "top30-\(belong.fileBelong)-\(date.yyyyMMdd)-" @@ -106,6 +105,13 @@ extension String { return TimeInterval(hour * 60 * 60 + min * 60 + sec) } + var HHmmssBySeconds: TimeInterval? { + guard let (hour, min, sec) = HHmmss else { + return nil + } + return TimeInterval(hour * 60 * 60 + min * 60 + sec) + } + func diffSecondsTwoCsvHHmmss(_ another: String) -> TimeInterval { (csvHHmmssBySeconds ?? 0) - (another.csvHHmmssBySeconds ?? 0) } diff --git a/KissMeIndex/Sources/KissIndex.swift b/KissMeIndex/Sources/KissIndex.swift index c1ab547..fe8630b 100644 --- a/KissMeIndex/Sources/KissIndex.swift +++ b/KissMeIndex/Sources/KissIndex.swift @@ -19,7 +19,7 @@ enum KissIndexType: String { } -class KissIndex { +class KissIndex: KissMe.ShopContext { func run() { guard CommandLine.argc >= 4 else { diff --git a/README.md b/README.md index adf2af8..7697e65 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) `candle [PNO]` | 종목의 분봉 열람. PNO 은 생략 가능. **data/(PNO)/min/candle-(yyyyMMdd).csv** 파일로 저장. `candle all [resume]` | 모든 종목의 분봉 열람. cron job 으로 돌리기 위해서 추가. **data/(PNO)/min/candle-(yyyyMMdd).csv** 파일로 저장. (resume) 을 기입하면, 이미 받은 파일은 검사하여, 데이터에 오류가 있으면 다시 받고 오류가 없으면 새롭게 열람하지 않음. `candle day [PNO]` | 종목의 최근 250일 동안의 일봉 열람. PNO 은 생략 가능. **data/(PNO)/day/candle-(yyyyMMdd).csv** 파일로 저장. -`candle day all` | 모든 종목의 최근 250일 동안의 일봉 열람. cron job 으로 오전 장이 시작전에 미리 수집. **data/(PNO)/day/candle-(yyyyMMdd).csv** 파일로 저장. +`candle day all [resume]` | 모든 종목의 최근 250일 동안의 일봉 열람. cron job 으로 오전 장이 시작전에 미리 수집. **data/(PNO)/day/candle-(yyyyMMdd).csv** 파일로 저장. `candle week [PNO]` | 종목의 최근 52주 동안의 주봉 열람. PNO 은 생략 가능. **data/(PNO)/week/candle-(yyyyMMdd).csv** 파일로 저장. -`candle week all` | 모든 종목의 최근 52주 동안의 주봉 열람. cron job 으로 오전 장이 시작전에 미리 수집. **data/(PNO)/week/candle-(yyyyMMdd).csv** 파일로 저장. +`candle week all [resume]` | 모든 종목의 최근 52주 동안의 주봉 열람. cron job 으로 오전 장이 시작전에 미리 수집. **data/(PNO)/week/candle-(yyyyMMdd).csv** 파일로 저장. `candle validate (기간)` | (기간) 타입의 모든 csv 파일에 대해서 데이터가 유효한지 검사. (기간) 으로는 **min**, **day**, **week** 을 지정하고, 생략되면 **min** 으로 간주. `investor [PNO]` | 종목의 투자자 거래량 열람. PNO 은 생략 가능. **data/(PNO)/investor/investor-(yyyyMMdd).csv** 파일로 저장. `investor all` | 모든 종목의 투자자 거래량 열람. **data/(PNO)/investor/investor-(yyyyMMdd).csv** 파일로 저장. diff --git a/bin/data b/bin/data index 659b8ff..8f92a1c 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit 659b8ff1698105c59ebda2ed94e289402a76164c +Subproject commit 8f92a1c7b8857dd05d73317de10b04c3f8f14d40 diff --git a/projects/macos/KissMe.xcodeproj/project.pbxproj b/projects/macos/KissMe.xcodeproj/project.pbxproj index e524571..8d27c33 100644 --- a/projects/macos/KissMe.xcodeproj/project.pbxproj +++ b/projects/macos/KissMe.xcodeproj/project.pbxproj @@ -36,8 +36,9 @@ 3435A7F72A35D82000D604F1 /* DomesticShortSelling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3435A7F62A35D82000D604F1 /* DomesticShortSelling.swift */; }; 349C26AB2A1EAE2400F3EC91 /* KissProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C26AA2A1EAE2400F3EC91 /* KissProfile.swift */; }; 34D3680F2A2AA0BE005E6756 /* PropertyIterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D3680E2A2AA0BE005E6756 /* PropertyIterable.swift */; }; - 34F190092A418E130068C697 /* LocalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F190082A418E130068C697 /* LocalContext.swift */; }; 34F1900C2A41982A0068C697 /* IndexResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F1900B2A41982A0068C697 /* IndexResult.swift */; }; + 34F1900F2A426D150068C697 /* ShopContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F1900E2A426D150068C697 /* ShopContext.swift */; }; + 34F190112A4394EB0068C697 /* LocalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F190102A4394EB0068C697 /* LocalContext.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -81,8 +82,9 @@ 3435A7F62A35D82000D604F1 /* DomesticShortSelling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomesticShortSelling.swift; sourceTree = ""; }; 349C26AA2A1EAE2400F3EC91 /* KissProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KissProfile.swift; sourceTree = ""; }; 34D3680E2A2AA0BE005E6756 /* PropertyIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyIterable.swift; sourceTree = ""; }; - 34F190082A418E130068C697 /* LocalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalContext.swift; sourceTree = ""; }; 34F1900B2A41982A0068C697 /* IndexResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexResult.swift; sourceTree = ""; }; + 34F1900E2A426D150068C697 /* ShopContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopContext.swift; sourceTree = ""; }; + 34F190102A4394EB0068C697 /* LocalContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalContext.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -125,6 +127,7 @@ 341F5EAD2A0A80EC00962D48 /* KissMe */ = { isa = PBXGroup; children = ( + 34F1900D2A426C1C0068C697 /* Context */, 34F1900A2A41981A0068C697 /* Index */, 341F5EF32A0F88AC00962D48 /* Common */, 341F5EEA2A0F882300962D48 /* Foreign */, @@ -195,7 +198,6 @@ 341F5F022A11A2BC00962D48 /* Credential.swift */, 341F5F062A14634F00962D48 /* Foundation+Extensions.swift */, 34D3680E2A2AA0BE005E6756 /* PropertyIterable.swift */, - 34F190082A418E130068C697 /* LocalContext.swift */, ); path = Common; sourceTree = ""; @@ -224,6 +226,15 @@ path = Index; sourceTree = ""; }; + 34F1900D2A426C1C0068C697 /* Context */ = { + isa = PBXGroup; + children = ( + 34F190102A4394EB0068C697 /* LocalContext.swift */, + 34F1900E2A426D150068C697 /* ShopContext.swift */, + ); + path = Context; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -342,16 +353,17 @@ 341F5EFD2A10931B00962D48 /* DomesticStockSearch.swift in Sources */, 341F5EE52A0F3EF400962D48 /* DomesticStock.swift in Sources */, 341F5EF72A0F8B0500962D48 /* DomesticStockResult.swift in Sources */, + 34F1900F2A426D150068C697 /* ShopContext.swift in Sources */, 341F5F0F2A15223A00962D48 /* SeibroRequest.swift in Sources */, 341F5EF02A0F886600962D48 /* ForeignFutures.swift in Sources */, 34F1900C2A41982A0068C697 /* IndexResult.swift in Sources */, 341F5EEC2A0F883900962D48 /* ForeignStock.swift in Sources */, + 34F190112A4394EB0068C697 /* LocalContext.swift in Sources */, 341F5EFF2A10955D00962D48 /* OrderRequest.swift in Sources */, 341F5EE92A0F87FB00962D48 /* DomesticStockPrice.swift in Sources */, 341F5EEE2A0F884300962D48 /* ForeignStockPrice.swift in Sources */, 341F5EDE2A0F300100962D48 /* Request.swift in Sources */, 349C26AB2A1EAE2400F3EC91 /* KissProfile.swift in Sources */, - 34F190092A418E130068C697 /* LocalContext.swift in Sources */, 341F5F012A11155100962D48 /* DomesticStockSearchResult.swift in Sources */, 341F5F142A16CD7A00962D48 /* DomesticShopProduct.swift in Sources */, 341F5EF22A0F887200962D48 /* DomesticFutures.swift in Sources */, diff --git a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme index 2763239..c2c08d6 100644 --- a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme +++ b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme @@ -53,7 +53,7 @@