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

31 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 17:12 +0000

1import math 

2 

3from finbot.execution.order import OrderSide 

4from finbot.risk import SizingDecision 

5 

6 

7class PositionSizer: 

8 def size( 

9 self, 

10 *, 

11 order_side: OrderSide, 

12 market_price: float, 

13 order_value_budget: float, 

14 fee_rate: float = 0.0, 

15 slippage_rate: float = 0.0, 

16 max_quantity: float | None = None, 

17 ) -> SizingDecision: 

18 self._validate_inputs( 

19 market_price=market_price, 

20 order_value_budget=order_value_budget, 

21 fee_rate=fee_rate, 

22 slippage_rate=slippage_rate, 

23 max_quantity=max_quantity, 

24 ) 

25 

26 if order_side == OrderSide.BUY: 

27 execution_price = market_price * (1.0 + slippage_rate) 

28 cost_per_unit = execution_price * (1.0 + fee_rate) 

29 

30 elif order_side == OrderSide.SELL: 

31 execution_price = market_price * (1.0 - slippage_rate) 

32 cost_per_unit = execution_price 

33 

34 else: 

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

36 

37 quantity = order_value_budget / cost_per_unit 

38 

39 if max_quantity is not None: 

40 quantity = min(quantity, max_quantity) 

41 

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

43 return SizingDecision.reject( 

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

45 ) 

46 

47 return SizingDecision.accept(quantity) 

48 

49 @staticmethod 

50 def _validate_inputs( 

51 *, 

52 market_price: float, 

53 order_value_budget: float, 

54 fee_rate: float, 

55 slippage_rate: float, 

56 max_quantity: float | None, 

57 ) -> None: 

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

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

60 

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

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

63 

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

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

66 

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

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

69 

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

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