Implement now command

This commit is contained in:
2023-05-27 22:53:34 +09:00
parent 3740b38fc8
commit bc88e14633
9 changed files with 162 additions and 38 deletions

View File

@@ -27,6 +27,12 @@ extension Date {
return dateFormatter.string(from: self)
}
public var HHmmss: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HHmmss"
return dateFormatter.string(from: self)
}
public static var appTime: TimeInterval {
ProcessInfo.processInfo.systemUptime
}

View File

@@ -137,7 +137,7 @@ extension Request {
}
let stringData = String(data: data, encoding: .utf8) ?? ""
print(stringData.prefix(1024))
//print(stringData.prefix(1024))
if responseDataLoggable {
let logName = "log/\(Date().yyyyMMdd_HHmmssSSSS_forFile)_\(url.lastPathComponent).json"
let logUrl = URL.currentDirectory().appending(path: logName)

View File

@@ -76,7 +76,7 @@ extension SeibroRequest {
}
let stringData = String(data: data, encoding: .utf8) ?? ""
print(stringData)
//print(stringData)
if responseDataLoggable {
let logName = "log/\(Date().yyyyMMdd_HHmmssSSSS_forFile)_\(url.lastPathComponent).json"
let logUrl = URL.currentDirectory().appending(path: logName)

View File

@@ -285,6 +285,8 @@ public struct Contract {
// MARK: Stock Order
extension KissAccount {
///
///
public func orderStock(contract: Contract) async throws -> OrderResult {
return try await withUnsafeThrowingContinuation { continuation in
@@ -332,6 +334,8 @@ extension KissAccount {
}
///
///
public func getStockBalance() async throws -> BalanceResult {
return try await withUnsafeThrowingContinuation { continuation in
@@ -353,6 +357,8 @@ extension KissAccount {
}
///
///
public func canOrderStock(productNo: String, division: OrderDivision, price: Int) async throws -> PossibleOrderResult {
return try await withUnsafeThrowingContinuation { continuation in

View File

@@ -77,7 +77,7 @@ extension Domestic {
"FID_ETC_CLS_CODE": "",
"FID_COND_MRKT_DIV_CODE": "J",
"FID_INPUT_ISCD": productNo,
"FID_INPUT_HOUR_1": startTime,
"FID_INPUT_HOUR_1": startTodayTime,
"FID_PW_DATA_INCU_YN": "N",
]
}
@@ -91,13 +91,13 @@ extension Domestic {
public let accessToken: String
let productNo: String
let startTime: String // HHMMSS
let startTodayTime: String // HHMMSS
public init(credential: Credential, accessToken: String, productNo: String, startTime: String) {
public init(credential: Credential, accessToken: String, productNo: String, startTodayTime: String) {
self.credential = credential
self.accessToken = accessToken
self.productNo = productNo
self.startTime = startTime
self.startTodayTime = startTodayTime
}
}
}
@@ -106,23 +106,48 @@ extension Domestic {
// MARK: Stock Price
extension KissAccount {
public func getCurrentPrice(completion: @escaping (Result<Bool, Error>) -> Void) {
guard let accessToken = accessToken else {
completion(.failure(GeneralError.invalidAccessToken))
return
///
///
public func getCurrentPrice(productNo: String) async throws -> CurrentPriceResult {
return try await withUnsafeThrowingContinuation { continuation in
guard let accessToken = accessToken else {
continuation.resume(throwing: GeneralError.invalidAccessToken)
return
}
let request = Domestic.StockCurrentPriceRequest(credential: credential, accessToken: accessToken, productNo: productNo)
request.query { result in
switch result {
case .success(let result):
continuation.resume(returning: result)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
// TODO:
}
public func getMinutePrice(completion: @escaping (Result<Bool, Error>) -> Void) {
guard let accessToken = accessToken else {
completion(.failure(GeneralError.invalidAccessToken))
return
///
///
public func getMinutePrice(productNo: String, startTodayTime: Date) async throws -> MinutePriceResult {
return try await withUnsafeThrowingContinuation { continuation in
guard let accessToken = accessToken else {
continuation.resume(throwing: GeneralError.invalidAccessToken)
return
}
let request = Domestic.StockTodayMinutePriceRequest(credential: credential, accessToken: accessToken, productNo: productNo, startTodayTime: startTodayTime.HHmmss)
request.query { result in
switch result {
case .success(let result):
continuation.resume(returning: result)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
// TODO:
}
}

View File

@@ -43,7 +43,7 @@ public struct CurrentPriceResult: 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"
@@ -85,7 +85,7 @@ public struct CurrentPriceResult: Codable {
public let koreanMarketName: String
///
public let newHighLowPriceClassCode: String
public let newHighLowPriceClassCode: String?
///
public let koreanBusinessTypeName: String
@@ -292,7 +292,7 @@ public struct CurrentPriceResult: Codable {
public let capitalCurrency: String
///
public let approachRate: String
public let approachRate: String?
///
public let foreignHoldQuantity: String

View File

@@ -96,13 +96,25 @@ extension DomesticShop {
}
public struct Item: Codable {
///
public let baseDate: String
///
public let shortCode: String
/// ISIN
public let isinCode: String
///
public let marketCategory: String
/// ()
public let itemName: String
///
public let corporationNo: String
private enum CodingKeys: String, CodingKey {
case baseDate = "basDt"
case shortCode = "srtnCd"
@@ -114,7 +126,11 @@ extension DomesticShop {
public init(_ array: [String]) {
self.baseDate = array[0]
self.shortCode = array[1]
/// shortCode A000000 A .
/// 7 , .
///
self.shortCode = (array[1].count == 7 ? String(array[1].suffix(6)): array[1])
self.isinCode = array[2]
self.marketCategory = array[3]
self.itemName = array[4]

View File

@@ -16,6 +16,7 @@ class KissConsole {
var productsLock = NSLock()
var products = [String: [DomesticShop.Product]]()
var currentShortCode: String?
enum KissCommand: String {
case quit = "quit"
@@ -34,6 +35,10 @@ class KissConsole {
//
case openBag = "open bag"
//
case now = "now"
case candle = "candle"
//
case loadShop = "load shop"
case updateShop = "update shop"
@@ -55,6 +60,8 @@ class KissConsole {
return true
case .openBag:
return true
case .now, .candle:
return true
case .loadShop, .updateShop, .look:
return false
case .showcase:
@@ -123,8 +130,12 @@ class KissConsole {
case .buy: await onBuy(args)
case .sell: await onSell(args)
case .cancel: await onCancel(args)
case .openBag: await onOpenBag()
case .now: await onNow(args)
case .candle: await onCandle(args)
case .loadShop: await onLoadShop()
case .updateShop: await onUpdateShop()
case .look: await onLook(args)
@@ -172,13 +183,22 @@ extension KissConsole {
//return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) }
}
private func getProductName(isin: String) -> String? {
private func getProduct(isin: String) -> DomesticShop.Product? {
productsLock.lock()
defer {
productsLock.unlock()
}
return products.compactMap { $0.value.first(where: { $0.isinCode == isin })?.itemName }.first
return products.compactMap { $0.value.first(where: { $0.isinCode == isin }) }.first
}
private func getProduct(shortCode: String) -> DomesticShop.Product? {
productsLock.lock()
defer {
productsLock.unlock()
}
return products.compactMap { $0.value.first(where: { $0.shortCode == shortCode }) }.first
}
private func loadShop(_ profile: Bool = false) {
@@ -366,6 +386,49 @@ extension KissConsole {
}
private func onNow(_ args: [String]) async {
let productNo: String? = (args.isEmpty ? currentShortCode: args[0])
guard let productNo = productNo else {
print("Invalid productNo")
return
}
do {
let result = try await account!.getCurrentPrice(productNo: productNo)
if let output = result.output {
let productName = getProduct(shortCode: output.shortProductCode)?.itemName ?? ""
print("\t종목명: ", productName)
print("\t업종명: ", output.koreanMarketName, output.koreanBusinessTypeName)
print("\t주식 현재가: ", output.currentStockPrice)
print("\t전일 대비: ", output.previousDayVariableRatio)
print("\t누적 거래 대금: ", output.accumulatedTradingAmount)
print("\t누적 거래량: ", output.accumulatedVolume)
print("\t전일 대비 거래량 비율: ", output.previousDayDiffVolumeRatio)
print("\t주식 시가: ", output.stockPrice)
print("\t주식 최고가: ", output.highestStockPrice)
print("\t주식 최저가: ", output.lowestStockPrice)
print("\t외국인 순매수 수량: ", output.foreignNetBuyingQuantity)
print("\t외국인 보유 수량: ", output.foreignHoldQuantity)
print("\t최종 공매도 체결 수량: ", output.lastShortSellingConclusionQuantity)
print("\t프로그램매매 순매수 수량: ", output.programTradeNetBuyingQuantity)
print("\t자본금: ", output.capital)
print("\t상장 주수: ", output.listedStockCount)
print("\tHTS 시가총액: ", output.htsTotalMarketValue)
print("\tPER: ", output.per)
print("\tPBR: ", output.pbr)
print("\t주식 단축 종목코드", output.shortProductCode)
}
} catch {
print("\(error)")
}
}
private func onCandle(_ args: [String]) async {
}
private func onLoadShop() async {
return await withUnsafeContinuation { continuation in
self.loadShop()
@@ -449,7 +512,11 @@ extension KissConsole {
}
for item in items {
if let first = item.value.first {
print("\(first.isinCode) \(item.key.maxSpace(20)) \(first.marketCategory) \(first.baseDate)")
currentShortCode = first.shortCode
print("\tISIN: ", first.isinCode)
print("\t상품명: ", item.key.maxSpace(20))
print("\t단축코드: ", first.shortCode)
print("\t시장구분: ", first.marketCategory, "\t기준일자: ", first.baseDate)
}
}
}
@@ -497,13 +564,14 @@ extension KissConsole {
let index = keys[1]
if let loves = account.getLoves(for: key), let index = Int(index) {
if index < loves.count {
guard let name = getProductName(isin: isin) else {
guard let product = getProduct(isin: isin) else {
print("No product about isin: \(isin)")
return
}
if account.setLove(KissProfile.Love(isin: isin, name: name), index: index, for: key) {
print("Success \(name)")
if account.setLove(KissProfile.Love(isin: isin, name: product.itemName), index: index, for: key) {
print("Success \(product.itemName)")
account.saveProfile()
currentShortCode = product.shortCode
}
else {
print("Invalid index: \(index) for \(key)")
@@ -514,13 +582,14 @@ extension KissConsole {
else {
/// key Love
///
guard let name = getProductName(isin: isin) else {
guard let product = getProduct(isin: isin) else {
print("No product about isin: \(isin)")
return
}
account.addLove(KissProfile.Love(isin: isin, name: name), for: keys[0])
print("Success \(name)")
account.addLove(KissProfile.Love(isin: isin, name: product.itemName), for: keys[0])
print("Success \(product.itemName)")
account.saveProfile()
currentShortCode = product.shortCode
}
}
}

View File

@@ -17,17 +17,19 @@ command | 설명
`login real` | Real 서버로 로그인. real-server.json 을 credential 로 사용.
`logout` | 접속한 서버에서 로그아웃
`top` | 상위 거래량 30종목 (평균거래량)
WIP `buy (ISCD) (수량)` | 구매
WIP `sell (ISCD) (수량)` | 판매
WIP `cancel (ISCD)` | 주문 취소
WIP `buy (ISIN) (수량)` | 구매
WIP `sell (ISIN) (수량)` | 판매
WIP `cancel (ISIN)` | 주문 취소
`open bag` | 보유 종목 열람
`now [ISIN]` | 종목의 현재가 열람. ISIN 은 생략 가능
`candle [ISIN]` | 종목의 분봉 열람. ISIN 은 생략 가능
`load shop` | data/shop-products.csv 로부터 전체 상품을 로딩
`update shop` | **금융위원회_KRX상장종목정보** 로부터 전체 상품을 얻어서 data/shop-products.csv 로 저장
`look (상품명)` | (상품명) 에 해당되는 ISCD 를 표시함
`look (상품명)` | (상품명) 에 해당되는 ISIN 를 표시함
WIP `showcase` | 추천 상품을 제안함
`loves` | 관심 종목 전체를 열람. profile.json 에 저장된 관심 종목을 표시함.
`love (탭).(번호) (ISCD)` | 관심 종목에 추가함. (번호) 를 지정하지 않으면 (탭) 마지막에 추가함.
`hate (탭) (ISCD)` | 관심 종목에서 삭제함.
`love (탭).(번호) (ISIN)` | 관심 종목에 추가함. (번호) 를 지정하지 않으면 (탭) 마지막에 추가함.
`hate (탭) (ISIN)` | 관심 종목에서 삭제함.
# KissCredential