From 89f398862cf527475eb8772e425722d8324ed153 Mon Sep 17 00:00:00 2001 From: ened Date: Sun, 11 Jun 2023 16:39:55 +0900 Subject: [PATCH] Implement "invest", "invest all" command --- .../Sources/Domestic/DomesticStockPrice.swift | 1 - .../Domestic/DomesticStockSearch.swift | 169 +++++++++++++++++- .../Domestic/DomesticStockSearchResult.swift | 145 +++++++++++++++ KissMeConsole/Sources/KissConsole+CSV.swift | 11 +- .../Sources/KissConsole+Candle.swift | 39 +++- .../Sources/KissConsole+Investor.swift | 28 +++ KissMeConsole/Sources/KissConsole.swift | 82 +++++---- README.md | 2 + bin/data | 2 +- .../KissMeConsole.xcodeproj/project.pbxproj | 4 + 10 files changed, 442 insertions(+), 41 deletions(-) create mode 100644 KissMeConsole/Sources/KissConsole+Investor.swift diff --git a/KissMe/Sources/Domestic/DomesticStockPrice.swift b/KissMe/Sources/Domestic/DomesticStockPrice.swift index 45f6414..73d0a1c 100644 --- a/KissMe/Sources/Domestic/DomesticStockPrice.swift +++ b/KissMe/Sources/Domestic/DomesticStockPrice.swift @@ -139,7 +139,6 @@ extension Domestic { } public var result: KResult? = nil public let credential: Credential - public var responseDataLoggable: Bool { true } private var trId: String { diff --git a/KissMe/Sources/Domestic/DomesticStockSearch.swift b/KissMe/Sources/Domestic/DomesticStockSearch.swift index e511a75..10886ac 100644 --- a/KissMe/Sources/Domestic/DomesticStockSearch.swift +++ b/KissMe/Sources/Domestic/DomesticStockSearch.swift @@ -61,6 +61,20 @@ public enum BelongClassCode: String, CustomStringConvertible { } +/// 입력 종목코드 +public enum MarketDivisionCode: String, Codable { + /// 전체 + case all = "0000" + /// 코스피 + case kospi = "0001" + /// 코스닥 + case kosdaq = "1001" + + // TODO: 기타(업종코드) + // https://apiportal.koreainvestment.com/community/10000000-0000-0011-0000-000000000002 +} + + extension Domestic { /// 국내주식시세 - 거래량순위[v1_국내주식-047] @@ -90,7 +104,7 @@ extension Domestic { [ "FID_COND_MRKT_DIV_CODE": "J", "FID_COND_SCR_DIV_CODE": "20171", - "FID_INPUT_ISCD": "0000", // TODO: 기타(업종코드) + "FID_INPUT_ISCD": MarketDivisionCode.all.rawValue, "FID_DIV_CLS_CODE": divisionClass.rawValue, "FID_BLNG_CLS_CODE": belongClass.rawValue, "FID_TRGT_CLS_CODE": "000000000", @@ -163,6 +177,136 @@ extension Domestic { self.baseDate = baseDate } } + + + /// 국내주식주문 - 주식현재가 투자자[v1_국내주식-012] + /// + public struct InvestorVolumeRequest: OrderRequest { + public typealias KResult = InvestorVolumeResult + + public var url: String { "/uapi/domestic-stock/v1/quotations/inquire-investor" } + 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": "J", + "FID_INPUT_ISCD": productNo, + ] + } + public var result: KResult? = nil + public let credential: Credential + public var responseDataLoggable: Bool { + return false + } + + + private var trId: String { + "FHKST01010900" + } + + public let accessToken: String + let productNo: String + + public init(credential: Credential, accessToken: String, productNo: String) { + self.credential = credential + self.accessToken = accessToken + self.productNo = productNo + } + } + + + /// 국내주식주문 - 국내기관_외국인 매매종목가집계[국내주식-037] + /// + public struct ForeignInstitutionVolumeRequest: OrderRequest { + public typealias KResult = String + + public var isMockAvailable: Bool { + credential.isMock == false + } + + public var url: String { "/uapi/domestic-stock/v1/quotations/foreign-institution-total" } + 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": "V", + "FID_COND_SCR_DIV_CODE": "16449", + "FID_INPUT_ISCD": marketDivision.rawValue, + "FID_DIV_CLS_CODE": order.rawValue, + "FID_RANK_SORT_CLS_CODE": tradeType.rawValue, + "FID_ETC_CLS_CODE": target.rawValue, + ] + } + public var result: KResult? = nil + public let credential: Credential + + + private var trId: String { + "FHPTJ04400000" + } + + public let accessToken: String + let marketDivision: MarketDivisionCode + let order: Order + let tradeType: TradeType + let target: Target + + /// 분류 구분 코드 + public enum Order: String { + /// 수량정열 + case volume = "0" + /// 금액정열 + case amount = "1" + } + + /// 순위 정렬 구분 코드 + public enum TradeType: String { + /// 순매수상위 + case topBuying = "0" + /// 순매도상위 + case topSelling = "1" + } + + /// 기타 구분 정렬 + public enum Target: String { + /// 전체 + case all = "0" + /// 외국인 + case foreigner = "1" + /// 기관계 + case institution = "2" + /// 기타 + case etc = "3" + } + + + public init(credential: Credential, accessToken: String, marketDivision: MarketDivisionCode, order: Order, tradeType: TradeType, target: Target) { + self.credential = credential + self.accessToken = accessToken + self.marketDivision = marketDivision + self.order = order + self.tradeType = tradeType + self.target = target + } + } } @@ -188,7 +332,7 @@ extension KissAccount { continuation.resume(throwing: GeneralError.invalidAccessToken) return } - + let request = Domestic.StockVolumeRankRequest(credential: credential, accessToken: accessToken, divisionClass: option.divisionClass, belongClass: option.belongClass) request.query { result in switch result { @@ -221,4 +365,25 @@ extension KissAccount { } } } + + /// 투자자 거래량 가져오기 + /// + public func getInvestorVolume(productNo: String) async throws -> InvestorVolumeResult { + return try await withUnsafeThrowingContinuation { continuation in + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + let request = Domestic.InvestorVolumeRequest(credential: credential, accessToken: accessToken, productNo: productNo) + 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/DomesticStockSearchResult.swift b/KissMe/Sources/Domestic/DomesticStockSearchResult.swift index 615c4e6..50b46ab 100644 --- a/KissMe/Sources/Domestic/DomesticStockSearchResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockSearchResult.swift @@ -190,3 +190,148 @@ public struct HolidyResult: Codable { } } } + + +public struct InvestorVolumeResult: Codable { + public let resultCode: String + public let messageCode: String + public let message: String + public let output: [OutputDetail]? + + private enum CodingKeys: String, CodingKey { + case resultCode = "rt_cd" + case messageCode = "msg_cd" + case message = "msg1" + case output = "output" + } + + public struct OutputDetail: Codable, PropertyIterable, ArrayDecodable { + /// 주식 영업 일자 + public let stockBusinessDate: String + + /// 주식 종가 + public let stockClosingPrice: String + + /// 전일 대비 + public let previousDayVariableRatio: String + + /// 전일 대비 부호 + public let previousDayVariableRatioSign: String + + /// 개인 순매수 수량 + public let personalNetBuyingQuantity: String + + /// 외국인 순매수 수량 + public let foreignNetBuyingQuantity: String + + /// 기관계 순매수 수량 + public let organizationNetBuyingQuantity: String + + /// 개인 순매수 거래 대금 + public let personalNetBuyingTradingAmount: String + + /// 외국인 순매수 거래 대금 + public let foreignNetBuyingTradingAmount: String + + /// 기관계 순매수 거래 대금 + public let organizationNetBuyingTradingAmount: String + + /// 개인 매수2 거래량 + public let personalBuyingVolume: String + + /// 외국인 매수2 거래량 + public let foreignBuyingVolume: String + + /// 기관계 매수2 거래량 + public let organizationBuyingVolume: String + + /// 개인 매수2 거래 대금 + public let personalBuyingTradingAmount: String + + /// 외국인 매수2 거래 대금 + public let foreignBuyingTradingAmount: String + + /// 기관계 매수2 거래 대금 + public let organizationBuyingTradingAmount: String + + /// 개인 매도 거래량 + public let personalSellingVolume: String + + /// 외국인 매도 거래량 + public let foreignSellingVolume: String + + /// 기관계 매도 거래량 + public let organizationSellingVolume: String + + /// 개인 매도 거래 대금 + public let personalSellingTradingAmount: String + + /// 외국인 매도 거래 대금 + public let foreignSellingTradingAmount: String + + /// 기관계 매도 거래 대금 + public let organizationSellingTradingAmount: String + + private enum CodingKeys: String, CodingKey { + case stockBusinessDate = "stck_bsop_date" + case stockClosingPrice = "stck_clpr" + case previousDayVariableRatio = "prdy_vrss" + case previousDayVariableRatioSign = "prdy_vrss_sign" + case personalNetBuyingQuantity = "prsn_ntby_qty" + case foreignNetBuyingQuantity = "frgn_ntby_qty" + case organizationNetBuyingQuantity = "orgn_ntby_qty" + case personalNetBuyingTradingAmount = "prsn_ntby_tr_pbmn" + case foreignNetBuyingTradingAmount = "frgn_ntby_tr_pbmn" + case organizationNetBuyingTradingAmount = "orgn_ntby_tr_pbmn" + case personalBuyingVolume = "prsn_shnu_vol" + case foreignBuyingVolume = "frgn_shnu_vol" + case organizationBuyingVolume = "orgn_shnu_vol" + case personalBuyingTradingAmount = "prsn_shnu_tr_pbmn" + case foreignBuyingTradingAmount = "frgn_shnu_tr_pbmn" + case organizationBuyingTradingAmount = "orgn_shnu_tr_pbmn" + case personalSellingVolume = "prsn_seln_vol" + case foreignSellingVolume = "frgn_seln_vol" + case organizationSellingVolume = "orgn_seln_vol" + case personalSellingTradingAmount = "prsn_seln_tr_pbmn" + case foreignSellingTradingAmount = "frgn_seln_tr_pbmn" + case organizationSellingTradingAmount = "orgn_seln_tr_pbmn" + } + + public init(array: [String]) throws { + guard array.count == 22 else { + throw GeneralError.incorrectArrayItems + } + self.stockBusinessDate = array[0] + self.stockClosingPrice = array[1] + self.previousDayVariableRatio = array[2] + self.previousDayVariableRatioSign = array[3] + self.personalNetBuyingQuantity = array[4] + self.foreignNetBuyingQuantity = array[5] + self.organizationNetBuyingQuantity = array[6] + self.personalNetBuyingTradingAmount = array[7] + self.foreignNetBuyingTradingAmount = array[8] + self.organizationNetBuyingTradingAmount = array[9] + self.personalBuyingVolume = array[10] + self.foreignBuyingVolume = array[11] + self.organizationBuyingVolume = array[12] + self.personalBuyingTradingAmount = array[13] + self.foreignBuyingTradingAmount = array[14] + self.organizationBuyingTradingAmount = array[15] + self.personalSellingVolume = array[16] + self.foreignSellingVolume = array[17] + self.organizationSellingVolume = array[18] + self.personalSellingTradingAmount = array[19] + self.foreignSellingTradingAmount = array[20] + self.organizationSellingTradingAmount = array[21] + } + + public static func symbols() -> [String] { + let i = try! OutputDetail(array: Array(repeating: "", count: 22)) + return Mirror(reflecting: i).children.compactMap { $0.label } + } + + public static func localizedSymbols() -> [String: String] { + [:] + } + } +} diff --git a/KissMeConsole/Sources/KissConsole+CSV.swift b/KissMeConsole/Sources/KissConsole+CSV.swift index 0dae28a..6313444 100644 --- a/KissMeConsole/Sources/KissConsole+CSV.swift +++ b/KissMeConsole/Sources/KissConsole+CSV.swift @@ -32,7 +32,7 @@ extension KissConsole { } static func productPriceUrl(productNo: String) -> URL { - let subPath = "data/\(productNo)" + let subPath = "data/\(productNo)/price" let subFile = "\(subPath)/prices.csv" let fileUrl = URL.currentDirectory().appending(path: subFile) @@ -40,6 +40,15 @@ extension KissConsole { return fileUrl } + static func investorFileUrl(productNo: String, day: String) -> URL { + let subPath = "data/\(productNo)/investor" + let subFile = "\(subPath)/investor-\(day).csv" + + let fileUrl = URL.currentDirectory().appending(path: subFile) + createSubpath(subPath) + return fileUrl + } + static func candleFileUrl(productNo: String, period: CandleFilePeriod, day: String) -> URL { assert(day.count == 8) let subPath = "data/\(productNo)/\(period.rawValue)" diff --git a/KissMeConsole/Sources/KissConsole+Candle.swift b/KissMeConsole/Sources/KissConsole+Candle.swift index 780fb1f..b52e465 100644 --- a/KissMeConsole/Sources/KissConsole+Candle.swift +++ b/KissMeConsole/Sources/KissConsole+Candle.swift @@ -78,12 +78,12 @@ extension KissConsole { } candles.sort(by: { $0.stockBusinessDate > $1.stockBusinessDate }) - guard let maxTime = candles.first?.stockBusinessDate else { + guard let recentDay = candles.first?.stockBusinessDate else { print("No price items") return false } - let fileUrl = KissConsole.candleFileUrl(productNo: productNo, period: period.filePeriod, day: maxTime) + let fileUrl = KissConsole.candleFileUrl(productNo: productNo, period: period.filePeriod, day: recentDay) try candles.writeCsv(toFile: fileUrl, localized: localized) print("wrote \(fileUrl.lastPathComponent) with \(candles.count)") @@ -159,6 +159,7 @@ extension KissConsole { } } + func checkHoliday(_ date: Date) async throws -> Bool { guard await KissContext.shared.targetDate.yyyyMMdd != date.yyyyMMdd else { return await KissContext.shared.isHoliday @@ -167,6 +168,40 @@ extension KissConsole { await KissContext.shared.updateHoliday(isHoliday, targetDate: date) return isHoliday } + + + func validateAllCsvs(filePriod: CandleFilePeriod) throws { + let urls = try FileManager.collectCsv(period: filePriod, candleDate: nil) + + var lastTime = Date.appTime + for (index, url) in urls.enumerated() { + + let r = validateCsv(filePriod: filePriod, url: url) + switch r { + case .ok, .invalidFileName: + break + default: + print("csv invalid: \(r) at \(url)") + throw GeneralError.invalidCandleCsvFile(r.description) + } + + let curTime = Date.appTime + if (curTime - lastTime) > 5 { + lastTime = curTime + print("checking... \(index+1)/\(urls.count)") + } + } + print("DONE csv valid \(urls.count)") + } + + + func validateCsv(filePriod: CandleFilePeriod, url: URL) -> CandleValidation { + switch filePriod { + case .minute: return KissConsole.validateCandleMinute(url) + case .day: return KissConsole.validateCandleDay(url) + case .weak: return KissConsole.validateCandleWeek(url) + } + } } diff --git a/KissMeConsole/Sources/KissConsole+Investor.swift b/KissMeConsole/Sources/KissConsole+Investor.swift new file mode 100644 index 0000000..d4de892 --- /dev/null +++ b/KissMeConsole/Sources/KissConsole+Investor.swift @@ -0,0 +1,28 @@ +// +// KissConsole+Investor.swift +// KissMeConsole +// +// Created by ened-book-m1 on 2023/06/11. +// + +import Foundation +import KissMe + + +extension KissConsole { + func getInvestor(productNo: String) async throws -> Bool { + let result = try await account!.getInvestorVolume(productNo: productNo) + if let output = result.output { + print(output.count) + + guard let recentDay = output.first?.stockBusinessDate else { + print("No investor items") + return false + } + + let fileUrl = KissConsole.investorFileUrl(productNo: productNo, day: recentDay) + try output.writeCsv(toFile: fileUrl, localized: localized) + } + return true + } +} diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index 4255a9d..abf560e 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -55,6 +55,10 @@ class KissConsole { case candleWeek = "candle week" case candleValidate = "candle validate" + // 투자자 열람 + case investor = "investor" + case investorAll = "investor all" + // 종목 열람 case loadShop = "load shop" case updateShop = "update shop" @@ -85,6 +89,8 @@ class KissConsole { return true case .candleValidate: return false + case .investor, .investorAll: + return true case .loadShop, .updateShop, .look: return false case .showcase: @@ -203,6 +209,9 @@ class KissConsole { case .candleDay: onCandleDay(args) case .candleWeek: onCandleWeek(args) case .candleValidate: onCandleValidate(args) + + case .investor: await onInvestor(args) + case .investorAll: onInvestorAll(args) case .loadShop: await onLoadShop() case .updateShop: await onUpdateShop() @@ -736,40 +745,6 @@ extension KissConsole { } - func validateAllCsvs(filePriod: CandleFilePeriod) throws { - let urls = try FileManager.collectCsv(period: filePriod, candleDate: nil) - - var lastTime = Date.appTime - for (index, url) in urls.enumerated() { - - let r = validateCsv(filePriod: filePriod, url: url) - switch r { - case .ok, .invalidFileName: - break - default: - print("csv invalid: \(r) at \(url)") - throw GeneralError.invalidCandleCsvFile(r.description) - } - - let curTime = Date.appTime - if (curTime - lastTime) > 5 { - lastTime = curTime - print("checking... \(index+1)/\(urls.count)") - } - } - print("DONE csv valid \(urls.count)") - } - - - func validateCsv(filePriod: CandleFilePeriod, url: URL) -> CandleValidation { - switch filePriod { - case .minute: return KissConsole.validateCandleMinute(url) - case .day: return KissConsole.validateCandleDay(url) - case .weak: return KissConsole.validateCandleWeek(url) - } - } - - private func onCandleValidate(_ args: [String]) { let period: CandleFilePeriod? if args.count == 1 { @@ -788,6 +763,44 @@ extension KissConsole { } + 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() + } + } + + private func onCandle(_ args: [String]) async { let productNo: String? = (args.isEmpty ? currentShortCode: args[0]) guard let productNo = productNo else { @@ -983,6 +996,7 @@ extension KissConsole { symbols.formUnion(PeriodPriceResult.OutputPrice.symbols()) symbols.formUnion(VolumeRankResult.OutputDetail.symbols()) symbols.formUnion(CurrentPriceResult.OutputDetail.symbols()) + symbols.formUnion(InvestorVolumeResult.OutputDetail.symbols()) let newNames = symbols.sorted(by: { $0 < $1 }) let nameUrl = KissConsole.localNamesUrl diff --git a/README.md b/README.md index 8b2a592..cb9b41c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) `candle week [PNO]` | 종목의 최근 52주 동안의 주봉 열람. PNO 은 생략 가능. **data/(PNO)/week/candle-(yyyyMMdd).csv** 파일로 저장. `candle week all` | 모든 종목의 최근 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** 파일로 저장. `load shop` | data/shop-products.csv 로부터 전체 상품을 로딩. `update shop` | **금융위원회_KRX상장종목정보** 로부터 전체 상품을 얻어서 **data/shop-products.csv** 로 저장. `look (상품명)` | (상품명) 에 해당되는 PNO 를 표시함. diff --git a/bin/data b/bin/data index ccb557d..80c2298 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit ccb557db23d29affd0fcc33183d5a22486f8213b +Subproject commit 80c229837764e6e0eab2303ede7d7daf1c79c842 diff --git a/projects/macos/KissMeConsole.xcodeproj/project.pbxproj b/projects/macos/KissMeConsole.xcodeproj/project.pbxproj index 4e59d0a..a0ed1c1 100644 --- a/projects/macos/KissMeConsole.xcodeproj/project.pbxproj +++ b/projects/macos/KissMeConsole.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 341F5ED42A0A8B9000962D48 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341F5ED32A0A8B9000962D48 /* main.swift */; }; 341F5F052A13B82F00962D48 /* test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341F5F042A13B82F00962D48 /* test.swift */; }; 341F5F092A1463A100962D48 /* KissConsole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341F5F082A1463A100962D48 /* KissConsole.swift */; }; + 3435A7F22A35A8A900D604F1 /* KissConsole+Investor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3435A7F12A35A8A900D604F1 /* KissConsole+Investor.swift */; }; 348168492A2F92AC00A50BD3 /* KissContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348168482A2F92AC00A50BD3 /* KissContext.swift */; }; 348168692A3420BD00A50BD3 /* LocalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348168682A3420BD00A50BD3 /* LocalContext.swift */; }; 349327F72A20E3E300097063 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349327F62A20E3E300097063 /* Foundation+Extensions.swift */; }; @@ -48,6 +49,7 @@ 341F5EDB2A0A8C4600962D48 /* KissMe.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KissMe.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 341F5F042A13B82F00962D48 /* test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = test.swift; sourceTree = ""; }; 341F5F082A1463A100962D48 /* KissConsole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KissConsole.swift; sourceTree = ""; }; + 3435A7F12A35A8A900D604F1 /* KissConsole+Investor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+Investor.swift"; sourceTree = ""; }; 348168482A2F92AC00A50BD3 /* KissContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KissContext.swift; sourceTree = ""; }; 348168682A3420BD00A50BD3 /* LocalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalContext.swift; sourceTree = ""; }; 349327F62A20E3E300097063 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = ""; }; @@ -96,6 +98,7 @@ 341F5F082A1463A100962D48 /* KissConsole.swift */, 34D3680C2A280801005E6756 /* KissConsole+Candle.swift */, 349843202A242AC900E85B08 /* KissConsole+CSV.swift */, + 3435A7F12A35A8A900D604F1 /* KissConsole+Investor.swift */, 349327F62A20E3E300097063 /* Foundation+Extensions.swift */, ); name = KissMeConsole; @@ -182,6 +185,7 @@ 341F5ED42A0A8B9000962D48 /* main.swift in Sources */, 349327F72A20E3E300097063 /* Foundation+Extensions.swift in Sources */, 341F5F092A1463A100962D48 /* KissConsole.swift in Sources */, + 3435A7F22A35A8A900D604F1 /* KissConsole+Investor.swift in Sources */, 341F5F052A13B82F00962D48 /* test.swift in Sources */, 348168692A3420BD00A50BD3 /* LocalContext.swift in Sources */, 349843212A242AC900E85B08 /* KissConsole+CSV.swift in Sources */,