Implement buy, sell, cancel command
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user