Delete modules
Browse files- modules/.DS_Store +0 -0
- modules/__init__.py +0 -0
- modules/compare_stock_prices.py +0 -109
- modules/cost_averaging.py +0 -122
- modules/dollar_cost_averaging.py +0 -101
- modules/portfolio_rebalancing.py +0 -241
- modules/rebalancing.py +0 -297
- modules/retirement_planning.py +0 -222
- modules/share_price_trend.py +0 -128
- modules/utils.py +0 -175
modules/.DS_Store
DELETED
Binary file (6.15 kB)
|
|
modules/__init__.py
DELETED
File without changes
|
modules/compare_stock_prices.py
DELETED
@@ -1,109 +0,0 @@
|
|
1 |
-
import io
|
2 |
-
import matplotlib.pyplot as plt
|
3 |
-
import FinanceDataReader as fdr
|
4 |
-
import pandas as pd
|
5 |
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
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]
|
24 |
-
try:
|
25 |
-
prices = future.result()
|
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():
|
90 |
-
html_table += f"<th>{stock_code.upper()}</th>"
|
91 |
-
html_table += "</tr></thead><tbody>"
|
92 |
-
|
93 |
-
dates = stock_prices[list(stock_prices.keys())[0]].index[::-1]
|
94 |
-
for date in dates:
|
95 |
-
html_table += f"<tr><td>{date.strftime('%Y-%m-%d')}</td>"
|
96 |
-
for stock_code in stock_prices.keys():
|
97 |
-
html_table += f"<td>{stock_prices[stock_code][date]:,.2f}</td>"
|
98 |
-
html_table += "</tr>"
|
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 |
-
# 예시 사용 방법
|
106 |
-
# stock_codes = "AAPL,MSFT,GOOGL"
|
107 |
-
# days = 30
|
108 |
-
# result = compare_stock_prices(stock_codes, days)
|
109 |
-
# print(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/cost_averaging.py
DELETED
@@ -1,122 +0,0 @@
|
|
1 |
-
from modules.utils import load_css
|
2 |
-
|
3 |
-
def cost_averaging(old_avg_price, old_quantity, new_price, new_quantity):
|
4 |
-
# 입력값을 숫자로 변환 (각 입력값이 None일 경우 0.0으로 설정)
|
5 |
-
old_avg_price = float(old_avg_price) if old_avg_price else 0.0
|
6 |
-
old_quantity = float(old_quantity) if old_quantity else 0.0
|
7 |
-
new_price = float(new_price) if new_price else 0.0
|
8 |
-
new_quantity = float(new_quantity) if new_quantity else 0.0
|
9 |
-
|
10 |
-
# 현재 투자 금액 계산 (이전 평균 가격 * 이전 수량)
|
11 |
-
current_investment = old_avg_price * old_quantity
|
12 |
-
# 추가 투자 금액 계산 (새 가격 * 새 수량)
|
13 |
-
additional_investment = new_price * new_quantity
|
14 |
-
# 총 투자 금액 계산 (현재 투자 금액 + 추가 투자 금액)
|
15 |
-
total_investment = current_investment + additional_investment
|
16 |
-
# 총 주식 수 계산 (이전 수량 + 새 수량)
|
17 |
-
total_quantity = old_quantity + new_quantity
|
18 |
-
# 새 평균 가격 계산 (총 투자 금액 / 총 주식 수)
|
19 |
-
new_avg_price = total_investment / total_quantity if total_quantity != 0 else 0.0
|
20 |
-
# 이전 수익률 계산 (이전 평균 가격을 기준으로)
|
21 |
-
old_return = (new_price / old_avg_price - 1) * 100 if old_avg_price != 0 else 0.0
|
22 |
-
# 새로운 수익률 계산 (새 가격 / 새 평균 가격 - 1) * 100
|
23 |
-
new_return = (new_price / new_avg_price - 1) * 100 if new_avg_price != 0 else 0.0
|
24 |
-
|
25 |
-
# 새 평균 가격, 총 수량, 총 투자 금액, 새로운 수익률, 추가 투자 금액, 이전 수익률 반환
|
26 |
-
return new_avg_price, total_quantity, total_investment, new_return, additional_investment, old_return
|
27 |
-
|
28 |
-
def gradio_cost_averaging(old_avg_price, old_quantity, new_price, new_quantity):
|
29 |
-
css = load_css()
|
30 |
-
|
31 |
-
# 입력값을 숫자로 변환
|
32 |
-
old_avg_price = float(old_avg_price) if old_avg_price else 0.0
|
33 |
-
old_quantity = float(old_quantity) if old_quantity else 0.0
|
34 |
-
new_price = float(new_price) if new_price else 0.0
|
35 |
-
new_quantity = float(new_quantity) if new_quantity else 0.0
|
36 |
-
|
37 |
-
# 평균 가격, 총 수량, 총 투자 금액, 수익률 및 추가 투자 금액 계산
|
38 |
-
new_avg_price, total_quantity, total_investment, new_return, additional_investment, old_return = cost_averaging(old_avg_price, old_quantity, new_price, new_quantity)
|
39 |
-
|
40 |
-
# 수익률에 따른 클래스 설정
|
41 |
-
new_return_class = ""
|
42 |
-
old_return_class = ""
|
43 |
-
if new_return > 0:
|
44 |
-
new_return_class = f"<span style='color: #4caf50; font-weight: bold;'>{new_return:+,.2f}%</span>"
|
45 |
-
elif new_return < 0:
|
46 |
-
new_return_class = f"<span style='color: #f44336; font-weight: bold;'>{new_return:,.2f}%</span>"
|
47 |
-
else:
|
48 |
-
new_return_class = f"<span><strong>0</strong></span>"
|
49 |
-
|
50 |
-
if old_return > 0:
|
51 |
-
old_return_class = f"<span style='color: #4caf50; font-weight: bold;'>{old_return:+,.2f}%</span>"
|
52 |
-
elif old_return < 0:
|
53 |
-
old_return_class = f"<span style='color: #f44336; font-weight: bold;'>{old_return:,.2f}%</span>"
|
54 |
-
else:
|
55 |
-
old_return_class = f"<span><strong>0</strong></span>"
|
56 |
-
|
57 |
-
# HTML 결과 생성
|
58 |
-
result_html = css + f"""
|
59 |
-
<div class="wrap-text" style="box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1); border-radius: 0.5rem; padding: 3rem; position: relative; width: 100%; padding: 1.5rem;">
|
60 |
-
<div>
|
61 |
-
<div style="margin-bottom: 1.5rem;">
|
62 |
-
<!-- 이전 수익률 표시 -->
|
63 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Old Return</div>
|
64 |
-
<div style="font-size: 1.5rem;">
|
65 |
-
{old_return_class}
|
66 |
-
</div>
|
67 |
-
<hr style="margin: 1.5rem 0;">
|
68 |
-
</div>
|
69 |
-
</div>
|
70 |
-
<div>
|
71 |
-
<div style="margin-bottom: 1.5rem;">
|
72 |
-
<!-- 새로운 수익률 표시 -->
|
73 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">New Return</div>
|
74 |
-
<div style="font-size: 1.5rem;">
|
75 |
-
{new_return_class}
|
76 |
-
</div>
|
77 |
-
<hr style="margin: 1.5rem 0;">
|
78 |
-
</div>
|
79 |
-
</div>
|
80 |
-
<div>
|
81 |
-
<div style="margin-bottom: 1.5rem;">
|
82 |
-
<!-- 추가 투자 금액 표시 -->
|
83 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Additional Investment</div>
|
84 |
-
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
85 |
-
<span style='color: #1678fb'>{additional_investment:,.0f}</span>
|
86 |
-
</div>
|
87 |
-
<hr style="margin: 1.5rem 0;">
|
88 |
-
</div>
|
89 |
-
</div>
|
90 |
-
<div>
|
91 |
-
<div style="margin-bottom: 1.5rem;">
|
92 |
-
<!-- 평균 가격 표시 -->
|
93 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Average Price</div>
|
94 |
-
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
95 |
-
<span style='color: #1678fb'>{new_avg_price:,.0f}</span>
|
96 |
-
</div>
|
97 |
-
<hr style="margin: 1.5rem 0;">
|
98 |
-
</div>
|
99 |
-
</div>
|
100 |
-
<div>
|
101 |
-
<div style="margin-bottom: 1.5rem;">
|
102 |
-
<!-- 총 수량 표시 -->
|
103 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Total Quantity</div>
|
104 |
-
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
105 |
-
<span style='color: #1678fb'>{total_quantity:,.0f}</span>
|
106 |
-
</div>
|
107 |
-
<hr style="margin: 1.5rem 0;">
|
108 |
-
</div>
|
109 |
-
</div>
|
110 |
-
<div>
|
111 |
-
<div style="margin-bottom: 1.5rem;">
|
112 |
-
<!-- 총 투자 금액 표시 -->
|
113 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Total Investment</div>
|
114 |
-
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
115 |
-
<span style='color: #1678fb'>{total_investment:,.0f}</span>
|
116 |
-
</div>
|
117 |
-
<hr style="margin: 1.5rem 0;">
|
118 |
-
</div>
|
119 |
-
</div>
|
120 |
-
</div>
|
121 |
-
"""
|
122 |
-
return result_html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/dollar_cost_averaging.py
DELETED
@@ -1,101 +0,0 @@
|
|
1 |
-
from modules.utils import load_css
|
2 |
-
|
3 |
-
def calculate_dollar_cost_averaging(old_avg_price, old_quantity, new_price, new_quantity):
|
4 |
-
# 입력값을 숫자로 변환 (각 입력값이 None일 경우 0.0으로 설정)
|
5 |
-
old_avg_price = float(old_avg_price) if old_avg_price else 0.0
|
6 |
-
old_quantity = float(old_quantity) if old_quantity else 0.0
|
7 |
-
new_price = float(new_price) if new_price else 0.0
|
8 |
-
new_quantity = float(new_quantity) if new_quantity else 0.0
|
9 |
-
|
10 |
-
# 현재 투자 금액 계산 (이전 평균 가격 * 이전 수량)
|
11 |
-
current_investment = old_avg_price * old_quantity
|
12 |
-
# 추가 투자 금액 계산 (새 가격 * 새 수량)
|
13 |
-
additional_investment = new_price * new_quantity
|
14 |
-
# 총 투자 금액 계산 (현재 투자 금액 + 추가 투자 금액)
|
15 |
-
total_investment = current_investment + additional_investment
|
16 |
-
# 총 주식 수 계산 (이전 수량 + 새 수량)
|
17 |
-
total_quantity = old_quantity + new_quantity
|
18 |
-
# 새 평균 가격 계산 (총 투자 금액 / 총 주식 수)
|
19 |
-
new_avg_price = total_investment / total_quantity if total_quantity != 0 else 0.0
|
20 |
-
# 이전 수익률 계산 (이전 평균 가격을 기준으로)
|
21 |
-
old_return = (new_price / old_avg_price - 1) * 100 if old_avg_price != 0 else 0.0
|
22 |
-
# 새로운 수익률 계산 (새 가격 / 새 평균 가격 - 1) * 100
|
23 |
-
new_return = (new_price / new_avg_price - 1) * 100 if new_avg_price != 0 else 0.0
|
24 |
-
|
25 |
-
# 새 평균 가격, 총 수량, 총 투자 금액, 새로운 수익률, 추가 투자 금액, 이전 수익률 반환
|
26 |
-
return new_avg_price, total_quantity, total_investment, new_return, additional_investment, old_return
|
27 |
-
|
28 |
-
def dollar_cost_averaging(old_avg_price, old_quantity, new_price, new_quantity):
|
29 |
-
css = load_css()
|
30 |
-
|
31 |
-
# 입력값을 숫자로 변환
|
32 |
-
old_avg_price = float(old_avg_price) if old_avg_price else 0.0
|
33 |
-
old_quantity = float(old_quantity) if old_quantity else 0.0
|
34 |
-
new_price = float(new_price) if new_price else 0.0
|
35 |
-
new_quantity = float(new_quantity) if new_quantity else 0.0
|
36 |
-
|
37 |
-
# 평균 가격, 총 수량, 총 투자 금액, 수익률 및 추가 투자 금액 계산
|
38 |
-
new_avg_price, total_quantity, total_investment, new_return, additional_investment, old_return = calculate_dollar_cost_averaging(old_avg_price, old_quantity, new_price, new_quantity)
|
39 |
-
|
40 |
-
# 수익률에 따른 클래스 설정
|
41 |
-
emoji_return = ""
|
42 |
-
new_return_class = ""
|
43 |
-
old_return_class = ""
|
44 |
-
if new_return > old_return:
|
45 |
-
emoji_return = "💧"
|
46 |
-
elif new_return < old_return:
|
47 |
-
emoji_return = "🔥"
|
48 |
-
else:
|
49 |
-
emoji_return = ""
|
50 |
-
|
51 |
-
if new_return > 0:
|
52 |
-
new_return_class = f"<span style='color: #4caf50; font-weight: bold;'>{new_return:+,.2f}%</span>"
|
53 |
-
elif new_return < 0:
|
54 |
-
new_return_class = f"<span style='color: #f44336; font-weight: bold;'>{new_return:,.2f}%</span>"
|
55 |
-
else:
|
56 |
-
new_return_class = f"<span><strong>0</strong></span>"
|
57 |
-
|
58 |
-
if old_return > 0:
|
59 |
-
old_return_class = f"<span style='color: #4caf50; font-weight: bold;'>{old_return:+,.2f}%</span>"
|
60 |
-
elif old_return < 0:
|
61 |
-
old_return_class = f"<span style='color: #f44336; font-weight: bold;'>{old_return:,.2f}%</span>"
|
62 |
-
else:
|
63 |
-
old_return_class = f"<span><strong>0</strong></span>"
|
64 |
-
|
65 |
-
# HTML 결과 생성
|
66 |
-
result_html = css + f"""
|
67 |
-
<div class="wrap-text">
|
68 |
-
<div>
|
69 |
-
<div style="margin-bottom: 1.5rem;">
|
70 |
-
<!-- 새로운 수익률 표시 -->
|
71 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">{emoji_return}</div>
|
72 |
-
<div style="font-size: 1.5rem;">
|
73 |
-
{old_return_class} ➜ {new_return_class}
|
74 |
-
</div>
|
75 |
-
<hr style="margin: 1.5rem 0;">
|
76 |
-
</div>
|
77 |
-
</div>
|
78 |
-
<div>
|
79 |
-
<div style="margin-bottom: 1.5rem;">
|
80 |
-
<!-- 평균 가격 표시 -->
|
81 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Average Price</div>
|
82 |
-
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
83 |
-
<span style='color: #1678fb'>{new_avg_price:,.0f}</span>
|
84 |
-
</div>
|
85 |
-
<hr style="margin: 1.5rem 0;">
|
86 |
-
</div>
|
87 |
-
</div>
|
88 |
-
<div>
|
89 |
-
<div style="margin-bottom: 1.5rem;">
|
90 |
-
<!-- 추가 투자 금액 표시 -->
|
91 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Transaction</div>
|
92 |
-
<div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
|
93 |
-
<span style='color: #1678fb'>{additional_investment:,.0f}</span>
|
94 |
-
</div>
|
95 |
-
<hr style="margin: 1.5rem 0;">
|
96 |
-
</div>
|
97 |
-
</div>
|
98 |
-
|
99 |
-
</div>
|
100 |
-
"""
|
101 |
-
return result_html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/portfolio_rebalancing.py
DELETED
@@ -1,241 +0,0 @@
|
|
1 |
-
import requests
|
2 |
-
import pytz
|
3 |
-
import math
|
4 |
-
import pandas as pd
|
5 |
-
import FinanceDataReader as fdr
|
6 |
-
import yfinance as yf
|
7 |
-
from datetime import datetime
|
8 |
-
from concurrent.futures import ThreadPoolExecutor
|
9 |
-
from modules.utils import load_css, get_currency_symbol, format_quantity
|
10 |
-
|
11 |
-
# 주어진 입력을 구문 분석하여 주식 보유량, 현금 금액 및 현금 비율을 계산하는 함수
|
12 |
-
def parse_input(holdings, cash_amount, cash_ratio):
|
13 |
-
lines = holdings.strip().split(',')
|
14 |
-
stock_inputs = []
|
15 |
-
total_target_weight = 0
|
16 |
-
|
17 |
-
for line in lines:
|
18 |
-
parts = line.split()
|
19 |
-
if len(parts) == 4:
|
20 |
-
stock_code, currency_code, quantity_expr, target_weight_expr = parts
|
21 |
-
quantity = math.floor(eval(quantity_expr.replace(' ', '')))
|
22 |
-
target_weight = eval(target_weight_expr.replace(' ', ''))
|
23 |
-
target_ratio = (1 - cash_ratio / 100) * target_weight
|
24 |
-
stock_inputs.append((currency_code, stock_code, quantity, target_weight, target_ratio))
|
25 |
-
total_target_weight += target_weight
|
26 |
-
|
27 |
-
cash_amount = math.floor(cash_amount) if cash_amount else 0
|
28 |
-
main_currency_cash_inputs = {'amount': cash_amount, 'target_weight': cash_ratio / 100.0}
|
29 |
-
|
30 |
-
stock_total_weight = total_target_weight
|
31 |
-
|
32 |
-
for i in range(len(stock_inputs)):
|
33 |
-
stock_inputs[i] = (stock_inputs[i][0], stock_inputs[i][1], stock_inputs[i][2], stock_inputs[i][3], (1 - main_currency_cash_inputs['target_weight']) * stock_inputs[i][3] / stock_total_weight)
|
34 |
-
|
35 |
-
return stock_inputs, main_currency_cash_inputs
|
36 |
-
|
37 |
-
# 주어진 통화 코드와 메인 통화 간의 환율을 가져오는 함수
|
38 |
-
def get_portfolio_exchange_rate(currency_code, main_currency):
|
39 |
-
if currency_code.lower() == main_currency.lower():
|
40 |
-
return 1.0
|
41 |
-
|
42 |
-
ticker = f"{currency_code.upper()}{main_currency.upper()}=X"
|
43 |
-
data = yf.download(ticker, period='1d', progress=False) # progress=False 추가
|
44 |
-
if not data.empty:
|
45 |
-
return data['Close'].iloc[0]
|
46 |
-
else:
|
47 |
-
raise ValueError("Failed to retrieve exchange rate data.")
|
48 |
-
|
49 |
-
# 주어진 주식 코드와 통화 코드에 대해 환율을 반영한 주식 가격을 가져오는 함수
|
50 |
-
def get_portfolio_exchange_reflected_stock_price(stock_code, currency_code, main_currency):
|
51 |
-
new_price = get_portfolio_current_stock_price(stock_code)
|
52 |
-
exchange_rate = get_portfolio_exchange_rate(currency_code, main_currency)
|
53 |
-
return math.floor(new_price * exchange_rate)
|
54 |
-
|
55 |
-
# 주어진 주식 코드의 현재 주식 가격을 가져오는 함수
|
56 |
-
def get_portfolio_current_stock_price(stock_code):
|
57 |
-
df = fdr.DataReader(stock_code)
|
58 |
-
return df['Close'].iloc[-1]
|
59 |
-
|
60 |
-
# 주어진 주식 입력 및 현금 입력을 바탕으로 포트폴리오를 구축하는 함수
|
61 |
-
def build_portfolio(stock_inputs, main_currency_cash_inputs, main_currency):
|
62 |
-
portfolio = {}
|
63 |
-
target_weights = {}
|
64 |
-
|
65 |
-
with ThreadPoolExecutor() as executor:
|
66 |
-
results = executor.map(lambda x: (x[1], get_portfolio_exchange_reflected_stock_price(x[1], x[0], main_currency), x[2], x[3], x[4], x[0]), stock_inputs)
|
67 |
-
|
68 |
-
for stock_code, new_price, quantity, target_weight, target_ratio, currency_code in results:
|
69 |
-
portfolio[stock_code] = {'quantity': quantity, 'price': new_price, 'target_weight': target_weight, 'currency': currency_code}
|
70 |
-
target_weights[stock_code] = target_ratio
|
71 |
-
|
72 |
-
return portfolio, target_weights, main_currency_cash_inputs
|
73 |
-
|
74 |
-
# 포트폴리오 재조정 정보를 가져오는 함수
|
75 |
-
def get_portfolio_rebalancing_info(portfolio, target_weights, main_currency_cash_inputs, main_currency):
|
76 |
-
css = load_css()
|
77 |
-
|
78 |
-
current_time = datetime.now().strftime("%b-%d-%Y")
|
79 |
-
|
80 |
-
total_value = sum(stock['price'] * stock['quantity'] for stock in portfolio.values()) + main_currency_cash_inputs['amount']
|
81 |
-
total_new_stock_value = 0
|
82 |
-
total_trade_value = 0
|
83 |
-
adjustments = []
|
84 |
-
|
85 |
-
currency_symbol = get_currency_symbol(main_currency)
|
86 |
-
|
87 |
-
# 포트폴리오 정보 생성
|
88 |
-
portfolio_info = css + f"""
|
89 |
-
<div class="wrap-text" style="box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1); border-radius: 0.5rem; padding: 3rem; position: relative; width: 100%; padding: 1.5rem;">
|
90 |
-
<div>
|
91 |
-
<div style="margin-bottom: 1.5rem;">
|
92 |
-
<!-- 전체 평가금액 표시 -->
|
93 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Market Value</div>
|
94 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{total_value:,.0f}</span>
|
95 |
-
As of {current_time}
|
96 |
-
<hr style="margin: 1.5rem 0;">
|
97 |
-
</div>
|
98 |
-
</div>
|
99 |
-
</div>
|
100 |
-
"""
|
101 |
-
|
102 |
-
# 현재 비율 및 가치를 계산
|
103 |
-
current_weights = {stock_code: (stock['price'] * stock['quantity'] / total_value) * 100 for stock_code, stock in portfolio.items()}
|
104 |
-
current_values = {stock_code: stock['price'] * stock['quantity'] for stock_code, stock in portfolio.items()}
|
105 |
-
|
106 |
-
# 현금을 현재 비율 및 가치에 포함
|
107 |
-
current_weights['CASH'] = (main_currency_cash_inputs['amount'] / total_value) * 100
|
108 |
-
current_values['CASH'] = main_currency_cash_inputs['amount']
|
109 |
-
|
110 |
-
# 현재 비율을 기준으로 주식을 내림차순으로 정렬
|
111 |
-
# sorted_stocks = sorted(current_weights.items(), key=lambda x: x[1], reverse=True)
|
112 |
-
sorted_stocks = current_weights.items()
|
113 |
-
|
114 |
-
# 현재 보유량 및 가치 섹션 표시
|
115 |
-
current_info_html = "<h3>Your Portfolio Holdings</h3><div class='table-container'><table>"
|
116 |
-
current_info_html += "<thead><tr><th>Stock Code</th><th>Current Weight (%)</th><th>Current Value</th></tr></thead><tbody>"
|
117 |
-
for stock_code, weight in sorted_stocks:
|
118 |
-
current_info_html += (
|
119 |
-
f"<tr>"
|
120 |
-
f"<td>{stock_code.upper()}</td>"
|
121 |
-
f"<td>{weight:.1f}%</td>"
|
122 |
-
f"<td>{currency_symbol}{current_values[stock_code]:,.0f}</td>"
|
123 |
-
f"</tr>"
|
124 |
-
)
|
125 |
-
current_info_html += "</tbody></table></div><br>"
|
126 |
-
|
127 |
-
# 조정 항목 생성
|
128 |
-
for stock_code, stock_data in portfolio.items():
|
129 |
-
current_value = stock_data['price'] * stock_data['quantity']
|
130 |
-
target_value = total_value * target_weights.get(stock_code, 0)
|
131 |
-
difference = target_value - current_value
|
132 |
-
trade_quantity = math.floor(difference / stock_data['price']) if difference > 0 else -math.ceil(-difference / stock_data['price'])
|
133 |
-
new_quantity = trade_quantity + stock_data['quantity']
|
134 |
-
new_value = new_quantity * stock_data['price']
|
135 |
-
trade_value = trade_quantity * stock_data['price']
|
136 |
-
total_trade_value += abs(trade_value)
|
137 |
-
total_new_stock_value += new_value
|
138 |
-
current_value_pct = (current_value / total_value) * 100
|
139 |
-
new_value_pct = (new_value / total_value) * 100
|
140 |
-
|
141 |
-
adjustments.append((difference, current_value, target_value, current_value_pct, trade_quantity, stock_code, stock_data['price'], new_value, trade_value, stock_data['quantity'], new_quantity, target_weights[stock_code], new_value_pct, stock_data['target_weight'], stock_data['currency']))
|
142 |
-
|
143 |
-
# 현금에 대한 조정 항목 생성
|
144 |
-
main_currency_new_amount = total_value - total_new_stock_value
|
145 |
-
main_currency_target_value = total_value * main_currency_cash_inputs['target_weight']
|
146 |
-
main_currency_difference = main_currency_new_amount - main_currency_cash_inputs['amount']
|
147 |
-
trade_quantity = main_currency_difference
|
148 |
-
new_quantity = main_currency_cash_inputs['amount'] + trade_quantity
|
149 |
-
new_value = new_quantity
|
150 |
-
trade_value = trade_quantity
|
151 |
-
current_value = main_currency_cash_inputs['amount']
|
152 |
-
current_value_pct = (current_value / total_value) * 100
|
153 |
-
new_value_pct = (new_value / total_value) * 100
|
154 |
-
|
155 |
-
adjustments.append((main_currency_difference, current_value, main_currency_target_value, current_value_pct, trade_quantity, 'CASH', 1, new_value, trade_value, main_currency_cash_inputs['amount'], new_quantity, main_currency_cash_inputs['target_weight'], new_value_pct, '', 'main_currency'))
|
156 |
-
|
157 |
-
# 통화별 포트폴리오 요약 생성
|
158 |
-
currency_totals = {stock_data['currency']: {'amount': 0, 'weight': 0} for stock_data in portfolio.values()}
|
159 |
-
|
160 |
-
for stock_code, stock_data in portfolio.items():
|
161 |
-
currency = stock_data['currency']
|
162 |
-
current_value = stock_data['price'] * stock_data['quantity']
|
163 |
-
currency_totals[currency]['amount'] += current_value
|
164 |
-
currency_totals[currency]['weight'] += current_value / total_value
|
165 |
-
|
166 |
-
currency_totals['CASH'] = {'amount': main_currency_cash_inputs['amount'], 'weight': main_currency_cash_inputs['amount'] / total_value}
|
167 |
-
# sorted_currencies = sorted(currency_totals.items(), key=lambda x: x[1]['weight'], reverse=True)
|
168 |
-
sorted_currencies = currency_totals.items()
|
169 |
-
|
170 |
-
|
171 |
-
# 통화별 요약 테이블 생성
|
172 |
-
currency_table = "<h3>Your Portfolio by Currency</h3><div class='table-container wrap-text'><table>"
|
173 |
-
currency_table += "<thead><tr><th>Currency</th><th>Total Weight (%)</th><th>Total Value</th></tr></thead><tbody>"
|
174 |
-
|
175 |
-
for currency, data in sorted_currencies:
|
176 |
-
currency_table += (
|
177 |
-
f"<tr>"
|
178 |
-
f"<td>{currency.upper()}</td>"
|
179 |
-
f"<td>{data['weight'] * 100:.1f}%</td>"
|
180 |
-
f"<td>{currency_symbol}{data['amount']:,}</td>"
|
181 |
-
f"</tr>"
|
182 |
-
)
|
183 |
-
|
184 |
-
currency_table += "</tbody></table></div><br>"
|
185 |
-
|
186 |
-
# 재조정 분석 테이블 생성
|
187 |
-
result_message = portfolio_info + current_info_html + currency_table + "<h3>Re-Balancing Analysis</h3><div class='table-container wrap-text'><table>"
|
188 |
-
result_message += "<thead><tr><th>Stock Code</th><th>Current Weight (%)</th><th>Target Weight</th><th>Target Ratio (%)</th><th>Buy or Sell?</th><th>Trade Amount</th><th>Current Price per Share</th><th>Estimated # of<br> Shares to Buy or Sell</th><th>Quantity of Units</th><th>Market Value</th><th>% Asset Allocation</th></tr></thead><tbody>"
|
189 |
-
|
190 |
-
for adj in adjustments:
|
191 |
-
difference, current_value, target_value, current_value_pct, trade_quantity, stock_code, price, new_value, trade_value, old_quantity, new_quantity, target_ratio, new_value_pct, target_weight, currency = adj
|
192 |
-
Buy_or_Sell = ""
|
193 |
-
if trade_quantity > 0:
|
194 |
-
Buy_or_Sell = f"<span class='buy-sell buy'>Buy</span>"
|
195 |
-
elif trade_quantity < 0:
|
196 |
-
Buy_or_Sell = f"<span class='buy-sell sell'>Sell</span>"
|
197 |
-
else:
|
198 |
-
Buy_or_Sell = f"<span></span>"
|
199 |
-
|
200 |
-
current_value_pct_str = f"{current_value_pct:.1f}%"
|
201 |
-
target_weight_str = f"<span class='highlight-edit'>{target_weight}</span>" if stock_code != 'CASH' else ''
|
202 |
-
target_ratio_str = f"<span class='highlight-edit'>{target_ratio * 100:.1f}%</span>" if stock_code == 'CASH' else f"{target_ratio * 100:.1f}%"
|
203 |
-
trade_value_str = f"<span class='highlight-sky'>{format_quantity(trade_value)}</span>" if trade_value != 0 else ''
|
204 |
-
price_str = f"{currency_symbol}{price:,.0f}" if stock_code != 'CASH' else ''
|
205 |
-
trade_quantity_str = (
|
206 |
-
f"<span class='highlight-sky'>{format_quantity(trade_quantity)}</span>"
|
207 |
-
if stock_code != 'CASH' and trade_value != 0 else ''
|
208 |
-
)
|
209 |
-
old_quantity_str = f"{old_quantity:,.0f} → {new_quantity:,.0f}" if stock_code != 'CASH' else ''
|
210 |
-
new_value_str = f"{currency_symbol}{new_value:,.0f}"
|
211 |
-
new_value_pct_str = f"{new_value_pct:.1f}%"
|
212 |
-
|
213 |
-
result_message += (
|
214 |
-
f"<tr>"
|
215 |
-
f"<td>{stock_code.upper()}</td>"
|
216 |
-
f"<td>{current_value_pct_str}</td>"
|
217 |
-
f"<td>{target_weight_str}</td>"
|
218 |
-
f"<td>{target_ratio_str}</td>"
|
219 |
-
f"<td>{Buy_or_Sell}</td>"
|
220 |
-
f"<td>{trade_value_str}</td>"
|
221 |
-
f"<td>{price_str}</td>"
|
222 |
-
f"<td>{trade_quantity_str}</td>"
|
223 |
-
f"<td>{old_quantity_str}</td>"
|
224 |
-
f"<td>{new_value_str}</td>"
|
225 |
-
f"<td>{new_value_pct_str}</td>"
|
226 |
-
f"</tr>"
|
227 |
-
)
|
228 |
-
|
229 |
-
result_message += "</tbody></table></div>"
|
230 |
-
|
231 |
-
return result_message
|
232 |
-
|
233 |
-
# 포트폴리오 재조정 도구의 주요 함수
|
234 |
-
def portfolio_rebalancing_tool(main_currency, holdings, cash_amount, cash_ratio):
|
235 |
-
try:
|
236 |
-
stock_inputs, main_currency_cash_inputs = parse_input(holdings, cash_amount, cash_ratio)
|
237 |
-
portfolio, target_weights, main_currency_cash_inputs = build_portfolio(stock_inputs, main_currency_cash_inputs, main_currency)
|
238 |
-
result = get_portfolio_rebalancing_info(portfolio, target_weights, main_currency_cash_inputs, main_currency)
|
239 |
-
return result
|
240 |
-
except Exception as e:
|
241 |
-
return str(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/rebalancing.py
DELETED
@@ -1,297 +0,0 @@
|
|
1 |
-
import math
|
2 |
-
import FinanceDataReader as fdr
|
3 |
-
import yfinance as yf
|
4 |
-
from concurrent.futures import ThreadPoolExecutor
|
5 |
-
from modules.utils import load_css, get_currency_symbol, format_quantity, plot_donut_chart, format_value, current_time
|
6 |
-
from collections import defaultdict
|
7 |
-
|
8 |
-
def parse_input(holdings, cash_amount):
|
9 |
-
try:
|
10 |
-
lines = holdings.strip().split(',')
|
11 |
-
stock_inputs = []
|
12 |
-
total_target_ratio = 0
|
13 |
-
|
14 |
-
for line in lines:
|
15 |
-
parts = line.split()
|
16 |
-
if len(parts) == 4:
|
17 |
-
stock_code, currency_code, quantity_expr, target_ratio_expr = parts
|
18 |
-
quantity = float(eval(quantity_expr.replace(' ', '')))
|
19 |
-
if target_ratio_expr.strip() == '[]':
|
20 |
-
target_ratio = 0
|
21 |
-
else:
|
22 |
-
target_ratio = float(eval(target_ratio_expr.strip('[]').replace(' ', '')))
|
23 |
-
|
24 |
-
stock_inputs.append((currency_code.upper(), stock_code, quantity, target_ratio))
|
25 |
-
total_target_ratio += target_ratio
|
26 |
-
|
27 |
-
return stock_inputs, cash_amount
|
28 |
-
except Exception as e:
|
29 |
-
raise ValueError(f"Input parsing error: {e}")
|
30 |
-
|
31 |
-
def get_portfolio_exchange_rate(currency_code, main_currency):
|
32 |
-
try:
|
33 |
-
if currency_code.lower() == main_currency.lower():
|
34 |
-
return 1.0
|
35 |
-
|
36 |
-
ticker = f"{currency_code.upper()}{main_currency.upper()}=X"
|
37 |
-
data = yf.download(ticker, period='1d', progress=False)
|
38 |
-
if not data.empty:
|
39 |
-
return data['Close'].iloc[0]
|
40 |
-
else:
|
41 |
-
raise ValueError("<p style='color: red;'>Failed to retrieve exchange rate data.</p>")
|
42 |
-
except Exception as e:
|
43 |
-
raise ValueError(f"<p style='color: red;'>Exchange rate retrieval error: {e}</p>")
|
44 |
-
|
45 |
-
def get_portfolio_exchange_reflected_stock_price(stock_code, currency_code, main_currency):
|
46 |
-
try:
|
47 |
-
new_price = get_portfolio_current_stock_price(stock_code)
|
48 |
-
exchange_rate = get_portfolio_exchange_rate(currency_code, main_currency)
|
49 |
-
return new_price * exchange_rate
|
50 |
-
except Exception as e:
|
51 |
-
raise ValueError(f"<p style='color: red;'>Exchange reflected stock price error: {e}</p>")
|
52 |
-
|
53 |
-
def get_portfolio_current_stock_price(stock_code):
|
54 |
-
try:
|
55 |
-
df = fdr.DataReader(stock_code)
|
56 |
-
return df['Close'].iloc[-1]
|
57 |
-
except Exception as e:
|
58 |
-
raise ValueError(f"<p style='color: red;'>Current stock price retrieval error: {e}</p>")
|
59 |
-
|
60 |
-
def set_default_ratios_if_zero(target_ratios):
|
61 |
-
total_target_ratio = sum(target_ratios.values())
|
62 |
-
if total_target_ratio == 0:
|
63 |
-
num_stocks = len(target_ratios)
|
64 |
-
default_ratio = 1 / num_stocks
|
65 |
-
return {stock_code: default_ratio for stock_code in target_ratios}
|
66 |
-
return target_ratios
|
67 |
-
|
68 |
-
def build_portfolio(stock_inputs, main_currency):
|
69 |
-
portfolio = {}
|
70 |
-
target_ratios = {}
|
71 |
-
|
72 |
-
with ThreadPoolExecutor() as executor:
|
73 |
-
results = list(executor.map(lambda x: (x[1], get_portfolio_exchange_reflected_stock_price(x[1], x[0], main_currency), x[2], x[3], x[0]), stock_inputs))
|
74 |
-
|
75 |
-
total_value = 0
|
76 |
-
for stock_code, new_price, quantity, target_ratio, currency_code in results:
|
77 |
-
portfolio[stock_code] = {'quantity': quantity, 'price': new_price, 'currency': currency_code}
|
78 |
-
target_ratios[stock_code] = target_ratio
|
79 |
-
total_value += new_price * quantity
|
80 |
-
|
81 |
-
target_ratios = set_default_ratios_if_zero(target_ratios)
|
82 |
-
|
83 |
-
return portfolio, target_ratios, total_value
|
84 |
-
|
85 |
-
def generate_portfolio_info(portfolio, total_value, main_currency):
|
86 |
-
css = load_css()
|
87 |
-
currency_symbol = get_currency_symbol(main_currency)
|
88 |
-
|
89 |
-
# 보유 종목별 총액을 계산합니다.
|
90 |
-
holdings_totals = {
|
91 |
-
stock_code: {
|
92 |
-
'value': stock['price'] * stock['quantity'],
|
93 |
-
'weight': (stock['price'] * stock['quantity'] / total_value)
|
94 |
-
}
|
95 |
-
for stock_code, stock in portfolio.items()
|
96 |
-
}
|
97 |
-
|
98 |
-
# 통화별 총액을 계산합니다.
|
99 |
-
currency_totals = defaultdict(lambda: {'value': 0, 'weight': 0})
|
100 |
-
for stock in portfolio.values():
|
101 |
-
currency = stock['currency']
|
102 |
-
value = stock['price'] * stock['quantity']
|
103 |
-
currency_totals[currency]['value'] += value
|
104 |
-
currency_totals[currency]['weight'] += value / total_value
|
105 |
-
|
106 |
-
# 현재 비중을 사용하여 포트폴리오 트리맵 차트를 생성합니다.
|
107 |
-
currunt_weights = {stock_code: details['weight'] for stock_code, details in holdings_totals.items()}
|
108 |
-
currunt_weights_chart = plot_donut_chart(currunt_weights)
|
109 |
-
|
110 |
-
currency_weights = {currency: details['weight'] for currency, details in currency_totals.items()}
|
111 |
-
currency_weights_chart = plot_donut_chart(currency_weights)
|
112 |
-
|
113 |
-
# HTML 생성
|
114 |
-
portfolio_info = css + f"""
|
115 |
-
<div class="wrap-text">
|
116 |
-
<h3>Your Portfolio Holdings</h3>
|
117 |
-
{currunt_weights_chart}
|
118 |
-
<div class='table-container wrap-text'>
|
119 |
-
<table>
|
120 |
-
<thead>
|
121 |
-
<tr><th>Stock Code</th><th>Current Weight (%)</th><th>Current Value</th></tr>
|
122 |
-
</thead>
|
123 |
-
<tbody>
|
124 |
-
{''.join(
|
125 |
-
f"<tr><td>{stock_code.upper()}</td><td>{details['weight'] * 100:.1f}%</td><td>{currency_symbol}{format_value(details['value'])}</td></tr>"
|
126 |
-
for stock_code, details in holdings_totals.items()
|
127 |
-
)}
|
128 |
-
</tbody>
|
129 |
-
</table>
|
130 |
-
</div>
|
131 |
-
|
132 |
-
<br>
|
133 |
-
</div>
|
134 |
-
"""
|
135 |
-
# <br>
|
136 |
-
# <h3>Your Portfolio by Currency</h3>
|
137 |
-
# {currency_weights_chart}
|
138 |
-
# <br>
|
139 |
-
# <div class='table-container wrap-text'>
|
140 |
-
# <table>
|
141 |
-
# <thead>
|
142 |
-
# <tr><th>Currency</th><th>Total Weight (%)</th><th>Total Value</th></tr>
|
143 |
-
# </thead>
|
144 |
-
# <tbody>
|
145 |
-
# {''.join(
|
146 |
-
# f"<tr><td>{currency.upper()}</td><td>{details['weight']* 100:.1f}%</td><td>{currency_symbol}{format_value(details['value'])}</td></tr>"
|
147 |
-
# for currency, details in currency_totals.items()
|
148 |
-
# )}
|
149 |
-
# </tbody>
|
150 |
-
# </table>
|
151 |
-
# </div>
|
152 |
-
return portfolio_info
|
153 |
-
|
154 |
-
def generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_currency, cash_amount, allow_selling):
|
155 |
-
css = load_css()
|
156 |
-
currency_symbol = get_currency_symbol(main_currency)
|
157 |
-
adjustments = []
|
158 |
-
|
159 |
-
new_total_value = cash_amount + total_value
|
160 |
-
total_target_ratio = sum(target_ratios.values())
|
161 |
-
|
162 |
-
target_ratios = set_default_ratios_if_zero(target_ratios)
|
163 |
-
|
164 |
-
for stock_code, stock_data in portfolio.items():
|
165 |
-
current_value = stock_data['price'] * stock_data['quantity']
|
166 |
-
target_ratio = target_ratios.get(stock_code, 0)
|
167 |
-
target_weight = target_ratio / total_target_ratio
|
168 |
-
target_value = new_total_value * target_weight
|
169 |
-
difference = target_value - current_value
|
170 |
-
|
171 |
-
# Allow selling이 false이고 현금이 음수인 경우 매도를 방지
|
172 |
-
if not allow_selling and (difference < 0 or cash_amount < 0):
|
173 |
-
trade_quantity = 0
|
174 |
-
else:
|
175 |
-
trade_quantity = difference / stock_data['price']
|
176 |
-
|
177 |
-
if trade_quantity > 0 and not allow_selling:
|
178 |
-
trade_quantity = min(trade_quantity, cash_amount / stock_data['price'])
|
179 |
-
|
180 |
-
trade_value = trade_quantity * stock_data['price']
|
181 |
-
new_quantity = trade_quantity + stock_data['quantity']
|
182 |
-
new_value = new_quantity * stock_data['price']
|
183 |
-
|
184 |
-
if trade_value > 0:
|
185 |
-
cash_amount -= trade_value
|
186 |
-
else:
|
187 |
-
cash_amount += abs(trade_value)
|
188 |
-
|
189 |
-
# 각 항목별 Buy_or_Sell 값을 루프 안에서 정의
|
190 |
-
Buy_or_Sell = ""
|
191 |
-
if trade_quantity > 0:
|
192 |
-
Buy_or_Sell = f"<span class='buy-sell buy'>Buy</span>"
|
193 |
-
elif trade_quantity < 0:
|
194 |
-
Buy_or_Sell = f"<span class='buy-sell sell'>Sell</span>"
|
195 |
-
else:
|
196 |
-
Buy_or_Sell = f"<span></span>"
|
197 |
-
|
198 |
-
adjustments.append({
|
199 |
-
'difference': difference,
|
200 |
-
'current_value': current_value,
|
201 |
-
'target_ratio': target_ratio,
|
202 |
-
'current_value_pct': current_value / total_value,
|
203 |
-
'trade_quantity': trade_quantity,
|
204 |
-
'stock_code': stock_code,
|
205 |
-
'price': stock_data['price'],
|
206 |
-
'new_value': new_value,
|
207 |
-
"Buy_or_Sell": Buy_or_Sell,
|
208 |
-
'trade_value': trade_value,
|
209 |
-
'old_quantity': stock_data['quantity'],
|
210 |
-
'new_quantity': new_quantity,
|
211 |
-
'target_weight': target_weight,
|
212 |
-
'currency': stock_data['currency'],
|
213 |
-
'new_value_pct': new_value / new_total_value
|
214 |
-
})
|
215 |
-
|
216 |
-
# HTML 생성
|
217 |
-
rebalancing_analysis = css + f"""
|
218 |
-
<div class="wrap-text">
|
219 |
-
<div style="margin-bottom: 1.5rem;">
|
220 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Re-Balancing Analysis | Your Portfolio Holdings as of {current_time}</div>
|
221 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_value(sum(adj['new_value'] for adj in adjustments))} </span>
|
222 |
-
(After Trades)
|
223 |
-
</div>
|
224 |
-
<div class='table-container wrap-text'>
|
225 |
-
<table>
|
226 |
-
<thead>
|
227 |
-
<tr>
|
228 |
-
<th colspan="1"></th>
|
229 |
-
<th colspan="2" class="header-bg-before">Your Current Portfolio (Before Trades)</th>
|
230 |
-
<th colspan="1"></th>
|
231 |
-
<th colspan="5" style='text-align: center'>Trades to Re-Balance Your Portfolio</th>
|
232 |
-
<th colspan="2" class="header-bg-after">Your Adjusted Portfolio (After Trades)</th>
|
233 |
-
</tr>
|
234 |
-
<tr>
|
235 |
-
<th>Stock Code</th>
|
236 |
-
<th class="header-bg-before">Total Value - {main_currency} {currency_symbol}</th>
|
237 |
-
<th class="header-bg-before">% Asset Allocation</th>
|
238 |
-
<th>Your Target Asset Allocation %</th>
|
239 |
-
<th>Buy or Sell?</th>
|
240 |
-
<th>Trade Amount - {main_currency} {currency_symbol}</th>
|
241 |
-
<th>Current Price per Share - {main_currency} {currency_symbol}</th>
|
242 |
-
<th>Estimated # of Shares to Buy or Sell</th>
|
243 |
-
<th>Shares Before and After</th>
|
244 |
-
<th class="header-bg-after">Total Value - {main_currency} {currency_symbol}</th>
|
245 |
-
<th class="header-bg-after">% Asset Allocation</th>
|
246 |
-
</tr>
|
247 |
-
<tr style="font-weight: bold;">
|
248 |
-
<td>Total</td>
|
249 |
-
<td>{format_value(sum(adj['current_value'] for adj in adjustments))}</td>
|
250 |
-
<td>{sum(adj['current_value'] for adj in adjustments) / total_value * 100:.1f}%</td>
|
251 |
-
<td></td>
|
252 |
-
<td></td>
|
253 |
-
<td>{format_value(sum(adj['trade_value'] for adj in adjustments))}</td>
|
254 |
-
<td></td>
|
255 |
-
<td></td>
|
256 |
-
<td></td>
|
257 |
-
<td>{format_value(sum(adj['new_value'] for adj in adjustments))}</td>
|
258 |
-
<td>{sum(adj['new_value'] for adj in adjustments) / new_total_value * 100:.1f}%</td>
|
259 |
-
</tr>
|
260 |
-
</thead>
|
261 |
-
<tbody>
|
262 |
-
{''.join(
|
263 |
-
f"<tr>"
|
264 |
-
f"<td>{adj['stock_code'].upper()}</td>"
|
265 |
-
f"<td>{format_value(adj['current_value'])}</td>"
|
266 |
-
f"<td>{adj['current_value_pct'] * 100:.1f}%</td>"
|
267 |
-
f"<td><span class='highlight-edit'>{adj['target_weight'] * 100:.1f}%</span></td>"
|
268 |
-
f"<td>{adj['Buy_or_Sell']}</td>"
|
269 |
-
f"<td><span class='highlight-sky'>{format_value(adj['trade_value'])}</span></td>"
|
270 |
-
f"<td>{adj['price']:,.2f}</td>"
|
271 |
-
f"<td><span class='highlight-sky'>{format_quantity(adj['trade_quantity'])}</span></td>"
|
272 |
-
f"<td>{format_quantity(adj['old_quantity'])} → {format_quantity(adj['new_quantity'])}</td>"
|
273 |
-
f"<td>{format_value(adj['new_value'])}</td>"
|
274 |
-
f"<td>{adj['new_value_pct'] * 100:.1f}%</td>"
|
275 |
-
f"</tr>"
|
276 |
-
for adj in adjustments
|
277 |
-
)}
|
278 |
-
</tbody>
|
279 |
-
</table>
|
280 |
-
</div>
|
281 |
-
<br>
|
282 |
-
|
283 |
-
</div>
|
284 |
-
"""
|
285 |
-
|
286 |
-
return rebalancing_analysis
|
287 |
-
|
288 |
-
def rebalancing_tool(main_currency, holdings, cash_amount, allow_selling):
|
289 |
-
try:
|
290 |
-
stock_inputs, cash_amount = parse_input(holdings, cash_amount)
|
291 |
-
portfolio, target_ratios, total_value = build_portfolio(stock_inputs, main_currency)
|
292 |
-
portfolio_info = generate_portfolio_info(portfolio, total_value, main_currency)
|
293 |
-
rebalancing_analysis = generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_currency, cash_amount, allow_selling)
|
294 |
-
|
295 |
-
return portfolio_info + rebalancing_analysis
|
296 |
-
except Exception as e:
|
297 |
-
return f"<p style='color: red;'>An error occurred: {e}</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/retirement_planning.py
DELETED
@@ -1,222 +0,0 @@
|
|
1 |
-
import base64
|
2 |
-
import csv
|
3 |
-
from io import StringIO
|
4 |
-
import matplotlib.pyplot as plt
|
5 |
-
import io
|
6 |
-
from modules.utils import load_css
|
7 |
-
|
8 |
-
def retirement_planning(
|
9 |
-
current_age=None,
|
10 |
-
retirement_age=None,
|
11 |
-
life_expectancy=None,
|
12 |
-
monthly_income_required=None,
|
13 |
-
inflation_rate=None,
|
14 |
-
current_investment=None,
|
15 |
-
monthly_investment=None,
|
16 |
-
annual_increase_in_monthly_investment=None, # 추가된 입력
|
17 |
-
reinvest_dividends=None,
|
18 |
-
pre_retirement_roi=None,
|
19 |
-
post_retirement_roi=None,
|
20 |
-
pre_retirement_dividend_yield=None,
|
21 |
-
post_retirement_dividend_yield=None
|
22 |
-
):
|
23 |
-
# NoneType일 때 0으로 처리
|
24 |
-
current_age = current_age if current_age is not None else 0
|
25 |
-
retirement_age = retirement_age if retirement_age is not None else 0
|
26 |
-
current_investment = current_investment if current_investment is not None else 0
|
27 |
-
monthly_investment = monthly_investment if monthly_investment is not None else 0
|
28 |
-
annual_increase_in_monthly_investment = annual_increase_in_monthly_investment if annual_increase_in_monthly_investment is not None else 0
|
29 |
-
pre_retirement_roi = pre_retirement_roi if pre_retirement_roi is not None else 0
|
30 |
-
post_retirement_roi = post_retirement_roi if post_retirement_roi is not None else 0
|
31 |
-
pre_retirement_dividend_yield = pre_retirement_dividend_yield if pre_retirement_dividend_yield is not None else 0
|
32 |
-
post_retirement_dividend_yield = post_retirement_dividend_yield if post_retirement_dividend_yield is not None else 0
|
33 |
-
life_expectancy = life_expectancy if life_expectancy is not None else 0
|
34 |
-
monthly_income_required = monthly_income_required if monthly_income_required is not None else 0
|
35 |
-
inflation_rate = inflation_rate if inflation_rate is not None else 0
|
36 |
-
|
37 |
-
# 은퇴 전후의 년 수 계산
|
38 |
-
if retirement_age > life_expectancy:
|
39 |
-
return "<p style='color: red;'>Error: Retirement age cannot be greater than life expectancy.</p>"
|
40 |
-
|
41 |
-
if retirement_age < current_age:
|
42 |
-
return "<p style='color: red;'>Error: Retirement age cannot be less than current age.</p>"
|
43 |
-
|
44 |
-
years_to_retirement = retirement_age - current_age
|
45 |
-
post_retirement_years = life_expectancy - retirement_age
|
46 |
-
|
47 |
-
# 현재 투자액으로 초기 투자 설정
|
48 |
-
total_investment = current_investment
|
49 |
-
|
50 |
-
# 은퇴 전 월간 이자율 계산
|
51 |
-
monthly_return_pre = (1 + pre_retirement_roi / 100) ** (1 / 12) - 1
|
52 |
-
|
53 |
-
# 은퇴 시점의 투자 계산
|
54 |
-
for year in range(years_to_retirement):
|
55 |
-
for month in range(12):
|
56 |
-
# 월간 투자액과 이자율을 적용하여 총 투자액 갱신
|
57 |
-
total_investment = (total_investment + monthly_investment) * (1 + monthly_return_pre)
|
58 |
-
# 배당금을 재투자할 경우 배당금 추가
|
59 |
-
if reinvest_dividends:
|
60 |
-
total_investment += total_investment * (pre_retirement_dividend_yield / 100 / 12)
|
61 |
-
# 연간 증가액을 월 투자액에 추가
|
62 |
-
monthly_investment += annual_increase_in_monthly_investment
|
63 |
-
|
64 |
-
# 은퇴 시작 시점의 총 투자액과 연간 배당 수익 저장
|
65 |
-
investment_at_retirement = total_investment
|
66 |
-
annual_dividend_at_retirement = investment_at_retirement * (pre_retirement_dividend_yield / 100)
|
67 |
-
monthly_dividend_at_retirement = annual_dividend_at_retirement / 12
|
68 |
-
|
69 |
-
# 은퇴 후 월간 이자율 계산
|
70 |
-
monthly_return_post = (1 + post_retirement_roi / 100) ** (1 / 12) - 1
|
71 |
-
|
72 |
-
# 연간 물가상승률을 반영한 월 생활비 계산
|
73 |
-
monthly_income_required_inflated = monthly_income_required
|
74 |
-
monthly_income_required_over_time = []
|
75 |
-
for age in range(current_age, life_expectancy + 1):
|
76 |
-
if age >= retirement_age:
|
77 |
-
monthly_income_required_over_time.append((age, monthly_income_required_inflated))
|
78 |
-
monthly_income_required_inflated *= (1 + inflation_rate / 100 / 12) ** 12
|
79 |
-
|
80 |
-
annual_income_required_at_retirement = monthly_income_required_over_time[0][1] * 12
|
81 |
-
monthly_income_required_at_retirement = monthly_income_required_over_time[0][1]
|
82 |
-
|
83 |
-
# 은퇴 후 투자 목록 초기화
|
84 |
-
post_retirement_investments = [(retirement_age, investment_at_retirement, annual_income_required_at_retirement, annual_dividend_at_retirement, monthly_dividend_at_retirement, monthly_income_required_at_retirement, annual_dividend_at_retirement - annual_income_required_at_retirement)]
|
85 |
-
|
86 |
-
# 은퇴 후 각 년도의 투자 및 배당 수익 계산
|
87 |
-
for year in range(1, post_retirement_years + 1):
|
88 |
-
# 은퇴 후 수익률을 적용하여 총 투자액 갱신
|
89 |
-
total_investment *= (1 + post_retirement_roi / 100)
|
90 |
-
# 연간 배당 수익 계산
|
91 |
-
annual_dividend_income = total_investment * (post_retirement_dividend_yield / 100)
|
92 |
-
# 월간 배당 수익 계산
|
93 |
-
monthly_dividend_income = annual_dividend_income / 12
|
94 |
-
# 연도별 물가상승률 반영한 월 생활비 갱신
|
95 |
-
inflated_income_required = monthly_income_required_over_time[year][1] if year < len(monthly_income_required_over_time) else monthly_income_required_over_time[-1][1]
|
96 |
-
# 각 연도별 투자와 배당 수익 및 월 생활비를 리스트에 추가
|
97 |
-
difference = annual_dividend_income - inflated_income_required * 12
|
98 |
-
post_retirement_investments.append((retirement_age + year, total_investment, inflated_income_required * 12, annual_dividend_income, monthly_dividend_income, inflated_income_required, difference))
|
99 |
-
|
100 |
-
# 마이너스 값의 difference 합계 계산
|
101 |
-
negative_differences_sum = sum(diff for _, _, _, _, _, _, diff in post_retirement_investments if diff < 0)
|
102 |
-
|
103 |
-
# CSV 파일 생성
|
104 |
-
csv_output = StringIO()
|
105 |
-
csv_writer = csv.writer(csv_output)
|
106 |
-
csv_writer.writerow(['Age', 'SAVINGS', 'Annual Income Required', 'Annual Dividend Income', 'Monthly Income Required', 'Monthly Dividend Income', 'Additional Cash Required'])
|
107 |
-
for age, investment, annual_income_required, annual_dividend_income, monthly_dividend_income, income_required, difference in post_retirement_investments:
|
108 |
-
additional_cash_required = f'{abs(difference):,.0f}' if difference < 0 else ''
|
109 |
-
csv_writer.writerow([age, f'{investment:,.0f}', f'{annual_income_required:,.0f}', f'{income_required:,.0f}', f'{annual_dividend_income:,.0f}', f'{monthly_dividend_income:,.0f}', additional_cash_required])
|
110 |
-
|
111 |
-
csv_string = csv_output.getvalue().encode('utf-8')
|
112 |
-
csv_base64 = base64.b64encode(csv_string).decode('utf-8')
|
113 |
-
|
114 |
-
# style.css에서 CSS 읽기
|
115 |
-
css = load_css()
|
116 |
-
|
117 |
-
# SVG 그래프 생성
|
118 |
-
fig, ax = plt.subplots()
|
119 |
-
ages = [investment[0] for investment in post_retirement_investments]
|
120 |
-
income_required = [investment[2] for investment in post_retirement_investments]
|
121 |
-
dividend_income = [investment[3] for investment in post_retirement_investments]
|
122 |
-
|
123 |
-
ax.plot(ages, income_required, label='Income Required', color='red')
|
124 |
-
ax.plot(ages, dividend_income, label='Dividend Income', color='green')
|
125 |
-
ax.set_xlabel('Age')
|
126 |
-
ax.set_ylabel('Amount')
|
127 |
-
# ax.set_title('Retirement Plan Overview')
|
128 |
-
ax.legend()
|
129 |
-
ax.grid(True)
|
130 |
-
|
131 |
-
# 그래프를 SVG 형식으로 저장
|
132 |
-
svg_output = io.StringIO()
|
133 |
-
plt.savefig(svg_output, format='svg')
|
134 |
-
plt.close(fig)
|
135 |
-
svg_data = svg_output.getvalue()
|
136 |
-
svg_base64 = base64.b64encode(svg_data.encode('utf-8')).decode('utf-8')
|
137 |
-
|
138 |
-
graph_html = f'<h3 style="margin-bottom: 0.5rem;">Retirement Planning: Income vs. Dividends</h3>'
|
139 |
-
|
140 |
-
# 은퇴 계획에 대한 HTML 결과 생성
|
141 |
-
result_html = css + f"""
|
142 |
-
<div class="wrap-text">
|
143 |
-
<div>
|
144 |
-
|
145 |
-
<div style="margin-bottom: 1.5rem;">
|
146 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Total Additional Cash Required After Retirement</div>
|
147 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{abs(negative_differences_sum):,.0f}</span>
|
148 |
-
<hr style="margin: 1.5rem 0;">
|
149 |
-
</div>
|
150 |
-
<div style="margin-bottom: 1.5rem;">
|
151 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Income Required Immediately After Retirement</div>
|
152 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{annual_income_required_at_retirement:,.0f}</span>
|
153 |
-
Annual
|
154 |
-
<div></div>
|
155 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{monthly_income_required_at_retirement:,.0f}</span>
|
156 |
-
MONTHLY
|
157 |
-
<hr style="margin: 1.5rem 0;">
|
158 |
-
</div>
|
159 |
-
<div style="margin-bottom: 1.5rem;">
|
160 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Dividend Income Immediately After Retirement</div>
|
161 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{annual_dividend_at_retirement:,.0f}</span>
|
162 |
-
Annual
|
163 |
-
<div></div>
|
164 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{monthly_dividend_at_retirement:,.0f}</span>
|
165 |
-
MONTHLY
|
166 |
-
<hr style="margin: 1.5rem 0;">
|
167 |
-
</div>
|
168 |
-
<div style="margin-bottom: 1.5rem;">
|
169 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;"></div>
|
170 |
-
{graph_html}
|
171 |
-
<img src="data:image/svg+xml;base64,{svg_base64}" alt="Retirement Plan Graph" style="width: 100%; height: auto;"/>
|
172 |
-
</div>
|
173 |
-
</div>
|
174 |
-
</div>
|
175 |
-
<div style="margin-bottom: 2rem;"></div>
|
176 |
-
<div style="display: flex; align-items: center; justify-content: space-between;">
|
177 |
-
<h3>Retirement Plan Overview</h3>
|
178 |
-
<a href="data:text/csv;base64,{csv_base64}" download="retirement_planning.csv" style="padding: 10px 20px; border: 1px solid; border-radius: 5px; background-color: #1678fb; color: white; text-decoration: none;">Download CSV</a>
|
179 |
-
</div>
|
180 |
-
<div class='table-container'>
|
181 |
-
<table>
|
182 |
-
<thead>
|
183 |
-
<tr>
|
184 |
-
<th>Age</th>
|
185 |
-
<th>SAVINGS</th>
|
186 |
-
<th>Annual Income Required</th>
|
187 |
-
<th>Monthly Income Required</th>
|
188 |
-
<th>Annual Dividend Income</th>
|
189 |
-
<th>Monthly Dividend Income</th>
|
190 |
-
<th>Additional Cash Required</th>
|
191 |
-
</tr>
|
192 |
-
</thead>
|
193 |
-
<tbody>
|
194 |
-
"""
|
195 |
-
|
196 |
-
# 각 연도별 투자와 배당 수익 및 월 생활비를 테이블에 추가
|
197 |
-
for age, investment, annual_income_required, annual_dividend_income, monthly_dividend_income, income_required, difference in post_retirement_investments:
|
198 |
-
additional_cash_required = f'{abs(difference):,.0f}' if difference < 0 else ''
|
199 |
-
result_html += f"""
|
200 |
-
<tr>
|
201 |
-
<td>{age}</td>
|
202 |
-
<td>{investment:,.0f}</td>
|
203 |
-
<td>{annual_income_required:,.0f}</td>
|
204 |
-
<td>{income_required:,.0f}</td>
|
205 |
-
<td>{annual_dividend_income:,.0f}</td>
|
206 |
-
<td>{monthly_dividend_income:,.0f}</td>
|
207 |
-
<td>{additional_cash_required}</td>
|
208 |
-
</tr>
|
209 |
-
"""
|
210 |
-
|
211 |
-
result_html += """
|
212 |
-
</tbody>
|
213 |
-
</table>
|
214 |
-
</div>
|
215 |
-
<p style="padding: 10px; border: 1px solid; border-radius: 5px; text-align: center; max-width: 400px; margin: 20px auto;">
|
216 |
-
<strong>Note:</strong> No additional investments or reinvestment of dividends after retirement.
|
217 |
-
</p>
|
218 |
-
"""
|
219 |
-
|
220 |
-
return result_html
|
221 |
-
|
222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/share_price_trend.py
DELETED
@@ -1,128 +0,0 @@
|
|
1 |
-
import io
|
2 |
-
import matplotlib.pyplot as plt
|
3 |
-
import FinanceDataReader as fdr
|
4 |
-
import pandas as pd
|
5 |
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
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 |
-
if df.empty:
|
14 |
-
print(f"<p style='color: red;'>No data available for {stock_code}</p>")
|
15 |
-
return None
|
16 |
-
return df['Close']
|
17 |
-
except Exception as e:
|
18 |
-
print(f"<p style='color: red;'>Failed to fetch data for {stock_code}: {e}</p>")
|
19 |
-
return None
|
20 |
-
|
21 |
-
def share_price_trend(stock_codes, days):
|
22 |
-
stock_prices = {}
|
23 |
-
with ThreadPoolExecutor(max_workers=10) as executor:
|
24 |
-
futures = {executor.submit(get_stock_prices, stock_code.strip(), int(days)): stock_code.strip() for stock_code in stock_codes.split(',')}
|
25 |
-
for future in as_completed(futures):
|
26 |
-
stock_code = futures[future]
|
27 |
-
try:
|
28 |
-
prices = future.result()
|
29 |
-
if prices is not None:
|
30 |
-
stock_prices[stock_code] = prices
|
31 |
-
except Exception as e:
|
32 |
-
print(f"<p style='color: red;'>Failed to fetch data for {stock_code}: {e}</p>")
|
33 |
-
|
34 |
-
if not stock_prices:
|
35 |
-
return "<p style='color: red;'>No data available for the provided stock codes.</p>"
|
36 |
-
|
37 |
-
plt.switch_backend('agg')
|
38 |
-
plt.style.use('tableau-colorblind10')
|
39 |
-
|
40 |
-
fig, ax = plt.subplots(figsize=(8, 4.5))
|
41 |
-
for stock_code, prices in stock_prices.items():
|
42 |
-
relative_prices = prices / prices.iloc[0]
|
43 |
-
ax.plot(prices.index, relative_prices, label=stock_code.upper())
|
44 |
-
|
45 |
-
# Remove the axes and ticks
|
46 |
-
ax.spines['top'].set_visible(False)
|
47 |
-
ax.spines['right'].set_visible(False)
|
48 |
-
ax.spines['left'].set_visible(False)
|
49 |
-
ax.spines['bottom'].set_visible(False)
|
50 |
-
|
51 |
-
ax.xaxis.set_visible(False)
|
52 |
-
ax.yaxis.set_visible(False)
|
53 |
-
|
54 |
-
# Add grid for better readability
|
55 |
-
ax.grid(True, which='both', linestyle='--', linewidth=0.5)
|
56 |
-
|
57 |
-
ax.legend()
|
58 |
-
plt.tight_layout()
|
59 |
-
|
60 |
-
svg_graph = io.StringIO()
|
61 |
-
plt.savefig(svg_graph, format='svg')
|
62 |
-
svg_graph.seek(0)
|
63 |
-
svg_data = svg_graph.getvalue()
|
64 |
-
plt.close()
|
65 |
-
|
66 |
-
svg_data = svg_data.replace('<svg ', '<svg width="100%" height="100%" ')
|
67 |
-
svg_data = svg_data.replace('</svg>', '''
|
68 |
-
<defs>
|
69 |
-
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
70 |
-
<stop offset="0%" style="stop-color:rgb(173,216,230);stop-opacity:1" />
|
71 |
-
<stop offset="100%" style="stop-color:rgb(0,191,255);stop-opacity:1" />
|
72 |
-
</linearGradient>
|
73 |
-
<filter id="dropshadow" height="130%">
|
74 |
-
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
75 |
-
<feOffset dx="2" dy="2" result="offsetblur"/>
|
76 |
-
<feMerge>
|
77 |
-
<feMergeNode/>
|
78 |
-
<feMergeNode in="SourceGraphic"/>
|
79 |
-
</feMerge>
|
80 |
-
</filter>
|
81 |
-
</defs>
|
82 |
-
<style>
|
83 |
-
@keyframes lineAnimation {
|
84 |
-
from {
|
85 |
-
stroke-dasharray: 0, 1000;
|
86 |
-
}
|
87 |
-
to {
|
88 |
-
stroke-dasharray: 1000, 0;
|
89 |
-
}
|
90 |
-
}
|
91 |
-
path {
|
92 |
-
animation: lineAnimation 1s linear forwards;
|
93 |
-
}
|
94 |
-
</style>
|
95 |
-
</svg>''')
|
96 |
-
|
97 |
-
svg_data = svg_data.replace('stroke="#1f77b4"', 'stroke="url(#grad1)" filter="url(#dropshadow)"')
|
98 |
-
|
99 |
-
html_table = "<h3>Stock Prices Data</h3><div class='table-container'><table>"
|
100 |
-
html_table += "<thead><tr><th>Date</th>"
|
101 |
-
for stock_code in stock_prices.keys():
|
102 |
-
html_table += f"<th>{stock_code.upper()}</th>"
|
103 |
-
html_table += "</tr></thead><tbody>"
|
104 |
-
|
105 |
-
# Create a date range with only business days
|
106 |
-
all_dates = pd.date_range(start=min(df.index.min() for df in stock_prices.values()), end=pd.to_datetime('today'), freq='B')
|
107 |
-
all_dates = all_dates[::-1] # Reverse the order of dates
|
108 |
-
|
109 |
-
for date in all_dates:
|
110 |
-
html_table += f"<tr><td>{date.strftime('%Y-%m-%d')}</td>"
|
111 |
-
for stock_code in stock_prices.keys():
|
112 |
-
price = stock_prices[stock_code].get(date, None)
|
113 |
-
if price is not None:
|
114 |
-
html_table += f"<td>{price:,.2f}</td>"
|
115 |
-
else:
|
116 |
-
html_table += "<td>N/A</td>"
|
117 |
-
html_table += "</tr>"
|
118 |
-
|
119 |
-
html_table += "</tbody></table></div>"
|
120 |
-
|
121 |
-
graph_html = f'<h3>Relative Stock Prices Over the Last {days} Days</h3>{svg_data}'
|
122 |
-
return graph_html + html_table
|
123 |
-
|
124 |
-
# Example usage
|
125 |
-
# stock_codes = "AAPL,MSFT,GOOGL"
|
126 |
-
# days = 30
|
127 |
-
# result = share_price_trend(stock_codes, days)
|
128 |
-
# print(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/utils.py
DELETED
@@ -1,175 +0,0 @@
|
|
1 |
-
import ssl
|
2 |
-
import logging
|
3 |
-
import gradio as gr
|
4 |
-
import matplotlib.pyplot as plt
|
5 |
-
from io import BytesIO
|
6 |
-
import base64
|
7 |
-
from datetime import datetime
|
8 |
-
|
9 |
-
# 로그 설정
|
10 |
-
#logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
11 |
-
|
12 |
-
# SSL 인증서 검증 비활성화
|
13 |
-
ssl._create_default_https_context = ssl._create_unverified_context
|
14 |
-
|
15 |
-
current_time = datetime.now().strftime("%b-%d-%Y")
|
16 |
-
|
17 |
-
def load_css():
|
18 |
-
with open('style.css', 'r', encoding='utf-8') as file:
|
19 |
-
css = file.read()
|
20 |
-
return f"<style>{css}</style>"
|
21 |
-
|
22 |
-
|
23 |
-
def format_quantity(quantity):
|
24 |
-
# 아주 작은 값을 0으로 처리
|
25 |
-
if abs(quantity) < 1e-5: # 임계값을 조정하여 더 적절한 값을 설정할 수 있습니다.
|
26 |
-
quantity = 0
|
27 |
-
if quantity < 0:
|
28 |
-
return f"({-quantity:,.1f})"
|
29 |
-
else:
|
30 |
-
return f"{quantity:,.1f}"
|
31 |
-
|
32 |
-
def format_value(value):
|
33 |
-
# 아주 작은 값을 0으로 처리
|
34 |
-
if abs(value) < 1e-5: # 임계값을 조정하여 더 적절한 값을 설정할 수 있습니다.
|
35 |
-
value = 0
|
36 |
-
if value < 0:
|
37 |
-
return f"({-value:,.0f})"
|
38 |
-
else:
|
39 |
-
return f"{value:,.0f}"
|
40 |
-
|
41 |
-
currency_symbols = {
|
42 |
-
"KRW": "₩",
|
43 |
-
"USD": "$",
|
44 |
-
"CAD": "$",
|
45 |
-
"EUR": "€",
|
46 |
-
"JPY": "¥",
|
47 |
-
"GBP": "£"
|
48 |
-
}
|
49 |
-
|
50 |
-
def get_currency_symbol(currency_code):
|
51 |
-
return currency_symbols.get(currency_code.upper(), "")
|
52 |
-
|
53 |
-
def get_currency_codes():
|
54 |
-
return list(currency_symbols.keys())
|
55 |
-
|
56 |
-
currency_codes = get_currency_codes()
|
57 |
-
|
58 |
-
# Helper function to add buttons
|
59 |
-
def clear_buttons(inputs):
|
60 |
-
clear_button = gr.ClearButton(value="Clear")
|
61 |
-
clear_button.click(
|
62 |
-
fn=lambda: [None] * len(inputs),
|
63 |
-
inputs=[],
|
64 |
-
outputs=inputs
|
65 |
-
)
|
66 |
-
return clear_button
|
67 |
-
|
68 |
-
def submit_buttons(inputs, update_fn, output):
|
69 |
-
submit_button = gr.Button(value="Run", variant="primary")
|
70 |
-
submit_button.click(
|
71 |
-
fn=update_fn,
|
72 |
-
inputs=inputs,
|
73 |
-
outputs=output
|
74 |
-
)
|
75 |
-
return submit_button
|
76 |
-
|
77 |
-
def on_change(inputs, update_ouutput, outputs):
|
78 |
-
for input_component in inputs:
|
79 |
-
input_component.change(
|
80 |
-
fn=update_ouutput,
|
81 |
-
inputs=inputs,
|
82 |
-
outputs=outputs
|
83 |
-
)
|
84 |
-
|
85 |
-
def render_components(component_rows):
|
86 |
-
for row in component_rows:
|
87 |
-
if isinstance(row, list):
|
88 |
-
with gr.Row():
|
89 |
-
for component in row:
|
90 |
-
component.render()
|
91 |
-
else:
|
92 |
-
row.render()
|
93 |
-
|
94 |
-
def create_tab(tab_name, inputs, outputs, update_fn, examples, component_rows):
|
95 |
-
with gr.TabItem(tab_name):
|
96 |
-
with gr.Row():
|
97 |
-
with gr.Column(elem_classes="input"):
|
98 |
-
render_components(component_rows)
|
99 |
-
clear_buttons(inputs)
|
100 |
-
submit_buttons(inputs, update_fn, outputs)
|
101 |
-
gr.Examples(examples=examples, cache_examples=False, inputs=inputs)
|
102 |
-
with gr.Column():
|
103 |
-
outputs.render()
|
104 |
-
on_change(inputs, update_fn, outputs)
|
105 |
-
|
106 |
-
import matplotlib.pyplot as plt
|
107 |
-
import numpy as np
|
108 |
-
from io import BytesIO
|
109 |
-
import base64
|
110 |
-
from matplotlib import font_manager
|
111 |
-
|
112 |
-
# Global dictionary to store color mapping
|
113 |
-
color_map_storage = {}
|
114 |
-
|
115 |
-
def get_color_for_label(index, color_map, num_labels):
|
116 |
-
"""Retrieve or generate color for the given index."""
|
117 |
-
if index not in color_map_storage:
|
118 |
-
cmap = plt.get_cmap(color_map)
|
119 |
-
# Generate a color based on index (inverse of the index for color intensity)
|
120 |
-
color_map_storage[index] = cmap(1 - index / (num_labels - 1))
|
121 |
-
return color_map_storage[index]
|
122 |
-
|
123 |
-
def plot_donut_chart(data, color_map='Blues', font_path='Quicksand-Regular.ttf', legend_fontsize=30):
|
124 |
-
# 데이터 필터링: 비중이 0이 아닌 항목만 추출
|
125 |
-
filtered_data = {k: v for k, v in data.items() if v > 0}
|
126 |
-
|
127 |
-
if not filtered_data:
|
128 |
-
return '<p>No data to display.</p>' # 데이터가 없는 경우 처리
|
129 |
-
|
130 |
-
# 비중에 따라 데이터를 정렬
|
131 |
-
sorted_data = sorted(filtered_data.items(), key=lambda item: item[1], reverse=True)
|
132 |
-
labels, sizes = zip(*sorted_data)
|
133 |
-
|
134 |
-
# 색상 맵을 설정
|
135 |
-
num_labels = len(labels)
|
136 |
-
|
137 |
-
# 원형 차트의 색상 리스트 생성
|
138 |
-
colors = [get_color_for_label(i, color_map, num_labels) for i in range(num_labels)]
|
139 |
-
|
140 |
-
# 도넛 차트 시각화
|
141 |
-
fig, ax = plt.subplots(figsize=(12, 8), dpi=300) # figsize와 dpi를 설정하여 해상도 높이기
|
142 |
-
wedges, _ = ax.pie(
|
143 |
-
sizes,
|
144 |
-
colors=colors,
|
145 |
-
labels=[None]*num_labels, # 라벨을 없애기
|
146 |
-
autopct=None, # 값 표시를 없애기
|
147 |
-
startangle=-90, # 12시 방향부터 시작
|
148 |
-
pctdistance=0.85,
|
149 |
-
wedgeprops=dict(width=0.4, edgecolor='w') # 도넛 차트
|
150 |
-
)
|
151 |
-
|
152 |
-
# y축 뒤집기
|
153 |
-
ax.invert_yaxis()
|
154 |
-
|
155 |
-
# 범례 생성
|
156 |
-
handles = [plt.Line2D([0], [0], marker='o', color='w', label=f'{label} {size * 100:.1f}%',
|
157 |
-
markersize=15, markerfacecolor=get_color_for_label(i, color_map, num_labels))
|
158 |
-
for i, (label, size) in enumerate(zip(labels, sizes))]
|
159 |
-
|
160 |
-
# 범례 추가, 제목 제거, 글자 크기를 키우고 범례 박스를 조정
|
161 |
-
ax.legend(handles=handles, loc='upper left', bbox_to_anchor=(1, 1),
|
162 |
-
prop=font_manager.FontProperties(fname=font_path, size=legend_fontsize), frameon=False)
|
163 |
-
|
164 |
-
# 축을 숨깁니다.
|
165 |
-
ax.axis('off')
|
166 |
-
|
167 |
-
# SVG로 저장
|
168 |
-
buf = BytesIO()
|
169 |
-
plt.savefig(buf, format='svg', bbox_inches='tight') # bbox_inches='tight'를 추가하여 범례가 잘리는 문제를 방지
|
170 |
-
plt.close(fig)
|
171 |
-
buf.seek(0)
|
172 |
-
|
173 |
-
# SVG 데이터를 base64로 인코딩
|
174 |
-
svg_str = buf.getvalue().decode('utf-8')
|
175 |
-
return f'<img src="data:image/svg+xml;base64,{base64.b64encode(svg_str.encode("utf-8")).decode("utf-8")}" />'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|