Add candle day/week commands
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:평균거래금액회전율"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
142
KissMeConsole/Sources/KissConsole+Candle.swift
Normal file
142
KissMeConsole/Sources/KissConsole+Candle.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,3 +8,5 @@
|
||||
import Foundation
|
||||
|
||||
KissConsole().run()
|
||||
|
||||
//move_candles_to_min_subdir()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
README.md
10
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 를 표시함.
|
||||
|
||||
2
bin/data
2
bin/data
Submodule bin/data updated: f11e0cee30...e62d901f86
@@ -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 */,
|
||||
|
||||
Reference in New Issue
Block a user