apsniper0673-dataprovider 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsniper0673_dataprovider-0.1.1/LICENSE +21 -0
- apsniper0673_dataprovider-0.1.1/PKG-INFO +8 -0
- apsniper0673_dataprovider-0.1.1/README.md +14 -0
- apsniper0673_dataprovider-0.1.1/pyproject.toml +19 -0
- apsniper0673_dataprovider-0.1.1/setup.cfg +4 -0
- apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/PKG-INFO +8 -0
- apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/SOURCES.txt +64 -0
- apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/dependency_links.txt +1 -0
- apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/top_level.txt +1 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/api.py +3 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/feather_manager.py +127 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/odi/df_odi.py +368 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/odi/odi_functions.py +46 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/convert_to_ppd_df.py +124 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/df_ppd.py +317 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/ppdps/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/ppdps/ppdp_baisc.py +128 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/ppdps/ppdp_custom.py +293 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/df_raw.py +141 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/df_raw_krx.py +128 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/df_raw_nxt.py +143 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/merge_raw.py +77 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/stock_data_handler_for_raw.py +16 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/stock_dataframe.py +169 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/flask/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/flask/rdf_client.py +118 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/flask/rdf_server.py +208 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/rdf/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/rdf/df_rdf.py +210 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/realtime_dataframe_provider.py +67 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/stock_data_provider.py +387 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/krx/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/krx/krx_data_provider.py +68 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/naver_data_client.py +147 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/naver_data_fetcher.py +357 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/naver_data_server.py +168 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/naver_data_provider.py +116 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/web_data_provider.py +101 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/simple.py +2 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/db_with_sql.py +340 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/stock_table_manager.py +176 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/tm_odi.py +60 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/tm_raw_krx.py +83 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/tm_raw_nxt.py +102 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/test_api.py +3 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/__init__.py +0 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/krx.py +184 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/naver.py +380 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/nxt.py +98 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/db/sql.py +36 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/get_available_server.py +32 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/handler_functions.py +23 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/singleday_data_handler.py +141 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/stock_data_handler.py +432 -0
- apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/update_upsert_df.py +86 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 HyuntaMansei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "apsniper0673_dataprovider"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "My stock data provider for APSniper0673"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "apsniper0673" }
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
dependencies = [
|
|
10
|
+
# "pandas",
|
|
11
|
+
# "numpy",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["setuptools>=64", "wheel"]
|
|
16
|
+
build-backend = "setuptools.build_meta"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/apsniper0673_dataprovider.egg-info/PKG-INFO
|
|
5
|
+
src/apsniper0673_dataprovider.egg-info/SOURCES.txt
|
|
6
|
+
src/apsniper0673_dataprovider.egg-info/dependency_links.txt
|
|
7
|
+
src/apsniper0673_dataprovider.egg-info/top_level.txt
|
|
8
|
+
src/mydataprovider/__init__.py
|
|
9
|
+
src/mydataprovider/api.py
|
|
10
|
+
src/mydataprovider/simple.py
|
|
11
|
+
src/mydataprovider/test_api.py
|
|
12
|
+
src/mydataprovider/frame/__init__.py
|
|
13
|
+
src/mydataprovider/frame/feather_manager.py
|
|
14
|
+
src/mydataprovider/frame/stock_dataframe.py
|
|
15
|
+
src/mydataprovider/frame/odi/df_odi.py
|
|
16
|
+
src/mydataprovider/frame/odi/odi_functions.py
|
|
17
|
+
src/mydataprovider/frame/ppd/__init__.py
|
|
18
|
+
src/mydataprovider/frame/ppd/convert_to_ppd_df.py
|
|
19
|
+
src/mydataprovider/frame/ppd/df_ppd.py
|
|
20
|
+
src/mydataprovider/frame/ppd/ppdps/__init__.py
|
|
21
|
+
src/mydataprovider/frame/ppd/ppdps/ppdp_baisc.py
|
|
22
|
+
src/mydataprovider/frame/ppd/ppdps/ppdp_custom.py
|
|
23
|
+
src/mydataprovider/frame/raw/__init__.py
|
|
24
|
+
src/mydataprovider/frame/raw/df_raw.py
|
|
25
|
+
src/mydataprovider/frame/raw/df_raw_krx.py
|
|
26
|
+
src/mydataprovider/frame/raw/df_raw_nxt.py
|
|
27
|
+
src/mydataprovider/frame/raw/merge_raw.py
|
|
28
|
+
src/mydataprovider/frame/raw/stock_data_handler_for_raw.py
|
|
29
|
+
src/mydataprovider/provider/__init__.py
|
|
30
|
+
src/mydataprovider/provider/stock_data_provider.py
|
|
31
|
+
src/mydataprovider/provider/rdp/__init__.py
|
|
32
|
+
src/mydataprovider/provider/rdp/realtime_dataframe_provider.py
|
|
33
|
+
src/mydataprovider/provider/rdp/flask/__init__.py
|
|
34
|
+
src/mydataprovider/provider/rdp/flask/rdf_client.py
|
|
35
|
+
src/mydataprovider/provider/rdp/flask/rdf_server.py
|
|
36
|
+
src/mydataprovider/provider/rdp/rdf/__init__.py
|
|
37
|
+
src/mydataprovider/provider/rdp/rdf/df_rdf.py
|
|
38
|
+
src/mydataprovider/provider/web/__init__.py
|
|
39
|
+
src/mydataprovider/provider/web/web_data_provider.py
|
|
40
|
+
src/mydataprovider/provider/web/krx/__init__.py
|
|
41
|
+
src/mydataprovider/provider/web/krx/krx_data_provider.py
|
|
42
|
+
src/mydataprovider/provider/web/naver/__init__.py
|
|
43
|
+
src/mydataprovider/provider/web/naver/naver_data_provider.py
|
|
44
|
+
src/mydataprovider/provider/web/naver/flask/__init__.py
|
|
45
|
+
src/mydataprovider/provider/web/naver/flask/naver_data_client.py
|
|
46
|
+
src/mydataprovider/provider/web/naver/flask/naver_data_fetcher.py
|
|
47
|
+
src/mydataprovider/provider/web/naver/flask/naver_data_server.py
|
|
48
|
+
src/mydataprovider/table/__init__.py
|
|
49
|
+
src/mydataprovider/table/db_with_sql.py
|
|
50
|
+
src/mydataprovider/table/stock_table_manager.py
|
|
51
|
+
src/mydataprovider/table/tm_odi.py
|
|
52
|
+
src/mydataprovider/table/tm_raw_krx.py
|
|
53
|
+
src/mydataprovider/table/tm_raw_nxt.py
|
|
54
|
+
src/mydataprovider/utility/__init__.py
|
|
55
|
+
src/mydataprovider/utility/get_available_server.py
|
|
56
|
+
src/mydataprovider/utility/crawler/__init__.py
|
|
57
|
+
src/mydataprovider/utility/crawler/krx.py
|
|
58
|
+
src/mydataprovider/utility/crawler/naver.py
|
|
59
|
+
src/mydataprovider/utility/crawler/nxt.py
|
|
60
|
+
src/mydataprovider/utility/db/sql.py
|
|
61
|
+
src/mydataprovider/z_handler/handler_functions.py
|
|
62
|
+
src/mydataprovider/z_handler/singleday_data_handler.py
|
|
63
|
+
src/mydataprovider/z_handler/stock_data_handler.py
|
|
64
|
+
src/mydataprovider/z_handler/update_upsert_df.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mydataprovider
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from mydataprovider.frame.odi.df_odi import odi
|
|
7
|
+
from mystockutil.etc import get_project_root
|
|
8
|
+
|
|
9
|
+
from mystockutil.logging.logging_setup import CustomAdapter, logger as original_logger
|
|
10
|
+
logger = CustomAdapter(original_logger, {'prefix': 'Feather'})
|
|
11
|
+
|
|
12
|
+
class _FeatherManager():
|
|
13
|
+
def __init__(self, base_feather_name:str) -> None:
|
|
14
|
+
self.feather_name = base_feather_name
|
|
15
|
+
self._set_feather_dir()
|
|
16
|
+
|
|
17
|
+
def _set_feather_dir(self) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Feather 파일을 저장할 디렉토리를 설정합니다.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
self.feather_dir = get_project_root().joinpath('feather')
|
|
23
|
+
except:
|
|
24
|
+
try:
|
|
25
|
+
self.feather_dir = get_project_root(marker='feather').joinpath('feather')
|
|
26
|
+
except:
|
|
27
|
+
raise RuntimeError("Cannot find feather directory. Please check your project structure.")
|
|
28
|
+
|
|
29
|
+
def _get_feather_dir(self) -> Path:
|
|
30
|
+
return self.feather_dir
|
|
31
|
+
def _generate_feather_path(self, date:pd.Timestamp=None) -> str:
|
|
32
|
+
if date is None:
|
|
33
|
+
# 디폴트 값으로 오늘 날짜를 사용
|
|
34
|
+
date = pd.Timestamp.today().normalize()
|
|
35
|
+
return os.path.join(self.feather_dir, date.strftime('%Y%m%d') + '_' + self.feather_name + '.feather')
|
|
36
|
+
def _find_latest_feather_path(self) -> Path | None:
|
|
37
|
+
# 파일 이름 패턴 정의: YYYYMMDD_table_name.feather
|
|
38
|
+
pattern = re.compile(r'(\d{8})_' + re.escape(self.feather_name) + r'\.feather$')
|
|
39
|
+
latest_date = None
|
|
40
|
+
latest_file = None
|
|
41
|
+
|
|
42
|
+
# base_dir 내의 모든 파일을 순회
|
|
43
|
+
for file in self.feather_dir.iterdir():
|
|
44
|
+
if file.is_file():
|
|
45
|
+
match = pattern.match(file.name)
|
|
46
|
+
if match:
|
|
47
|
+
date_str = match.group(1)
|
|
48
|
+
try:
|
|
49
|
+
file_date = pd.to_datetime(date_str, format='%Y%m%d')
|
|
50
|
+
if (latest_date is None) or (file_date > latest_date):
|
|
51
|
+
latest_date = file_date
|
|
52
|
+
latest_file = file
|
|
53
|
+
except ValueError:
|
|
54
|
+
logger.warning(f"Filename '{file.name}' has invalid date format.")
|
|
55
|
+
continue
|
|
56
|
+
if latest_file is None:
|
|
57
|
+
logger.warning(f"No feather files found for table '{self.feather_name}' in '{self.feather_dir}'.")
|
|
58
|
+
return latest_file
|
|
59
|
+
|
|
60
|
+
def _load_latest_feather(self) -> pd.DataFrame:
|
|
61
|
+
"""
|
|
62
|
+
지정된 테이블 이름에 해당하는 최신 Feather 파일을 로드
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
table_name (str): 테이블 이름
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
pd.DataFrame: 로드된 데이터프레임
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
FileNotFoundError: 해당 테이블에 대한 Feather 파일이 존재하지 않을 경우
|
|
72
|
+
"""
|
|
73
|
+
latest_file = self._find_latest_feather_path()
|
|
74
|
+
|
|
75
|
+
if latest_file:
|
|
76
|
+
logger.info(f"Loading latest file: {latest_file}")
|
|
77
|
+
df = pd.read_feather(latest_file)
|
|
78
|
+
return df
|
|
79
|
+
else:
|
|
80
|
+
raise FileNotFoundError(f"No feather files found for table '{self.feather_name}' in '{self.feather_dir}'.")
|
|
81
|
+
|
|
82
|
+
class FeatherManager_del(_FeatherManager):
|
|
83
|
+
def delete_old_feathers(self, keep:int=3) -> None:
|
|
84
|
+
"""
|
|
85
|
+
feather 디렉토리 내에서 가장 최근 `keep`개 파일을 제외하고 모두 삭제합니다.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
keep (int): 보존할 최신 feather 파일 개수. 그 외는 삭제됨.
|
|
89
|
+
"""
|
|
90
|
+
pattern = re.compile(r'(\d{8})_' + re.escape(self.feather_name) + r'\.feather$')
|
|
91
|
+
files_with_dates = []
|
|
92
|
+
|
|
93
|
+
for file in self.feather_dir.iterdir():
|
|
94
|
+
if file.is_file():
|
|
95
|
+
match = pattern.match(file.name)
|
|
96
|
+
if match:
|
|
97
|
+
try:
|
|
98
|
+
date = pd.to_datetime(match.group(1), format='%Y%m%d')
|
|
99
|
+
files_with_dates.append((date, file))
|
|
100
|
+
except ValueError:
|
|
101
|
+
logger.warning(f"Filename '{file.name}' has invalid date format.")
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# 날짜 기준으로 정렬
|
|
105
|
+
files_with_dates.sort(reverse=True)
|
|
106
|
+
|
|
107
|
+
# 삭제할 파일들 추출
|
|
108
|
+
files_to_delete = files_with_dates[keep:]
|
|
109
|
+
|
|
110
|
+
for _, file in files_to_delete:
|
|
111
|
+
try:
|
|
112
|
+
file.unlink()
|
|
113
|
+
logger.info(f"Deleted old feather file: {file}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Failed to delete file {file}: {e}")
|
|
116
|
+
|
|
117
|
+
class FeatherManager(FeatherManager_del):
|
|
118
|
+
def load_from_feather(self) -> pd.DataFrame:
|
|
119
|
+
return self._load_latest_feather()
|
|
120
|
+
|
|
121
|
+
def save_to_feather(self, df:pd.DataFrame):
|
|
122
|
+
last_date = df.index.get_level_values('일자').max()
|
|
123
|
+
df.to_feather(self._generate_feather_path(date=last_date))
|
|
124
|
+
logger.info(f"Data saved to feather file: {self._generate_feather_path(date=last_date)}")
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
pass
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# 보편적 Library
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
# User Defined Library
|
|
6
|
+
from mystockutil.logging.logging_setup import CustomAdapter, logger as original_logger
|
|
7
|
+
logger = CustomAdapter(original_logger, {'prefix': 'ODI'})
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Opening Day Information (ODI) 관리 클래스
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Essential Imports
|
|
14
|
+
import exchange_calendars as ecal
|
|
15
|
+
|
|
16
|
+
# Essential User Defined Imports
|
|
17
|
+
from mydataprovider.table.tm_odi import TM_ODI
|
|
18
|
+
from mydataprovider.frame.odi.odi_functions import get_offset_value_with_target
|
|
19
|
+
|
|
20
|
+
class _ODI():
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
self.tm = TM_ODI() # ODI 테이블을 관리하는 클래스
|
|
25
|
+
|
|
26
|
+
# 개장일에 대한 정보를 저장하는 DataFrame
|
|
27
|
+
# index: date, columns: '개장여부', 'raw', 'ppd'
|
|
28
|
+
# index는 pd.Timestamp로 사용, 오름차순 정렬
|
|
29
|
+
self.df:pd.DataFrame = pd.DataFrame()
|
|
30
|
+
|
|
31
|
+
class ODI_data_ready(_ODI):
|
|
32
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
# 장시작 후 실시간 데이터 수집 가능 시간
|
|
36
|
+
self.krx_realtime_data_ready_time = datetime.time(hour=9, minute=20) # KRX 개장 후 20분 후
|
|
37
|
+
self.nxt_realtime_data_ready_time = datetime.time(hour=8, minute=20) # NXT 개장 후 20분 후
|
|
38
|
+
|
|
39
|
+
# KRX와 NXT의 기준시간 데이터
|
|
40
|
+
self.krx_final_data_ready_time = datetime.time(hour=18, minute=30) # KRX 폐장 후 30분 후
|
|
41
|
+
self.nxt_final_data_ready_time = datetime.time(hour=20, minute=10) # NXT 폐장 후 30분 후
|
|
42
|
+
|
|
43
|
+
def is_krx_realtime_data_ready(self) -> bool:
|
|
44
|
+
"""현재 시간이 KRX 실시간 데이터 수집 가능 시간인지 확인
|
|
45
|
+
휴장일인 경우에는 True를 반환한다."""
|
|
46
|
+
if not self.today_is_openday:
|
|
47
|
+
return True
|
|
48
|
+
else:
|
|
49
|
+
return pd.Timestamp.now().time() >= self.krx_realtime_data_ready_time
|
|
50
|
+
def is_nxt_realtime_data_ready(self) -> bool:
|
|
51
|
+
"""현재 시간이 NXT 실시간 데이터 수집 가능 시간인지 확인
|
|
52
|
+
휴장일인 경우에는 True를 반환한다."""
|
|
53
|
+
if not self.today_is_openday:
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
return pd.Timestamp.now().time() >= self.nxt_realtime_data_ready_time
|
|
57
|
+
def is_today_krx_final_data_ready(self) -> bool:
|
|
58
|
+
if not self.today_is_openday:
|
|
59
|
+
return True
|
|
60
|
+
else:
|
|
61
|
+
return pd.Timestamp.now().time() >= self.krx_final_data_ready_time
|
|
62
|
+
def is_today_nxt_final_data_ready(self) -> bool:
|
|
63
|
+
if not self.today_is_openday:
|
|
64
|
+
return True
|
|
65
|
+
else:
|
|
66
|
+
return pd.Timestamp.now().time() >= self.nxt_final_data_ready_time
|
|
67
|
+
@property
|
|
68
|
+
def today_is_openday(self)-> bool:
|
|
69
|
+
today:pd.Timestamp = pd.Timestamp.today().normalize()
|
|
70
|
+
if today in self.df.index:
|
|
71
|
+
# index에 있으면 그 값을 리턴
|
|
72
|
+
return self.df.loc[[today]]['개장여부'].iloc[0]
|
|
73
|
+
# return self.df.loc[today]['개장여부']
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError("Today's data is not in the DataFrame.")
|
|
76
|
+
@property
|
|
77
|
+
def today_is_holiday(self) -> bool:
|
|
78
|
+
return not self.today_is_openday
|
|
79
|
+
@property
|
|
80
|
+
def today(self) -> pd.Timestamp:
|
|
81
|
+
return pd.Timestamp.today().normalize()
|
|
82
|
+
@property
|
|
83
|
+
def yesterday(self) -> pd.Timestamp:
|
|
84
|
+
return pd.Timestamp.today().normalize() - pd.Timedelta(1, unit='D')
|
|
85
|
+
@property
|
|
86
|
+
def tomorrow(self) -> pd.Timestamp:
|
|
87
|
+
return pd.Timestamp.today().normalize() + pd.Timedelta(1, unit='D')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ODI_with_df(ODI_data_ready):
|
|
91
|
+
# 금일 주위의 데이터를 보여줌
|
|
92
|
+
def get_recent_days_df(self, past_days:int = 10, forward_days:int=10) -> pd.DataFrame:
|
|
93
|
+
"""금일을 기준으로 최근 days일의 ODI DataFrame을 반환한다."""
|
|
94
|
+
start_date = self.today - pd.Timedelta(past_days, unit='D')
|
|
95
|
+
end_date = self.today + pd.Timedelta(forward_days, unit='D')
|
|
96
|
+
return self.df[(self.df.index >= start_date) & (self.df.index <= end_date)]
|
|
97
|
+
@property
|
|
98
|
+
def df_recent(self) -> pd.DataFrame:
|
|
99
|
+
"""금일을 포함한 최근 ODI DataFrame을 반환한다."""
|
|
100
|
+
days = 5
|
|
101
|
+
return self.get_recent_days_df(days, days)
|
|
102
|
+
@property
|
|
103
|
+
def df_past_recent(self) -> pd.DataFrame:
|
|
104
|
+
"""금일을 포함한 최근 ODI DataFrame을 반환한다."""
|
|
105
|
+
days = 5
|
|
106
|
+
return self.get_recent_days_df(days, 0)
|
|
107
|
+
@property
|
|
108
|
+
def df_future_recent(self) -> pd.DataFrame:
|
|
109
|
+
"""금일을 포함한 최근 ODI DataFrame을 반환한다."""
|
|
110
|
+
days = 5
|
|
111
|
+
return self.get_recent_days_df(0, days)
|
|
112
|
+
@property
|
|
113
|
+
def df_on(self):
|
|
114
|
+
return self.df[self.df['개장여부']==True]
|
|
115
|
+
@property
|
|
116
|
+
def df_off(self):
|
|
117
|
+
return self.df[self.df['개장여부']==False]
|
|
118
|
+
@property
|
|
119
|
+
def days(self):
|
|
120
|
+
return self.df.index
|
|
121
|
+
@property
|
|
122
|
+
def ondays(self):
|
|
123
|
+
return self.df_on.index
|
|
124
|
+
@property
|
|
125
|
+
def offdays(self):
|
|
126
|
+
return self.df_off.index
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ODI_with_TM(ODI_with_df):
|
|
130
|
+
def fetch_whole(self):
|
|
131
|
+
self.df = self.tm.fetch_whole()
|
|
132
|
+
|
|
133
|
+
def add_onday(self, date: datetime.date):
|
|
134
|
+
"""특정일을 개장일로 추가한다."""
|
|
135
|
+
date = pd.to_datetime(date).normalize()
|
|
136
|
+
self._add_date(date, is_open=True)
|
|
137
|
+
|
|
138
|
+
def add_offday(self, date: datetime.date):
|
|
139
|
+
"""특정일을 휴일로 추가한다."""
|
|
140
|
+
date = pd.to_datetime(date)
|
|
141
|
+
self._add_date(date, is_open=False)
|
|
142
|
+
|
|
143
|
+
def _add_date(self, date: pd.Timestamp, is_open: bool):
|
|
144
|
+
"""특정일을 개장일 또는 휴일로 추가한다.
|
|
145
|
+
is_open이 True인 경우 개장일로, False인 경우 휴일로 추가한다.
|
|
146
|
+
"""
|
|
147
|
+
date = pd.to_datetime(date).normalize()
|
|
148
|
+
if date not in self.df.index:
|
|
149
|
+
# 필요한 컬럼만 지정해서 안전하게 추가
|
|
150
|
+
new_row = pd.Series({
|
|
151
|
+
'일자': date,
|
|
152
|
+
'개장여부': is_open,
|
|
153
|
+
'개장시간': pd.Timedelta(hours=9),
|
|
154
|
+
'폐장시간': pd.Timedelta(hours=15, minutes=30)
|
|
155
|
+
}, name=date)
|
|
156
|
+
index_name = self.df.index.name
|
|
157
|
+
self.df = pd.concat([self.df, new_row.to_frame().T])
|
|
158
|
+
self.df.index.name = index_name # 또는 '일자'
|
|
159
|
+
else:
|
|
160
|
+
self.df.loc[date, '개장여부'] = True
|
|
161
|
+
self.df.sort_index(inplace=True)
|
|
162
|
+
self._post_date(date)
|
|
163
|
+
|
|
164
|
+
def _post_date(self, date: datetime.date):
|
|
165
|
+
"""특정일을 DB에 업데이트한다."""
|
|
166
|
+
date = pd.to_datetime(date).normalize()
|
|
167
|
+
df = self.df.loc[date:date, :]
|
|
168
|
+
self.tm.post_df(df)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ODI_premaker(ODI_with_TM):
|
|
172
|
+
def make_base_odi_in_year(self, year:int):
|
|
173
|
+
# 먼저 해당 연도에서 df에 없는 모든 날짜를 추가한다.
|
|
174
|
+
# 그 후, 토/일 공휴일을 추가한다.
|
|
175
|
+
# krx에서 추가적인 공휴일을 추가한다.
|
|
176
|
+
|
|
177
|
+
xkrx = ecal.get_calendar('XKRX')
|
|
178
|
+
|
|
179
|
+
all_days = pd.date_range(f"{year}-01-01", f"{year}-12-31", freq='D')
|
|
180
|
+
open_days = xkrx.schedule.loc[f'{year}-01-01':f'{year}-12-31']
|
|
181
|
+
# Fixme: 작동 하는지 확인 필요
|
|
182
|
+
open_days = open_days.index.normalize()
|
|
183
|
+
off_days = all_days.difference(open_days)
|
|
184
|
+
|
|
185
|
+
# 이미 checked된 날짜는 제외한다.
|
|
186
|
+
days_in_df = self.df.index[(self.df.index >= pd.Timestamp(f"{year}-01-01")) & (self.df.index <= pd.Timestamp(f"{year}-12-31"))]
|
|
187
|
+
subset = self.df.loc[days_in_df]
|
|
188
|
+
filtered_subset = subset[subset['checked']]
|
|
189
|
+
days_in_df = filtered_subset.index
|
|
190
|
+
days_not_in_df = all_days.difference(days_in_df)
|
|
191
|
+
for d in days_not_in_df:
|
|
192
|
+
if d in open_days:
|
|
193
|
+
self.add_onday(d)
|
|
194
|
+
elif d in off_days:
|
|
195
|
+
self.add_offday(d)
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError(f"Date {d} is not in open_days or off_days.")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ODI_base_calculation(ODI_premaker):
|
|
201
|
+
# 인접날짜 조회
|
|
202
|
+
def get_next_day(self, date: pd.Timestamp=None, offset:int=1):
|
|
203
|
+
"""date로부터 offset만큼 떨어진 날짜를 반환"""
|
|
204
|
+
date = pd.to_datetime(date).normalize() if date is not None else pd.Timestamp.today().normalize()
|
|
205
|
+
return date+pd.Timedelta(offset, unit='D')
|
|
206
|
+
|
|
207
|
+
def get_next_open_day(self, date: pd.Timestamp, offset:int=1):
|
|
208
|
+
"""date로부터 offset만큼 떨어진 개장일을 반환
|
|
209
|
+
offset이 0이 아닌 경우, 해당 날짜가 개장일 여부는 상관 없음
|
|
210
|
+
"""
|
|
211
|
+
# 이 경우, offset이 양수인지, 음수인지에 따라 처리가 달라진다.
|
|
212
|
+
date = pd.to_datetime(date).normalize()
|
|
213
|
+
# offset이 0인 경우, 해당 날짜가 개장일인지 확인
|
|
214
|
+
if offset == 0:
|
|
215
|
+
if date in self.ondays:
|
|
216
|
+
return date
|
|
217
|
+
else:
|
|
218
|
+
# offset이 0인데 개장일이 아닌 경우 에러 발생
|
|
219
|
+
raise ValueError(f"{date} is not an open day.")
|
|
220
|
+
return get_offset_value_with_target(self.ondays, date, offset)
|
|
221
|
+
|
|
222
|
+
def get_next_off_day(self, date: datetime.date, offset:int=1):
|
|
223
|
+
"""date로부터 offset만큼 떨어진 휴일을 반환
|
|
224
|
+
offset이 0이 아닌 경우, 해당 날짜가 개장일 여부는 상관 없음
|
|
225
|
+
"""
|
|
226
|
+
date = pd.to_datetime(date).normalize()
|
|
227
|
+
if offset == 0:
|
|
228
|
+
if date in self.offdays:
|
|
229
|
+
return date
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError(f"{date} is not an off day.")
|
|
232
|
+
date = pd.to_datetime(date)
|
|
233
|
+
return get_offset_value_with_target(self.offdays, date, offset)
|
|
234
|
+
|
|
235
|
+
def get_prev_day(self, date: pd.Timestamp, offset:int=1) -> pd.Timestamp:
|
|
236
|
+
"""date로부터 offset만큼 떨어진 이전 날짜를 반환"""
|
|
237
|
+
return self.get_next_day(date, -offset)
|
|
238
|
+
def get_prev_open_day(self, date: pd.Timestamp, offset:int=1) -> pd.Timestamp:
|
|
239
|
+
"""date로부터 개장일 중 offset만큼 떨어진 이전 개장일을 반환"""
|
|
240
|
+
return self.get_next_open_day(date, -offset)
|
|
241
|
+
def get_prev_off_day(self, date: pd.Timestamp, offset:int=1) -> pd.Timestamp:
|
|
242
|
+
"""date로부터 휴일 중 offset만큼 떨어진 이전 휴일을 반환"""
|
|
243
|
+
return self.get_next_off_day(date, -offset)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ODI_calculation(ODI_base_calculation):
|
|
247
|
+
"""
|
|
248
|
+
ODI를 계산하는 클래스
|
|
249
|
+
---- ODI 조회 ----
|
|
250
|
+
next_day, next_open_day, next_off_day 메쏘드는 자동으로 금일기준으로 조회. offset 사용 가능
|
|
251
|
+
Fixme: 미래시점의 경우, DB에 없는 경우 처리가 필요.
|
|
252
|
+
get_next_day, get_next_open_day, get_next_off_day 메쏘드의 경우 특정일을 기준으로 사용할 수 있다.
|
|
253
|
+
"""
|
|
254
|
+
@property
|
|
255
|
+
def next_day(self):
|
|
256
|
+
"""오늘 기준으로 다음 날을 반환한다."""
|
|
257
|
+
return self.get_next_day(pd.Timestamp.today().normalize())
|
|
258
|
+
@property
|
|
259
|
+
def next_open_day(self):
|
|
260
|
+
"""오늘 기준으로 다음 개장일을 반환한다."""
|
|
261
|
+
return self.get_next_open_day(pd.Timestamp.today().normalize())
|
|
262
|
+
@property
|
|
263
|
+
def next_off_day(self):
|
|
264
|
+
"""오늘 기준으로 다음 휴일을 반환한다."""
|
|
265
|
+
return self.get_next_off_day(pd.Timestamp.today().normalize())
|
|
266
|
+
@property
|
|
267
|
+
def prev_day(self):
|
|
268
|
+
"""오늘 기준으로 이전 날을 반환한다."""
|
|
269
|
+
return self.get_next_day(pd.Timestamp.today().normalize(), -1)
|
|
270
|
+
@property
|
|
271
|
+
def prev_open_day(self):
|
|
272
|
+
"""오늘 기준으로 이전 개장일을 반환한다."""
|
|
273
|
+
return self.get_next_open_day(pd.Timestamp.today().normalize(), -1)
|
|
274
|
+
@property
|
|
275
|
+
def recent_open_day(self):
|
|
276
|
+
"""금일 포함 최근 개장일을 반환한다."""
|
|
277
|
+
if self.today_is_openday:
|
|
278
|
+
return self.today
|
|
279
|
+
return self.recent_open_day_except_today
|
|
280
|
+
@property
|
|
281
|
+
def recent_open_day_except_today(self):
|
|
282
|
+
"""오늘을 제외한 최근 개장일을 반환한다."""
|
|
283
|
+
return self.get_prev_open_day(self.today)
|
|
284
|
+
@property
|
|
285
|
+
def second_recent_open_day(self):
|
|
286
|
+
"""금일 포함 두번째로 최근 개장일을 반환한다.
|
|
287
|
+
즉, 오늘이 개장일인 경우, 이전 개장일을 반환하고,
|
|
288
|
+
오늘이 휴일인 경우, 전전 개장일을 반환한다.
|
|
289
|
+
"""
|
|
290
|
+
if self.today_is_openday:
|
|
291
|
+
return self.get_prev_open_day(self.today)
|
|
292
|
+
return self.get_prev_open_day(self.today, 2)
|
|
293
|
+
|
|
294
|
+
# 최근 개장일 인덱스 반환
|
|
295
|
+
def get_recent_open_days_index(self, past_days:int=5, forward_days:int=5):
|
|
296
|
+
"""금일을 기준으로 최근 개장일의 인덱스를 반환한다.
|
|
297
|
+
past_days: 과거 개장일 수
|
|
298
|
+
forward_days: 미래 개장일 수
|
|
299
|
+
"""
|
|
300
|
+
past = self.ondays[self.ondays <= self.today][-past_days:] # 과거 개장일
|
|
301
|
+
forward = self.ondays[self.ondays > self.today][:forward_days]
|
|
302
|
+
return past.append(forward).sort_values()
|
|
303
|
+
def get_recent_past_open_days_index(self, past_days:int=5):
|
|
304
|
+
"""금일을 포함한 최근 과거 개장일의 인덱스를 반환한다.
|
|
305
|
+
past_days: 과거 개장일 수
|
|
306
|
+
"""
|
|
307
|
+
return self.get_recent_open_days_index(past_days, 0)
|
|
308
|
+
@property
|
|
309
|
+
def recent_open_days_index(self):
|
|
310
|
+
return self.get_recent_open_days_index(5, 5)
|
|
311
|
+
@property
|
|
312
|
+
def recent_past_open_days_index(self):
|
|
313
|
+
return self.get_recent_past_open_days_index(5)
|
|
314
|
+
@property
|
|
315
|
+
def recent_future_open_days_index(self):
|
|
316
|
+
return self.get_recent_open_days_index(0, 5) # 금일 제외 미래 개장일만
|
|
317
|
+
|
|
318
|
+
# 특정일 개장유무 조회 - 해당일이 index에 없는 경우 에러 발생
|
|
319
|
+
def is_open_day(self, date: datetime.date):
|
|
320
|
+
"""특정일이 개장일인지 확인한다."""
|
|
321
|
+
date = pd.to_datetime(date).normalize()
|
|
322
|
+
if date not in self.days:
|
|
323
|
+
raise ValueError(f"{date} is not in the index of ODI DataFrame.")
|
|
324
|
+
return True if date in self.ondays else False
|
|
325
|
+
# 개장일 range 조회
|
|
326
|
+
def get_open_days(self, start_date: pd.Timestamp, end_date: pd.Timestamp):
|
|
327
|
+
"""특정 기간 내의 개장일을 반환한다."""
|
|
328
|
+
start_date = pd.to_datetime(start_date).normalize()
|
|
329
|
+
end_date = pd.to_datetime(end_date).normalize()
|
|
330
|
+
return self.ondays[(self.ondays >= start_date) & (self.ondays <= end_date)]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class OpeningDayInformation(ODI_calculation):
|
|
334
|
+
"""
|
|
335
|
+
ODI를 유지하고 사용하는 클래스. 내부적인 작동은 감추도록 한다 > 외부에서 사용하는 함수들에 대해서만 정의할 것
|
|
336
|
+
1. 필요한 기능
|
|
337
|
+
-1. 특정일 추가 - 휴일, 개장일 모두 추가 가능
|
|
338
|
+
-2. 특정일이 개장일인지 확인
|
|
339
|
+
추후
|
|
340
|
+
-3. 특정일의 개장/폐장 시간도 설정 가능
|
|
341
|
+
|
|
342
|
+
DF의 구조
|
|
343
|
+
- index로 date를 사용. date는 pd.Timestamp
|
|
344
|
+
- columns: '일자', '개장여부', '개장시간', '폐장시간'
|
|
345
|
+
"""
|
|
346
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
347
|
+
super().__init__(*args, **kwargs)
|
|
348
|
+
self.fetch_whole() # ODI DataFrame을 서버에서 가져온다.
|
|
349
|
+
# print(self.df.index)
|
|
350
|
+
|
|
351
|
+
odi = OpeningDayInformation()
|
|
352
|
+
|
|
353
|
+
def test_add():
|
|
354
|
+
"""ODI에 특정일을 추가하는 테스트"""
|
|
355
|
+
odi.add_onday('2027-01-01')
|
|
356
|
+
|
|
357
|
+
def test_df_recent():
|
|
358
|
+
print("Testing df_recent property...")
|
|
359
|
+
print(odi.df_recent)
|
|
360
|
+
print("Testing df_past_recent property...")
|
|
361
|
+
print(odi.df_past_recent)
|
|
362
|
+
print("Testing df_future_recent property...")
|
|
363
|
+
print(odi.df_future_recent)
|
|
364
|
+
|
|
365
|
+
if __name__ == '__main__':
|
|
366
|
+
print(odi.get_recent_open_days_index())
|
|
367
|
+
print(odi.get_recent_past_open_days_index())
|
|
368
|
+
print("Finished ODI test.")
|