cryman38 commited on
Commit
351021a
·
verified ·
1 Parent(s): cf2f23e

Delete modules

Browse files
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")}" />'