Files
KissMe/KissMeIndex/Sources/KissIndex.swift
2023-06-29 13:18:05 +09:00

367 lines
15 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
print("\(appName) KMI-0001 yyyyMMdd HHmmss [config.json]")
return
}
guard let kmi = CommandLine.arguments[1].kmiIndex else {
print("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 {
print("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:
print("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)
} catch {
assertionFailure(error.localizedDescription)
}
}
func writeOutput(_ output: [KissIndexResult.Output], kmi: KissIndexType) {
let result = KissIndexResult(code: 200, message: "OK", kmi: kmi.rawValue, output: output)
do {
let jsonData = try JSONEncoder().encode(result)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
} catch {
assertionFailure(error.localizedDescription)
}
}
// 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)
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 * 7 {
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)
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 * 7 {
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
}
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)
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 * 7 {
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 normalizeAndWrite(scoreMap: [String: Double], includeName: Bool = false, kmi: KissIndexType) {
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 {
print("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)
}
writeOutput(outputs, kmi: kmi)
}
}
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)
}
}