apsniper0673-grsheetmanager 0.1.4__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 sniper0673
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_grsheetmanager
3
+ Version: 0.1.4
4
+ Summary: My grsheetmanager for APSniper0673
5
+ Author: apsniper0673
6
+ Requires-Python: >=3.8
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1 @@
1
+ # constants
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "apsniper0673_grsheetmanager"
3
+ version = "0.1.4"
4
+ description = "My grsheetmanager 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_grsheetmanager
3
+ Version: 0.1.4
4
+ Summary: My grsheetmanager for APSniper0673
5
+ Author: apsniper0673
6
+ Requires-Python: >=3.8
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/apsniper0673_grsheetmanager.egg-info/PKG-INFO
5
+ src/apsniper0673_grsheetmanager.egg-info/SOURCES.txt
6
+ src/apsniper0673_grsheetmanager.egg-info/dependency_links.txt
7
+ src/apsniper0673_grsheetmanager.egg-info/top_level.txt
8
+ src/mygrsheetmanager/__init__.py
9
+ src/mygrsheetmanager/df_wish_list.py
10
+ src/mygrsheetmanager/gr_sheet_manager.py
11
+ src/mygrsheetmanager/gr_sheet_manager_client.py
12
+ src/mygrsheetmanager/gr_sheet_manager_server.py
13
+ src/mygrsheetmanager/single_trader_wish_list.py
@@ -0,0 +1,79 @@
1
+ """
2
+ 여러 트레이더들의 wish list를 관리하는 DataFrame을 생성하는 모듈입니다.
3
+ """
4
+
5
+ import pandas as pd
6
+ from mygrsheetmanager.single_trader_wish_list import SingleDfWishList
7
+
8
+ class _DfWithWishList:
9
+ def __init__(self, trader_names: list):
10
+ """
11
+ 트레이더 이름 리스트를 받아서 DataFrame을 초기화합니다.
12
+ :param trader_names: 트레이더 이름의 리스트
13
+ """
14
+ self.trader_names = trader_names
15
+ # self.df_wish_list = pd.DataFrame(index=trader_names) # index: 트레이더 이름, columns: 종목명, values: 매수 희망 여부
16
+ self.wish_list_dict = {name: SingleDfWishList(trader_name=name) for name in trader_names}
17
+ @property
18
+ def df(self) -> pd.DataFrame:
19
+ """
20
+ 트레이더들의 매수 희망 종목 리스트를 DataFrame 형태로 반환합니다.
21
+ :return: DataFrame
22
+ index: 트레이더 이름, columns: 종목명, values: 매수 희망 여부
23
+ """
24
+ # DataFrame을 생성할 때, 각 트레이더의 wish_list를 합칩니다.
25
+ dfs = []
26
+ for trader_name in self.trader_names:
27
+ s = self.wish_list_dict[trader_name].wish_list
28
+
29
+ # 비어 있는 경우라도 빈 DataFrame을 넣어줌
30
+ if s.empty:
31
+ dfs.append(pd.DataFrame(index=[trader_name])) # 빈 row만 있는 DataFrame
32
+ else:
33
+ df = s.to_frame().T
34
+ df.index = [trader_name]
35
+ dfs.append(df)
36
+
37
+ df_merged = pd.concat(dfs, axis=0).fillna("")
38
+ df_merged.index.name = "trader"
39
+ return df_merged.reindex(index=self.trader_names).sort_index(axis=1)
40
+
41
+ class DfWithWishList_manage_single(_DfWithWishList):
42
+ def add_wish(self, trader_name: str, stock_name: str, wish: str = "매수") -> None:
43
+ """
44
+ 특정 트레이더의 매수 희망 종목을 추가합니다.
45
+ :param trader_name: 트레이더 이름
46
+ :param stock_name: 종목명
47
+ :param wish: 매수 희망 여부 (예: "매수", "관심")
48
+ """
49
+ if trader_name in self.wish_list_dict:
50
+ self.wish_list_dict[trader_name].add_wish(stock_name, wish)
51
+ def add_wishes(self, trader_name: str, stock_names: list, wish: str = "매수") -> None:
52
+ """
53
+ 특정 트레이더의 매수 희망 종목을 일괄적으로 추가합니다.
54
+ :param trader_name: 트레이더 이름
55
+ :param stock_names: 종목명 리스트
56
+ :param wish: 매수 희망 여부 (예: "매수", "관심")
57
+ """
58
+ if trader_name in self.wish_list_dict:
59
+ for stock_name in stock_names:
60
+ self.wish_list_dict[trader_name].add_wish(stock_name, wish)
61
+ def remove_wish(self, trader_name: str, stock_name: str) -> None:
62
+ """
63
+ 특정 트레이더의 매수 희망 종목을 제거합니다.
64
+ :param trader_name: 트레이더 이름
65
+ :param stock_name: 제거할 종목명
66
+ """
67
+ if trader_name in self.wish_list_dict:
68
+ self.wish_list_dict[trader_name].remove_wish(stock_name)
69
+ def replace_wishes(self, trader_name: str, stock_names: list, wish: str = "매수") -> None:
70
+ """
71
+ 특정 트레이더의 매수 희망 종목을 일괄적으로 교체합니다.
72
+ :param trader_name: 트레이더 이름
73
+ :param stock_names: 종목명 리스트
74
+ :param wish: 매수 희망 여부 (예: "매수", "관심")
75
+ """
76
+ if trader_name in self.wish_list_dict:
77
+ self.wish_list_dict[trader_name].replace_wishes(stock_names, wish)
78
+ class DfWithWishList(DfWithWishList_manage_single):
79
+ pass
@@ -0,0 +1,109 @@
1
+ """
2
+ 트레이더 GR의 구글시트 매니저
3
+ 구현 기능
4
+ 1. 여러 GR 트레이더의 매수 희망 종목에 대해서 정리하여 관리
5
+ wish_list라는 sheet에 트레이더별로 종목을 정리하여 관리한다.
6
+ 트레이더 클래스는 여러개 생성되어도, 이 클래스는 하나만 존재한다.
7
+ """
8
+ from mygrsheetmanager.df_wish_list import DfWithWishList
9
+ from mystockutil.spreadsheet.spreadsheet_manager import SpreadsheetManager
10
+ from myutil.path import get_credentials_path
11
+
12
+ CRED_DIR = get_credentials_path()
13
+ CRED_JSON_PATH = CRED_DIR / 'vertical-album-400707-49b25aaf32d5.json'
14
+
15
+ SHEET_ID = "1iK7UY7g_fSRSAOpIEYrFR41p_qK68LhkcIfqr7NUxug"
16
+ # TRADER_NAMES = [
17
+ # '황금비0', '황금비1', '황금비2',
18
+ # # '황금비3', '황금비4',
19
+ # '황금비5', '황금비6',
20
+ # '황금비_test',
21
+ # ]
22
+ TRADER_NAMES = [
23
+ '뉴황금비0', '뉴황금비1',
24
+ # '황금비2',
25
+ # '황금비3', '황금비4',
26
+ # '황금비5',
27
+ '황금비6',
28
+ '황금비_test',
29
+ ]
30
+
31
+ class _GRSheetManager:
32
+ """
33
+ self._df_wish_list: pd.DataFrame
34
+ index: 트레이더 이름
35
+ columns: 종목명
36
+ values: 매수 희망 여부로, "매수" 등등의 문자열로 표현된다.
37
+ """
38
+ def __init__(self):
39
+ self.sheet_manager = SpreadsheetManager(
40
+ creds_json_path=CRED_JSON_PATH,
41
+ sheet_id=SHEET_ID,
42
+ )
43
+ self.wish_sheet_name = 'wish_list'
44
+ self.sell_sheet_name = 'sell_list'
45
+ self.wish_list = DfWithWishList(trader_names=TRADER_NAMES[0:-1])
46
+ # 매도리스트도 동일한 클래스로 생성해 준다.
47
+ self.sell_list = DfWithWishList(trader_names=TRADER_NAMES[0:-1])
48
+
49
+ class GRSheetManager_posting(_GRSheetManager):
50
+ def post_wish_df(self) -> None:
51
+ """
52
+ wish_list 시트에 DataFrame을 업로드한다.
53
+ :param df: 업로드할 DataFrame
54
+ """
55
+ self.sheet_manager.upload_df(self.wish_list.df, worksheet_name=self.wish_sheet_name, index=True)
56
+ def post_sell_df(self) -> None:
57
+ """
58
+ sell_list 시트에 DataFrame을 업로드한다.
59
+ :param df: 업로드할 DataFrame
60
+ """
61
+ self.sheet_manager.upload_df(self.sell_list.df, worksheet_name=self.sell_sheet_name, index=True)
62
+
63
+ class GRSheetManager(GRSheetManager_posting):
64
+ pass
65
+
66
+ """테스트용 코드"""
67
+ def test_add_single_wish():
68
+ mgr = GRSheetManager_posting()
69
+ mgr.wish_list.add_wish("황금비0", "삼성전자", "매수")
70
+ df = mgr.wish_list.df
71
+ assert df.loc["황금비0", "삼성전자"] == "매수"
72
+
73
+ def test_multiple_wishes():
74
+ mgr = GRSheetManager_posting()
75
+ mgr.wish_list.add_wish("황금비1", "LG화학", "관심")
76
+ mgr.wish_list.add_wish("황금비1", "카카오", "매수")
77
+ df = mgr.wish_list.df
78
+ assert df.loc["황금비1", "LG화학"] == "관심"
79
+ assert df.loc["황금비1", "카카오"] == "매수"
80
+
81
+ def test_replace_wishes():
82
+ mgr = GRSheetManager_posting()
83
+ mgr.wish_list.wish_list_dict["황금비2"].replace_wishes(["현대차", "기아"], wish="매수")
84
+ df = mgr.wish_list.df
85
+ assert df.loc["황금비2", "현대차"] == "매수"
86
+ assert df.loc["황금비2", "기아"] == "매수"
87
+
88
+ def test_remove_wish():
89
+ mgr = GRSheetManager_posting()
90
+ mgr.wish_list.add_wish("황금비3", "셀트리온", "매수")
91
+ mgr.wish_list.wish_list_dict["황금비3"].remove_wish("셀트리온")
92
+ df = mgr.wish_list.df
93
+ assert "셀트리온" not in df.columns or df.loc["황금비3", "셀트리온"] == ""
94
+
95
+ def run_test_case(title: str, func):
96
+ print(f"\n[TEST] {title}")
97
+ try:
98
+ func()
99
+ print("→ ✅ 성공")
100
+ except Exception as e:
101
+ print(f"→ ❌ 실패: {e}")
102
+
103
+ if __name__ == "__main__":
104
+ run_test_case("단일 종목 추가", test_add_single_wish)
105
+ run_test_case("다수 종목 추가", test_multiple_wishes)
106
+ run_test_case("비어 있는 트레이더 포함", test_empty_wish_trader_included)
107
+ run_test_case("일괄 종목 교체", test_replace_wishes)
108
+ run_test_case("종목 삭제", test_remove_wish)
109
+ print("\n모든 테스트 완료")
@@ -0,0 +1,161 @@
1
+ """
2
+ GR 트레이더의 구글시트 매니저 – 클라이언트용
3
+ """
4
+ import requests
5
+ import pandas as pd
6
+
7
+ # Essential imports
8
+ from mystockutil.server.get_available_server import get_available_server
9
+
10
+ # 로컬 호스트를 먼저 찾는다 > 데이터 절약하기 위해서
11
+ server_candidates = [
12
+ 'http://localhost:6019', # 백업서버
13
+ 'http://localhost:6009',
14
+ 'http://brstk2.iptime.org:6019',
15
+ 'http://brstk2.iptime.org:6009',
16
+ 'http://brstk.com:6019', # 백업서버
17
+ 'http://brstk.com:6009',
18
+ 'http://brstk.iptime.org:6009',
19
+ 'http://brstk.iptime.org:6019',
20
+ ]
21
+
22
+ # SERVER_URL = "http://localhost:6009"
23
+ SERVER_URL = get_available_server(server_candidates=server_candidates)
24
+
25
+ # 안전한 API 호출을 위한 데코레이터
26
+ def safe_api_call(func):
27
+ def wrapper(*args, **kwargs):
28
+ try:
29
+ return func(*args, **kwargs)
30
+ except Exception as e:
31
+ print(f"[API 오류] {func.__name__} 실패: {e}")
32
+ return None
33
+ return wrapper
34
+
35
+
36
+
37
+ def add_wish(trader_name: str, stock_name: str, wish: str = "매수"):
38
+ url = f"{SERVER_URL}/wishlist/{trader_name}"
39
+ data = {
40
+ "mode": "add",
41
+ "stock_name": stock_name,
42
+ "wish": wish,
43
+ }
44
+ return requests.post(url, json=data).json()
45
+
46
+ def replace_wishes(trader_name: str, stock_names: list, wish: str = "매수"):
47
+ url = f"{SERVER_URL}/wishlist/{trader_name}"
48
+ data = {
49
+ "mode": "replace",
50
+ "stock_names": stock_names,
51
+ "wish": wish,
52
+ }
53
+ return requests.post(url, json=data).json()
54
+
55
+ def delete_wish(trader_name: str, stock_name: str):
56
+ url = f"{SERVER_URL}/wishlist/{trader_name}"
57
+ data = {
58
+ "mode": "delete",
59
+ "stock_name": stock_name,
60
+ }
61
+ return requests.post(url, json=data).json()
62
+
63
+ def get_all_wishlist():
64
+ return requests.get(f"{SERVER_URL}/wishlist").json()
65
+
66
+ def get_trader_wishlist(trader_name: str):
67
+ return requests.get(f"{SERVER_URL}/wishlist/{trader_name}").json()
68
+
69
+ """
70
+ sell_list에 대해서 동일한 작업 수행
71
+ """
72
+ def add_sell(trader_name: str, stock_name: str, wish: str = "매도"):
73
+ url = f"{SERVER_URL}/selllist/{trader_name}"
74
+ data = {
75
+ "mode": "add",
76
+ "stock_name": stock_name,
77
+ "wish": wish,
78
+ }
79
+ return requests.post(url, json=data).json()
80
+
81
+ def replace_sells(trader_name: str, stock_names: list, wish: str = "매수"):
82
+ url = f"{SERVER_URL}/selllist/{trader_name}"
83
+ data = {
84
+ "mode": "replace",
85
+ "stock_names": stock_names,
86
+ "wish": wish,
87
+ }
88
+ return requests.post(url, json=data).json()
89
+
90
+ def delete_sell(trader_name: str, stock_name: str):
91
+ url = f"{SERVER_URL}/selllist/{trader_name}"
92
+ data = {
93
+ "mode": "delete",
94
+ "stock_name": stock_name,
95
+ }
96
+ return requests.post(url, json=data).json()
97
+
98
+ def get_all_selllist():
99
+ return requests.get(f"{SERVER_URL}/selllist").json()
100
+
101
+ def get_trader_selllist(trader_name: str):
102
+ return requests.get(f"{SERVER_URL}/selllist/{trader_name}").json()
103
+
104
+ class SingleTraderWishListClient:
105
+ """싱글 트레이더의 wish list를 관리하는 클라이언트 클래스"""
106
+
107
+ def __init__(self, trader_name: str):
108
+ self.trader_name = trader_name
109
+
110
+ @safe_api_call
111
+ def add_wish(self, stock_name: str, wish: str = "매수"):
112
+ return add_wish(self.trader_name, stock_name, wish)
113
+ @safe_api_call
114
+ def replace_wishes(self, stock_names: list, wish: str = "매수"):
115
+ return replace_wishes(self.trader_name, stock_names, wish)
116
+ @safe_api_call
117
+ def delete_wish(self, stock_name: str):
118
+ return delete_wish(self.trader_name, stock_name)
119
+ @safe_api_call
120
+ def get_wishlist(self):# -> Any:
121
+ data = get_trader_wishlist(self.trader_name)
122
+ df = pd.DataFrame.from_records(data['data'])
123
+ return df
124
+ @safe_api_call
125
+ def get_all_wishlist(self):
126
+ data = get_all_wishlist()
127
+ df = pd.DataFrame.from_records(data['data'])
128
+ return df
129
+ """
130
+ selllist 코드
131
+ """
132
+ @safe_api_call
133
+ def add_sell(self, stock_name: str, wish: str = "매도"):
134
+ return add_sell(self.trader_name, stock_name, wish)
135
+ @safe_api_call
136
+ def replace_sells(self, stock_names: list, wish: str = "매도"):
137
+ return replace_sells(self.trader_name, stock_names, wish)
138
+ @safe_api_call
139
+ def delete_sell(self, stock_name: str):
140
+ return delete_sell(self.trader_name, stock_name)
141
+ @safe_api_call
142
+ def get_selllist(self):# -> Any:
143
+ data = get_trader_selllist(self.trader_name)
144
+ df = pd.DataFrame.from_records(data['data'])
145
+ return df
146
+ @safe_api_call
147
+ def get_all_selllist(self):
148
+ data = get_all_selllist()
149
+ df = pd.DataFrame.from_records(data['data'])
150
+ return df
151
+
152
+
153
+ if __name__ == "__main__":
154
+ # 예시 사용법
155
+ client = SingleTraderWishListClient("황금비1")
156
+ print(client.add_wish("삼성전자", "매수"))
157
+ print(client.get_wishlist())
158
+ print(client.replace_wishes(["삼성전자", "LG전자"], "추매"))
159
+ print(client.delete_wish("LG전자"))
160
+ print(client.replace_wishes(["카카오", "네이버"], "매도"))
161
+ print(client.get_wishlist())
@@ -0,0 +1,142 @@
1
+ """
2
+ GR Sheet Manager를 구동하는 서버 코드
3
+
4
+ API 종류:
5
+ ------------
6
+ GET /wishlist
7
+ 전체 wish list를 JSON(table) 형식으로 보내줍니다.
8
+ POST /wishlist/<trader_name>
9
+ 특정 트레이더의 wish를 추가, 교체 또는 삭제합니다.
10
+ body 예시 (application/json):
11
+ {
12
+ "mode": "add", # add | replace | delete
13
+ "stock_name": "삼성전자", # add/delete 모드에서 필요
14
+ "stock_names": ["삼성전자", "LG화학"], # replace 모드에서 필요
15
+ "wish": "머야" # 기본값: "머야"
16
+ }
17
+
18
+ 실행 방식:
19
+ ------------
20
+ $ python wishlist_server.py
21
+
22
+ * 개발 동안은 Flask 내장 서버로 가능
23
+ * 관리 환경은 Waitress 또는 Gunicorn + systemd 구조로 구성
24
+
25
+ 필요 패키지:
26
+ ------------
27
+ flask, pandas, gspread, google-auth, SpreadsheetManager 관련 패키지
28
+ """
29
+
30
+ from flask import Flask, jsonify, request
31
+ from mygrsheetmanager.gr_sheet_manager import GRSheetManager
32
+
33
+ app = Flask(__name__)
34
+
35
+ grsheet = GRSheetManager() # 싱글턴으로 사용
36
+
37
+ def _push_wish_to_sheet() -> None:
38
+ """Google Sheet에 wish list를 복사합니다."""
39
+ try:
40
+ grsheet.post_wish_df()
41
+ except Exception as exc:
42
+ app.logger.exception("Google Sheet에 복사 실패")
43
+ raise exc
44
+
45
+ @app.route("/wishlist", methods=["GET"])
46
+ def get_wishlist():
47
+ """GR 트레이더 전체의 wish list를 JSON(table) 형식으로 보내줍니다."""
48
+ df = grsheet.wish_list.df
49
+ return df.to_json(orient="table", force_ascii=False), 200, {"Content-Type": "application/json; charset=utf-8"}
50
+
51
+ @app.route("/wishlist/<trader_name>", methods=["GET"])
52
+ def get_trader_wish(trader_name):
53
+ df = grsheet.wish_list.df
54
+ if trader_name not in df.index:
55
+ return jsonify({"error": f"{trader_name}의 wish list가 없습니다."}), 404
56
+ return df.loc[[trader_name]].to_json(orient="table", force_ascii=False)
57
+
58
+ @app.route("/wishlist/<trader>", methods=["POST"])
59
+ def upsert_wish(trader: str):
60
+ """GR 트레이더의 wish 추가 (add), 교체 (replace), 삭제 (delete)를 지원합니다."""
61
+ data = request.get_json(force=True)
62
+ mode: str = data.get("mode", "add")
63
+ wish: str = data.get("wish", "매수")
64
+
65
+ if mode == "add":
66
+ stock = data.get("stock_name")
67
+ if not stock:
68
+ return jsonify({"error": "add 모드에서는 stock_name이 필요합니다."}), 400
69
+ grsheet.wish_list.add_wish(trader, stock, wish)
70
+ elif mode == "replace":
71
+ # 빈 리스트도 허용
72
+ stocks = data.get("stock_names")
73
+ grsheet.wish_list.replace_wishes(trader, stocks, wish)
74
+ elif mode == "delete":
75
+ stock = data.get("stock_name")
76
+ if not stock:
77
+ return jsonify({"error": "delete 모드에서는 stock_name이 필요합니다."}), 400
78
+ grsheet.wish_list.remove_wish(trader, stock)
79
+ else:
80
+ return jsonify({"error": f"알 수 없는 mode: {mode}"}), 400
81
+
82
+ _push_wish_to_sheet()
83
+ return jsonify({"status": "ok"})
84
+
85
+ """
86
+ sell_list 코드
87
+ """
88
+ def _push_sell_to_sheet() -> None:
89
+ """Google Sheet에 sell list를 복사합니다."""
90
+ try:
91
+ grsheet.post_sell_df()
92
+ except Exception as exc:
93
+ app.logger.exception("Google Sheet에 복사 실패")
94
+ raise exc
95
+
96
+ @app.route("/selllist", methods=["GET"])
97
+ def get_selllist():
98
+ """GR 트레이더 전체의 sell list를 JSON(table) 형식으로 보내줍니다."""
99
+ df = grsheet.sell_list.df
100
+ return df.to_json(orient="table", force_ascii=False), 200, {"Content-Type": "application/json; charset=utf-8"}
101
+
102
+ @app.route("/selllist/<trader_name>", methods=["GET"])
103
+ def get_trader_sell(trader_name):
104
+ df = grsheet.sell_list.df
105
+ if trader_name not in df.index:
106
+ return jsonify({"error": f"{trader_name}의 sell list가 없습니다."}), 404
107
+ return df.loc[[trader_name]].to_json(orient="table", force_ascii=False)
108
+
109
+ @app.route("/selllist/<trader>", methods=["POST"])
110
+ def upsert_sell(trader: str):
111
+ """GR 트레이더의 sell 추가 (add), 교체 (replace), 삭제 (delete)를 지원합니다."""
112
+ data = request.get_json(force=True)
113
+ mode: str = data.get("mode", "add")
114
+ sell: str = data.get("sell", "매도")
115
+
116
+ if mode == "add":
117
+ stock = data.get("stock_name")
118
+ if not stock:
119
+ return jsonify({"error": "add 모드에서는 stock_name이 필요합니다."}), 400
120
+ grsheet.sell_list.add_wish(trader, stock, sell)
121
+ elif mode == "replace":
122
+ # 빈 리스트도 허용
123
+ stocks = data.get("stock_names")
124
+ grsheet.sell_list.replace_wishes(trader, stocks, sell)
125
+ elif mode == "delete":
126
+ stock = data.get("stock_name")
127
+ if not stock:
128
+ return jsonify({"error": "delete 모드에서는 stock_name이 필요합니다."}), 400
129
+ grsheet.sell_list.remove_wish(trader, stock)
130
+ else:
131
+ return jsonify({"error": f"알 수 없는 mode: {mode}"}), 400
132
+
133
+ _push_sell_to_sheet()
134
+ return jsonify({"status": "ok"})
135
+
136
+ @app.route("/")
137
+ def ping():
138
+ return "OK"
139
+
140
+ if __name__ == "__main__":
141
+ # host='0.0.0.0' 은 개방적인 접속을 위한 파라미터
142
+ app.run(host="0.0.0.0", port=6009, debug=True)
@@ -0,0 +1,31 @@
1
+ """
2
+ 싱글 트레이더의 매수 희망 종목 리스트를 관리하는 클래스입니다.
3
+ """
4
+ import pandas as pd
5
+
6
+ class _SingleDfWishList:
7
+ def __init__(self, trader_name):
8
+ self.trader_name = trader_name
9
+ self.wish_list = pd.Series() # index: 종목명, values: 매수 희망 여부로, "매수" 등등의 문자열로 표현된다.
10
+ class SingleDfWishList(_SingleDfWishList):
11
+ def add_wish(self, stock_name: str, wish: str="매수") -> None:
12
+ """
13
+ 매수 희망 종목을 추가한다.
14
+ :param stock_name: 종목명
15
+ :param wish: 매수 희망 여부 (예: "매수", "관심")
16
+ """
17
+ self.wish_list[stock_name] = wish
18
+ def remove_wish(self, stock_name: str) -> None:
19
+ """
20
+ 매수 희망 종목을 제거한다.
21
+ :param stock_name: 제거할 종목명
22
+ """
23
+ if stock_name in self.wish_list:
24
+ del self.wish_list[stock_name]
25
+ def replace_wishes(self, stock_names: list, wish: str="매수") -> None:
26
+ """
27
+ 매수 희망 종목을 일괄적으로 교체한다.
28
+ :param stock_names: 종목명 리스트
29
+ :param wish: 매수 희망 여부 (예: "매수", "관심")
30
+ """
31
+ self.wish_list = pd.Series(index=stock_names, data=wish)