Files
crypto_trader/src/risk/position_sizing.py

145 lines
4.5 KiB
Python
Raw Normal View History

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