336 lines
12 KiB
Swift
336 lines
12 KiB
Swift
//
|
|
// KissConsole+DB.swift
|
|
// KissMeConsole
|
|
//
|
|
// Created by ened-book-m1 on 11/3/24.
|
|
//
|
|
|
|
import Foundation
|
|
import KissMe
|
|
import KissMeme
|
|
|
|
|
|
extension KissConsole {
|
|
}
|
|
|
|
func test_build_min_db() {
|
|
// guard let enumerator = FileManager.subPathFiles("data") else {
|
|
// return
|
|
// }
|
|
|
|
//let db = try KissDB(directory: url)
|
|
|
|
//test_check_name_parsed()
|
|
//test_date_time()
|
|
//build_min_db_from_candle_csv()
|
|
test_select_min_db()
|
|
}
|
|
|
|
private func test_field_type() {
|
|
let v1: Int64 = 0
|
|
let v2: Int64 = 1
|
|
let v3: Int64 = 65536
|
|
let v4: Int64 = -2
|
|
|
|
print("\(v1.fieldType)")
|
|
print("\(v2.fieldType)")
|
|
print("\(v3.fieldType)")
|
|
print("\(v4.fieldType)")
|
|
print("done")
|
|
}
|
|
|
|
private func test_date_time() {
|
|
let kissDate = Date.date(yyyyMMdd: "20200101", HHmmss: "000000")
|
|
let timestamp = UInt64(kissDate!.timeIntervalSince1970)
|
|
print("timestamp: \(timestamp)")
|
|
print("kissDate: \(kissDate!.timeIntervalSince2020)")
|
|
print("today: \(UInt32(Date().timeIntervalSince2020))")
|
|
|
|
let value: UInt64 = 1234567890
|
|
let d = Data(value: value)
|
|
print(d.hexString)
|
|
}
|
|
|
|
enum CandleDataFieldType: UInt8 {
|
|
case uint8 = 1 // 8 bits unsigned integer
|
|
case uint16 = 2 // 16 bits unsigned integer
|
|
case uint32 = 4 // 32 bits unsigned integer
|
|
case uint64 = 8 // 64 bits unsigned integer
|
|
case double = 10 // 8 byte float point
|
|
case float = 11 // 4 byte float point
|
|
}
|
|
|
|
extension Int64 {
|
|
var fieldType: CandleDataFieldType {
|
|
let unsignedValue = UInt64(bitPattern: self)
|
|
if unsignedValue & ~UInt64(UInt8.max) == 0 {
|
|
return .uint8
|
|
}
|
|
else if unsignedValue & ~UInt64(UInt16.max) == 0 {
|
|
return .uint16
|
|
}
|
|
else if unsignedValue & ~UInt64(UInt32.max) == 0 {
|
|
return .uint32
|
|
}
|
|
else if unsignedValue & ~UInt64.max == 0 {
|
|
return .uint64
|
|
}
|
|
// If the value cannot be represented as an unsigned integer, check for float or double
|
|
else {
|
|
// Check if the value can be represented as a Float
|
|
if self >= Int64(Float.leastNonzeroMagnitude.bitPattern) && self <= Int64(Float.greatestFiniteMagnitude.bitPattern) {
|
|
return .float
|
|
}
|
|
// Otherwise, use Double
|
|
else {
|
|
return .double
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Domestic.Candle: @retroactive Equatable {
|
|
public static func == (lhs: Domestic.Candle, rhs: Domestic.Candle) -> Bool {
|
|
return
|
|
lhs.stockBusinessDate == rhs.stockBusinessDate &&
|
|
lhs.stockConclusionTime == rhs.stockConclusionTime &&
|
|
lhs.accumulatedTradingAmount == rhs.accumulatedTradingAmount &&
|
|
lhs.currentStockPrice == rhs.currentStockPrice &&
|
|
lhs.stockOpenningPrice == rhs.stockOpenningPrice &&
|
|
lhs.highestStockPrice == rhs.highestStockPrice &&
|
|
lhs.lowestStockPrice == rhs.lowestStockPrice &&
|
|
lhs.conclusionVolume == rhs.conclusionVolume
|
|
}
|
|
}
|
|
|
|
struct CandleData {
|
|
let key: Data
|
|
let data: Data
|
|
|
|
var candleKey: UInt32 { key.value_UInt32 }
|
|
var candleDate: String { Date(timeIntervalSince2020: TimeInterval(key.value_UInt32)).yyyyMMddHHmmss_UTC }
|
|
|
|
init(key: Data, data: Data) {
|
|
self.key = key
|
|
self.data = data
|
|
}
|
|
|
|
init(candle: Domestic.Candle) throws {
|
|
guard let keyDate = candle.stockFullDate.yyyyMMddHHmmss_UTC_toDate else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": stockFullDate = \(candle.stockFullDate)")
|
|
}
|
|
self.key = Data(value: UInt32(keyDate.timeIntervalSince2020))
|
|
|
|
guard let accumulatedTradingAmount = Int64(candle.accumulatedTradingAmount) else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": accumulatedTradingAmount = \(candle.accumulatedTradingAmount)")
|
|
}
|
|
guard let currentStockPrice = Int64(candle.currentStockPrice) else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": currentStockPrice = \(candle.currentStockPrice)")
|
|
}
|
|
guard let stockOpenningPrice = Int64(candle.stockOpenningPrice) else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": stockOpenningPrice = \(candle.stockOpenningPrice)")
|
|
}
|
|
guard let highestStockPrice = Int64(candle.highestStockPrice) else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": highestStockPrice = \(candle.highestStockPrice)")
|
|
}
|
|
guard let lowestStockPrice = Int64(candle.lowestStockPrice) else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": lowestStockPrice = \(candle.lowestStockPrice)")
|
|
}
|
|
guard let conclusionVolume = Int64(candle.conclusionVolume) else {
|
|
throw GeneralError.invalidCandleValue(candle.stockFullDate + ": conclusionVolume = \(candle.conclusionVolume)")
|
|
}
|
|
|
|
let values = [accumulatedTradingAmount, currentStockPrice, stockOpenningPrice, highestStockPrice, lowestStockPrice, conclusionVolume]
|
|
|
|
var typeFields = [UInt8]()
|
|
var valuesData = Data()
|
|
for value in values {
|
|
let valueData: Data
|
|
|
|
let fieldType = value.fieldType
|
|
typeFields.append(fieldType.rawValue)
|
|
|
|
switch fieldType {
|
|
case .uint8: valueData = Data(value: UInt8(value))
|
|
case .uint16: valueData = Data(value: UInt16(value))
|
|
case .uint32: valueData = Data(value: UInt32(value))
|
|
case .uint64: valueData = Data(value: UInt64(value))
|
|
case .float: valueData = Data(value: Float(value))
|
|
case .double: valueData = Data(value: Double(value))
|
|
}
|
|
valuesData.append(valueData)
|
|
}
|
|
|
|
var data = Data()
|
|
data.append(contentsOf: typeFields)
|
|
data.append(valuesData)
|
|
self.data = data
|
|
print("data: \(data.count)")
|
|
}
|
|
|
|
var candle: Domestic.Candle {
|
|
let stockFullDate = Date(timeIntervalSince2020: TimeInterval(key.value_UInt32)).yyyyMMddHHmmss_UTC
|
|
assert(stockFullDate.count == 8+6, "invalid key length")
|
|
let stockBusinessDate = String(stockFullDate.prefix(8))
|
|
let stockConclusionTime = String(stockFullDate.suffix(6))
|
|
|
|
let typeFields = [UInt8](data[0 ..< 6])
|
|
var values = [stockBusinessDate, stockConclusionTime]
|
|
|
|
print("candle data: \(data.count)")
|
|
|
|
var start = 6
|
|
for field in typeFields {
|
|
let value: String
|
|
|
|
switch CandleDataFieldType(rawValue: field)! {
|
|
case .uint8: value = String(data.subdata(in: start ..< start+1).value_UInt8); start += 1
|
|
case .uint16: value = String(data.subdata(in: start ..< start+2).value_UInt16); start += 2
|
|
case .uint32: value = String(data.subdata(in: start ..< start+4).value_UInt32); start += 4
|
|
case .uint64: value = String(data.subdata(in: start ..< start+8).value_UInt64); start += 8
|
|
case .float: value = String(data.subdata(in: start ..< start+4).value_Float); start += 4
|
|
case .double: value = String(data.subdata(in: start ..< start+8).value_Double); start += 8
|
|
}
|
|
values.append(value)
|
|
}
|
|
return try! Domestic.Candle(array: values, source: "")
|
|
}
|
|
}
|
|
|
|
private func build_min_db(_ productNo: String, _ candle_csvs: [URL]) {
|
|
let dataPath = URL.currentDirectory().appending(path: "data")
|
|
|
|
for csvUrl in candle_csvs {
|
|
let candleMinName = CandleMinuteFileName()
|
|
if let (_, yyyyMMdd) = candleMinName.matchedUrl(csvUrl.path), let year = Int(yyyyMMdd.prefix(4)) {
|
|
let yearDbPath = dataPath.appending(path: "\(productNo)/min/candle-\(year).db1")
|
|
//try? FileManager.default.removeItem(at: directory)
|
|
try? FileManager.default.createDirectory(at: yearDbPath, withIntermediateDirectories: true)
|
|
|
|
do {
|
|
let candles = try [Domestic.Candle].readCsv(fromFile: csvUrl)
|
|
|
|
let db = try KissDB(directory: yearDbPath)
|
|
try db.begin()
|
|
|
|
for candle in candles {
|
|
let candleData = try CandleData(candle: candle)
|
|
let item = KissDB.DataItem(key: candleData.key, value: candleData.data)
|
|
try db.insert(item: item)
|
|
|
|
if candleData.candle != candle {
|
|
assertionFailure("invalid candle data")
|
|
}
|
|
}
|
|
|
|
try db.commit()
|
|
} catch {
|
|
print("\(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func build_min_db_from_candle_csv() {
|
|
guard let enumerator = FileManager.subPathFiles("data") else {
|
|
return
|
|
}
|
|
|
|
var lastProductNo: String?
|
|
|
|
let candleMinName = CandleMinuteFileName()
|
|
var allCandles = [String: [URL]]()
|
|
for case let fileUrl as URL in enumerator {
|
|
guard let (productNo, yyyyMMdd) = candleMinName.matchedUrl(fileUrl.path) else {
|
|
continue
|
|
}
|
|
|
|
// Select only one product no
|
|
if lastProductNo == nil {
|
|
lastProductNo = productNo
|
|
}
|
|
else {
|
|
if lastProductNo! != productNo {
|
|
break
|
|
}
|
|
}
|
|
|
|
if allCandles.keys.contains(productNo) {
|
|
allCandles[productNo]!.append(fileUrl)
|
|
}
|
|
else {
|
|
allCandles[productNo] = [fileUrl]
|
|
}
|
|
print("product: \(productNo) \(yyyyMMdd)")
|
|
}
|
|
print("total \(allCandles.count)")
|
|
|
|
if let productCandles = allCandles.first {
|
|
build_min_db(productCandles.key, productCandles.value)
|
|
}
|
|
}
|
|
|
|
|
|
private func test_select_min_db() {
|
|
let yearDbPath = URL(filePath: "/Users/ened/Kiss/KissMe/bin/data/047040/min/candle-2023.db1")
|
|
do {
|
|
let startTime = KissDB.appTime
|
|
|
|
let db = try KissDB(directory: yearDbPath)
|
|
try db.begin()
|
|
|
|
try db.select(into: { (dataItem: KissDB.DataItem) -> Bool in
|
|
//let candleData = CandleData(key: dataItem.key, data: dataItem.value)
|
|
//print("\(candleData.candleDate) : \(candleData.candle.accumulatedTradingAmount)")
|
|
return true
|
|
})
|
|
|
|
try db.rollback()
|
|
|
|
let endTime = KissDB.appTime
|
|
print("DB count: \(db.count) insertAll elapsed: \(endTime - startTime)")
|
|
} catch {
|
|
print("\(error)")
|
|
}
|
|
}
|
|
|
|
|
|
private func test_check_name_parsed() {
|
|
let candleMinName = CandleMinuteFileName()
|
|
let url = "/Users/ened/Kiss/KissMe/bin/data/000020/min/candle-20230705.csv"
|
|
guard let (productNo, yyyyMMdd) = candleMinName.matchedUrl(url) else {
|
|
return
|
|
}
|
|
print(productNo, yyyyMMdd)
|
|
}
|
|
|
|
|
|
class CandleMinuteFileName {
|
|
|
|
let regex: NSRegularExpression
|
|
|
|
init() {
|
|
let pattern = ".*/(\\d{6})/min/candle-(\\d{8})\\.csv$"
|
|
regex = try! NSRegularExpression(pattern: pattern, options: [])
|
|
}
|
|
|
|
func matchedUrl(_ fileUrl: String) -> (productNo: String, yyyyMMdd: String)? {
|
|
let range = NSRange(location: 0, length: fileUrl.utf16.count)
|
|
let results = regex.matches(in: fileUrl, range: range)
|
|
let fragments = results.map { result in
|
|
(0 ..< result.numberOfRanges).map {
|
|
let nsRange = result.range(at: $0)
|
|
if let range = Range(nsRange, in: fileUrl) {
|
|
return String(fileUrl[range])
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
if let first = fragments.first, first.count == 3 {
|
|
return (first[1], first[2])
|
|
}
|
|
return nil
|
|
}
|
|
}
|