Implement KMI-0004 index set

This commit is contained in:
2023-06-23 14:02:04 +09:00
parent e9ab446a89
commit e5c56c5d12
13 changed files with 210 additions and 141 deletions

View File

@@ -59,6 +59,63 @@ extension Date {
let fullDate = yyyyMMdd + HHmmss
return dateFormatter.date(from: fullDate)
}
public var yyyyMMdd_split: (year: Int, month: Int, day: Int)? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let components = Calendar.current.dateComponents(sets, from: self)
guard let year = components.year, let month = components.month, let day = components.day else {
return nil
}
return (year, month, day)
}
public var HHmmss_split: (hour: Int, minute: Int, second: Int)? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let components = Calendar.current.dateComponents(sets, from: self)
guard let hour = components.hour, let minute = components.minute, let second = components.second else {
return nil
}
return (hour, minute, second)
}
public func changing(hour: Int?, min: Int?, sec: Int?, timeZone: String = "KST") -> Date? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
var components = Calendar.current.dateComponents(sets, from: self)
components.timeZone = TimeZone(abbreviation: timeZone)
if let hour = hour {
components.hour = hour
}
if let min = min {
components.minute = min
}
if let sec = sec {
components.second = sec
}
components.nanosecond = 0
return Calendar.current.date(from: components)
}
public mutating func change(hour: Int, min: Int, sec: Int, timeZone: String = "KST") {
if let newDate = changing(hour: hour, min: min, sec: sec, timeZone: timeZone) {
self = newDate
}
}
public func changing(year: Int, month: Int, day: Int, timeZone: String = "KST") -> Date? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
var components = Calendar.current.dateComponents(sets, from: self)
components.timeZone = TimeZone(abbreviation: timeZone)
components.year = year
components.month = month
components.day = day
return Calendar.current.date(from: components)
}
public mutating func change(year: Int, month: Int, day: Int, timeZone: String = "KST") {
if let newDate = changing(year: year, month: month, day: day, timeZone: timeZone) {
self = newDate
}
}
}

View File

@@ -13,6 +13,7 @@ public struct Domestic {
public typealias CandlePeriod = PeriodPriceResult.OutputPrice
public typealias Top = VolumeRankResult.OutputDetail
public typealias CurrentPrice = CurrentPriceResult.OutputDetail
public typealias Investor = InvestorVolumeResult.OutputDetail
}

View File

@@ -60,66 +60,6 @@ extension String {
}
extension Date {
public var yyyyMMdd_split: (year: Int, month: Int, day: Int)? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let components = Calendar.current.dateComponents(sets, from: self)
guard let year = components.year, let month = components.month, let day = components.day else {
return nil
}
return (year, month, day)
}
public var HHmmss_split: (hour: Int, minute: Int, second: Int)? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let components = Calendar.current.dateComponents(sets, from: self)
guard let hour = components.hour, let minute = components.minute, let second = components.second else {
return nil
}
return (hour, minute, second)
}
public func changing(hour: Int?, min: Int?, sec: Int?, timeZone: String = "KST") -> Date? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
var components = Calendar.current.dateComponents(sets, from: self)
components.timeZone = TimeZone(abbreviation: timeZone)
if let hour = hour {
components.hour = hour
}
if let min = min {
components.minute = min
}
if let sec = sec {
components.second = sec
}
components.nanosecond = 0
return Calendar.current.date(from: components)
}
public mutating func change(hour: Int, min: Int, sec: Int, timeZone: String = "KST") {
if let newDate = changing(hour: hour, min: min, sec: sec, timeZone: timeZone) {
self = newDate
}
}
public func changing(year: Int, month: Int, day: Int, timeZone: String = "KST") -> Date? {
let sets: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
var components = Calendar.current.dateComponents(sets, from: self)
components.timeZone = TimeZone(abbreviation: timeZone)
components.year = year
components.month = month
components.day = day
return Calendar.current.date(from: components)
}
public mutating func change(year: Int, month: Int, day: Int, timeZone: String = "KST") {
if let newDate = changing(year: year, month: month, day: day, timeZone: timeZone) {
self = newDate
}
}
}
extension FileManager {
/// period: If nil, all period of csv collected.

View File

@@ -15,7 +15,7 @@ extension KissConsole {
if let output = result.output {
print("Total output: \(output.count) productNo: \(productNo)")
var months = [String: [InvestorVolumeResult.OutputDetail]]()
var months = [String: [Domestic.Investor]]()
for item in output {
let yyyyMM = String(item.stockBusinessDate.prefix(6))
if let _ = months[yyyyMM] {

View File

@@ -46,7 +46,7 @@ extension KissConsole {
return true
}
private func printCurrentPrice(_ output: CurrentPriceResult.OutputDetail) {
private func printCurrentPrice(_ output: Domestic.CurrentPrice) {
let productName = getProduct(shortCode: output.shortProductCode)?.itemName ?? ""
print("\t종목명: ", productName)
print("\t업종명: ", output.koreanMarketName, output.koreanBusinessTypeName ?? "")

View File

@@ -23,7 +23,7 @@ extension KissConsole {
}
// MARK: prices.csv
// MARK: prices-yyyyMM01.csv
extension KissConsole {
private func onTest_001() {

View File

@@ -8,21 +8,3 @@
import Foundation
KissConsole().run()
// 1,000 . (...)
// , prices.csv comma .
/*
import KissMe
let path = URL(filePath: "/Users/ened/Kiss/KissMe/bin/data/065350/price/prices.csv")
let data = try [CapturePrice].readCsv(fromFile: path, verifyHeader: true)
if let last = data.last {
print(last)
}
let shopProductsUrl = URL.currentDirectory().appending(path: "data/shop-products.csv")
let context = ShopContext()
context.loadShop(url: shopProductsUrl)
*/

View File

@@ -274,9 +274,9 @@ private func split_investor_csv() {
let productNoDir = investorDir.deletingLastPathComponent()
let productNo = productNoDir.lastPathComponent
let output = try [InvestorVolumeResult.OutputDetail].readCsv(fromFile: url, verifyHeader: true)
let output = try [Domestic.Investor].readCsv(fromFile: url, verifyHeader: true)
var months = [String: [InvestorVolumeResult.OutputDetail]]()
var months = [String: [Domestic.Investor]]()
for item in output {
let yyyyMM = String(item.stockBusinessDate.prefix(6))
if let _ = months[yyyyMM] {

View File

@@ -15,23 +15,71 @@ extension KissIndex {
loadShop(url: KissIndex.shopProductsUrl)
}
// TODO: config ,
let semaphore = DispatchSemaphore(value: 0)
Task {
let netSumLock = NSLock()
var netSum = [String: Int]()
/// 3 , .
/// 3 , .
///
let investors = try await collectInvestors(date: date, recentCount: 3) { productNo, investors in
if investors.count == 3,
let netBuying1 = Int(investors[0].foreignNetBuyingQuantity),
let netBuying2 = Int(investors[1].foreignNetBuyingQuantity),
let netBuying3 = Int(investors[2].foreignNetBuyingQuantity) {
if (netBuying1 > 0 && netBuying2 > 0 && netBuying3 > 0) ||
(netBuying1 < 0 && netBuying2 < 0 && netBuying3 < 0) {
let sum = netBuying1 + netBuying2 + netBuying3
netSumLock.lock()
if let _ = netSum[productNo] {
netSum[productNo]! += sum
}
else {
netSum[productNo] = sum
}
netSumLock.unlock()
return true
}
}
return false
}
var scoreMap = [String: Double]()
for (productNo, _) in investors {
let score: Double
if let sum = netSum[productNo] {
if sum < 0 {
score = -log10(abs(Double(sum)))
}
else {
score = log10(Double(sum))
}
}
else {
score = 0
}
if let _ = scoreMap[productNo] {
scoreMap[productNo]! += score
}
else {
scoreMap[productNo] = score
}
}
// TODO: work
normalizeAndWrite(scoreMap: scoreMap, includeName: true, kmi: kmi)
/// << >>
/// 3 .
semaphore.signal()
}
semaphore.wait()
}
private func pickNearInvestorUrl(productNo: String, date: Date) {
let subPath = "data/\(productNo)/investor"
let subFile = "investor-\(date.yyyyMM01).csv"
let fileUrl = URL.currentDirectory().appending(path: subFile)
// TODO: work
}
}

View File

@@ -155,6 +155,53 @@ extension KissIndex {
}
return prices
}
func collectInvestors(date: Date, recentCount: Int, filter: @escaping (String, [Domestic.Investor]) -> Bool) async throws -> [(String,Domestic.Investor)] {
let investors = try await withThrowingTaskGroup(of: (String, Domestic.Investor)?.self, returning: [(String, Domestic.Investor)].self) { taskGroup in
let all = getAllProducts()
let (yyyy, mm, dd) = date.yyyyMMdd_split!
for item in all {
taskGroup.addTask {
let investorUrl = KissIndex.pickNearInvestorUrl(productNo: item.shortCode, date: date)
let prevMonthDate = date.changing(year: yyyy, month: mm-1, day: dd)!
let prevMonthInvestorUrl = KissIndex.pickNearInvestorUrl(productNo: item.shortCode, date: prevMonthDate)
var investors = try [Domestic.Investor].readCsv(fromFile: investorUrl)
if let prevInvestors = try? [Domestic.Investor].readCsv(fromFile: prevMonthInvestorUrl) {
investors.append(contentsOf: prevInvestors)
}
var targetInvestors = [Domestic.Investor]()
var prevDays = 1
var desiredDate: Date? = date
while desiredDate != nil, prevDays < 20 {
let selected = investors.filter { $0.stockBusinessDate == desiredDate!.yyyyMMdd }
targetInvestors.append(contentsOf: selected)
if targetInvestors.count >= recentCount {
break
}
desiredDate = desiredDate!.changing(year: yyyy, month: mm, day: dd-prevDays)
prevDays += 1
}
if filter(item.shortCode, targetInvestors), let first = targetInvestors.first {
return (item.shortCode, first)
}
return nil
}
}
var taskResult = [(String, Domestic.Investor)]()
for try await result in taskGroup.compactMap( { $0 }) {
taskResult.append(result)
}
return taskResult
}
return investors
}
}
@@ -173,13 +220,17 @@ extension KissIndex {
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"
let priceFile = "prices-\(date.yyyyMM01).csv"
return URL.currentDirectory().appending(path: "\(subPath)/\(priceFile)")
}
private static func pickNearInvestorUrl(productNo: String, date: Date) -> URL {
let subPath = "data/\(productNo)/investor"
let investorFile = "investor-\(date.yyyyMM01).csv"
return URL.currentDirectory().appending(path: "\(subPath)/\(investorFile)")
}
}
@@ -187,12 +238,13 @@ extension KissIndex {
func normalizeAndWrite(scoreMap: [String: Double], includeName: Bool = false, kmi: KissIndexType) {
let totalScores = scoreMap.reduce(0, { $0 + $1.value })
let positiveTotalScores = scoreMap.reduce(0, { $0 + ($1.value > 0 ? $1.value: 0) })
let negativeTotalScores = abs(scoreMap.reduce(0, { $0 + ($1.value < 0 ? $1.value: 0) }))
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 weight = Double(array.1) / (array.1 > 0 ? Double(positiveTotalScores): Double(negativeTotalScores))
let name: String? = (includeName ? getProduct(shortCode: array.0)?.itemName: nil)
let output = KissIndexResult.Output(shortCode: array.0, productName: name, weight: weight)
outputs.append(output)

View File

@@ -24,7 +24,7 @@ Command | 설명
`cancel (PNO) (ONO) (수량)` | 주문 내역의 일부를 취소. (수량) 에 -82 로 입력하면 전체수량.
WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) 에 -82 로 입력하면 전체수량.
`open bag` | 보유 종목 열람. 보유 주식은 **data/account-stocks.csv** 파일로 저장. 잔고 상황은 **data/account-amount.csv** 파일로 저장.
`now [PNO]` | 종목의 현재가 열람. PNO 은 생략 가능. **data/(PNO)/price/prices.csv** 파일로 저장.
`now [PNO]` | 종목의 현재가 열람. PNO 은 생략 가능. **data/(PNO)/price/prices-(yyyyMM01).csv** 파일로 저장.
`candle [PNO]` | 종목의 분봉 열람. PNO 은 생략 가능. **data/(PNO)/min/candle-(yyyyMMdd).csv** 파일로 저장.
`candle all [resume]` | 모든 종목의 분봉 열람. cron job 으로 돌리기 위해서 추가. **data/(PNO)/min/candle-(yyyyMMdd).csv** 파일로 저장. (resume) 을 기입하면, 이미 받은 파일은 검사하여, 데이터에 오류가 있으면 다시 받고 오류가 없으면 새롭게 열람하지 않음.
`candle day [PNO]` | 종목의 최근 250일 동안의 일봉 열람. PNO 은 생략 가능. **data/(PNO)/day/candle-(yyyyMMdd).csv** 파일로 저장.
@@ -32,10 +32,10 @@ WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량)
`candle week [PNO]` | 종목의 최근 52주 동안의 주봉 열람. PNO 은 생략 가능. **data/(PNO)/week/candle-(yyyyMMdd).csv** 파일로 저장.
`candle week all [resume]` | 모든 종목의 최근 52주 동안의 주봉 열람. cron job 으로 오전 장이 시작전에 미리 수집. **data/(PNO)/week/candle-(yyyyMMdd).csv** 파일로 저장.
`candle validate (기간)` | (기간) 타입의 모든 csv 파일에 대해서 데이터가 유효한지 검사. (기간) 으로는 **min**, **day**, **week** 을 지정하고, 생략되면 **min** 으로 간주.
`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 [resume]` | 모든 종목의 공매도 잔고를 열람. **data/shorts/(yyyy)/shorts-(yyyyMMdd).csv** 파일로 저장.
`investor [PNO]` | 종목의 투자자 거래량 열람. PNO 은 생략 가능. **data/(PNO)/investor/investor-(yyyyMM01).csv** 파일로 저장.
`investor all` | 모든 종목의 투자자 거래량 열람. **data/(PNO)/investor/investor-(yyyyMM01).csv** 파일로 저장.
`shorts [PNO]` | 공매도 잔고를 열람. PNO 은 생략 가능. **data/shorts/(yyyy)/shorts-(yyyyMM01).csv** 파일로 저장.
`shorts all [resume]` | 모든 종목의 공매도 잔고를 열람. **data/shorts/(yyyy)/shorts-(yyyyMM01).csv** 파일로 저장.
`load shop` | data/shop-products.csv 로부터 전체 상품을 로딩.
`update shop` | **금융위원회_KRX상장종목정보** 로부터 전체 상품을 얻어서 **data/shop-products.csv** 로 저장.
`look (상품명)` | (상품명) 에 해당되는 PNO 를 표시함.
@@ -67,24 +67,12 @@ KISS_ASSERT_COMMA_CSV_DATA | `true`, `1` 이면, CSV 데이터에 comma 문자
KissMeIndex 는 지표 집합(index set)을 추출하는 도구입니다.
## INPUT
다음은 지표 데이터를 추출하는 예제입니다.
INPUT 으로는 다음과 같은 값을 제공합니다.
* 환경설정 : config.json 파일로 특정 지표에서 보정으로 필요로 하는 설정을 기입합니다.
* 현재시간 : timestamp 값을 반드시 필요로 합니다. simulator 에서도 이 기능을 활용할 수 있습니다.
## OUTPUT
OUTPUT 은 다음과 같은 값을 json 형태로 제공합니다.
* shortCode : 추천종목 코드 번호입니다.
* weight : [-1.0, 1.0] 사이의 가중치 값입니다. 음수이면 매도 성향이고, 양수이면 매수성향입니다.
## Example
첫라인은 INPUT 이고, 이후의 json 데이터는 OUTPUT 입니다.
```bash
./KissMeIndex KMI-0001 20230616 100000 config.json
./KissMeIndex KMI-0005 20230616 105900 config.json
{
"code": 200,
"message": "OK",
@@ -102,6 +90,27 @@ OUTPUT 은 다음과 같은 값을 json 형태로 제공합니다.
}
```
### INPUT
* (indexApp) KMI-(number) (date) (time) (config.json)
* (indexApp) 는 지표를 추출하는 app binary 입니다. INPUT, OUTPUT 형식만 맞출 수 있다면, 다양한 도구를 통해서 만들 수 있습니다.
* KMI-(number) 는 고유의 지표 번호입니다. 하나의 app 에서 여러 지표를 추출할 수 있습니다.
* (date) 는 yyyyMMdd 형식의 날짜입니다.
* (time) 는 HHmmss 형식의 시간입니다.
* (config.json) 는 config 로 상세한 설정을 할 수 있는 json 파일입니다. 여기의 내용은 (indexApp) 과 KMI-(number) 에 따라서 다르게 구성될 수 있습니다.
### OUTPUT
json 파일 형식으로 결과를 제공합니다.
* `code`: 에러코드입니다. `200` 은 성공입니다.
* `message`: 상세한 메시지를 의미합니다. 성공하면 `OK` 로 표시합니다.
* `kmi`: 데이터로 제공되는 KMI 지표입니다.
* `output`: 지표 데이터 배열입니다.
* `shortCode` : 추천종목 코드 번호
* `productName` : 종목명
* `weight` : [-1.0, 1.0] 사이의 가중치 값입니다. 음수이면 매도 성향이고, 양수이면 매수성향입니다.
# KissMeMatrix

View File

@@ -39,26 +39,6 @@
}
```
### INPUT
* (indexApp) KMI-(number) (date) (time) (config.json)
* (indexApp) 는 지표를 추출하는 app binary 입니다. INPUT, OUTPUT 형식만 맞출 수 있다면, 다양한 도구를 통해서 만들 수 있습니다.
* KMI-(number) 는 고유의 지표 번호입니다. 하나의 app 에서 여러 지표를 추출할 수 있습니다.
* (date) 는 yyyyMMdd 형식의 날짜입니다.
* (time) 는 HHmmss 형식의 시간입니다.
* (config.json) 는 config 로 상세한 설정을 할 수 있는 json 파일입니다. 여기의 내용은 (indexApp) 과 KMI-(number) 에 따라서 다르게 구성될 수 있습니다.
### OUTPUT
json 파일 형식으로 결과를 제공합니다.
* `code`: 에러코드. `200` 은 성공.
* `message`: 상세한 메시지. 성공하면 `OK`.
* `kmi`: 요청에 제공되는 KMI 지표
* `output`: 지표 데이터
* `shortCode` : 추천종목 코드 번호
* `productName` : 종목명
* `weight` : [-1.0, 1.0] 사이의 가중치 값. 음수이면 매도 성향이고, 양수이면 매수성향.
### Configuration
(config.json) 현재 지원하는 환경설정 정보가 없습니다.

View File

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