Spaces:
Running
Running
Delete modules
Browse files- modules/.DS_Store +0 -0
- modules/__init__.py +0 -0
- modules/dollar_cost_averaging.py +0 -101
- modules/rebalancing.py +0 -261
- modules/retirement_planning.py +0 -222
- modules/share_price_trend.py +0 -128
- modules/utils.py +0 -164
modules/.DS_Store
DELETED
Binary file (6.15 kB)
|
|
modules/__init__.py
DELETED
File without changes
|
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/rebalancing.py
DELETED
@@ -1,261 +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_treemap
|
6 |
-
|
7 |
-
def parse_input(holdings, cash_amount):
|
8 |
-
try:
|
9 |
-
lines = holdings.strip().split(',')
|
10 |
-
stock_inputs = []
|
11 |
-
total_target_ratio = 0
|
12 |
-
|
13 |
-
for line in lines:
|
14 |
-
parts = line.split()
|
15 |
-
if len(parts) == 4:
|
16 |
-
stock_code, currency_code, quantity_expr, target_ratio_expr = parts
|
17 |
-
# μλκ³Ό λΉμ€μ μ«μλ‘ λ³ν
|
18 |
-
quantity = float(quantity_expr.replace(' ', ''))
|
19 |
-
# λͺ©ν λΉμ¨μ΄ λΉ λκ΄νΈμΌ κ²½μ° 0μΌλ‘ μ²λ¦¬
|
20 |
-
if target_ratio_expr.strip() == '[]':
|
21 |
-
target_ratio = 0
|
22 |
-
else:
|
23 |
-
target_ratio = float(eval(target_ratio_expr.strip('[]').replace(' ', '')))
|
24 |
-
|
25 |
-
# μλμ μ μλ‘ λ΄λ¦Ό μ²λ¦¬
|
26 |
-
quantity = math.floor(quantity)
|
27 |
-
|
28 |
-
stock_inputs.append((currency_code.upper(), stock_code, quantity, target_ratio))
|
29 |
-
total_target_ratio += target_ratio
|
30 |
-
|
31 |
-
return stock_inputs, cash_amount
|
32 |
-
except Exception as e:
|
33 |
-
raise ValueError(f"Input parsing error: {e}")
|
34 |
-
|
35 |
-
def get_portfolio_exchange_rate(currency_code, main_currency):
|
36 |
-
try:
|
37 |
-
if currency_code.lower() == main_currency.lower():
|
38 |
-
return 1.0
|
39 |
-
|
40 |
-
ticker = f"{currency_code.upper()}{main_currency.upper()}=X"
|
41 |
-
data = yf.download(ticker, period='1d', progress=False)
|
42 |
-
if not data.empty:
|
43 |
-
return data['Close'].iloc[0]
|
44 |
-
else:
|
45 |
-
raise ValueError("<p style='color: red;'>Failed to retrieve exchange rate data.</p>")
|
46 |
-
except Exception as e:
|
47 |
-
raise ValueError(f"<p style='color: red;'>Exchange rate retrieval error: {e}</p>")
|
48 |
-
|
49 |
-
def get_portfolio_exchange_reflected_stock_price(stock_code, currency_code, main_currency):
|
50 |
-
try:
|
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 |
-
except Exception as e:
|
55 |
-
raise ValueError(f"<p style='color: red;'>Exchange reflected stock price error: {e}</p>")
|
56 |
-
|
57 |
-
def get_portfolio_current_stock_price(stock_code):
|
58 |
-
try:
|
59 |
-
df = fdr.DataReader(stock_code)
|
60 |
-
return df['Close'].iloc[-1]
|
61 |
-
except Exception as e:
|
62 |
-
raise ValueError(f"<p style='color: red;'>Current stock price retrieval error: {e}</p>")
|
63 |
-
|
64 |
-
def build_portfolio(stock_inputs, main_currency):
|
65 |
-
portfolio = {}
|
66 |
-
target_ratios = {}
|
67 |
-
|
68 |
-
with ThreadPoolExecutor() as executor:
|
69 |
-
results = 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)
|
70 |
-
|
71 |
-
total_value = 0
|
72 |
-
for stock_code, new_price, quantity, target_ratio, currency_code in results:
|
73 |
-
portfolio[stock_code] = {'quantity': quantity, 'price': new_price, 'currency': currency_code}
|
74 |
-
target_ratios[stock_code] = target_ratio
|
75 |
-
total_value += new_price * quantity
|
76 |
-
|
77 |
-
# λͺ©ν λΉμ¨μ ν©μ΄ 0μΈ κ²½μ°, κΈ°λ³Έ λΉμ¨ μ€μ
|
78 |
-
total_target_ratio = sum(target_ratios.values())
|
79 |
-
if total_target_ratio == 0:
|
80 |
-
# κΈ°λ³Έ λΉμ¨μ μ€μ νκ±°λ, κ· λ± λΆλ°°
|
81 |
-
num_stocks = len(stock_inputs)
|
82 |
-
default_ratio = num_stocks / num_stocks
|
83 |
-
for stock_code in target_ratios:
|
84 |
-
target_ratios[stock_code] = default_ratio
|
85 |
-
|
86 |
-
return portfolio, target_ratios, total_value
|
87 |
-
|
88 |
-
def generate_portfolio_info(portfolio, target_ratios, total_value, main_currency, cash_amount):
|
89 |
-
css = load_css()
|
90 |
-
currency_symbol = get_currency_symbol(main_currency)
|
91 |
-
|
92 |
-
# νμ¬ μν μ°¨νΈ
|
93 |
-
current_weights = {stock_code: (stock['price'] * stock['quantity'] / total_value) * 100 for stock_code, stock in portfolio.items()}
|
94 |
-
current_chart = plot_treemap(current_weights)
|
95 |
-
|
96 |
-
portfolio_info = css + f"""
|
97 |
-
<div class="wrap-text">
|
98 |
-
<div>
|
99 |
-
<div style="margin-bottom: 1.5rem;">
|
100 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Your Current Portfolio (Before Trades)</div>
|
101 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_quantity(total_value)}</span>
|
102 |
-
<hr style="margin: 1.5rem 0;">
|
103 |
-
{current_chart}
|
104 |
-
</div>
|
105 |
-
</div>
|
106 |
-
<br>
|
107 |
-
"""
|
108 |
-
|
109 |
-
# νμ¬ λΉμ¨ λ° κ°μΉλ₯Ό κ³μ°
|
110 |
-
current_values = {stock_code: stock['price'] * stock['quantity'] for stock_code, stock in portfolio.items()}
|
111 |
-
sorted_stocks = current_values.items()
|
112 |
-
|
113 |
-
# νμ¬ λ³΄μ λ λ° κ°μΉ μΉμ
νμ
|
114 |
-
current_info_html = "<h3>Your Portfolio Holdings</h3><div class='table-container'><table>"
|
115 |
-
current_info_html += "<thead><tr><th>Stock Code</th><th>Current Weight (%)</th><th>Current Value</th></tr></thead><tbody>"
|
116 |
-
for stock_code, current_value in sorted_stocks:
|
117 |
-
weight = (current_value / total_value) * 100
|
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_value:,.0f}</td>"
|
123 |
-
f"</tr>"
|
124 |
-
)
|
125 |
-
current_info_html += "</tbody></table></div><br>"
|
126 |
-
|
127 |
-
# ν΅νλ³ ν¬νΈν΄λ¦¬μ€ μμ½ μμ±
|
128 |
-
currency_totals = {currency: {'amount': 0, 'weight': 0} for currency in set(stock_data['currency'] for stock_data in portfolio.values())}
|
129 |
-
for stock_code, stock_data in portfolio.items():
|
130 |
-
currency = stock_data['currency']
|
131 |
-
current_value = stock_data['price'] * stock_data['quantity']
|
132 |
-
currency_totals[currency]['amount'] += current_value
|
133 |
-
currency_totals[currency]['weight'] += current_value / total_value
|
134 |
-
|
135 |
-
sorted_currencies = currency_totals.items()
|
136 |
-
|
137 |
-
# ν΅νλ³ μμ½ ν
μ΄λΈ μμ±
|
138 |
-
currency_table = "<h3>Your Portfolio by Currency</h3><div class='table-container wrap-text'><table>"
|
139 |
-
currency_table += "<thead><tr><th>Currency</th><th>Total Weight (%)</th><th>Total Value</th></tr></thead><tbody>"
|
140 |
-
for currency, data in sorted_currencies:
|
141 |
-
currency_table += (
|
142 |
-
f"<tr>"
|
143 |
-
f"<td>{currency.upper()}</td>"
|
144 |
-
f"<td>{data['weight'] * 100:.1f}%</td>"
|
145 |
-
f"<td>{currency_symbol}{data['amount']:,}</td>"
|
146 |
-
f"</tr>"
|
147 |
-
)
|
148 |
-
currency_table += "</tbody></table></div><br>"
|
149 |
-
|
150 |
-
return portfolio_info + current_info_html + currency_table
|
151 |
-
|
152 |
-
def generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_currency, cash_amount, allow_selling):
|
153 |
-
currency_symbol = get_currency_symbol(main_currency)
|
154 |
-
|
155 |
-
total_trade_value = 0
|
156 |
-
adjustments = []
|
157 |
-
new_total_value = cash_amount + total_value
|
158 |
-
|
159 |
-
for stock_code, stock_data in portfolio.items():
|
160 |
-
current_value = stock_data['price'] * stock_data['quantity']
|
161 |
-
target_ratio = target_ratios.get(stock_code, 0)
|
162 |
-
target_weight = target_ratio / sum(target_ratios.values())
|
163 |
-
target_value = new_total_value * target_weight
|
164 |
-
difference = target_value - current_value
|
165 |
-
|
166 |
-
if not allow_selling and difference < 0:
|
167 |
-
# λ§€λλ₯Ό νμ©νμ§ μλ κ²½μ°, λ§€λλ μλ΅
|
168 |
-
trade_quantity = 0
|
169 |
-
else:
|
170 |
-
trade_quantity = math.floor(difference / stock_data['price']) if difference > 0 else -math.ceil(-difference / stock_data['price'])
|
171 |
-
|
172 |
-
# κ±°λκ° κ°λ₯νμ§ νμΈ (νκΈ μκ³ νμΈ ν¬ν¨)
|
173 |
-
if trade_quantity > 0 and not allow_selling:
|
174 |
-
trade_quantity = min(trade_quantity, cash_amount // stock_data['price'])
|
175 |
-
|
176 |
-
new_quantity = trade_quantity + stock_data['quantity']
|
177 |
-
new_value = new_quantity * stock_data['price']
|
178 |
-
trade_value = trade_quantity * stock_data['price']
|
179 |
-
total_trade_value += abs(trade_value)
|
180 |
-
|
181 |
-
current_value_pct = (current_value / total_value) * 100
|
182 |
-
adjustments.append((difference, current_value, target_ratio, current_value_pct, trade_quantity, stock_code, stock_data['price'], new_value, trade_value, stock_data['quantity'], new_quantity, target_weight, stock_data['currency']))
|
183 |
-
|
184 |
-
total_new_stock_value = sum(adj[7] for adj in adjustments)
|
185 |
-
new_weights = {stock_code: (new_value / total_new_stock_value) * 100 for _, _, _, _, _, stock_code, _, new_value, _, _, _, _, _ in adjustments}
|
186 |
-
new_chart = plot_treemap(new_weights)
|
187 |
-
|
188 |
-
for i in range(len(adjustments)):
|
189 |
-
(difference, current_value, target_ratio, current_value_pct, trade_quantity, stock_code, price, new_value, trade_value, old_quantity, new_quantity, target_weight, currency) = adjustments[i]
|
190 |
-
new_value_pct = (new_value / total_new_stock_value) * 100
|
191 |
-
adjustments[i] = (difference, current_value, target_ratio, current_value_pct, trade_quantity, stock_code, price, new_value, trade_value, old_quantity, new_quantity, target_weight, new_value_pct, currency)
|
192 |
-
|
193 |
-
cash_balance = new_total_value - total_new_stock_value
|
194 |
-
|
195 |
-
result_message = f"""
|
196 |
-
<div>
|
197 |
-
<div style="margin-bottom: 1.5rem;">
|
198 |
-
<div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Your Adjusted Portfolio (After Trades)</div>
|
199 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_quantity(total_new_stock_value)}</span>
|
200 |
-
Adjusted Portfolio
|
201 |
-
<div></div>
|
202 |
-
<span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_quantity(cash_balance)}</span>
|
203 |
-
cash balance
|
204 |
-
<hr style="margin: 1.5rem 0;">
|
205 |
-
{new_chart}
|
206 |
-
</div>
|
207 |
-
</div>
|
208 |
-
<br>
|
209 |
-
<h3>Trades to Re-Balance Your Portfolio</h3>
|
210 |
-
<div class='table-container wrap-text'><table>"""
|
211 |
-
result_message += "<thead><tr><th>Stock Code</th><th>Current Weight (%)</th><th>Target Ratio</th><th>Target Weight (%)</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>"
|
212 |
-
|
213 |
-
for (difference, current_value, target_ratio, current_value_pct, trade_quantity, stock_code, price, new_value, trade_value, old_quantity, new_quantity, target_weight, new_value_pct, currency) in adjustments:
|
214 |
-
Buy_or_Sell = ""
|
215 |
-
if trade_quantity > 0:
|
216 |
-
Buy_or_Sell = f"<span class='buy-sell buy'>Buy</span>"
|
217 |
-
elif trade_quantity < 0:
|
218 |
-
Buy_or_Sell = f"<span class='buy-sell sell'>Sell</span>"
|
219 |
-
else:
|
220 |
-
Buy_or_Sell = f"<span></span>"
|
221 |
-
|
222 |
-
current_value_pct_str = f"{current_value_pct:.1f}%"
|
223 |
-
target_ratio_str = f"<span class='highlight-edit'>{target_ratio}</span>"
|
224 |
-
target_weight_str = f"{target_weight * 100:.1f}%"
|
225 |
-
trade_value_str = f"<span class='highlight-sky'>{format_quantity(trade_value)}</span>" if trade_value != 0 else ''
|
226 |
-
price_str = f"{currency_symbol}{price:,.0f}"
|
227 |
-
trade_quantity_str = f"<span class='highlight-sky'>{format_quantity(trade_quantity)}</span>" if trade_value != 0 else ''
|
228 |
-
old_quantity_str = f"{old_quantity:,.0f} β {new_quantity:,.0f}"
|
229 |
-
new_value_str = f"{currency_symbol}{new_value:,.0f}"
|
230 |
-
new_value_pct_str = f"{new_value_pct:.1f}%"
|
231 |
-
|
232 |
-
result_message += (
|
233 |
-
f"<tr>"
|
234 |
-
f"<td>{stock_code.upper()}</td>"
|
235 |
-
f"<td>{current_value_pct_str}</td>"
|
236 |
-
f"<td>{target_ratio_str}</td>"
|
237 |
-
f"<td>{target_weight_str}</td>"
|
238 |
-
f"<td>{Buy_or_Sell}</td>"
|
239 |
-
f"<td>{trade_value_str}</td>"
|
240 |
-
f"<td>{price_str}</td>"
|
241 |
-
f"<td>{trade_quantity_str}</td>"
|
242 |
-
f"<td>{old_quantity_str}</td>"
|
243 |
-
f"<td>{new_value_str}</td>"
|
244 |
-
f"<td>{new_value_pct_str}</td>"
|
245 |
-
f"</tr>"
|
246 |
-
)
|
247 |
-
|
248 |
-
result_message += "</tbody></table><br></div></div>"
|
249 |
-
|
250 |
-
return result_message
|
251 |
-
|
252 |
-
def rebalancing_tool(main_currency, holdings, cash_amount, allow_selling):
|
253 |
-
try:
|
254 |
-
stock_inputs, cash_amount = parse_input(holdings, cash_amount)
|
255 |
-
portfolio, target_ratios, total_value = build_portfolio(stock_inputs, main_currency)
|
256 |
-
portfolio_info = generate_portfolio_info(portfolio, target_ratios, total_value, main_currency, cash_amount)
|
257 |
-
rebalancing_analysis = generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_currency, cash_amount, allow_selling)
|
258 |
-
|
259 |
-
return portfolio_info + rebalancing_analysis
|
260 |
-
except Exception as e:
|
261 |
-
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,164 +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 |
-
if quantity < 0:
|
25 |
-
return f"({-quantity:,})"
|
26 |
-
else:
|
27 |
-
return f"{quantity:,}"
|
28 |
-
|
29 |
-
currency_symbols = {
|
30 |
-
"KRW": "β©",
|
31 |
-
"USD": "$",
|
32 |
-
"CAD": "$",
|
33 |
-
"EUR": "β¬",
|
34 |
-
"JPY": "Β₯",
|
35 |
-
"GBP": "Β£"
|
36 |
-
}
|
37 |
-
|
38 |
-
def get_currency_symbol(currency_code):
|
39 |
-
return currency_symbols.get(currency_code.upper(), "")
|
40 |
-
|
41 |
-
def get_currency_codes():
|
42 |
-
return list(currency_symbols.keys())
|
43 |
-
|
44 |
-
currency_codes = get_currency_codes()
|
45 |
-
|
46 |
-
# Helper function to add buttons
|
47 |
-
def clear_buttons(inputs):
|
48 |
-
clear_button = gr.ClearButton(value="Clear")
|
49 |
-
clear_button.click(
|
50 |
-
fn=lambda: [None] * len(inputs),
|
51 |
-
inputs=[],
|
52 |
-
outputs=inputs
|
53 |
-
)
|
54 |
-
return clear_button
|
55 |
-
|
56 |
-
def submit_buttons(inputs, update_fn, output):
|
57 |
-
submit_button = gr.Button(value="Run", variant="primary")
|
58 |
-
submit_button.click(
|
59 |
-
fn=update_fn,
|
60 |
-
inputs=inputs,
|
61 |
-
outputs=output
|
62 |
-
)
|
63 |
-
return submit_button
|
64 |
-
|
65 |
-
def on_change(inputs, update_ouutput, outputs):
|
66 |
-
for input_component in inputs:
|
67 |
-
input_component.change(
|
68 |
-
fn=update_ouutput,
|
69 |
-
inputs=inputs,
|
70 |
-
outputs=outputs
|
71 |
-
)
|
72 |
-
|
73 |
-
def render_components(component_rows):
|
74 |
-
for row in component_rows:
|
75 |
-
if isinstance(row, list):
|
76 |
-
with gr.Row():
|
77 |
-
for component in row:
|
78 |
-
component.render()
|
79 |
-
else:
|
80 |
-
row.render()
|
81 |
-
|
82 |
-
def create_tab(tab_name, inputs, outputs, update_fn, examples, component_rows):
|
83 |
-
with gr.TabItem(tab_name):
|
84 |
-
with gr.Row():
|
85 |
-
with gr.Column(elem_classes="input"):
|
86 |
-
render_components(component_rows)
|
87 |
-
clear_buttons(inputs)
|
88 |
-
submit_buttons(inputs, update_fn, outputs)
|
89 |
-
# gr.Examples(examples=examples, cache_examples=False, inputs=inputs)
|
90 |
-
with gr.Column():
|
91 |
-
outputs.render()
|
92 |
-
on_change(inputs, update_fn, outputs)
|
93 |
-
|
94 |
-
import matplotlib.pyplot as plt
|
95 |
-
import squarify
|
96 |
-
from io import BytesIO
|
97 |
-
import base64
|
98 |
-
from matplotlib import font_manager
|
99 |
-
|
100 |
-
# Global dictionary to store color mapping
|
101 |
-
color_map_storage = {}
|
102 |
-
|
103 |
-
def get_color_for_label(label, color_map, num_labels):
|
104 |
-
"""Retrieve or generate color for the given label."""
|
105 |
-
if label not in color_map_storage:
|
106 |
-
cmap = plt.get_cmap(color_map)
|
107 |
-
# Generate a color based on label index
|
108 |
-
index = len(color_map_storage) % num_labels
|
109 |
-
color_map_storage[label] = cmap(index / (num_labels - 1))
|
110 |
-
return color_map_storage[label]
|
111 |
-
|
112 |
-
def plot_treemap(data, color_map='Set3', font_path='Quicksand-Regular.ttf', legend_fontsize=30):
|
113 |
-
# λ°μ΄ν° νν°λ§: λΉμ€μ΄ 0μ΄ μλ νλͺ©λ§ μΆμΆ
|
114 |
-
filtered_data = {k: v for k, v in data.items() if v > 0}
|
115 |
-
|
116 |
-
if not filtered_data:
|
117 |
-
return '<p>No data to display.</p>' # λ°μ΄ν°κ° μλ κ²½μ° μ²λ¦¬
|
118 |
-
|
119 |
-
labels = [label.upper() for label in filtered_data.keys()] # λΌλ²¨μ λλ¬Έμλ‘ λ³ν
|
120 |
-
sizes = list(filtered_data.values())
|
121 |
-
|
122 |
-
# ν°νΈ μ€μ
|
123 |
-
font_properties = font_manager.FontProperties(fname=font_path, size=legend_fontsize)
|
124 |
-
|
125 |
-
# μμ λ§΅μ μ€μ
|
126 |
-
num_labels = len(labels)
|
127 |
-
|
128 |
-
if len(labels) == 1:
|
129 |
-
# νλͺ©μ΄ νλμΌ λ, μ 체 μ¬κ°νμ 그립λλ€.
|
130 |
-
rectangles = [{'x': 0, 'y': 0, 'dx': 100, 'dy': 100}]
|
131 |
-
colors = [get_color_for_label(labels[0], color_map, num_labels)] # μμ νλλ‘ μ±μ°κΈ°
|
132 |
-
else:
|
133 |
-
norm_sizes = squarify.normalize_sizes(sizes, 100, 100)
|
134 |
-
rectangles = squarify.squarify(norm_sizes, x=0, y=0, dx=100, dy=100)
|
135 |
-
colors = [get_color_for_label(label, color_map, num_labels) for label in labels]
|
136 |
-
|
137 |
-
# νΈλ¦¬λ§΅ μκ°ν
|
138 |
-
fig, ax = plt.subplots(figsize=(12, 8), dpi=300) # figsizeμ dpiλ₯Ό μ€μ νμ¬ ν΄μλ λμ΄κΈ°
|
139 |
-
for rect, color in zip(rectangles, colors):
|
140 |
-
x, y, dx, dy = rect['x'], rect['y'], rect['dx'], rect['dy']
|
141 |
-
ax.add_patch(plt.Rectangle((x, y), dx, dy, color=color, alpha=0.7))
|
142 |
-
|
143 |
-
# λ²λ‘ μμ±
|
144 |
-
handles = [plt.Line2D([0], [0], marker='s', color='w', label=f'{label} {size:.1f}%',
|
145 |
-
markersize=45, markerfacecolor=get_color_for_label(label, color_map, num_labels))
|
146 |
-
for label, size in zip(labels, sizes)]
|
147 |
-
|
148 |
-
# λ²λ‘ μΆκ°, μ λͺ© μ κ±°, κΈμ ν¬κΈ°λ₯Ό ν€μ°κ³ λ²λ‘ λ°μ€λ₯Ό μ‘°μ
|
149 |
-
ax.legend(handles=handles, loc='upper left', bbox_to_anchor=(1, 1),
|
150 |
-
prop=font_properties, frameon=False)
|
151 |
-
|
152 |
-
ax.set_xlim(0, 100)
|
153 |
-
ax.set_ylim(0, 100)
|
154 |
-
ax.axis('off') # μΆμ μ¨κΉλλ€.
|
155 |
-
|
156 |
-
# SVGλ‘ μ μ₯
|
157 |
-
buf = BytesIO()
|
158 |
-
plt.savefig(buf, format='svg', bbox_inches='tight') # bbox_inches='tight'λ₯Ό μΆκ°νμ¬ λ²λ‘κ° μ리λ λ¬Έμ λ₯Ό λ°©μ§
|
159 |
-
plt.close(fig)
|
160 |
-
buf.seek(0)
|
161 |
-
|
162 |
-
# SVG λ°μ΄ν°λ₯Ό base64λ‘ μΈμ½λ©
|
163 |
-
svg_str = buf.getvalue().decode('utf-8')
|
164 |
-
return f'<img src="data:image/svg+xml;base64,{base64.b64encode(svg_str.encode("utf-8")).decode("utf-8")}" />'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|