From f28f6277c4be525752e809bedc6d20d293bcae4c Mon Sep 17 00:00:00 2001 From: ened Date: Tue, 20 Jun 2023 23:22:02 +0900 Subject: [PATCH] Implement "shorts all resume" command --- .../Common/Foundation+Extensions.swift | 18 +++++++ KissMeConsole/Sources/KissConsole+Price.swift | 47 +++++++++++++++++-- KissMeConsole/Sources/KissConsole.swift | 14 ++++-- README.md | 2 +- bin/data | 2 +- documents/KMI/KMI-0002.md | 14 ++++++ scripts/build.sh | 8 ++-- 7 files changed, 94 insertions(+), 11 deletions(-) diff --git a/KissMe/Sources/Common/Foundation+Extensions.swift b/KissMe/Sources/Common/Foundation+Extensions.swift index 3945655..ac89165 100644 --- a/KissMe/Sources/Common/Foundation+Extensions.swift +++ b/KissMe/Sources/Common/Foundation+Extensions.swift @@ -9,6 +9,13 @@ import Foundation extension Date { + public var yyyyMM: String { + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "KST") + dateFormatter.dateFormat = "yyyyMM" + return dateFormatter.string(from: self) + } + public var yyyyMMdd: String { let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(abbreviation: "KST") @@ -199,6 +206,17 @@ public func valueToString(_ any: Any) -> String { extension Array where Element: PropertyIterable { + + public func mergeCsv(toFile file: URL, merging: (_ this: [Element], _ file: [Element]) -> [Element], localized: Bool) throws where Element: ArrayDecodable { + guard file.isFileExists == true else { + try writeCsv(toFile: file, localized: localized) + return + } + let oldData = try Self.readCsv(fromFile: file, verifyHeader: true) + let finalData = merging(self, oldData) + try finalData.writeCsv(toFile: file, localized: localized) + } + public func writeCsv(toFile file: URL, appendable: Bool = false, localized: Bool) throws { if appendable, file.isFileExists == true { try appendAtEnd(ofCsv: file) diff --git a/KissMeConsole/Sources/KissConsole+Price.swift b/KissMeConsole/Sources/KissConsole+Price.swift index 67f7059..02c18b3 100644 --- a/KissMeConsole/Sources/KissConsole+Price.swift +++ b/KissMeConsole/Sources/KissConsole+Price.swift @@ -65,14 +65,46 @@ extension KissConsole { print("\t주식 단축 종목코드", output.shortProductCode) } - func getShorts(productNo: String) async throws -> Bool { + func getShortsLastDate(productNo: String) -> Date? { + let startDate = Date().addingTimeInterval(-365 * SecondsForOneDay) + var backDate = Date() + + /// -29일씩 이전으로 돌아가면서, 마지막으로 csv 로 저장했던 날짜를 찾는다. + while startDate < backDate { + let day = backDate.yyyyMM + "01" + let fileUrl = KissConsole.shortsFileUrl(productNo: productNo, day: day) + guard let _ = fileUrl.isFileExists else { + backDate = backDate.addingTimeInterval(-29 * SecondsForOneDay) + continue + } + guard let shorts = try? [DomesticExtra.Shorts].readCsv(fromFile: fileUrl) else { + return Date.date(yyyyMMdd: day, HHmmss: "000000") + } + guard let (yyyy, mm, dd) = shorts.first?.stockBusinessDate.yyyyMMdd else { + return Date.date(yyyyMMdd: day, HHmmss: "000000") + } + guard let newDate = backDate.changing(year: yyyy, month: mm, day: dd) else { + return Date.date(yyyyMMdd: day, HHmmss: "000000") + } + return newDate.addingTimeInterval(SecondsForOneDay) + } + return nil + } + + func getShorts(productNo: String, startAt: Date?) async throws -> Bool { guard let product = getProduct(shortCode: productNo) else { print("Invalid product \(productNo)") return false } let endDate = Date() - let startDate = endDate.addingTimeInterval(-365 * SecondsForOneDay) + let startDate: Date + if let startAt = startAt { + startDate = startAt + } + else { + startDate = endDate.addingTimeInterval(-365 * SecondsForOneDay) + } print("Getting \(product.isinCode), \(startDate.yyyyMMdd) ~ \(endDate.yyyyMMdd)") let result = try await KissAccount.getShortSellingBalance(isinCode: product.isinCode, startDate: startDate, endDate: endDate) @@ -97,7 +129,16 @@ extension KissConsole { } let fileUrl = KissConsole.shortsFileUrl(productNo: productNo, day: month+"01") - try descBlock.writeCsv(toFile: fileUrl, localized: false) + try descBlock.mergeCsv(toFile: fileUrl, merging: { this, file in + var merged = this + for old in file { + if nil == this.first(where: { $0.stockBusinessDate == old.stockBusinessDate }) { + merged.append(old) + } + } + merged.sort(by: { $0.stockBusinessDate > $1.stockBusinessDate }) + return merged + }, localized: false) } } try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredShortsTPS) diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index ecaa42c..7edbfb8 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -656,7 +656,7 @@ extension KissConsole { let semaphore = DispatchSemaphore(value: 0) Task { await KissContext.shared.update(resuming: false) - if args.count == 1, args[0] == "resume" { + if args.count == 1, args[0].lowercased() == "resume" { await KissContext.shared.update(resuming: true) } semaphore.signal() @@ -854,7 +854,7 @@ extension KissConsole { return } do { - _ = try await getShorts(productNo: productNo) + _ = try await getShorts(productNo: productNo, startAt: nil) } catch { print(error) } @@ -867,7 +867,15 @@ extension KissConsole { let semaphore = DispatchSemaphore(value: 0) Task { do { - let success = try await getShorts(productNo: item.shortCode) + let startAt: Date? + if args.count == 1, args[0].lowercased() == "resume" { + startAt = getShortsLastDate(productNo: item.shortCode) + } + else { + startAt = nil + } + + let success = try await getShorts(productNo: item.shortCode, startAt: startAt) print("DONE \(success)") } catch { print(error) diff --git a/README.md b/README.md index 9c29661..bdbd64d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) `investor [PNO]` | 종목의 투자자 거래량 열람. PNO 은 생략 가능. **data/(PNO)/investor/investor-(yyyyMMdd).csv** 파일로 저장. `investor all` | 모든 종목의 투자자 거래량 열람. **data/(PNO)/investor/investor-(yyyyMMdd).csv** 파일로 저장. `shorts [PNO]` | 공매도 잔고를 열람. PNO 은 생략 가능. **data/shorts/(yyyy)/shorts-(yyyyMMdd).csv** 파일로 저장. -`shorts all` | 모든 종목의 공매도 잔고를 열람. **data/shorts/(yyyy)/shorts-(yyyyMMdd).csv** 파일로 저장. +`shorts all [resume]` | 모든 종목의 공매도 잔고를 열람. **data/shorts/(yyyy)/shorts-(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 1117afa..659b8ff 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit 1117afa184691a6c3bb9ad34b0c3aa23cb41e1ff +Subproject commit 659b8ff1698105c59ebda2ed94e289402a76164c diff --git a/documents/KMI/KMI-0002.md b/documents/KMI/KMI-0002.md index b181662..25325fe 100644 --- a/documents/KMI/KMI-0002.md +++ b/documents/KMI/KMI-0002.md @@ -1,2 +1,16 @@ # KMI-0002 +## How to + +공매도 거래량의 변화와 잔고를 분석하여, 변화를 감지하면 매수/매도 성향의 추천을 제공합니다. + +공매도 거래량 정보는 다음의 API 를 통해서 얻고 있습니다. + +* + +## Usage + + +## Configuration + +현재 여기에는 환경설정 정보가 없습니다. diff --git a/scripts/build.sh b/scripts/build.sh index 4267a6a..2463441 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -5,7 +5,9 @@ ## $ ./scripts/build.sh ## -./build_any.sh KissMeConsole -#./build_any.sh KissMeBatch -#./build_any.sh KissGram +THIS_PATH=`dirname "$0"` + +${THIS_PATH}/build_any.sh KissMeConsole +#${THIS_PATH}/build_any.sh KissMeBatch +#${THIS_PATH}/build_any.sh KissGram