From fbf0cccfc88b2d4f0c6cfd508243751d3fc5c090 Mon Sep 17 00:00:00 2001 From: ened Date: Thu, 1 Jun 2023 09:36:47 +0900 Subject: [PATCH] Add candle day/week commands --- .../Sources/Domestic/DomesticStockPrice.swift | 84 +++++- .../Domestic/DomesticStockPriceResult.swift | 243 ++++++++++++++++++ .../Domestic/DomesticStockSearch.swift | 13 +- .../Sources/KissConsole+Candle.swift | 142 ++++++++++ KissMeConsole/Sources/KissConsole.swift | 175 +++++++------ KissMeConsole/Sources/main.swift | 2 + KissMeConsole/Sources/test.swift | 33 +++ README.md | 10 +- bin/data | 2 +- .../KissMeConsole.xcodeproj/project.pbxproj | 4 + 10 files changed, 622 insertions(+), 86 deletions(-) create mode 100644 KissMeConsole/Sources/KissConsole+Candle.swift diff --git a/KissMe/Sources/Domestic/DomesticStockPrice.swift b/KissMe/Sources/Domestic/DomesticStockPrice.swift index f456de3..241d7f5 100644 --- a/KissMe/Sources/Domestic/DomesticStockPrice.swift +++ b/KissMe/Sources/Domestic/DomesticStockPrice.swift @@ -106,12 +106,71 @@ extension Domestic { self.isNext = isNext } } + + + /// 국내주식시세 - 국내주식기간별시세(일/주/월/년)[v1_국내주식-016] + /// + public struct StockPeriodPriceRequest: TokenRequest { + public typealias KResult = PeriodPriceResult + + public var url: String { + "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice" + } + public var method: Method { .get } + + public var header: [String: String?] { + [ + "authorization": "Bearer \(accessToken)", + "appkey": credential.appKey, + "appsecret": credential.appSecret, + "tr_id": trId, + "custtype": CustomerType.personal.rawValue, + ] + } + public var body: [String: Any] { + [ + "FID_COND_MRKT_DIV_CODE": "", + "FID_INPUT_ISCD": productNo, + "FID_INPUT_DATE_1": startDate, + "FID_INPUT_DATE_2": endDate, + "FID_PERIOD_DIV_CODE": period.rawValue, + "FID_ORG_ADJ_PRC": priceType.rawValue, + ] + } + public var result: KResult? = nil + public let credential: Credential + public var responseDataLoggable: Bool { + return false + } + + + private var trId: String { + "FHKST03010100" + } + + public let accessToken: String + let productNo: String + let startDate: String /// yyyyMMdd + let endDate: String /// yyyyMMdd + let period: PeriodDivision + let priceType: PriceType + + public init(credential: Credential, accessToken: String, productNo: String, startDate: String, endDate: String, period: PeriodDivision, priceType: PriceType) { + self.credential = credential + self.accessToken = accessToken + self.productNo = productNo + self.startDate = startDate + self.endDate = startDate + self.period = period + self.priceType = priceType + } + } } // MARK: Stock Price extension KissAccount { - + /// 현재 종목 시세를 가져오기 /// public func getCurrentPrice(productNo: String) async throws -> CurrentPriceResult { @@ -156,4 +215,27 @@ extension KissAccount { } } } + + + /// 종목 일/주/월/년 봉을 가져오기 + /// + public func getPeriodPrice(productNo: String, startDate: Date, endDate: Date, period: PeriodDivision) async throws -> PeriodPriceResult { + return try await withUnsafeThrowingContinuation { continuation in + + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + let request = Domestic.StockPeriodPriceRequest(credential: credential, accessToken: accessToken, productNo: productNo, startDate: startDate.yyyyMMdd, endDate: endDate.yyyyMMdd, period: period, priceType: .adjusted) + request.query { result in + switch result { + case .success(let result): + continuation.resume(returning: result) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } } diff --git a/KissMe/Sources/Domestic/DomesticStockPriceResult.swift b/KissMe/Sources/Domestic/DomesticStockPriceResult.swift index 6c93a6e..520a91d 100644 --- a/KissMe/Sources/Domestic/DomesticStockPriceResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockPriceResult.swift @@ -39,6 +39,49 @@ public enum MarketWarning: String, Codable { } +/// 기간분류코드 +public enum PeriodDivision: String, Codable { + /// 일봉 + case daily = "D" + /// 주봉 + case weekly = "W" + /// 월봉 + case monthly = "M" + /// 년봉 + case yearly = "Y" +} + + +/// 락 구분 코드 +public enum ExDivision: String, Codable { + /// 해당사항없음 (락이 발생안한 경우) + case none = "00" + /// 권리락 + case exRights = "01" + /// 배당락 + case exDividend = "02" + /// 분배락 + case exDistribution = "03" + /// 권배락 + case exRightsDividend = "04" + /// 중간(분기)배당락 + case exHalfQuarterDividend = "05" + /// 권리중간배당락 + case exRightsHalfDividend = "06" + /// 권리분기배당락 + case exRightsQuaterDividend = "07" +} + + +/// 수정주가 +public enum PriceType: String, Codable { + /// 수정주가 + case adjusted = "0" + /// 원주가 + case original = "1" +} + + public struct CurrentPriceResult: Codable { public let resultCode: String public let messageCode: String @@ -512,3 +555,203 @@ public struct MinutePriceResult: Codable { } } } + + +public struct PeriodPriceResult: Codable { + public let resultCode: String + public let messageCode: String + public let message: String + public let output1: OutputSummary? + public let output2: [OutputPrice] + + private enum CodingKeys: String, CodingKey { + case resultCode = "rt_cd" + case messageCode = "msg_cd" + case message = "msg1" + case output1 + case output2 + } + + public struct OutputSummary: Codable { + /// 전일 대비 + public let previousDayVariableRatio: String + + /// 전일 대비 부호 + public let previousDayVariableRatioSign: String + + /// 전일 대비율 + public let previousDayDiffRatio: String + + /// 주식 전일 종가 + public let previousDayStockClosingPrice: String + + /// 누적 거래량 + public let accumulatedVolume: String + + /// 누적 거래 대금 + public let accumulatedTradingAmount: String + + /// HTS 한글 종목명 + public let htsProductName: String + + /// 주식 현재가 + public let currentStockPrice: String + + /// 주식 단축 종목코드 + public let shortProductCode: String + + /// 전일 거래량 + public let previousDayVolume: String + + /// 주식 상한가 + public let maximumStockPrice: String + + /// 주식 하한가 + public let minimumStockPrice: String + + /// 주식 시가 + public let stockPrice: String + + /// 주식 최고가 + public let highestStockPrice: String + + /// 주식 최저가 + public let lowestStockPrice: String + + + /// 주식 전일 시가 + public let yesterdayStockPrice: String + + /// 주식 전일 최고가 + public let yesterdayHighestStockPrice: String + + /// 주식 전일 최저가 + public let yesterdayLowestStockPrice: String + + /// 매도호가 + public let askingPrice: String + + /// 매수호가 + public let biddingPrice: String + + /// 전일 대비 거래량 + public let previousDayDiffVolume: String + + /// 거래량 회전율 + public let volumeTurnoverRate: String + + /// 주식 액면가 + public let stockFacePrice: String + + /// 상장 주수 + public let listedStockCount: String + + /// 자본금 + public let capital: String + + /// HTS 시가총액 + public let htsTotalMarketValue: String + + /// PER + public let per: String + + /// EPS + public let eps: String + + /// PBR + public let pbr: String + + /// 전체 융자 잔고 비율 + public let totalOutstandingloanRate: String + + private enum CodingKeys: String, CodingKey { + case previousDayVariableRatio = "prdy_vrss" + case previousDayVariableRatioSign = "prdy_vrss_sign" + case previousDayDiffRatio = "prdy_ctrt" + case previousDayStockClosingPrice = "stck_prdy_clpr" + case accumulatedVolume = "acml_vol" + case accumulatedTradingAmount = "acml_tr_pbmn" + case htsProductName = "hts_kor_isnm" + case currentStockPrice = "stck_prpr" + case shortProductCode = "stck_shrn_iscd" + case previousDayVolume = "prdy_vol" + case maximumStockPrice = "stck_mxpr" + case minimumStockPrice = "stck_llam" + case stockPrice = "stck_oprc" + case highestStockPrice = "stck_hgpr" + case lowestStockPrice = "stck_lwpr" + case yesterdayStockPrice = "stck_prdy_oprc" + case yesterdayHighestStockPrice = "stck_prdy_hgpr" + case yesterdayLowestStockPrice = "stck_prdy_lwpr" + case askingPrice = "askp" + case biddingPrice = "bidp" + case previousDayDiffVolume = "prdy_vrss_vol" + case volumeTurnoverRate = "vol_tnrt" + case stockFacePrice = "stck_fcam" + case listedStockCount = "lstn_stcn" + case capital = "cpfn" + case htsTotalMarketValue = "hts_avls" + case per = "per" + case eps = "eps" + case pbr = "pbr" + case totalOutstandingloanRate = "whol_loan_rmnd_rate" + } + } + + public struct OutputPrice: 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" + } + } +} diff --git a/KissMe/Sources/Domestic/DomesticStockSearch.swift b/KissMe/Sources/Domestic/DomesticStockSearch.swift index 14a0f08..176192b 100644 --- a/KissMe/Sources/Domestic/DomesticStockSearch.swift +++ b/KissMe/Sources/Domestic/DomesticStockSearch.swift @@ -18,7 +18,7 @@ public enum DivisionClassCode: String { } -public enum BelongClassCode: String { +public enum BelongClassCode: String, CustomStringConvertible { /// 평균거래량 case averageVolume = "0" /// 거래증가율 @@ -29,6 +29,17 @@ public enum BelongClassCode: String { case transactionValue = "3" /// 평균거래금액회전율 case averageTransactionValueTurnoverRate = "4" + + + public var description: String { + switch self { + case .averageVolume: return "0:평균거래량" + case .volumeIncreaseRate: return "1:거래증가율" + case .averageVolumeTurnoverRate: return "2:평균거래회전율" + case .transactionValue: return "3:거래금액순" + case .averageTransactionValueTurnoverRate: return "4:평균거래금액회전율" + } + } } diff --git a/KissMeConsole/Sources/KissConsole+Candle.swift b/KissMeConsole/Sources/KissConsole+Candle.swift new file mode 100644 index 0000000..f84cae5 --- /dev/null +++ b/KissMeConsole/Sources/KissConsole+Candle.swift @@ -0,0 +1,142 @@ +// +// 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 + +/// How many seconds does 1 day have? +let SecondsForOneDay: TimeInterval = 60 * 60 * 24 + + +extension KissConsole { + + var last250Days: (startDate: Date, endDate: Date) { + let endDate = Date().changing(hour: 0, min: 0, sec: 0)! + let startDate = endDate.addingTimeInterval(-250 * SecondsForOneDay) + return (startDate, endDate) + } + + + var last52Weeks: (startDate: Date, endDate: Date) { + // TODO: 주당 가격을 얻기 위해, 거래 시작일을 마지막 장 개설일로 보정할 필요가 있을까? + let endDate = Date().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" + case weak = "week" + } + + + func candleFileUrl(productNo: String, period: CandleFilePeriod, day: String) -> URL { + assert(day.count == 6) + let subPath = "data/\(productNo)/\(period.rawValue)" + let subFile = "\(subPath)/candle-\(day).csv" + let fileUrl = URL.currentDirectory().appending(path: subFile) + createSubpath(subPath) + return fileUrl + } + + + func getCandle(productNo: String, period: PeriodDivision, startDate: Date, endDate: Date) 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 + + let result = try await account!.getPeriodPrice(productNo: productNo, startDate: startDate, endDate: endDate, period: .daily) + +// let fileUrl = candleFileUrl(productNo: productNo, period: "min", day: minTime) +// KissConsole.writeCandle(candles, fileUrl: fileUrl) + + 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 + } + } + + candles.sort(by: { $0.stockBusinessDate < $1.stockBusinessDate }) + guard let minTime = candles.first?.stockBusinessDate else { + print("No price items") + return false + } + + let fileUrl = candleFileUrl(productNo: productNo, period: .minute, day: minTime) + KissConsole.writeCandle(candles, fileUrl: fileUrl) + return true + } catch { + print("\(error)") + return false + } + } +} diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index b2cfc18..c338ccb 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -11,13 +11,13 @@ import KissMe class KissConsole { private var credential: Credential? = nil - private var account: KissAccount? = nil + var account: KissAccount? = nil private var shop: KissShop? = nil private var productsLock = NSLock() private var products = [String: [DomesticShop.Product]]() private var currentShortCode: String? - private var currentCandleShortCode: String? + var currentCandleShortCode: String? private enum KissCommand: String { case quit = "quit" @@ -42,6 +42,8 @@ class KissConsole { case now = "now" case candle = "candle" case candleAll = "candle all" + case candleDay = "candle day" + case candleWeek = "candle week" // 종목 열람 case loadShop = "load shop" @@ -64,7 +66,7 @@ class KissConsole { return true case .openBag: return true - case .now, .candle, .candleAll: + case .now, .candle, .candleAll, .candleDay, .candleWeek: return true case .loadShop, .updateShop, .look: return false @@ -180,6 +182,8 @@ class KissConsole { case .now: await onNow(args) case .candle: await onCandle(args) case .candleAll: onCancleAll() + case .candleDay: onCandleDay(args) + case .candleWeek: onCandleWeek(args) case .loadShop: await onLoadShop() case .updateShop: await onUpdateShop() @@ -200,7 +204,7 @@ class KissConsole { extension KissConsole { - private func createSubpath(_ name: String) { + func createSubpath(_ name: String) { let subPath = URL.currentDirectory().appending(path: name) try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true) } @@ -319,8 +323,17 @@ extension KissConsole { } - private func onTop(_ arg: [String]) async { - let option = RankingOption(divisionClass: .all, belongClass: .averageVolume) + 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 rank = try await account!.getVolumeRanking(option: option) @@ -558,6 +571,82 @@ extension KissConsole { } + private func onCandleDay(_ args: [String]) { + if args.count == 1, args[0] == "all" { + onCandleDayAll() + return + } + + let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) + guard let productNo = productNo else { + print("Invalid productNo") + return + } + + let (startDate, endDate) = last250Days + let semaphore = DispatchSemaphore(value: 0) + Task { + let success = await getCandle(productNo: productNo, period: .daily, startDate: startDate, endDate: endDate) + print("DONE \(success) \(productNo)") + semaphore.signal() + } + semaphore.wait() + } + + + 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) + print("DONE \(success) \(item.shortCode)") + semaphore.signal() + } + semaphore.wait() + } + } + + + private func onCandleWeek(_ args: [String]) { + if args.count == 1, args[0] == "all" { + onCandleDayAll() + return + } + + let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) + guard let productNo = productNo else { + print("Invalid productNo") + return + } + + let (startDate, endDate) = last52Weeks + let semaphore = DispatchSemaphore(value: 0) + Task { + let success = await getCandle(productNo: productNo, period: .weekly, startDate: startDate, endDate: endDate) + print("DONE \(success) \(productNo)") + semaphore.signal() + } + semaphore.wait() + } + + + 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) + print("DONE \(success) \(item.shortCode)") + semaphore.signal() + } + semaphore.wait() + } + } + + private func onCandle(_ args: [String]) async { let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { @@ -566,80 +655,6 @@ extension KissConsole { } _ = await getCandle(productNo: productNo) } - - /// Limit to request candle with `preferCandleTPS` - private var preferCandleTPS: UInt64 { - return 19 - } - - private 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() - //nextTime.change(hour: 17, min: 0, sec: 0) - //nextTime.change(year: 2023, month: 5, day: 26) - //nextTime.change(hour: 9, min: 1, sec: 0) - - 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 / preferCandleTPS) - } - else { - print("minute price finished") - break - } - } - - candles.sort(by: { $0.stockBusinessDate < $1.stockBusinessDate }) - guard let minTime = candles.first?.stockBusinessDate else { - print("No price items") - return false - } - - let subPath = "data/\(productNo)" - let subFile = "\(subPath)/candle-\(minTime).csv" - let fileUrl = URL.currentDirectory().appending(path: subFile) - createSubpath(subPath) - KissConsole.writeCandle(candles, fileUrl: fileUrl) - return true - } catch { - print("\(error)") - return false - } - } private func onLoadShop() async { diff --git a/KissMeConsole/Sources/main.swift b/KissMeConsole/Sources/main.swift index 59b95cf..387a388 100644 --- a/KissMeConsole/Sources/main.swift +++ b/KissMeConsole/Sources/main.swift @@ -8,3 +8,5 @@ import Foundation KissConsole().run() + +//move_candles_to_min_subdir() diff --git a/KissMeConsole/Sources/test.swift b/KissMeConsole/Sources/test.swift index 92401e4..b0db32f 100644 --- a/KissMeConsole/Sources/test.swift +++ b/KissMeConsole/Sources/test.swift @@ -159,3 +159,36 @@ private func check_candle_csv() { } } } + + +func move_candles_to_min_subdir() { + guard let enumerator = subPathFiles("data") else { + return + } + + var urls = [URL]() + for case let fileUrl as URL in enumerator { + guard fileUrl.pathExtension == "csv" else { + continue + } + urls.append(fileUrl) + } + + for fileUrl in urls { + let fileName = fileUrl.lastPathComponent + let upper = fileUrl.deletingLastPathComponent() + + let newPath = upper.appending(path: "min") + let newUrl = newPath.appending(path: fileName) + + //print("file: \(fileUrl) -> \(newUrl)") + do { + try FileManager.default.createDirectory(at: upper, withIntermediateDirectories: true) + try FileManager.default.moveItem(at: fileUrl, to: newUrl) + } + catch { + print(error) + exit(1) + } + } +} diff --git a/README.md b/README.md index 06d255f..c72a94c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ command | 설명 `login mock` | Mock 서버로 로그인. mock-server.json 을 credential 로 사용. `login real` | Real 서버로 로그인. real-server.json 을 credential 로 사용. `logout` | 접속한 서버에서 로그아웃. -`top` | 상위 거래량 30종목 (평균거래량) +`top (0,1,2,3,4)` | 상위 거래량 30종목 (0:평균거래량, 1:거래증가율, 2:평균거래회전율, 3:거래금액순, 4:평균거래금액회전율) `buy (PNO) (가격) (수량)` | 상품을 구매. (가격) 에 -8282 로 입력하면 시장가격. (수량) 에 -82 로 입력하면 최대수량. `buy check (PNO) (가격)` | 현재 잔고로 구매가 가능한 수량을 확인. `sell (PNO) (가격) (수량)` | 보유한 상품을 판매. (가격) 에 -8282 로 입력하면 시장가격. @@ -25,8 +25,12 @@ command | 설명 WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) 에 -82 로 입력하면 전체수량. `open bag` | 보유 종목 열람. `now [PNO]` | 종목의 현재가 열람. PNO 은 생략 가능. -`candle [PNO]` | 종목의 분봉 열람. PNO 은 생략 가능. -`candle all` | 모든 종목의 분봉 열람. cron job 으로 돌리기 위해서 추가. +`candle [PNO]` | 종목의 분봉 열람. PNO 은 생략 가능. data/(PNO)/min/candle-(yyyyMMdd).csv 파일로 저장. +`candle all` | 모든 종목의 분봉 열람. cron job 으로 돌리기 위해서 추가. data/(PNO)/min/candle-(yyyyMMdd).csv 파일로 저장. +`candle day [PNO]` | 종목의 최근 250일 동안의 일봉 열람. PNO 은 생략 가능. data/(PNO)/day/candle-(yyyyMMdd).csv 파일로 저장. +`candle day all` | 모든 종목의 최근 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 파일로 저장. `load shop` | data/shop-products.csv 로부터 전체 상품을 로딩. `update shop` | **금융위원회_KRX상장종목정보** 로부터 전체 상품을 얻어서 data/shop-products.csv 로 저장. `look (상품명)` | (상품명) 에 해당되는 PNO 를 표시함. diff --git a/bin/data b/bin/data index f11e0ce..e62d901 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit f11e0cee30d1c5ea4d61d19e00e891bebb2cb56f +Subproject commit e62d901f86637a29b837183123db74dbc26fe51f diff --git a/projects/macos/KissMeConsole.xcodeproj/project.pbxproj b/projects/macos/KissMeConsole.xcodeproj/project.pbxproj index 3d10e65..244a34c 100644 --- a/projects/macos/KissMeConsole.xcodeproj/project.pbxproj +++ b/projects/macos/KissMeConsole.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 341F5F092A1463A100962D48 /* KissConsole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341F5F082A1463A100962D48 /* KissConsole.swift */; }; 349327F72A20E3E300097063 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349327F62A20E3E300097063 /* Foundation+Extensions.swift */; }; 349843212A242AC900E85B08 /* KissConsole+CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349843202A242AC900E85B08 /* KissConsole+CSV.swift */; }; + 34D3680D2A280801005E6756 /* KissConsole+Candle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D3680C2A280801005E6756 /* KissConsole+Candle.swift */; }; 34EE76862A1C391B009761D2 /* KissMe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 341F5EDB2A0A8C4600962D48 /* KissMe.framework */; }; 34EE76872A1C391B009761D2 /* KissMe.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 341F5EDB2A0A8C4600962D48 /* KissMe.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -48,6 +49,7 @@ 349327F62A20E3E300097063 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = ""; }; 3498431E2A24287600E85B08 /* KissMeConsoleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KissMeConsoleTests.swift; sourceTree = ""; }; 349843202A242AC900E85B08 /* KissConsole+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+CSV.swift"; sourceTree = ""; }; + 34D3680C2A280801005E6756 /* KissConsole+Candle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+Candle.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -86,6 +88,7 @@ 341F5ED32A0A8B9000962D48 /* main.swift */, 341F5F042A13B82F00962D48 /* test.swift */, 341F5F082A1463A100962D48 /* KissConsole.swift */, + 34D3680C2A280801005E6756 /* KissConsole+Candle.swift */, 349843202A242AC900E85B08 /* KissConsole+CSV.swift */, 349327F62A20E3E300097063 /* Foundation+Extensions.swift */, ); @@ -169,6 +172,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 34D3680D2A280801005E6756 /* KissConsole+Candle.swift in Sources */, 341F5ED42A0A8B9000962D48 /* main.swift in Sources */, 349327F72A20E3E300097063 /* Foundation+Extensions.swift in Sources */, 341F5F092A1463A100962D48 /* KissConsole.swift in Sources */,