From bb48abd3d3f33ffa3875f615276e0b3a33ca17b2 Mon Sep 17 00:00:00 2001 From: ened Date: Thu, 22 Jun 2023 18:50:20 +0900 Subject: [PATCH] Implement KMI-0003 index set --- KissMe/Sources/Index/IndexResult.swift | 8 ++ KissMeIndex/Sources/KissIndex+0002.swift | 120 +++--------------- KissMeIndex/Sources/KissIndex+0003.swift | 46 ++++++- KissMeIndex/Sources/KissIndex+0005.swift | 18 +-- KissMeIndex/Sources/KissIndex.swift | 113 +++++++++++++++++ documents/KMI/KMI-0003.md | 48 +++++++ documents/KMI/KMI-0005.md | 15 ++- .../xcschemes/KissMeIndex.xcscheme | 2 +- 8 files changed, 244 insertions(+), 126 deletions(-) diff --git a/KissMe/Sources/Index/IndexResult.swift b/KissMe/Sources/Index/IndexResult.swift index a037c72..0346602 100644 --- a/KissMe/Sources/Index/IndexResult.swift +++ b/KissMe/Sources/Index/IndexResult.swift @@ -16,10 +16,18 @@ public struct KissIndexResult: Codable { public struct Output: Codable { public let shortCode: String + public let productName: String? public let weight: Double public init(shortCode: String, weight: Double) { self.shortCode = shortCode + self.productName = nil + self.weight = weight + } + + public init(shortCode: String, productName: String?, weight: Double) { + self.shortCode = shortCode + self.productName = productName self.weight = weight } } diff --git a/KissMeIndex/Sources/KissIndex+0002.swift b/KissMeIndex/Sources/KissIndex+0002.swift index 6d6cf0f..21a6bd2 100644 --- a/KissMeIndex/Sources/KissIndex+0002.swift +++ b/KissMeIndex/Sources/KissIndex+0002.swift @@ -18,119 +18,33 @@ extension KissIndex { let semaphore = DispatchSemaphore(value: 0) Task { -// var scoreMap = [String: Int]() - do { - let shorts = try await collectShorts(date: date) + let shorts = try await collectShorts(date: date) { aShorts in + /// 공매도 잔고 비중 (1%) 이상 종목 리스트 + if let ratio = Double(aShorts.shortSellingBalanceRatio), ratio >= 0.01 { + return true + } + return false + } print(shorts.count) - let prices = try await collectPrices(date: date) + let prices = try await collectPrices(date: date) { price in + if let quantity = Int(price.lastShortSellingConclusionQuantity), quantity > 0 { + /// 최종 공매도 체결 수량이 잔고량에 비해서 높으면? + /// lastShortSellingConclusionQuantity + return true + } + return false + } print(prices.count) + } catch { print(error) + writeError(error, kmi: kmi) } semaphore.signal() } semaphore.wait() } - - - private func collectShorts(date: Date) async throws -> [DomesticExtra.Shorts] { - let shorts = try await withThrowingTaskGroup(of: DomesticExtra.Shorts?.self, returning: [DomesticExtra.Shorts].self) { taskGroup in - let all = getAllProducts() - let yyyyMMdd = date.yyyyMMdd - - for item in all { - taskGroup.addTask { - let shortsUrl = KissIndex.pickNearShortsUrl(productNo: item.shortCode, date: date) - - let shorts = try [DomesticExtra.Shorts].readCsv(fromFile: shortsUrl) - let targetShorts = shorts.filter { $0.stockBusinessDate == yyyyMMdd } - - /// 공매도 잔고 비중 (1%) 이상 종목 리스트 - if let aShorts = targetShorts.first, let ratio = Double(aShorts.shortSellingBalanceRatio), ratio >= 0.01 { - return aShorts - } - return nil - } - } - - var taskResult = [DomesticExtra.Shorts]() - for try await result in taskGroup.compactMap( { $0 }) { - taskResult.append(result) - } - return taskResult - } - return shorts - } - - - private func collectPrices(date: Date) async throws -> [CapturePrice] { - let prices = try await withThrowingTaskGroup(of: CapturePrice?.self, returning: [CapturePrice].self) { taskGroup in - let all = getAllProducts() - let yyyyMMdd = date.yyyyMMdd - let dateHHmmss = date.HHmmss - - for item in all { - taskGroup.addTask { - let pricesUrl = KissIndex.pickNearPricesUrl(productNo: item.shortCode, date: date) - let prices = try [CapturePrice].readCsv(fromFile: pricesUrl) - let targetPrices = prices.filter { $0.stockBusinessDate == yyyyMMdd && $0.captureTime <= dateHHmmss } - .sorted(by: { dateHHmmss.diffSecondsTwoHHmmss($0.captureTime) < dateHHmmss.diffSecondsTwoHHmmss($1.captureTime) }) - - if let price = targetPrices.first, let quantity = Int(price.lastShortSellingConclusionQuantity), quantity > 0 { - /// 최종 공매도 체결 수량이 잔고량에 비해서 높으면? - /// lastShortSellingConclusionQuantity - return price - } - return nil - } - } - - var taskResult = [CapturePrice]() - for try await result in taskGroup.compactMap( { $0 }) { - taskResult.append(result) - } - return taskResult - } - return prices - } - - - private static var shopProductsUrl: URL { - URL.currentDirectory().appending(path: "data/shop-products.csv") - } - - private static func pickNearShortsUrl(productNo: String, date: Date) -> URL { - let subPath = "data/\(productNo)/shorts" - let monthFile = "shorts-\(date.yyyyMM01).csv" - - return URL.currentDirectory().appending(path: "\(subPath)/\(monthFile)") - } - - private static func pickNearPricesUrl(productNo: String, date: Date) -> URL { - let subPath = "data/\(productNo)/price" - let priceFile = "prices.csv" - - // TODO: work month file - //let monthFile = "prices-\(date.yyyyMM01).csv" - - return URL.currentDirectory().appending(path: "\(subPath)/\(priceFile)") - } -} - - -extension String { - func diffSecondsTwoHHmmss(_ another: String) -> TimeInterval { - guard let (hour, min, sec) = self.HHmmss else { - return Double.greatestFiniteMagnitude - } - guard let (dHour, dMin, dSec) = another.HHmmss else { - return Double.greatestFiniteMagnitude - } - let seconds = (hour * 60 * 60 + min * 60 + sec) - let dSeconds = (dHour * 60 * 60 + dMin * 60 + dSec) - return TimeInterval(seconds - dSeconds) - } } diff --git a/KissMeIndex/Sources/KissIndex+0003.swift b/KissMeIndex/Sources/KissIndex+0003.swift index 4be960c..3142104 100644 --- a/KissMeIndex/Sources/KissIndex+0003.swift +++ b/KissMeIndex/Sources/KissIndex+0003.swift @@ -6,11 +6,55 @@ // import Foundation +import KissMe extension KissIndex { func indexSet_0003(date: Date, config: String?, kmi: KissIndexType) { - // TODO: work + if productsCount == 0 { + loadShop(url: KissIndex.shopProductsUrl) + } + + let semaphore = DispatchSemaphore(value: 0) + Task { + var scoreMap = [String: Double]() + + do { + let prices = try await collectPrices(date: date) { price in + if let per = Double(price.per), per <= 20.0 { + return true + } + return false + } + //print(prices.count) + + for price in prices { + let per = Double(price.per)! + var score: Double = 0 + if per <= 10 { + score = (10 - per) * 5 + } + else if per <= 20 { + score = (20 - per) * 4 + } + + if let _ = scoreMap[price.shortProductCode] { + scoreMap[price.shortProductCode]! += score + } + else { + scoreMap[price.shortProductCode] = score + } + } + + normalizeAndWrite(scoreMap: scoreMap, includeName: true, kmi: kmi) + + } catch { + writeError(error, kmi: kmi) + } + + semaphore.signal() + } + semaphore.wait() } } diff --git a/KissMeIndex/Sources/KissIndex+0005.swift b/KissMeIndex/Sources/KissIndex+0005.swift index ade24c1..3521395 100644 --- a/KissMeIndex/Sources/KissIndex+0005.swift +++ b/KissMeIndex/Sources/KissIndex+0005.swift @@ -15,14 +15,14 @@ extension KissIndex { let belongs: [BelongClassCode] = [.averageVolume, .volumeIncreaseRate, .averageVolumeTurnoverRate, .transactionValue, .averageTransactionValueTurnoverRate] do { - var scoreMap = [String: Int]() - + var scoreMap = [String: Double]() + for belong in belongs { let topUrl = try KissIndex.pickNearTopProductsUrl(belong, date: date) let data = try [VolumeRankResult.OutputDetail].readCsv(fromFile: topUrl, verifyHeader: true) for (index, item) in data.enumerated() { - let score = (30 - index) + let score = Double(30 - index) if let _ = scoreMap[item.shortProductNo] { scoreMap[item.shortProductNo]! += score } @@ -32,17 +32,7 @@ extension KissIndex { } } - let totalScores = scoreMap.reduce(0, { $0 + $1.value }) - let scoreArray = scoreMap.map { ($0.key, $0.value) }.sorted(by: { $0.1 > $1.1 }) - - var outputs = [KissIndexResult.Output]() - for array in scoreArray { - let weight = Double(array.1) / Double(totalScores) - let output = KissIndexResult.Output(shortCode: array.0, weight: weight) - outputs.append(output) - } - - writeOutput(outputs, kmi: kmi) + normalizeAndWrite(scoreMap: scoreMap, includeName: true, kmi: kmi) } catch { writeError(error, kmi: kmi) diff --git a/KissMeIndex/Sources/KissIndex.swift b/KissMeIndex/Sources/KissIndex.swift index fe8630b..d524186 100644 --- a/KissMeIndex/Sources/KissIndex.swift +++ b/KissMeIndex/Sources/KissIndex.swift @@ -97,6 +97,107 @@ class KissIndex: KissMe.ShopContext { } +extension KissIndex { + + func collectShorts(date: Date, filter: @escaping (DomesticExtra.Shorts) -> Bool) async throws -> [DomesticExtra.Shorts] { + let shorts = try await withThrowingTaskGroup(of: DomesticExtra.Shorts?.self, returning: [DomesticExtra.Shorts].self) { taskGroup in + let all = getAllProducts() + let yyyyMMdd = date.yyyyMMdd + + for item in all { + taskGroup.addTask { + let shortsUrl = KissIndex.pickNearShortsUrl(productNo: item.shortCode, date: date) + + let shorts = try [DomesticExtra.Shorts].readCsv(fromFile: shortsUrl) + let targetShorts = shorts.filter { $0.stockBusinessDate == yyyyMMdd } + + if let aShorts = targetShorts.first, filter(aShorts) { + return aShorts + } + return nil + } + } + + var taskResult = [DomesticExtra.Shorts]() + for try await result in taskGroup.compactMap( { $0 }) { + taskResult.append(result) + } + return taskResult + } + return shorts + } + + func collectPrices(date: Date, filter: @escaping (CapturePrice) -> Bool) async throws -> [CapturePrice] { + let prices = try await withThrowingTaskGroup(of: CapturePrice?.self, returning: [CapturePrice].self) { taskGroup in + let all = getAllProducts() + let yyyyMMdd = date.yyyyMMdd + let dateHHmmss = date.HHmmss + + for item in all { + taskGroup.addTask { + let pricesUrl = KissIndex.pickNearPricesUrl(productNo: item.shortCode, date: date) + let prices = try [CapturePrice].readCsv(fromFile: pricesUrl) + let targetPrices = prices.filter { $0.stockBusinessDate == yyyyMMdd && $0.captureTime <= dateHHmmss } + .sorted(by: { dateHHmmss.diffSecondsTwoHHmmss($0.captureTime) < dateHHmmss.diffSecondsTwoHHmmss($1.captureTime) }) + + if let price = targetPrices.first, filter(price) { + return price + } + return nil + } + } + + var taskResult = [CapturePrice]() + for try await result in taskGroup.compactMap( { $0 }) { + taskResult.append(result) + } + return taskResult + } + return prices + } + + static var shopProductsUrl: URL { + URL.currentDirectory().appending(path: "data/shop-products.csv") + } + + private static func pickNearShortsUrl(productNo: String, date: Date) -> URL { + let subPath = "data/\(productNo)/shorts" + let monthFile = "shorts-\(date.yyyyMM01).csv" + + return URL.currentDirectory().appending(path: "\(subPath)/\(monthFile)") + } + + private static func pickNearPricesUrl(productNo: String, date: Date) -> URL { + let subPath = "data/\(productNo)/price" + let priceFile = "prices.csv" + + // TODO: work month file + //let monthFile = "prices-\(date.yyyyMM01).csv" + + return URL.currentDirectory().appending(path: "\(subPath)/\(priceFile)") + } +} + + +extension KissIndex { + + func normalizeAndWrite(scoreMap: [String: Double], includeName: Bool = false, kmi: KissIndexType) { + let totalScores = scoreMap.reduce(0, { $0 + $1.value }) + let scoreArray = scoreMap.map { ($0.key, $0.value) }.sorted(by: { $0.1 > $1.1 }) + + var outputs = [KissIndexResult.Output]() + for array in scoreArray { + let weight = Double(array.1) / Double(totalScores) + let name: String? = (includeName ? getProduct(shortCode: array.0)?.itemName: nil) + let output = KissIndexResult.Output(shortCode: array.0, productName: name, weight: weight) + outputs.append(output) + } + + writeOutput(outputs, kmi: kmi) + } +} + + extension String { var kmiIndex: String? { guard utf8.count == 8, String(prefix(4)).uppercased() == "KMI-" else { @@ -108,4 +209,16 @@ extension String { } return "KMI-\(index)" } + + func diffSecondsTwoHHmmss(_ another: String) -> TimeInterval { + guard let (hour, min, sec) = self.HHmmss else { + return Double.greatestFiniteMagnitude + } + guard let (dHour, dMin, dSec) = another.HHmmss else { + return Double.greatestFiniteMagnitude + } + let seconds = (hour * 60 * 60 + min * 60 + sec) + let dSeconds = (dHour * 60 * 60 + dMin * 60 + dSec) + return TimeInterval(seconds - dSeconds) + } } diff --git a/documents/KMI/KMI-0003.md b/documents/KMI/KMI-0003.md index e64545c..0a39d97 100644 --- a/documents/KMI/KMI-0003.md +++ b/documents/KMI/KMI-0003.md @@ -1,2 +1,50 @@ # KMI-0003 +## How to + +PER 의 적정값에 해당되는 종목을 선별합니다. + +**한국투자증권**에서 우선적으로 제공되는 PER 를 사용합니다. + +차후에는 PER 값을 예측하도록 개선될 예정입니다. + +적합한 PER 를 선별하는 기준은 다음과 같습니다. + +* PER 10.0 이하에 대해서는 (10 - PER) * 5 점수를 부여합니다. +* PER 20.0 이하에 대해서는 (20 - PER) * 4 점수를 부여합니다. +* PER 20.0 초과의 종목은 선별되지 않습니다. + + +## Usage + +다음은 지표 데이터(index set)를 추출하는 방법입니다. + +```bash +./KissMeIndex KMI-0003 20230621 100000 config.json +{ + "code": 200, + "message": "OK", + "kmi": "KMI-0003", + "output": [ + { + "weight": 0.054464283149246694, + "shortCode": "251370", + "productName": "와이엠티" + }, + { + "weight": 0.053163216418169394, + "shortCode": "112610", + "productName": "씨에스윈드" + }, + { + "weight": 0.050551000379436384, + "shortCode": "000520", + "productName": "삼일제약" + }, +... +``` + + +## Configuration + +현재 여기에는 환경설정 정보가 없습니다. diff --git a/documents/KMI/KMI-0005.md b/documents/KMI/KMI-0005.md index ee84fc8..0e3b770 100644 --- a/documents/KMI/KMI-0005.md +++ b/documents/KMI/KMI-0005.md @@ -41,7 +41,7 @@ ### INPUT * (indexApp) KMI-(number) (date) (time) (config.json) - * (indexApp) 는 지표를 추출하는 앱입니다. INPUT, OUTPUT 형식만 맞출 수 있다면, 다양한 도구를 통해서 만들 수 있습니다. + * (indexApp) 는 지표를 추출하는 app binary 입니다. INPUT, OUTPUT 형식만 맞출 수 있다면, 다양한 도구를 통해서 만들 수 있습니다. * KMI-(number) 는 고유의 지표 번호입니다. 하나의 app 에서 여러 지표를 추출할 수 있습니다. * (date) 는 yyyyMMdd 형식의 날짜입니다. * (time) 는 HHmmss 형식의 시간입니다. @@ -51,12 +51,13 @@ json 파일 형식으로 결과를 제공합니다. -* code: 에러코드를 의미합니다. `200` 은 성공. -* message: 상세한 메시지를 의미합니다. 성공하면 `OK`. -* kmi: 요청에 제공되는 KMI 지표를 의미합니다. -* output: 지표 데이터입니다. - * shortCode : 추천종목 코드 번호입니다. - * weight : [-1.0, 1.0] 사이의 가중치 값입니다. 음수이면 매도 성향이고, 양수이면 매수성향입니다. +* `code`: 에러코드. `200` 은 성공. +* `message`: 상세한 메시지. 성공하면 `OK`. +* `kmi`: 요청에 제공되는 KMI 지표 +* `output`: 지표 데이터 + * `shortCode` : 추천종목 코드 번호 + * `productName` : 종목명 + * `weight` : [-1.0, 1.0] 사이의 가중치 값. 음수이면 매도 성향이고, 양수이면 매수성향. ### Configuration diff --git a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme index c2c08d6..0a12f9d 100644 --- a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme +++ b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme @@ -53,7 +53,7 @@