대신증권 CYBOS PLUS 프로그램 구현 (20) - 전종목 차트 데이터 저장하기 ③
프로그램 구현 목표
- 문제점 보완하기 ③: 조회하는 시점의 날짜 데이터 획득하기
- 문제점 보완하기 ④: 저장된 날짜를 기준으로 차트 데이터를 조회하고 새 날짜 저장하기
지난 게시글에서 이미 저장되어 있는 차트 데이터 조회 일자를 불러오는 기능까지는 구현했으니 이번 게시글에서는 `time` 라이브러리를 활용하여 차트 데이터를 조회하는 시점의 날짜 데이터를 획득하고, 차트 데이터 조회를 완료한 경우 그 날짜 데이터를 테이블에 저장하는 기능을 구현해볼 예정이다. 아마도 이번 게시글에서 전종목 차트 데이터 저장 기능은 모두 구현할 수 있을 것이다.
문제점 보완하기 ③: 조회하는 시점의 날짜 데이터 획득하기
파이썬에서 조회하는 시점의 날짜 데이터를 얻어올 수 있는 방법은 바로 `datetime` 라이브러리를 활용하는 것이다. 이 라이브러리는 시간에 대한 정보를 다룰 수 있는 라이브러리로, 오늘의 날짜와 시간 정보를 불러온다거나 아니면 오늘로부터 며칠 전의 날짜는 어떤 값인지를 구한다거나 아니면 특정 날짜가 주말인지 아닌지(공휴일인지 아닌지까지를 구분하는 기능은 공휴일은 기본적으로 국가별로 다르기 때문에 지원되지 않는다.)를 구한다거나 하는 기능을 사용할 수 있다. 사실, 차트 데이터를 조회하는 시점의 날짜 데이터를 획득하는 것은 어렵지 않다. `datetime` 라이브러리의 `now()` 함수를 활용하면 되기 때문이다. 아래의 코드를 확인해보자.
※ Line: 12, 32, 33
## Boss.py ##
import pandas as pd
import win32com.client
from pywinauto import application
import con_mysql
from COM import CpSysDib
from COM import CpUtil
from COM import CpTrade
from COM import DsCbo1
import time
import datetime
from threading import Thread
## GUI ##
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from gui import qt_gui
main_ui = uic.loadUiType("main.ui")[0]
class cybos(QMainWindow, main_ui):
def __init__(self):
super().__init__()
self.setupUi(self)
self._open_cybosplus() ## 자동 로그인 함수
self._load_account() ## 계좌 조회 함수
self._load_DB() ## 데이터베이스 조회 함수
self.saved_point = con_mysql.manage_db()._read_table('saved_point', 'saved_point')
print(self.saved_point)
# self._load_savepoint() ## saved_point 테이블 생성 함수
now_date = datetime.datetime.now()
print(now_date)
▶ 실행 결과 확인하기
2024-09-22 14:23:25.074249
실행 결과를 보면 알 수 있겠지만, 우리가 필요한 연월일 정보 뿐만 아니라 굳이 필요하지 않은 시분초에 대한 정보도 함께 전달해주고 있는데, 이 데이터를 단순하게 YYYYMMDD(20240922)와 같은 형식으로 바꾸고 싶은 경우에는 `strftime()` 함수를 사용해주면 된다. 이 함수는 우리가 만들고자 하는 데이터에 해당하는 값들을 뜻하는 정보(연도는 %Y, 월은 %m, 일은 %d와 같은 정보)를 전달해주어야 하는데, 자세한 내용은 datetime 라이브러리에 대한 문서(클릭 시 이동, strftime() and strptime() Format Codes 부분 참조)를 참고해보도록 하고 여기서는 연월일과 시분초에 대해서만 간략하게 알아보고 넘어가도록 하자.
- 연: %Y
- 월: %m
- 일: %d
- 시: %H (12시간 기준은 %I (L 아니고 I))
- 분: %M
- 초: %S
그렇다면 현재 시간을 위의 2024-09-22 14:23:25가 아니라 20240922 14:23:25와 같은 형태로 표기하고자 할 경우에는 `strftime` 함수에 어떠한 정보를 전달해주어야 할까 ? 기존에 작성했던 코드의 뒷 부분에다가 `strftime()` 부분을 추가하여 프로그램을 실행시켜보면 20240922 14:30:22와 같은 형태로 시간 데이터가 출력된다는 것을 확인할 수 있다.
## YYYYMMDD HH:MM:SS 표시 방법
now_date = datetime.datetime.now().strftime(f"%Y%m%d %H:%M:%S")
그렇다면 시분초 데이터는 나중에 구현하도록 하고, 일단은 %Y%m%d 부분만 사용해서 연월일 데이터만 가져가도록 하자. 차트 데이터를 저장하는 데에 있어서 시간까지는 필요하지 않기 때문이다. 하지만 우리가 한 가지 생각해주어야 할 부분이 있다. 바로 차트 데이터를 조회하는 시점이 언제인가에 대한 내용이다. 일반적으로 장이 마감되는 시점은 4시 즈음(3시 30분으로 많이들 알고 있겠지만 장종료후 거래를 포함하면 조금 더 열려있다)인데, 본인의 경우에는 차트 데이터가 올바르게 저장되어 있지 않을 수 있는 점을 고려하여 4시 30분쯤이 되어야만 그 날의 차트 데이터가 모두 저장되었다고 보는 편이다. 다시 말해, 4시 30분 이전에는 차트 데이터가 완성되지 않았을 것이기 때문에 차트 데이터를 저장한다고 하더라도 오늘 날짜가 아니라 어제 날짜 데이터를 저장해주는 것이다. 아래의 몇 가지 상황을 살펴보도록 하자.
- 평일 3시에 조회한 차트 데이터 : 어제 날짜로 저장
- 평일 5시에 조회한 차트 데이터 : 오늘 날짜로 저장
- 주말 3시에 조회한 차트 데이터 : 평일 마지막 거래일로 저장
- 주말 5시에 조회한 차트 데이터 : 평일 마지막 거래일로 저장
이제 뭔가 좀 경우의 수를 따져서 날짜 데이터를 구해야 한다는 것을 생각하니 골치가 아파올 수도 있으니, 현 시점에서 차트 데이터가 완성된 날짜를 구하는 함수를 하나 만들어서 자동으로 계산해내도록 해보자. 간단하게 말해서, 오늘이 주말이면 이전의 평일 날짜를 반환하도록 하고 오늘이 평일이면 시간에 따라서 어제 날짜든 오늘 날짜든 차트 데이터가 모두 완성되었을 가장 최신의 날짜를 반환하도록 하는 것이다. 일단 이와 같은 단순한 기능이 담긴 함수들을 모두 넣어줄 etc.py 파일을 생성한 후, 그 안에 함수를 만들어보도록 하자.
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
pass
이제 `datetime.datetime.now()`를 사용하여 오늘 날짜에 대한 모든 정보를 불러온 후, `strftime()` 함수를 활용하여 오늘 날짜(`now_date`)와 현 시점의 시간(`now_time`)을 반환받아보자.
※ Line: 6 ~ 9
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
if __name__ == "__main__":
_return_latestdate()
▶ 실행 결과 확인하기
[20240922] 144634
이제 오늘 날짜가 주말인지 아닌지를 확인할 수 있어야 하는데, 이 기능은 `weekday()` 함수가 담당하고 있다. 하지만 이 기능을 수행하기 위해서는 `strftime()` 함수를 통해 데이터타입을 변경(Type: datetime → Type: str)하기 전에 있는 데이터(Type: datetime)로 판단해주어야 한다. 왜냐하면 `strftime()` 함수를 사용한 후의 데이터는 데이터타입을 확인해보면 알겠지만 단순한 문자열에 해당하기 때문이다. 따라서 주말인지 평일인지의 여부는 `now_date` 변수가 아니라 데이터타입을 변경하기 전인 `standard` 변수를 통해 확인해주면 된다. 이 `weekday()` 함수는 0에서 6까지의 값을 반환해주는데, 월요일이 0에 해당하고 일요일이 6에 해당한다. 즉, 이 결과값이 5 또는 6이라면 주말에 해당하고 0에서 4 사이의 값이라면 평일에 해당한다는 것이다.
※ Line: 11
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
print(standard.weekday())
if __name__ == "__main__":
_return_latestdate()
▶ 실행 결과 확인하기(2024년 9월 22일은 일요일이므로 6이 반환됨)
[20240922] 145332
6
이제 조건문을 사용하여 평일과 주말을 구분해주도록 하자.
※ Line: 13 ~ 16
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
print(standard.weekday())
if standard.weekday() <= 4: ## 평일
pass
else: ## 주말
pass
if __name__ == "__main__":
_return_latestdate()
평일과 주말을 구분했다면 평일의 경우에는 이제 시간을 기준으로 하여 오늘 날짜를 반환할지 어제 날짜를 반환할지를 결정하는 코드를 추가해주어야 한다. 앞서 이야기했던 것처럼 차트 데이터가 완성된 시간을 4시 30분을 기준으로 한다고 가정하고 어제 날짜와 오늘 날짜 중 어떠한 날짜를 반환할지 판단하는 기능을 구현해보도록 하자. 이 기능을 구현하는 방법은 단순하게 "163000"과 같은 문자열로 판단의 기준이 되는 기준 시간을 정해놓고 문자열('str')로 바꾸어주었던 `now_time` 변수와의 대소 비교를 통해 처리하면 된다. 아래의 코드를 보면 이 글을 작성하는 시점인 14시 59분 28초를 기준으로 하여 "163000"과 "143000"이라는 두 가지 값을 놓고 대소비교를 하도록 해두었는데, 현재 시간이 오후 4시 30분을 의미하는 "163000"보다 작냐는 물음에는 맞다(True를 반환)라고 하고 있으며 현재 시간이 오후 2시 30분을 의미하는 "143000"보다 작냐는 물음에는 아니다(False를 반환)라고 하고 있음을 확인할 수 있다.
※ Line: 12, 13
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
print(standard.weekday())
print(now_time <= "163000")
print(now_time <= "143000")
if standard.weekday() <= 4: ## 평일
pass
else: ## 주말
pass
if __name__ == "__main__":
_return_latestdate()
▶ 실행 결과 확인하기
[20240922] 145928
6
True
False
이제 여기까지의 코드를 정리하면 아래와 같은 세 가지 케이스로 정리할 수 있을 것이다.
- 평일 오후 4시 30분 이전: 어제 날짜 반환 (Line: 12)
- 평일 오후 4시 30분 이후: 오늘 날짜 반환 (Line: 14)
- 주말: 가장 최근의 평일 날짜 반환 (Line: 16)
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
if standard.weekday() <= 4: ## 평일
if now_time <= "163000": ## 어제 날짜 반환
pass
else: ## 오늘 날짜 반환
pass
else: ## 주말(가장 최근의 평일 날짜 반환)
pass
if __name__ == "__main__":
_return_latestdate()
여기서 위의 세 가지 경우의 수 중에서 두 번째 경우의 수(오늘 날짜 반환)는 간단하다. 왜냐하면 `now_date`를 바로 반환해주면 되기 때문이다. 그렇다면 첫 번째 경우의 수(어제 날짜 반환)는 오늘 날짜보다도 하루 이전인 어제의 날짜 데이터를 반환해주어야 하는데, 다행히도 `datetime` 라이브러리는 날짜를 계산할 수 있도록 하는 함수인 `timedelta()` 함수를 제공해주고 있다. 이 함수는 앞서 사용해보았던 `weekday()` 함수와 마찬가지로 데이터타입이 'datetime'인 데이터에 대해서만 사용할 수 있기 때문에, 아직 데이터타입을 문자열('str')로 변경해주기 전의 상태인 `standard` 변수를 대상으로 사용해주면 된다. 아래의 코드를 확인해보도록 하자.
※ Line: 11 ~ 13, 19(두 번째 경우의 수 처리)
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
print(f"어제 날짜를 구해봅시다.")
print(standard)
print(standard - datetime.timedelta(days=1))
if standard.weekday() <= 4: ## 평일
if now_time <= "163000": ## 어제 날짜 반환
pass
else: ## 오늘 날짜 반환
return now_date
else: ## 주말(가장 최근의 평일 날짜 반환)
pass
if __name__ == "__main__":
_return_latestdate()
▶ 실행 결과 확인하기(세 번째 줄은 standard(현재 시점), 네 번째 줄은 하루를 뺀 어제 이 시간의 날짜를 출력)
[20240922] 150637
어제 날짜를 구해봅시다.
2024-09-22 15:06:37.725474
2024-09-21 15:06:37.725474
그렇다면 이제 어제 날짜를 구하는 방법에 대해 알아보았으니 어제 날짜를 반환해야 하는 지점에서 어제 날짜를 계산하여 `yesterday`라는 변수에 저장해준 후에, 이 변수를 대상으로 다시 `strftime()` 함수를 활용하여 YYYYMMDD와 같은 형태로 데이터를 반환하도록 해주자.
※ Line: 13, 14
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
print(f"[{now_date}] {now_time}")
if standard.weekday() <= 4: ## 평일
if now_time <= "163000": ## 어제 날짜 반환
yesterday = standard - datetime.timedelta(days=1)
return yesterday.strftime("%Y%m%d")
else: ## 오늘 날짜 반환
return now_date
else: ## 주말(가장 최근의 평일 날짜 반환)
pass
if __name__ == "__main__":
_return_latestdate()
이제 마지막으로 구현해줄 부분은 바로 오늘이 주말인 경우이다. 이 부분도 비교적 간단하게 기능을 구현해줄 수 있다. 논리 구조를 설명하자면 ①오늘이 주말이라면 ②어제도 주말인지 ③그제도 주말인지 ④3일 전도 주말인지와 같이 하루씩 이전으로 돌아가면서 평일인 날을 발견하면 그 날의 날짜를 반환하도록 하면 된다. 이 경우에도 하루씩 이전으로 돌아가는 방법은 `timedelta`를 사용하면 되지만 그 날이 평일이 될 때까지는 계속해서 반복해서 하루씩 더 빼면서 확인해주어야 하므로 `for`문을 사용하면 된다. 아래의 코드를 보면서 한 줄씩 차근차근 코드를 구현해보도록 하자.
※ Line: 17 ~ 22, 25 ~ 26
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
now_date = standard.strftime("%Y%m%d")
now_time = standard.strftime("%H%M%S")
if standard.weekday() <= 4: ## 평일
if now_time <= "163000": ## 어제 날짜 반환
yesterday = standard - datetime.timedelta(days=1)
return yesterday.strftime("%Y%m%d")
else: ## 오늘 날짜 반환
return now_date
else: ## 주말(가장 최근의 평일 날짜 반환)
for i in range(1, 20): ## ① 반복해서 최대 20일까지 하루씩 빼라
isWeek = standard - datetime.timedelta(days=i) ## ② i일을 뺀 날
if isWeek.weekday() <= 4: ## ③ i일을 뺀 날(isWeek)이 평일이라면
return isWeek.strftime("%Y%m%d") ## ④ 그 날을 YYYYMMDD 형식으로 반환
else: ## ③ i일을 뺀 날이 평일이 아니라면
pass ## ④ 다시 for문으로 돌아가서 재계산
if __name__ == "__main__":
latest_date = _return_latestdate()
print(latest_date)
▶ 실행 결과 확인하기(2024년 9월 22일(일요일)을 기준으로 가장 최근의 평일은 2024년 9월 20일(금요일)이다.)
20240920
여기서 앞서 간단하게 정리하고 넘어갔던 '평일이고 오후 4시 30분 이전인 경우'에 대해 한 가지 부분을 더 고려해주어야 한다. 예를 들어 오늘이 화요일 새벽 2시라면 당연히 월요일의 날짜를 반환해주면 되겠지만, 오늘이 월요일 새벽 2시인 경우라면 이 코드는 일요일의 날짜를 반환하게 된다. 왜냐하면 단순하게 어제 날짜를 반환해줄 뿐, 어제 날짜가 주말인지 아닌지에 대해 판단하는 기능은 빠져있기 때문이다. 따라서 '평일 오후 4시 30분 이전인 경우'에 대해서도 반복문을 통해 어제 날짜가 주말인지를 판단해주는 기능을 추가해주어야 한다.
`for` 반복문의 뒤에 반복할 대상이 되는 `range(시작, 종료)` 부분은 `i`라는 값에 대해 `시작`부터 `종료`까지 1씩 더하면서 반복하라는 의미인데, 조회하는 당일에 대한 검증은 모두 종료되었으니 '시작' 데이터는 당연히 1을 넣어줘야 한다.
※ Line: 10 ~ 13
## etc.py ##
import datetime
def _return_latestdate():
"""현 시점 기준 가장 최신의 거래일 데이터를 반환"""
standard = datetime.datetime.now()
if standard.weekday() <= 4: ## 평일
now_time = standard.strftime("%H%M%S")
if now_time <= "163000":
for i in range(1, 10):
yesterday = standard - datetime.timedelta(days=i)
if yesterday.weekday() <= 4:
return yesterday.strftime("%Y%m%d") ## 어제 날짜 반환
else:
return standard.strftime("%Y%m%d") ## 오늘 날짜 반환
else: ## 주말
for i in range(1, 20):
isWeek = standard - datetime.timedelta(days=i)
if isWeek.weekday() <= 4:
return isWeek.strftime("%Y%m%d") ## 최근 평일 반환
if __name__ == "__main__":
pass
이제 Boss.py 파일로 돌아와서 etc.py 파일을 불러온 후 우리가 만들었던 함수를 사용하여 최신 거래일 데이터를 얻어올 수 있다.
※ Line: 7, 35, 36
## Boss.py ##
import pandas as pd
import win32com.client
from pywinauto import application
import con_mysql
import etc
from COM import CpSysDib
from COM import CpUtil
from COM import CpTrade
from COM import DsCbo1
import time
import datetime
from threading import Thread
## GUI ##
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from gui import qt_gui
main_ui = uic.loadUiType("main.ui")[0]
class cybos(QMainWindow, main_ui):
def __init__(self):
super().__init__()
self.setupUi(self)
self._open_cybosplus() ## 자동 로그인 함수
self._load_account() ## 계좌 조회 함수
self._load_DB() ## 데이터베이스 조회 함수
self.saved_point = con_mysql.manage_db()._read_table('saved_point', 'saved_point')
print(self.saved_point)
# self._load_savepoint() ## saved_point 테이블 생성 함수
now_date = datetime.datetime.now().strftime(f"%Y%m%d")
print(now_date)
self.the_latest_date = etc._return_latestdate()
print(f"현시점 최신 거래일:{self.the_latest_date}")
## GUI 관련 이벤트 처리 ##
self.pushButton.clicked.connect(self._GetStockListByMarket)
self.pushButton_2.clicked.connect(self._day_range)
self.pushButton_3.clicked.connect(self._len_chart)
self.comboBox_2.currentIndexChanged.connect(lambda: qt_gui.etc(self).comboBox_2_CIC())
self.pushButton_4.clicked.connect(self._save_all_chart)
▶ 실행 결과 확인하기(불필요한 출력 부분은 제외했습니다.)
20240922
현시점 최신 거래일:20240920
20240923
현시점 최신 거래일:20240920
여기까지가 우리가 `self.saved_point` 테이블에 입력할 날짜 데이터를 불러오는 코드였다. 이제 차트 데이터를 조회한 후에 이 데이터를 저장하도록 하면 된다.
문제점 보완하기 ④: 저장된 날짜를 기준으로 차트 데이터를 조회하고 새 날짜 저장하기
차트 데이터를 조회할 때, 사전에 저장되어 있는 데이터와 차트 데이터를 조회하는 시점을 기준으로 입력되어 있어야 하는 데이터(`self.the_latest_date`) 간에 대소 비교를 통해 차트 데이터를 조회해야 하는 종목인지 아닌지를 구분해서 조회하지 않아도 되는 종목(이미 저장된 날짜가 최신 거래일 날짜인 경우)이라면 pass 구문을 통해 별도로 작업을 처리하지 않아도 되게끔 구현하면 된다. 다른 부분들은 차치하고, 5분봉 차트 데이터에 대한 정보 먼저 구현해보도록 하자.
현재 기존에 차트 데이터를 조회한 날짜 정보가 담겨 있는 변수(`self.saved_point`) 내에서 특정 종목의 5분봉 차트 데이터 조회 일자를 조회하는 기능이 구현되어 있는데, 이 정보는 `sp_min5` 변수(sp는 saved_point의 약자)에 담겨져 있다. 그렇다면 이 변수와 함께 `self.the_latest_date` 변수를 비교해서, 최신 거래일 날짜 데이터가 저장된 날짜보다 크다면(차트 데이터를 현행화해야 한다면) 차트 데이터를 조회하도록 해보자.
※ Line: 28 ~ 32
def _save_all_chart(self):
"""전종목 차트 데이터 조회 후 저장하는 함수"""
all_code = self._GetStockListByMarket()
data_len = 1000000 ## 차트 데이터를 조회할 개수
for item_code in all_code:
print(f"조회 중인 종목코드:{item_code}")
print(f" 이 종목코드가 self.saved_point 변수(DataFrame)에서 갖는 인덱스 번호를 추출합니다.")
idx_num = self.saved_point.index[self.saved_point['item_code'] == item_code]
print(f" 인덱스 번호는 {idx_num} 입니다.")
sp_min3 = self.saved_point.loc[idx_num, 'min3_data'].item()
sp_min5 = self.saved_point.loc[idx_num, 'min5_data'].item()
sp_min15 = self.saved_point.loc[idx_num, 'min15_data'].item()
sp_min30 = self.saved_point.loc[idx_num, 'min30_data'].item()
sp_day = self.saved_point.loc[idx_num, 'day_data'].item()
sp_week = self.saved_point.loc[idx_num, 'week_data'].item()
sp_month = self.saved_point.loc[idx_num, 'month_data'].item()
print(f"저장되어 있는 3분봉 차트 데이터 조회일자:{sp_min3}")
print(f"저장되어 있는 5분봉 차트 데이터 조회일자:{sp_min5}")
print(f"저장되어 있는 15분봉 차트 데이터 조회일자:{sp_min15}")
print(f"저장되어 있는 30분봉 차트 데이터 조회일자:{sp_min30}")
print(f"저장되어 있는 일봉 차트 데이터 조회일자:{sp_day}")
print(f"저장되어 있는 주봉 차트 데이터 조회일자:{sp_week}")
print(f"저장되어 있는 월봉 차트 데이터 조회일자:{sp_month}")
print(f"5분봉 차트를 조회합니다.")
print(f"저장된 날짜:{sp_min5}, 최신 거래일:{self.the_latest_date}")
print(f"{sp_min5 < self.the_latest_date}")
if sp_min5 < self.the_latest_date:
CpSysDib.StockChart().chart_MT(item_code, "m", data_len, 5, save_gubun=True) ## 5분봉 조회
▶ 실행 결과 확인하기
5분봉 차트를 조회합니다.
저장된 날짜:0, 최신 거래일:20240920
True
이제 저장된 날짜와 최신 거래일 날짜 간의 대소 비교를 통해 차트 데이터 조회의 여부를 결정했으니 기존에 제작했던 코드와 마찬가지로 차트 데이터를 조회하고 저장하면 된다. 하지만 아직 '저장된 날짜'를 현행화하는 코드가 빠져있다. 즉, 저장된 날짜에 오늘 날짜를 입력해주지 않으면 저장된 날짜는 0으로만 남아있기 때문에 차트 데이터를 평생 다시 조회하게 될 것이다. 그렇다면 오늘 날짜를 입력해주는 방법에는 무엇이 있을까 ? 바로 기존 데이터가 담겨 있는 `self.saved_point` 변수에 오늘 날짜를 입력해준 후, 그 테이블을 다시 MySQL에 저장하도록 하면 된다.
※ Line: 28 ~ 36
def _save_all_chart(self):
"""전종목 차트 데이터 조회 후 저장하는 함수"""
all_code = self._GetStockListByMarket()
data_len = 1000000 ## 차트 데이터를 조회할 개수
for item_code in all_code:
print(f"조회 중인 종목코드:{item_code}")
print(f" 이 종목코드가 self.saved_point 변수(DataFrame)에서 갖는 인덱스 번호를 추출합니다.")
idx_num = self.saved_point.index[self.saved_point['item_code'] == item_code]
print(f" 인덱스 번호는 {idx_num} 입니다.")
sp_min3 = self.saved_point.loc[idx_num, 'min3_data'].item()
sp_min5 = self.saved_point.loc[idx_num, 'min5_data'].item()
sp_min15 = self.saved_point.loc[idx_num, 'min15_data'].item()
sp_min30 = self.saved_point.loc[idx_num, 'min30_data'].item()
sp_day = self.saved_point.loc[idx_num, 'day_data'].item()
sp_week = self.saved_point.loc[idx_num, 'week_data'].item()
sp_month = self.saved_point.loc[idx_num, 'month_data'].item()
print(f"저장되어 있는 3분봉 차트 데이터 조회일자:{sp_min3}")
print(f"저장되어 있는 5분봉 차트 데이터 조회일자:{sp_min5}")
print(f"저장되어 있는 15분봉 차트 데이터 조회일자:{sp_min15}")
print(f"저장되어 있는 30분봉 차트 데이터 조회일자:{sp_min30}")
print(f"저장되어 있는 일봉 차트 데이터 조회일자:{sp_day}")
print(f"저장되어 있는 주봉 차트 데이터 조회일자:{sp_week}")
print(f"저장되어 있는 월봉 차트 데이터 조회일자:{sp_month}")
print(f"5분봉 차트를 조회합니다.")
if sp_min5 < self.the_latest_date:
CpSysDib.StockChart().chart_MT(item_code, "m", data_len, 5, save_gubun=True) ## 5분봉 조회
print(f"5분봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'min5_data'] = self.the_latest_date
print(self.saved_point)
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point)
else:
print(f"이미 최신 데이터입니다.")
▶ 실행 결과 확인하기('saved_point' 테이블이 이미 존재한다는 오류가 발생할 것이다.)
저장되어 있는 3분봉 차트 데이터 조회일자:0
저장되어 있는 5분봉 차트 데이터 조회일자:0
저장되어 있는 15분봉 차트 데이터 조회일자:0
저장되어 있는 30분봉 차트 데이터 조회일자:0
저장되어 있는 일봉 차트 데이터 조회일자:0
저장되어 있는 주봉 차트 데이터 조회일자:0
저장되어 있는 월봉 차트 데이터 조회일자:0
5분봉 차트를 조회합니다.
item_code min3_data min5_data ... day_data week_data month_data
0 A000020 0 20240920 ... 0 0 0
1 A000040 0 0 ... 0 0 0
2 A000050 0 0 ... 0 0 0
3 A000070 0 0 ... 0 0 0
4 A000075 0 0 ... 0 0 0
... ... ... ... ... ... ... ...
3993 A950160 0 0 ... 0 0 0
3994 A950170 0 0 ... 0 0 0
3995 A950190 0 0 ... 0 0 0
3996 A950200 0 0 ... 0 0 0
3997 A950220 0 0 ... 0 0 0
오류가 발생했습니다. 데이터를 저장하지 않습니다. 오류 메시지:Table 'saved_point' already exists.
MySQL은 왜 이미 존재한다는 이유로 우리의 저장을 방해하는 것일까 ? 프로그램은 알잘딱깔센이 불가능하다. 오히려 이렇게 저장되지 않게끔 만들어져 있는 게 한편으로는 오히려 잘 된 일이다. 그렇다면 우리는 변경된 데이터를 어떻게 저장할 수 있을까 ? 바로 `to_sql` 메서드의 옵션으로 제공되는 `if_exists=`에 특정 값을 전달해주면 된다. 하지만 우리는 이미 테이블을 저장할 기능을 담당할 함수를 con_mysql.py 파일에 제작해두었는데, 이 부분을 조금 수정해주면 된다. 일단 `def _save_table()` 함수의 인자 부분에서 `if_exists=`에 들어갈 값을 전달받도록 조금 수정해보도록 하자.
함수의 인자 부분을 보면 맨 마지막에 `append_option=''`와 같이 기재되어 있는데, 이는 다른 인자들과는 다소 다르게 정의되어 있음을 알 수 있다. 이러한 방식으로 인자를 정의하는 이유는 간단하다. 일단 이와 같이 인자를 정의하는 것은 이 함수를 호출할 때 `append_option`으로 전달받은 인자가 없는 경우에는 이 인자의 값으로 ''를 적용하라는 것과 동일한 개념인데, 이렇게 처리하는 이유는 우리가 기존에 제작한 코드들이 이미 이 옵션을 고려하지 않아도 되게끔 만들어져 있기 때문이다.
※ Line: 6
## con_mysql.py ##
class manage_db:
def __init__(self):
pass
def _save_table(self, db_name, table_name, table_data, append_option=''):
"""
table_data(데이터프레임, 차트 데이터 등)을 데이터베이스(db_name)에
table_name(보통 종목코드가 됨)라는 이름으로 하여 저장"""
if type(table_data) != pd.DataFrame: ## 데이터타입이 데이터프레임이 아닌 경우
print(f"전달받은 데이터의 데이터타입이 데이터프레임이 아닙니다.")
return False ## False를 반환하고 종료
with sqlalCon(f'{db_name}') as con:
try:
table_data.to_sql(name=table_name, con=con.engine, index=False)
print(f"데이터가 저장되었습니다. (테이블명:{table_name})")
이 부분이 이해가 갔다면 Line 15에 있는 `try` 구문의 아래 부분을 수정해주면 되는데, 일단 기본적으로 `append_option` 변수의 값이 공란인 경우와 그렇지 않은 경우로 하여 두 개의 조건문을 만들고 변수의 값이 공란이 아닌 경우에는 `to_sql` 함수의 `if_exists=` 옵션 값으로 `append_option`을 전달해주도록 하자. 이 옵션은 아래의 세 가지 값을 전달할 수 있다.
※ Line: 15 ~ 20
- if_exists="append" : 기존에 있는 데이터 + 새로 저장할 데이터
- if_exists="replace" : 기존에 있는 데이터 삭제 후 새로 저장할 데이터 저장
- if_exists="fail" : 작업 미처리(기존에 있는 데이터 남김)
class manage_db:
def __init__(self):
pass
def _save_table(self, db_name, table_name, table_data, append_option=''):
"""
table_data(데이터프레임, 차트 데이터 등)을 데이터베이스(db_name)에
table_name(보통 종목코드가 됨)라는 이름으로 하여 저장"""
if type(table_data) != pd.DataFrame: ## 데이터타입이 데이터프레임이 아닌 경우
print(f"전달받은 데이터의 데이터타입이 데이터프레임이 아닙니다.")
return False ## False를 반환하고 종료
with sqlalCon(f'{db_name}') as con:
try:
if append_option == "":
table_data.to_sql(name=table_name, con=con.engine, index=False)
print(f"데이터가 저장되었습니다. (테이블명:{table_name})")
else:
table_data.to_sql(name=table_name, con=con.engine, index=False, if_exists=append_option)
print(f"데이터가 저장되었습니다. (테이블명:{table_name})")
except Exception as e:
con.session.rollback()
print(f"오류가 발생했습니다. 데이터를 저장하지 않습니다. 오류 메시지:{e}")
finally:
con.session.commit()
print(f"데이터를 저장하였습니다.")
print(f"MySQL 연결을 종료합니다.")
con.engine.dispose() ## MySQL 프로그램 연결 종료
print(f"MySQL과의 연결이 종료되었습니다.")
그 후에 다시 Boss.py 파일에 있는 `def _save_all_chart()` 함수로 돌아와서 데이터를 저장하는 부분을 아래와 같이 수정하고 차트 데이터를 다시 저장해보도록 하자.
※ Line: 34
def _save_all_chart(self):
"""전종목 차트 데이터 조회 후 저장하는 함수"""
all_code = self._GetStockListByMarket()
data_len = 1000000 ## 차트 데이터를 조회할 개수
for item_code in all_code:
print(f"조회 중인 종목코드:{item_code}")
print(f" 이 종목코드가 self.saved_point 변수(DataFrame)에서 갖는 인덱스 번호를 추출합니다.")
idx_num = self.saved_point.index[self.saved_point['item_code'] == item_code]
print(f" 인덱스 번호는 {idx_num} 입니다.")
sp_min3 = self.saved_point.loc[idx_num, 'min3_data'].item()
sp_min5 = self.saved_point.loc[idx_num, 'min5_data'].item()
sp_min15 = self.saved_point.loc[idx_num, 'min15_data'].item()
sp_min30 = self.saved_point.loc[idx_num, 'min30_data'].item()
sp_day = self.saved_point.loc[idx_num, 'day_data'].item()
sp_week = self.saved_point.loc[idx_num, 'week_data'].item()
sp_month = self.saved_point.loc[idx_num, 'month_data'].item()
print(f"저장되어 있는 3분봉 차트 데이터 조회일자:{sp_min3}")
print(f"저장되어 있는 5분봉 차트 데이터 조회일자:{sp_min5}")
print(f"저장되어 있는 15분봉 차트 데이터 조회일자:{sp_min15}")
print(f"저장되어 있는 30분봉 차트 데이터 조회일자:{sp_min30}")
print(f"저장되어 있는 일봉 차트 데이터 조회일자:{sp_day}")
print(f"저장되어 있는 주봉 차트 데이터 조회일자:{sp_week}")
print(f"저장되어 있는 월봉 차트 데이터 조회일자:{sp_month}")
print(f"5분봉 차트를 조회합니다.")
if sp_min5 < self.the_latest_date:
CpSysDib.StockChart().chart_MT(item_code, "m", data_len, 5, save_gubun=True) ## 5분봉 조회
print(f"5분봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'min5_data'] = self.the_latest_date
print(self.saved_point)
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point, append_option="replace")
else:
print(f"이미 최신 데이터입니다.")
▶ 실행 결과 확인하기(분봉 차트 데이터 저장 기능은 여전히 오류가 발생)
오류가 발생했습니다. 데이터를 저장하지 않습니다. 오류 메시지:Table 'a000020' already exists.
5분봉 차트를 저장했습니다.
item_code min3_data min5_data ... day_data week_data month_data
0 A000020 0 20240920 ... 0 0 0
1 A000040 0 0 ... 0 0 0
2 A000050 0 0 ... 0 0 0
3 A000070 0 0 ... 0 0 0
4 A000075 0 0 ... 0 0 0
... ... ... ... ... ... ... ...
3993 A950160 0 0 ... 0 0 0
3994 A950170 0 0 ... 0 0 0
3995 A950190 0 0 ... 0 0 0
3996 A950200 0 0 ... 0 0 0
3997 A950220 0 0 ... 0 0 0
데이터가 저장되었습니다. (테이블명:saved_point)
데이터를 저장하였습니다.
▶ 실행 결과 확인하기(프로그램을 다시 실행시켜보면, 이미 최신 데이터라고 차트 데이터를 조회하지 않음)
조회 중인 종목코드:A000020
이 종목코드가 self.saved_point 변수(DataFrame)에서 갖는 인덱스 번호를 추출합니다.
인덱스 번호는 Int64Index([0], dtype='int64') 입니다.
저장되어 있는 3분봉 차트 데이터 조회일자:0
저장되어 있는 5분봉 차트 데이터 조회일자:20240920
저장되어 있는 15분봉 차트 데이터 조회일자:0
저장되어 있는 30분봉 차트 데이터 조회일자:0
저장되어 있는 일봉 차트 데이터 조회일자:0
저장되어 있는 주봉 차트 데이터 조회일자:0
저장되어 있는 월봉 차트 데이터 조회일자:0
5분봉 차트를 조회합니다.
이미 최신 데이터입니다.
15분봉 차트를 조회합니다.
하지만 앞서 살펴봤듯이, 분봉 차트 데이터를 조회하는 기능에 있어서도 기존에 차트 데이터가 존재한다는 것을 이유로 차트 데이터를 조회했음에도 불구하고 저장하지 않는 오류가 있었는데, 이에 대해서도 `if_exists='replace'`라는 옵션을 전달해주면 된다.
## CpSysDib.py ##
## def chart_DWM() 함수의 하단 부분 ##
if save_gubun: ## 이는 if save_gubun == True: 구문을 요약한 것이다.
if request == "D": ## 일봉 차트 저장
con_mysql.manage_db()._save_table(db_name='day_data', table_name=f'{item_code.lower()}', table_data=chart_data, append_option='replace')
elif request == "W": ## 주봉 차트 저장
con_mysql.manage_db()._save_table(db_name='week_data', table_name=f'{item_code.lower()}', table_data=chart_data, append_option='replace')
elif request == "M": ## 월봉 차트 저장
con_mysql.manage_db()._save_table(db_name='month_data', table_name=f'{item_code.lower()}', table_data=chart_data, append_option='replace')
return chart_data
## CpSysDib.py ##
## def chart_MT() 함수의 하단 부분 ##
if save_gubun: ## 이는 if save_gubun == True: 구문을 요약한 것이다.
if request == "m": ## 분봉 차트 저장
con_mysql.manage_db()._save_table(db_name=f'min{cycle}_data', table_name=f'{item_code.lower()}', table_data=chart_data, append_option='replace')
elif request == "T": ## 틱봉 차트 저장
con_mysql.manage_db()._save_table(db_name=f'tick{cycle}_data', table_name=f'{item_code.lower()}', table_data=chart_data, append_option='replace')
return chart_data
그 후 다시 Boss.py 파일에서 차트 데이터를 조회하는 부분 중 다른 유형의 차트 데이터들에 대해서도 5분봉 차트 데이터를 조회하는 구조와 동일하게 바꾸어주면 된다.
※ Line: 28 ~ 71
def _save_all_chart(self):
"""전종목 차트 데이터 조회 후 저장하는 함수"""
all_code = self._GetStockListByMarket()
data_len = 1000000 ## 차트 데이터를 조회할 개수
for item_code in all_code:
print(f"조회 중인 종목코드:{item_code}")
print(f" 이 종목코드가 self.saved_point 변수(DataFrame)에서 갖는 인덱스 번호를 추출합니다.")
idx_num = self.saved_point.index[self.saved_point['item_code'] == item_code]
print(f" 인덱스 번호는 {idx_num} 입니다.")
sp_min3 = self.saved_point.loc[idx_num, 'min3_data'].item()
sp_min5 = self.saved_point.loc[idx_num, 'min5_data'].item()
sp_min15 = self.saved_point.loc[idx_num, 'min15_data'].item()
sp_min30 = self.saved_point.loc[idx_num, 'min30_data'].item()
sp_day = self.saved_point.loc[idx_num, 'day_data'].item()
sp_week = self.saved_point.loc[idx_num, 'week_data'].item()
sp_month = self.saved_point.loc[idx_num, 'month_data'].item()
print(f"저장되어 있는 3분봉 차트 데이터 조회일자:{sp_min3}")
print(f"저장되어 있는 5분봉 차트 데이터 조회일자:{sp_min5}")
print(f"저장되어 있는 15분봉 차트 데이터 조회일자:{sp_min15}")
print(f"저장되어 있는 30분봉 차트 데이터 조회일자:{sp_min30}")
print(f"저장되어 있는 일봉 차트 데이터 조회일자:{sp_day}")
print(f"저장되어 있는 주봉 차트 데이터 조회일자:{sp_week}")
print(f"저장되어 있는 월봉 차트 데이터 조회일자:{sp_month}")
print(f"5분봉 차트를 조회합니다.")
if sp_min5 < self.the_latest_date:
CpSysDib.StockChart().chart_MT(item_code, "m", data_len, 5, save_gubun=True) ## 5분봉 조회
print(f"5분봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'min5_data'] = self.the_latest_date
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point, append_option="replace")
else:
print(f"이미 최신 데이터입니다.")
print(f"15분봉 차트를 조회합니다.")
if sp_min15 < self.the_latest_date:
CpSysDib.StockChart().chart_MT(item_code, "m", data_len, 15, save_gubun=True) ## 15분봉 조회
print(f"15분봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'min15_data'] = self.the_latest_date
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point, append_option="replace")
else:
print(f"이미 최신 데이터입니다.")
print(f"일봉 차트를 조회합니다.")
if sp_day < self.the_latest_date:
CpSysDib.StockChart().chart_DWM(item_code, "D", data_len, save_gubun=True) ## 일봉 조회
print(f"일봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'day_data'] = self.the_latest_date
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point, append_option="replace")
else:
print(f"이미 최신 데이터입니다.")
print(f"주봉 차트를 조회합니다.")
if sp_week < self.the_latest_date:
CpSysDib.StockChart().chart_DWM(item_code, "W", data_len, save_gubun=True) ## 주봉 조회
print(f"주봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'week_data'] = self.the_latest_date
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point, append_option="replace")
else:
print(f"이미 최신 데이터입니다.")
print(f"월봉 차트를 조회합니다.")
if sp_month < self.the_latest_date:
CpSysDib.StockChart().chart_DWM(item_code, "M", data_len, save_gubun=True) ## 월봉 조회
print(f"월봉 차트를 저장했습니다.")
self.saved_point.loc[idx_num, 'month_data'] = self.the_latest_date
con_mysql.manage_db()._save_table(db_name='saved_point', table_name='saved_point', table_data=self.saved_point, append_option="replace")
else:
print(f"이미 최신 데이터입니다.")
▶ 실행 결과 확인하기(MySQL에 정상적으로 저장되고 있는 모습을 확인할 수 있음)
여기까지 해서 전종목 차트 데이터를 계속해서 조회하는 방법에 대한 코드를 마무리할 수 있다. 하지만 분봉 차트 데이터와 관련해서는 `if_exists=` 옵션으로 'replace'(기존 데이터 삭제 후 신규 데이터 저장)가 아니라 기존 데이터에 새롭게 조회한 데이터만을 추가하여 저장하고 싶을 수도 있는데, 이 부분은 번외로 하여 작성해보고자 한다.
'AUTO TRADE > [대신증권] CYBOS PLUS' 카테고리의 다른 글
대신증권 CYBOS PLUS 프로그램 구현 (22) - 실시간 데이터 수신받기 ② (1) | 2024.09.30 |
---|---|
대신증권 CYBOS PLUS 프로그램 구현 (21) - 실시간 데이터 수신받기 ① (7) | 2024.09.27 |
대신증권 CYBOS PLUS 프로그램 구현 (19) - 전종목 차트 데이터 저장하기 ② (0) | 2024.09.21 |
대신증권 CYBOS PLUS 프로그램 구현 (18) - 전종목 차트 데이터 저장하기 ① (7) | 2024.09.18 |
대신증권 CYBOS PLUS 프로그램 구현 (17) - 데이터베이스 관리 자동화 (0) | 2024.09.18 |
소중한 공감 감사합니다