398 lines
16 KiB
Swift
398 lines
16 KiB
Swift
//
|
|
// KissIndex.swift
|
|
// KissMeIndex
|
|
//
|
|
// Created by ened-book-m1 on 2023/06/20.
|
|
//
|
|
|
|
import Foundation
|
|
import KissMe
|
|
|
|
|
|
enum KissIndexType: String {
|
|
case _0001 = "KMI-0001"
|
|
case _0002 = "KMI-0002"
|
|
case _0003 = "KMI-0003"
|
|
case _0004 = "KMI-0004"
|
|
case _0005 = "KMI-0005"
|
|
case _0006 = "KMI-0006"
|
|
}
|
|
|
|
|
|
class KissIndex: KissMe.ShopContext {
|
|
|
|
func run() {
|
|
guard CommandLine.argc >= 4 else {
|
|
let appName = (CommandLine.arguments[0] as NSString).lastPathComponent
|
|
printError("\(appName) KMI-0001 yyyyMMdd HHmmss [config.json]")
|
|
return
|
|
}
|
|
|
|
guard let kmi = CommandLine.arguments[1].kmiIndex else {
|
|
printError("Invalid KMI index name")
|
|
return
|
|
}
|
|
|
|
let day = CommandLine.arguments[2]
|
|
let hour = CommandLine.arguments[3]
|
|
guard let date = Date.date(yyyyMMdd: day, HHmmss: hour) else {
|
|
printError("Invalid timestamp: \(day) \(hour)")
|
|
return
|
|
}
|
|
|
|
let config: String?
|
|
if CommandLine.argc >= 5 {
|
|
config = CommandLine.arguments[4]
|
|
}
|
|
else {
|
|
config = nil
|
|
}
|
|
|
|
let kmiType = KissIndexType(rawValue: kmi)
|
|
switch kmiType {
|
|
case ._0001:
|
|
indexSet_0001(date: date, config: config, kmi: kmiType!)
|
|
case ._0002:
|
|
indexSet_0002(date: date, config: config, kmi: kmiType!)
|
|
case ._0003:
|
|
indexSet_0003(date: date, config: config, kmi: kmiType!)
|
|
case ._0004:
|
|
indexSet_0004(date: date, config: config, kmi: kmiType!)
|
|
case ._0005:
|
|
indexSet_0005(date: date, config: config, kmi: kmiType!)
|
|
case ._0006:
|
|
indexSet_0006(date: date, config: config, kmi: kmiType!)
|
|
|
|
default:
|
|
printError("Unsupported index set: \(kmi)")
|
|
}
|
|
}
|
|
|
|
|
|
func writeError(_ error: Error, moreMessage: String? = nil, kmi: KissIndexType) {
|
|
var message = error.localizedDescription
|
|
if let moreMessage = moreMessage {
|
|
message += "\n\(moreMessage)"
|
|
}
|
|
|
|
let result = KissIndexResult(code: 300, message: message, kmi: kmi.rawValue, output: [])
|
|
do {
|
|
let jsonData = try JSONEncoder().encode(result)
|
|
//let jsonString = String(data: jsonData, encoding: .utf8)!
|
|
//print(jsonString)
|
|
FileHandle.standardOutput.write(jsonData)
|
|
} catch {
|
|
printError(error)
|
|
}
|
|
}
|
|
|
|
func writeOutput(_ output: [KissIndexResult.Output], kmi: KissIndexType) {
|
|
let result = KissIndexResult(code: 200, message: "OK", kmi: kmi.rawValue, output: output)
|
|
//printError("result......333")
|
|
do {
|
|
let jsonData = try JSONEncoder().encode(result)
|
|
//printError("result......4444 \(jsonData.count)")
|
|
//let jsonString = String(data: jsonData, encoding: .utf8)!
|
|
//printError("result......55555 \(jsonString.count)")
|
|
//print(jsonString)
|
|
FileHandle.standardOutput.write(jsonData)
|
|
//printError("result......666")
|
|
} catch {
|
|
printError(error)
|
|
}
|
|
}
|
|
|
|
// private func isHoliday(_ date: Date) -> Bool {
|
|
//
|
|
// }
|
|
}
|
|
|
|
|
|
extension KissIndex {
|
|
|
|
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 (yyyy, mm, dd) = date.yyyyMMdd_split!
|
|
|
|
for item in all {
|
|
taskGroup.addTask {
|
|
let shortsUrl = KissIndex.pickNearShortsUrl(productNo: item.shortCode, date: date)
|
|
guard shortsUrl.isFileExists == true else {
|
|
return nil
|
|
}
|
|
|
|
let prevMonthDate = date.changing(year: yyyy, month: mm-1, day: dd)!
|
|
let prevMonthShortsUrl = KissIndex.pickNearShortsUrl(productNo: item.shortCode, date: prevMonthDate)
|
|
|
|
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
|
|
var desired_yyyyMMdd = desiredDate!.yyyyMMdd
|
|
|
|
while desiredDate != nil, prevDays < recentCount * 5 {
|
|
let selected = shorts.filter { $0.stockBusinessDate == desired_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)
|
|
desired_yyyyMMdd = desiredDate!.yyyyMMdd
|
|
prevDays += 1
|
|
}
|
|
|
|
if filter(item.shortCode, targetShorts), let first = targetShorts.first {
|
|
return first
|
|
}
|
|
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, recentCount: Int, filter: @escaping ([CapturePrice]) -> Bool) async throws -> [CapturePrice] {
|
|
let prices = try await withThrowingTaskGroup(of: CapturePrice?.self, returning: [CapturePrice].self) { taskGroup in
|
|
let all = getAllProducts()
|
|
let (yyyy, mm, dd) = date.yyyyMMdd_split!
|
|
|
|
for item in all {
|
|
taskGroup.addTask {
|
|
let pricesUrl = KissIndex.pickNearPricesUrl(productNo: item.shortCode, date: date)
|
|
guard pricesUrl.isFileExists == true else {
|
|
return nil
|
|
}
|
|
|
|
let prevMonthDate = date.changing(year: yyyy, month: mm-1, day: dd)!
|
|
let prevMonthInvestorUrl = KissIndex.pickNearInvestorUrl(productNo: item.shortCode, date: prevMonthDate)
|
|
|
|
var prices = try [CapturePrice].readCsv(fromFile: pricesUrl)
|
|
if let prevPrices = try? [CapturePrice].readCsv(fromFile: prevMonthInvestorUrl) {
|
|
prices.append(contentsOf: prevPrices)
|
|
}
|
|
|
|
var targetPrices = [CapturePrice]()
|
|
var collectedDay = 0
|
|
var prevDay = 1
|
|
var desiredDate: Date? = date
|
|
var desired_yyyyMMdd = desiredDate!.yyyyMMdd
|
|
|
|
while desiredDate != nil, prevDay < recentCount * 5 {
|
|
let selected = prices.filter { $0.stockBusinessDate == desired_yyyyMMdd }
|
|
targetPrices.append(contentsOf: selected)
|
|
if selected.count > 0 {
|
|
collectedDay += 1
|
|
}
|
|
if collectedDay >= recentCount {
|
|
break
|
|
}
|
|
desiredDate = desiredDate!.changing(year: yyyy, month: mm, day: dd-prevDay)
|
|
desired_yyyyMMdd = desiredDate!.yyyyMMdd
|
|
prevDay += 1
|
|
//printError(item.shortCode, desired_yyyyMMdd)
|
|
}
|
|
|
|
targetPrices.sort(by: { $0.stockDateTime > $1.stockDateTime })
|
|
if filter(targetPrices), let first = targetPrices.first {
|
|
return first
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var taskResult = [CapturePrice]()
|
|
for try await result in taskGroup.compactMap( { $0 }) {
|
|
taskResult.append(result)
|
|
}
|
|
return taskResult
|
|
}
|
|
return prices
|
|
}
|
|
|
|
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)
|
|
guard pricesUrl.isFileExists == true else {
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 collectedDay = 0
|
|
var prevDays = 1
|
|
var desiredDate: Date? = date
|
|
var desired_yyyyMMdd = desiredDate!.yyyyMMdd
|
|
|
|
/// recentCount * 7 는 일주일에 최소 거래일이 1개는 있다고 판단하여 지정된 상수값
|
|
///
|
|
while desiredDate != nil, prevDays < recentCount * 5 {
|
|
let selected = investors.filter { $0.stockBusinessDate == desired_yyyyMMdd }
|
|
targetInvestors.append(contentsOf: selected)
|
|
if selected.count > 0 {
|
|
collectedDay += 1
|
|
}
|
|
if collectedDay >= recentCount {
|
|
break
|
|
}
|
|
desiredDate = desiredDate!.changing(year: yyyy, month: mm, day: dd-prevDays)
|
|
desired_yyyyMMdd = desiredDate!.yyyyMMdd
|
|
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
|
|
}
|
|
}
|
|
|
|
|
|
extension KissIndex {
|
|
|
|
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-\(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)")
|
|
}
|
|
}
|
|
|
|
|
|
extension KissIndex {
|
|
|
|
func normalizeByTotal(scoreMap: [String: Double], includeName: Bool = false) -> [KissIndexResult.Output] {
|
|
|
|
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) / (array.1 >= 0 ? Double(positiveTotalScores): Double(negativeTotalScores))
|
|
if weight.isNaN {
|
|
printError("Ignored NaN: \(array.1) \(positiveTotalScores) \(negativeTotalScores) from \(array.0)")
|
|
continue
|
|
}
|
|
let name: String? = (includeName ? getProduct(shortCode: array.0)?.itemName: nil)
|
|
let output = KissIndexResult.Output(shortCode: array.0, productName: name, weight: weight)
|
|
outputs.append(output)
|
|
}
|
|
|
|
return outputs
|
|
}
|
|
|
|
func normalizeByScale(scoreMap: [String: Double], includeName: Bool = false, scale: Double) -> [KissIndexResult.Output] {
|
|
let scoreArray = scoreMap.map { ($0.key, $0.value) }.sorted(by: { $0.1 > $1.1 })
|
|
|
|
var outputs = [KissIndexResult.Output]()
|
|
for array in scoreArray {
|
|
let weight = min(scale, Double(array.1)) / scale
|
|
let name: String? = (includeName ? getProduct(shortCode: array.0)?.itemName: nil)
|
|
let output = KissIndexResult.Output(shortCode: array.0, productName: name, weight: weight)
|
|
outputs.append(output)
|
|
}
|
|
|
|
return outputs
|
|
}
|
|
}
|
|
|
|
|
|
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)
|
|
}
|
|
}
|