Coverage for src/finbot/execution/broker.py: 100%

34 statements  

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

1import math 

2from datetime import datetime, UTC 

3 

4from finbot.execution.fill import Fill 

5from finbot.execution.order import Order, OrderSide 

6from finbot.portfolio.portfolio import Portfolio 

7 

8 

9class SimulatedBroker: 

10 """A simulated financy broker. TBD: Replace with real brokers""" 

11 

12 def __init__(self, portfolio: Portfolio, fee_rate: float = 0.0, slippage_rate: float = 0.0): 

13 if not math.isfinite(fee_rate): 

14 raise ValueError("Fee rate must be a valid number.") 

15 if not math.isfinite(slippage_rate): 

16 raise ValueError("Slippage rate must be a valid number.") 

17 

18 if fee_rate < 0: 

19 raise ValueError("Fee rate cannot be negative.") 

20 if not 0 <= slippage_rate <= 1.0: 

21 raise ValueError("Slippage rate must be between 0.0 and 1.0.") 

22 

23 self.portfolio = portfolio 

24 self.fee_rate = fee_rate 

25 self.slippage_rate = slippage_rate 

26 

27 def execute( 

28 self, 

29 order: Order, 

30 market_price: float, 

31 timestamp: datetime | None = None, 

32 ) -> Fill: 

33 if not math.isfinite(market_price): 

34 raise ValueError("Market price must be a valid number.") 

35 if market_price <= 0: 

36 raise ValueError("Market price must be greater than zero.") 

37 

38 if timestamp is None: timestamp = datetime.now(UTC) 

39 

40 if order.side == OrderSide.BUY: 

41 price = market_price * (1 + self.slippage_rate) 

42 elif order.side == OrderSide.SELL: 

43 price = market_price * (1 - self.slippage_rate) 

44 else: 

45 raise ValueError(f"Unsupported order side: {order.side}") 

46 

47 slippage_cost = abs(price - market_price) * order.quantity 

48 fee = price * order.quantity * self.fee_rate 

49 

50 fill = Fill( 

51 symbol=order.symbol, 

52 side=order.side, 

53 quantity=order.quantity, 

54 price=price, 

55 timestamp=timestamp, 

56 fee=fee, 

57 slippage_cost=slippage_cost, 

58 ) 

59 

60 self.portfolio.apply_fill(fill) 

61 

62 return fill