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.
- apsniper0673_stockutil-0.1.0/LICENSE +21 -0
- apsniper0673_stockutil-0.1.0/PKG-INFO +8 -0
- apsniper0673_stockutil-0.1.0/README.md +1 -0
- apsniper0673_stockutil-0.1.0/pyproject.toml +19 -0
- apsniper0673_stockutil-0.1.0/setup.cfg +4 -0
- apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/PKG-INFO +8 -0
- apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/SOURCES.txt +23 -0
- apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/dependency_links.txt +1 -0
- apsniper0673_stockutil-0.1.0/src/apsniper0673_stockutil.egg-info/top_level.txt +1 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/__init__.py +2 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/df/format.py +150 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/df/type.py +46 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/df/voperation.py +56 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/ds.py +2 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/etc.py +260 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/logging/__init__.py +2 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/logging/logging_setup.py +36 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/messenger/__init__.py +0 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/messenger/discord/__init__.py +0 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/messenger/discord/sender.py +44 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/naming/__init__.py +1 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/naming/name.py +25 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/spreadsheet_manager.py +52 -0
- apsniper0673_stockutil-0.1.0/src/mystockutil/trader_window_manager.py +129 -0
- 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 @@
|
|
|
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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mystockutil
|
|
@@ -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,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,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})
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|