diff --git a/KissMe/Sources/Index/KissIndexResult.swift b/KissMe/Sources/Index/KissIndexResult.swift index 31642b4..5671c28 100644 --- a/KissMe/Sources/Index/KissIndexResult.swift +++ b/KissMe/Sources/Index/KissIndexResult.swift @@ -44,11 +44,15 @@ public struct KissIndexResult: Codable { public struct KissMatrixResult: Codable { public let code: Int public let kmis: [String] + public let day: String + public let time: String public let output: [KissIndexResult.Output] - public init(code: Int, kmis: [String], output: [KissIndexResult.Output]) { + public init(code: Int, kmis: [String], day: String, time: String, output: [KissIndexResult.Output]) { self.code = code self.kmis = kmis + self.day = day + self.time = time self.output = output } } diff --git a/KissMeGolder/Sources/KissSimulator.swift b/KissMeGolder/Sources/KissSimulator.swift new file mode 100644 index 0000000..dada9cf --- /dev/null +++ b/KissMeGolder/Sources/KissSimulator.swift @@ -0,0 +1,154 @@ +// +// KissSimulator.swift +// KissMeGolder +// +// Created by ened-book-m1 on 2023/06/30. +// + +import Foundation +import KissMe + + +struct Stock: Codable { + let productNo: String + let quantity: Int + let averagePrice: Int +} + + +class Balance: Codable { + var cash: Int + var stocks: [Stock] + + init() { + cash = 10_000_000 + stocks = [] + } +} + + +class KissSimulator: ShopContext { + let balance: Balance + private var dataPath: URL + private let candleCache = CandleCache() + + override init() { + balance = Balance() + dataPath = URL.currentDirectory().appending(path: "data") + + super.init() + loadShop(url: dataPath.appending(path: "shop-products.csv")) + } + + + init(balanceJson: URL) { + do { + let data = try Data(contentsOf: balanceJson, options: .uncached) + let balance = try JSONDecoder().decode(Balance.self, from: data) + self.balance = balance + } catch { + printError(error) + self.balance = Balance() + } + dataPath = URL.currentDirectory().appending(path: "data") + } + + + func simulate(_ result: KissMatrixResult) { + // 전체 잔고를 설정하고, 최소한의 현금 비중을 설정함. + // 상위 30 종목에 대해서, 밸런싱을 수행 + // 상위 30 종목에서 멀어질수록, (수익이 발생할수록) 보유 수량을 점차 줄여가고, 반대로 상위 30 종목에 대해서는 보유 수량을 늘림 + + // weight 의 비율에 따라서 1-2개씩 사모으는 전략으로 갈 것인가? + // weight 의 비율에 따라서 1-2개씩 팔아서 현금을 모으는 전략일까? + + let topCount = min(result.output.count / 2, 30) + let topResult = result.output.prefix(topCount) + + let bottomCount = min(result.output.count - topCount, 20) + let bottomResult = result.output.suffix(bottomCount) + + /// 매수 전략을 수행 + for item in topResult { + guard let price = candleCache.getPrice(productNo: item.shortCode, day: result.day, time: result.time) else { + continue + } + + } + + /// 매도 전략을 수행 + for item in bottomResult { + + } + } + + func simulate(logAt: String, startDate: Date, endDate: Date, interval: TimeInterval = 10 * 60) { + var curDate = startDate + while curDate <= endDate { + guard let result = loadLog(logAt: logAt, day: curDate.yyyyMMdd, time: curDate.HHmmss) else { + printError("Cannot load log at \(curDate.yyyyMMdd_HHmmss_forTime)") + break + } + + simulate(result) + curDate.addTimeInterval(interval) + } + } +} + + +extension KissSimulator { + + private func loadLog(logAt: String, day: String, time: String) -> KissMatrixResult? { + let subPath = "\(logAt)/\(day)_\(time)_0000.log" + let logUrl = URL.currentDirectory().appending(path: subPath) + + do { + let data = try Data(contentsOf: logUrl, options: .uncached) + let result = try JSONDecoder().decode(KissMatrixResult.self, from: data) + return result + } catch { + printError(error) + return nil + } + } +} + + +public class CandleCache { + + private var candleCaches: [String: [Domestic.Candle]] = [:] + + func getCandle(productNo: String, day: String, time: String) -> Domestic.Candle? { + let basePath = URL.currentDirectory().appending(path: "data") + let candleUrl = basePath.appending(path: "\(productNo)/min/candle-\(day).log") + + let candles: [Domestic.Candle] + + if let cache = candleCaches[candleUrl.path] { + candles = cache + } + else { + do { + candles = try [Domestic.Candle].readCsv(fromFile: candleUrl) + candleCaches[candleUrl.path] = candles + } catch { + printError(error) + return nil + } + } + + guard let candle = candles.first(where: { $0.stockConclusionTime == time }) else { + return nil + } + return candle + } + + + func getPrice(productNo: String, day: String, time: String) -> Int? { + guard let candle = getCandle(productNo: productNo, day: day, time: time) else { + return nil + } + return Int(candle.currentStockPrice) + } +} diff --git a/KissMeGolder/Sources/main.swift b/KissMeGolder/Sources/main.swift index 2ec4e0d..eb2cdc9 100644 --- a/KissMeGolder/Sources/main.swift +++ b/KissMeGolder/Sources/main.swift @@ -7,4 +7,12 @@ import Foundation +#if true KissGolder().run() + +#else + let startDate = "20230626 090000".modelDate! + let endDate = "20230630 153000".modelDate! + + KissSimulator().simulate(logAt: "data", startDate: startDate, endDate: endDate) +#endif diff --git a/KissMeMatrix/Sources/KissMatrix.swift b/KissMeMatrix/Sources/KissMatrix.swift index 64579ef..8390704 100644 --- a/KissMeMatrix/Sources/KissMatrix.swift +++ b/KissMeMatrix/Sources/KissMatrix.swift @@ -85,7 +85,7 @@ class KissMatrix { return taskResult } - mergeResult(results) + mergeResult(results, date: runDate) semaphore.signal() } @@ -144,7 +144,7 @@ class KissMatrix { } - private func mergeResult(_ results: [KissIndexResult]) { + private func mergeResult(_ results: [KissIndexResult], date: Date) { let indexCount = results.count var mergedOutput = [String: [KissIndexResult.Output]]() for result in results { @@ -168,7 +168,7 @@ class KissMatrix { let kmis = results.map { $0.kmi } - let matrixResult = KissMatrixResult(code: 200, kmis: kmis, output: normalized) + let matrixResult = KissMatrixResult(code: 200, kmis: kmis, day: date.yyyyMMdd, time: date.HHmmss, output: normalized) do { let jsonData = try JSONEncoder().encode(matrixResult) try FileHandle.standardOutput.write(contentsOf: jsonData) diff --git a/bin/data b/bin/data index 8ef77ec..5f307c3 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit 8ef77ece1861f8231174c003e3578d651cce8f9c +Subproject commit 5f307c3bb158a3a5529cde5ca8dc46da19262b73 diff --git a/projects/macos/KissMeGolder.xcodeproj/project.pbxproj b/projects/macos/KissMeGolder.xcodeproj/project.pbxproj index fc835b0..d03fc58 100644 --- a/projects/macos/KissMeGolder.xcodeproj/project.pbxproj +++ b/projects/macos/KissMeGolder.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 340A4DCC2A4E4C37005A1FBA /* KissGolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340A4DCB2A4E4C37005A1FBA /* KissGolder.swift */; }; + 340A4DD22A4EBEB0005A1FBA /* KissSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340A4DD12A4EBEB0005A1FBA /* KissSimulator.swift */; }; 3498435C2A24DC1300E85B08 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3498435B2A24DC1300E85B08 /* main.swift */; }; 349843642A24DC7800E85B08 /* KissMe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 349843632A24DC7800E85B08 /* KissMe.framework */; }; 349843652A24DC7800E85B08 /* KissMe.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 349843632A24DC7800E85B08 /* KissMe.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -38,6 +39,7 @@ /* Begin PBXFileReference section */ 340A4DCB2A4E4C37005A1FBA /* KissGolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KissGolder.swift; sourceTree = ""; }; + 340A4DD12A4EBEB0005A1FBA /* KissSimulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KissSimulator.swift; sourceTree = ""; }; 349843582A24DC1300E85B08 /* KissMeGolder */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = KissMeGolder; sourceTree = BUILT_PRODUCTS_DIR; }; 3498435B2A24DC1300E85B08 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 349843632A24DC7800E85B08 /* KissMe.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KissMe.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -77,6 +79,7 @@ children = ( 3498435B2A24DC1300E85B08 /* main.swift */, 340A4DCB2A4E4C37005A1FBA /* KissGolder.swift */, + 340A4DD12A4EBEB0005A1FBA /* KissSimulator.swift */, ); name = KissMeGolder; path = ../../KissMeGolder/Sources; @@ -150,6 +153,7 @@ buildActionMask = 2147483647; files = ( 340A4DCC2A4E4C37005A1FBA /* KissGolder.swift in Sources */, + 340A4DD22A4EBEB0005A1FBA /* KissSimulator.swift in Sources */, 3498435C2A24DC1300E85B08 /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;