cryman38 commited on
Commit
a1ad9ef
ยท
verified ยท
1 Parent(s): 7ca78bc

Upload 16 files

Browse files
Files changed (3) hide show
  1. modules/rebalancing.py +35 -53
  2. modules/utils.py +38 -38
  3. style.css +17 -4
modules/rebalancing.py CHANGED
@@ -2,7 +2,7 @@ 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, format_value
6
  from collections import defaultdict
7
 
8
  def parse_input(holdings, cash_amount):
@@ -105,49 +105,16 @@ def generate_portfolio_info(portfolio, total_value, main_currency):
105
 
106
  # ํ˜„์žฌ ๋น„์ค‘์„ ์‚ฌ์šฉํ•˜์—ฌ ํฌํŠธํด๋ฆฌ์˜ค ํŠธ๋ฆฌ๋งต ์ฐจํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
107
  currunt_weights = {stock_code: details['weight'] for stock_code, details in holdings_totals.items()}
108
- current_chart = plot_treemap(currunt_weights)
109
 
 
 
 
110
  # HTML ์ƒ์„ฑ
111
  portfolio_info = css + f"""
112
  <div class="wrap-text">
113
- <div>
114
- <div style="margin-bottom: 1.5rem;">
115
- <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Your Current Portfolio</div>
116
- <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_value(total_value)}</span> (Before Trades)
117
- <hr style="margin: 1.5rem 0;">
118
- {current_chart}
119
- </div>
120
- </div>
121
- <br>
122
  <h3>Your Portfolio Holdings</h3>
123
- <div class='table-container wrap-text'>
124
- <table>
125
- <thead>
126
- <tr><th>Stock Code</th><th>Current Weight (%)</th><th>Current Value</th></tr>
127
- </thead>
128
- <tbody>
129
- {''.join(
130
- f"<tr><td>{stock_code.upper()}</td><td>{details['weight'] * 100:.1f}%</td><td>{currency_symbol}{format_value(details['value'])}</td></tr>"
131
- for stock_code, details in holdings_totals.items()
132
- )}
133
- </tbody>
134
- </table>
135
- </div>
136
- <br>
137
- <h3>Your Portfolio by Currency</h3>
138
- <div class='table-container wrap-text'>
139
- <table>
140
- <thead>
141
- <tr><th>Currency</th><th>Total Weight (%)</th><th>Total Value</th></tr>
142
- </thead>
143
- <tbody>
144
- {''.join(
145
- f"<tr><td>{currency.upper()}</td><td>{details['weight']* 100:.1f}%</td><td>{currency_symbol}{format_value(details['value'])}</td></tr>"
146
- for currency, details in currency_totals.items()
147
- )}
148
- </tbody>
149
- </table>
150
- </div>
151
  <br>
152
  </div>
153
  """
@@ -216,43 +183,58 @@ def generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_cu
216
  'new_value_pct': new_value / new_total_value
217
  })
218
 
219
- # ์‹ ๊ทœ ๋น„์ค‘์„ ์‚ฌ์šฉํ•˜์—ฌ ํฌํŠธํด๋ฆฌ์˜ค ํŠธ๋ฆฌ๋งต ์ฐจํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
220
- new_weights = {adj['stock_code']: adj['new_value'] / new_total_value for adj in adjustments}
221
- new_chart = plot_treemap(new_weights)
222
-
223
  # HTML ์ƒ์„ฑ
224
  rebalancing_analysis = css + f"""
225
  <div class="wrap-text">
 
226
  <div style="margin-bottom: 1.5rem;">
227
- <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Your Adjusted Portfolio</div>
228
  <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_value(new_total_value)}</span>
229
  (After Trades)
230
- <hr style="margin: 1.5rem 0;">
231
- {new_chart}
232
  </div>
233
- <br>
234
- <h3>Trades to Re-Balance Your Portfolio</h3>
235
  <div class='table-container wrap-text'>
236
  <table>
237
  <thead>
 
 
 
 
 
 
238
  <tr>
239
  <th>Stock Code</th>
240
- <th>Current Weight (%)</th>
 
241
  <th>Target Ratio</th>
242
  <th>Target Weight (%)</th>
243
  <th>Buy or Sell?</th>
244
  <th>Trade Amount - {main_currency} {currency_symbol}</th>
245
  <th>Current Price per Share - {main_currency} {currency_symbol}</th>
246
  <th>Estimated # of Shares to Buy or Sell</th>
247
- <th>Quantity of Units</th>
248
- <th>Total Value - {main_currency} {currency_symbol}</th>
249
- <th>% Asset Allocation</th>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  </tr>
251
  </thead>
252
  <tbody>
253
  {''.join(
254
  f"<tr>"
255
- f"<td>{adj['stock_code'].upper()}</td>"
 
256
  f"<td>{adj['current_value_pct'] * 100:.1f}%</td>"
257
  f"<td><span class='highlight-edit'>{adj['target_ratio']}</span></td>"
258
  f"<td>{adj['target_weight'] * 100:.1f}%</td>"
 
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):
 
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
  <br>
119
  </div>
120
  """
 
183
  'new_value_pct': new_value / new_total_value
184
  })
185
 
 
 
 
 
186
  # HTML ์ƒ์„ฑ
187
  rebalancing_analysis = css + f"""
188
  <div class="wrap-text">
189
+ <hr style="margin: 1.5rem 0;">
190
  <div style="margin-bottom: 1.5rem;">
191
+ <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>
192
  <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_value(new_total_value)}</span>
193
  (After Trades)
 
 
194
  </div>
 
 
195
  <div class='table-container wrap-text'>
196
  <table>
197
  <thead>
198
+ <tr>
199
+ <th colspan="1"></th>
200
+ <th colspan="2" class="header-bg-before">Your Current Portfolio (Before Trades)</th>
201
+ <th colspan="6" style='text-align: center'>Trades to Re-Balance Your Portfolio</th>
202
+ <th colspan="3" class="header-bg-after">Your Adjusted Portfolio (After Trades)</th>
203
+ </tr>
204
  <tr>
205
  <th>Stock Code</th>
206
+ <th class="header-bg-before">Current Value - {main_currency} {currency_symbol}</th>
207
+ <th class="header-bg-before">Current Weight (%)</th>
208
  <th>Target Ratio</th>
209
  <th>Target Weight (%)</th>
210
  <th>Buy or Sell?</th>
211
  <th>Trade Amount - {main_currency} {currency_symbol}</th>
212
  <th>Current Price per Share - {main_currency} {currency_symbol}</th>
213
  <th>Estimated # of Shares to Buy or Sell</th>
214
+ <th class="header-bg-after">Quantity of Units</th>
215
+ <th class="header-bg-after">Total Value - {main_currency} {currency_symbol}</th>
216
+ <th class="header-bg-after">% Asset Allocation</th>
217
+ </tr>
218
+ <tr style="font-weight: bold;">
219
+ <td>Total</td>
220
+ <td>{format_value(sum(adj['current_value'] for adj in adjustments))}</td>
221
+ <td>{sum(adj['current_value'] for adj in adjustments) / total_value * 100:.1f}%</td>
222
+ <td></td>
223
+ <td></td>
224
+ <td></td>
225
+ <td>{format_value(sum(adj['trade_value'] for adj in adjustments))}</td>
226
+ <td></td>
227
+ <td></td>
228
+ <td></td>
229
+ <td>{format_value(sum(adj['new_value'] for adj in adjustments))}</td>
230
+ <td>{sum(adj['new_value'] for adj in adjustments) / new_total_value * 100:.1f}%</td>
231
  </tr>
232
  </thead>
233
  <tbody>
234
  {''.join(
235
  f"<tr>"
236
+ f"<td><span class='highlight-edit'>{adj['stock_code'].upper()}</span></td>"
237
+ f"<td>{format_value(adj['current_value'])}</td>"
238
  f"<td>{adj['current_value_pct'] * 100:.1f}%</td>"
239
  f"<td><span class='highlight-edit'>{adj['target_ratio']}</span></td>"
240
  f"<td>{adj['target_weight'] * 100:.1f}%</td>"
modules/utils.py CHANGED
@@ -21,17 +21,23 @@ def load_css():
21
 
22
 
23
  def format_quantity(quantity):
 
 
 
24
  if quantity < 0:
25
  return f"({-quantity:,.1f})"
26
  else:
27
  return f"{quantity:,.1f}"
28
 
29
  def format_value(value):
 
 
 
30
  if value < 0:
31
  return f"({-value:,.0f})"
32
  else:
33
  return f"{value:,.0f}"
34
-
35
  currency_symbols = {
36
  "KRW": "โ‚ฉ",
37
  "USD": "$",
@@ -98,7 +104,7 @@ def create_tab(tab_name, inputs, outputs, update_fn, examples, component_rows):
98
  on_change(inputs, update_fn, outputs)
99
 
100
  import matplotlib.pyplot as plt
101
- import squarify
102
  from io import BytesIO
103
  import base64
104
  from matplotlib import font_manager
@@ -106,16 +112,15 @@ from matplotlib import font_manager
106
  # Global dictionary to store color mapping
107
  color_map_storage = {}
108
 
109
- def get_color_for_label(label, color_map, num_labels):
110
- """Retrieve or generate color for the given label."""
111
- if label not in color_map_storage:
112
  cmap = plt.get_cmap(color_map)
113
- # Generate a color based on label index
114
- index = len(color_map_storage) % num_labels
115
- color_map_storage[label] = cmap(index / (num_labels - 1))
116
- return color_map_storage[label]
117
 
118
- def plot_treemap(data, color_map='Set3', font_path='Quicksand-Regular.ttf', legend_fontsize=30):
119
  # ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง: ๋น„์ค‘์ด 0์ด ์•„๋‹Œ ํ•ญ๋ชฉ๋งŒ ์ถ”์ถœ
120
  filtered_data = {k: v for k, v in data.items() if v > 0}
121
 
@@ -125,44 +130,39 @@ def plot_treemap(data, color_map='Set3', font_path='Quicksand-Regular.ttf', lege
125
  # ๋น„์ค‘์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๋ฅผ ์ •๋ ฌ
126
  sorted_data = sorted(filtered_data.items(), key=lambda item: item[1], reverse=True)
127
  labels, sizes = zip(*sorted_data)
128
-
129
- # ๋ผ๋ฒจ์„ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜
130
- labels = [label.upper() for label in labels]
131
- sizes = list(sizes)
132
 
133
- # ํฐํŠธ ์„ค์ •
134
- font_properties = font_manager.FontProperties(fname=font_path, size=legend_fontsize)
135
-
136
  # ์ƒ‰์ƒ ๋งต์„ ์„ค์ •
137
  num_labels = len(labels)
 
 
 
138
 
139
- if len(labels) == 1:
140
- # ํ•ญ๋ชฉ์ด ํ•˜๋‚˜์ผ ๋•Œ, ์ „์ฒด ์‚ฌ๊ฐํ˜•์„ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.
141
- rectangles = [{'x': 0, 'y': 0, 'dx': 100, 'dy': 100}]
142
- colors = [get_color_for_label(labels[0], color_map, num_labels)] # ์ƒ‰์ƒ ํ•˜๋‚˜๋กœ ์ฑ„์šฐ๊ธฐ
143
- else:
144
- norm_sizes = squarify.normalize_sizes(sizes, 100, 100)
145
- rectangles = squarify.squarify(norm_sizes, x=0, y=0, dx=100, dy=100)
146
- colors = [get_color_for_label(label, color_map, num_labels) for label in labels]
147
-
148
- # ํŠธ๋ฆฌ๋งต ์‹œ๊ฐํ™”
149
  fig, ax = plt.subplots(figsize=(12, 8), dpi=300) # figsize์™€ dpi๋ฅผ ์„ค์ •ํ•˜์—ฌ ํ•ด์ƒ๋„ ๋†’์ด๊ธฐ
150
- for rect, color in zip(rectangles, colors):
151
- x, y, dx, dy = rect['x'], rect['y'], rect['dx'], rect['dy']
152
- ax.add_patch(plt.Rectangle((x, y), dx, dy, color=color, alpha=0.7))
153
-
 
 
 
 
 
 
 
 
 
154
  # ๋ฒ”๋ก€ ์ƒ์„ฑ
155
- handles = [plt.Line2D([0], [0], marker='s', color='w', label=f'{label} {size * 100:.1f}%',
156
- markersize=45, markerfacecolor=get_color_for_label(label, color_map, num_labels))
157
- for label, size in zip(labels, sizes)]
158
 
159
  # ๋ฒ”๋ก€ ์ถ”๊ฐ€, ์ œ๋ชฉ ์ œ๊ฑฐ, ๊ธ€์ž ํฌ๊ธฐ๋ฅผ ํ‚ค์šฐ๊ณ  ๋ฒ”๋ก€ ๋ฐ•์Šค๋ฅผ ์กฐ์ •
160
  ax.legend(handles=handles, loc='upper left', bbox_to_anchor=(1, 1),
161
- prop=font_properties, frameon=False)
162
 
163
- ax.set_xlim(0, 100)
164
- ax.set_ylim(0, 100)
165
- ax.axis('off') # ์ถ•์„ ์ˆจ๊น๋‹ˆ๋‹ค.
166
 
167
  # SVG๋กœ ์ €์žฅ
168
  buf = BytesIO()
 
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": "$",
 
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
 
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
 
 
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()
style.css CHANGED
@@ -169,13 +169,23 @@
169
  /* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
172
  .table-container table {
173
  width: 100%;
174
  border-collapse: collapse;
175
  }
176
 
177
  .table-container th, .table-container td {
178
- border: 1px hidden #ddd;
179
  padding: 8px;
180
  text-align: left;
181
  background-color: var(--background-color-light);
@@ -214,12 +224,15 @@
214
  /* ์–ด๋‘์šด ํ…Œ๋งˆ ์ ์šฉ */
215
  @media (prefers-color-scheme: dark) {
216
  .table-container {
217
- border: 1px hidden #444;
 
 
 
 
218
  }
219
-
220
  .table-container th,
221
  .table-container td {
222
- border: 1px hidden #444;
223
  background: #333;
224
  color: #ccc;
225
  }
 
169
  /* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
170
  }
171
 
172
+ .header-bg-before {
173
+ background-color: #e7f9ef !important; /* ์›ํ•˜๋Š” ์ƒ‰์ƒ์œผ๋กœ ๋ณ€๊ฒฝ */
174
+ color: #000; /* ํ…์ŠคํŠธ ์ƒ‰์ƒ */
175
+ }
176
+
177
+ .header-bg-after {
178
+ background-color: #d9d2e9 !important;/* ์›ํ•˜๋Š” ์ƒ‰์ƒ์œผ๋กœ ๋ณ€๊ฒฝ */
179
+ color: #000; /* ํ…์ŠคํŠธ ์ƒ‰์ƒ */
180
+ }
181
+
182
  .table-container table {
183
  width: 100%;
184
  border-collapse: collapse;
185
  }
186
 
187
  .table-container th, .table-container td {
188
+ border: 1px solid #ddd;
189
  padding: 8px;
190
  text-align: left;
191
  background-color: var(--background-color-light);
 
224
  /* ์–ด๋‘์šด ํ…Œ๋งˆ ์ ์šฉ */
225
  @media (prefers-color-scheme: dark) {
226
  .table-container {
227
+ border: 1px solid #444;
228
+ }
229
+ .header-bg-before,
230
+ .header-bg-after {
231
+ color: #000 !important;
232
  }
 
233
  .table-container th,
234
  .table-container td {
235
+ border: 1px solid #444;
236
  background: #333;
237
  color: #ccc;
238
  }