Spaces:
Running
Running
Upload 14 files
Browse files- modules/compare_stock_prices.py +43 -24
- modules/retirement_planning.py +3 -3
- style.css +30 -23
modules/compare_stock_prices.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import io
|
2 |
-
import base64
|
3 |
import matplotlib.pyplot as plt
|
4 |
import FinanceDataReader as fdr
|
5 |
import pandas as pd
|
@@ -7,24 +6,18 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7 |
|
8 |
def get_stock_prices(stock_code, days):
|
9 |
try:
|
10 |
-
# 주식 데이터를 가져옴
|
11 |
df = fdr.DataReader(stock_code)
|
12 |
end_date = pd.to_datetime('today')
|
13 |
-
# 영업일 기준으로 시작 날짜를 계산
|
14 |
start_date = pd.date_range(end=end_date, periods=days, freq='B')[0]
|
15 |
-
# 지정된 기간 동안의 주식 종가 데이터를 필터링
|
16 |
df = df[(df.index >= start_date) & (df.index <= end_date)]
|
17 |
return df['Close']
|
18 |
except Exception as e:
|
19 |
-
# 데이터 가져오기 실패 시 에러 메시지 출력
|
20 |
print(f"Failed to fetch data for {stock_code}: {e}")
|
21 |
return None
|
22 |
|
23 |
def compare_stock_prices(stock_codes, days):
|
24 |
-
# 주식 그래프 생성을 위한 병렬 처리
|
25 |
stock_prices = {}
|
26 |
with ThreadPoolExecutor(max_workers=10) as executor:
|
27 |
-
# 각 주식 코드에 대해 get_stock_prices 함수를 비동기로 실행
|
28 |
futures = {executor.submit(get_stock_prices, stock_code.strip(), int(days)): stock_code.strip() for stock_code in stock_codes.split(',')}
|
29 |
for future in as_completed(futures):
|
30 |
stock_code = futures[future]
|
@@ -33,39 +26,64 @@ def compare_stock_prices(stock_codes, days):
|
|
33 |
if prices is not None:
|
34 |
stock_prices[stock_code] = prices
|
35 |
except Exception as e:
|
36 |
-
# 데이터 가져오기 실패 시 에러 메시지 출력
|
37 |
print(f"Failed to fetch data for {stock_code}: {e}")
|
38 |
|
39 |
-
# Matplotlib의 기본 설정을 사용하여 백엔드를 설정
|
40 |
plt.switch_backend('agg')
|
|
|
41 |
|
42 |
-
|
43 |
-
fig, ax = plt.subplots(figsize=(10, 6))
|
44 |
for stock_code, prices in stock_prices.items():
|
45 |
-
# 첫 번째 데이터 포인트를 기준으로 상대적 가격 계산
|
46 |
relative_prices = prices / prices.iloc[0]
|
47 |
-
|
48 |
-
ax.plot(prices.index, relative_prices, label=stock_code.upper()) # 주식 코드를 대문자로 표시
|
49 |
|
50 |
-
# 위와 오른쪽 테두리 없애기
|
51 |
ax.spines['top'].set_visible(False)
|
52 |
ax.spines['right'].set_visible(False)
|
53 |
|
54 |
ax.set_xlabel('Date')
|
55 |
ax.set_ylabel('Relative Price (Normalized to 1)')
|
56 |
-
# ax.set_title(f'Relative Stock Prices Over the Last {days} Days')
|
57 |
ax.legend()
|
58 |
plt.tight_layout()
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
|
68 |
-
# 주식 가격 데이터를 HTML 테이블로 변환
|
69 |
html_table = "<h3>Stock Prices Data</h3><div class='table-container'><table>"
|
70 |
html_table += "<thead><tr><th>Date</th>"
|
71 |
for stock_code in stock_prices.keys():
|
@@ -81,6 +99,7 @@ def compare_stock_prices(stock_codes, days):
|
|
81 |
|
82 |
html_table += "</tbody></table></div>"
|
83 |
|
|
|
84 |
return graph_html + html_table
|
85 |
|
86 |
# 예시 사용 방법
|
|
|
1 |
import io
|
|
|
2 |
import matplotlib.pyplot as plt
|
3 |
import FinanceDataReader as fdr
|
4 |
import pandas as pd
|
|
|
6 |
|
7 |
def get_stock_prices(stock_code, days):
|
8 |
try:
|
|
|
9 |
df = fdr.DataReader(stock_code)
|
10 |
end_date = pd.to_datetime('today')
|
|
|
11 |
start_date = pd.date_range(end=end_date, periods=days, freq='B')[0]
|
|
|
12 |
df = df[(df.index >= start_date) & (df.index <= end_date)]
|
13 |
return df['Close']
|
14 |
except Exception as e:
|
|
|
15 |
print(f"Failed to fetch data for {stock_code}: {e}")
|
16 |
return None
|
17 |
|
18 |
def compare_stock_prices(stock_codes, days):
|
|
|
19 |
stock_prices = {}
|
20 |
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
|
21 |
futures = {executor.submit(get_stock_prices, stock_code.strip(), int(days)): stock_code.strip() for stock_code in stock_codes.split(',')}
|
22 |
for future in as_completed(futures):
|
23 |
stock_code = futures[future]
|
|
|
26 |
if prices is not None:
|
27 |
stock_prices[stock_code] = prices
|
28 |
except Exception as e:
|
|
|
29 |
print(f"Failed to fetch data for {stock_code}: {e}")
|
30 |
|
|
|
31 |
plt.switch_backend('agg')
|
32 |
+
plt.style.use('tableau-colorblind10') # 테마 변경
|
33 |
|
34 |
+
fig, ax = plt.subplots(figsize=(8, 4.5))
|
|
|
35 |
for stock_code, prices in stock_prices.items():
|
|
|
36 |
relative_prices = prices / prices.iloc[0]
|
37 |
+
ax.plot(prices.index, relative_prices, label=stock_code.upper())
|
|
|
38 |
|
|
|
39 |
ax.spines['top'].set_visible(False)
|
40 |
ax.spines['right'].set_visible(False)
|
41 |
|
42 |
ax.set_xlabel('Date')
|
43 |
ax.set_ylabel('Relative Price (Normalized to 1)')
|
|
|
44 |
ax.legend()
|
45 |
plt.tight_layout()
|
46 |
|
47 |
+
svg_graph = io.StringIO()
|
48 |
+
plt.savefig(svg_graph, format='svg')
|
49 |
+
svg_graph.seek(0)
|
50 |
+
svg_data = svg_graph.getvalue()
|
51 |
+
plt.close()
|
52 |
+
|
53 |
+
svg_data = svg_data.replace('<svg ', '<svg width="100%" height="100%" ')
|
54 |
+
svg_data = svg_data.replace('</svg>', '''
|
55 |
+
<defs>
|
56 |
+
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
57 |
+
<stop offset="0%" style="stop-color:rgb(173,216,230);stop-opacity:1" />
|
58 |
+
<stop offset="100%" style="stop-color:rgb(0,191,255);stop-opacity:1" />
|
59 |
+
</linearGradient>
|
60 |
+
<filter id="dropshadow" height="130%">
|
61 |
+
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
62 |
+
<feOffset dx="2" dy="2" result="offsetblur"/>
|
63 |
+
<feMerge>
|
64 |
+
<feMergeNode/>
|
65 |
+
<feMergeNode in="SourceGraphic"/>
|
66 |
+
</feMerge>
|
67 |
+
</filter>
|
68 |
+
</defs>
|
69 |
+
<style>
|
70 |
+
@keyframes lineAnimation {
|
71 |
+
from {
|
72 |
+
stroke-dasharray: 0, 1000;
|
73 |
+
}
|
74 |
+
to {
|
75 |
+
stroke-dasharray: 1000, 0;
|
76 |
+
}
|
77 |
+
}
|
78 |
+
path {
|
79 |
+
animation: lineAnimation 1s linear forwards;
|
80 |
+
}
|
81 |
+
</style>
|
82 |
+
</svg>''')
|
83 |
+
|
84 |
+
# Replace line color with gradient, add shadow filter, and apply animation
|
85 |
+
svg_data = svg_data.replace('stroke="#1f77b4"', 'stroke="url(#grad1)" filter="url(#dropshadow)"')
|
86 |
|
|
|
87 |
html_table = "<h3>Stock Prices Data</h3><div class='table-container'><table>"
|
88 |
html_table += "<thead><tr><th>Date</th>"
|
89 |
for stock_code in stock_prices.keys():
|
|
|
99 |
|
100 |
html_table += "</tbody></table></div>"
|
101 |
|
102 |
+
graph_html = f'<h3>Relative Stock Prices Over the Last {days} Days</h3>{svg_data}'
|
103 |
return graph_html + html_table
|
104 |
|
105 |
# 예시 사용 방법
|
modules/retirement_planning.py
CHANGED
@@ -62,7 +62,7 @@ def retirement_planning(current_age=None, retirement_age=None, current_investmen
|
|
62 |
<div>
|
63 |
<div style="margin-bottom: 1.5rem;">
|
64 |
<!-- 은퇴 시점의 총 투자액 표시 -->
|
65 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">
|
66 |
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
67 |
<span style='color: #1678fb'>{investment_at_retirement:,.0f}</span>
|
68 |
</div>
|
@@ -72,7 +72,7 @@ def retirement_planning(current_age=None, retirement_age=None, current_investmen
|
|
72 |
<div>
|
73 |
<div style="margin-bottom: 1.5rem;">
|
74 |
<!-- 은퇴 시점의 연간 및 월간 배당 수익 표시 -->
|
75 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">
|
76 |
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{annual_dividend_at_retirement:,.0f}</span>
|
77 |
Annual
|
78 |
<p></p>
|
@@ -88,7 +88,7 @@ def retirement_planning(current_age=None, retirement_age=None, current_investmen
|
|
88 |
<thead>
|
89 |
<tr>
|
90 |
<th>Age</th>
|
91 |
-
<th>
|
92 |
<th>Annual</th>
|
93 |
<th>Monthly</th>
|
94 |
</tr>
|
|
|
62 |
<div>
|
63 |
<div style="margin-bottom: 1.5rem;">
|
64 |
<!-- 은퇴 시점의 총 투자액 표시 -->
|
65 |
+
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">SAVINGS at retirement</div>
|
66 |
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
67 |
<span style='color: #1678fb'>{investment_at_retirement:,.0f}</span>
|
68 |
</div>
|
|
|
72 |
<div>
|
73 |
<div style="margin-bottom: 1.5rem;">
|
74 |
<!-- 은퇴 시점의 연간 및 월간 배당 수익 표시 -->
|
75 |
+
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">DIVIDEND INCOME at retirement</div>
|
76 |
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{annual_dividend_at_retirement:,.0f}</span>
|
77 |
Annual
|
78 |
<p></p>
|
|
|
88 |
<thead>
|
89 |
<tr>
|
90 |
<th>Age</th>
|
91 |
+
<th>SAVINGS</th>
|
92 |
<th>Annual</th>
|
93 |
<th>Monthly</th>
|
94 |
</tr>
|
style.css
CHANGED
@@ -159,63 +159,70 @@
|
|
159 |
}
|
160 |
|
161 |
}
|
162 |
-
|
163 |
/* 기본 테이블 스타일 */
|
164 |
.table-container {
|
165 |
max-width: 100%;
|
166 |
overflow-x: auto;
|
167 |
border: 1px solid #ddd;
|
168 |
-
|
169 |
|
170 |
-
|
171 |
width: 100%;
|
172 |
border-collapse: collapse;
|
173 |
table-layout: auto;
|
174 |
-
|
175 |
|
176 |
-
|
177 |
-
|
178 |
padding: 8px 16px;
|
179 |
border: 1px solid #ddd;
|
180 |
background: #fff;
|
181 |
/* white-space: nowrap; */
|
182 |
text-align: left;
|
183 |
text-transform: uppercase;
|
184 |
-
|
185 |
|
186 |
-
|
187 |
background: #f9f9f9;
|
188 |
-
|
189 |
-
|
190 |
|
191 |
-
|
192 |
-
|
193 |
position: sticky;
|
194 |
left: 0;
|
195 |
background: #f9f9f9;
|
196 |
z-index: 2;
|
197 |
-
|
198 |
|
199 |
-
|
200 |
-
|
|
|
|
|
|
|
|
|
|
|
201 |
.table-container {
|
202 |
-
|
203 |
}
|
204 |
|
205 |
.table-container th,
|
206 |
.table-container td {
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
}
|
211 |
|
212 |
.table-container thead {
|
213 |
-
|
214 |
}
|
215 |
|
216 |
.table-container th:first-child,
|
217 |
.table-container td:first-child {
|
218 |
-
|
219 |
}
|
220 |
-
|
221 |
-
|
|
|
|
|
|
|
|
|
|
159 |
}
|
160 |
|
161 |
}
|
|
|
162 |
/* 기본 테이블 스타일 */
|
163 |
.table-container {
|
164 |
max-width: 100%;
|
165 |
overflow-x: auto;
|
166 |
border: 1px solid #ddd;
|
167 |
+
}
|
168 |
|
169 |
+
.table-container table {
|
170 |
width: 100%;
|
171 |
border-collapse: collapse;
|
172 |
table-layout: auto;
|
173 |
+
}
|
174 |
|
175 |
+
.table-container th,
|
176 |
+
.table-container td {
|
177 |
padding: 8px 16px;
|
178 |
border: 1px solid #ddd;
|
179 |
background: #fff;
|
180 |
/* white-space: nowrap; */
|
181 |
text-align: left;
|
182 |
text-transform: uppercase;
|
183 |
+
}
|
184 |
|
185 |
+
.table-container thead {
|
186 |
background: #f9f9f9;
|
187 |
+
}
|
|
|
188 |
|
189 |
+
.table-container th:first-child,
|
190 |
+
.table-container td:first-child {
|
191 |
position: sticky;
|
192 |
left: 0;
|
193 |
background: #f9f9f9;
|
194 |
z-index: 2;
|
195 |
+
}
|
196 |
|
197 |
+
/* 마우스 오버 효과 */
|
198 |
+
.table-container tr:hover td {
|
199 |
+
background: #f1f1f1;
|
200 |
+
}
|
201 |
+
|
202 |
+
/* 다크 모드 */
|
203 |
+
@media (prefers-color-scheme: dark) {
|
204 |
.table-container {
|
205 |
+
border: 1px solid #444;
|
206 |
}
|
207 |
|
208 |
.table-container th,
|
209 |
.table-container td {
|
210 |
+
border: 1px solid #444;
|
211 |
+
background: #333;
|
212 |
+
color: #ccc;
|
213 |
}
|
214 |
|
215 |
.table-container thead {
|
216 |
+
background: #444;
|
217 |
}
|
218 |
|
219 |
.table-container th:first-child,
|
220 |
.table-container td:first-child {
|
221 |
+
background: #444;
|
222 |
}
|
223 |
+
|
224 |
+
/* 다크 모드 마우스 오버 효과 */
|
225 |
+
.table-container tr:hover td {
|
226 |
+
background: #555;
|
227 |
+
}
|
228 |
+
}
|