diff --git a/KissMe/Sources/Context/ShopContext.swift b/KissMe/Sources/Context/ShopContext.swift index a438ebf..876973a 100644 --- a/KissMe/Sources/Context/ShopContext.swift +++ b/KissMe/Sources/Context/ShopContext.swift @@ -49,25 +49,14 @@ open class ShopContext { extension ShopContext { - public func loadShop(url: URL, profile: Bool = false) { - let appTime1 = Date.appTime + public func loadShop(url: URL, loggable: Bool = false) { guard let stringCsv = try? String(contentsOfFile: url.path) else { return } - let appTime2 = Date.appTime - if profile { - print("\tloading file \(appTime2 - appTime1) elapsed") - } - var products = [String: [DomesticShop.Product]]() let rows = stringCsv.split(separator: "\n") - let appTime3 = Date.appTime - if profile { - print("\trow split \(appTime3 - appTime2) elapsed") - } - for (i, row) in rows.enumerated() { let array = row.split(separator: ",", omittingEmptySubsequences: false).map { String($0) } if i == 0, array[0] == "baseDate" { @@ -82,14 +71,12 @@ extension ShopContext { products[product.itemName] = [product] } } - let appTime4 = Date.appTime - if profile { - print("\tparse product \(appTime4 - appTime3) elapsed") - } setProducts(products) - let totalCount = products.reduce(0, { $0 + $1.value.count }) - print("load products \(totalCount) with \(products.count) key") + if loggable { + let totalCount = products.reduce(0, { $0 + $1.value.count }) + print("load products \(totalCount) with \(products.count) key") + } } private func setProducts(_ products: [String: [DomesticShop.Product]]) { diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index 7325386..fe2502a 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -851,7 +851,7 @@ extension KissConsole { private func onLoadShop() async { return await withUnsafeContinuation { continuation in - self.loadShop(url: KissConsole.shopProductsUrl) + self.loadShop(url: KissConsole.shopProductsUrl, loggable: true) continuation.resume() } } diff --git a/KissMeIndex/Sources/KissIndex+0002.swift b/KissMeIndex/Sources/KissIndex+0002.swift index 985dfcf..8583048 100644 --- a/KissMeIndex/Sources/KissIndex+0002.swift +++ b/KissMeIndex/Sources/KissIndex+0002.swift @@ -9,37 +9,37 @@ import Foundation import KissMe +private struct KMI_0002_Config: Codable { + + enum Strategy: String, Codable { + case balanceRatioIncreased = "BALANCE_RATIO_INCREASED" + case shorts3DayIncreased = "3DAY_INCREASED" + } + + let strategy: Strategy + + static let `default`: Strategy = .balanceRatioIncreased +} + + extension KissIndex { func indexSet_0002(date: Date, config: String?, kmi: KissIndexType) { if productsCount == 0 { loadShop(url: KissIndex.shopProductsUrl) } + let strategy = loadConfig(config) let semaphore = DispatchSemaphore(value: 0) Task { do { - let shorts = try await collectShorts(date: date) { aShorts in - /// 공매도 잔고 비중 (1%) 이상 종목 리스트 - if let ratio = Double(aShorts.shortSellingBalanceRatio), ratio >= 0.01 { - return true - } - return false + switch strategy { + case .balanceRatioIncreased: + try await balanceRatioIncreased(date: date, kmi: kmi) + case .shorts3DayIncreased: + try await shorts3DayIncreased(date: date, kmi: kmi) } - //print(shorts.count) - - 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) } @@ -47,4 +47,121 @@ extension KissIndex { } semaphore.wait() } + + + private func loadConfig(_ config: String?) -> KMI_0002_Config.Strategy { + var strategy = KMI_0002_Config.default + if let config = config { + do { + let configUrl = URL.currentDirectory().appending(path: config) + let data = try Data(contentsOf: configUrl, options: .uncached) + let configData = try JSONDecoder().decode(KMI_0002_Config.self, from: data) + strategy = configData.strategy + } catch { + } + } + return strategy + } + + + /// 3일전보다 공매도 잔고 비중이 일정량(0.01%) 상승한 종목을 추려낸다. + /// + private func balanceRatioIncreased(date: Date, kmi: KissIndexType) async throws { + let increasedRatioLock = NSLock() + var increasedRatio = [String: Double]() + + let _ = try await collectShorts(date: date, recentCount: 3) { productNo, shorts in + if shorts.count >= 3, let endShort = shorts.first, let startShort = shorts.last { + if let endRatio = Double(endShort.shortSellingBalanceRatio), + let startRatio = Double(startShort.shortSellingBalanceRatio) { + let increaseRatio = endRatio - startRatio + if abs(increaseRatio) > 0.01 { + + increasedRatioLock.lock() + if let _ = increasedRatio[productNo] { + increasedRatio[productNo]! += increaseRatio + } + else { + increasedRatio[productNo] = increaseRatio + } + increasedRatioLock.unlock() + return true + } + } + } + return false + } + + var scoreMap = [String: Double]() + for (productNo, ratio) in increasedRatio { + let score = -ratio + + if let _ = scoreMap[productNo] { + scoreMap[productNo]! += score + } + else { + scoreMap[productNo] = score + } + } + + normalizeAndWrite(scoreMap: scoreMap, includeName: true, kmi: kmi) + } + + + /// 3일동안 누적 공매도 체결 수량이 상장주식수의 일정 비율(0.01%) 이상 증가한 종목을 추려낸다. + /// + private func shorts3DayIncreased(date: Date, kmi: KissIndexType) async throws { + let sumRatioLock = NSLock() + var sumRatio = [String: Double]() + + let prices = try await collectPrices(date: date, recentCount: 3) { prices in + if prices.count >= 3 { + var curDay: String? = nil + var aPriceEachDay = [CapturePrice]() + for price in prices { + if curDay != price.stockBusinessDate { + aPriceEachDay.append(price) + curDay = price.stockBusinessDate + } + } + + /// lastShortSellingConclusionQuantity 값에 음수는 없음. + let sumConclusionQuantity = aPriceEachDay.reduce(0, { $0 + (Int($1.lastShortSellingConclusionQuantity) ?? 0) }) + + let ratio = Double(sumConclusionQuantity * 100) / Double(prices[0].listedStockCount)! + //print("ratio: \(ratio), sumConclusionQuantity: \(sumConclusionQuantity)") + guard ratio > 0.01 else { + return false + } + + let productNo = prices[0].shortProductCode + + sumRatioLock.lock() + if let _ = sumRatio[productNo] { + sumRatio[productNo]! += ratio + } + else { + sumRatio[productNo] = ratio + } + sumRatioLock.unlock() + return true + } + return false + } + + var scoreMap = [String: Double]() + for price in prices { + let productNo = price.shortProductCode + let score = -(sumRatio[productNo] ?? 0) + + if let _ = scoreMap[productNo] { + scoreMap[productNo]! += score + } + else { + scoreMap[productNo] = score + } + } + + normalizeAndWrite(scoreMap: scoreMap, includeName: true, kmi: kmi) + } } diff --git a/KissMeIndex/Sources/KissIndex+0004.swift b/KissMeIndex/Sources/KissIndex+0004.swift index a813970..0d3a8f3 100644 --- a/KissMeIndex/Sources/KissIndex+0004.swift +++ b/KissMeIndex/Sources/KissIndex+0004.swift @@ -8,7 +8,7 @@ import Foundation -struct KMI_0004_Config: Codable { +private struct KMI_0004_Config: Codable { enum Strategy: String, Codable { case foreigner3DayFocusing = "3DAY_FOCUSING" diff --git a/KissMeIndex/Sources/KissIndex.swift b/KissMeIndex/Sources/KissIndex.swift index 0ebfa4d..35c58ea 100644 --- a/KissMeIndex/Sources/KissIndex.swift +++ b/KissMeIndex/Sources/KissIndex.swift @@ -104,20 +104,43 @@ class KissIndex: KissMe.ShopContext { extension KissIndex { - func collectShorts(date: Date, filter: @escaping (DomesticExtra.Shorts) -> Bool) async throws -> [DomesticExtra.Shorts] { + func collectShorts(date: Date, recentCount: Int, filter: @escaping (String, [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 + let (yyyy, mm, dd) = date.yyyyMMdd_split! 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 } + let prevMonthDate = date.changing(year: yyyy, month: mm-1, day: dd)! + let prevMonthShortsUrl = KissIndex.pickNearShortsUrl(productNo: item.shortCode, date: prevMonthDate) - if let aShorts = targetShorts.first, filter(aShorts) { - return aShorts + var shorts = try [DomesticExtra.Shorts].readCsv(fromFile: shortsUrl) + if let prevShorts = try? [DomesticExtra.Shorts].readCsv(fromFile: prevMonthShortsUrl) { + shorts.append(contentsOf: prevShorts) + } + + var targetShorts = [DomesticExtra.Shorts]() + var collectedDay = 0 + var prevDays = 1 + var desiredDate: Date? = date + + while desiredDate != nil, prevDays < recentCount * 7 { + let selected = shorts.filter { $0.stockBusinessDate == desiredDate!.yyyyMMdd } + targetShorts.append(contentsOf: selected) + if selected.count > 0 { + collectedDay += 1 + } + if collectedDay >= recentCount { + break + } + desiredDate = desiredDate!.changing(year: yyyy, month: mm, day: dd-prevDays) + prevDays += 1 + } + + if filter(item.shortCode, targetShorts), let first = targetShorts.first { + return first } return nil } diff --git a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme index 0147ad6..3a243fc 100644 --- a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme +++ b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme @@ -53,7 +53,7 @@