Upload 2 files
Browse files- app.py +200 -0
- requirements.txt +6 -0
app.py
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import logging
|
3 |
+
import math
|
4 |
+
import FinanceDataReader as fdr
|
5 |
+
import requests
|
6 |
+
import ssl
|
7 |
+
from datetime import datetime
|
8 |
+
|
9 |
+
# 현재 날짜를 "Jun-20-2024" 형식으로 가져오기
|
10 |
+
current_date = datetime.now().strftime("%b-%d-%Y")
|
11 |
+
|
12 |
+
# SSL 인증서 검증 비활성화
|
13 |
+
ssl._create_default_https_context = ssl._create_unverified_context
|
14 |
+
|
15 |
+
# 로깅 설정
|
16 |
+
logging.basicConfig(level=logging.INFO)
|
17 |
+
logger = logging.getLogger(__name__)
|
18 |
+
|
19 |
+
exchange_rates = {}
|
20 |
+
|
21 |
+
class StockCodeNotFoundError(Exception):
|
22 |
+
pass
|
23 |
+
|
24 |
+
class CurrencyConversionError(Exception):
|
25 |
+
pass
|
26 |
+
|
27 |
+
def parse_input(text):
|
28 |
+
logger.info(f"Parsing input: {text}")
|
29 |
+
try:
|
30 |
+
lines = text.strip().split(',')
|
31 |
+
stock_inputs = []
|
32 |
+
total_target_weight = 0
|
33 |
+
krw_cash = None
|
34 |
+
|
35 |
+
for line in lines:
|
36 |
+
parts = line.split()
|
37 |
+
if len(parts) == 4:
|
38 |
+
country_code, stock_code, quantity_expr, target_weight_expr = parts
|
39 |
+
quantity = math.floor(eval(quantity_expr.replace(' ', '')))
|
40 |
+
target_weight = eval(target_weight_expr.replace(' ', ''))
|
41 |
+
stock_inputs.append((country_code, stock_code, quantity, target_weight))
|
42 |
+
total_target_weight += target_weight
|
43 |
+
elif len(parts) == 2:
|
44 |
+
cash_amount_expr, target_weight_expr = parts
|
45 |
+
cash_amount = math.floor(eval(cash_amount_expr.replace(' ', '')))
|
46 |
+
krw_cash = {'amount': cash_amount, 'target_weight': eval(target_weight_expr.replace(' ', ''))}
|
47 |
+
elif len(parts) == 1:
|
48 |
+
cash_amount_expr = parts[0]
|
49 |
+
cash_amount = math.floor(eval(cash_amount_expr.replace(' ', '')))
|
50 |
+
krw_cash = {'amount': cash_amount, 'target_weight': 0}
|
51 |
+
else:
|
52 |
+
raise ValueError("Invalid input format.")
|
53 |
+
|
54 |
+
if krw_cash is None:
|
55 |
+
krw_cash = {'amount': 0, 'target_weight': 0}
|
56 |
+
|
57 |
+
cash_ratio = krw_cash['target_weight']
|
58 |
+
stock_total_weight = total_target_weight
|
59 |
+
|
60 |
+
for i in range(len(stock_inputs)):
|
61 |
+
stock_inputs[i] = (stock_inputs[i][0], stock_inputs[i][1], stock_inputs[i][2], (1 - cash_ratio) * stock_inputs[i][3] / stock_total_weight)
|
62 |
+
|
63 |
+
logger.info(f"Parsed stock inputs: {stock_inputs}, KRW cash: {krw_cash}")
|
64 |
+
return stock_inputs, krw_cash
|
65 |
+
except (ValueError, SyntaxError) as e:
|
66 |
+
logger.error("Error parsing input", exc_info=e)
|
67 |
+
raise ValueError("Invalid input format. Example: krw 458730 530 8, krw 368590 79 2, 518192")
|
68 |
+
|
69 |
+
def get_exchange_rate(country_code):
|
70 |
+
if country_code.upper() in exchange_rates:
|
71 |
+
return exchange_rates[country_code.upper()]
|
72 |
+
|
73 |
+
logger.info(f"Fetching exchange rate - {country_code}")
|
74 |
+
if country_code.upper() == 'KRW':
|
75 |
+
return 1
|
76 |
+
|
77 |
+
url = f"https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRW{country_code.upper()}"
|
78 |
+
try:
|
79 |
+
response = requests.get(url, verify=False)
|
80 |
+
response.raise_for_status()
|
81 |
+
data = response.json()
|
82 |
+
exchange_rate = data[0]['basePrice']
|
83 |
+
exchange_rates[country_code.upper()] = exchange_rate
|
84 |
+
logger.info(f"Exchange rate - {country_code}: {exchange_rate}")
|
85 |
+
return exchange_rate
|
86 |
+
except (requests.RequestException, IndexError) as e:
|
87 |
+
logger.error(f"Error fetching exchange rate - {country_code}", exc_info=e)
|
88 |
+
raise CurrencyConversionError("Error fetching exchange rate. Please enter a valid country code.")
|
89 |
+
|
90 |
+
def get_exchange_reflected_stock_price(stock_code, country_code):
|
91 |
+
logger.info(f"Fetching exchange reflected stock price - {stock_code} in {country_code}")
|
92 |
+
try:
|
93 |
+
current_price = get_current_stock_price(stock_code)
|
94 |
+
exchange_rate = get_exchange_rate(country_code)
|
95 |
+
reflected_price = math.floor(current_price * exchange_rate)
|
96 |
+
logger.info(f"Reflected stock price - {stock_code}: {reflected_price}")
|
97 |
+
return reflected_price
|
98 |
+
except (StockCodeNotFoundError, CurrencyConversionError) as e:
|
99 |
+
logger.error(f"Error fetching reflected stock price - {stock_code} in {country_code}", exc_info=e)
|
100 |
+
raise e
|
101 |
+
|
102 |
+
def get_current_stock_price(stock_code):
|
103 |
+
logger.info(f"Fetching current stock price - {stock_code}")
|
104 |
+
try:
|
105 |
+
df = fdr.DataReader(stock_code)
|
106 |
+
current_price = df['Close'].iloc[-1]
|
107 |
+
logger.info(f"Current stock price - {stock_code}: {current_price}")
|
108 |
+
return current_price
|
109 |
+
except ValueError as e:
|
110 |
+
logger.error(f"Error fetching stock price - {stock_code}", exc_info=e)
|
111 |
+
raise StockCodeNotFoundError("Stock code not found. Please enter a valid stock code.")
|
112 |
+
|
113 |
+
def build_portfolio(stock_inputs, krw_cash):
|
114 |
+
portfolio = {}
|
115 |
+
target_weights = {}
|
116 |
+
|
117 |
+
logger.info(f"Building portfolio - stock inputs: {stock_inputs}, KRW cash: {krw_cash}")
|
118 |
+
|
119 |
+
for stock_input in stock_inputs:
|
120 |
+
country_code, stock_code, quantity, target_weight = stock_input
|
121 |
+
current_price = get_exchange_reflected_stock_price(stock_code, country_code)
|
122 |
+
portfolio[stock_code] = {'quantity': quantity, 'price': current_price, 'country_code': country_code}
|
123 |
+
target_weights[stock_code] = target_weight
|
124 |
+
|
125 |
+
if krw_cash is None:
|
126 |
+
krw_cash = {'amount': 0, 'target_weight': 0}
|
127 |
+
|
128 |
+
logger.info(f"Portfolio built: {portfolio}, target weights: {target_weights}, KRW cash: {krw_cash}")
|
129 |
+
return portfolio, target_weights, krw_cash
|
130 |
+
|
131 |
+
def get_portfolio_rebalancing_info(portfolio, target_weights, krw_cash):
|
132 |
+
logger.info("Calculating portfolio rebalancing information")
|
133 |
+
|
134 |
+
total_value = sum(stock['price'] * stock['quantity'] for stock in portfolio.values()) + krw_cash['amount']
|
135 |
+
total_new_stock_value = 0
|
136 |
+
total_trade_value = 0
|
137 |
+
adjustments = []
|
138 |
+
|
139 |
+
for stock_code, stock_data in portfolio.items():
|
140 |
+
current_value = stock_data['price'] * stock_data['quantity']
|
141 |
+
target_weight = target_weights.get(stock_code, 0)
|
142 |
+
target_value = total_value * target_weights.get(stock_code, 0)
|
143 |
+
difference = target_value - current_value
|
144 |
+
trade_quantity = math.floor(difference / stock_data['price']) if difference > 0 else -math.ceil(-difference / stock_data['price'])
|
145 |
+
new_quantity = trade_quantity + stock_data['quantity']
|
146 |
+
trade_value = trade_quantity * stock_data['price']
|
147 |
+
total_trade_value += abs(trade_value)
|
148 |
+
new_value = trade_value + current_value
|
149 |
+
total_new_stock_value += new_value
|
150 |
+
current_value_pct = (current_value / total_value) * 100
|
151 |
+
|
152 |
+
adjustments.append((difference, current_value, target_weight, current_value_pct, trade_quantity, stock_code, stock_data['price'], new_value, trade_value, stock_data['quantity'], new_quantity))
|
153 |
+
|
154 |
+
result_message = ""
|
155 |
+
for adjustment in adjustments:
|
156 |
+
difference, current_value, target_weight, current_value_pct, trade_quantity, stock_code, price, new_value, trade_value, old_quantity, new_quantity = adjustment
|
157 |
+
new_value_pct = (new_value / total_value) * 100
|
158 |
+
formatted_trade_quantity = f"{trade_quantity:+,}" if trade_quantity != 0 else "0"
|
159 |
+
result_message += (
|
160 |
+
f"{stock_code.upper()} @{price:,}\n"
|
161 |
+
f" {current_value:,} [{current_value_pct:.1f}%]\n"
|
162 |
+
f" [{target_weight * 100:.1f}%] {formatted_trade_quantity}\n\n"
|
163 |
+
)
|
164 |
+
|
165 |
+
if krw_cash:
|
166 |
+
krw_new_amount = total_value - total_new_stock_value
|
167 |
+
krw_target_weight = krw_cash['target_weight']
|
168 |
+
krw_difference = krw_new_amount - krw_cash['amount']
|
169 |
+
krw_new_pct = (krw_new_amount / total_value) * 100
|
170 |
+
formatted_krw_difference = f"{krw_difference:+,}" if krw_difference != 0 else "0"
|
171 |
+
result_message += (
|
172 |
+
f"Cash\n"
|
173 |
+
f" {krw_cash['amount']:,} [{(krw_cash['amount'] / total_value) * 100:.1f}%]\n"
|
174 |
+
f" [{krw_target_weight * 100:.1f}%] {formatted_krw_difference}\n\n"
|
175 |
+
)
|
176 |
+
|
177 |
+
result_message += f"Total Portfolio Value: {total_value:,}"
|
178 |
+
logger.info("Portfolio rebalancing information calculated")
|
179 |
+
return result_message
|
180 |
+
|
181 |
+
def rebalance_portfolio(input_text):
|
182 |
+
try:
|
183 |
+
stock_inputs, krw_cash = parse_input(input_text)
|
184 |
+
portfolio, target_weights, krw_cash = build_portfolio(stock_inputs, krw_cash)
|
185 |
+
result_message = get_portfolio_rebalancing_info(portfolio, target_weights, krw_cash)
|
186 |
+
return result_message
|
187 |
+
except Exception as e:
|
188 |
+
return str(e)
|
189 |
+
|
190 |
+
# Gradio 인터페이스 설정
|
191 |
+
interface = gr.Interface(
|
192 |
+
fn=rebalance_portfolio,
|
193 |
+
inputs="text",
|
194 |
+
outputs="text",
|
195 |
+
title=f"Re-Balancing Analysis | Your Portfolio Holdings as of {current_date} | All Accounts",
|
196 |
+
description="Main currency: KRW | Enter portfolio data in the format: usd schd 21 8, krw 368590 530 2, 518192 1/3"
|
197 |
+
)
|
198 |
+
|
199 |
+
if __name__ == "__main__":
|
200 |
+
interface.launch(share=True)
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
python-telegram-bot==13.15
|
2 |
+
beautifulsoup4
|
3 |
+
finance-datareader
|
4 |
+
requests
|
5 |
+
plotly
|
6 |
+
gradio
|