Coverage for src/finbot/execution/fill.py: 100%
38 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 17:12 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 17:12 +0000
1import math
2from dataclasses import dataclass
3from datetime import datetime
4from typing import Any
6from finbot.execution.order import OrderSide
9@dataclass(frozen=True)
10class Fill:
11 symbol: str
12 side: OrderSide
13 quantity: float
14 price: float
15 timestamp: datetime
16 fee: float = 0.0
17 slippage_cost: float = 0.0
19 def __post_init__(self):
20 if not isinstance(self.symbol, str) or not self.symbol.strip():
21 raise ValueError("Symbol must be a non-empty string.")
23 if not math.isfinite(self.quantity):
24 raise ValueError("Quantity must be a valid number.")
25 if not math.isfinite(self.price):
26 raise ValueError("Price must be a valid number.")
27 if not math.isfinite(self.fee):
28 raise ValueError("Fee must be a valid number.")
29 if not math.isfinite(self.slippage_cost):
30 raise ValueError("Slippage cost must be a valid number.")
32 if self.quantity <= 0:
33 raise ValueError("Quantity must be greater than zero.")
34 if self.price <= 0:
35 raise ValueError("Price must be greater than zero.")
36 if self.fee < 0:
37 raise ValueError("Fee cannot be negative.")
38 if self.slippage_cost < 0:
39 raise ValueError("Slippage cost cannot be negative.")
41 def serialize(self) -> dict[str, Any]:
42 return {
43 "symbol": self.symbol,
44 "side": self.side.value,
45 "quantity": self.quantity,
46 "price": self.price,
47 "timestamp": self.timestamp.isoformat(),
48 "fee": self.fee,
49 "slippage_cost": self.slippage_cost,
50 }
52 @classmethod
53 def deserialize(cls, data: dict[str, Any]) -> "Fill":
54 return cls(
55 symbol=data["symbol"],
56 side=OrderSide(data["side"]),
57 quantity=data["quantity"],
58 price=data["price"],
59 timestamp=datetime.fromisoformat(data["timestamp"]),
60 fee=data.get("fee", 0.0),
61 slippage_cost=data.get("slippage_cost", 0.0),
62 )