"""Position sizing rules.""" from decimal import Decimal from typing import Optional from src.core.config import get_config from src.core.logger import get_logger from src.exchanges.base import BaseExchangeAdapter logger = get_logger(__name__) class PositionSizingManager: """Manages position sizing calculations.""" def __init__(self): """Initialize position sizing manager.""" self.config = get_config() self.logger = get_logger(__name__) def calculate_size( self, symbol: str, price: Decimal, balance: Decimal, risk_percent: Optional[Decimal] = None, exchange_adapter: Optional[BaseExchangeAdapter] = None ) -> Decimal: """Calculate position size, accounting for fees. Args: symbol: Trading symbol price: Entry price balance: Available balance risk_percent: Risk percentage (uses config default if None) exchange_adapter: Exchange adapter for fee calculation (optional) Returns: Calculated position size """ if risk_percent is None: risk_percent = Decimal(str( self.config.get("risk.position_size_percent", 2.0) )) / 100 position_value = balance * risk_percent # Account for fees by reserving fee amount from src.trading.fee_calculator import get_fee_calculator fee_calculator = get_fee_calculator() # Reserve ~0.4% for round-trip fees fee_reserve = fee_calculator.calculate_fee_reserve( position_value=position_value, exchange_adapter=exchange_adapter, reserve_percent=0.004 # 0.4% for round-trip ) # Adjust position value to account for fees adjusted_position_value = position_value - fee_reserve if price > 0: quantity = adjusted_position_value / price return max(Decimal(0), quantity) # Ensure non-negative return Decimal(0) def calculate_kelly_criterion( self, win_rate: float, avg_win: float, avg_loss: float ) -> Decimal: """Calculate position size using Kelly Criterion. Args: win_rate: Win rate (0.0 to 1.0) avg_win: Average win amount avg_loss: Average loss amount Returns: Kelly percentage """ if avg_loss == 0: return Decimal(0) kelly = (win_rate * avg_win - (1 - win_rate) * avg_loss) / avg_win # Use fractional Kelly (half) for safety return Decimal(str(kelly / 2)) def validate_position_size( self, symbol: str, quantity: Decimal, price: Decimal, balance: Decimal, exchange_adapter: Optional[BaseExchangeAdapter] = None ) -> bool: """Validate position size against limits, accounting for fees. Args: symbol: Trading symbol quantity: Position quantity price: Entry price balance: Available balance exchange_adapter: Exchange adapter for fee calculation (optional) Returns: True if position size is valid """ position_value = quantity * price # Calculate estimated fee for this trade from src.trading.fee_calculator import get_fee_calculator from src.core.database import OrderType fee_calculator = get_fee_calculator() estimated_fee = fee_calculator.calculate_fee( quantity=quantity, price=price, order_type=OrderType.MARKET, # Use market as worst case exchange_adapter=exchange_adapter ) total_cost = position_value + estimated_fee # Check if exceeds available balance (including fees) if total_cost > balance: self.logger.warning( f"Position cost {total_cost} (value: {position_value}, fee: {estimated_fee}) " f"exceeds balance {balance}" ) return False # Check against risk limits max_position_percent = Decimal(str( self.config.get("risk.position_size_percent", 2.0) )) / 100 if position_value > balance * max_position_percent: self.logger.warning(f"Position size exceeds risk limit") return False return True