cryman38 commited on
Commit
68d783a
Β·
verified Β·
1 Parent(s): cf64bf4

Upload 18 files

Browse files
app.py CHANGED
@@ -27,8 +27,6 @@ from interface.retirement_planning_interface import (
27
  examples as retirement_planning_examples,
28
  component_rows as retirement_planning_component_rows,
29
  )
30
- from interface.about_interface import render_about_tab
31
- from modules.utils import create_tab
32
 
33
  tabs_configuration = [
34
  ("πŸš€ Re-Balancing Calculator", rebalancing_inputs, rebalancing_output, rebalancing_update_output, rebalancing_examples, rebalancing_component_rows),
@@ -49,12 +47,64 @@ def initial_load(*args):
49
 
50
  return (rebalancing_results, share_price_trend_results, dollar_cost_averaging_results, retirement_results)
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  with gr.Blocks(css='style.css') as demo:
 
53
  with gr.Column(elem_id="col-container"):
54
  with gr.Tabs():
55
  for tab in tabs_configuration:
56
  create_tab(*tab)
57
- render_about_tab()
58
 
59
  # demo.load(
60
  # initial_load,
 
27
  examples as retirement_planning_examples,
28
  component_rows as retirement_planning_component_rows,
29
  )
 
 
30
 
31
  tabs_configuration = [
32
  ("πŸš€ Re-Balancing Calculator", rebalancing_inputs, rebalancing_output, rebalancing_update_output, rebalancing_examples, rebalancing_component_rows),
 
47
 
48
  return (rebalancing_results, share_price_trend_results, dollar_cost_averaging_results, retirement_results)
49
 
50
+ MARKDOWN = """
51
+ # TuneIt πŸ”₯
52
+
53
+ """
54
+ # Helper function to add buttons
55
+ def clear_buttons(inputs):
56
+ clear_button = gr.ClearButton(value="Clear")
57
+ clear_button.click(
58
+ fn=lambda: [None] * len(inputs),
59
+ inputs=[],
60
+ outputs=inputs
61
+ )
62
+ return clear_button
63
+
64
+ def submit_buttons(inputs, update_fn, output):
65
+ submit_button = gr.Button(value="Run", variant="primary")
66
+ submit_button.click(
67
+ fn=update_fn,
68
+ inputs=inputs,
69
+ outputs=output
70
+ )
71
+ return submit_button
72
+
73
+ def on_change(inputs, update_ouutput, outputs):
74
+ for input_component in inputs:
75
+ input_component.change(
76
+ fn=update_ouutput,
77
+ inputs=inputs,
78
+ outputs=outputs
79
+ )
80
+
81
+ def render_components(component_rows):
82
+ for row in component_rows:
83
+ if isinstance(row, list):
84
+ with gr.Row():
85
+ for component in row:
86
+ component.render()
87
+ else:
88
+ row.render()
89
+
90
+ def create_tab(tab_name, inputs, outputs, update_fn, examples, component_rows):
91
+ with gr.TabItem(tab_name):
92
+ with gr.Row():
93
+ with gr.Column(elem_classes="input", scale=1):
94
+ render_components(component_rows)
95
+ clear_buttons(inputs)
96
+ submit_buttons(inputs, update_fn, outputs)
97
+ with gr.Column(scale=3):
98
+ outputs.render()
99
+ gr.Examples(examples=examples, cache_examples=False, inputs=inputs)
100
+ on_change(inputs, update_fn, outputs)
101
+
102
  with gr.Blocks(css='style.css') as demo:
103
+ # gr.Markdown(MARKDOWN)
104
  with gr.Column(elem_id="col-container"):
105
  with gr.Tabs():
106
  for tab in tabs_configuration:
107
  create_tab(*tab)
 
108
 
109
  # demo.load(
110
  # initial_load,
interface/rebalancing_interface.py CHANGED
@@ -35,7 +35,7 @@ allow_sales = gr.Checkbox(
35
 
36
  input = [main_currency, holdings, cash_amount, allow_sales]
37
  output = gr.HTML()
38
- component_rows = [main_currency, holdings, cash_amount, allow_sales]
39
 
40
  # Define the update function
41
  def update_output(*args):
 
35
 
36
  input = [main_currency, holdings, cash_amount, allow_sales]
37
  output = gr.HTML()
38
+ component_rows = [[main_currency], holdings, [cash_amount, allow_sales]]
39
 
40
  # Define the update function
41
  def update_output(*args):
interface/share_price_trend_interface.py CHANGED
@@ -16,7 +16,7 @@ stock_codes = gr.Textbox(
16
  )
17
  period = gr.Slider(
18
  label="Number of Days",
19
- value=90,
20
  minimum=7
21
  )
22
 
 
16
  )
17
  period = gr.Slider(
18
  label="Number of Days",
19
+ value=365,
20
  minimum=7
21
  )
22
 
modules/rebalancing.py CHANGED
@@ -185,11 +185,15 @@ def generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_cu
185
  # HTML 생성
186
  rebalancing_analysis = css + f"""
187
  <div class="wrap-text">
188
- <div style="margin-bottom: 1.5rem;">
189
- <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>
190
- <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_value(sum(adj['new_value'] for adj in adjustments))} </span>
191
- (After Trades)
192
- </div>
 
 
 
 
193
  <div class='table-container wrap-text'>
194
  <table>
195
  <thead>
@@ -228,7 +232,7 @@ def generate_rebalancing_analysis(portfolio, target_ratios, total_value, main_cu
228
  <tbody>
229
  {''.join(
230
  f"<tr>"
231
- f"<td>{adj['stock_code'].upper()}</td>"
232
  f"<td>{format_value(adj['current_value'])}</td>"
233
  f"<td>{adj['current_value_pct'] * 100:.1f}%</td>"
234
  f"<td style='text-align: center !important;'><span class='highlight-edit'>{adj['target_weight'] * 100:.0f}%</span></td>"
 
185
  # HTML 생성
186
  rebalancing_analysis = css + f"""
187
  <div class="wrap-text">
188
+ <div>
189
+ <div style="margin-bottom: 1.5rem;">
190
+ <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>
191
+ <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{currency_symbol}{format_value(sum(adj['new_value'] for adj in adjustments))}</span>
192
+ (After Trades)
193
+ <hr style="margin: 1.5rem 0;">
194
+ <br>
195
+ </div>
196
+ </div>
197
  <div class='table-container wrap-text'>
198
  <table>
199
  <thead>
 
232
  <tbody>
233
  {''.join(
234
  f"<tr>"
235
+ f"<td><span class='highlight-black'>{adj['stock_code'].upper()}</span></td>"
236
  f"<td>{format_value(adj['current_value'])}</td>"
237
  f"<td>{adj['current_value_pct'] * 100:.1f}%</td>"
238
  f"<td style='text-align: center !important;'><span class='highlight-edit'>{adj['target_weight'] * 100:.0f}%</span></td>"
modules/retirement_planning.py CHANGED
@@ -2,8 +2,122 @@ 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,
@@ -13,7 +127,7 @@ def retirement_planning(
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,
@@ -101,75 +215,38 @@ def retirement_planning(
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>
@@ -177,46 +254,10 @@ def retirement_planning(
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
-
 
2
  import csv
3
  from io import StringIO
4
  import matplotlib.pyplot as plt
5
+ import plotly.graph_objects as go
6
  import io
7
+ from modules.utils import load_css, format_value
8
+
9
+ def create_retirement_graph(post_retirement_investments):
10
+ ages = [investment[0] for investment in post_retirement_investments]
11
+ income_required = [investment[5] for investment in post_retirement_investments]
12
+ dividend_income = [investment[4] for investment in post_retirement_investments]
13
+
14
+ fig = go.Figure()
15
+ fig.add_trace(go.Scatter(x=ages, y=income_required, mode='lines', name='Income Required', line=dict(color='#ffca28'), showlegend=False))
16
+ fig.add_trace(go.Scatter(x=ages, y=dividend_income, mode='lines', name='Dividend Income', line=dict(color='#89c9e6'), showlegend=False))
17
+
18
+ fig.update_layout(
19
+ xaxis_title='Age',
20
+ yaxis_title='Income',
21
+ xaxis=dict(
22
+ showline=True,
23
+ showgrid=True,
24
+ gridcolor='LightGray', # 색상 선택
25
+ gridwidth=1 # κ²©μžμ„  λ‘κ»˜
26
+ ),
27
+ yaxis=dict(
28
+ showline=True,
29
+ showgrid=True,
30
+ gridcolor='LightGray', # 색상 선택
31
+ gridwidth=1, # κ²©μžμ„  λ‘κ»˜
32
+ tickformat=',.0f' # Format as thousands (e.g., 1,000)
33
+ ),
34
+ hovermode='x',
35
+ margin=dict(l=0, r=0, t=0, b=0),
36
+ autosize=True
37
+ )
38
+
39
+ graph_html = fig.to_html(full_html=False, include_plotlyjs='cdn')
40
+ return base64.b64encode(graph_html.encode('utf-8')).decode('utf-8')
41
+
42
+ def create_retirement_table(post_retirement_investments, monthly_income_required):
43
+ result_html = f"""
44
+ <div class='table-container'>
45
+ <table>
46
+ <thead>
47
+ <tr>
48
+ <th colspan="1"></th>
49
+ <th colspan="1"></th>
50
+ <th colspan="2" class="header-bg-required">Income Required</th>
51
+ <th colspan="2" class="header-bg-divdend">Dividend Income</th>
52
+ <th colspan="1">Shortfall</th>
53
+ </tr>
54
+ <tr>
55
+ <th>Age</th>
56
+ <th>Investment</th>
57
+ <th class="header-bg-required">Annual</th>
58
+ <th class="header-bg-required">Monthly <p>(CPP {monthly_income_required:,.0f})</p></th>
59
+ <th class="header-bg-divdend">Annual</th>
60
+ <th class="header-bg-divdend">Monthly</th>
61
+ <th>Annual</th>
62
+ </tr>
63
+ </thead>
64
+ <tbody>
65
+ """
66
+ additional_cash_total = 0
67
+ for age, investment, annual_income_required, annual_dividend_income, monthly_dividend_income, income_required, difference in post_retirement_investments:
68
+ additional_cash_required = f'{format_value(difference)}' if difference < 0 else ''
69
+ additional_cash_total += difference if difference < 0 else 0
70
+ result_html += f"""
71
+ <tr>
72
+ <td>{age}</td>
73
+ <td>{investment:,.0f}</td>
74
+ <td>{annual_income_required:,.0f}</td>
75
+ <td><span class='highlight-edit'>{income_required:,.0f}</span></td>
76
+ <td>{annual_dividend_income:,.0f}</td>
77
+ <td><span class='highlight-sky'>{monthly_dividend_income:,.0f}</span></td>
78
+ <td>{additional_cash_required}</td>
79
+ </tr>
80
+ """
81
+ result_html += f"""
82
+ </tbody>
83
+ <tfoot>
84
+ <tr>
85
+ <td colspan="6" style="text-align: right; font-weight: bold;">Total Shortfall:</td>
86
+ <td style="font-weight: bold;">{abs(additional_cash_total):,.0f}</td>
87
+ </tr>
88
+ </tfoot>
89
+ </table>
90
+ </div>
91
+ """
92
+ return result_html
93
+
94
+ def create_csv_file(post_retirement_investments, monthly_income_required):
95
+ csv_output = StringIO()
96
+ csv_writer = csv.writer(csv_output)
97
+ csv_writer.writerow([
98
+ 'Age',
99
+ 'Investment',
100
+ 'Annual Income Required',
101
+ f'Monthly Income Required (CPP {monthly_income_required:,.0f})',
102
+ 'Annual Dividend Income',
103
+ 'Monthly Dividend Income',
104
+ 'Shortfall'
105
+ ])
106
+ for age, investment, annual_income_required, annual_dividend_income, monthly_dividend_income, income_required, difference in post_retirement_investments:
107
+ additional_cash_required = f'{abs(difference):,.0f}' if difference < 0 else ''
108
+ csv_writer.writerow([
109
+ age,
110
+ f'{investment:,.0f}',
111
+ f'{annual_income_required:,.0f}',
112
+ f'{income_required:,.0f}',
113
+ f'{annual_dividend_income:,.0f}',
114
+ f'{monthly_dividend_income:,.0f}',
115
+ additional_cash_required
116
+ ])
117
+ csv_data = csv_output.getvalue()
118
+ csv_base64 = base64.b64encode(csv_data.encode('utf-8')).decode('utf-8')
119
+
120
+ return csv_base64
121
 
122
  def retirement_planning(
123
  current_age=None,
 
127
  inflation_rate=None,
128
  current_investment=None,
129
  monthly_investment=None,
130
+ annual_increase_in_monthly_investment=None,
131
  reinvest_dividends=None,
132
  pre_retirement_roi=None,
133
  post_retirement_roi=None,
 
215
  negative_differences_sum = sum(diff for _, _, _, _, _, _, diff in post_retirement_investments if diff < 0)
216
 
217
  # CSV 파일 생성
218
+ csv_base64 = create_csv_file(post_retirement_investments, monthly_income_required)
 
 
 
 
 
219
 
220
+ # CSS μŠ€νƒ€μΌ λ‘œλ”©
 
 
 
221
  css = load_css()
222
 
223
+ # κ·Έλž˜ν”„ 생성
224
+ svg_base64 = create_retirement_graph(post_retirement_investments)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ # ν‘œ 생성
227
+ table_html = create_retirement_table(post_retirement_investments, monthly_income_required)
228
 
229
+ # HTML κ²°κ³Ό 생성
230
  result_html = css + f"""
231
  <div class="wrap-text">
232
  <div>
 
 
 
 
 
 
233
  <div style="margin-bottom: 1.5rem;">
234
  <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Income Required Immediately After Retirement</div>
 
 
 
235
  <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{monthly_income_required_at_retirement:,.0f}</span>
236
+ Monthly
237
  <hr style="margin: 1.5rem 0;">
238
  </div>
239
  <div style="margin-bottom: 1.5rem;">
240
  <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Dividend Income Immediately After Retirement</div>
 
 
 
241
  <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{monthly_dividend_at_retirement:,.0f}</span>
242
+ Monthly
243
  <hr style="margin: 1.5rem 0;">
244
  </div>
245
  <div style="margin-bottom: 1.5rem;">
246
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Retirement Planning</div>
247
+ <iframe src="data:text/html;base64,{svg_base64}" style="width: 100%; height: 400px; border: none;"></iframe>
248
+ <hr style="margin: 1.5rem 0;">
249
+ </div>
250
  </div>
251
  </div>
252
  <div style="margin-bottom: 2rem;"></div>
 
254
  <h3>Retirement Plan Overview</h3>
255
  <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>
256
  </div>
257
+ """ + table_html + """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  <p style="padding: 10px; border: 1px solid; border-radius: 5px; text-align: center; max-width: 400px; margin: 20px auto;">
259
  <strong>Note:</strong> No additional investments or reinvestment of dividends after retirement.
260
  </p>
261
  """
262
+
263
+ return result_html
 
 
modules/share_price_trend.py CHANGED
@@ -19,8 +19,13 @@ def get_stock_prices(stock_code, days):
19
  print(f"<p style='color: red;'>Failed to fetch data for {stock_code}: {e}</p>")
20
  return None
21
 
 
 
 
22
  def share_price_trend(stock_codes, days):
23
  stock_prices = {}
 
 
24
  with ThreadPoolExecutor(max_workers=10) as executor:
25
  futures = {executor.submit(get_stock_prices, stock_code.strip(), int(days)): stock_code.strip() for stock_code in stock_codes.split(',')}
26
  for future in as_completed(futures):
@@ -44,8 +49,8 @@ def share_price_trend(stock_codes, days):
44
  y=relative_prices,
45
  mode='lines',
46
  name=stock_code.upper(),
47
- hoverinfo='text',
48
- hovertext=[f'{stock_code.upper()}: {date.strftime("%Y-%m-%d")}: {price:.2f}' for date, price in zip(prices.index, prices)]
49
  ))
50
 
51
  fig.update_layout(
@@ -53,10 +58,20 @@ def share_price_trend(stock_codes, days):
53
  xaxis_title='Date',
54
  yaxis_title='Relative Price',
55
  showlegend=True,
56
- xaxis=dict(showline=False, showgrid=False),
57
- yaxis=dict(showline=False, showgrid=False),
 
 
 
 
 
 
 
 
 
 
58
  hovermode='x',
59
- margin=dict(l=0, r=0, t=0, b=0), # Adjust margins to ensure the plot fits well
60
  autosize=True
61
  )
62
 
 
19
  print(f"<p style='color: red;'>Failed to fetch data for {stock_code}: {e}</p>")
20
  return None
21
 
22
+ import plotly.graph_objects as go
23
+ from concurrent.futures import ThreadPoolExecutor, as_completed
24
+
25
  def share_price_trend(stock_codes, days):
26
  stock_prices = {}
27
+
28
+ # Assume `get_stock_prices` is defined elsewhere
29
  with ThreadPoolExecutor(max_workers=10) as executor:
30
  futures = {executor.submit(get_stock_prices, stock_code.strip(), int(days)): stock_code.strip() for stock_code in stock_codes.split(',')}
31
  for future in as_completed(futures):
 
49
  y=relative_prices,
50
  mode='lines',
51
  name=stock_code.upper(),
52
+ hovertemplate='<b>%{text}</b><br>Price: %{y:.2f}<extra></extra>',
53
+ text=[f'{stock_code.upper()}: {price:.2f}' for price in prices]
54
  ))
55
 
56
  fig.update_layout(
 
58
  xaxis_title='Date',
59
  yaxis_title='Relative Price',
60
  showlegend=True,
61
+ xaxis=dict(
62
+ showline=True,
63
+ showgrid=True,
64
+ gridcolor='LightGray', # Grid color
65
+ gridwidth=1, # Grid line width
66
+ ),
67
+ yaxis=dict(
68
+ showline=True,
69
+ showgrid=True,
70
+ gridcolor='LightGray', # Grid color
71
+ gridwidth=1, # Grid line width
72
+ ),
73
  hovermode='x',
74
+ margin=dict(l=50, r=50, t=50, b=50), # Adjust margins
75
  autosize=True
76
  )
77
 
modules/utils.py CHANGED
@@ -55,53 +55,6 @@ def get_currency_codes():
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", scale=1):
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(scale=3):
103
- outputs.render()
104
- # on_change(inputs, update_fn, outputs)
105
 
106
  import matplotlib.pyplot as plt
107
  import numpy as np
 
55
 
56
  currency_codes = get_currency_codes()
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  import matplotlib.pyplot as plt
60
  import numpy as np
style.css CHANGED
@@ -18,13 +18,14 @@
18
  @import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;700&display=swap');
19
  @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;700&display=swap');
20
  @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap');
 
21
 
22
 
23
  #col-container {
24
  margin: 0 auto;
25
  max-width: 100%;
26
- font-family: 'Quicksand', 'ui-sans-serif', 'system-ui', 'sans-serif';
27
- text-transform: uppercase;
28
 
29
  }
30
 
@@ -35,6 +36,20 @@
35
  /* box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); */
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  .code {
39
  font-family: 'IBM Plex Mono', 'ui-monospace', 'Consolas', 'monospace';
40
  }
@@ -68,29 +83,33 @@
68
  --highlight-sky-bg-color-dark: #89c9e6;
69
  --highlight-sky-text-color-light: #000000;
70
  --highlight-sky-text-color-dark: #000000;
71
- --highlight-black-light: #000000;
72
  --highlight-black-dark: #ffffff;
73
  --total-value-color-light: #000000;
74
  --total-value-color-dark: #ffeb3b;
75
  }
76
 
77
  .buy-sell {
78
- padding: 5px 10px;
79
- border-radius: 5px;
80
  font-weight: bold;
81
- display: inline-block;
82
  margin: 2px;
83
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
84
  }
85
 
86
  .buy {
87
- background-color: var(--buy-color);
88
- color: white !important;
 
 
89
  }
90
 
91
  .sell {
92
- background-color: var(--sell-color);
93
- color: white !important;
 
 
94
  }
95
 
96
  .highlight-edit {
@@ -165,7 +184,7 @@
165
  overflow: auto;
166
  margin-bottom: 20px;
167
  position: relative;
168
- max-height: 600px;
169
  /* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
170
  }
171
 
@@ -174,6 +193,16 @@
174
  color: #000; /* ν…μŠ€νŠΈ 색상 */
175
  }
176
 
 
 
 
 
 
 
 
 
 
 
177
  .header-bg-after {
178
  background-color: #d9d2e9 !important;/* μ›ν•˜λŠ” μƒ‰μƒμœΌλ‘œ λ³€κ²½ */
179
  color: #000; /* ν…μŠ€νŠΈ 색상 */
@@ -206,7 +235,14 @@
206
  z-index: 2;
207
  }
208
 
209
- .table-container td:first-child, .table-container th:first-child {
 
 
 
 
 
 
 
210
  position: sticky;
211
  left: 0;
212
  z-index: 1;
@@ -234,7 +270,9 @@
234
  border: 1px solid #444;
235
  }
236
  .header-bg-before,
237
- .header-bg-after {
 
 
238
  color: #000 !important;
239
  }
240
  .table-container th,
 
18
  @import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;700&display=swap');
19
  @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;700&display=swap');
20
  @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap');
21
+ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap');
22
 
23
 
24
  #col-container {
25
  margin: 0 auto;
26
  max-width: 100%;
27
+ /* font-family: 'Quicksand', 'ui-sans-serif', 'system-ui', 'sans-serif'; */
28
+ /* text-transform: uppercase; */
29
 
30
  }
31
 
 
36
  /* box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); */
37
  }
38
 
39
+ .content {
40
+ -webkit-text-size-adjust: 100%;
41
+ tab-size: 4;
42
+ font-size: 1.125rem;
43
+ line-height: 1.75rem;
44
+ box-sizing: border-box;
45
+ border-width: 0;
46
+ border-style: solid;
47
+ border-color: #e5e7eb;
48
+ margin: 0;
49
+ margin-top: 1.25em;
50
+ margin-bottom: 1.25em;
51
+ }
52
+
53
  .code {
54
  font-family: 'IBM Plex Mono', 'ui-monospace', 'Consolas', 'monospace';
55
  }
 
83
  --highlight-sky-bg-color-dark: #89c9e6;
84
  --highlight-sky-text-color-light: #000000;
85
  --highlight-sky-text-color-dark: #000000;
86
+ --highlight-black-light: #0c343d;
87
  --highlight-black-dark: #ffffff;
88
  --total-value-color-light: #000000;
89
  --total-value-color-dark: #ffeb3b;
90
  }
91
 
92
  .buy-sell {
93
+ /* padding: 5px 10px;
94
+ border-radius: 5px; */
95
  font-weight: bold;
96
+ /* display: inline-block;
97
  margin: 2px;
98
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
99
  }
100
 
101
  .buy {
102
+ /* background-color: var(--buy-color);
103
+ color: white !important; */
104
+ color: var(--buy-color);
105
+
106
  }
107
 
108
  .sell {
109
+ /* background-color: var(--sell-color);
110
+ color: white !important; */
111
+ color: var(--sell-color);
112
+
113
  }
114
 
115
  .highlight-edit {
 
184
  overflow: auto;
185
  margin-bottom: 20px;
186
  position: relative;
187
+ max-height: 100%;
188
  /* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
189
  }
190
 
 
193
  color: #000; /* ν…μŠ€νŠΈ 색상 */
194
  }
195
 
196
+ .header-bg-divdend {
197
+ background-color: #89c9e6 !important;
198
+ color: #000; /* ν…μŠ€νŠΈ 색상 */
199
+ }
200
+
201
+ .header-bg-required {
202
+ background-color: #ffca28 !important;
203
+ color: #000; /* ν…μŠ€νŠΈ 색상 */
204
+ }
205
+
206
  .header-bg-after {
207
  background-color: #d9d2e9 !important;/* μ›ν•˜λŠ” μƒ‰μƒμœΌλ‘œ λ³€κ²½ */
208
  color: #000; /* ν…μŠ€νŠΈ 색상 */
 
235
  z-index: 2;
236
  }
237
 
238
+ .table-container td:first-child {
239
+ position: sticky;
240
+ left: 0;
241
+ z-index: 1;
242
+ /* background-color: var(--highlight-color-light); */
243
+ }
244
+
245
+ .table-container th:first-child {
246
  position: sticky;
247
  left: 0;
248
  z-index: 1;
 
270
  border: 1px solid #444;
271
  }
272
  .header-bg-before,
273
+ .header-bg-after,
274
+ .header-bg-divdend,
275
+ .header-bg-required {
276
  color: #000 !important;
277
  }
278
  .table-container th,