1450 lines
47 KiB
Swift
1450 lines
47 KiB
Swift
//
|
|
// KissConsole.swift
|
|
// KissMeConsole
|
|
//
|
|
// Created by ened-book-m1 on 2023/05/17.
|
|
//
|
|
|
|
import Foundation
|
|
import KissMe
|
|
|
|
|
|
class KissConsole: KissMe.ShopContext {
|
|
/// 인증 관련 json 정보
|
|
private var credential: Credential? = nil
|
|
|
|
/// 한국투자증권 개인 계정
|
|
var account: KissAccount? = nil
|
|
|
|
/// 주식시장 거래 상품 목록들
|
|
private var shop: KissShop? = nil
|
|
|
|
/// 현재 candle 파일로 저장 중인 productNo
|
|
var currentCandleShortCode: String?
|
|
|
|
/// CSV 파일을 저장할 때, field name 에 대해서 한글(true) 또는 영문(false)로 기록할지 설정
|
|
var localized: Bool = false
|
|
|
|
var indexContext: IndexContext
|
|
|
|
let maxCandleDay: Int = 250
|
|
|
|
// 005930: 삼성전자
|
|
static let defaultProductNo: String = "005930"
|
|
|
|
|
|
private enum KissCommand: String {
|
|
case quit = "quit"
|
|
|
|
// 로그인
|
|
case loginMock = "login mock"
|
|
case loginReal = "login real"
|
|
case logout = "logout"
|
|
|
|
// 랭킹
|
|
case top = "top"
|
|
case topAll = "top all"
|
|
|
|
// 매매
|
|
case buy = "buy"
|
|
case buyCheck = "buy check"
|
|
case sell = "sell"
|
|
case cancel = "cancel"
|
|
case modify = "modify"
|
|
|
|
// 보유 종목
|
|
case openBag = "open bag"
|
|
|
|
// 종목 시세
|
|
case now = "now"
|
|
case nowAll = "now all"
|
|
case candle = "candle"
|
|
case candleAll = "candle all"
|
|
case candleDay = "candle day"
|
|
case candleWeek = "candle week"
|
|
case candleValidate = "candle validate"
|
|
|
|
// 투자자 열람
|
|
case investor = "investor"
|
|
case investorAll = "investor all"
|
|
|
|
// 공매도 열람
|
|
case shorts = "shorts"
|
|
case shortsAll = "shorts all"
|
|
|
|
// KRX 지수 열람
|
|
case index = "index"
|
|
case indexPortfolio = "index portfolio"
|
|
|
|
// 종목 상품 열람
|
|
case loadShop = "load shop"
|
|
case updateShop = "update shop"
|
|
case look = "look"
|
|
|
|
// 지수 상품 열람
|
|
case loadIndex = "load index"
|
|
case updateIndex = "update index"
|
|
|
|
// 휴장일
|
|
case holiday = "holiday"
|
|
|
|
// 진열 종목 (시스템에서 제공하는 추천 리스트)
|
|
case showcase = "showcase"
|
|
|
|
// 관심 종목
|
|
case loves = "loves" // 열람
|
|
case love = "love" // love nuts.1 ISCD
|
|
case hate = "hate" // hate nuts.1 ISCD
|
|
|
|
// 기타
|
|
case localizeNames = "localize names"
|
|
case localizeOnOff = "localize"
|
|
case test = "test"
|
|
|
|
// 웹소켓
|
|
case real = "real"
|
|
|
|
// DB1
|
|
case db = "db"
|
|
|
|
// 뉴스
|
|
case news = "news"
|
|
case newsAll = "news all"
|
|
|
|
var needLogin: Bool {
|
|
switch self {
|
|
case .quit, .loginMock, .loginReal:
|
|
return false
|
|
case .logout, .top, .topAll, .buy, .buyCheck, .sell, .cancel, .modify:
|
|
return true
|
|
case .openBag:
|
|
return true
|
|
case .now, .nowAll, .candle, .candleAll, .candleDay, .candleWeek:
|
|
return true
|
|
case .candleValidate:
|
|
return false
|
|
case .investor, .investorAll:
|
|
return true
|
|
case .shorts, .shortsAll, .index, .indexPortfolio:
|
|
return false
|
|
case .loadShop, .updateShop, .look, .holiday:
|
|
return false
|
|
case .loadIndex, .updateIndex:
|
|
return false
|
|
case .showcase:
|
|
return false
|
|
case .loves, .love, .hate, .localizeNames, .localizeOnOff:
|
|
return false
|
|
case .test:
|
|
return true
|
|
case .real:
|
|
return true
|
|
case .db:
|
|
return false
|
|
case .news, .newsAll:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isLogined: Bool {
|
|
account != nil
|
|
}
|
|
|
|
override init() {
|
|
let jsonUrl = URL.currentDirectory().appending(path: "shop-server.json")
|
|
shop = try? KissShop(jsonUrl: jsonUrl)
|
|
|
|
KissConsole.createSubpath("log")
|
|
KissConsole.createSubpath("data")
|
|
|
|
indexContext = IndexContext()
|
|
|
|
super.init()
|
|
lastLogin()
|
|
loadLocalName()
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
await onLoadShop()
|
|
await onLoadIndex()
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
|
|
setCurrent(productNo: KissConsole.defaultProductNo)
|
|
}
|
|
|
|
private func getCommand(_ line: String) -> (KissCommand?, [String]) {
|
|
let args = line.split(separator: " ")
|
|
let double = args.prefix(upTo: min(2, args.count)).joined(separator: " ")
|
|
if let cmd = KissCommand(rawValue: double) {
|
|
return (cmd, args.suffixStrings(from: 2))
|
|
}
|
|
let single = args.prefix(upTo: min(1, args.count)).joined(separator: " ")
|
|
if let cmd = KissCommand(rawValue: single) {
|
|
return (cmd, args.suffixStrings(from: 1))
|
|
}
|
|
return (nil, [])
|
|
}
|
|
|
|
func run() {
|
|
guard CommandLine.argc == 1 else {
|
|
let line = CommandLine.arguments.suffix(Int(CommandLine.argc)-1).joined(separator: " ")
|
|
let (cmd, args) = getCommand(line)
|
|
if cmd?.needLogin == true {
|
|
guard isLogined else {
|
|
print("Need to login")
|
|
exit(1)
|
|
}
|
|
}
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let success = await run(command: cmd, args: args, line: line)
|
|
if !success {
|
|
exit(2)
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
return
|
|
}
|
|
|
|
print("Enter command:")
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var loop = true
|
|
|
|
while loop {
|
|
guard let line = readLine(strippingNewline: true) else {
|
|
continue
|
|
}
|
|
let (cmd, args) = getCommand(line)
|
|
if cmd?.needLogin == true {
|
|
guard isLogined else {
|
|
print("Need to login")
|
|
continue
|
|
}
|
|
}
|
|
|
|
Task {
|
|
_ = await run(command: cmd, args: args, line: line)
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
|
|
loop = (cmd != .quit)
|
|
}
|
|
}
|
|
|
|
private func run(command: KissCommand?, args: [String], line: String) async -> Bool {
|
|
switch command {
|
|
case .quit: break
|
|
case .loginMock: await onLogin(isMock: true)
|
|
case .loginReal: await onLogin(isMock: false)
|
|
case .logout: await onLogout()
|
|
case .top: await onTop(args)
|
|
case .topAll: onTopAll()
|
|
|
|
case .buy: await onBuy(args)
|
|
case .buyCheck: await onBuyCheck(args)
|
|
case .sell: await onSell(args)
|
|
case .cancel: await onCancel(args)
|
|
case .modify: await onModify(args)
|
|
|
|
case .openBag: await onOpenBag()
|
|
|
|
case .now: await onNow(args)
|
|
case .nowAll: onNowAll(args)
|
|
case .candle: await onCandle(args)
|
|
case .candleAll: onCancleAll(args)
|
|
case .candleDay: onCandleDay(args)
|
|
case .candleWeek: onCandleWeek(args)
|
|
case .candleValidate: onCandleValidate(args)
|
|
|
|
case .investor: await onInvestor(args)
|
|
case .investorAll: onInvestorAll(args)
|
|
|
|
case .shorts: await onShorts(args)
|
|
case .shortsAll: onShortsAll(args)
|
|
|
|
case .index: await onIndex(args)
|
|
case .indexPortfolio: await onIndexPortfolio(args)
|
|
|
|
case .loadShop: await onLoadShop()
|
|
case .updateShop: await onUpdateShop()
|
|
case .look: await onLook(args)
|
|
case .holiday: await onHoliday(args)
|
|
|
|
case .loadIndex: await onLoadIndex()
|
|
case .updateIndex: await onUpdateIndex()
|
|
|
|
case .showcase: await onShowcase()
|
|
case .loves: await onLoves()
|
|
case .love: await onLove(args)
|
|
case .hate: await onHate(args)
|
|
case .localizeNames: await onLocalizeNames()
|
|
case .localizeOnOff: await onLocalizeOnOff(args)
|
|
case .test: onTest(args)
|
|
|
|
case .real: await onReal(args)
|
|
|
|
case .db: await onDB(args)
|
|
|
|
case .news: await onNews(args)
|
|
case .newsAll: onNewsAll(args)
|
|
|
|
default:
|
|
print("Unknown command: \(line)")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
extension KissConsole {
|
|
|
|
static func createSubpath(_ name: String) {
|
|
let subPath = URL.currentDirectory().appending(path: name)
|
|
try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true)
|
|
}
|
|
|
|
private func lastLogin() {
|
|
let profile = KissProfile()
|
|
guard let isMock = profile.isMock else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
let lastCredential = try KissCredential(isMock: isMock)
|
|
let lastAccount = KissAccount(credential: lastCredential)
|
|
guard let expiredAt = lastAccount.accessTokenExpiredDate else {
|
|
return
|
|
}
|
|
|
|
if expiredAt > Date() {
|
|
credential = lastCredential
|
|
account = lastAccount
|
|
|
|
let expiredAt = account?.accessTokenExpiredDate?.yyyyMMdd_HHmmss_forTime ?? ""
|
|
print("resume \(isMock ? "mock login": "real login") expired at \(expiredAt)")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
private func loadLocalName() {
|
|
LocalContext.shared.load(KissConsole.localNamesUrl)
|
|
}
|
|
}
|
|
|
|
|
|
extension KissConsole {
|
|
|
|
private func onLogin(isMock: Bool) async {
|
|
guard !isLogined else {
|
|
print("Already loginged")
|
|
return
|
|
}
|
|
|
|
do {
|
|
credential = try KissCredential(isMock: isMock)
|
|
account = KissAccount(credential: credential!)
|
|
if try await account!.login() {
|
|
print("Success")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onLogout() async {
|
|
do {
|
|
_ = try await account?.logout()
|
|
credential = nil
|
|
account = nil
|
|
print("Success")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onTop(_ args: [String]) async {
|
|
var belongCode = "0"
|
|
if args.count == 1, let code = Int(args[0]) {
|
|
belongCode = String(code)
|
|
}
|
|
guard let belongClass = BelongClassCode(rawValue: belongCode) else {
|
|
print("Incorrect belong type: \(belongCode)")
|
|
return
|
|
}
|
|
print("TOP: \(belongClass.description)")
|
|
let option = RankingOption(divisionClass: .all, belongClass: belongClass)
|
|
|
|
do {
|
|
let curDate = Date()
|
|
let rank = try await account!.getVolumeRanking(option: option)
|
|
guard let output = rank.output else {
|
|
print("Error \(rank.messageCode) \(rank.message)")
|
|
return
|
|
}
|
|
|
|
print("랭킹 단축상품코드 상품명 가격 평균거래량 누적거래대금")
|
|
for item in output {
|
|
print("\(item.dataRank) \(item.shortProductNo) \(item.htsProductName.maxSpace(20)) \(item.currentStockPrice.maxSpace(10, digitBy: 3)) \(item.averageVolume.maxSpace(15, digitBy: 3)) \(item.accumulatedTradingAmount.maxSpace(25, digitBy: 3))")
|
|
}
|
|
let fileUrl = KissConsole.topProductsUrl(belongClass, date: curDate)
|
|
try output.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("wrote \(fileUrl.lastPathComponent) with \(output.count)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onTopAll() {
|
|
let belongs: [BelongClassCode] = [.averageVolume, .volumeIncreaseRate, .averageVolumeTurnoverRate, .transactionValue, .averageTransactionValueTurnoverRate]
|
|
|
|
for belong in belongs {
|
|
let option = RankingOption(divisionClass: .all, belongClass: belong)
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
do {
|
|
let curDate = Date()
|
|
let rank = try await account!.getVolumeRanking(option: option)
|
|
guard let output = rank.output else {
|
|
print("Error \(rank.messageCode) \(rank.message)")
|
|
return
|
|
}
|
|
|
|
let fileUrl = KissConsole.topProductsUrl(belong, date: curDate)
|
|
try output.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("wrote \(fileUrl.lastPathComponent) with \(output.count)")
|
|
try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredTopTPS)
|
|
} catch {
|
|
print(error)
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onBuy(_ args: [String]) async {
|
|
guard args.count == 3 else {
|
|
print("Missing buy paramters: buy (PNO) (PRICE) (QUANTITY)")
|
|
return
|
|
}
|
|
let productNo = args[0]
|
|
guard let product = getProduct(shortCode: productNo) else {
|
|
print("No product \(productNo)")
|
|
return
|
|
}
|
|
guard let price = Int(args[1]), (price == -8282 || price >= 100) else {
|
|
print("Invalid price: \(args[1])")
|
|
return
|
|
}
|
|
guard let quantity = Int(args[2]), (quantity == -82 || quantity > 0) else {
|
|
print("Invalid quantity: \(args[2])")
|
|
return
|
|
}
|
|
|
|
let division: OrderDivision = (price == -8282 ? .marketPrice: .limits)
|
|
let contract = Contract(productNo: productNo,
|
|
orderType: .buy,
|
|
orderDivision: division,
|
|
orderQuantity: quantity, orderPrice: price)
|
|
do {
|
|
let result = try await account!.orderStock(contract: contract)
|
|
if let output = result.output {
|
|
print("Success \(product.itemName) orderNo: \(output.orderNo) at \(output.orderTime)")
|
|
}
|
|
else {
|
|
print("Failed \(result.resultCode) \(result.messageCode) \(result.message)")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onBuyCheck(_ args: [String]) async {
|
|
guard args.count == 2 else {
|
|
print("Missing buy check paramters: buy check (PNO) (PRICE)")
|
|
return
|
|
}
|
|
let productNo = args[0]
|
|
guard let product = getProduct(shortCode: productNo) else {
|
|
print("No product \(productNo)")
|
|
return
|
|
}
|
|
guard let price = Int(args[1]), price >= 100 else {
|
|
print("Invalid price: \(args[1])")
|
|
return
|
|
}
|
|
|
|
do {
|
|
let result = try await account!.canOrderStock(productNo: productNo, division: .limits, price: price)
|
|
if let output = result.output {
|
|
print("Success \(product.itemName) \(output)")
|
|
}
|
|
else {
|
|
print("Failed \(result.resultCode) \(result.messageCode) \(result.message)")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onSell(_ args: [String]) async {
|
|
guard args.count == 3 else {
|
|
print("Missing sell paramters: sell (PNO) (PRICE) (QUANTITY)")
|
|
return
|
|
}
|
|
let productNo = args[0]
|
|
guard let product = getProduct(shortCode: productNo) else {
|
|
print("No product \(productNo)")
|
|
return
|
|
}
|
|
guard let price = Int(args[1]), (price == -8282 || price >= 100) else {
|
|
print("Invalid price: \(args[1])")
|
|
return
|
|
}
|
|
guard let quantity = Int(args[2]), quantity > 0 else {
|
|
print("Invalid quantity: \(args[2])")
|
|
return
|
|
}
|
|
|
|
let division: OrderDivision = (price == -8282 ? .marketPrice: .limits)
|
|
let contract = Contract(productNo: productNo,
|
|
orderType: .sell,
|
|
orderDivision: division,
|
|
orderQuantity: quantity, orderPrice: price)
|
|
do {
|
|
let result = try await account!.orderStock(contract: contract)
|
|
if let output = result.output {
|
|
print("Success \(product.itemName) orderNo: \(output.orderNo) at \(output.orderTime)")
|
|
}
|
|
else {
|
|
print("Failed \(result.resultCode) \(result.messageCode) \(result.message)")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onCancel(_ args: [String]) async {
|
|
guard args.count == 3 else {
|
|
print("Missing cancel paramters: cancel (PNO) (QUANTITY)")
|
|
return
|
|
}
|
|
let productNo = args[0]
|
|
guard let product = getProduct(shortCode: productNo) else {
|
|
print("No product \(productNo)")
|
|
return
|
|
}
|
|
let orderNo = args[1]
|
|
guard orderNo.count >= 1 else {
|
|
print("Invalid orderNo: \(args[1])")
|
|
return
|
|
}
|
|
guard let quantity = Int(args[2]), (quantity == -82 || quantity > 0) else {
|
|
print("Invalid quantity: \(args[2])")
|
|
return
|
|
}
|
|
|
|
do {
|
|
let cancel = ContractCancel(productNo: productNo,
|
|
orderNo: orderNo,
|
|
orderQuantity: (quantity == -82 ? 0: quantity))
|
|
let result = try await account!.cancelOrder(cancel: cancel)
|
|
if let output = result.output {
|
|
print("Success \(product.itemName) orderNo: \(output.orderNo) at \(output.orderTime)")
|
|
}
|
|
else {
|
|
print("Failed \(result.resultCode) \(result.messageCode) \(result.message)")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onModify(_ args: [String]) async {
|
|
// TODO: work
|
|
}
|
|
|
|
|
|
private func onOpenBag() async {
|
|
do {
|
|
let result = try await account!.getStockBalance()
|
|
if let stocks = result.output1 {
|
|
for item in stocks {
|
|
print("\(item)")
|
|
}
|
|
|
|
let fileUrl = KissConsole.accountStocksUrl
|
|
try stocks.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("wrote \(fileUrl.lastPathComponent) with \(stocks.count)")
|
|
}
|
|
|
|
if let amounts = result.output2 {
|
|
for item in amounts {
|
|
print("\(item)")
|
|
}
|
|
let fileUrl = KissConsole.accountAmountUrl
|
|
try amounts.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("wrote \(fileUrl.lastPathComponent) with \(amounts.count)")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onNow(_ args: [String]) async {
|
|
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
|
|
guard let productNo = productNo else {
|
|
print("Invalid productNo")
|
|
return
|
|
}
|
|
let success = await getCurrentPrice(productNo: productNo)
|
|
if success {
|
|
setCurrent(productNo: productNo)
|
|
}
|
|
}
|
|
|
|
|
|
private func onNowAll(_ args: [String]) {
|
|
let all = getAllProducts()
|
|
for item in all {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let holiday = try? await checkHoliday(Date())
|
|
if holiday == true {
|
|
print("DONE today is holiday")
|
|
return
|
|
}
|
|
|
|
let success = await getCurrentPrice(productNo: item.shortCode)
|
|
#if DEBUG
|
|
if !success {
|
|
exit(-100)
|
|
}
|
|
#endif
|
|
print("DONE \(success) \(item.shortCode)")
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onCancleAll(_ args: [String]) {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
await KissContext.shared.update(resuming: false)
|
|
if args.count == 1, args[0].lowercased() == "resume" {
|
|
await KissContext.shared.update(resuming: true)
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
|
|
|
|
let all = getAllProducts()
|
|
for item in all {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let holiday = try? await checkHoliday(Date())
|
|
if holiday == true {
|
|
print("DONE today is holiday")
|
|
return
|
|
}
|
|
|
|
if await KissContext.shared.isResuming {
|
|
let curDate = Date()
|
|
let url = KissConsole.candleFileUrl(productNo: item.shortCode, period: .minute, day: curDate.yyyyMMdd)
|
|
let r = validateCsv(filePriod: .minute, url: url)
|
|
switch r {
|
|
case .ok, .invalidFileName: return
|
|
default: break
|
|
}
|
|
}
|
|
|
|
let success = await getCandle(productNo: item.shortCode)
|
|
print("DONE \(success) \(item.shortCode)")
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onCandleDay(_ args: [String]) {
|
|
if args.count >= 1, args[0] == "all" {
|
|
let otherArgs = args[1...].map { String($0) }
|
|
onCandleDayAll(otherArgs)
|
|
return
|
|
}
|
|
|
|
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
|
|
guard let productNo = productNo else {
|
|
print("Invalid productNo")
|
|
return
|
|
}
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let success = await getCandle(productNo: productNo, period: .daily, count: maxCandleDay, startDate: nil)
|
|
print("DONE \(success) \(productNo)")
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
|
|
|
|
private func onCandleDayAll(_ args: [String]) {
|
|
let resume: Bool = (args.count == 1 && args[0] == "resume")
|
|
|
|
let all = getAllProducts()
|
|
for item in all {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let startDate = (resume ? try? getLatestDateFromCandle(item.shortCode, period: .daily): nil)
|
|
if startDate?.yyyyMMdd == Date().yyyyMMdd {
|
|
print("DONE skipped \(item.shortCode)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
let count: Int
|
|
if let startDate = startDate {
|
|
let daysBefore = ceil(startDate.distance(to: Date()) / SecondsForOneDay)
|
|
count = Int(daysBefore)
|
|
}
|
|
else {
|
|
count = maxCandleDay
|
|
}
|
|
|
|
|
|
let success = await getCandle(productNo: item.shortCode, period: .daily, count: count, startDate: startDate)
|
|
print("DONE \(success) \(item.shortCode)")
|
|
semaphore.signal()
|
|
#if DEBUG
|
|
//if !success {
|
|
// exit(99)
|
|
//}
|
|
#endif
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onCandleWeek(_ args: [String]) {
|
|
if args.count >= 1, args[0] == "all" {
|
|
let otherArgs = args[1...].map { String($0) }
|
|
onCandleWeekAll(otherArgs)
|
|
return
|
|
}
|
|
|
|
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
|
|
guard let productNo = productNo else {
|
|
print("Invalid productNo")
|
|
return
|
|
}
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let success = await getCandle(productNo: productNo, period: .weekly, count: 52, startDate: nil)
|
|
print("DONE \(success) \(productNo)")
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
|
|
|
|
private func onCandleWeekAll(_ args: [String]) {
|
|
let resume: Bool = (args.count == 1 && args[0] == "resume")
|
|
|
|
let all = getAllProducts()
|
|
for item in all {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let startDate = (resume ? try? getLatestDateFromCandle(item.shortCode, period: .weekly): nil)
|
|
if startDate?.yyyyMMdd == Date().yyyyMMdd {
|
|
print("DONE skipped \(item.shortCode)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
let success = await getCandle(productNo: item.shortCode, period: .weekly, count: 52, startDate: startDate)
|
|
print("DONE \(success) \(item.shortCode)")
|
|
semaphore.signal()
|
|
#if DEBUG
|
|
//if !success {
|
|
// exit(99)
|
|
//}
|
|
#endif
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onCandleValidate(_ args: [String]) {
|
|
let period: CandleFilePeriod?
|
|
if args.count == 1 {
|
|
period = CandleFilePeriod(rawValue: args[0])
|
|
}
|
|
else {
|
|
period = .minute
|
|
}
|
|
|
|
guard let period = period else { return }
|
|
do {
|
|
try validateAllCsvs(filePriod: period)
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onInvestor(_ args: [String]) async {
|
|
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
|
|
guard let productNo = productNo else {
|
|
print("Invalid productNo")
|
|
return
|
|
}
|
|
do {
|
|
_ = try await getInvestor(productNo: productNo)
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onInvestorAll(_ args: [String]) {
|
|
let all = getAllProducts()
|
|
for item in all {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
let holiday = try? await checkHoliday(Date())
|
|
if holiday == true {
|
|
print("DONE today is holiday")
|
|
return
|
|
}
|
|
|
|
do {
|
|
let success = try await getInvestor(productNo: item.shortCode)
|
|
print("DONE \(success) \(item.shortCode)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onShorts(_ args: [String]) async {
|
|
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
|
|
guard let productNo = productNo else {
|
|
print("Invalid productNo")
|
|
return
|
|
}
|
|
do {
|
|
_ = try await getShorts(productNo: productNo, startAt: nil)
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onShortsAll(_ args: [String]) {
|
|
let all = getAllProducts()
|
|
for item in all {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
do {
|
|
let startAt: Date?
|
|
if args.count == 1, args[0].lowercased() == "resume" {
|
|
startAt = getShortsLastDate(productNo: item.shortCode)
|
|
}
|
|
else {
|
|
startAt = nil
|
|
}
|
|
|
|
let success = try await getShorts(productNo: item.shortCode, startAt: startAt)
|
|
print("DONE \(success)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
print("FINISHED")
|
|
}
|
|
|
|
|
|
private func onIndex(_ args: [String]) async {
|
|
let indexType: DomesticExtra.IndexType? = (args.isEmpty ? .krx: .init(rawValue: args[0]))
|
|
guard let indexType = indexType else {
|
|
print("Invalid KRX index type")
|
|
return
|
|
}
|
|
do {
|
|
let curDate = Date()
|
|
if curDate.isBeforeMarketOpenning {
|
|
print("Before market openning")
|
|
return
|
|
}
|
|
let result = try await KissAccount.getIndexPrice(indexType: indexType, date: curDate)
|
|
guard result.output.isEmpty == false else {
|
|
print("empty result")
|
|
return
|
|
}
|
|
|
|
let fileUrl = KissConsole.indexPriceFileUrl(date: result.currentDate, type: indexType)
|
|
try result.output.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("DONE \(result.output.count) type: \(indexType.rawValue)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onIndexPortfolio(_ args: [String]) async {
|
|
do {
|
|
let indices = indexContext.getAllIndices()
|
|
let date = Date()
|
|
|
|
for index in indices {
|
|
let result = try await KissAccount.getIndexPortfolio(indexId: index.index1Code, indexId2: index.index2Code, date: date)
|
|
guard result.output.isEmpty == false else {
|
|
print("empty result on \(index.indexFullCode)")
|
|
continue
|
|
}
|
|
|
|
let fileUrl = KissConsole.indexPortfolioFileUrl(date: date, type: index.indexType, indexFullCode: index.indexFullCode)
|
|
try result.output.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("DONE \(result.output.count) \(index.indexFullCode)")
|
|
|
|
try await Task.sleep(nanoseconds: 1_000_000_000 / PreferredIndexTPS)
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onCandle(_ args: [String]) async {
|
|
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
|
|
guard let productNo = productNo else {
|
|
print("Invalid productNo")
|
|
return
|
|
}
|
|
_ = await getCandle(productNo: productNo)
|
|
}
|
|
|
|
|
|
private func onLoadShop() async {
|
|
return await withUnsafeContinuation { continuation in
|
|
self.loadShop(url: KissConsole.shopProductsUrl, loggable: true)
|
|
continuation.resume()
|
|
}
|
|
}
|
|
|
|
|
|
private func onUpdateShop() async {
|
|
guard let _ = shop else {
|
|
print("Invalid shop instance")
|
|
return
|
|
}
|
|
|
|
var baseDate = Date()
|
|
var shopItems = [DomesticShop.Product]()
|
|
|
|
for _ in 0 ..< 7 {
|
|
print("try to get shop at date \(baseDate.yyyyMMdd)")
|
|
shopItems = await getAllProduct(baseDate: baseDate)
|
|
if shopItems.isEmpty {
|
|
let oneDaySeconds: TimeInterval = 60 * 60 * 24
|
|
baseDate = baseDate.addingTimeInterval(-oneDaySeconds)
|
|
}
|
|
else {
|
|
break
|
|
}
|
|
}
|
|
|
|
do {
|
|
let fileUrl = KissConsole.shopProductsUrl
|
|
try shopItems.writeCsv(toFile: fileUrl, localized: localized)
|
|
print("wrote \(fileUrl.lastPathComponent) with \(shopItems.count)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onLoadIndex() async {
|
|
return await withUnsafeContinuation { continuation in
|
|
self.indexContext.loadIndex(url: KissConsole.indexProductsFileUrl(), loggable: true)
|
|
continuation.resume()
|
|
}
|
|
}
|
|
|
|
|
|
private func onUpdateIndex() async {
|
|
do {
|
|
let result = try await KissAccount.getAllIndices()
|
|
if result.block.isEmpty == false {
|
|
let fileUrl = KissConsole.indexProductsFileUrl()
|
|
try result.block.writeCsv(toFile: fileUrl, localized: localized)
|
|
}
|
|
print("DONE \(result.block.count)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
func getAllProduct(baseDate: Date) async -> [DomesticShop.Product] {
|
|
var pageNo = 0
|
|
var shopItems = [DomesticShop.Product]()
|
|
|
|
do {
|
|
while true {
|
|
pageNo += 1
|
|
print("get pageNo: \(pageNo)")
|
|
|
|
let (totalCount, items) = try await shop!.getProduct(baseDate: baseDate, pageNo: pageNo)
|
|
shopItems.append(contentsOf: items)
|
|
|
|
print("got pageNo: \(pageNo) - \(shopItems.count)/\(totalCount)")
|
|
if totalCount == 0 || shopItems.count >= totalCount {
|
|
break
|
|
}
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
return shopItems
|
|
}
|
|
|
|
|
|
private func onLook(_ args: [String]) async {
|
|
guard args.count >= 1 else {
|
|
print("No target name")
|
|
return
|
|
}
|
|
let productName = String(args[0])
|
|
//print(args, productName, "\(productName.count)")
|
|
guard let items = getProducts(similarName: productName), items.isEmpty == false else {
|
|
print("No products like \(productName)")
|
|
return
|
|
}
|
|
for item in items {
|
|
if let first = item.value.first {
|
|
print("\tISIN: ", first.isinCode)
|
|
print("\t상품명: ", item.key.maxSpace(20))
|
|
print("\t단축코드: ", first.shortCode)
|
|
print("\t시장구분: ", first.marketCategory, "\t기준일자: ", first.baseDate)
|
|
setCurrent(productNo: first.shortCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func onHoliday(_ args: [String]) async {
|
|
var date = Date()
|
|
if args.count == 1, args[0].utf8.count == 8, let day = args[0].yyyyMMdd {
|
|
date.change(year: day.0, month: day.1, day: day.2)
|
|
}
|
|
|
|
let targetDay = (Date().yyyyMMdd == date.yyyyMMdd ? "today": date.yyyyMMdd)
|
|
|
|
do {
|
|
let holiday = try await checkHoliday(date)
|
|
if holiday {
|
|
print("DONE \(targetDay) is holiday")
|
|
}
|
|
else {
|
|
print("DONE \(targetDay) is business day")
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onShowcase() async {
|
|
// TODO: write
|
|
}
|
|
|
|
|
|
private func onLoves() async {
|
|
guard let account = account else { return }
|
|
let loves = account.getLoves()
|
|
for tab in loves.sorted(by: { $0.key < $1.key }) {
|
|
printLoveTab(tab.key, loves: tab.value)
|
|
}
|
|
}
|
|
|
|
private func printLoveTab(_ key: String, loves: [KissProfile.Love]) {
|
|
print("Page: \(key)")
|
|
for item in loves {
|
|
print(" \(item.isin) \(item.name)")
|
|
}
|
|
}
|
|
|
|
|
|
private func onLove(_ args: [String]) async {
|
|
guard let account = account else { return }
|
|
guard args.count > 0 else {
|
|
print("Invalid love\nlove ")
|
|
return
|
|
}
|
|
|
|
if args.count == 1 {
|
|
let key = args[0]
|
|
guard let loves = account.getLoves(for: key) else { return }
|
|
printLoveTab(key, loves: loves)
|
|
}
|
|
else if args.count == 2 {
|
|
let keys = args[0].split(separator: ".").map { String($0) }
|
|
let isin = args[1]
|
|
if keys.count == 2 {
|
|
/// key 탭의 index 위치에 Love 를 추가
|
|
///
|
|
let key = keys[0]
|
|
let index = keys[1]
|
|
if let loves = account.getLoves(for: key), let index = Int(index) {
|
|
if index < loves.count {
|
|
guard let product = getProduct(isin: isin) else {
|
|
print("No product about isin: \(isin)")
|
|
return
|
|
}
|
|
if account.setLove(KissProfile.Love(isin: isin, name: product.itemName), index: index, for: key) {
|
|
print("Success \(product.itemName)")
|
|
account.saveProfile()
|
|
setCurrent(productNo: product.shortCode)
|
|
}
|
|
else {
|
|
print("Invalid index: \(index) for \(key)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
/// key 탭의 맨뒤에 Love 를 추가
|
|
///
|
|
guard let product = getProduct(isin: isin) else {
|
|
print("No product about isin: \(isin)")
|
|
return
|
|
}
|
|
account.addLove(KissProfile.Love(isin: isin, name: product.itemName), for: keys[0])
|
|
print("Success \(product.itemName)")
|
|
account.saveProfile()
|
|
setCurrent(productNo: product.shortCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func onHate(_ args: [String]) async {
|
|
guard let account = account else { return }
|
|
guard args.count > 1 else {
|
|
print("Invalid hate")
|
|
return
|
|
}
|
|
|
|
let key = args[0]
|
|
let isin = args[1]
|
|
if let love = account.removeLove(isin: isin, for: key) {
|
|
print("Success \(love.name)")
|
|
}
|
|
}
|
|
|
|
|
|
private func onLocalizeNames() async {
|
|
var symbols = Set<String>()
|
|
|
|
symbols.formUnion(DomesticShop.ProductResponse.Item.symbols())
|
|
symbols.formUnion(Domestic.BalanceResult.OutputStock.symbols())
|
|
symbols.formUnion(Domestic.BalanceResult.OutputAmount.symbols())
|
|
symbols.formUnion(MinutePriceResult.OutputPrice.symbols())
|
|
symbols.formUnion(PeriodPriceResult.OutputPrice.symbols())
|
|
symbols.formUnion(VolumeRankResult.OutputDetail.symbols())
|
|
symbols.formUnion(CurrentPriceResult.OutputDetail.symbols())
|
|
symbols.formUnion(CapturePrice.symbols())
|
|
symbols.formUnion(InvestorVolumeResult.OutputDetail.symbols())
|
|
symbols.formUnion(DomesticExtra.IndexPriceResult.Output.symbols())
|
|
symbols.formUnion(DomesticExtra.IndexPortfolioResult.Output.symbols())
|
|
symbols.formUnion(DomesticExtra.AllIndicesResult.Block.symbols())
|
|
symbols.formUnion(DomesticExtra.ShortSellingBalanceResult.OutBlock.symbols())
|
|
let newNames = symbols.sorted(by: { $0 < $1 })
|
|
|
|
let nameUrl = KissConsole.localNamesUrl
|
|
var addedNameCount = 0
|
|
var curNames = try! [LocalName].readCsv(fromFile: nameUrl, verifyHeader: true)
|
|
for name in newNames {
|
|
if false == curNames.contains(where: { $0.fieldName == name }) {
|
|
let item = try! LocalName(array: [name, ""], source: "")
|
|
curNames.append(item)
|
|
addedNameCount += 1
|
|
}
|
|
}
|
|
if addedNameCount > 0 {
|
|
do {
|
|
try curNames.writeCsv(toFile: nameUrl, localized: localized)
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
print("Success \(nameUrl.lastPathComponent) total: \(curNames.count), new: \(addedNameCount)")
|
|
}
|
|
|
|
|
|
private func onLocalizeOnOff(_ args: [String]) async {
|
|
guard args.count == 1 else {
|
|
print("Missing on/off option")
|
|
return
|
|
}
|
|
guard let option = OnOff(rawValue: args[0]) else {
|
|
print("Invalid on/off option")
|
|
return
|
|
}
|
|
switch option {
|
|
case .on:
|
|
localized = true
|
|
case .off:
|
|
localized = false
|
|
}
|
|
print("Localization \(option.rawValue)")
|
|
}
|
|
|
|
|
|
private func onReal(_ args: [String]) async {
|
|
guard args.count == 2 else {
|
|
print("Missing PNO and on/off")
|
|
return
|
|
}
|
|
let productNo = args[0]
|
|
guard let option = OnOff(rawValue: args[1]) else {
|
|
print("Invalid on/off option")
|
|
return
|
|
}
|
|
switch option {
|
|
case .on:
|
|
break
|
|
case .off:
|
|
break
|
|
}
|
|
print("WebSocket listening \(option.rawValue) for \(productNo)")
|
|
}
|
|
|
|
|
|
private func onDB(_ args: [String]) async {
|
|
guard args.count >= 3 else {
|
|
print("Missing options")
|
|
return
|
|
}
|
|
guard args[0] == "candle" else {
|
|
print("Missing candle or something")
|
|
return
|
|
}
|
|
switch args[1] {
|
|
case "build":
|
|
onCandleDB_Build(Array(args.dropFirst(2)))
|
|
case "validate":
|
|
onCandleDB_Validate(Array(args.dropFirst(2)))
|
|
case "count":
|
|
onCandleDB_Count(Array(args.dropFirst(2)))
|
|
default:
|
|
onCandleDB_Select(Array(args.dropFirst(1)))
|
|
}
|
|
}
|
|
|
|
|
|
private func onCandleDB_Build(_ args: [String]) {
|
|
var isBuildResumed = false
|
|
let productNo: String?
|
|
switch args[0] {
|
|
case "all":
|
|
productNo = nil
|
|
case "resume":
|
|
productNo = nil
|
|
isBuildResumed = true
|
|
default:
|
|
productNo = args[0]
|
|
}
|
|
let year = args[safe: 1]?.isYear == true ? args[safe: 1]: nil
|
|
let month = args[safe: 2]?.isMonth == true ? args[safe: 2]: nil
|
|
let day = args[safe: 3]?.isDay == true ? args[safe: 3]: nil
|
|
|
|
let candleFiles = collectCandleMinuteFiles(productNo: productNo, year: year, month: month, day: day)
|
|
if candleFiles.isEmpty {
|
|
print("No candle files of productNo: \(productNo ?? "all")")
|
|
return
|
|
}
|
|
let totalCsvFiles = candleFiles.reduce(0, { $0 + $1.value.count })
|
|
print("Building total candle files... \(totalCsvFiles)")
|
|
|
|
var curStep = 0
|
|
let maxStep = candleFiles.keys.count
|
|
for (productNo, csvFiles) in candleFiles {
|
|
curStep += 1
|
|
print("Building candle db for productNo: \(productNo), csv: \(csvFiles.count), step: \(curStep)/\(maxStep)")
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
defer {
|
|
semaphore.signal()
|
|
}
|
|
|
|
if isBuildResumed, let year = year {
|
|
if isCandleMinuteDBExisted(productNo: productNo, year: year) {
|
|
print("Skipping to build candle db for productNo: \(productNo) by resume")
|
|
return
|
|
}
|
|
}
|
|
let startTime = Date.appTime
|
|
if buildCandleMinuteDB(productNo: productNo, csvFiles: csvFiles) {
|
|
print("Success candle db with elapsed time: \(Date.appTime - startTime)")
|
|
}
|
|
|
|
try await Task.sleep(nanoseconds: 1_000_000_000 / 5)
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
}
|
|
|
|
|
|
private func onCandleDB_Validate(_ args: [String]) {
|
|
guard let productNo = args[safe: 0] else {
|
|
print("Missing productNo on validate")
|
|
return
|
|
}
|
|
guard let year = args[safe: 1], year.isYear else {
|
|
print("Missing year on validate")
|
|
return
|
|
}
|
|
let startTime = Date.appTime
|
|
if validateCandleMinuteDB(productNo: productNo, year: year) {
|
|
print("Success validate candle db with elapsed time: \(Date.appTime - startTime)")
|
|
}
|
|
}
|
|
|
|
|
|
private func onCandleDB_Count(_ args: [String]) {
|
|
guard let productNo = args[safe: 0] else {
|
|
print("Missing productNo on count")
|
|
return
|
|
}
|
|
guard let year = args[safe: 1], year.isYear else {
|
|
print("Missing year on validate")
|
|
return
|
|
}
|
|
let count = countCandleMinunteDB(productNo: productNo, year: year)
|
|
print("Total db item count: \(count) for productNo: \(productNo), year: \(year)")
|
|
}
|
|
|
|
|
|
private func onCandleDB_Select(_ args: [String]) {
|
|
guard let productNo = args[safe: 0] else {
|
|
print("Missing productNo on select")
|
|
return
|
|
}
|
|
guard let day = args[safe: 1], let (yyyy, MM, dd) = day.yyyyMMdd else {
|
|
print("Missing yyyyMMdd on select")
|
|
return
|
|
}
|
|
let HH = args[safe: 2]?.HH
|
|
let candles = selectCandleMinuteDB(productNo: productNo, year: yyyy, month: MM, day: dd, hour: HH)
|
|
let at = day + (HH == nil ? "": String(format: " %02d", HH!))
|
|
print("Total selected db item count: \(candles.count) for product: \(productNo), at: \(at)")
|
|
}
|
|
|
|
|
|
private func onNews(_ args: [String]) async {
|
|
guard args.count == 1, let day = args[0].yyyyMMdd_toDate else {
|
|
print("Missing day")
|
|
return
|
|
}
|
|
do {
|
|
let list = try await DomesticNews.collectList(day: day)
|
|
let obj = NewsList(list: list)
|
|
try writeNewsList(obj, day: day)
|
|
print("Success \(day.yyyyMMdd) total: \(list.count)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
|
|
private func onNewsAll(_ args: [String]) {
|
|
// guard args.count == 1, let day = args[0].yyyyMMdd_toDate else {
|
|
// print("Missing day")
|
|
// return
|
|
// }
|
|
let day = "20230801".yyyyMMdd_toDate!
|
|
do {
|
|
let obj = try readNewsList(day: day)
|
|
print("Success \(day.yyyyMMdd) total: \(obj.list.count)")
|
|
|
|
if let title = obj.list.first {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
Task {
|
|
do {
|
|
let article = try await DomesticNews.getArticle(title.url)
|
|
print("\(article)")
|
|
} catch {
|
|
print(error)
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
semaphore.wait()
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
// 특정 날짜의 뉴스 목록 & 뉴스 기사를 가져옴.
|
|
}
|
|
}
|
|
|
|
|
|
private extension Array {
|
|
func suffixStrings(from: Int) -> [String] where Element == String.SubSequence {
|
|
guard from < count else {
|
|
return []
|
|
}
|
|
return suffix(from: from).map { String($0) }
|
|
}
|
|
|
|
subscript(safe index: Int) -> Element? {
|
|
return indices.contains(index) ? self[index] : nil
|
|
}
|
|
}
|
|
|
|
|
|
enum OnOff: String {
|
|
case on
|
|
case off
|
|
}
|