157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
|
|
"""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
|