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 = "

Your Portfolio Holdings

" current_info_html += "" for stock_code, weight in sorted_stocks: current_info_html += ( f"" f"" f"" f"" f"" ) current_info_html += "
Stock CodeCurrent Weight (%)Current Value
{stock_code.upper()}{weight:.1f}%{currency_symbol}{current_values[stock_code]:,.0f}

" 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"""

{currency_symbol}{total_value:,.0f} as of {current_time}


""" 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 = "

Your Portfolio by Currency

" currency_table += "" for currency, data in sorted_currencies: currency_table += ( f"" f"" f"" f"" f"" ) currency_table += "
CurrencyTotal Weight (%)Total Value
{currency.upper()}{data['weight'] * 100:.1f}%{currency_symbol}{data['amount']:,}

" result_message = portfolio_info + current_info_html + currency_table + "

Re-Balancing Analysis

" result_message += "" 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"Buy" elif trade_quantity < 0: Buy_or_Sell = f"Sell" else: Buy_or_Sell = f"" current_value_pct_str = f"{current_value_pct:.1f}%" target_weight_str = f"{target_weight}" if stock_code != 'CASH' else '' target_ratio_str = f"{target_ratio * 100:.1f}%" if stock_code == 'CASH' else f"{target_ratio * 100:.1f}%" trade_value_str = f"{format_quantity(trade_value)}" if trade_value != 0 else '' price_str = f"{currency_symbol}{price:,.0f}" if stock_code != 'CASH' else '' trade_quantity_str = ( f"{format_quantity(trade_quantity)}" 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"" f"" f"" f"" f"" f"" f"" f"" f"" f"" f"" f"" f"" ) result_message += "
Stock CodeCurrent Weight (%)Target WeightTarget Ratio (%)Buy or Sell?Trade AmountCurrent Price per ShareEstimated # of
Shares to Buy or Sell
Quantity of UnitsMarket Value% Asset Allocation
{stock_code.upper()}{current_value_pct_str}{target_weight_str}{target_ratio_str}{Buy_or_Sell}{trade_value_str}{price_str}{trade_quantity_str}{old_quantity_str}{new_value_str}{new_value_pct_str}
" 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)