155 lines
4.5 KiB
Swift
155 lines
4.5 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|