penti/backend/app/backtest/rebalancer.py

157 lines
5.1 KiB
Python
Raw Permalink Normal View History

2026-01-31 23:30:51 +09:00
"""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