Add candle day/week commands

This commit is contained in:
2023-06-01 09:36:47 +09:00
parent 62e67fe441
commit fbf0cccfc8
10 changed files with 622 additions and 86 deletions

View File

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

View File

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

View File

@@ -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:평균거래금액회전율"
}
}
}

View File

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

View File

@@ -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 {

View File

@@ -8,3 +8,5 @@
import Foundation
KissConsole().run()
//move_candles_to_min_subdir()

View File

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

View File

@@ -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 를 표시함.

View File

@@ -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 = "<group>"; };
3498431E2A24287600E85B08 /* KissMeConsoleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KissMeConsoleTests.swift; sourceTree = "<group>"; };
349843202A242AC900E85B08 /* KissConsole+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+CSV.swift"; sourceTree = "<group>"; };
34D3680C2A280801005E6756 /* KissConsole+Candle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KissConsole+Candle.swift"; sourceTree = "<group>"; };
/* 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 */,