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.
Files changed (66) hide show
  1. apsniper0673_dataprovider-0.1.1/LICENSE +21 -0
  2. apsniper0673_dataprovider-0.1.1/PKG-INFO +8 -0
  3. apsniper0673_dataprovider-0.1.1/README.md +14 -0
  4. apsniper0673_dataprovider-0.1.1/pyproject.toml +19 -0
  5. apsniper0673_dataprovider-0.1.1/setup.cfg +4 -0
  6. apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/PKG-INFO +8 -0
  7. apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/SOURCES.txt +64 -0
  8. apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/dependency_links.txt +1 -0
  9. apsniper0673_dataprovider-0.1.1/src/apsniper0673_dataprovider.egg-info/top_level.txt +1 -0
  10. apsniper0673_dataprovider-0.1.1/src/mydataprovider/__init__.py +0 -0
  11. apsniper0673_dataprovider-0.1.1/src/mydataprovider/api.py +3 -0
  12. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/__init__.py +0 -0
  13. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/feather_manager.py +127 -0
  14. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/odi/df_odi.py +368 -0
  15. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/odi/odi_functions.py +46 -0
  16. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/__init__.py +0 -0
  17. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/convert_to_ppd_df.py +124 -0
  18. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/df_ppd.py +317 -0
  19. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/ppdps/__init__.py +0 -0
  20. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/ppdps/ppdp_baisc.py +128 -0
  21. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/ppd/ppdps/ppdp_custom.py +293 -0
  22. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/__init__.py +0 -0
  23. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/df_raw.py +141 -0
  24. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/df_raw_krx.py +128 -0
  25. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/df_raw_nxt.py +143 -0
  26. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/merge_raw.py +77 -0
  27. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/raw/stock_data_handler_for_raw.py +16 -0
  28. apsniper0673_dataprovider-0.1.1/src/mydataprovider/frame/stock_dataframe.py +169 -0
  29. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/__init__.py +0 -0
  30. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/__init__.py +0 -0
  31. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/flask/__init__.py +0 -0
  32. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/flask/rdf_client.py +118 -0
  33. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/flask/rdf_server.py +208 -0
  34. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/rdf/__init__.py +0 -0
  35. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/rdf/df_rdf.py +210 -0
  36. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/rdp/realtime_dataframe_provider.py +67 -0
  37. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/stock_data_provider.py +387 -0
  38. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/__init__.py +0 -0
  39. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/krx/__init__.py +0 -0
  40. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/krx/krx_data_provider.py +68 -0
  41. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/__init__.py +0 -0
  42. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/__init__.py +0 -0
  43. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/naver_data_client.py +147 -0
  44. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/naver_data_fetcher.py +357 -0
  45. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/flask/naver_data_server.py +168 -0
  46. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/naver/naver_data_provider.py +116 -0
  47. apsniper0673_dataprovider-0.1.1/src/mydataprovider/provider/web/web_data_provider.py +101 -0
  48. apsniper0673_dataprovider-0.1.1/src/mydataprovider/simple.py +2 -0
  49. apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/__init__.py +0 -0
  50. apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/db_with_sql.py +340 -0
  51. apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/stock_table_manager.py +176 -0
  52. apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/tm_odi.py +60 -0
  53. apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/tm_raw_krx.py +83 -0
  54. apsniper0673_dataprovider-0.1.1/src/mydataprovider/table/tm_raw_nxt.py +102 -0
  55. apsniper0673_dataprovider-0.1.1/src/mydataprovider/test_api.py +3 -0
  56. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/__init__.py +0 -0
  57. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/__init__.py +0 -0
  58. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/krx.py +184 -0
  59. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/naver.py +380 -0
  60. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/crawler/nxt.py +98 -0
  61. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/db/sql.py +36 -0
  62. apsniper0673_dataprovider-0.1.1/src/mydataprovider/utility/get_available_server.py +32 -0
  63. apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/handler_functions.py +23 -0
  64. apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/singleday_data_handler.py +141 -0
  65. apsniper0673_dataprovider-0.1.1/src/mydataprovider/z_handler/stock_data_handler.py +432 -0
  66. 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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: apsniper0673_dataprovider
3
+ Version: 0.1.1
4
+ Summary: My stock data provider for APSniper0673
5
+ Author: apsniper0673
6
+ Requires-Python: >=3.8
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,14 @@
1
+ # mystockdataprovider
2
+
3
+ 모듈 하이라키
4
+
5
+ Most Independent
6
+ - odi
7
+ - utility에 있는 모든 모듈
8
+ - crawler
9
+ - db 등등
10
+
11
+ Hierarchy
12
+ - frame > raw > ppd > rdf > rdp > sdp
13
+ - provider > krx / naver > wdp > rdp > sdp
14
+
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: apsniper0673_dataprovider
3
+ Version: 0.1.1
4
+ Summary: My stock data provider for APSniper0673
5
+ Author: apsniper0673
6
+ Requires-Python: >=3.8
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -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,3 @@
1
+ from .frame.odi.df_odi import odi
2
+ from .table.db_with_sql import DBWithSQL
3
+ from .provider.stock_data_provider import sdp, StockDataProvider
@@ -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.")