Coverage for src/finbot/execution/models.py: 89%

101 statements  

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

1import math 

2from dataclasses import dataclass 

3from datetime import datetime 

4from enum import StrEnum 

5from typing import Any 

6 

7 

8class OrderSide(StrEnum): 

9 BUY = "buy" 

10 SELL = "sell" 

11 

12 

13class OrderType(StrEnum): 

14 """tbd""" 

15 MARKET = "market" 

16 LIMIT = "limit" 

17 STOP = "stop" 

18 

19 

20class OrderStatus(StrEnum): 

21 PENDING = "pending" 

22 OPEN = "open" 

23 PARTIALLY_FILLED = "partially_filled" 

24 FILLED = "filled" 

25 CANCELED = "canceled" 

26 REJECTED = "rejected" 

27 EXPIRED = "expired" 

28 

29 

30@dataclass(frozen=True) 

31class Order: 

32 symbol: str 

33 side: OrderSide 

34 quantity: float 

35 client_order_id: str | None = None 

36 

37 def __post_init__(self): 

38 if not isinstance(self.symbol, str) or not self.symbol.strip(): 

39 raise ValueError("Symbol must be a non-empty string.") 

40 

41 if not math.isfinite(self.quantity): 

42 raise ValueError("Quantity must have a finite value.") 

43 if self.quantity <= 0: 

44 raise ValueError("Quantity must be greater than zero.") 

45 

46 if self.client_order_id is not None and ( 

47 not isinstance(self.client_order_id, str) 

48 or not self.client_order_id.strip() 

49 ): 

50 raise ValueError("Client order ID must be a non-empty string.") 

51 

52 

53@dataclass(frozen=True) 

54class OrderState: 

55 order_id: str 

56 order: Order 

57 status: OrderStatus 

58 

59 def __post_init__(self): 

60 if not isinstance(self.order_id, str) or not self.order_id.strip(): 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true

61 raise ValueError("Order ID must be a non-empty string.") 

62 

63 

64@dataclass(frozen=True) 

65class Fill: 

66 symbol: str 

67 side: OrderSide 

68 quantity: float 

69 price: float 

70 timestamp: datetime 

71 fee: float = 0.0 

72 slippage_cost: float = 0.0 

73 fill_id: str | None = None 

74 order_id: str | None = None 

75 

76 def __post_init__(self): 

77 if not isinstance(self.symbol, str) or not self.symbol.strip(): 

78 raise ValueError("Symbol must be a non-empty string.") 

79 

80 if not math.isfinite(self.quantity): 

81 raise ValueError("Quantity must be a valid number.") 

82 if not math.isfinite(self.price): 

83 raise ValueError("Price must be a valid number.") 

84 if not math.isfinite(self.fee): 

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

86 if not math.isfinite(self.slippage_cost): 

87 raise ValueError("Slippage cost must be a valid number.") 

88 

89 if self.quantity <= 0: 

90 raise ValueError("Quantity must be greater than zero.") 

91 if self.price <= 0: 

92 raise ValueError("Price must be greater than zero.") 

93 if self.fee < 0: 

94 raise ValueError("Fee cannot be negative.") 

95 if self.slippage_cost < 0: 

96 raise ValueError("Slippage cost cannot be negative.") 

97 

98 if self.fill_id is not None and ( 98 ↛ 101line 98 didn't jump to line 101 because the condition on line 98 was never true

99 not isinstance(self.fill_id, str) or not self.fill_id.strip() 

100 ): 

101 raise ValueError("Fill ID must be a non-empty string.") 

102 

103 if self.order_id is not None and ( 103 ↛ 106line 103 didn't jump to line 106 because the condition on line 103 was never true

104 not isinstance(self.order_id, str) or not self.order_id.strip() 

105 ): 

106 raise ValueError("Order ID must be a non-empty string.") 

107 

108 def serialize(self) -> dict[str, Any]: 

109 data = { 

110 "symbol": self.symbol, 

111 "side": self.side.value, 

112 "quantity": self.quantity, 

113 "price": self.price, 

114 "timestamp": self.timestamp.isoformat(), 

115 "fee": self.fee, 

116 "slippage_cost": self.slippage_cost, 

117 } 

118 

119 if self.fill_id is not None: 119 ↛ 121line 119 didn't jump to line 121 because the condition on line 119 was always true

120 data["fill_id"] = self.fill_id 

121 if self.order_id is not None: 121 ↛ 124line 121 didn't jump to line 124 because the condition on line 121 was always true

122 data["order_id"] = self.order_id 

123 

124 return data 

125 

126 @classmethod 

127 def deserialize(cls, data: dict[str, Any]) -> "Fill": 

128 return cls( 

129 symbol=data["symbol"], 

130 side=OrderSide(data["side"]), 

131 quantity=data["quantity"], 

132 price=data["price"], 

133 timestamp=datetime.fromisoformat(data["timestamp"]), 

134 fee=data.get("fee", 0.0), 

135 slippage_cost=data.get("slippage_cost", 0.0), 

136 fill_id=data.get("fill_id"), 

137 order_id=data.get("order_id"), 

138 ) 

139 

140 

141@dataclass(frozen=True) 

142class BrokerPosition: 

143 symbol: str 

144 quantity: float 

145 average_entry_price: float 

146 

147 def __post_init__(self): 

148 if not isinstance(self.symbol, str) or not self.symbol.strip(): 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true

149 raise ValueError("Symbol must be a non-empty string.") 

150 

151 if not math.isfinite(self.quantity): 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true

152 raise ValueError("Position quantity must be finite.") 

153 

154 if not math.isfinite(self.average_entry_price): 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true

155 raise ValueError("Average entry price must be finite.") 

156 if self.average_entry_price <= 0: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true

157 raise ValueError("Average entry price must be greater than zero.")