"""Portfolio rebalancing logic for backtesting.""" from typing import Dict, List, Tuple from decimal import Decimal from datetime import datetime from app.backtest.portfolio import BacktestPortfolio class Rebalancer: """포트폴리오 리밸런서.""" def __init__(self, portfolio: BacktestPortfolio): """ 초기화. Args: portfolio: 백테스트 포트폴리오 """ self.portfolio = portfolio def rebalance( self, target_tickers: List[str], current_prices: Dict[str, Decimal], trade_date: datetime, equal_weight: bool = True, target_weights: Dict[str, float] = None ) -> Tuple[List[dict], List[dict]]: """ 포트폴리오 리밸런싱. Args: target_tickers: 목표 종목 리스트 current_prices: 현재 가격 {ticker: price} trade_date: 거래일 equal_weight: 동일 가중 여부 (기본 True) target_weights: 목표 비중 {ticker: weight} (equal_weight=False일 때 사용) Returns: (매도 거래 리스트, 매수 거래 리스트) """ # 가격 업데이트 self.portfolio.update_prices(current_prices) # 현재 보유 종목 current_tickers = set(self.portfolio.positions.keys()) target_tickers_set = set(target_tickers) # 매도할 종목 (현재 보유 중이지만 목표에 없는 종목) tickers_to_sell = current_tickers - target_tickers_set sell_trades = [] for ticker in tickers_to_sell: position = self.portfolio.positions[ticker] price = current_prices.get(ticker, position.current_price) success = self.portfolio.sell( ticker=ticker, quantity=position.quantity, price=price, trade_date=trade_date ) if success: sell_trades.append({ 'ticker': ticker, 'action': 'sell', 'quantity': float(position.quantity), 'price': float(price), 'date': trade_date }) # 총 포트폴리오 가치 (매도 후) total_value = self.portfolio.get_total_value() # 목표 비중 계산 if equal_weight: weights = {ticker: 1.0 / len(target_tickers) for ticker in target_tickers} else: weights = target_weights or {} # 목표 금액 계산 target_values = { ticker: total_value * Decimal(str(weights.get(ticker, 0))) for ticker in target_tickers } # 현재 보유 금액 current_values = { ticker: self.portfolio.positions[ticker].market_value if ticker in self.portfolio.positions else Decimal("0") for ticker in target_tickers } buy_trades = [] for ticker in target_tickers: target_value = target_values[ticker] current_value = current_values[ticker] price = current_prices.get(ticker) if price is None or price == 0: continue # 매수/매도 필요 금액 delta_value = target_value - current_value if delta_value > 0: # 매수 quantity = delta_value / price # 정수 주로 변환 (소수점 버림) quantity = Decimal(int(quantity)) if quantity > 0: success = self.portfolio.buy( ticker=ticker, quantity=quantity, price=price, trade_date=trade_date ) if success: buy_trades.append({ 'ticker': ticker, 'action': 'buy', 'quantity': float(quantity), 'price': float(price), 'date': trade_date }) elif delta_value < 0: # 추가 매도 quantity = abs(delta_value) / price quantity = Decimal(int(quantity)) if quantity > 0 and ticker in self.portfolio.positions: # 보유 수량을 초과하지 않도록 max_quantity = self.portfolio.positions[ticker].quantity quantity = min(quantity, max_quantity) success = self.portfolio.sell( ticker=ticker, quantity=quantity, price=price, trade_date=trade_date ) if success: sell_trades.append({ 'ticker': ticker, 'action': 'sell', 'quantity': float(quantity), 'price': float(price), 'date': trade_date }) return sell_trades, buy_trades