// // KissConsole.swift // KissMeConsole // // Created by ened-book-m1 on 2023/05/17. // import Foundation import KissMe class KissConsole: KissMe.ShopContext { /// 인증 관련 json 정보 private var credential: Credential? = nil /// 한국투자증권 개인 계정 var account: KissAccount? = nil /// 주식시장 거래 상품 목록들 private var shop: KissShop? = nil /// 현재 candle 파일로 저장 중인 productNo var currentCandleShortCode: String? /// CSV 파일을 저장할 때, field name 에 대해서 한글(true) 또는 영문(false)로 기록할지 설정 var localized: Bool = false var indexContext: IndexContext let maxCandleDay: Int = 250 // 005930: 삼성전자 static let defaultProductNo: String = "005930" private enum KissCommand: String { case quit = "quit" // 로그인 case loginMock = "login mock" case loginReal = "login real" case logout = "logout" // 랭킹 case top = "top" case topAll = "top all" // 매매 case buy = "buy" case buyCheck = "buy check" case sell = "sell" case cancel = "cancel" case modify = "modify" // 보유 종목 case openBag = "open bag" // 종목 시세 case now = "now" case nowAll = "now all" case candle = "candle" case candleAll = "candle all" case candleDay = "candle day" case candleWeek = "candle week" case candleValidate = "candle validate" // 투자자 열람 case investor = "investor" case investorAll = "investor all" // 공매도 열람 case shorts = "shorts" case shortsAll = "shorts all" // KRX 지수 열람 case index = "index" case indexPortfolio = "index portfolio" // 종목 상품 열람 case loadShop = "load shop" case updateShop = "update shop" case look = "look" // 지수 상품 열람 case loadIndex = "load index" case updateIndex = "update index" // 휴장일 case holiday = "holiday" // 진열 종목 (시스템에서 제공하는 추천 리스트) case showcase = "showcase" // 관심 종목 case loves = "loves" // 열람 case love = "love" // love nuts.1 ISCD case hate = "hate" // hate nuts.1 ISCD // 기타 case localizeNames = "localize names" case localizeOnOff = "localize" case test = "test" // 웹소켓 case real = "real" // DB1 case db = "db" // 뉴스 case news = "news" case newsAll = "news all" var needLogin: Bool { switch self { case .quit, .loginMock, .loginReal: return false case .logout, .top, .topAll, .buy, .buyCheck, .sell, .cancel, .modify: return true case .openBag: return true case .now, .nowAll, .candle, .candleAll, .candleDay, .candleWeek: return true case .candleValidate: return false case .investor, .investorAll: return true case .shorts, .shortsAll, .index, .indexPortfolio: return false case .loadShop, .updateShop, .look, .holiday: return false case .loadIndex, .updateIndex: return false case .showcase: return false case .loves, .love, .hate, .localizeNames, .localizeOnOff: return false case .test: return true case .real: return true case .db: return false case .news, .newsAll: return false } } } private var isLogined: Bool { account != nil } override init() { let jsonUrl = URL.currentDirectory().appending(path: "shop-server.json") shop = try? KissShop(jsonUrl: jsonUrl) KissConsole.createSubpath("log") KissConsole.createSubpath("data") indexContext = IndexContext() super.init() lastLogin() loadLocalName() let semaphore = DispatchSemaphore(value: 0) Task { await onLoadShop() await onLoadIndex() semaphore.signal() } semaphore.wait() setCurrent(productNo: KissConsole.defaultProductNo) } private func getCommand(_ line: String) -> (KissCommand?, [String]) { let args = line.split(separator: " ") let double = args.prefix(upTo: min(2, args.count)).joined(separator: " ") if let cmd = KissCommand(rawValue: double) { return (cmd, args.suffixStrings(from: 2)) } let single = args.prefix(upTo: min(1, args.count)).joined(separator: " ") if let cmd = KissCommand(rawValue: single) { return (cmd, args.suffixStrings(from: 1)) } return (nil, []) } func run() { guard CommandLine.argc == 1 else { let line = CommandLine.arguments.suffix(Int(CommandLine.argc)-1).joined(separator: " ") let (cmd, args) = getCommand(line) if cmd?.needLogin == true { guard isLogined else { print("Need to login") exit(1) } } let semaphore = DispatchSemaphore(value: 0) Task { let success = await run(command: cmd, args: args, line: line) if !success { exit(2) } semaphore.signal() } semaphore.wait() return } print("Enter command:") let semaphore = DispatchSemaphore(value: 0) var loop = true while loop { guard let line = readLine(strippingNewline: true) else { continue } let (cmd, args) = getCommand(line) if cmd?.needLogin == true { guard isLogined else { print("Need to login") continue } } Task { _ = await run(command: cmd, args: args, line: line) semaphore.signal() } semaphore.wait() loop = (cmd != .quit) } } private func run(command: KissCommand?, args: [String], line: String) async -> Bool { switch command { case .quit: break case .loginMock: await onLogin(isMock: true) case .loginReal: await onLogin(isMock: false) case .logout: await onLogout() case .top: await onTop(args) case .topAll: onTopAll() case .buy: await onBuy(args) case .buyCheck: await onBuyCheck(args) case .sell: await onSell(args) case .cancel: await onCancel(args) case .modify: await onModify(args) case .openBag: await onOpenBag() case .now: await onNow(args) case .nowAll: onNowAll(args) case .candle: await onCandle(args) case .candleAll: onCancleAll(args) case .candleDay: onCandleDay(args) case .candleWeek: onCandleWeek(args) case .candleValidate: onCandleValidate(args) case .investor: await onInvestor(args) case .investorAll: onInvestorAll(args) case .shorts: await onShorts(args) case .shortsAll: onShortsAll(args) case .index: await onIndex(args) case .indexPortfolio: await onIndexPortfolio(args) case .loadShop: await onLoadShop() case .updateShop: await onUpdateShop() case .look: await onLook(args) case .holiday: await onHoliday(args) case .loadIndex: await onLoadIndex() case .updateIndex: await onUpdateIndex() case .showcase: await onShowcase() case .loves: await onLoves() case .love: await onLove(args) case .hate: await onHate(args) case .localizeNames: await onLocalizeNames() case .localizeOnOff: await onLocalizeOnOff(args) case .test: onTest(args) case .real: await onReal(args) case .db: await onDB(args) case .news: await onNews(args) case .newsAll: onNewsAll(args) default: print("Unknown command: \(line)") return false } return true } } extension KissConsole { static func createSubpath(_ name: String) { let subPath = URL.currentDirectory().appending(path: name) try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true) } private func lastLogin() { let profile = KissProfile() guard let isMock = profile.isMock else { return } do { let lastCredential = try KissCredential(isMock: isMock) let lastAccount = KissAccount(credential: lastCredential) guard let expiredAt = lastAccount.accessTokenExpiredDate else { return } if expiredAt > Date() { credential = lastCredential account = lastAccount let expiredAt = account?.accessTokenExpiredDate?.yyyyMMdd_HHmmss_forTime ?? "" print("resume \(isMock ? "mock login": "real login") expired at \(expiredAt)") } } catch { print(error) } } private func loadLocalName() { LocalContext.shared.load(KissConsole.localNamesUrl) } } extension KissConsole { private func onLogin(isMock: Bool) async { guard !isLogined else { print("Already loginged") return } do { credential = try KissCredential(isMock: isMock) account = KissAccount(credential: credential!) if try await account!.login() { print("Success") } } catch { print(error) } } private func onLogout() async { do { _ = try await account?.logout() credential = nil account = nil print("Success") } catch { print(error) } } private func onTop(_ args: [String]) async { var belongCode = "0" if args.count == 1, let code = Int(args[0]) { belongCode = String(code) } guard let belongClass = BelongClassCode(rawValue: belongCode) else { print("Incorrect belong type: \(belongCode)") return } print("TOP: \(belongClass.description)") let option = RankingOption(divisionClass: .all, belongClass: belongClass) do { let curDate = Date() let rank = try await account!.getVolumeRanking(option: option) guard let output = rank.output else { print("Error \(rank.messageCode) \(rank.message)") return } print("랭킹 단축상품코드 상품명 가격 평균거래량 누적거래대금") for item in output { print("\(item.dataRank) \(item.shortProductNo) \(item.htsProductName.maxSpace(20)) \(item.currentStockPrice.maxSpace(10, digitBy: 3)) \(item.averageVolume.maxSpace(15, digitBy: 3)) \(item.accumulatedTradingAmount.maxSpace(25, digitBy: 3))") } let fileUrl = KissConsole.topProductsUrl(belongClass, date: curDate) try output.writeCsv(toFile: fileUrl, localized: localized) print("wrote \(fileUrl.lastPathComponent) with \(output.count)") } catch { print(error) } } private func onTopAll() { let belongs: [BelongClassCode] = [.averageVolume, .volumeIncreaseRate, .averageVolumeTurnoverRate, .transactionValue, .averageTransactionValueTurnoverRate] for belong in belongs { let option = RankingOption(divisionClass: .all, belongClass: belong) let semaphore = DispatchSemaphore(value: 0) Task { do { let curDate = Date() let rank = try await account!.getVolumeRanking(option: option) guard let output = rank.output else { print("Error \(rank.messageCode) \(rank.message)") return } let fileUrl = KissConsole.topProductsUrl(belong, date: curDate) try output.writeCsv(toFile: fileUrl, localized: localized) print("wrote \(fileUrl.lastPathComponent) with \(output.count)") try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredTopTPS) } catch { print(error) } semaphore.signal() } semaphore.wait() } print("FINISHED") } private func onBuy(_ args: [String]) async { guard args.count == 3 else { print("Missing buy paramters: buy (PNO) (PRICE) (QUANTITY)") return } let productNo = args[0] guard let product = getProduct(shortCode: productNo) else { print("No product \(productNo)") return } guard let price = Int(args[1]), (price == -8282 || price >= 100) else { print("Invalid price: \(args[1])") return } guard let quantity = Int(args[2]), (quantity == -82 || quantity > 0) else { print("Invalid quantity: \(args[2])") return } let division: OrderDivision = (price == -8282 ? .marketPrice: .limits) let contract = Contract(productNo: productNo, orderType: .buy, orderDivision: division, orderQuantity: quantity, orderPrice: price) do { let result = try await account!.orderStock(contract: contract) if let output = result.output { print("Success \(product.itemName) orderNo: \(output.orderNo) at \(output.orderTime)") } else { print("Failed \(result.resultCode) \(result.messageCode) \(result.message)") } } catch { print(error) } } private func onBuyCheck(_ args: [String]) async { guard args.count == 2 else { print("Missing buy check paramters: buy check (PNO) (PRICE)") return } let productNo = args[0] guard let product = getProduct(shortCode: productNo) else { print("No product \(productNo)") return } guard let price = Int(args[1]), price >= 100 else { print("Invalid price: \(args[1])") return } do { let result = try await account!.canOrderStock(productNo: productNo, division: .limits, price: price) if let output = result.output { print("Success \(product.itemName) \(output)") } else { print("Failed \(result.resultCode) \(result.messageCode) \(result.message)") } } catch { print(error) } } private func onSell(_ args: [String]) async { guard args.count == 3 else { print("Missing sell paramters: sell (PNO) (PRICE) (QUANTITY)") return } let productNo = args[0] guard let product = getProduct(shortCode: productNo) else { print("No product \(productNo)") return } guard let price = Int(args[1]), (price == -8282 || price >= 100) else { print("Invalid price: \(args[1])") return } guard let quantity = Int(args[2]), quantity > 0 else { print("Invalid quantity: \(args[2])") return } let division: OrderDivision = (price == -8282 ? .marketPrice: .limits) let contract = Contract(productNo: productNo, orderType: .sell, orderDivision: division, orderQuantity: quantity, orderPrice: price) do { let result = try await account!.orderStock(contract: contract) if let output = result.output { print("Success \(product.itemName) orderNo: \(output.orderNo) at \(output.orderTime)") } else { print("Failed \(result.resultCode) \(result.messageCode) \(result.message)") } } catch { print(error) } } private func onCancel(_ args: [String]) async { guard args.count == 3 else { print("Missing cancel paramters: cancel (PNO) (QUANTITY)") return } let productNo = args[0] guard let product = getProduct(shortCode: productNo) else { print("No product \(productNo)") return } let orderNo = args[1] guard orderNo.count >= 1 else { print("Invalid orderNo: \(args[1])") return } guard let quantity = Int(args[2]), (quantity == -82 || quantity > 0) else { print("Invalid quantity: \(args[2])") return } do { let cancel = ContractCancel(productNo: productNo, orderNo: orderNo, orderQuantity: (quantity == -82 ? 0: quantity)) let result = try await account!.cancelOrder(cancel: cancel) if let output = result.output { print("Success \(product.itemName) orderNo: \(output.orderNo) at \(output.orderTime)") } else { print("Failed \(result.resultCode) \(result.messageCode) \(result.message)") } } catch { print(error) } } private func onModify(_ args: [String]) async { // TODO: work } private func onOpenBag() async { do { let result = try await account!.getStockBalance() if let stocks = result.output1 { for item in stocks { print("\(item)") } let fileUrl = KissConsole.accountStocksUrl try stocks.writeCsv(toFile: fileUrl, localized: localized) print("wrote \(fileUrl.lastPathComponent) with \(stocks.count)") } if let amounts = result.output2 { for item in amounts { print("\(item)") } let fileUrl = KissConsole.accountAmountUrl try amounts.writeCsv(toFile: fileUrl, localized: localized) print("wrote \(fileUrl.lastPathComponent) with \(amounts.count)") } } catch { print(error) } } private func onNow(_ args: [String]) async { let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { print("Invalid productNo") return } let success = await getCurrentPrice(productNo: productNo) if success { setCurrent(productNo: productNo) } } private func onNowAll(_ args: [String]) { let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) Task { let holiday = try? await checkHoliday(Date()) if holiday == true { print("DONE today is holiday") return } let success = await getCurrentPrice(productNo: item.shortCode) #if DEBUG if !success { exit(-100) } #endif print("DONE \(success) \(item.shortCode)") semaphore.signal() } semaphore.wait() } print("FINISHED") } private func onCancleAll(_ args: [String]) { let semaphore = DispatchSemaphore(value: 0) Task { await KissContext.shared.update(resuming: false) if args.count == 1, args[0].lowercased() == "resume" { await KissContext.shared.update(resuming: true) } semaphore.signal() } semaphore.wait() let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) Task { let holiday = try? await checkHoliday(Date()) if holiday == true { print("DONE today is holiday") return } if await KissContext.shared.isResuming { let curDate = Date() let url = KissConsole.candleFileUrl(productNo: item.shortCode, period: .minute, day: curDate.yyyyMMdd) let r = validateCsv(filePriod: .minute, url: url) switch r { case .ok, .invalidFileName: return default: break } } let success = await getCandle(productNo: item.shortCode) print("DONE \(success) \(item.shortCode)") semaphore.signal() } semaphore.wait() } print("FINISHED") } private func onCandleDay(_ args: [String]) { if args.count >= 1, args[0] == "all" { let otherArgs = args[1...].map { String($0) } onCandleDayAll(otherArgs) return } let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { print("Invalid productNo") return } let semaphore = DispatchSemaphore(value: 0) Task { let success = await getCandle(productNo: productNo, period: .daily, count: maxCandleDay, startDate: nil) print("DONE \(success) \(productNo)") semaphore.signal() } semaphore.wait() } 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) Task { let startDate = (resume ? try? getLatestDateFromCandle(item.shortCode, period: .daily): nil) if startDate?.yyyyMMdd == Date().yyyyMMdd { print("DONE skipped \(item.shortCode)") semaphore.signal() return } let count: Int if let startDate = startDate { let daysBefore = ceil(startDate.distance(to: Date()) / SecondsForOneDay) count = Int(daysBefore) } else { count = maxCandleDay } let success = await getCandle(productNo: item.shortCode, period: .daily, count: count, startDate: startDate) print("DONE \(success) \(item.shortCode)") semaphore.signal() #if DEBUG //if !success { // exit(99) //} #endif } semaphore.wait() } print("FINISHED") } private func onCandleWeek(_ args: [String]) { if args.count >= 1, args[0] == "all" { let otherArgs = args[1...].map { String($0) } onCandleWeekAll(otherArgs) return } let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { print("Invalid productNo") return } let semaphore = DispatchSemaphore(value: 0) Task { let success = await getCandle(productNo: productNo, period: .weekly, count: 52, startDate: nil) print("DONE \(success) \(productNo)") semaphore.signal() } semaphore.wait() } 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) Task { let startDate = (resume ? try? getLatestDateFromCandle(item.shortCode, period: .weekly): nil) if startDate?.yyyyMMdd == Date().yyyyMMdd { print("DONE skipped \(item.shortCode)") semaphore.signal() return } let success = await getCandle(productNo: item.shortCode, period: .weekly, count: 52, startDate: startDate) print("DONE \(success) \(item.shortCode)") semaphore.signal() #if DEBUG //if !success { // exit(99) //} #endif } semaphore.wait() } print("FINISHED") } private func onCandleValidate(_ args: [String]) { let period: CandleFilePeriod? if args.count == 1 { period = CandleFilePeriod(rawValue: args[0]) } else { period = .minute } guard let period = period else { return } do { try validateAllCsvs(filePriod: period) } catch { print(error) } } private func onInvestor(_ args: [String]) async { let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { print("Invalid productNo") return } do { _ = try await getInvestor(productNo: productNo) } catch { print(error) } } private func onInvestorAll(_ args: [String]) { let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) Task { let holiday = try? await checkHoliday(Date()) if holiday == true { print("DONE today is holiday") return } do { let success = try await getInvestor(productNo: item.shortCode) print("DONE \(success) \(item.shortCode)") } catch { print(error) } semaphore.signal() } semaphore.wait() } print("FINISHED") } private func onShorts(_ args: [String]) async { let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { print("Invalid productNo") return } do { _ = try await getShorts(productNo: productNo, startAt: nil) } catch { print(error) } } private func onShortsAll(_ args: [String]) { let all = getAllProducts() for item in all { let semaphore = DispatchSemaphore(value: 0) Task { do { let startAt: Date? if args.count == 1, args[0].lowercased() == "resume" { startAt = getShortsLastDate(productNo: item.shortCode) } else { startAt = nil } let success = try await getShorts(productNo: item.shortCode, startAt: startAt) print("DONE \(success)") } catch { print(error) } semaphore.signal() } semaphore.wait() } print("FINISHED") } private func onIndex(_ args: [String]) async { let indexType: DomesticExtra.IndexType? = (args.isEmpty ? .krx: .init(rawValue: args[0])) guard let indexType = indexType else { print("Invalid KRX index type") return } do { let curDate = Date() if curDate.isBeforeMarketOpenning { print("Before market openning") return } let result = try await KissAccount.getIndexPrice(indexType: indexType, date: curDate) guard result.output.isEmpty == false else { print("empty result") return } let fileUrl = KissConsole.indexPriceFileUrl(date: result.currentDate, type: indexType) try result.output.writeCsv(toFile: fileUrl, localized: localized) print("DONE \(result.output.count) type: \(indexType.rawValue)") } catch { print(error) } } private func onIndexPortfolio(_ args: [String]) async { do { let indices = indexContext.getAllIndices() let date = Date() for index in indices { let result = try await KissAccount.getIndexPortfolio(indexId: index.index1Code, indexId2: index.index2Code, date: date) guard result.output.isEmpty == false else { print("empty result on \(index.indexFullCode)") continue } let fileUrl = KissConsole.indexPortfolioFileUrl(date: date, type: index.indexType, indexFullCode: index.indexFullCode) try result.output.writeCsv(toFile: fileUrl, localized: localized) print("DONE \(result.output.count) \(index.indexFullCode)") try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredIndexTPS) } } catch { print(error) } } private func onCandle(_ args: [String]) async { let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { print("Invalid productNo") return } _ = await getCandle(productNo: productNo) } private func onLoadShop() async { return await withUnsafeContinuation { continuation in self.loadShop(url: KissConsole.shopProductsUrl, loggable: true) continuation.resume() } } private func onUpdateShop() async { guard let _ = shop else { print("Invalid shop instance") return } var baseDate = Date() var shopItems = [DomesticShop.Product]() for _ in 0 ..< 7 { print("try to get shop at date \(baseDate.yyyyMMdd)") shopItems = await getAllProduct(baseDate: baseDate) if shopItems.isEmpty { let oneDaySeconds: TimeInterval = 60 * 60 * 24 baseDate = baseDate.addingTimeInterval(-oneDaySeconds) } else { break } } do { let fileUrl = KissConsole.shopProductsUrl try shopItems.writeCsv(toFile: fileUrl, localized: localized) print("wrote \(fileUrl.lastPathComponent) with \(shopItems.count)") } catch { print(error) } } private func onLoadIndex() async { return await withUnsafeContinuation { continuation in self.indexContext.loadIndex(url: KissConsole.indexProductsFileUrl(), loggable: true) continuation.resume() } } private func onUpdateIndex() async { do { let result = try await KissAccount.getAllIndices() if result.block.isEmpty == false { let fileUrl = KissConsole.indexProductsFileUrl() try result.block.writeCsv(toFile: fileUrl, localized: localized) } print("DONE \(result.block.count)") } catch { print(error) } } func getAllProduct(baseDate: Date) async -> [DomesticShop.Product] { var pageNo = 0 var shopItems = [DomesticShop.Product]() do { while true { pageNo += 1 print("get pageNo: \(pageNo)") let (totalCount, items) = try await shop!.getProduct(baseDate: baseDate, pageNo: pageNo) shopItems.append(contentsOf: items) print("got pageNo: \(pageNo) - \(shopItems.count)/\(totalCount)") if totalCount == 0 || shopItems.count >= totalCount { break } } } catch { print(error) } return shopItems } private func onLook(_ args: [String]) async { guard args.count >= 1 else { print("No target name") return } let productName = String(args[0]) //print(args, productName, "\(productName.count)") guard let items = getProducts(similarName: productName), items.isEmpty == false else { print("No products like \(productName)") return } for item in items { if let first = item.value.first { print("\tISIN: ", first.isinCode) print("\t상품명: ", item.key.maxSpace(20)) print("\t단축코드: ", first.shortCode) print("\t시장구분: ", first.marketCategory, "\t기준일자: ", first.baseDate) setCurrent(productNo: first.shortCode) } } } private func onHoliday(_ args: [String]) async { var date = Date() if args.count == 1, args[0].utf8.count == 8, let day = args[0].yyyyMMdd { date.change(year: day.0, month: day.1, day: day.2) } let targetDay = (Date().yyyyMMdd == date.yyyyMMdd ? "today": date.yyyyMMdd) do { let holiday = try await checkHoliday(date) if holiday { print("DONE \(targetDay) is holiday") } else { print("DONE \(targetDay) is business day") } } catch { print(error) } } private func onShowcase() async { // TODO: write } private func onLoves() async { guard let account = account else { return } let loves = account.getLoves() for tab in loves.sorted(by: { $0.key < $1.key }) { printLoveTab(tab.key, loves: tab.value) } } private func printLoveTab(_ key: String, loves: [KissProfile.Love]) { print("Page: \(key)") for item in loves { print(" \(item.isin) \(item.name)") } } private func onLove(_ args: [String]) async { guard let account = account else { return } guard args.count > 0 else { print("Invalid love\nlove ") return } if args.count == 1 { let key = args[0] guard let loves = account.getLoves(for: key) else { return } printLoveTab(key, loves: loves) } else if args.count == 2 { let keys = args[0].split(separator: ".").map { String($0) } let isin = args[1] if keys.count == 2 { /// key 탭의 index 위치에 Love 를 추가 /// let key = keys[0] let index = keys[1] if let loves = account.getLoves(for: key), let index = Int(index) { if index < loves.count { guard let product = getProduct(isin: isin) else { print("No product about isin: \(isin)") return } if account.setLove(KissProfile.Love(isin: isin, name: product.itemName), index: index, for: key) { print("Success \(product.itemName)") account.saveProfile() setCurrent(productNo: product.shortCode) } else { print("Invalid index: \(index) for \(key)") } } } } else { /// key 탭의 맨뒤에 Love 를 추가 /// guard let product = getProduct(isin: isin) else { print("No product about isin: \(isin)") return } account.addLove(KissProfile.Love(isin: isin, name: product.itemName), for: keys[0]) print("Success \(product.itemName)") account.saveProfile() setCurrent(productNo: product.shortCode) } } } private func onHate(_ args: [String]) async { guard let account = account else { return } guard args.count > 1 else { print("Invalid hate") return } let key = args[0] let isin = args[1] if let love = account.removeLove(isin: isin, for: key) { print("Success \(love.name)") } } private func onLocalizeNames() async { var symbols = Set() symbols.formUnion(DomesticShop.ProductResponse.Item.symbols()) symbols.formUnion(Domestic.BalanceResult.OutputStock.symbols()) symbols.formUnion(Domestic.BalanceResult.OutputAmount.symbols()) symbols.formUnion(MinutePriceResult.OutputPrice.symbols()) symbols.formUnion(PeriodPriceResult.OutputPrice.symbols()) symbols.formUnion(VolumeRankResult.OutputDetail.symbols()) symbols.formUnion(CurrentPriceResult.OutputDetail.symbols()) symbols.formUnion(CapturePrice.symbols()) symbols.formUnion(InvestorVolumeResult.OutputDetail.symbols()) symbols.formUnion(DomesticExtra.IndexPriceResult.Output.symbols()) symbols.formUnion(DomesticExtra.IndexPortfolioResult.Output.symbols()) symbols.formUnion(DomesticExtra.AllIndicesResult.Block.symbols()) symbols.formUnion(DomesticExtra.ShortSellingBalanceResult.OutBlock.symbols()) let newNames = symbols.sorted(by: { $0 < $1 }) let nameUrl = KissConsole.localNamesUrl var addedNameCount = 0 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, ""], source: "") curNames.append(item) addedNameCount += 1 } } if addedNameCount > 0 { do { try curNames.writeCsv(toFile: nameUrl, localized: localized) } catch { print(error) } } print("Success \(nameUrl.lastPathComponent) total: \(curNames.count), new: \(addedNameCount)") } private func onLocalizeOnOff(_ args: [String]) async { guard args.count == 1 else { print("Missing on/off option") return } guard let option = OnOff(rawValue: args[0]) else { print("Invalid on/off option") return } switch option { case .on: localized = true case .off: localized = false } print("Localization \(option.rawValue)") } private func onReal(_ args: [String]) async { guard args.count == 2 else { print("Missing PNO and on/off") return } let productNo = args[0] guard let option = OnOff(rawValue: args[1]) else { print("Invalid on/off option") return } switch option { case .on: break case .off: break } print("WebSocket listening \(option.rawValue) for \(productNo)") } private func onDB(_ args: [String]) async { guard args.count >= 3 else { print("Missing options") return } guard args[0] == "candle" else { print("Missing candle or something") return } switch args[1] { case "build": onCandleDB_Build(Array(args.dropFirst(2))) case "validate": onCandleDB_Validate(Array(args.dropFirst(2))) case "count": onCandleDB_Count(Array(args.dropFirst(2))) default: onCandleDB_Select(Array(args.dropFirst(1))) } } private func onCandleDB_Build(_ args: [String]) { var isBuildResumed = false let productNo: String? switch args[0] { case "all": productNo = nil case "resume": productNo = nil isBuildResumed = true default: productNo = args[0] } let year = args[safe: 1]?.isYear == true ? args[safe: 1]: nil let month = args[safe: 2]?.isMonth == true ? args[safe: 2]: nil let day = args[safe: 3]?.isDay == true ? args[safe: 3]: nil let candleFiles = collectCandleMinuteFiles(productNo: productNo, year: year, month: month, day: day) if candleFiles.isEmpty { print("No candle files of productNo: \(productNo ?? "all")") return } let totalCsvFiles = candleFiles.reduce(0, { $0 + $1.value.count }) print("Building total candle files... \(totalCsvFiles)") var curStep = 0 let maxStep = candleFiles.keys.count for (productNo, csvFiles) in candleFiles { curStep += 1 print("Building candle db for productNo: \(productNo), csv: \(csvFiles.count), step: \(curStep)/\(maxStep)") let semaphore = DispatchSemaphore(value: 0) Task { defer { semaphore.signal() } if isBuildResumed, let year = year { if isCandleMinuteDBExisted(productNo: productNo, year: year) { print("Skipping to build candle db for productNo: \(productNo) by resume") return } } let startTime = Date.appTime if buildCandleMinuteDB(productNo: productNo, csvFiles: csvFiles) { print("Success candle db with elapsed time: \(Date.appTime - startTime)") } try await Task.sleep(nanoseconds: 1_000_000_000 / 5) } semaphore.wait() } } private func onCandleDB_Validate(_ args: [String]) { guard let productNo = args[safe: 0] else { print("Missing productNo on validate") return } guard let year = args[safe: 1], year.isYear else { print("Missing year on validate") return } let startTime = Date.appTime if validateCandleMinuteDB(productNo: productNo, year: year) { print("Success validate candle db with elapsed time: \(Date.appTime - startTime)") } } private func onCandleDB_Count(_ args: [String]) { guard let productNo = args[safe: 0] else { print("Missing productNo on count") return } guard let year = args[safe: 1], year.isYear else { print("Missing year on validate") return } let count = countCandleMinunteDB(productNo: productNo, year: year) print("Total db item count: \(count) for productNo: \(productNo), year: \(year)") } private func onCandleDB_Select(_ args: [String]) { guard let productNo = args[safe: 0] else { print("Missing productNo on select") return } guard let day = args[safe: 1], let (yyyy, MM, dd) = day.yyyyMMdd else { print("Missing yyyyMMdd on select") return } let HH = args[safe: 2]?.HH let candles = selectCandleMinuteDB(productNo: productNo, year: yyyy, month: MM, day: dd, hour: HH) let at = day + (HH == nil ? "": String(format: " %02d", HH!)) print("Total selected db item count: \(candles.count) for product: \(productNo), at: \(at)") } private func onNews(_ args: [String]) async { guard args.count == 1, let day = args[0].yyyyMMdd_toDate else { print("Missing day") return } do { let list = try await DomesticNews.collectList(day: day) let obj = NewsList(list: list) try writeNewsList(obj, day: day) print("Success \(day.yyyyMMdd) total: \(list.count)") } catch { print(error) } } private func onNewsAll(_ args: [String]) { // guard args.count == 1, let day = args[0].yyyyMMdd_toDate else { // print("Missing day") // return // } let day = "20230801".yyyyMMdd_toDate! do { let obj = try readNewsList(day: day) print("Success \(day.yyyyMMdd) total: \(obj.list.count)") if let title = obj.list.first { let semaphore = DispatchSemaphore(value: 0) Task { do { let article = try await DomesticNews.getArticle(title.url) print("\(article)") } catch { print(error) } semaphore.signal() } semaphore.wait() } } catch { print(error) } // 특정 날짜의 뉴스 목록 & 뉴스 기사를 가져옴. } } private extension Array { func suffixStrings(from: Int) -> [String] where Element == String.SubSequence { guard from < count else { return [] } return suffix(from: from).map { String($0) } } subscript(safe index: Int) -> Element? { return indices.contains(index) ? self[index] : nil } } enum OnOff: String { case on case off }