Add profile & enhance command line
This commit is contained in:
@@ -265,66 +265,89 @@ public struct Contract {
|
||||
public let orderDivision: OrderDivision
|
||||
public let orderQuantity: Int
|
||||
public let orderPrice: Int
|
||||
|
||||
public init(productNo: String, orderType: OrderType, orderDivision: OrderDivision, orderQuantity: Int, orderPrice: Int) {
|
||||
self.productNo = productNo
|
||||
self.orderType = orderType
|
||||
self.orderDivision = orderDivision
|
||||
self.orderQuantity = orderQuantity
|
||||
self.orderPrice = orderPrice
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Stock Order
|
||||
extension KissAccount {
|
||||
|
||||
public func orderStock(contract: Contract, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let accessToken = accessToken else {
|
||||
completion(.failure(GeneralError.invalidAccessToken))
|
||||
return
|
||||
}
|
||||
|
||||
let request = Domestic.StockOrderRequest(credential: credential, accessToken: accessToken, contract: contract)
|
||||
request.query { result in
|
||||
switch result {
|
||||
case .success(let result):
|
||||
completion(.success(true))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
public func orderStock(contract: Contract) async throws -> OrderResult {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
guard let accessToken = accessToken else {
|
||||
continuation.resume(throwing: GeneralError.invalidAccessToken)
|
||||
return
|
||||
}
|
||||
|
||||
let request = Domestic.StockOrderRequest(credential: credential, accessToken: accessToken, contract: contract)
|
||||
request.query { result in
|
||||
switch result {
|
||||
case .success(let result):
|
||||
continuation.resume(returning: result)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func cancelOrder(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let accessToken = accessToken else {
|
||||
completion(.failure(GeneralError.invalidAccessToken))
|
||||
return
|
||||
public func cancelOrder() async throws -> Bool {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
guard let accessToken = accessToken else {
|
||||
continuation.resume(throwing: GeneralError.invalidAccessToken)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
|
||||
public func changeOrder(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let accessToken = accessToken else {
|
||||
completion(.failure(GeneralError.invalidAccessToken))
|
||||
return
|
||||
public func changeOrder() async throws -> Bool {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
guard let accessToken = accessToken else {
|
||||
continuation.resume(throwing: GeneralError.invalidAccessToken)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
|
||||
public func getStockBalance(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let accessToken = accessToken else {
|
||||
completion(.failure(GeneralError.invalidAccessToken))
|
||||
return
|
||||
public func getStockBalance() async throws -> Bool {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
guard let accessToken = accessToken else {
|
||||
continuation.resume(throwing: GeneralError.invalidAccessToken)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
|
||||
public func canOrderStock(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard let accessToken = accessToken else {
|
||||
completion(.failure(GeneralError.invalidAccessToken))
|
||||
return
|
||||
public func canOrderStock() async throws -> Bool {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
|
||||
guard let accessToken = accessToken else {
|
||||
continuation.resume(throwing: GeneralError.invalidAccessToken)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
|
||||
// TODO: work
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ public struct VolumeRankResult: Codable {
|
||||
public let resultCode: String
|
||||
public let messageCode: String
|
||||
public let message: String
|
||||
public let output1: [OutputDetail]?
|
||||
public let output: [OutputDetail]?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case resultCode = "rt_cd"
|
||||
case messageCode = "msg_cd"
|
||||
case message = "msg1"
|
||||
case output1 = "Output1"
|
||||
case output = "output"
|
||||
}
|
||||
|
||||
public struct OutputDetail: Codable {
|
||||
|
||||
@@ -11,18 +11,62 @@ import Foundation
|
||||
public class KissAccount {
|
||||
|
||||
let credential: Credential
|
||||
var profileLock = NSLock()
|
||||
var profile = Profile()
|
||||
|
||||
var accessTokenLock = NSLock()
|
||||
var accessToken: String?
|
||||
var accessToken: String? {
|
||||
profileLock.lock()
|
||||
defer {
|
||||
profileLock.unlock()
|
||||
}
|
||||
return profile.recent?.accessToken
|
||||
}
|
||||
|
||||
public init(credential: Credential) {
|
||||
self.credential = credential
|
||||
self.accessToken = nil
|
||||
|
||||
// TODO: Profile 을 저장하고 로드하기
|
||||
// 만약 로드한 accessToken 이 유효하면 자동 로그인 성공
|
||||
}
|
||||
|
||||
func setAccessToken(_ accessToken: String?) {
|
||||
accessTokenLock.lock()
|
||||
self.accessToken = accessToken
|
||||
accessTokenLock.unlock()
|
||||
func setAccessToken(_ accessToken: String, expired: Date) {
|
||||
profileLock.lock()
|
||||
defer {
|
||||
profileLock.unlock()
|
||||
}
|
||||
|
||||
profile.recent = Recent(accessToken: accessToken, accessTokenExpired: expired)
|
||||
}
|
||||
|
||||
func resetAccessToken() {
|
||||
profileLock.lock()
|
||||
defer {
|
||||
profileLock.unlock()
|
||||
}
|
||||
|
||||
profile.recent = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension KissAccount {
|
||||
|
||||
public class Profile: Codable {
|
||||
public var recent: Recent?
|
||||
public var loves: [Love]
|
||||
|
||||
init() {
|
||||
recent = nil
|
||||
loves = []
|
||||
}
|
||||
}
|
||||
|
||||
public struct Recent: Codable {
|
||||
public let accessToken: String
|
||||
public let accessTokenExpired: Date
|
||||
}
|
||||
|
||||
public struct Love: Codable {
|
||||
// TODO: write
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ extension KissAccount {
|
||||
request.query { result in
|
||||
switch result {
|
||||
case .success(let result):
|
||||
self.setAccessToken(result.accessToken)
|
||||
self.setAccessToken(result.accessToken, expired: result.accessTokenExpiredDate)
|
||||
continuation.resume(returning: true)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
@@ -145,7 +145,7 @@ extension KissAccount {
|
||||
switch result {
|
||||
case .success(let result):
|
||||
if result.code == 200 {
|
||||
self.setAccessToken(nil)
|
||||
self.resetAccessToken()
|
||||
continuation.resume(returning: true)
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -10,14 +10,24 @@ import Foundation
|
||||
|
||||
public struct TokenResult: Codable {
|
||||
public let accessToken: String
|
||||
public let accessTokenExpired: String
|
||||
public let tokenType: String
|
||||
public let expiresIn: Int
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case accessTokenExpired = "access_token_token_expired"
|
||||
case tokenType = "token_type"
|
||||
case expiresIn = "expires_in"
|
||||
}
|
||||
|
||||
public var accessTokenExpiredDate: Date {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
let date = dateFormatter.date(from: accessTokenExpired)!
|
||||
return date
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,24 +19,47 @@ class KissConsole {
|
||||
|
||||
enum KissCommand: String {
|
||||
case quit = "quit"
|
||||
|
||||
// 로그인
|
||||
case loginMock = "login mock"
|
||||
case loginReal = "login real"
|
||||
case logout = "logout"
|
||||
case search = "search"
|
||||
case top = "top"
|
||||
|
||||
// 매매
|
||||
case buy = "buy"
|
||||
case sell = "sell"
|
||||
case cancel = "cancel"
|
||||
|
||||
// 보유 종목
|
||||
case openBag = "open bag"
|
||||
|
||||
// 종목 열람
|
||||
case loadShop = "load shop"
|
||||
case updateShop = "update shop"
|
||||
case look = "look"
|
||||
|
||||
// 진열 종목 (시스템에서 제공하는 추천 리스트)
|
||||
case showcase = "showcase"
|
||||
|
||||
// 관심 종목
|
||||
case love = "love" // love nuts.1
|
||||
case hate = "hate" // hate nuts.1
|
||||
|
||||
var needLogin: Bool {
|
||||
switch self {
|
||||
case .quit, .loginMock, .loginReal:
|
||||
return false
|
||||
case .logout, .search, .buy, .sell:
|
||||
case .logout, .top, .buy, .sell, .cancel:
|
||||
return true
|
||||
case .loadShop, .updateShop, .look:
|
||||
return false
|
||||
case .showcase:
|
||||
return false
|
||||
case .love, .hate:
|
||||
return false
|
||||
case .openBag:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,10 +74,13 @@ class KissConsole {
|
||||
|
||||
createSubpath("log")
|
||||
createSubpath("data")
|
||||
onLoadShop()
|
||||
|
||||
Task {
|
||||
await onLoadShop()
|
||||
}
|
||||
}
|
||||
|
||||
func getCommand(_ line: String) -> (KissCommand?, [String]) {
|
||||
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) {
|
||||
@@ -90,10 +116,11 @@ class KissConsole {
|
||||
case .loginMock: await onLogin(isMock: true)
|
||||
case .loginReal: await onLogin(isMock: false)
|
||||
case .logout: await onLogout()
|
||||
case .search: await onSearch(args)
|
||||
case .top: await onTop(args)
|
||||
case .buy: await onBuy(args)
|
||||
case .sell: await onSell(args)
|
||||
case .loadShop: onLoadShop()
|
||||
case .cancel: await onCancel(args)
|
||||
case .loadShop: await onLoadShop()
|
||||
case .updateShop: await onUpdateShop()
|
||||
case .look: await onLook(args)
|
||||
default:
|
||||
@@ -131,10 +158,10 @@ extension KissConsole {
|
||||
productsLock.unlock()
|
||||
}
|
||||
return products.filter { $0.key.contains(similarName) }
|
||||
// return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) }
|
||||
//return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) }
|
||||
}
|
||||
|
||||
private func loadShop(_ profile: Bool = true) {
|
||||
private func loadShop(_ profile: Bool = false) {
|
||||
let appTime1 = Date.appTime
|
||||
guard let stringCsv = try? String(contentsOfFile: shopProducts.path) else {
|
||||
return
|
||||
@@ -206,11 +233,14 @@ extension KissConsole {
|
||||
}
|
||||
|
||||
|
||||
private func onSearch(_ arg: [String]) async {
|
||||
private func onTop(_ arg: [String]) async {
|
||||
let option = RankingOption(divisionClass: .all, belongClass: .averageVolume)
|
||||
|
||||
do {
|
||||
_ = try await account?.getVolumeRanking(option: option)
|
||||
let rank = try await account?.getVolumeRanking(option: option)
|
||||
print("\(rank)")
|
||||
|
||||
// TODO: json 을 가공해서 csv table 로 보여주기
|
||||
} catch {
|
||||
print("\(error)")
|
||||
}
|
||||
@@ -218,21 +248,58 @@ extension KissConsole {
|
||||
|
||||
|
||||
private func onBuy(_ args: [String]) async {
|
||||
// TODO: work
|
||||
guard args.count == 3,
|
||||
let price = Int(args[1]),
|
||||
let quantity = Int(args[2]) else {
|
||||
return
|
||||
}
|
||||
|
||||
let productNo = args[0]
|
||||
if price < 100 || quantity <= 0 {
|
||||
print("Invalid price or quantity")
|
||||
return
|
||||
}
|
||||
|
||||
let contract = Contract(productNo: productNo,
|
||||
orderType: .buy,
|
||||
orderDivision: .limits,
|
||||
orderQuantity: quantity, orderPrice: price)
|
||||
do {
|
||||
let result = try await account?.orderStock(contract: contract)
|
||||
print(result)
|
||||
} catch {
|
||||
print("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func onSell(_ args: [String]) async {
|
||||
// TODO: work
|
||||
|
||||
}
|
||||
|
||||
|
||||
private func onLoadShop() {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.loadShop()
|
||||
private func onCancel(_ args: [String]) async {
|
||||
// TODO: work
|
||||
|
||||
do {
|
||||
try await account?.cancelOrder() { result in
|
||||
|
||||
}
|
||||
} catch {
|
||||
print("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func onLoadShop() async {
|
||||
return await withUnsafeContinuation { continuation in
|
||||
self.loadShop()
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func onUpdateShop() async {
|
||||
guard let _ = shop else {
|
||||
print("Invalid shop instance")
|
||||
@@ -294,20 +361,21 @@ extension KissConsole {
|
||||
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)")
|
||||
//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("\(first.shortCode) \(item.key.maxSpace(20)) \(first.marketCategory) \(first.baseDate)")
|
||||
print("\(first.isinCode) \(item.key.maxSpace(20)) \(first.marketCategory) \(first.baseDate)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,6 +391,7 @@ private extension Array {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension String {
|
||||
func maxSpace(_ length: Int) -> String {
|
||||
let count = unicodeScalars.reduce(0) {
|
||||
|
||||
Reference in New Issue
Block a user