apsniper0673-stockutil 0.1.0__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 (25) hide show
  1. apsniper0673_stockutil-0.1.0/LICENSE +21 -0
  2. apsniper0673_stockutil-0.1.0/PKG-INFO +8 -0
  3. apsniper0673_stockutil-0.1.0/README.md +1 -0
  4. apsniper0673_stockutil-0.1.0/pyproject.toml +19 -0
  5. apsniper0673_stockutil-0.1.0/setup.cfg +4 -0
  6. apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/PKG-INFO +8 -0
  7. apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/SOURCES.txt +23 -0
  8. apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/dependency_links.txt +1 -0
  9. apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/top_level.txt +1 -0
  10. apsniper0673_stockutil-0.1.0/src/mystockutil/__init__.py +2 -0
  11. apsniper0673_stockutil-0.1.0/src/mystockutil/df/format.py +150 -0
  12. apsniper0673_stockutil-0.1.0/src/mystockutil/df/type.py +46 -0
  13. apsniper0673_stockutil-0.1.0/src/mystockutil/df/voperation.py +56 -0
  14. apsniper0673_stockutil-0.1.0/src/mystockutil/ds.py +2 -0
  15. apsniper0673_stockutil-0.1.0/src/mystockutil/etc.py +260 -0
  16. apsniper0673_stockutil-0.1.0/src/mystockutil/logging/__init__.py +2 -0
  17. apsniper0673_stockutil-0.1.0/src/mystockutil/logging/logging_setup.py +36 -0
  18. apsniper0673_stockutil-0.1.0/src/mystockutil/messenger/__init__.py +0 -0
  19. apsniper0673_stockutil-0.1.0/src/mystockutil/messenger/discord/__init__.py +0 -0
  20. apsniper0673_stockutil-0.1.0/src/mystockutil/messenger/discord/sender.py +44 -0
  21. apsniper0673_stockutil-0.1.0/src/mystockutil/naming/__init__.py +1 -0
  22. apsniper0673_stockutil-0.1.0/src/mystockutil/naming/name.py +25 -0
  23. apsniper0673_stockutil-0.1.0/src/mystockutil/spreadsheet_manager.py +52 -0
  24. apsniper0673_stockutil-0.1.0/src/mystockutil/trader_window_manager.py +129 -0
  25. apsniper0673_stockutil-0.1.0/src/mystockutil/type_convert.py +121 -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_stockutil
3
+ Version: 0.1.0
4
+ Summary: My stock utility package
5
+ Author: sniper0673
6
+ Requires-Python: >=3.8
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1 @@
1
+ # mystockdataprovider
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "apsniper0673_stockutil"
3
+ version = "0.1.0"
4
+ description = "My stock utility package"
5
+ authors = [
6
+ { name = "sniper0673" }
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_stockutil
3
+ Version: 0.1.0
4
+ Summary: My stock utility package
5
+ Author: sniper0673
6
+ Requires-Python: >=3.8
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/apsniper0673_stockutil.egg-info/PKG-INFO
5
+ src/apsniper0673_stockutil.egg-info/SOURCES.txt
6
+ src/apsniper0673_stockutil.egg-info/dependency_links.txt
7
+ src/apsniper0673_stockutil.egg-info/top_level.txt
8
+ src/mystockutil/__init__.py
9
+ src/mystockutil/ds.py
10
+ src/mystockutil/etc.py
11
+ src/mystockutil/spreadsheet_manager.py
12
+ src/mystockutil/trader_window_manager.py
13
+ src/mystockutil/type_convert.py
14
+ src/mystockutil/df/format.py
15
+ src/mystockutil/df/type.py
16
+ src/mystockutil/df/voperation.py
17
+ src/mystockutil/logging/__init__.py
18
+ src/mystockutil/logging/logging_setup.py
19
+ src/mystockutil/messenger/__init__.py
20
+ src/mystockutil/messenger/discord/__init__.py
21
+ src/mystockutil/messenger/discord/sender.py
22
+ src/mystockutil/naming/__init__.py
23
+ src/mystockutil/naming/name.py
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+ __author__ = "sniper0673"
@@ -0,0 +1,150 @@
1
+ import os
2
+ import datetime as dt
3
+
4
+ import pandas as pd
5
+ import numpy as np
6
+ from tabulate import tabulate
7
+
8
+ from mystockutil.type_convert import convert_numeric_columns
9
+
10
+ from mystockutil.logging.logging_setup import CustomAdapter, logger as original_logger
11
+ logger = CustomAdapter(logger=original_logger, extra={'prefix': 'format'})
12
+
13
+ """데이터프레임을 보여주거나 출력하기 위한 함수들을 모아놓은 모듈입니다."""
14
+
15
+ zero_decimal_columns = [
16
+ '매수총액', '매도총액', '매수평균가', '매수액', '매도액', '현매수', '현재가',
17
+ '평가손익', '실현', '평가', '총평가', '손익', '총손익', '실현손익', 'TR실현손익', '총실현손익', '미기록손익',
18
+ '예수금', 'D+1예수금', 'D+2예수금',
19
+ '현예산', '현금', '초기예산', '잔여예산', '현재예산'
20
+ '포지션사이즈', '총수수료',
21
+ '가용예산', '투입예산', '예비예산', '현재예산',
22
+ '수량', '매수수량', '매도수량', '매도가능수량', '매도가능수량_최대',
23
+ '매수금액', '매도금액', '금액', '평가금액'
24
+ ]
25
+ one_decimal_columns = [
26
+ '평균보유일수', '평균보유', '평균포지션', '발생주기',
27
+ ]
28
+ two_decimal_columns = [
29
+
30
+ ]
31
+ percent_columns = [
32
+ '수익률', '평균수익률', '추정CAGR', '승률', 'CAGR', '변동률',
33
+ '매도비중', '매도비중_fluct', '매도비중_return',
34
+ ]
35
+
36
+ EXCEL_PATH = r'./df_execl'
37
+
38
+ def myprint(*args, **kwargs):
39
+ """
40
+ print 함수의 wrapper로, DataFrame이나 dict는 엑셀로 저장하고 출력하며,
41
+ 그 외 값은 일반 print로 출력합니다.
42
+ """
43
+ if len(args) == 0:
44
+ return
45
+
46
+ first_arg, *rest_args = args
47
+
48
+ # DataFrame 또는 dict인 경우 별도 처리
49
+ if isinstance(first_arg, pd.DataFrame) or isinstance(first_arg, dict):
50
+ fixed_order = kwargs.pop('fixed_order', False)
51
+ print_df(first_arg, fixed_order=fixed_order)
52
+ if isinstance(first_arg, pd.DataFrame):
53
+ save_df_to_excel_and_csv(first_arg, EXCEL_PATH)
54
+ else:
55
+ print(first_arg)
56
+
57
+ # 나머지 인자 재귀 처리
58
+ if rest_args:
59
+ myprint(*rest_args, **kwargs)
60
+
61
+ def save_df_to_excel_and_csv(df: pd.DataFrame, dirpath: str) -> None:
62
+ """
63
+ DataFrame을 지정된 디렉토리에 엑셀 파일로 저장합니다.
64
+ 파일명은 현재 날짜와 시간을 포함하여 생성되며,
65
+ 동일한 파일명이 존재할 경우 숫자를 붙여 중복을 방지합니다.
66
+
67
+ Parameters
68
+ ----------
69
+ df : pd.DataFrame
70
+ 저장할 데이터프레임
71
+ dirpath : str
72
+ 엑셀 파일을 저장할 디렉토리 경로
73
+ """
74
+ # 디렉토리가 존재하지 않으면 생성
75
+ os.makedirs(dirpath, exist_ok=True)
76
+
77
+ # 현재 날짜와 시간으로 기본 파일명 생성
78
+ timestamp = dt.datetime.now().strftime('%Y%m%d_%H%M%S')
79
+ base_filename = f'dataframe_{timestamp}.xlsx'
80
+ filepath = os.path.join(dirpath, base_filename)
81
+
82
+ # 동일한 파일명이 있으면 숫자 추가
83
+ counter = 1
84
+ while os.path.exists(filepath):
85
+ filename_with_counter = f'dataframe_{timestamp}_{counter}.xlsx'
86
+ filepath = os.path.join(dirpath, filename_with_counter)
87
+ counter += 1
88
+
89
+ # DataFrame을 엑셀로 저장
90
+ df.to_excel(filepath, index=False)
91
+ logger.info(f"DataFrame saved to {filepath}")
92
+ # df.to_csv(filepath.replace('.xlsx', '.csv'), index=False, encoding='utf-8-sig')
93
+ # logger.info(f"DataFrame saved to {filepath.replace('.xlsx', '.csv')}")
94
+
95
+ def print_df(df:pd.DataFrame, fixed_order=False)->None:
96
+ if isinstance(df, dict):
97
+ # dict 형태로 들어온 경우 DF로 변환
98
+ df = pd.DataFrame([df])
99
+ if fixed_order:
100
+ print(tabulate(format_df_to_show(df), headers='keys', tablefmt='psql'))
101
+ else:
102
+ print(tabulate(prettify(df), headers='keys', tablefmt='psql'))
103
+
104
+ def prettify(df:pd.DataFrame)->pd.DataFrame:
105
+ return format_df_to_show(order_df_to_show(df))
106
+
107
+ def format_df_to_show(df:pd.DataFrame)->pd.DataFrame:
108
+ df = convert_numeric_columns(df)
109
+ cols_to_modify = zero_decimal_columns + one_decimal_columns + two_decimal_columns + percent_columns
110
+ for col in df.columns:
111
+ if col in zero_decimal_columns:
112
+ df[col] = df[col].map('{:,.0f}'.format)
113
+ elif col in one_decimal_columns:
114
+ df[col] = df[col].map('{:,.1f}'.format)
115
+ elif col in two_decimal_columns:
116
+ df[col] = df[col].map('{:,.2f}'.format)
117
+ elif col in percent_columns or any(x in col for x in ['%', '대비', '수익률']):
118
+ df[col] = df[col].apply(
119
+ lambda x: '{:.2%}'.format(x) if isinstance(x, (int, float)) else x
120
+ )
121
+ # df[col] = df[col].map('{:.2%}'.format)
122
+ elif 'cagr' in col.lower():
123
+ if (df[col] <= 1).all():
124
+ # 모든 값이 1 이하일 때 → 100 곱해서 소수점 1자리
125
+ df[col] = (df[col] * 100).map('{:,.1f}%'.format)
126
+ else:
127
+ # 그렇지 않으면 → 그냥 소수점 1자리
128
+ df[col] = df[col].map('{:,.1f}'.format)
129
+ return df
130
+
131
+ def order_df_to_show(df:pd.DataFrame)->pd.DataFrame:
132
+ first_order_columns = [
133
+ '종목명', '수량', '매수총액', '평가손익', '실현손익', '수익률'
134
+ ]
135
+ second_order_columns = [
136
+ '매수가', '매수평균가', '매수총액', '매도가', '매도평균가', '매도총액'
137
+ ]
138
+ last_order_columns = [
139
+ '매수일', '매도일', '증권사', '계좌번호'
140
+ ]
141
+ existing_columns = df.columns.copy()
142
+ for cols in [second_order_columns, first_order_columns]:
143
+ cols_to_move = [c for c in cols if c in existing_columns]
144
+ cols_to_stay = [c for c in existing_columns if c not in cols]
145
+ df = df[cols_to_move + cols_to_stay]
146
+ for cols in [last_order_columns]:
147
+ cols_to_move = [c for c in cols if c in existing_columns]
148
+ cols_to_stay = [c for c in existing_columns if c not in cols]
149
+ df = df[cols_to_stay + cols_to_move]
150
+ return df
@@ -0,0 +1,46 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ def init_df_with_type(
5
+ df: pd.DataFrame,
6
+ int_cols: list[str] = None,
7
+ float_cols: list[str] = None,
8
+ insert_when_not_exist:bool = True,
9
+ fill_int_value: int = 0,
10
+ fill_float_value: float = 0.0
11
+ ) -> pd.DataFrame:
12
+ """
13
+ DataFrame의 특정 열을 지정된 타입으로 초기화합니다.
14
+ Parameters
15
+ ----------
16
+ df : pd.DataFrame
17
+ 초기화할 DataFrame
18
+ int_cols : list[str], optional
19
+ 정수형으로 초기화할 열의 리스트, 기본값은 None
20
+ float_cols : list[str], optional
21
+ 실수형으로 초기화할 열의 리스트, 기본값은 None
22
+ insert_when_not_exist : bool, optional
23
+ 지정된 열이 DataFrame에 없을 경우 새로 추가할지 여부, 기본값은 True
24
+ """
25
+ df = df.copy() # 원본 DataFrame을 변경하지 않도록 복사
26
+ if int_cols is None:
27
+ int_cols = []
28
+ if float_cols is None:
29
+ float_cols = []
30
+ # 정수형 열 초기화
31
+ for col in int_cols:
32
+ if col not in df.columns:
33
+ if insert_when_not_exist:
34
+ df[col] = 0
35
+ else:
36
+ continue
37
+ df[col] = df[col].fillna(fill_int_value).astype(int)
38
+ # 실수형 열 초기화
39
+ for col in float_cols:
40
+ if col not in df.columns:
41
+ if insert_when_not_exist:
42
+ df[col] = 0.0
43
+ else:
44
+ continue
45
+ df[col] = df[col].fillna(fill_float_value).astype(float)
46
+ return df
@@ -0,0 +1,56 @@
1
+ import pandas as pd
2
+ from pandas import Series
3
+ import numpy as np
4
+ from typing import List
5
+
6
+ """
7
+ 데이터프레임의 벡터화된 연산들을 정의한 모듈입니다.
8
+ df는 '일자'와 '종목코드'의 MultiIndex를 가지고 있는 경우를 Default로 가정합니다."""
9
+
10
+ def count(df:pd.DataFrame, reset:Series, reset_value:int=1, counting:Series=None, group_by:bool=True, groups:List[str]=['종목코드'])->pd.Series:
11
+ """
12
+ reset이 True일 때, 카운트는 reset_value로 초기화
13
+ False일 때, 카운트에 counting을 더함"""
14
+ df = df.copy()
15
+ if group_by and groups == ['종목코드']:
16
+ if counting is None:
17
+ # df와 동일한 길이의 1로 이루어진 Series를 만듭니다.
18
+ counting = Series(1, index=df.index)
19
+ return _count_default(df=df, reset=reset, reset_value=reset_value, counting=counting)
20
+ else:
21
+ raise NotImplementedError("Not implemented yet.")
22
+ def _count_default(df:pd.DataFrame, reset:Series, reset_value:int, counting:Series)->pd.Series:
23
+ """
24
+ 종목코드별로 카운트를 계산합니다."""
25
+ df['counting'] = counting
26
+ df['reset'] = reset
27
+ df['카운트그룹'] = df.groupby('종목코드')['reset'].cumsum() # 카운트그룹은 0부터 시작함
28
+ df['카운트'] = df.groupby(['종목코드', '카운트그룹'])['counting'].cumsum()
29
+ return df['카운트']
30
+
31
+ def switch(df:pd.DataFrame, on:Series, off:Series, default_bool:bool=False, direction:bool=True, group_by:bool=True, groups:List[str]=['종목코드'])->pd.Series:
32
+ """
33
+ switch가 on인 경우에는 True값을 off가 될 때까지 유지
34
+ 제일 초기값은 False가 Default
35
+ direction이 True면 ffll, False면 bfill
36
+ """
37
+ df = df.copy()
38
+ if group_by and groups == ['종목코드']:
39
+ return _switch_default(df=df, on=on, off=off, default_bool=default_bool, direction=direction)
40
+ else:
41
+ raise NotImplementedError("Not implemented yet.")
42
+
43
+ def _switch_default(df:pd.DataFrame, on:Series, off:Series, default_bool:bool, direction:bool)->pd.Series:
44
+ """
45
+ 종목코드별로 스위치를 계산합니다."""
46
+ df['on'] = on
47
+ df['off'] = off
48
+ df['switch'] = np.nan
49
+ df['switch'] = df['switch'].mask(df['on'], True)
50
+ df['switch'] = df['switch'].mask(df['off'], False)
51
+ if direction:
52
+ df['switch'] = df.groupby('종목코드')['switch'].ffill()
53
+ else:
54
+ df['switch'] = df.groupby('종목코드')['switch'].bfill()
55
+ df['switch'] = df['switch'].fillna(default_bool)
56
+ return df['switch'].astype(bool)
@@ -0,0 +1,2 @@
1
+ def make_dict(**kwargs):
2
+ return kwargs
@@ -0,0 +1,260 @@
1
+ from pathlib import Path
2
+ import ipynbname
3
+ import pandas as pd
4
+ import numpy as np
5
+
6
+
7
+ from mystockutil.logging.logging_setup import CustomAdapter, logger as original_logger
8
+ logger = CustomAdapter(original_logger, {'prefix': 'Utility'})
9
+
10
+ import inspect
11
+ from pathlib import Path
12
+ def get_project_root(marker: str = "pyproject.toml") -> Path:
13
+ """
14
+ 이 함수를 호출한 파일 기준으로 상위 디렉토리에서 marker (예: pyproject.toml)를 찾는다.
15
+ """
16
+ # 호출한 파일의 경로
17
+ caller_path = Path(inspect.stack()[1].filename).resolve()
18
+
19
+ for parent in caller_path.parents:
20
+ if (parent / marker).exists():
21
+ return parent
22
+
23
+ raise RuntimeError(f"Cannot find project root containing {marker} from {caller_path}")
24
+
25
+ def get_base_dir(sub_dirs=[]):
26
+ """
27
+ 현재 실행 중인 파일의 디렉토리를 기준으로 base_dir을 설정합니다.
28
+ Jupyter Notebook과 스크립트 모두에서 동작합니다.
29
+ """
30
+ try:
31
+ # Jupyter Notebook 환경에서 현재 노트북의 경로를 가져옵니다.
32
+ notebook_path = ipynbname.path()
33
+ base_dir = Path(notebook_path).parent.joinpath(*sub_dirs)
34
+ return base_dir
35
+ except (FileNotFoundError, ImportError):
36
+ # 스크립트 실행 환경 또는 ipynbname 사용 불가 시
37
+ try:
38
+ # 스크립트 파일의 경로를 가져옵니다.
39
+ script_path = Path(__file__).resolve()
40
+ base_dir = script_path.parent.parent.joinpath(*sub_dirs)
41
+ return base_dir
42
+ except NameError:
43
+ # __file__이 정의되지 않은 경우 (예: 대화형 환경)
44
+ # 현재 작업 디렉토리를 기준으로 설정합니다.
45
+ base_dir = Path.cwd().joinpath('table', *sub_dirs)
46
+ return base_dir
47
+
48
+ def remove_unnecessary_symbols(df:pd.DataFrame) -> pd.DataFrame:
49
+ """
50
+ 불필요한 종목 제거
51
+ df - 종목 데이터프레임, 종목코드와 종목명이 포함되어 있어야 함
52
+ df - index가 설정되어 있지 않아야 함
53
+ """
54
+ #
55
+ columns = df.columns
56
+ if '시장ID' in columns:
57
+ new_df = df.copy()
58
+ new_df = new_df[
59
+ ~new_df['종목명'].str.contains(r'\d+호$') &
60
+ ~new_df['종목명'].str.contains('스팩') &
61
+ new_df['종목코드'].str.endswith('0') &
62
+ new_df['시장ID'].str.contains('STK|KSQ')
63
+ ]
64
+ else:
65
+ new_df = df.reset_index(drop=False)
66
+ new_df = new_df[
67
+ ~new_df['종목명'].str.contains(r'\d+호$') &
68
+ ~new_df['종목명'].str.contains('스팩') &
69
+ new_df['종목코드'].str.endswith('0')
70
+ ]
71
+ return new_df
72
+
73
+ # 현재 시장의 상하한가 계산
74
+ def calculate_limit_current(price: pd.Series, side: str, after: bool = False) -> int:
75
+ """
76
+ side: 'buy' or 'sell'
77
+ """
78
+ if side == 'buy':
79
+ return calculate_upper_limit(price, after)
80
+ else:
81
+ return calculate_lower_limit(price, after)
82
+ def calculate_upper_limit(price: pd.Series, after: bool = False) -> int:
83
+ if not after:
84
+ # 장중 상한가 계산
85
+ return round_down_price_with_series_current(price * 1.3)
86
+ else:
87
+ # 시간외 상한가 계산
88
+ return round_down_price_with_series_current(price * 1.1)
89
+ def calculate_lower_limit(price: pd.Series, after: bool = False) -> int:
90
+ if not after:
91
+ # 장중 하한가 계산
92
+ return round_up_price_with_series_current(price * 0.7)
93
+ else:
94
+ # 시간외 하한가 계산
95
+ return round_up_price_with_series_current(price * 0.9)
96
+
97
+ # 가격 올림처리
98
+ def round_up_price_with_series_current(prices: pd.Series) -> pd.Series:
99
+ # 각 구간에 해당하는 step 값을 정의
100
+ bins = [-float('inf'), 2000, 5000, 20000, 50000, 200000, 500000, float('inf')]
101
+ steps = [1, 5, 10, 50, 100, 500, 1000]
102
+
103
+ # pd.cut을 사용해 각 price에 대해 적절한 step 값을 할당
104
+ step_series = pd.cut(prices, bins=bins, labels=steps, right=False).astype(int)
105
+
106
+ # 각 price를 step에 맞춰 올림 처리
107
+ rounded_prices = ((np.ceil(prices) + step_series - 0.1) // step_series) * step_series
108
+ return rounded_prices
109
+ def round_up_price_with_series_kospi_current(prices: pd.Series) -> pd.Series:
110
+ return round_up_price_with_series_current(prices)
111
+ def round_up_price_with_series_kosdaq_current(prices: pd.Series) -> pd.Series:
112
+ return round_up_price_with_series_current(prices)
113
+
114
+ def round_up_price_with_series_kospi_old(prices: pd.Series) -> pd.Series:
115
+ """
116
+ 과거 Kospi 시장의 가격 구간에 따른 올림 반올림 처리.
117
+ """
118
+ # 과거 Kospi 시장에서 각 구간에 해당하는 step 값을 정의
119
+ bins = [-float('inf'), 1000, 5000, 10000, 50000, 100000, 500000, float('inf')]
120
+ steps = [1, 5, 10, 50, 100, 500, 1000]
121
+
122
+ # pd.cut을 사용해 각 price에 대해 적절한 step 값을 할당
123
+ step_series = pd.cut(prices, bins=bins, labels=steps, right=False).astype(int)
124
+
125
+ # 각 price를 step에 맞춰 올림 처리
126
+ rounded_prices = ((np.ceil(prices) + step_series - 0.1) // step_series) * step_series
127
+ return rounded_prices
128
+ def round_up_price_with_series_kosdaq_old(prices: pd.Series) -> pd.Series:
129
+ """
130
+ 과거 Kosdaq 시장의 가격 구간에 따른 올림 반올림 처리.
131
+ """
132
+ # 과거 Kosdaq 시장에서 각 구간에 해당하는 step 값을 정의
133
+ bins = [-float('inf'), 1000, 5000, 10000, 50000, float('inf')]
134
+ steps = [1, 5, 10, 50, 100]
135
+
136
+ # pd.cut을 사용해 각 price에 대해 적절한 step 값을 할당
137
+ step_series = pd.cut(prices, bins=bins, labels=steps, right=False).astype(int)
138
+
139
+ # 각 price를 step에 맞춰 올림 처리
140
+ rounded_prices = ((np.ceil(prices) + step_series - 0.1) // step_series) * step_series
141
+ return rounded_prices
142
+
143
+ # 가격 내림처리
144
+ def round_down_price_with_series_current(prices:pd.Series)->pd.Series:
145
+ # 각 구간에 해당하는 step 값을 정의
146
+ bins = [-float('inf'), 2000, 5000, 20000, 50000, 200000, 500000, float('inf')]
147
+ steps = [1, 5, 10, 50, 100, 500, 1000]
148
+
149
+ # pd.cut을 사용해 각 price에 대해 적절한 step 값을 할당
150
+ step_series = pd.cut(prices, bins=bins, labels=steps, right=False).astype(int)
151
+
152
+ # 각 price를 step에 맞춰 반올림 (내림)
153
+ rounded_prices = (prices // step_series) * step_series
154
+ return rounded_prices
155
+ def round_down_price_with_series_kospi_current(prices:pd.Series)->pd.Series:
156
+ return round_down_price_with_series_current(prices)
157
+ def round_down_price_with_series_kosdaq_current(prices:pd.Series)->pd.Series:
158
+ return round_down_price_with_series_current(prices)
159
+
160
+ def round_down_price_with_series_kospi_old(prices:pd.Series)->pd.Series:
161
+ """
162
+ 과거 Kospi 시장의 가격 구간에 따른 내림 반올림 처리.
163
+ """
164
+ # 과거 Kospi 시장에서 각 구간에 해당하는 step 값을 정의
165
+ bins = [-float('inf'), 1000, 5000, 10000, 50000, 100000, 500000, float('inf')]
166
+ steps = [1, 5, 10, 50, 100, 500, 1000]
167
+
168
+ # pd.cut을 사용해 각 price에 대해 적절한 step 값을 할당
169
+ step_series = pd.cut(prices, bins=bins, labels=steps, right=False).astype(int)
170
+
171
+ # 각 price를 step에 맞춰 반올림 (내림)
172
+ rounded_prices = (prices // step_series) * step_series
173
+ return rounded_prices
174
+ def round_down_price_with_series_kosdaq_old(prices:pd.Series)->pd.Series:
175
+ """
176
+ 과거 Kosdaq 시장의 가격 구간에 따른 내림 반올림 처리.
177
+ """
178
+ # 과거 Kosdaq 시장에서 각 구간에 해당하는 step 값을 정의
179
+ bins = [-float('inf'), 1000, 5000, 10000, 50000, float('inf')]
180
+ steps = [1, 5, 10, 50, 100]
181
+
182
+ # pd.cut을 사용해 각 price에 대해 적절한 step 값을 할당
183
+ step_series = pd.cut(prices, bins=bins, labels=steps, right=False).astype(int)
184
+
185
+ # 각 price를 step에 맞춰 반올림 (내림)
186
+ rounded_prices = (prices // step_series) * step_series
187
+ return rounded_prices
188
+
189
+ # float 값을 시리즈로 변환하여 계산
190
+ def round_up_price_current(price:float)->int:
191
+ price_as_series = pd.Series(price)
192
+ return int(round_up_price_with_series_current(price_as_series)[0])
193
+ def round_down_price_current(price:float)->int:
194
+ price_as_series = pd.Series(price)
195
+ return int(round_down_price_with_series_current(price_as_series)[0])
196
+
197
+ # 하나의 값에 대한 계산(위의 함수를 이용하지 않음)
198
+ def round_down_price(price, market:str='kospi', when:str='current'):
199
+ """
200
+ 2023.1.25일부로 수정됨.
201
+ """
202
+ if isinstance(price, (pd.DataFrame, pd.Series)):
203
+ return price.apply(round_down_price, market=market, when=when)
204
+ price_unit = get_price_unit(price, market, when)
205
+ return (price // price_unit) * price_unit
206
+ def round_up_price(price, market:str='kospi', when:str='current'):
207
+ if isinstance(price, (pd.DataFrame, pd.Series)):
208
+ return price.apply(round_up_price)
209
+ price_unit = get_price_unit(price, market, when)
210
+ return ((np.ceil(price) + price_unit - 0.1) // price_unit) * price_unit
211
+ def get_price_unit(price, market, when) -> int:
212
+ """
213
+ 2023.1.25일부로 수정됨.
214
+ """
215
+ if when == 'current':
216
+ if price < 2000:
217
+ step = 1 # 2,000원 미만일 때 1원 단위
218
+ elif price < 5000:
219
+ step = 5 # 2,000원 이상 5,000원 미만일 때 5원 단위
220
+ elif price < 20000:
221
+ step = 10 # 5,000원 이상 20,000원 미만일 때 10원 단위
222
+ elif price < 50000:
223
+ step = 50 # 20,000원 이상 50,000원 미만일 때 50원 단위
224
+ elif price < 200000:
225
+ step = 100 # 50,000원 이상 200,000원 미만일 때 100원 단위
226
+ elif price < 500000:
227
+ step = 500 # 200,000원 이상 500,000원 미만일 때 500원 단위
228
+ else:
229
+ step = 1000 # 500,000원 이상일 때 1,000원 단위
230
+
231
+ elif market.lower() == 'kospi' or market == '코스피':
232
+ if price < 1000:
233
+ step = 1 # 1,000원 미만일 때 1원 단위
234
+ elif price < 5000:
235
+ step = 5 # 1,000원 이상 5,000원 미만일 때 5원 단위
236
+ elif price < 10000:
237
+ step = 10 # 5,000원 이상 10,000원 미만일 때 10원 단위
238
+ elif price < 50000:
239
+ step = 50 # 20,000원 이상 50,000원 미만일 때 50원 단위
240
+ elif price < 100000:
241
+ step = 100 # 50,000원 이상 200,000원 미만일 때 100원 단위
242
+ elif price < 500000:
243
+ step = 500 # 200,000원 이상 500,000원 미만일 때 500원 단위
244
+ else:
245
+ step = 1000 # 500,000원 이상일 때 1,000원 단위
246
+ else: # 과거 Kosdaq인 경우
247
+ if price < 1000:
248
+ step = 1 # 1,000원 미만일 때 1원 단위
249
+ elif price < 5000:
250
+ step = 5 # 1,000원 이상 5,000원 미만일 때 5원 단위
251
+ elif price < 10000:
252
+ step = 10 # 5,000원 이상 10,000원 미만일 때 10원 단위
253
+ elif price < 50000:
254
+ step = 50 # 20,000원 이상 50,000원 미만일 때 50원 단위
255
+ else:
256
+ step = 100 # 50,000원 이상 200,000원 미만일 때 100원 단위
257
+ return step
258
+
259
+ if __name__ == '__main__':
260
+ print(df)
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+ __author__ = "Hyuntamansei"
@@ -0,0 +1,36 @@
1
+ import logging
2
+
3
+ # 로거 생성
4
+ logger = logging.getLogger('my_logger')
5
+ logger.setLevel(logging.DEBUG)
6
+
7
+ # 중복 핸들러 방지
8
+ if logger.hasHandlers():
9
+ logger.handlers.clear()
10
+
11
+ # 콘솔 핸들러 생성
12
+ console_handler = logging.StreamHandler()
13
+ console_handler.setLevel(logging.DEBUG)
14
+
15
+ # 파일 핸들러 생성
16
+ file_handler = logging.FileHandler('stock.log')
17
+ file_handler.setLevel(logging.DEBUG)
18
+
19
+ # 포맷 설정
20
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
21
+ formatter = logging.Formatter('%(levelname)s - %(message)s - %(asctime)s')
22
+ console_handler.setFormatter(formatter)
23
+ file_handler.setFormatter(formatter)
24
+
25
+ # 핸들러 추가
26
+ logger.addHandler(console_handler)
27
+ logger.addHandler(file_handler)
28
+
29
+ # LoggerAdapter 사용하여 파일별 접두어 설정
30
+ class CustomAdapter(logging.LoggerAdapter):
31
+ def process(self, msg, kwargs):
32
+ return '[%s] %s' % (self.extra['prefix'], msg), kwargs
33
+
34
+ # 각 파일마다 'prefix'를 다르게 설정
35
+ def get_custom_logger(prefix):
36
+ return CustomAdapter(logger, {'prefix': prefix})
@@ -0,0 +1,44 @@
1
+ import requests
2
+
3
+ class DiscordSender:
4
+ def __init__(self, webhook_url):
5
+ self.webhook_url = webhook_url
6
+
7
+ def send(self, message):
8
+ requests.post(self.webhook_url, json={"content": message})
9
+
10
+ # 황금비 대표
11
+ gr_sender = DiscordSender('https://discord.com/api/webhooks/1347084901844779018/sqgHY3P14seiIPXd15-AIxIftRdfzFV9Or2fYtBB1XevLevpPweNJm9H5cdIW0HcKwj1')
12
+
13
+ # 황금비 개별
14
+ gr_entry_zero = DiscordSender('https://discord.com/api/webhooks/1362630337888260116/NPTk3V8wOKZ8sjS-g0o-4OSZgy7CFib83furIDONAomphFZGS9sdYwy6CaxLSkd0J2_s')
15
+ gr_entry_one = DiscordSender('https://discord.com/api/webhooks/1362666742978973796/uW_RoUBRSgwryY8L0QQy_W7YH6VjTuvyy8-iUgKJkShlYWaj4xz_G9BEnxN5axUFtO9i')
16
+ gr_entry_two = DiscordSender('https://discord.com/api/webhooks/1362666785362411610/oBK59j4o4_IJ_3Tuwny5vJEgZHWOfpRTBuTwt-sCSDH2GHEigWEWkIE3hZPUpJ9TgZlb')
17
+ gr_entry_three = DiscordSender('https://discord.com/api/webhooks/1362666807101751416/h5e4NoKMcQTClpFhQL10sg5GR-L-QQVBZj0pKTRTwfUZNq1vwVu4i9rJLLgaW7HhEog0')
18
+ gr_entry_four = DiscordSender('https://discord.com/api/webhooks/1362666830669287465/0WbVPFOYHfZzx-eBzyJ-nfiKF5iIjtiVHClocAT2wzKCeFtkB9yrg3mAMc10Wc9PqlUw')
19
+ gr_entry_five = DiscordSender('https://discord.com/api/webhooks/1362666854987862066/aFSajyALHttWjLabAl6UBebD7doiyOSj3gIFcqFGCaFiq6imBVsGOE0NG15jtuBfRKVo')
20
+ gr_entry_hf = DiscordSender('https://discord.com/api/webhooks/1354984972661952673/vIiCfFlh8gNNMtdyenZr_s6d_ENU37Bgab5y6bR_49i-XJ04zCU2HUPiDb_F-Rx0E2RN')
21
+
22
+ gr_exit_zero = DiscordSender('https://discord.com/api/webhooks/1374232079012986960/BaT5zEcIUrCDYzFfHpgS1GPhZFNKdLfunpY8gbIaja4K3QsdU7sdySJWD2aGpfpwalZi')
23
+ gr_exit_one = DiscordSender('https://discord.com/api/webhooks/1374238305100627998/oC2dpWdkUDvgz5g0glB-SJmjgvrXINi73cNpHHYEo0VW3jhsGxMswBohsqy84LTlRFnZ')
24
+ gr_exit_two = DiscordSender('https://discord.com/api/webhooks/1374238355478548601/-RK0X7wHTC7OAKBAnXCb4Sa46F1UutdmJhrTiGBUDh1ypLpEj6RmuA1m9pDH8R4X0jOj')
25
+ gr_exit_three = DiscordSender('https://discord.com/api/webhooks/1374238411682222181/A0F2OWzdYonp-1-N6B1mfXlajhLelL8sTlvRFlAnHCohfgL_yViPyfe6k322H3UENnsf')
26
+ gr_exit_four = DiscordSender('https://discord.com/api/webhooks/1374238464278528020/IMO8inC-CEOLweQg-AfyeHK-4Cj1ynXqaWjVpVxoqRet4NNG872toy9m_ZvUjK1IOsOI')
27
+ gr_exit_five = DiscordSender('https://discord.com/api/webhooks/1374238519794340004/ptC2C2LSiTeUiktTo2-O1ByBegM9IOFs2AcIbdW1w-1MgUh6cZ6jlEyKxQy_xTHNT0ky')
28
+
29
+
30
+ # Scheduler sender
31
+ ms_sender = DiscordSender('https://discord.com/api/webhooks/1347085128509165579/Tqo-NevMGrQ5HZXjaAVfMk4t5-OW2C_zNzCVeT9z0-HwQwvJcov0u2qKxNkdTCKaghrf')
32
+ ss_sender = DiscordSender('https://discord.com/api/webhooks/1353572795178549288/GLrhkNAHMbmlMrpf2JLQ_7Zuns-fHthmUT_Vq7UnCsFU3keEDywxmci2yyPeXfLWVnhV')
33
+
34
+ # gr_two = DiscordSender('https://discord.com/api/webhooks/1354984967373066373/0NLNQJ_7UIoV-yt53TIIw9ZyGleHgkbTnFzJ8v_JdXdJUfLEQgqzCizSoV4F9MhA3pPi')
35
+ # gr_three = DiscordSender('https://discord.com/api/webhooks/1357226724496244837/9GrTKJjgUD8bGd8uxEWc-FBVV_iZ6-SGYj40b3b8H3EE7YsrLkk3jh7BOn7_wIR6B7R9')
36
+ # gr_four = DiscordSender('https://discord.com/api/webhooks/1359768724751650847/ykUDJIMzNoI1W98p4cVBoHz4kDFqXmsLFf_UmSS2d5muHDINyyipWTqTUswOyyP1Ev9C')
37
+ # gr_five = DiscordSender('https://discord.com/api/webhooks/1359768729948258354/seFMt3APXQY4HE6tjWmwhAig5HAlyxlAXd8Ayjy8gIFLD_SE6btxJD9yhBkdeu4U_JFT')
38
+ # gr_hf = DiscordSender('https://discord.com/api/webhooks/1354984972661952673/vIiCfFlh8gNNMtdyenZr_s6d_ENU37Bgab5y6bR_49i-XJ04zCU2HUPiDb_F-Rx0E2RN')
39
+
40
+ if __name__ == '__main__':
41
+ senders = [gr_sender, ms_sender]
42
+ for sender in senders:
43
+ sender.send("Hello, World!")
44
+ sender.send("This is a test message.")
@@ -0,0 +1 @@
1
+ from .name import Name
@@ -0,0 +1,25 @@
1
+ import re
2
+
3
+ class Name:
4
+ """클래스 이름을 자동으로 생성하는 클래스
5
+ """
6
+ def __init__(self, *args, **kwargs):
7
+ super().__init__()
8
+ # __init__가 후행 호출되는 경우에도 작동할 수 있도록 설계
9
+ if not hasattr(self, '_comment'):
10
+ self._comment:str = None
11
+ if not hasattr(self, 'version'):
12
+ self.version:str = None
13
+ @property
14
+ def name(self):
15
+ """comment에서 최신 버전을 찾아 name을 리턴"""
16
+ if hasattr(self, '_comment'):
17
+ versions = re.findall(r'v\d+\.\d+', self._comment) # 모든 vX.XX 패턴 찾기
18
+ else:
19
+ versions = []
20
+ latest_version = versions[-1] if versions else "v0.00" # 가장 마지막 버전 선택
21
+ if hasattr(self, 'version'):
22
+ self.version = latest_version
23
+ if hasattr(self, '_name'):
24
+ return f'{self._name}{latest_version}'
25
+ return f'{self.__class__.__name__}{latest_version}'
@@ -0,0 +1,52 @@
1
+ import pandas as pd
2
+ import gspread
3
+ from gspread_dataframe import set_with_dataframe, get_as_dataframe
4
+
5
+ class SpreadsheetManager:
6
+ def __init__(self, creds_json_path: str, sheet_id: str):
7
+ self.creds_json_path = creds_json_path
8
+ self.sheet_id = sheet_id
9
+ self.client = self._authorize()
10
+
11
+ def _authorize(self):
12
+ scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
13
+ creds = gspread.service_account(filename=self.creds_json_path, scopes=scope)
14
+ return creds
15
+
16
+ def upload_df(self, df: pd.DataFrame, worksheet_name: str, index: bool = True) -> None:
17
+ spreadsheet = self.client.open_by_key(self.sheet_id)
18
+ worksheet = self._get_or_create_worksheet(spreadsheet, worksheet_name)
19
+ worksheet.clear()
20
+ set_with_dataframe(worksheet, df, include_index=index)
21
+
22
+ def download_df(self, worksheet_name: str) -> pd.DataFrame:
23
+ spreadsheet = self.client.open_by_key(self.sheet_id)
24
+ worksheet = self._get_or_create_worksheet(spreadsheet, worksheet_name)
25
+ return get_as_dataframe(worksheet, header=0, index_col=None, dtype=str)
26
+
27
+ def _get_or_create_worksheet(self, spreadsheet, worksheet_name: str):
28
+ try:
29
+ return spreadsheet.worksheet(worksheet_name)
30
+ except gspread.WorksheetNotFound:
31
+ return spreadsheet.add_worksheet(title=worksheet_name, rows="100", cols="20")
32
+
33
+ def test():
34
+ creds_json_path = r'./credentials/vertical-album-400707-49b25aaf32d5.json'
35
+ sheet_id = "1iK7UY7g_fSRSAOpIEYrFR41p_qK68LhkcIfqr7NUxug"
36
+ sheet_manager = SpreadsheetManager(creds_json_path, sheet_id)
37
+
38
+ # Test uploading a DataFrame
39
+ df = pd.DataFrame({
40
+ 'A': [1, 2, 3],
41
+ 'B': [4, 5, 6]
42
+ })
43
+ sheet_manager.upload_df(df, "TestWorksheet")
44
+
45
+ # Test downloading a DataFrame
46
+ downloaded_df = sheet_manager.download_df("TestWorksheet")
47
+ print_df(downloaded_df)
48
+
49
+ if __name__ == "__main__":
50
+ from utility.df.format import print_df
51
+ test()
52
+ print("SpreadsheetManager test completed successfully.")
@@ -0,0 +1,129 @@
1
+ import psutil
2
+ import win32gui
3
+ import win32con
4
+ import win32process
5
+ import win32api
6
+
7
+ # 마지막 모니터의 작업 영역 (작업표시줄 제외)
8
+ def get_last_monitor_work_area():
9
+ monitors = win32api.EnumDisplayMonitors()
10
+ last_monitor = monitors[-1][0]
11
+ info = win32api.GetMonitorInfo(last_monitor)
12
+ return info['Work'] # (left, top, right, bottom)
13
+
14
+ # 마지막 모니터의 전체 해상도 (작업표시줄 포함)
15
+ def get_last_monitor_resolution():
16
+ monitors = win32api.EnumDisplayMonitors()
17
+ last_monitor = monitors[-1][0]
18
+ info = win32api.GetMonitorInfo(last_monitor)
19
+ mon_left, mon_top, mon_right, mon_bottom = info['Monitor']
20
+ return mon_right - mon_left, mon_bottom - mon_top
21
+
22
+ # 실제 표시되는 콘솔 창의 HWND 반환
23
+ def get_real_visible_hwnd_by_pid(pid):
24
+ hwnds = []
25
+ def callback(hwnd, hwnds):
26
+ _, found_pid = win32process.GetWindowThreadProcessId(hwnd)
27
+ class_name = win32gui.GetClassName(hwnd)
28
+ if found_pid == pid and win32gui.IsWindowVisible(hwnd):
29
+ if class_name == "PseudoConsoleWindow":
30
+ parent = win32gui.GetParent(hwnd)
31
+ if parent and win32gui.IsWindowVisible(parent):
32
+ hwnds.append(parent)
33
+ else:
34
+ hwnds.append(hwnd)
35
+ return True
36
+ win32gui.EnumWindows(callback, hwnds)
37
+ return hwnds[0] if hwnds else None
38
+
39
+ # 창 위치 이동 (프레임 보정 포함)
40
+ def set_window_pos(hwnd, x, y, width, height, is_4k=False):
41
+ if hwnd and win32gui.IsWindow(hwnd):
42
+ # 마진 조정
43
+ if is_4k:
44
+ horizontal_margin = -12
45
+ vertical_margin = -14
46
+ else:
47
+ horizontal_margin = -6
48
+ vertical_margin = -8
49
+
50
+ x += horizontal_margin
51
+ y += vertical_margin
52
+ width -= horizontal_margin * 2
53
+ height -= vertical_margin
54
+
55
+ win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
56
+ win32gui.SetWindowPos(hwnd, None, x, y, width, height,
57
+ win32con.SWP_NOZORDER | win32con.SWP_SHOWWINDOW)
58
+
59
+ # 전체 정렬 로직
60
+ def adjust_windows_by_recent_procs():
61
+ # 작업 가능한 영역 (창 위치 기준)
62
+ left, top, right, bottom = get_last_monitor_work_area()
63
+ screen_w = right - left
64
+ screen_h = bottom - top
65
+
66
+ # 전체 해상도 기준 4K 판별
67
+ mon_w, mon_h = get_last_monitor_resolution()
68
+ is_4k = mon_w * mon_h >= 8000000
69
+ is_portrait = screen_h > screen_w
70
+
71
+ left_w = screen_w // 2
72
+ right_w = screen_w - left_w
73
+ top_h = screen_h // 2
74
+ bottom_h = screen_h - top_h
75
+
76
+ # 위치 설정
77
+ if is_4k:
78
+ if is_portrait:
79
+ positions = [
80
+ (left, top, screen_w, top_h),
81
+ (left, top, left_w, top_h),
82
+ (left + left_w, top, right_w, top_h),
83
+ (left, top + top_h, screen_w, bottom_h),
84
+ (left, top + top_h, left_w, bottom_h),
85
+ (left + left_w, top + top_h, right_w, bottom_h),
86
+ ]
87
+ else:
88
+ positions = [
89
+ (left, top, left_w, screen_h),
90
+ (left, top, left_w, top_h),
91
+ (left, top + top_h, left_w, bottom_h),
92
+ (left + left_w, top, right_w, screen_h),
93
+ (left + left_w, top, right_w, top_h),
94
+ (left + left_w, top + top_h, right_w, bottom_h),
95
+ ]
96
+ else:
97
+ positions = [
98
+ (left, top, left_w, screen_h),
99
+ (left, top, left_w, top_h),
100
+ (left, top + top_h, left_w, bottom_h),
101
+ (left + left_w, top, right_w, screen_h),
102
+ (left + left_w, top, right_w, top_h),
103
+ (left + left_w, top + top_h, right_w, bottom_h),
104
+ ]
105
+
106
+ # 현재 Python을 실행한 cmd.exe 제외
107
+ parent_pid = psutil.Process().ppid()
108
+
109
+ procs = [
110
+ p for p in psutil.process_iter(['pid', 'name', 'create_time'])
111
+ if p.info['name'] == 'cmd.exe' and p.info['pid'] != parent_pid
112
+ ]
113
+
114
+ procs.sort(key=lambda p: p.info['create_time'], reverse=True)
115
+ selected_procs = procs[:6]
116
+ selected_procs.reverse()
117
+
118
+ for i, proc in enumerate(selected_procs):
119
+ hwnd = get_real_visible_hwnd_by_pid(proc.info['pid'])
120
+ if hwnd:
121
+ x, y, w, h = positions[i]
122
+ print(f"[INFO] Moving PID {proc.info['pid']} to ({x}, {y}, {w}, {h})")
123
+ set_window_pos(hwnd, x, y, w, h, is_4k=is_4k)
124
+ else:
125
+ print(f"[WARN] No visible HWND found for PID {proc.info['pid']}")
126
+
127
+ # 실행
128
+ if __name__ == "__main__":
129
+ adjust_windows_by_recent_procs()
@@ -0,0 +1,121 @@
1
+ import re
2
+
3
+ def extract_clean_int(s: str) -> int:
4
+ """
5
+ 문자열에서 숫자와 쉼표를 추출하여 정수로 변환합니다.
6
+ 처리할 수 없는 경우 0을 반환합니다."""
7
+ if not isinstance(s, str):
8
+ return s
9
+ # 부호 포함 숫자 패턴: + 또는 -가 있을 수 있고, 그 뒤에 숫자와 쉼표
10
+ match = re.search(r'[-+]?\d[\d,]*', s)
11
+ if match:
12
+ num_str = match.group().replace(',', '') # 쉼표 제거
13
+ return int(num_str)
14
+ return 0
15
+
16
+ import pandas as pd
17
+ import numpy as np
18
+ import re
19
+
20
+ def clean_numeric_string(val: str, percent_mode='float') -> float:
21
+ """
22
+ Cleans and extracts numeric values from strings.
23
+ Handles negative values, commas, backslashes, currency symbols, and percentages.
24
+ """
25
+ if pd.isna(val):
26
+ return val
27
+
28
+ val = str(val).strip()
29
+ val = val.replace('\\', '').replace(',', '')
30
+
31
+ # Handle percentage
32
+ if '%' in val:
33
+ val = val.replace('%', '')
34
+ try:
35
+ val = float(val)
36
+ return val / 100 if percent_mode == 'float' else val
37
+ except ValueError:
38
+ return np.nan
39
+
40
+ # Remove all non-numeric characters except digits, minus, and dot
41
+ val = re.sub(r'[^\d\.-]', '', val)
42
+ try:
43
+ return float(val)
44
+ except ValueError:
45
+ return np.nan
46
+
47
+ int_cols = []
48
+ float_cols = []
49
+ date_cols = []
50
+ string_cols = [
51
+ '종목코드',
52
+ ]
53
+ def convert_numeric_columns(
54
+ df: pd.DataFrame,
55
+ force_int_cols=None,
56
+ force_float_cols=None,
57
+ force_date_cols=None,
58
+ force_string_cols=None,
59
+ nan_policy='keep', # 'keep', 'zero', 'drop'
60
+ percent_mode='float' # 'float' → 0.724, 'raw' → 72.4
61
+ ) -> pd.DataFrame:
62
+ force_int_cols = set(force_int_cols or int_cols)
63
+ force_float_cols = set(force_float_cols or float_cols)
64
+ force_date_cols = set(force_date_cols or date_cols)
65
+ force_string_cols = set(force_string_cols or string_cols)
66
+
67
+ result_df = df.copy()
68
+
69
+ for col in result_df.columns:
70
+ series = result_df[col]
71
+
72
+ # Skip string columns
73
+ if col in force_string_cols:
74
+ result_df[col] = series.astype(str)
75
+ continue
76
+ # Step 1: Date columns
77
+ if col in force_date_cols:
78
+ result_df[col] = pd.to_datetime(series, errors='coerce')
79
+ continue
80
+
81
+ # Step 2: Numeric-like string cleaning
82
+ if col in force_int_cols or col in force_float_cols:
83
+ cleaned = series.apply(lambda x: clean_numeric_string(x, percent_mode=percent_mode))
84
+ else:
85
+ # Try detecting numeric-like columns (basic heuristic on sample)
86
+ sample = series.dropna().astype(str).head(10)
87
+ if all(re.fullmatch(r'\d{4}[./-]\d{2}[./-]\d{2}', x) for x in sample):
88
+ continue # 완전한 날짜 패턴이면 스킵
89
+
90
+ if all(re.search(r'[\d₩%,\\]', x) for x in sample):
91
+ cleaned = series.apply(lambda x: clean_numeric_string(x, percent_mode=percent_mode))
92
+ else:
93
+ continue # skip non-numeric columns
94
+
95
+ # Step 3: NaN policy
96
+ if nan_policy == 'zero':
97
+ cleaned = cleaned.fillna(0)
98
+ elif nan_policy == 'drop':
99
+ cleaned = cleaned.dropna()
100
+
101
+ # Step 4: Type decision
102
+ if col in force_int_cols:
103
+ result_df[col] = cleaned.astype('Int64')
104
+ elif col in force_float_cols:
105
+ result_df[col] = cleaned.astype(float)
106
+ else:
107
+ non_na = cleaned.dropna()
108
+
109
+ # 소수점 없는 float인지 확인 (정수로 안전하게 변환 가능한지)
110
+ is_integral = non_na.apply(lambda x: float(x).is_integer()).all()
111
+ if is_integral:
112
+ result_df[col] = cleaned.astype('Int64')
113
+ else:
114
+ result_df[col] = cleaned.astype(float)
115
+
116
+ # if np.allclose(non_na, non_na.astype(int)):
117
+ # result_df[col] = cleaned.astype('Int64')
118
+ # else:
119
+ # result_df[col] = cleaned.astype(float)
120
+
121
+ return result_df