Implement KMI-0003 index set
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
현재 여기에는 환경설정 정보가 없습니다.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "KMI-0002 20230616 100000"
|
||||
argument = "KMI-0003 20230621 100000"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
|
||||
Reference in New Issue
Block a user