Spaces:
Sleeping
Sleeping
import math | |
import pytz | |
from datetime import datetime | |
from concurrent.futures import ThreadPoolExecutor | |
import FinanceDataReader as fdr | |
from utils.css import load_css | |
def parse_input(text, cash_amount, cash_ratio): | |
lines = text.strip().split(',') | |
stock_inputs = [] | |
total_target_weight = 0 | |
for line in lines: | |
parts = line.split() | |
if len(parts) == 4: | |
stock_code, currency_code, quantity_expr, target_weight_expr = parts | |
quantity = math.floor(eval(quantity_expr.replace(' ', ''))) | |
target_weight = eval(target_weight_expr.replace(' ', '')) | |
target_ratio = (1 - cash_ratio / 100) * target_weight | |
stock_inputs.append((currency_code, stock_code, quantity, target_weight, target_ratio)) | |
total_target_weight += target_weight | |
cash_amount = math.floor(cash_amount) if cash_amount else 0 | |
default_currency_cash = {'amount': cash_amount, 'target_weight': cash_ratio / 100.0} | |
stock_total_weight = total_target_weight | |
for i in range(len(stock_inputs)): | |
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) | |
return stock_inputs, default_currency_cash | |
def get_exchange_rate(currency_code, default_currency): | |
if currency_code.lower() == default_currency.lower(): | |
return 1.0 | |
ticker = f"{currency_code.upper()}{default_currency.upper()}=X" | |
data = fdr.DataReader(ticker) | |
if not data.empty: | |
return data['Close'].iloc[0] | |
else: | |
raise ValueError("Failed to retrieve exchange rate data.") | |
def get_exchange_reflected_stock_price(stock_code, currency_code, default_currency): | |
new_price = get_current_stock_price(stock_code) | |
exchange_rate = get_exchange_rate(currency_code, default_currency) | |
return math.floor(new_price * exchange_rate) | |
def get_current_stock_price(stock_code): | |
df = fdr.DataReader(stock_code) | |
return df['Close'].iloc[-1] | |
def build_portfolio(stock_inputs, default_currency_cash, default_currency): | |
portfolio = {} | |
target_weights = {} | |
with ThreadPoolExecutor() as executor: | |
results = executor.map(lambda x: (x[1], get_exchange_reflected_stock_price(x[1], x[0], default_currency), x[2], x[3], x[4], x[0]), stock_inputs) | |
for stock_code, new_price, quantity, target_weight, target_ratio, currency_code in results: | |
portfolio[stock_code] = {'quantity': quantity, 'price': new_price, 'target_weight': target_weight, 'currency': currency_code} | |
target_weights[stock_code] = target_ratio | |
return portfolio, target_weights, default_currency_cash | |
def format_quantity(quantity): | |
if quantity < 0: | |
return f"({-quantity:,})" | |
else: | |
return f"{quantity:,}" | |
def get_portfolio_rebalancing_info(portfolio, target_weights, krw_cash, default_currency): | |
css = load_css() | |
kst = pytz.timezone('Asia/Seoul') | |
current_time = datetime.now(kst).strftime("%I:%M %p %b-%d-%Y") | |
total_value = sum(stock['price'] * stock['quantity'] for stock in portfolio.values()) + krw_cash['amount'] | |
total_new_stock_value = 0 | |
total_trade_value = 0 | |
adjustments = [] | |
currency_symbols = { | |
"KRW": "₩", | |
"USD": "$", | |
"EUR": "€", | |
"JPY": "¥", | |
"GBP": "£", | |
"AUD": "A$", | |
"CAD": "C$", | |
"CHF": "CHF", | |
"CNY": "¥", | |
"INR": "₹", | |
"BRL": "R$", | |
"ZAR": "R", | |
"SGD": "S$", | |
"HKD": "HK$" | |
} | |
currency_symbol = currency_symbols.get(default_currency.upper(), "") | |
# Calculate current weights and values | |
current_weights = {stock_code: (stock['price'] * stock['quantity'] / total_value) * 100 for stock_code, stock in portfolio.items()} | |
current_values = {stock_code: stock['price'] * stock['quantity'] for stock_code, stock in portfolio.items()} | |
# Include cash in current weights and values | |
current_weights['CASH'] = (krw_cash['amount'] / total_value) * 100 | |
current_values['CASH'] = krw_cash['amount'] | |
# Sort stocks by current weight in descending order | |
sorted_stocks = sorted(current_weights.items(), key=lambda x: x[1], reverse=True) | |
# Display current weights and values section | |
current_info_html = "<h3>Your Portfolio Holdings</h3><div class='table-container'><table style='border-collapse: collapse;'>" | |
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>" | |
for stock_code, weight in sorted_stocks: | |
current_info_html += ( | |
f"<tr>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{stock_code.upper()}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{weight:.1f}%</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{currency_symbol}{current_values[stock_code]:,.0f}</td>" | |
f"</tr>" | |
) | |
current_info_html += "</tbody></table></div><br>" | |
for stock_code, stock_data in portfolio.items(): | |
current_value = stock_data['price'] * stock_data['quantity'] | |
target_value = total_value * target_weights.get(stock_code, 0) | |
difference = target_value - current_value | |
trade_quantity = math.floor(difference / stock_data['price']) if difference > 0 else -math.ceil(-difference / stock_data['price']) | |
new_quantity = trade_quantity + stock_data['quantity'] | |
new_value = new_quantity * stock_data['price'] | |
trade_value = trade_quantity * stock_data['price'] | |
total_trade_value += abs(trade_value) | |
total_new_stock_value += new_value | |
current_value_pct = (current_value / total_value) * 100 | |
new_value_pct = (new_value / total_value) * 100 | |
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'])) | |
krw_new_amount = total_value - total_new_stock_value | |
krw_target_value = total_value * krw_cash['target_weight'] | |
krw_difference = krw_new_amount - krw_cash['amount'] | |
trade_quantity = krw_difference | |
new_quantity = krw_cash['amount'] + trade_quantity | |
new_value = new_quantity | |
trade_value = trade_quantity | |
current_value = krw_cash['amount'] | |
current_value_pct = (current_value / total_value) * 100 | |
new_value_pct = (new_value / total_value) * 100 | |
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')) | |
portfolio_info = css + f""" | |
<div><br> | |
<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> | |
<br></div> | |
""" | |
currency_totals = {stock_data['currency']: {'amount': 0, 'weight': 0} for stock_data in portfolio.values()} | |
for stock_code, stock_data in portfolio.items(): | |
currency = stock_data['currency'] | |
current_value = stock_data['price'] * stock_data['quantity'] | |
currency_totals[currency]['amount'] += current_value | |
currency_totals[currency]['weight'] += current_value / total_value | |
currency_totals['CASH'] = {'amount': krw_cash['amount'], 'weight': krw_cash['amount'] / total_value} | |
sorted_currencies = sorted(currency_totals.items(), key=lambda x: x[1]['weight'], reverse=True) | |
currency_table = "<h3>Your Portfolio by Currency</h3><div class='table-container wrap-text'><table style='border-collapse: collapse;'>" | |
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>" | |
for currency, data in sorted_currencies: | |
currency_table += ( | |
f"<tr>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{currency.upper()}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{data['weight'] * 100:.1f}%</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{currency_symbol}{data['amount']:,}</td>" | |
f"</tr>" | |
) | |
currency_table += "</tbody></table></div><br>" | |
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;'>" | |
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>" | |
for adj in adjustments: | |
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 | |
Buy_or_Sell = "" | |
if trade_quantity > 0: | |
Buy_or_Sell = f"<span class='buy-sell buy'>Buy</span>" | |
elif trade_quantity < 0: | |
Buy_or_Sell = f"<span class='buy-sell sell'>Sell</span>" | |
else: | |
Buy_or_Sell = f"<span></span>" | |
current_value_pct_str = f"{current_value_pct:.1f}%" | |
target_weight_str = f"<span class='highlight-edit'>{target_weight}</span>" if stock_code != 'CASH' else '' | |
target_ratio_str = f"<span class='highlight-edit'>{target_ratio * 100:.1f}%</span>" if stock_code == 'CASH' else f"{target_ratio * 100:.1f}%" | |
trade_value_str = f"<span class='highlight-sky'>{format_quantity(trade_value)}</span>" if trade_value != 0 else '' | |
price_str = f"{currency_symbol}{price:,.0f}" if stock_code != 'CASH' else '' | |
trade_quantity_str = ( | |
f"<span class='highlight-sky'>{format_quantity(trade_quantity)}</span>" | |
if stock_code != 'CASH' and trade_value != 0 else '' | |
) | |
old_quantity_str = f"{old_quantity:,.0f} → {new_quantity:,.0f}" if stock_code != 'CASH' else '' | |
new_value_str = f"{currency_symbol}{new_value:,.0f}" | |
new_value_pct_str = f"{new_value_pct:.1f}%" | |
result_message += ( | |
f"<tr>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{stock_code.upper()}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{current_value_pct_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{target_weight_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{target_ratio_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{Buy_or_Sell}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{trade_value_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{price_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{trade_quantity_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{old_quantity_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{new_value_str}</td>" | |
f"<td style='border: 1px hidden #ddd; text-align: center;'>{new_value_pct_str}</td>" | |
f"</tr>" | |
) | |
result_message += "</tbody></table></div>" | |
return result_message | |
def rebalancing_tool(user_input, cash_amount, cash_ratio, default_currency): | |
try: | |
stock_inputs, default_currency_cash = parse_input(user_input, cash_amount, cash_ratio) | |
portfolio, target_weights, default_currency_cash = build_portfolio(stock_inputs, default_currency_cash, default_currency) | |
result = get_portfolio_rebalancing_info(portfolio, target_weights, default_currency_cash, default_currency) | |
return result | |
except Exception as e: | |
return str(e) | |