Implement now command
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: 분봉 정보를 가져오기
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
README.md
14
README.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user