Coverage for src/finbot/risk/position_sizer.py: 96%

31 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-04 12:05 +0000

1import math 

2 

3from finbot.execution.models import OrderSide 

4from finbot.risk.models import SizingDecision 

5 

6 

7class PositionSizer: 

8 def size( 

9 self, 

10 order_side: OrderSide, 

11 market_price: float, 

12 order_value_budget: float, 

13 fee_rate: float = 0.0, 

14 slippage_rate: float = 0.0, 

15 max_quantity: float | None = None, 

16 ) -> SizingDecision: 

17 self._validate_inputs( 

18 market_price=market_price, 

19 order_value_budget=order_value_budget, 

20 fee_rate=fee_rate, 

21 slippage_rate=slippage_rate, 

22 max_quantity=max_quantity, 

23 ) 

24 

25 if order_side == OrderSide.BUY: 

26 execution_price = market_price * (1.0 + slippage_rate) 

27 cost_per_unit = execution_price * (1.0 + fee_rate) 

28 

29 elif order_side == OrderSide.SELL: 

30 execution_price = market_price * (1.0 - slippage_rate) 

31 cost_per_unit = execution_price 

32 

33 else: 

34 return SizingDecision.reject(f"Unsupported order side: {order_side}") 

35 

36 quantity = order_value_budget / cost_per_unit 

37 

38 if max_quantity is not None: 

39 quantity = min(quantity, max_quantity) 

40 

41 if quantity <= 0: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true

42 return SizingDecision.reject( 

43 "Order value budget is too small to produce a positive quantity.", 

44 ) 

45 

46 return SizingDecision.accept(quantity) 

47 

48 @staticmethod 

49 def _validate_inputs( 

50 *, 

51 market_price: float, 

52 order_value_budget: float, 

53 fee_rate: float, 

54 slippage_rate: float, 

55 max_quantity: float | None, 

56 ) -> None: 

57 if not math.isfinite(market_price) or market_price <= 0: 

58 raise ValueError("market_price must be a positive finite number.") 

59 

60 if not math.isfinite(order_value_budget) or order_value_budget <= 0: 

61 raise ValueError("order_value_budget must be a positive finite number.") 

62 

63 if not math.isfinite(fee_rate) or fee_rate < 0: 

64 raise ValueError("fee_rate must be a non-negative finite number.") 

65 

66 if not math.isfinite(slippage_rate) or not 0.0 <= slippage_rate <= 1.0: 

67 raise ValueError("slippage_rate must be between 0.0 and 1.0.") 

68 

69 if max_quantity is not None and (not math.isfinite(max_quantity) or max_quantity <= 0): 

70 raise ValueError("max_quantity must be a positive finite number.")