Spaces:
Running
Running
Upload 35 files
Browse files- app.py +20 -0
- interface/__init__.py +0 -0
- interface/__pycache__/__init__.cpython-312.pyc +0 -0
- interface/__pycache__/about.cpython-312.pyc +0 -0
- interface/__pycache__/about_interface.cpython-312.pyc +0 -0
- interface/__pycache__/compare_interface.cpython-312.pyc +0 -0
- interface/__pycache__/compare_stock_prices_interface.cpython-312.pyc +0 -0
- interface/__pycache__/cost_averaging_interface.cpython-312.pyc +0 -0
- interface/__pycache__/portfolio_interface.cpython-312.pyc +0 -0
- interface/__pycache__/portfolio_rebalancing_interface.cpython-312.pyc +0 -0
- interface/__pycache__/retirement_interface.cpython-312.pyc +0 -0
- interface/__pycache__/retirement_planning_interface.cpython-312.pyc +0 -0
- interface/about_interface.py +29 -0
- interface/compare_stock_prices_interface.py +21 -0
- interface/cost_averaging_interface.py +21 -0
- interface/portfolio_rebalancing_interface.py +24 -0
- interface/retirement_planning_interface.py +31 -0
- modules/__init__.py +0 -0
- modules/__pycache__/__init__.cpython-312.pyc +0 -0
- modules/__pycache__/compare.cpython-312.pyc +0 -0
- modules/__pycache__/compare_stock_prices.cpython-312.pyc +0 -0
- modules/__pycache__/cost_averaging.cpython-312.pyc +0 -0
- modules/__pycache__/finance_data.cpython-312.pyc +0 -0
- modules/__pycache__/portfolio.cpython-312.pyc +0 -0
- modules/__pycache__/portfolio_rebalancing.cpython-312.pyc +0 -0
- modules/__pycache__/retirement.cpython-312.pyc +0 -0
- modules/__pycache__/retirement_planning.cpython-312.pyc +0 -0
- modules/__pycache__/utils.cpython-312.pyc +0 -0
- modules/compare_stock_prices.py +49 -0
- modules/cost_averaging.py +102 -0
- modules/portfolio_rebalancing.py +225 -0
- modules/retirement_planning.py +114 -0
- modules/utils.py +36 -0
- requirements.txt +10 -0
- 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 |
+
[](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 |
+
}
|