cryman38 commited on
Commit
cafd04f
ยท
verified ยท
1 Parent(s): e84aa11

Upload 35 files

Browse files
Files changed (35) hide show
  1. app.py +20 -0
  2. interface/__init__.py +0 -0
  3. interface/__pycache__/__init__.cpython-312.pyc +0 -0
  4. interface/__pycache__/about.cpython-312.pyc +0 -0
  5. interface/__pycache__/about_interface.cpython-312.pyc +0 -0
  6. interface/__pycache__/compare_interface.cpython-312.pyc +0 -0
  7. interface/__pycache__/compare_stock_prices_interface.cpython-312.pyc +0 -0
  8. interface/__pycache__/cost_averaging_interface.cpython-312.pyc +0 -0
  9. interface/__pycache__/portfolio_interface.cpython-312.pyc +0 -0
  10. interface/__pycache__/portfolio_rebalancing_interface.cpython-312.pyc +0 -0
  11. interface/__pycache__/retirement_interface.cpython-312.pyc +0 -0
  12. interface/__pycache__/retirement_planning_interface.cpython-312.pyc +0 -0
  13. interface/about_interface.py +29 -0
  14. interface/compare_stock_prices_interface.py +21 -0
  15. interface/cost_averaging_interface.py +21 -0
  16. interface/portfolio_rebalancing_interface.py +24 -0
  17. interface/retirement_planning_interface.py +31 -0
  18. modules/__init__.py +0 -0
  19. modules/__pycache__/__init__.cpython-312.pyc +0 -0
  20. modules/__pycache__/compare.cpython-312.pyc +0 -0
  21. modules/__pycache__/compare_stock_prices.cpython-312.pyc +0 -0
  22. modules/__pycache__/cost_averaging.cpython-312.pyc +0 -0
  23. modules/__pycache__/finance_data.cpython-312.pyc +0 -0
  24. modules/__pycache__/portfolio.cpython-312.pyc +0 -0
  25. modules/__pycache__/portfolio_rebalancing.cpython-312.pyc +0 -0
  26. modules/__pycache__/retirement.cpython-312.pyc +0 -0
  27. modules/__pycache__/retirement_planning.cpython-312.pyc +0 -0
  28. modules/__pycache__/utils.cpython-312.pyc +0 -0
  29. modules/compare_stock_prices.py +49 -0
  30. modules/cost_averaging.py +102 -0
  31. modules/portfolio_rebalancing.py +225 -0
  32. modules/retirement_planning.py +114 -0
  33. modules/utils.py +36 -0
  34. requirements.txt +10 -0
  35. style.css +253 -0
app.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from interface.portfolio_rebalancing_interface import portfolio_rebalancing_interface
3
+ from interface.compare_stock_prices_interface import compare_stock_prices_interface
4
+ from interface.cost_averaging_interface import cost_averaging_interface
5
+ from interface.retirement_planning_interface import retirement_planning_interface
6
+ from interface.about_interface import render as render_about_tab
7
+
8
+ with gr.Blocks(css='style.css') as demo:
9
+ with gr.Column(elem_id="col-container"):
10
+ with gr.Tabs():
11
+ render_about_tab()
12
+ with gr.TabItem("Portfolio"):
13
+ portfolio_rebalancing_interface.render()
14
+ with gr.TabItem("Compare"):
15
+ compare_stock_prices_interface.render()
16
+ with gr.TabItem("Cost Averaging"):
17
+ cost_averaging_interface.render()
18
+ with gr.TabItem("Retirement Planning"):
19
+ retirement_planning_interface.render()
20
+ demo.launch(share=True)
interface/__init__.py ADDED
File without changes
interface/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (163 Bytes). View file
 
interface/__pycache__/about.cpython-312.pyc ADDED
Binary file (2.07 kB). View file
 
interface/__pycache__/about_interface.cpython-312.pyc ADDED
Binary file (2.53 kB). View file
 
interface/__pycache__/compare_interface.cpython-312.pyc ADDED
Binary file (1.08 kB). View file
 
interface/__pycache__/compare_stock_prices_interface.cpython-312.pyc ADDED
Binary file (1.11 kB). View file
 
interface/__pycache__/cost_averaging_interface.cpython-312.pyc ADDED
Binary file (1.12 kB). View file
 
interface/__pycache__/portfolio_interface.cpython-312.pyc ADDED
Binary file (1.44 kB). View file
 
interface/__pycache__/portfolio_rebalancing_interface.cpython-312.pyc ADDED
Binary file (1.5 kB). View file
 
interface/__pycache__/retirement_interface.cpython-312.pyc ADDED
Binary file (2.14 kB). View file
 
interface/__pycache__/retirement_planning_interface.cpython-312.pyc ADDED
Binary file (2.16 kB). View file
 
interface/about_interface.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ def render():
4
+ with gr.TabItem("๐Ÿ“„ About"):
5
+ gr.Markdown("""
6
+ ## About This Tool
7
+ This tool provides comprehensive financial analysis and portfolio management interfaces. It helps users to track stock performances, analyze retirement plans, and rebalance their portfolios based on real-time market data.
8
+
9
+ ## How to Use
10
+ - **Portfolio**: Enter your current holdings Format: `[ Ticker Currency Quantity Weight, ... ]`, cash amount, and desired cash ratio to get insights on how to rebalance your portfolio.
11
+ - **Compare**: Compare historical stock prices for a given set of stocks over a specified period.
12
+ - **Cost Averaging**: Calculate the average cost of shares after a new purchase and see how it affects your potential return.
13
+ - **Retirement Planning**: Plan your retirement savings based on your current age, investment, and expected returns.
14
+
15
+ ## Disclaimer
16
+ The information provided by this tool is for general informational purposes only. All information on the site is provided in good faith, however, we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability, or completeness of any information on the site. Your use of the site and your reliance on any information on the site is solely at your own risk.
17
+
18
+ ## Support Us
19
+ If you find this tool useful and would like to support the development of such projects, please consider making a donation. Your support is greatly appreciated.
20
+
21
+ [![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M8SBRC396DPBW)
22
+
23
+ Or, if you prefer, you can also support us through Toss at:
24
+
25
+ <a href="https://toss.me/eichijei" target="_blank">
26
+ <img src="https://static.toss.im/logos/png/1x/logo-toss.png" alt="Donate with Toss" style="width: 150px;">
27
+ </a>
28
+ """)
29
+
interface/compare_stock_prices_interface.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from modules.utils import load_css
3
+ from modules.compare_stock_prices import compare_stock_prices
4
+
5
+ # Define the interface for the Compare tab
6
+ def compare_stock_prices_interface_fn(stock_codes, period):
7
+ result = compare_stock_prices(stock_codes, period)
8
+ css = load_css()
9
+ return css + result
10
+
11
+ compare_inputs = [
12
+ gr.Textbox(label="๐Ÿ“ˆ Stock Codes", lines=2, placeholder="Enter stock codes separated by comma (e.g., AAPL,GOOGL,MSFT)", value="SCHD,QQQ"),
13
+ gr.Textbox(label="๐Ÿ“† Period (days)", value=90)
14
+ ]
15
+
16
+ compare_stock_prices_interface = gr.Interface(
17
+ fn=compare_stock_prices_interface_fn,
18
+ inputs=compare_inputs,
19
+ outputs=gr.HTML(),
20
+ live=False
21
+ )
interface/cost_averaging_interface.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from modules.cost_averaging import gradio_cost_averaging, load_css
3
+
4
+ # Define the interface for the Cost Averaging tab
5
+ def cost_averaging_interface_fn(old_avg_price, old_quantity, new_price, new_quantity):
6
+ result = gradio_cost_averaging(old_avg_price, old_quantity, new_price, new_quantity)
7
+ css = load_css()
8
+ return css + result
9
+
10
+ cost_averaging_interface = gr.Interface(
11
+ fn=cost_averaging_interface_fn,
12
+ inputs= [
13
+ gr.Number(label="First Purchase Price", value=100000),
14
+ gr.Number(label="First Purchase Quantity", value=10),
15
+ gr.Number(label="Second Purchase Price", value=50000),
16
+ gr.Number(label="Second Purchase Quantity", value="")
17
+ ],
18
+ outputs=gr.HTML(),
19
+ live=True
20
+ )
21
+
interface/portfolio_rebalancing_interface.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from modules.portfolio_rebalancing import portfolio_rebalancing_tool, load_css
3
+ from modules.utils import get_currency_codes
4
+
5
+ def portfolio_rebalancing_interface_fn(input_text, cash_amount, cash_ratio, default_currency):
6
+ result = portfolio_rebalancing_tool(input_text, cash_amount, cash_ratio, default_currency)
7
+ css = load_css()
8
+ return css + result
9
+
10
+ currency_codes = get_currency_codes()
11
+
12
+ portfolio_inputs = [
13
+ gr.Textbox(label="๐Ÿ”ฅ Holdings", lines=2, placeholder="Format: [ Ticker Currency Quantity Weight, ... ]", value="SCHD USD 500 8,\nQQQ USD 20 2"),
14
+ gr.Number(label="๐Ÿชต Cash", value=0),
15
+ gr.Slider(label="โš–๏ธ Cash Ratio (%)", value=15, minimum=0, maximum=100, step=1),
16
+ gr.Radio(label="๐Ÿ’ฑ Default Currency", choices=currency_codes, value="KRW")
17
+ ]
18
+
19
+ portfolio_rebalancing_interface = gr.Interface(
20
+ fn=portfolio_rebalancing_interface_fn,
21
+ inputs=portfolio_inputs,
22
+ outputs=gr.HTML(),
23
+ live=True
24
+ )
interface/retirement_planning_interface.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from modules.utils import load_css
3
+ from modules.retirement_planning import retirement_planning
4
+
5
+ def retirement_planning_interface_fn(current_age, retirement_age, current_investment, monthly_investment, pre_retirement_roi, post_retirement_roi, pre_retirement_dividend_yield, post_retirement_dividend_yield, reinvest_dividends, life_expectancy):
6
+ result = retirement_planning(current_age, retirement_age, current_investment, monthly_investment, pre_retirement_roi, post_retirement_roi, pre_retirement_dividend_yield, post_retirement_dividend_yield, reinvest_dividends, life_expectancy)
7
+ css = load_css()
8
+ return css + result
9
+
10
+
11
+ retirement_planning_inputs = [
12
+ gr.Slider(label="Current Age (15-60 Years)", value=15, minimum=15, maximum=60, step=1),
13
+ gr.Slider(label="Retirement Age (Upto 70 Years)", value=55, minimum=15, maximum=70, step=1),
14
+ gr.Number(label="Current Investment ()", value=10000000),
15
+ gr.Number(label="Monthly Investment ()", value=500000),
16
+ gr.Number(label="Expected Return On Investment (Pre-retirement) (%)", value=8),
17
+ gr.Number(label="Expected Return On Investment (Post-retirement) (%)", value=8),
18
+ gr.Number(label="Expected Dividend Yield (Pre-retirement) (%)", value=3.3),
19
+ gr.Number(label="Expected Dividend Yield (Post-retirement) (%)", value=3.3),
20
+ gr.Checkbox(label="Reinvest Dividends", value=True),
21
+ gr.Slider(label="Life Expectancy (Upto 100 Years)", value=80, minimum=30, maximum=100, step=1)
22
+ ]
23
+
24
+ retirement_planning_interface = gr.Interface(
25
+ fn=retirement_planning_interface_fn,
26
+ inputs=retirement_planning_inputs,
27
+ outputs=gr.HTML(),
28
+ live=True
29
+ )
30
+
31
+
modules/__init__.py ADDED
File without changes
modules/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (161 Bytes). View file
 
modules/__pycache__/compare.cpython-312.pyc ADDED
Binary file (2.76 kB). View file
 
modules/__pycache__/compare_stock_prices.cpython-312.pyc ADDED
Binary file (3.36 kB). View file
 
modules/__pycache__/cost_averaging.cpython-312.pyc ADDED
Binary file (4.2 kB). View file
 
modules/__pycache__/finance_data.cpython-312.pyc ADDED
Binary file (2.2 kB). View file
 
modules/__pycache__/portfolio.cpython-312.pyc ADDED
Binary file (10.1 kB). View file
 
modules/__pycache__/portfolio_rebalancing.cpython-312.pyc ADDED
Binary file (13.6 kB). View file
 
modules/__pycache__/retirement.cpython-312.pyc ADDED
Binary file (4.59 kB). View file
 
modules/__pycache__/retirement_planning.cpython-312.pyc ADDED
Binary file (4.6 kB). View file
 
modules/__pycache__/utils.cpython-312.pyc ADDED
Binary file (1.51 kB). View file
 
modules/compare_stock_prices.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import base64
3
+ import matplotlib.pyplot as plt
4
+ import FinanceDataReader as fdr
5
+ import pandas as pd
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+ from bs4 import BeautifulSoup
8
+
9
+ def get_stock_prices(stock_code, days):
10
+ try:
11
+ df = fdr.DataReader(stock_code)
12
+ df = df[df.index >= df.index.max() - pd.DateOffset(days=days)] # ์ตœ๊ทผ days์ผ ๋ฐ์ดํ„ฐ๋กœ ์ œํ•œ
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
+ # ์ฃผ์‹ ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
20
+ stock_prices = {}
21
+ with ThreadPoolExecutor() as executor:
22
+ futures = {executor.submit(get_stock_prices, stock_code.strip(), int(days)): stock_code.strip() for stock_code in stock_codes.split(',')}
23
+ for future in as_completed(futures):
24
+ stock_code = futures[future]
25
+ try:
26
+ prices = future.result()
27
+ if prices is not None:
28
+ stock_prices[stock_code] = prices
29
+ except Exception as e:
30
+ print(f"Failed to fetch data for {stock_code}: {e}")
31
+
32
+ # ๊ฐ ์ฃผ์‹์— ๋Œ€ํ•œ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆผ
33
+ plt.figure(figsize=(10, 6))
34
+ for stock_code, prices in stock_prices.items():
35
+ relative_prices = prices / prices.iloc[0] # ์ฒซ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ƒ๋Œ€์  ๊ฐ€๊ฒฉ ๊ณ„์‚ฐ
36
+ plt.plot(prices.index, relative_prices, label=stock_code.upper()) # ์ฃผ์‹ ์ฝ”๋“œ๋ฅผ ๋Œ€๋ฌธ์ž๋กœ ํ‘œ์‹œ
37
+ plt.xlabel('Date')
38
+ plt.ylabel('Relative Price (Normalized to 1)')
39
+ plt.title(f'Relative Stock Prices Over the Last {days} Days')
40
+ plt.legend()
41
+
42
+ # ๊ทธ๋ž˜ํ”„๋ฅผ HTML๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜
43
+ html_graph = io.BytesIO()
44
+ plt.savefig(html_graph, format='png', dpi=300)
45
+ html_graph.seek(0)
46
+ graph_encoded = base64.b64encode(html_graph.getvalue()).decode()
47
+ graph_html = f'<img src="data:image/png;base64,{graph_encoded}"/>'
48
+
49
+ return graph_html
modules/cost_averaging.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.utils import load_css
2
+
3
+ def cost_averaging(old_avg_price, old_quantity, new_price, new_quantity):
4
+ # ์ž…๋ ฅ๊ฐ’์„ ์ˆซ์ž๋กœ ๋ณ€ํ™˜
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
+ new_return = (new_price / new_avg_price - 1 ) * 100 if new_avg_price != 0 else 0.0
22
+
23
+ return new_avg_price, total_quantity, total_investment, new_return, additional_investment
24
+
25
+ def gradio_cost_averaging(old_avg_price, old_quantity, new_price, new_quantity):
26
+ css = load_css()
27
+
28
+ # ์ž…๋ ฅ๊ฐ’์„ ์ˆซ์ž๋กœ ๋ณ€ํ™˜
29
+ old_avg_price = float(old_avg_price) if old_avg_price else 0.0
30
+ old_quantity = float(old_quantity) if old_quantity else 0.0
31
+ new_price = float(new_price) if new_price else 0.0
32
+ new_quantity = float(new_quantity) if new_quantity else 0.0
33
+ new_price = float(new_price) if new_price else 0.0
34
+
35
+ new_avg_price, total_quantity, total_investment, new_return, additional_investment = cost_averaging(old_avg_price, old_quantity, new_price, new_quantity)
36
+
37
+ new_return_class = ""
38
+ if new_return > 0:
39
+ new_return_class = f"<span style='color: #4caf50; font-weight: bold;'>{new_return:+,.2f}%</span>"
40
+ elif new_return < 0:
41
+ new_return_class = f"<span style='color: #f44336; font-weight: bold;'>{new_return:,.2f}%</span>"
42
+ else:
43
+ new_return_class = f"<span><strong>0</strong></span>"
44
+
45
+ result_html = css+ f"""
46
+
47
+ <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;">
48
+ <div>
49
+ <div style="margin-bottom: 1.5rem;">
50
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Return</div>
51
+ <div style="font-size: 1.5rem;">
52
+ <span></span>
53
+ <span style='color: #1678fb; font-weight: bold;'>{new_return_class}</span>
54
+ </div>
55
+ <hr style="margin: 1.5rem 0;">
56
+ </div>
57
+ </div>
58
+ <div>
59
+ <div style="margin-bottom: 1.5rem;">
60
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Average Price</div>
61
+ <div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
62
+ <span></span>
63
+ <span style='color: #1678fb'>{new_avg_price:,.0f}</span>
64
+ </div>
65
+ <hr style="margin: 1.5rem 0;">
66
+ </div>
67
+ </div>
68
+ <div>
69
+ <div style="margin-bottom: 1.5rem;">
70
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Total Quantity</div>
71
+ <div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
72
+ <span></span>
73
+ <span style='color: #1678fb'>{total_quantity:,.0f}</span>
74
+ </div>
75
+ <hr style="margin: 1.5rem 0;">
76
+ </div>
77
+ </div>
78
+ <div>
79
+ <div style="margin-bottom: 1.5rem;">
80
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Additional Investment</div>
81
+ <div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
82
+ <span></span>
83
+ <span style='color: #1678fb'>{additional_investment:,.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
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Total Investment</div>
91
+ <div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
92
+ <span></span>
93
+ <span style='color: #1678fb'>{total_investment:,.0f}</span>
94
+ </div>
95
+ <hr style="margin: 1.5rem 0;">
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ """
101
+
102
+ return result_html
modules/portfolio_rebalancing.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytz
2
+ import math
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, as_completed
9
+ from modules.utils import load_css, get_currency_symbol
10
+
11
+ def parse_input(text, cash_amount, cash_ratio):
12
+ lines = text.strip().split(',')
13
+ stock_inputs = []
14
+ total_target_weight = 0
15
+
16
+ for line in lines:
17
+ parts = line.split()
18
+ if len(parts) == 4:
19
+ stock_code, currency_code, quantity_expr, target_weight_expr = parts
20
+ quantity = math.floor(eval(quantity_expr.replace(' ', '')))
21
+ target_weight = eval(target_weight_expr.replace(' ', ''))
22
+ target_ratio = (1 - cash_ratio / 100) * target_weight
23
+ stock_inputs.append((currency_code, stock_code, quantity, target_weight, target_ratio))
24
+ total_target_weight += target_weight
25
+
26
+ cash_amount = math.floor(cash_amount) if cash_amount else 0
27
+ default_currency_cash = {'amount': cash_amount, 'target_weight': cash_ratio / 100.0}
28
+
29
+ stock_total_weight = total_target_weight
30
+
31
+ for i in range(len(stock_inputs)):
32
+ stock_inputs[i] = (stock_inputs[i][0], stock_inputs[i][1], stock_inputs[i][2], stock_inputs[i][3], (1 - default_currency_cash['target_weight']) * stock_inputs[i][3] / stock_total_weight)
33
+
34
+ return stock_inputs, default_currency_cash
35
+
36
+ def get_portfolio_exchange_rate(currency_code, default_currency):
37
+ if currency_code.lower() == default_currency.lower():
38
+ return 1.0
39
+
40
+ ticker = f"{currency_code.upper()}{default_currency.upper()}=X"
41
+ data = yf.download(ticker, period='1d')
42
+ if not data.empty:
43
+ return data['Close'].iloc[0]
44
+ else:
45
+ raise ValueError("Failed to retrieve exchange rate data.")
46
+
47
+ def get_portfolio_exchange_reflected_stock_price(stock_code, currency_code, default_currency):
48
+ new_price = get_portfolio_current_stock_price(stock_code)
49
+ exchange_rate = get_portfolio_exchange_rate(currency_code, default_currency)
50
+ return math.floor(new_price * exchange_rate)
51
+
52
+ def get_portfolio_current_stock_price(stock_code):
53
+ df = fdr.DataReader(stock_code)
54
+ return df['Close'].iloc[-1]
55
+
56
+ def build_portfolio(stock_inputs, default_currency_cash, default_currency):
57
+ portfolio = {}
58
+ target_weights = {}
59
+
60
+ with ThreadPoolExecutor() as executor:
61
+ results = executor.map(lambda x: (x[1], get_portfolio_exchange_reflected_stock_price(x[1], x[0], default_currency), x[2], x[3], x[4], x[0]), stock_inputs)
62
+
63
+ for stock_code, new_price, quantity, target_weight, target_ratio, currency_code in results:
64
+ portfolio[stock_code] = {'quantity': quantity, 'price': new_price, 'target_weight': target_weight, 'currency': currency_code}
65
+ target_weights[stock_code] = target_ratio
66
+
67
+ return portfolio, target_weights, default_currency_cash
68
+
69
+ def format_quantity(quantity):
70
+ if quantity < 0:
71
+ return f"({-quantity:,})"
72
+ else:
73
+ return f"{quantity:,}"
74
+
75
+ def get_portfolio_rebalancing_info(portfolio, target_weights, krw_cash, default_currency):
76
+ css = load_css()
77
+
78
+ kst = pytz.timezone('Asia/Seoul')
79
+ current_time = datetime.now(kst).strftime("%I:%M %p %b-%d-%Y")
80
+
81
+ total_value = sum(stock['price'] * stock['quantity'] for stock in portfolio.values()) + krw_cash['amount']
82
+ total_new_stock_value = 0
83
+ total_trade_value = 0
84
+ adjustments = []
85
+
86
+ currency_symbol = get_currency_symbol(default_currency)
87
+
88
+ # Calculate current weights and values
89
+ current_weights = {stock_code: (stock['price'] * stock['quantity'] / total_value) * 100 for stock_code, stock in portfolio.items()}
90
+ current_values = {stock_code: stock['price'] * stock['quantity'] for stock_code, stock in portfolio.items()}
91
+
92
+ # Include cash in current weights and values
93
+ current_weights['CASH'] = (krw_cash['amount'] / total_value) * 100
94
+ current_values['CASH'] = krw_cash['amount']
95
+
96
+ # Sort stocks by current weight in descending order
97
+ sorted_stocks = sorted(current_weights.items(), key=lambda x: x[1], reverse=True)
98
+
99
+ # Display current weights and values section
100
+ current_info_html = "<h3>Your Portfolio Holdings</h3><div class='table-container'><table style='border-collapse: collapse;'>"
101
+ current_info_html += "<thead><tr><th style='border: 1px hidden #ddd; text-align: center;'>Stock Code</th><th style='border: 1px hidden #ddd; text-align: center;'>Current Weight (%)</th><th style='border: 1px hidden #ddd; text-align: center;'>Current Value</th></tr></thead><tbody>"
102
+ for stock_code, weight in sorted_stocks:
103
+ current_info_html += (
104
+ f"<tr>"
105
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{stock_code.upper()}</td>"
106
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{weight:.1f}%</td>"
107
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{currency_symbol}{current_values[stock_code]:,.0f}</td>"
108
+ f"</tr>"
109
+ )
110
+ current_info_html += "</tbody></table></div><br>"
111
+
112
+ for stock_code, stock_data in portfolio.items():
113
+ current_value = stock_data['price'] * stock_data['quantity']
114
+ target_value = total_value * target_weights.get(stock_code, 0)
115
+ difference = target_value - current_value
116
+ trade_quantity = math.floor(difference / stock_data['price']) if difference > 0 else -math.ceil(-difference / stock_data['price'])
117
+ new_quantity = trade_quantity + stock_data['quantity']
118
+ new_value = new_quantity * stock_data['price']
119
+ trade_value = trade_quantity * stock_data['price']
120
+ total_trade_value += abs(trade_value)
121
+ total_new_stock_value += new_value
122
+ current_value_pct = (current_value / total_value) * 100
123
+ new_value_pct = (new_value / total_value) * 100
124
+
125
+ 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']))
126
+
127
+ krw_new_amount = total_value - total_new_stock_value
128
+ krw_target_value = total_value * krw_cash['target_weight']
129
+ krw_difference = krw_new_amount - krw_cash['amount']
130
+ trade_quantity = krw_difference
131
+ new_quantity = krw_cash['amount'] + trade_quantity
132
+ new_value = new_quantity
133
+ trade_value = trade_quantity
134
+ current_value = krw_cash['amount']
135
+ current_value_pct = (current_value / total_value) * 100
136
+ new_value_pct = (new_value / total_value) * 100
137
+
138
+ adjustments.append((krw_difference, current_value, krw_target_value, current_value_pct, trade_quantity, 'CASH', 1, new_value, trade_value, krw_cash['amount'], new_quantity, krw_cash['target_weight'], new_value_pct, '', 'KRW'))
139
+
140
+ portfolio_info = css + f"""
141
+ <div><br>
142
+ <p><span class="wrap-text" style='font-size: 1.6rem; font-weight: bold; color: #1678fb;'>{currency_symbol}{total_value:,.0f}</span> as of <span style='color: #6e6e73;'>{current_time}</span></p>
143
+ <br></div>
144
+ """
145
+
146
+ currency_totals = {stock_data['currency']: {'amount': 0, 'weight': 0} for stock_data in portfolio.values()}
147
+
148
+ for stock_code, stock_data in portfolio.items():
149
+ currency = stock_data['currency']
150
+ current_value = stock_data['price'] * stock_data['quantity']
151
+ currency_totals[currency]['amount'] += current_value
152
+ currency_totals[currency]['weight'] += current_value / total_value
153
+
154
+ currency_totals['CASH'] = {'amount': krw_cash['amount'], 'weight': krw_cash['amount'] / total_value}
155
+ sorted_currencies = sorted(currency_totals.items(), key=lambda x: x[1]['weight'], reverse=True)
156
+
157
+ currency_table = "<h3>Your Portfolio by Currency</h3><div class='table-container wrap-text'><table style='border-collapse: collapse;'>"
158
+ currency_table += "<thead><tr><th style='border: 1px hidden #ddd; text-align: center;'>Currency</th><th style='border: 1px hidden #ddd; text-align: center;'>Total Weight (%)</th><th style='border: 1px hidden #ddd; text-align: center;'>Total Value</th></tr></thead><tbody>"
159
+
160
+ for currency, data in sorted_currencies:
161
+ currency_table += (
162
+ f"<tr>"
163
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{currency.upper()}</td>"
164
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{data['weight'] * 100:.1f}%</td>"
165
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{currency_symbol}{data['amount']:,}</td>"
166
+ f"</tr>"
167
+ )
168
+
169
+ currency_table += "</tbody></table></div><br>"
170
+
171
+ result_message = portfolio_info + current_info_html + currency_table + "<h3>Re-Balancing Analysis</h3><div class='table-container wrap-text'><table style='border-collapse: collapse;'>"
172
+ result_message += "<thead><tr><th style='border: 1px hidden #ddd; text-align: center;'>Stock Code</th><th style='border: 1px hidden #ddd; text-align: center;'>Current Weight (%)</th><th style='border: 1px hidden #ddd; text-align: center;'>Target Weight</th><th style='border: 1px hidden #ddd; text-align: center;'>Target Ratio (%)</th><th style='border: 1px hidden #ddd; text-align: center;'>Buy or Sell?</th><th style='border: 1px hidden #ddd; text-align: center;'>Trade Amount</th><th style='border: 1px hidden #ddd; text-align: center;'>Current Price per Share</th><th style='border: 1px hidden #ddd; text-align: center;'>Estimated # of<br> Shares to Buy or Sell</th><th style='border: 1px hidden #ddd; text-align: center;'>Quantity of Units</th><th style='border: 1px hidden #ddd; text-align: center;'>Market Value</th><th style='border: 1px hidden #ddd; text-align: center;'>% Asset Allocation</th></tr></thead><tbody>"
173
+
174
+ for adj in adjustments:
175
+ 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
176
+ Buy_or_Sell = ""
177
+ if trade_quantity > 0:
178
+ Buy_or_Sell = f"<span class='buy-sell buy'>Buy</span>"
179
+ elif trade_quantity < 0:
180
+ Buy_or_Sell = f"<span class='buy-sell sell'>Sell</span>"
181
+ else:
182
+ Buy_or_Sell = f"<span></span>"
183
+
184
+ current_value_pct_str = f"{current_value_pct:.1f}%"
185
+ target_weight_str = f"<span class='highlight-edit'>{target_weight}</span>" if stock_code != 'CASH' else ''
186
+ target_ratio_str = f"<span class='highlight-edit'>{target_ratio * 100:.1f}%</span>" if stock_code == 'CASH' else f"{target_ratio * 100:.1f}%"
187
+ trade_value_str = f"<span class='highlight-sky'>{format_quantity(trade_value)}</span>" if trade_value != 0 else ''
188
+ price_str = f"{currency_symbol}{price:,.0f}" if stock_code != 'CASH' else ''
189
+ trade_quantity_str = (
190
+ f"<span class='highlight-sky'>{format_quantity(trade_quantity)}</span>"
191
+ if stock_code != 'CASH' and trade_value != 0 else ''
192
+ )
193
+ old_quantity_str = f"{old_quantity:,.0f} โ†’ {new_quantity:,.0f}" if stock_code != 'CASH' else ''
194
+ new_value_str = f"{currency_symbol}{new_value:,.0f}"
195
+ new_value_pct_str = f"{new_value_pct:.1f}%"
196
+
197
+ result_message += (
198
+ f"<tr>"
199
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{stock_code.upper()}</td>"
200
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{current_value_pct_str}</td>"
201
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{target_weight_str}</td>"
202
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{target_ratio_str}</td>"
203
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{Buy_or_Sell}</td>"
204
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{trade_value_str}</td>"
205
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{price_str}</td>"
206
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{trade_quantity_str}</td>"
207
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{old_quantity_str}</td>"
208
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{new_value_str}</td>"
209
+ f"<td style='border: 1px hidden #ddd; text-align: center;'>{new_value_pct_str}</td>"
210
+ f"</tr>"
211
+ )
212
+
213
+ result_message += "</tbody></table></div>"
214
+
215
+ return result_message
216
+
217
+ def portfolio_rebalancing_tool(user_input, cash_amount, cash_ratio, default_currency):
218
+ try:
219
+ stock_inputs, default_currency_cash = parse_input(user_input, cash_amount, cash_ratio)
220
+ portfolio, target_weights, default_currency_cash = build_portfolio(stock_inputs, default_currency_cash, default_currency)
221
+ result = get_portfolio_rebalancing_info(portfolio, target_weights, default_currency_cash, default_currency)
222
+ return result
223
+ except Exception as e:
224
+ return str(e)
225
+
modules/retirement_planning.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.utils import load_css
2
+
3
+ def retirement_planning(current_age, retirement_age, current_investment, monthly_investment, pre_retirement_roi, post_retirement_roi, pre_retirement_dividend_yield, post_retirement_dividend_yield, reinvest_dividends, life_expectancy):
4
+ # NoneType์ผ ๋•Œ 0์œผ๋กœ ์ฒ˜๋ฆฌ
5
+ current_age = current_age if current_age is not None else 0
6
+ retirement_age = retirement_age if retirement_age is not None else 0
7
+ current_investment = current_investment if current_investment is not None else 0
8
+ monthly_investment = monthly_investment if monthly_investment is not None else 0
9
+ pre_retirement_roi = pre_retirement_roi if pre_retirement_roi is not None else 0
10
+ post_retirement_roi = post_retirement_roi if post_retirement_roi is not None else 0
11
+ pre_retirement_dividend_yield = pre_retirement_dividend_yield if pre_retirement_dividend_yield is not None else 0
12
+ life_expectancy = life_expectancy if life_expectancy is not None else 0
13
+
14
+ # ์€ํ‡ด ์ „ํ›„์˜ ๋…„ ์ˆ˜ ๊ณ„์‚ฐ
15
+ years_to_retirement = retirement_age - current_age
16
+ post_retirement_years = life_expectancy - retirement_age
17
+
18
+ # ํ˜„์žฌ ํˆฌ์ž์•ก์œผ๋กœ ์ดˆ๊ธฐ ํˆฌ์ž ์„ค์ •
19
+ total_investment = current_investment
20
+
21
+ # ์€ํ‡ด ์ „ ์›”๊ฐ„ ์ด์ž์œจ
22
+ # ์—ฐ๊ฐ„ ์ˆ˜์ต๋ฅ ์„ ์›”๊ฐ„ ์ˆ˜์ต๋ฅ ๋กœ ๋ณ€ํ™˜: (1 + ์—ฐ๊ฐ„ ์ˆ˜์ต๋ฅ )^(1/12) - 1
23
+ monthly_return_pre = (1 + pre_retirement_roi / 100) ** (1/12) - 1
24
+
25
+ # ์€ํ‡ด ์‹œ์ ์˜ ํˆฌ์ž ๊ณ„์‚ฐ
26
+ for year in range(years_to_retirement):
27
+ for month in range(12):
28
+ total_investment = (total_investment + monthly_investment) * (1 + monthly_return_pre)
29
+ if reinvest_dividends:
30
+ total_investment += total_investment * (pre_retirement_dividend_yield / 100 / 12)
31
+
32
+ # ์€ํ‡ด ์‹œ์ž‘ ์‹œ์ ์˜ ์ด ํˆฌ์ž์•ก๊ณผ ์—ฐ๊ฐ„ ๋ฐฐ๋‹น ์ˆ˜์ต ์ €์žฅ
33
+ investment_at_retirement = total_investment
34
+ annual_dividend_at_retirement = investment_at_retirement * (pre_retirement_dividend_yield / 100)
35
+ monthly_dividend_at_retirement = annual_dividend_at_retirement / 12
36
+
37
+ # ์€ํ‡ด ํ›„ ์›”๊ฐ„ ์ด์ž์œจ
38
+ # ์—ฐ๊ฐ„ ์ˆ˜์ต๋ฅ ์„ ์›”๊ฐ„ ์ˆ˜์ต๋ฅ ๋กœ ๋ณ€ํ™˜: (1 + ์—ฐ๊ฐ„ ์ˆ˜์ต๋ฅ )^(1/12) - 1
39
+ monthly_return_post = (1 + post_retirement_roi / 100) ** (1/12) - 1
40
+
41
+ # ์€ํ‡ด ํ›„ ํˆฌ์ž ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
42
+ post_retirement_investments = [(retirement_age, investment_at_retirement, annual_dividend_at_retirement, monthly_dividend_at_retirement)]
43
+
44
+ # ์€ํ‡ด ํ›„ ๊ฐ ๋…„๋„์˜ ํˆฌ์ž ๋ฐ ๋ฐฐ๋‹น ์ˆ˜์ต ๊ณ„์‚ฐ
45
+ for year in range(1, post_retirement_years + 1):
46
+ total_investment *= (1 + post_retirement_roi / 100) # ์€ํ‡ด ํ›„ ์ˆ˜์ต๋ฅ  ์ ์šฉ
47
+ annual_dividend_income = total_investment * (post_retirement_dividend_yield / 100) # ์—ฐ๊ฐ„ ๋ฐฐ๋‹น ์ˆ˜์ต ๊ณ„์‚ฐ
48
+ monthly_dividend_income = annual_dividend_income / 12 # ์›”๊ฐ„ ๋ฐฐ๋‹น ์ˆ˜์ต ๊ณ„์‚ฐ
49
+ post_retirement_investments.append((retirement_age + year, total_investment, annual_dividend_income, monthly_dividend_income))
50
+
51
+ # style.css์—์„œ CSS ์ฝ๊ธฐ
52
+ css = load_css()
53
+
54
+ # ์€ํ‡ด ๊ณ„ํš์— ๋Œ€ํ•œ HTML ๊ฒฐ๊ณผ ์ƒ์„ฑ
55
+ result_html = css + f"""
56
+ <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;">
57
+ <div>
58
+ <div style="margin-bottom: 1.5rem;">
59
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Total investment at retirement:</div>
60
+ <div style="font-size: 1.5rem; font-weight: bold; color: #1c75bc;">
61
+ <span style='color: #1678fb'>{investment_at_retirement:,.0f}</span>
62
+ </div>
63
+ <hr style="margin: 1.5rem 0;">
64
+ </div>
65
+ </div>
66
+ <div>
67
+ <div style="margin-bottom: 1.5rem;">
68
+ <div style="font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 1.5rem;">Dividend income at retirement:</div>
69
+ <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{annual_dividend_at_retirement:,.0f}</span>
70
+ Annual
71
+ <p></p>
72
+ <span style='font-size: 1.5rem; font-weight: bold; color: #1678fb'>{monthly_dividend_at_retirement:,.0f}</span>
73
+ Monthly
74
+ <hr style="margin: 1.5rem 0;">
75
+ </div>
76
+ </div>
77
+ <div>
78
+
79
+ </div>
80
+ </div>
81
+ <h3>Dividend Income After Retirement</h3>
82
+ <div class='table-container'>
83
+ <table style='border-collapse: collapse; width: 100%;'>
84
+ <thead>
85
+ <tr>
86
+ <th style='border: 1px solid #ddd; padding: 0.5rem;'>Age</th>
87
+ <th style='border: 1px solid #ddd; padding: 0.5rem;'>Total</th>
88
+ <th style='border: 1px solid #ddd; padding: 0.5rem;'>Annual</th>
89
+ <th style='border: 1px solid #ddd; padding: 0.5rem;'>Monthly</th>
90
+ </tr>
91
+ </thead>
92
+ <tbody>
93
+ """
94
+
95
+ for age, investment, annual_dividend_income, monthly_dividend_income in post_retirement_investments:
96
+ result_html += f"""
97
+ <tr>
98
+ <td style='border: 1px solid #ddd; padding: 0.5rem;'>{age}</td>
99
+ <td style='border: 1px solid #ddd; padding: 0.5rem;'>{investment:,.0f}</td>
100
+ <td style='border: 1px solid #ddd; padding: 0.5rem;'>{annual_dividend_income:,.0f}</td>
101
+ <td style='border: 1px solid #ddd; padding: 0.5rem;'>{monthly_dividend_income:,.0f}</td>
102
+ </tr>
103
+ """
104
+
105
+ result_html += """
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+ <p style="padding: 10px; border: 1px solid; border-radius: 5px; text-align: center; max-width: 400px; margin: 20px auto;">
110
+ <strong>Note:</strong> No additional investments or reinvestment of dividends after retirement.
111
+ </p>
112
+ """
113
+
114
+ return result_html
modules/utils.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ssl
2
+ import logging
3
+
4
+ # ๋กœ๊ทธ ์„ค์ •
5
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
6
+
7
+ # SSL ์ธ์ฆ์„œ ๊ฒ€์ฆ ๋น„ํ™œ์„ฑํ™”
8
+ ssl._create_default_https_context = ssl._create_unverified_context
9
+
10
+ def load_css():
11
+ with open('style.css', 'r', encoding='utf-8') as file:
12
+ css = file.read()
13
+ return f"<style>{css}</style>"
14
+
15
+ currency_symbols = {
16
+ "KRW": "โ‚ฉ",
17
+ "USD": "$",
18
+ "EUR": "โ‚ฌ",
19
+ "JPY": "ยฅ",
20
+ "GBP": "ยฃ",
21
+ "AUD": "A$",
22
+ "CAD": "C$",
23
+ "CHF": "CHF",
24
+ "CNY": "ยฅ",
25
+ "INR": "โ‚น",
26
+ "BRL": "R$",
27
+ "ZAR": "R",
28
+ "SGD": "S$",
29
+ "HKD": "HK$"
30
+ }
31
+
32
+ def get_currency_symbol(currency_code):
33
+ return currency_symbols.get(currency_code.upper(), "")
34
+
35
+ def get_currency_codes():
36
+ return list(currency_symbols.keys())
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ python-telegram-bot==13.15
2
+ beautifulsoup4
3
+ finance-datareader
4
+ requests
5
+ plotly
6
+ gradio
7
+ matplotlib
8
+ pandas
9
+ pytz
10
+ yfinance
style.css ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700&display=swap');
2
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap');
3
+
4
+ #col-container {
5
+ margin: 0 auto;
6
+ max-width: 100%;
7
+ font-family: 'Montserrat', 'ui-sans-serif', 'system-ui', 'sans-serif';
8
+ }
9
+
10
+ .code {
11
+ font-family: 'IBM Plex Mono', 'ui-monospace', 'Consolas', 'monospace';
12
+ }
13
+
14
+ .wrap-text {
15
+ word-wrap: break-word; /* ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•ด */
16
+ overflow-wrap: break-word; /* ๊ธด ๋‹จ์–ด๋„ ์ ์ ˆํžˆ ๋Š์–ด์„œ ์ค„๋ฐ”๊ฟˆ */
17
+ white-space: normal; /* ํ…์ŠคํŠธ ๊ณต๊ฐ„์— ๋งž๊ฒŒ ์ค„๋ฐ”๊ฟˆ */
18
+ }
19
+ :root {
20
+ --background-color-light: #ffffff;
21
+ --background-color-dark: #121212;
22
+ --text-color-light: #333333;
23
+ --text-color-dark: #ffffff;
24
+ --highlight-color-light: #f7f7f7;
25
+ --highlight-color-dark: #1e1e1e;
26
+ --header-color-light: #000000;
27
+ --header-color-dark: #ffffff;
28
+ --buy-color: #4caf50;
29
+ --sell-color: #f44336;
30
+ --highlight-edit-bg-color-light: #fff2cc;
31
+ --highlight-edit-text-color-light: #0000ff;
32
+ --highlight-edit-bg-color-dark: hsl(45, 100%, 70%); /* ์–ด๋‘์šด ๋ฐฐ๊ฒฝ */
33
+ --highlight-edit-text-color-dark: hsl(240, 100%, 50%); /* ๋ฐ์€ ํ…์ŠคํŠธ */
34
+ --highlight-yellow-bg-color-light: #ffeb3b;
35
+ --highlight-yellow-bg-color-dark: #ffca28;
36
+ --highlight-yellow-text-color-light: #000000;
37
+ --highlight-yellow-text-color-dark: #000000;
38
+ --highlight-sky-bg-color-light: #ddf5fd;
39
+ --highlight-sky-bg-color-dark: #89c9e6;
40
+ --highlight-sky-text-color-light: #000000;
41
+ --highlight-sky-text-color-dark: #000000;
42
+ --highlight-black-light: #000000;
43
+ --highlight-black-dark: #ffffff;
44
+ --total-value-color-light: #000000;
45
+ --total-value-color-dark: #ffeb3b;
46
+ }
47
+
48
+ body {
49
+ font-family: 'Roboto', sans-serif;
50
+ line-height: 1.6;
51
+ color: var(--text-color-light);
52
+ background-color: var(--background-color-light);
53
+ padding: 20px;
54
+ }
55
+
56
+ .buy-sell {
57
+ padding: 5px 10px;
58
+ border-radius: 5px;
59
+ font-weight: bold;
60
+ display: inline-block;
61
+ margin: 2px;
62
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
63
+ }
64
+
65
+ .buy {
66
+ background-color: var(--buy-color);
67
+ color: white !important;
68
+ }
69
+
70
+ .dashboard {
71
+ /* background-color: #f6fcfe; */
72
+ /* border: 1px solid #89c9e6; */
73
+ text-align: left;
74
+ padding: 10px;
75
+ font-size: 1rem;
76
+ border-radius: 18px;
77
+ box-shadow: 2px 4px 12px #00000014;
78
+ transition: all .3s cubic-bezier(0,0,.5,1);
79
+ }
80
+
81
+ .sell {
82
+ background-color: var(--sell-color);
83
+ color: white !important;
84
+ }
85
+ .highlight-edit {
86
+ background-color: var(--highlight-edit-bg-color-light);
87
+ color: var(--highlight-edit-text-color-light) !important;
88
+ padding: 5px 10px;
89
+ font-weight: bold;
90
+ border-radius: 5px;
91
+ display: inline-block;
92
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
93
+ }
94
+ .highlight-black {
95
+ background-color: var(--highlight-black-light);
96
+ color: var(--text-color-dark) !important;
97
+ padding: 5px 10px;
98
+ font-weight: bold;
99
+ border-radius: 5px;
100
+ display: inline-block;
101
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
102
+ }
103
+
104
+ .highlight-yellow {
105
+ background-color: var(--highlight-yellow-bg-color-light);
106
+ color: var(--highlight-yellow-text-color-light) !important;
107
+ padding: 5px 10px;
108
+ font-weight: bold;
109
+ border-radius: 5px;
110
+ display: inline-block;
111
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
112
+ }
113
+
114
+ .highlight-sky {
115
+ background-color: var(--highlight-sky-bg-color-light);
116
+ color: var(--highlight-sky-text-color-light) !important;
117
+ padding: 5px 10px;
118
+ font-weight: bold;
119
+ border-radius: 5px;
120
+ display: inline-block;
121
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
122
+ }
123
+
124
+ .container {
125
+ font-size: 1.2rem;
126
+ margin-bottom: 15px;
127
+ padding: 20px;
128
+ border: 1px solid #ddd;
129
+ border-radius: 5px;
130
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
131
+ background-color: var(--highlight-color-light);
132
+ }
133
+
134
+ .header {
135
+ font-size: 1.2rem;
136
+ font-weight: bold;
137
+ color: var(--header-color-light);
138
+ margin-top: 10px;
139
+ text-align: center;
140
+ }
141
+
142
+ .total-value {
143
+ color: var(--total-value-color-light);
144
+ font-size: 3rem;
145
+ }
146
+
147
+ .summary {
148
+ font-size: 1.2rem;
149
+ color: var(--text-color-light);
150
+ margin-top: 10px;
151
+ text-align: center;
152
+ }
153
+
154
+ .table-container {
155
+ width: 100%;
156
+ overflow: auto;
157
+ margin-bottom: 20px;
158
+ position: relative;
159
+ max-height: 600px;
160
+ border: 1px hidden #ddd;
161
+ overflow-y: auto; /* ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„: ์„ธ๋กœ ์Šคํฌ๋กค ํ—ˆ์šฉ */
162
+ }
163
+
164
+ .table-container table {
165
+ width: 100%;
166
+ border-collapse: collapse;
167
+ }
168
+
169
+ .table-container th, .table-container td {
170
+ border: 1px hidden #ddd;
171
+ padding: 8px;
172
+ text-align: left;
173
+ background-color: var(--background-color-light);
174
+ }
175
+
176
+ .table-container th {
177
+ background-color: var(--highlight-color-light);
178
+ position: sticky;
179
+ top: 0;
180
+ z-index: 2;
181
+ }
182
+
183
+ .table-container td:first-child, .table-container th:first-child {
184
+ position: sticky;
185
+ left: 0;
186
+ z-index: 1;
187
+ }
188
+
189
+ .table-container th:first-child {
190
+ z-index: 3;
191
+ }
192
+
193
+ @media (prefers-color-scheme: dark) {
194
+ body {
195
+ color: var(--text-color-dark);
196
+ background-color: var(--background-color-dark);
197
+ }
198
+
199
+ .container {
200
+ background-color: var(--highlight-color-dark);
201
+ }
202
+
203
+ .header {
204
+ color: var(--header-color-dark);
205
+ }
206
+
207
+ .total-value {
208
+ color: var(--total-value-color-dark);
209
+ }
210
+
211
+ .table-container th, .table-container td {
212
+ background-color: var(--background-color-dark);
213
+ color: var(--text-color-dark);
214
+ }
215
+
216
+ .highlight-yellow {
217
+ background-color: var(--highlight-yellow-bg-color-dark);
218
+ color: var (--highlight-yellow-text-color-dark) !important;
219
+ }
220
+
221
+ .highlight-black {
222
+ background-color: var(--highlight-black-dark);
223
+ color: var(--text-color-light) !important;
224
+ }
225
+
226
+ .highlight-sky {
227
+ background-color: var(--highlight-sky-bg-color-dark);
228
+ color: var(--highlight-sky-text-color-dark) !important;
229
+ }
230
+ .highlight-edit {
231
+ background-color: var(--highlight-edit-bg-color-dark);
232
+ color: var(--highlight-edit-text-color-dark) !important;
233
+ }
234
+
235
+ .table-container th {
236
+ background-color: var(--highlight-color-dark);
237
+ }
238
+
239
+ .buy-sell, .highlight-black, .highlight-yellow, .highlight-sky, .highlight-edit {
240
+ box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
241
+ }
242
+ .dashboard {
243
+ background-color: #2b2b2b; /* ์–ด๋‘์šด ๋ฐฐ๊ฒฝ์ƒ‰ */
244
+ border: 1px solid #555555; /* ์–ด๋‘์šด ํ…Œ๋‘๋ฆฌ ์ƒ‰์ƒ */
245
+ color: #e0e0e0; /* ๋ฐ์€ ๊ธ€์ž ์ƒ‰์ƒ */
246
+ text-align: left;
247
+ padding: 10px;
248
+ font-size: 1rem;
249
+ border-radius: 18px;
250
+ box-shadow: 2px 4px 12px #00000050; /* ๋‹คํฌ๋ชจ๋“œ์— ์–ด์šธ๋ฆฌ๋Š” ๊ทธ๋ฆผ์ž ์ƒ‰์ƒ */
251
+ transition: all .3s cubic-bezier(0,0,.5,1);
252
+ }
253
+ }