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
« 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
8class OrderSide(StrEnum):
9 BUY = "buy"
10 SELL = "sell"
13class OrderType(StrEnum):
14 """tbd"""
15 MARKET = "market"
16 LIMIT = "limit"
17 STOP = "stop"
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"
30@dataclass(frozen=True)
31class Order:
32 symbol: str
33 side: OrderSide
34 quantity: float
35 client_order_id: str | None = None
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.")
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.")
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.")
53@dataclass(frozen=True)
54class OrderState:
55 order_id: str
56 order: Order
57 status: OrderStatus
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.")
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
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.")
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.")
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.")
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.")
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.")
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 }
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
124 return data
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 )
141@dataclass(frozen=True)
142class BrokerPosition:
143 symbol: str
144 quantity: float
145 average_entry_price: float
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.")
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.")
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.")