From ee491c31074a1ac763ef74eed0afffd282910109 Mon Sep 17 00:00:00 2001 From: ened Date: Tue, 30 May 2023 19:30:22 +0900 Subject: [PATCH] Implement buy, sell, cancel command --- KissMe/Sources/Domestic/DomesticStock.swift | 39 ++++- .../Domestic/DomesticStockResult.swift | 8 +- KissMeConsole/Sources/KissConsole.swift | 160 +++++++++++++++--- README.md | 9 +- 4 files changed, 178 insertions(+), 38 deletions(-) diff --git a/KissMe/Sources/Domestic/DomesticStock.swift b/KissMe/Sources/Domestic/DomesticStock.swift index d7ca66f..992f440 100644 --- a/KissMe/Sources/Domestic/DomesticStock.swift +++ b/KissMe/Sources/Domestic/DomesticStock.swift @@ -32,7 +32,7 @@ public struct Domestic { "ACNT_PRDT_CD": accountNo2, "PDNO": productNo, "ORD_DVSN": orderDivision.code, - "ORD_QTY": orderQuantity, + "ORD_QTY": String(orderQuantity), "ORD_UNPR": String(orderPrice), ] } @@ -130,7 +130,7 @@ public struct Domestic { let orderPrice: Int let isAllQuantity: Bool - public init(credential: Credential, accessToken: String, productNo: String, orderOrganizationNo: String, orderNo: String, orderDivision: OrderDivision, orderRevisionType: OrderRevisionType, orderQuantity: Int = 0, orderPrice: Int) { + public init(credential: Credential, accessToken: String, productNo: String, orderOrganizationNo: String, orderNo: String, orderDivision: OrderDivision, orderRevisionType: OrderRevisionType, orderQuantity: Int, orderPrice: Int) { self.credential = credential self.accessToken = accessToken self.productNo = productNo @@ -229,8 +229,8 @@ public struct Domestic { "PDNO": productNo, "ORD_UNPR": String(orderPrice), "ORD_DVSN": orderDivision.code, - "CMA_EVLU_AMT_ICLD_YN": "", - "OVRS_ICLD_YN": "", + "CMA_EVLU_AMT_ICLD_YN": "N", + "OVRS_ICLD_YN": "N", ] } public var result: KResult? = nil @@ -282,6 +282,21 @@ public struct Contract { } +public struct ContractCancel { + public let productNo: String + public let orderNo: String + + // 모든 수량을 취소하려면 0 으로 설정 + public let orderQuantity: Int + + public init(productNo: String, orderNo: String, orderQuantity: Int) { + self.productNo = productNo + self.orderNo = orderNo + self.orderQuantity = orderQuantity + } +} + + // MARK: Stock Order extension KissAccount { @@ -308,15 +323,23 @@ extension KissAccount { } - public func cancelOrder() async throws -> Bool { + public func cancelOrder(cancel: ContractCancel) async throws -> OrderRevisionResult { return try await withUnsafeThrowingContinuation { continuation in - guard let _ = accessToken else { + guard let accessToken = accessToken else { continuation.resume(throwing: GeneralError.invalidAccessToken) return } - // TODO: work + let request = Domestic.StockOrderRevisionRequest(credential: credential, accessToken: accessToken, productNo: cancel.productNo, orderOrganizationNo: "", orderNo: cancel.orderNo, orderDivision: .limits, orderRevisionType: .cancel, orderQuantity: cancel.orderQuantity, orderPrice: 0) + request.query { result in + switch result { + case .success(let result): + continuation.resume(returning: result) + case .failure(let error): + continuation.resume(throwing: error) + } + } } } @@ -357,7 +380,7 @@ extension KissAccount { } - /// 주식을 주문할 수 있는지 판단하기 + /// 주문 가능한 주식 수량 판단하기 /// public func canOrderStock(productNo: String, division: OrderDivision, price: Int) async throws -> PossibleOrderResult { return try await withUnsafeThrowingContinuation { continuation in diff --git a/KissMe/Sources/Domestic/DomesticStockResult.swift b/KissMe/Sources/Domestic/DomesticStockResult.swift index a6cf336..f715385 100644 --- a/KissMe/Sources/Domestic/DomesticStockResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockResult.swift @@ -18,12 +18,12 @@ public struct OrderResult: Codable { public let resultCode: String public let messageCode: String public let message: String - public let output: [OutputDetail]? + public let output: OutputDetail? private enum CodingKeys: String, CodingKey { case resultCode = "rt_cd" case messageCode = "msg_cd" - case message = "msg" + case message = "msg1" case output } @@ -45,7 +45,7 @@ public struct OrderRevisionResult: Codable { public let resultCode: String public let messageCode: String public let message: String - public let output: [OutputDetail]? + public let output: OutputDetail? private enum CodingKeys: String, CodingKey { case resultCode = "rt_cd" @@ -303,7 +303,7 @@ public struct PossibleOrderResult: Codable { public let resultCode: String public let messageCode: String public let message: String - public let output: [OutputPossibleDetail]? + public let output: OutputPossibleDetail? private enum CodingKeys: String, CodingKey { case resultCode = "rt_cd" diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index 855e517..2d92bf5 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -30,8 +30,10 @@ class KissConsole { // 매매 case buy = "buy" + case buyCheck = "buy check" case sell = "sell" case cancel = "cancel" + case modify = "modify" // 보유 종목 case openBag = "open bag" @@ -58,7 +60,7 @@ class KissConsole { switch self { case .quit, .loginMock, .loginReal: return false - case .logout, .top, .buy, .sell, .cancel: + case .logout, .top, .buy, .buyCheck, .sell, .cancel, .modify: return true case .openBag: return true @@ -85,13 +87,16 @@ class KissConsole { createSubpath("log") createSubpath("data") lastLogin() - + let semaphore = DispatchSemaphore(value: 0) Task { await onLoadShop() semaphore.signal() } semaphore.wait() + + // 005930: 삼성전자 + setCurrent(productNo: "005930") } private func getCommand(_ line: String) -> (KissCommand?, [String]) { @@ -165,8 +170,10 @@ class KissConsole { case .top: await onTop(args) 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() @@ -312,6 +319,15 @@ extension KissConsole { print("\(error)") } } + + private func setCurrent(productNo: String) { + productsLock.lock() + currentShortCode = productNo + productsLock.unlock() + + let productName = getProduct(shortCode: productNo)?.itemName ?? "" + print("current product \(productNo) \(productName)") + } } @@ -369,30 +385,65 @@ extension KissConsole { private func onBuy(_ args: [String]) async { guard args.count == 3 else { + print("Missing buy paramters: buy (PNO) (PRICE) (QUANTITY)") return } - guard let price = Int(args[1]) else { - return - } - guard let quantity = Int(args[2]) else { - return - } - let productNo = args[0] - if price < 100 || quantity <= 0 { - print("Invalid price or quantity") + 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: .limits, + orderDivision: division, orderQuantity: quantity, orderPrice: price) do { - let result = try await account?.orderStock(contract: contract) - - // TODO: 매수처리 이후 - print(result) + 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)") } @@ -400,22 +451,85 @@ extension KissConsole { private func onSell(_ args: [String]) async { - // TODO: work + 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 { - // TODO: work + 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 _ = try await account?.cancelOrder() + 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() @@ -445,7 +559,6 @@ extension KissConsole { do { let result = try await account!.getCurrentPrice(productNo: productNo) if let output = result.output { - currentShortCode = output.shortProductCode let productName = getProduct(shortCode: output.shortProductCode)?.itemName ?? "" print("\t종목명: ", productName) print("\t업종명: ", output.koreanMarketName, output.koreanBusinessTypeName) @@ -467,6 +580,7 @@ extension KissConsole { print("\tPER: ", output.per) print("\tPBR: ", output.pbr) print("\t주식 단축 종목코드", output.shortProductCode) + setCurrent(productNo: output.shortProductCode) } } catch { print("\(error)") @@ -637,11 +751,11 @@ extension KissConsole { } for item in items { if let first = item.value.first { - currentShortCode = first.shortCode 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) } } } @@ -697,7 +811,7 @@ extension KissConsole { if account.setLove(KissProfile.Love(isin: isin, name: product.itemName), index: index, for: key) { print("Success \(product.itemName)") account.saveProfile() - currentShortCode = product.shortCode + setCurrent(productNo: product.shortCode) } else { print("Invalid index: \(index) for \(key)") @@ -715,7 +829,7 @@ extension KissConsole { account.addLove(KissProfile.Love(isin: isin, name: product.itemName), for: keys[0]) print("Success \(product.itemName)") account.saveProfile() - currentShortCode = product.shortCode + setCurrent(productNo: product.shortCode) } } } diff --git a/README.md b/README.md index 20b9ca6..ffd8959 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,11 @@ command | 설명 `login real` | Real 서버로 로그인. real-server.json 을 credential 로 사용. `logout` | 접속한 서버에서 로그아웃. `top` | 상위 거래량 30종목 (평균거래량) -WIP `buy (PNO) (수량)` | 구매 -WIP `sell (PNO) (수량)` | 판매 -WIP `cancel (PNO)` | 주문 취소 +`buy (PNO) (가격) (수량)` | 상품을 구매. (가격) 에 -8282 로 입력하면 시장가격. (수량) 에 -82 로 입력하면 최대수량. +`buy check (PNO) (가격)` | 현재 잔고로 구매가 가능한 수량을 확인. +`sell (PNO) (가격) (수량)` | 보유한 상품을 판매. (가격) 에 -8282 로 입력하면 시장가격. +`cancel (PNO) (ONO) (수량)` | 주문 내역의 일부를 취소. (수량) 에 -82 로 입력하면 전체수량. +WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) 에 -82 로 입력하면 전체수량. `open bag` | 보유 종목 열람. `now [PNO]` | 종목의 현재가 열람. PNO 은 생략 가능. `candle [PNO]` | 종목의 분봉 열람. PNO 은 생략 가능. @@ -34,6 +36,7 @@ WIP `showcase` | 추천 상품을 제안함. `hate (탭) (PNO)` | 관심 종목에서 삭제함. * PNO 는 `Product NO` 의 약자이고, 상품의 `단축코드` (shortCode) 와 동일합니다. +* ONO 는 `Order NO` 의 약자이고, 고유한 주문번호 입니다. # KissMeMatrix