Implement KMI-0003 index set

This commit is contained in:
2023-06-22 18:50:20 +09:00
parent a113749d48
commit bb48abd3d3
8 changed files with 244 additions and 126 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
현재 여기에는 환경설정 정보가 없습니다.

View File

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

View File

@@ -53,7 +53,7 @@
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "KMI-0002 20230616 100000"
argument = "KMI-0003 20230621 100000"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>