diff --git a/gsy_framework/constants_limits.py b/gsy_framework/constants_limits.py index 26994619..98b174a9 100644 --- a/gsy_framework/constants_limits.py +++ b/gsy_framework/constants_limits.py @@ -338,7 +338,7 @@ class HeartBeat: JWT_TOKEN_EXPIRY_IN_SECS = 48 * 3600 DEFAULT_PRECISION = 8 -ENERGY_RATE_PRECISION = 5 +ENERGY_RATE_PRECISION = DEFAULT_PRECISION # In order to cover conversion and reverse-conversion to 5 decimal points, the tolerance has to be # 0.00002. That way off-by-one consecutive rounding errors would not be treated as errors, e.g. # when recalculating the original energy rate in trade chains. diff --git a/gsy_framework/data_classes.py b/gsy_framework/data_classes.py index bf783fb6..8b409ae8 100644 --- a/gsy_framework/data_classes.py +++ b/gsy_framework/data_classes.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=invalid-name # pylint: disable=redefined-builtin # pylint: disable=too-many-arguments @@ -31,9 +32,11 @@ from pendulum import DateTime from gsy_framework.constants_limits import ( - DEFAULT_PRECISION, FLOATING_POINT_TOLERANCE, ENERGY_RATE_PRECISION) -from gsy_framework.utils import ( - limit_float_precision, datetime_to_string_incl_seconds, str_to_pendulum_datetime) + DEFAULT_PRECISION, + FLOATING_POINT_TOLERANCE, + ENERGY_RATE_PRECISION, +) +from gsy_framework.utils import datetime_to_string_incl_seconds, str_to_pendulum_datetime def json_datetime_serializer(datetime_obj: DateTime) -> Optional[str]: @@ -45,14 +48,22 @@ def json_datetime_serializer(datetime_obj: DateTime) -> Optional[str]: class BaseBidOffer: """Base class defining shared functionality of Bid and Offer market structures.""" - def __init__(self, id: str, creation_time: DateTime, price: float, energy: float, - original_price: Optional[float] = None, time_slot: DateTime = None): + + def __init__( + self, + id: str, + creation_time: DateTime, + price: float, + energy: float, + original_price: Optional[float] = None, + time_slot: DateTime = None, + ): self.id = str(id) self.creation_time = creation_time self.time_slot = time_slot # market slot of creation - self.original_price = limit_float_precision(original_price or price) - self.price = limit_float_precision(price) - self.energy = limit_float_precision(energy) + self.original_price = original_price or price + self.price = price + self.energy = energy self.type = self.__class__.__name__ @property @@ -112,18 +123,24 @@ def from_serializable_dict(cls, offer_bid_dict: Dict) -> Union["Offer", "Bid"]: order_dict_copy.pop("energy_rate", None) order_dict_copy["creation_time"] = ( str_to_pendulum_datetime(order_dict_copy["creation_time"]) - if order_dict_copy.get("creation_time") else None) + if order_dict_copy.get("creation_time") + else None + ) order_dict_copy["time_slot"] = ( str_to_pendulum_datetime(order_dict_copy["time_slot"]) - if order_dict_copy.get("time_slot") else None) + if order_dict_copy.get("time_slot") + else None + ) if order_dict_copy.get("seller"): order_dict_copy["seller"] = TraderDetails.from_serializable_dict( - order_dict_copy["seller"]) + order_dict_copy["seller"] + ) if order_dict_copy.get("buyer"): order_dict_copy["buyer"] = TraderDetails.from_serializable_dict( - order_dict_copy["buyer"]) + order_dict_copy["buyer"] + ) if object_type == "Offer": return Offer(**order_dict_copy) @@ -143,6 +160,7 @@ class TraderDetails: Details about the trader. Includes trader name and unique identifier, and also the original trader for this order. """ + name: str uuid: str origin: Optional[str] = None @@ -150,10 +168,10 @@ class TraderDetails: def __eq__(self, other: "TraderDetails") -> bool: return ( - self.name == other.name and - self.origin == other.origin and - self.uuid == other.uuid and - self.origin_uuid == other.origin_uuid + self.name == other.name + and self.origin == other.origin + and self.uuid == other.uuid + and self.origin_uuid == other.origin_uuid ) def serializable_dict(self) -> Dict: @@ -162,7 +180,7 @@ def serializable_dict(self) -> Dict: "name": self.name, "origin": self.origin, "origin_uuid": self.origin_uuid, - "uuid": self.uuid + "uuid": self.uuid, } @staticmethod @@ -173,11 +191,25 @@ def from_serializable_dict(trader_details: Dict) -> "TraderDetails": class Offer(BaseBidOffer): """Offer class""" - def __init__(self, id: str, creation_time: DateTime, price: float, - energy: float, seller: TraderDetails, original_price: Optional[float] = None, - time_slot: DateTime = None): - super().__init__(id=id, creation_time=creation_time, price=price, energy=energy, - original_price=original_price, time_slot=time_slot) + + def __init__( + self, + id: str, + creation_time: DateTime, + price: float, + energy: float, + seller: TraderDetails, + original_price: Optional[float] = None, + time_slot: DateTime = None, + ): + super().__init__( + id=id, + creation_time=creation_time, + price=price, + energy=energy, + original_price=original_price, + time_slot=time_slot, + ) self.seller = seller def __hash__(self) -> int: @@ -186,37 +218,54 @@ def __hash__(self) -> int: def __repr__(self) -> str: return ( f"") + f" '{self.seller.name} {self.energy_rate}'>" + ) def __str__(self) -> str: - return (f"{{{self.id!s:.6s}}} [origin: {self.seller.origin}] " - f"[{self.seller.name}]: {self.energy} kWh @ {self.price} " - f"@ {self.energy_rate}") + return ( + f"{{{self.id!s:.6s}}} [origin: {self.seller.origin}] " + f"[{self.seller.name}]: {self.energy} kWh @ {self.price} " + f"@ {self.energy_rate}" + ) def serializable_dict(self) -> Dict: """Return a json serializable representation of the class.""" return {**super().serializable_dict(), "seller": self.seller.serializable_dict()} def __eq__(self, other: "Offer") -> bool: - return (self.id == other.id and - isclose(self.energy_rate, other.energy_rate, rel_tol=FLOATING_POINT_TOLERANCE) and - isclose(self.energy, other.energy, rel_tol=FLOATING_POINT_TOLERANCE) and - isclose(self.price, other.price, rel_tol=FLOATING_POINT_TOLERANCE) and - self.seller == other.seller and - self.creation_time == other.creation_time and - self.time_slot == other.time_slot) + return ( + self.id == other.id + and isclose(self.energy_rate, other.energy_rate, rel_tol=FLOATING_POINT_TOLERANCE) + and isclose(self.energy, other.energy, rel_tol=FLOATING_POINT_TOLERANCE) + and isclose(self.price, other.price, rel_tol=FLOATING_POINT_TOLERANCE) + and self.seller == other.seller + and self.creation_time == other.creation_time + and self.time_slot == other.time_slot + ) def csv_values(self) -> Tuple: """Return values of class members that are needed for creation of CSV export.""" rate = round(self.energy_rate, 4) - return (self.creation_time, rate, self.energy, self.price, self.seller.name, - self.seller.origin) + return ( + self.creation_time, + rate, + self.energy, + self.price, + self.seller.name, + self.seller.origin, + ) @classmethod def csv_fields(cls) -> Tuple: """Return labels for csv_values for CSV export.""" - return ("creation_time", "rate [ct./kWh]", "energy [kWh]", "price [ct.]", "seller", - "seller origin") + return ( + "creation_time", + "rate [ct./kWh]", + "energy [kWh]", + "price [ct.]", + "seller", + "seller origin", + ) @property def accumulated_grid_fees(self): @@ -227,19 +276,37 @@ def accumulated_grid_fees(self): def copy(offer: "Offer") -> "Offer": """Return a copy of an offer Object.""" return Offer( - offer.id, offer.creation_time, offer.price, offer.energy, seller=offer.seller, - original_price=offer.original_price, time_slot=offer.time_slot) + offer.id, + offer.creation_time, + offer.price, + offer.energy, + seller=offer.seller, + original_price=offer.original_price, + time_slot=offer.time_slot, + ) class Bid(BaseBidOffer): """Bid class.""" - def __init__(self, id: str, creation_time: DateTime, price: float, - energy: float, buyer: TraderDetails, - original_price: Optional[float] = None, - time_slot: Optional[DateTime] = None - ): - super().__init__(id=id, creation_time=creation_time, price=price, energy=energy, - original_price=original_price, time_slot=time_slot) + + def __init__( + self, + id: str, + creation_time: DateTime, + price: float, + energy: float, + buyer: TraderDetails, + original_price: Optional[float] = None, + time_slot: Optional[DateTime] = None, + ): + super().__init__( + id=id, + creation_time=creation_time, + price=price, + energy=energy, + original_price=original_price, + time_slot=time_slot, + ) self.buyer = buyer def __hash__(self) -> int: @@ -264,14 +331,26 @@ def serializable_dict(self) -> Dict: def csv_values(self) -> Tuple: """Return values of class members that are needed for creation of CSV export.""" rate = round(self.energy_rate, 4) - return (self.creation_time, rate, self.energy, self.price, self.buyer.name, - self.buyer.origin) + return ( + self.creation_time, + rate, + self.energy, + self.price, + self.buyer.name, + self.buyer.origin, + ) @classmethod def csv_fields(cls) -> Tuple: """Return labels for csv_values for CSV export.""" - return ("creation_time", "rate [ct./kWh]", "energy [kWh]", "price [ct.]", "buyer", - "buyer origin") + return ( + "creation_time", + "rate [ct./kWh]", + "energy [kWh]", + "price [ct.]", + "buyer", + "buyer origin", + ) @property def accumulated_grid_fees(self): @@ -279,15 +358,18 @@ def accumulated_grid_fees(self): return self.original_price - self.price def __eq__(self, other: "Bid") -> bool: - return (self.id == other.id and - self.buyer == other.buyer and - self.creation_time == other.creation_time and - self.time_slot == other.time_slot) + return ( + self.id == other.id + and self.buyer == other.buyer + and self.creation_time == other.creation_time + and self.time_slot == other.time_slot + ) @dataclass class TradeBidOfferInfo: """Class that contains information about the original bid or offer.""" + original_bid_rate: Optional[float] propagated_bid_rate: Optional[float] original_offer_rate: Optional[float] @@ -312,24 +394,33 @@ def from_json(trade_bid_offer_info: Union[str, Dict]) -> "TradeBidOfferInfo": def __eq__(self, other: "TradeBidOfferInfo") -> bool: return ( - self.original_offer_rate == other.original_offer_rate and - self.original_bid_rate == other.original_bid_rate and - self.propagated_bid_rate == other.propagated_bid_rate and - self.propagated_offer_rate == other.propagated_offer_rate and - self.trade_rate == self.trade_rate) + self.original_offer_rate == other.original_offer_rate + and self.original_bid_rate == other.original_bid_rate + and self.propagated_bid_rate == other.propagated_bid_rate + and self.propagated_offer_rate == other.propagated_offer_rate + and self.trade_rate == self.trade_rate + ) class Trade: """Trade class.""" - def __init__(self, id: str, creation_time: DateTime, - seller: TraderDetails, buyer: TraderDetails, - traded_energy: float, trade_price: float, - offer: Offer = None, - bid: Bid = None, - residual: Optional[Union[Offer, Bid]] = None, - offer_bid_trade_info: Optional[TradeBidOfferInfo] = None, - fee_price: Optional[float] = None, time_slot: Optional[DateTime] = None, - matching_requirements: Optional[Dict] = None): + + def __init__( + self, + id: str, + creation_time: DateTime, + seller: TraderDetails, + buyer: TraderDetails, + traded_energy: float, + trade_price: float, + offer: Offer = None, + bid: Bid = None, + residual: Optional[Union[Offer, Bid]] = None, + offer_bid_trade_info: Optional[TradeBidOfferInfo] = None, + fee_price: Optional[float] = None, + time_slot: Optional[DateTime] = None, + matching_requirements: Optional[Dict] = None, + ): self.id = str(id) self.creation_time = creation_time @@ -342,9 +433,7 @@ def __init__(self, id: str, creation_time: DateTime, self.seller = seller self.buyer = buyer self.matching_requirements = matching_requirements - self.match_details = { - "offer": offer, "bid": bid - } + self.match_details = {"offer": offer, "bid": bid} def __str__(self) -> str: return ( @@ -356,19 +445,34 @@ def __str__(self) -> str: f"{self.match_details['offer'].id if self.match_details['offer'] else ''} " f"{self.match_details['bid'].id if self.match_details['bid'] else ''} " f"[fee: {self.fee_price} cts.] " - f"{self.matching_requirements or ''}") + f"{self.matching_requirements or ''}" + ) @classmethod def csv_fields(cls) -> Tuple: """Return labels for csv_values for CSV export.""" - return ("creation_time", "rate [ct./kWh]", "energy [kWh]", "seller", "seller origin", - "buyer", "buyer origin") + return ( + "creation_time", + "rate [ct./kWh]", + "energy [kWh]", + "seller", + "seller origin", + "buyer", + "buyer origin", + ) def csv_values(self) -> Tuple: """Return values of class members that are needed for creation of CSV export.""" rate = round(self.trade_rate, 4) - return (self.creation_time, rate, self.traded_energy, self.seller.name, - self.seller.origin, self.buyer.name, self.buyer.origin) + return ( + self.creation_time, + rate, + self.traded_energy, + self.seller.name, + self.seller.origin, + self.buyer.name, + self.buyer.origin, + ) def to_json_string(self) -> str: """Return json string of the representation.""" @@ -386,23 +490,32 @@ def from_dict(cls, trade_dict: Dict) -> "Trade": (including offer, bid, residual and datetimes). """ trade_dict["offer"] = ( - BaseBidOffer.from_json(trade_dict["offer"]) if trade_dict.get("offer") else None) + BaseBidOffer.from_json(trade_dict["offer"]) if trade_dict.get("offer") else None + ) trade_dict["bid"] = ( - BaseBidOffer.from_json(trade_dict["bid"]) if trade_dict.get("bid") else None) + BaseBidOffer.from_json(trade_dict["bid"]) if trade_dict.get("bid") else None + ) trade_dict["residual"] = ( - BaseBidOffer.from_json(trade_dict["residual"]) if trade_dict.get("residual") else None) + BaseBidOffer.from_json(trade_dict["residual"]) if trade_dict.get("residual") else None + ) trade_dict["creation_time"] = ( str_to_pendulum_datetime(trade_dict["creation_time"]) - if trade_dict.get("creation_time") else None) + if trade_dict.get("creation_time") + else None + ) trade_dict["time_slot"] = ( str_to_pendulum_datetime(trade_dict["time_slot"]) - if trade_dict.get("time_slot") else None) + if trade_dict.get("time_slot") + else None + ) trade_dict["offer_bid_trade_info"] = ( TradeBidOfferInfo.from_json(trade_dict["offer_bid_trade_info"]) - if trade_dict.get("offer_bid_trade_info") else None) + if trade_dict.get("offer_bid_trade_info") + else None + ) if trade_dict["seller"] is not None: trade_dict["seller"] = TraderDetails.from_serializable_dict(trade_dict["seller"]) @@ -430,16 +543,18 @@ def serializable_dict(self) -> Dict: """Return a json serializable representation of the class.""" return { "type": "Trade", - "match_type": ( - "Bid" if self.is_bid_trade else "Offer" - ), + "match_type": ("Bid" if self.is_bid_trade else "Offer"), "id": self.id, "bid": ( self.match_details["bid"].serializable_dict() - if self.match_details.get("bid") else None), + if self.match_details.get("bid") + else None + ), "offer": ( self.match_details["offer"].serializable_dict() - if self.match_details.get("offer") else None), + if self.match_details.get("offer") + else None + ), "residual": self.residual.serializable_dict() if self.residual is not None else None, "energy": self.traded_energy, "energy_rate": self.trade_rate, @@ -451,7 +566,9 @@ def serializable_dict(self) -> Dict: "time_slot": datetime_to_string_incl_seconds(self.time_slot), "offer_bid_trade_info": ( self.offer_bid_trade_info.serializable_dict() - if self.offer_bid_trade_info else None) + if self.offer_bid_trade_info + else None + ), } @classmethod @@ -460,43 +577,53 @@ def from_serializable_dict(cls, trade_dict: Dict) -> "Trade": return Trade( id=trade_dict["id"], creation_time=trade_dict["creation_time"], - seller=trade_dict["seller"], buyer=trade_dict["buyer"], - traded_energy=trade_dict["energy"], trade_price=trade_dict["price"], - offer=trade_dict["offer"], bid=trade_dict["bid"], + seller=trade_dict["seller"], + buyer=trade_dict["buyer"], + traded_energy=trade_dict["energy"], + trade_price=trade_dict["price"], + offer=trade_dict["offer"], + bid=trade_dict["bid"], offer_bid_trade_info=trade_dict.get("offer_bid_trade_info"), residual=trade_dict.get("residual"), - time_slot=trade_dict["time_slot"]) + time_slot=trade_dict["time_slot"], + ) def __eq__(self, other: "Trade") -> bool: return ( - self.id == other.id and - self.creation_time == other.creation_time and - self.time_slot == other.time_slot and - self.match_details["offer"] == other.match_details["offer"] and - self.match_details["bid"] == other.match_details["bid"] and - self.seller == other.seller and - self.buyer == other.buyer and - self.traded_energy == other.traded_energy and - self.trade_price == other.trade_price and - self.residual == other.residual and - self.offer_bid_trade_info == other.offer_bid_trade_info) + self.id == other.id + and self.creation_time == other.creation_time + and self.time_slot == other.time_slot + and self.match_details["offer"] == other.match_details["offer"] + and self.match_details["bid"] == other.match_details["bid"] + and self.seller == other.seller + and self.buyer == other.buyer + and self.traded_energy == other.traded_energy + and self.trade_price == other.trade_price + and self.residual == other.residual + and self.offer_bid_trade_info == other.offer_bid_trade_info + ) class BalancingOffer(Offer): """BalancingOffer class.""" def __repr__(self) -> str: - return (f"") + return ( + f"" + ) def __str__(self) -> str: - return (f"") + return ( + f"" + ) class BalancingTrade(Trade): """BalancingTrade class.""" + def __str__(self) -> str: return ( f"{{{self.id!s:.6s}}} [{self.seller.name} -> {self.buyer.name}] " @@ -510,6 +637,7 @@ def __str__(self) -> str: @dataclass class BidOfferMatch: """Representation of a market match.""" + market_id: str time_slot: str bid: Bid.serializable_dict @@ -531,7 +659,7 @@ def serializable_dict(self) -> Dict: "offer": self.offer, "selected_energy": self.selected_energy, "trade_rate": self.trade_rate, - "matching_requirements": self.matching_requirements + "matching_requirements": self.matching_requirements, } @classmethod @@ -546,7 +674,13 @@ def is_valid_dict(cls, bid_offer_match: Dict) -> bool: """Check whether a serialized dict can be a valid BidOfferMatch instance.""" is_valid = True required_arguments = ( - "market_id", "time_slot", "bid", "offer", "selected_energy", "trade_rate") + "market_id", + "time_slot", + "bid", + "offer", + "selected_energy", + "trade_rate", + ) if not all(key in bid_offer_match for key in required_arguments): is_valid = False elif not isinstance(bid_offer_match["market_id"], str): @@ -555,8 +689,10 @@ def is_valid_dict(cls, bid_offer_match: Dict) -> bool: is_valid = False elif not isinstance(bid_offer_match["trade_rate"], (int, float)): is_valid = False - elif not (bid_offer_match.get("matching_requirements") is None or - isinstance(bid_offer_match.get("matching_requirements"), Dict)): + elif not ( + bid_offer_match.get("matching_requirements") is None + or isinstance(bid_offer_match.get("matching_requirements"), Dict) + ): is_valid = False return is_valid @@ -569,8 +705,8 @@ def bid_energy(self): """ if "bid_requirement" in (self.matching_requirements or {}): return ( - self.matching_requirements["bid_requirement"].get("energy") - or self.bid["energy"]) + self.matching_requirements["bid_requirement"].get("energy") or self.bid["energy"] + ) return self.bid["energy"] @property @@ -582,29 +718,26 @@ def bid_energy_rate(self): """ if "bid_requirement" in (self.matching_requirements or {}): if "price" in self.matching_requirements["bid_requirement"]: - return ( - self.matching_requirements["bid_requirement"].get("price") / - self.bid_energy) + return self.matching_requirements["bid_requirement"].get("price") / self.bid_energy return self.bid["energy_rate"] @dataclass class Clearing: """Class that contains information about the market clearing.""" + rate: float energy: float def serializable_dict(self): """Return a json serializable representation of the class.""" - return { - "rate": self.rate, - "energy": self.energy - } + return {"rate": self.rate, "energy": self.energy} @dataclass class MarketClearingState: """MarketClearingState class.""" + cumulative_offers: Dict[str, Dict[DateTime, Dict]] = field(default_factory=dict) cumulative_bids: Dict[str, Dict[DateTime, Dict]] = field(default_factory=dict) clearing: Dict[str, Dict[DateTime, Clearing]] = field(default_factory=dict) diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index bd4e11c5..656ff9bd 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=missing-function-docstring # pylint: disable=missing-class-docstring # pylint: disable=attribute-defined-outside-init @@ -22,14 +23,25 @@ import json import uuid from copy import deepcopy +from math import isclose import pytest from pendulum import DateTime, datetime from gsy_framework.data_classes import ( - BidOfferMatch, BaseBidOffer, Offer, Bid, json_datetime_serializer, - TradeBidOfferInfo, Trade, BalancingOffer, BalancingTrade, Clearing, - MarketClearingState, TraderDetails) + BidOfferMatch, + BaseBidOffer, + Offer, + Bid, + json_datetime_serializer, + TradeBidOfferInfo, + Trade, + BalancingOffer, + BalancingTrade, + Clearing, + MarketClearingState, + TraderDetails, +) from gsy_framework.utils import datetime_to_string_incl_seconds @@ -56,72 +68,86 @@ def test_serializable_dict(): bid={"type": "bid"}, offer={"type": "offer"}, selected_energy=1, - trade_rate=1) - expected_dict = {"market_id": "market_id", - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": 1, - "trade_rate": 1, - "matching_requirements": None} + trade_rate=1, + ) + expected_dict = { + "market_id": "market_id", + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": 1, + "trade_rate": 1, + "matching_requirements": None, + } assert bid_offer_match.serializable_dict() == expected_dict @staticmethod def test_is_valid_dict(): """Test the is_valid_dict method of BidOfferMatch dataclass.""" - bid_offer_match = {"market_id": "market_id", - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": 1, - "trade_rate": 1} + bid_offer_match = { + "market_id": "market_id", + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": 1, + "trade_rate": 1, + } assert BidOfferMatch.is_valid_dict(bid_offer_match) # Key does not exist - bid_offer_match = {"market_id": "market_id", - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": 1, - } + bid_offer_match = { + "market_id": "market_id", + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": 1, + } assert not BidOfferMatch.is_valid_dict(bid_offer_match) # Wrong type - bid_offer_match = {"market_id": "market_id", - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": 1, - "trade_rate": ""} + bid_offer_match = { + "market_id": "market_id", + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": 1, + "trade_rate": "", + } assert not BidOfferMatch.is_valid_dict(bid_offer_match) # No market id - bid_offer_match = {"market_id": None, - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": 1, - "trade_rate": ""} + bid_offer_match = { + "market_id": None, + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": 1, + "trade_rate": "", + } assert not BidOfferMatch.is_valid_dict(bid_offer_match) # No selected energy - bid_offer_match = {"market_id": "market_id", - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": None, - "trade_rate": ""} + bid_offer_match = { + "market_id": "market_id", + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": None, + "trade_rate": "", + } assert not BidOfferMatch.is_valid_dict(bid_offer_match) @staticmethod def test_from_dict(): """Test the from_dict method of BidOfferMatch dataclass.""" - expected_dict = {"market_id": "market_id", - "time_slot": "2021-10-06T12:00", - "bid": {"type": "bid"}, - "offer": {"type": "offer"}, - "selected_energy": 1, - "trade_rate": 1} + expected_dict = { + "market_id": "market_id", + "time_slot": "2021-10-06T12:00", + "bid": {"type": "bid"}, + "offer": {"type": "offer"}, + "selected_energy": 1, + "trade_rate": 1, + } bid_offer_match = BidOfferMatch.from_dict(expected_dict) assert bid_offer_match.market_id == expected_dict["market_id"] assert bid_offer_match.time_slot == expected_dict["time_slot"] @@ -137,8 +163,14 @@ def test_from_dict_returns_none_for_invalid_dict(): @staticmethod def test_bid_energy(): """Test whether the bid_energy property returns the correct value.""" - orders_match = BidOfferMatch(bid={"energy": 10}, offer={}, market_id="", - time_slot="", selected_energy=1, trade_rate=1) + orders_match = BidOfferMatch( + bid={"energy": 10}, + offer={}, + market_id="", + time_slot="", + selected_energy=1, + trade_rate=1, + ) assert orders_match.bid_energy == 10 # When there is a bid_requirement, it should be prioritized @@ -147,15 +179,21 @@ def test_bid_energy(): @staticmethod def test_bid_energy_rate(): - orders_match = BidOfferMatch(bid={"energy_rate": 2, - "energy": 1}, offer={}, market_id="", - time_slot="", selected_energy=1, trade_rate=1) + orders_match = BidOfferMatch( + bid={"energy_rate": 2, "energy": 1}, + offer={}, + market_id="", + time_slot="", + selected_energy=1, + trade_rate=1, + ) assert orders_match.bid_energy_rate == 2 # When there is a bid_requirement, it should be prioritized orders_match.matching_requirements = {"bid_requirement": {"price": 4}} assert orders_match.bid_energy_rate == ( orders_match.matching_requirements["bid_requirement"]["price"] - / orders_match.selected_energy) + / orders_match.selected_energy + ) class TestBaseBidOffer: @@ -175,59 +213,58 @@ def setup_method(self): def test_init(self): """Test __init__.""" - bid_offer = BaseBidOffer( - **self.initial_data - ) + bid_offer = BaseBidOffer(**self.initial_data) assert bid_offer.id == str(self.initial_data["id"]) assert bid_offer.creation_time == self.initial_data["creation_time"] assert bid_offer.price == self.initial_data["price"] assert bid_offer.energy == self.initial_data["energy"] assert bid_offer.original_price == self.initial_data["original_price"] - assert bid_offer.energy_rate == round( - self.initial_data["price"] / self.initial_data["energy"], 5) + assert isclose( + bid_offer.energy_rate, + self.initial_data["price"] / self.initial_data["energy"], + rel_tol=1e-06, + ) # Test whether the original_price will resort to the "price" member self.initial_data.pop("original_price") - bid_offer = BaseBidOffer( - **self.initial_data - ) + bid_offer = BaseBidOffer(**self.initial_data) assert bid_offer.original_price == self.initial_data["price"] def test_update_price(self): - bid_offer = BaseBidOffer( - **self.initial_data - ) + bid_offer = BaseBidOffer(**self.initial_data) bid_offer.update_price(30) assert bid_offer.price == 30 assert bid_offer.energy_rate == 30 / bid_offer.energy def test_update_energy(self): - bid_offer = BaseBidOffer( - **self.initial_data - ) + bid_offer = BaseBidOffer(**self.initial_data) bid_offer.update_energy(40) assert bid_offer.energy == 40 assert bid_offer.energy_rate == bid_offer.price / 40 def test_to_json_string(self): bid_offer_keys = { - "id", "creation_time", "time_slot", "original_price", "price", "energy", - "type", "energy_rate"} - bid_offer = BaseBidOffer( - **self.initial_data - ) + "id", + "creation_time", + "time_slot", + "original_price", + "price", + "energy", + "type", + "energy_rate", + } + bid_offer = BaseBidOffer(**self.initial_data) obj_dict = json.loads(bid_offer.to_json_string()) assert set(obj_dict.keys()) == bid_offer_keys assert json.dumps(obj_dict, sort_keys=True) == json.dumps( - {key: getattr(bid_offer, key) for key in bid_offer_keys}, sort_keys=True, - default=json_datetime_serializer + {key: getattr(bid_offer, key) for key in bid_offer_keys}, + sort_keys=True, + default=json_datetime_serializer, ) def test_serializable_dict(self): - bid_offer = BaseBidOffer( - **self.initial_data - ) + bid_offer = BaseBidOffer(**self.initial_data) assert bid_offer.serializable_dict() == { "type": "BaseBidOffer", "id": str(bid_offer.id), @@ -236,29 +273,22 @@ def test_serializable_dict(self): "energy_rate": bid_offer.energy_rate, "original_price": bid_offer.original_price, "creation_time": datetime_to_string_incl_seconds(bid_offer.creation_time), - "time_slot": datetime_to_string_incl_seconds(bid_offer.time_slot) + "time_slot": datetime_to_string_incl_seconds(bid_offer.time_slot), } def test_from_json(self): - offer = Offer( - **self.initial_data, - seller=self._seller - ) + offer = Offer(**self.initial_data, seller=self._seller) offer_json = offer.to_json_string() assert offer == Offer.from_json(offer_json) - bid = Bid( - **self.initial_data, - buyer=self._buyer - ) + bid = Bid(**self.initial_data, buyer=self._buyer) bid_json = bid.to_json_string() assert bid == Bid.from_json(bid_json) @pytest.mark.parametrize("time_stamp", [None, DEFAULT_DATETIME]) def test_from_json_deals_with_time_stamps_correctly(self, time_stamp): updated_initial_data = self.initial_data - updated_initial_data.update({"time_slot": time_stamp, - "creation_time": time_stamp}) + updated_initial_data.update({"time_slot": time_stamp, "creation_time": time_stamp}) bid = Bid(**updated_initial_data, buyer=self._buyer) bid_json = bid.to_json_string() bid = Bid.from_json(bid_json) @@ -288,21 +318,22 @@ def setup_method(self): "energy": 30, "original_price": 8, "seller": TraderDetails("seller", str(uuid.uuid4())), - "time_slot": DEFAULT_DATETIME + "time_slot": DEFAULT_DATETIME, } def test_init(self): - offer = Offer( - **self.initial_data - ) + offer = Offer(**self.initial_data) assert offer.id == str(self.initial_data["id"]) assert offer.creation_time == self.initial_data["creation_time"] assert offer.time_slot == self.initial_data["time_slot"] assert offer.price == self.initial_data["price"] assert offer.energy == self.initial_data["energy"] - assert offer.original_price == self.initial_data["original_price"] - assert offer.energy_rate == round( - self.initial_data["price"] / self.initial_data["energy"], 5) + assert isclose(offer.original_price, self.initial_data["original_price"]) + assert isclose( + offer.energy_rate, + self.initial_data["price"] / self.initial_data["energy"], + rel_tol=1e-06, + ) assert offer.seller.name == "seller" assert offer.seller.uuid == self.initial_data["seller"].uuid assert offer.seller.origin is None @@ -312,23 +343,25 @@ def test_hash(self): offer = Offer( **self.initial_data, ) - assert offer.__hash__() == hash(offer.id) + assert hash(offer) == hash(offer.id) def test_repr(self): offer = Offer( **self.initial_data, ) - assert (repr(offer) == - f"") + assert ( + repr(offer) == f"" + ) def test_str(self): offer = Offer( **self.initial_data, ) - assert (str(offer) == - f"{{{offer.id!s:.6s}}} [origin: {offer.seller.origin}] " - f"[{offer.seller.name}]: {offer.energy} kWh @ {offer.price} @ {offer.energy_rate}") + assert ( + str(offer) == f"{{{offer.id!s:.6s}}} [origin: {offer.seller.origin}] " + f"[{offer.seller.name}]: {offer.energy} kWh @ {offer.price} @ {offer.energy_rate}" + ) def test_serializable_dict(self): offer = Offer( @@ -343,7 +376,7 @@ def test_serializable_dict(self): "original_price": offer.original_price, "creation_time": datetime_to_string_incl_seconds(offer.creation_time), "seller": offer.seller.serializable_dict(), - "time_slot": datetime_to_string_incl_seconds(offer.time_slot) + "time_slot": datetime_to_string_incl_seconds(offer.time_slot), } def test_from_dict(self): @@ -353,36 +386,38 @@ def test_from_dict(self): assert Offer.from_dict(offer.serializable_dict()) == offer def test_eq(self): - offer = Offer( - **self.initial_data - ) - other_offer = Offer( - **self.initial_data - ) + offer = Offer(**self.initial_data) + other_offer = Offer(**self.initial_data) assert offer == other_offer other_offer.id = "other_id" assert offer != other_offer def test_csv_values(self): - offer = Offer( - **self.initial_data - ) + offer = Offer(**self.initial_data) rate = round(offer.energy_rate, 4) assert offer.csv_values() == ( - offer.creation_time, rate, offer.energy, offer.price, offer.seller.name, - offer.seller.origin) + offer.creation_time, + rate, + offer.energy, + offer.price, + offer.seller.name, + offer.seller.origin, + ) @staticmethod def test_csv_fields(): - assert (Offer.csv_fields() == - ("creation_time", "rate [ct./kWh]", "energy [kWh]", "price [ct.]", "seller", - "seller origin")) + assert Offer.csv_fields() == ( + "creation_time", + "rate [ct./kWh]", + "energy [kWh]", + "price [ct.]", + "seller", + "seller origin", + ) def test_copy(self): - offer = Offer( - **self.initial_data - ) + offer = Offer(**self.initial_data) second_offer = Offer.copy(offer) assert offer == second_offer @@ -400,51 +435,46 @@ def setup_method(self): "energy": 30, "original_price": 8, "buyer": TraderDetails("buyer", str(uuid.uuid4())), - "time_slot": DEFAULT_DATETIME + "time_slot": DEFAULT_DATETIME, } def test_init(self): - bid = Bid( - **self.initial_data - ) + bid = Bid(**self.initial_data) assert bid.id == str(self.initial_data["id"]) assert bid.creation_time == self.initial_data["creation_time"] assert bid.price == self.initial_data["price"] assert bid.energy == self.initial_data["energy"] assert bid.original_price == self.initial_data["original_price"] - assert bid.energy_rate == round( - self.initial_data["price"] / self.initial_data["energy"], 5) + assert isclose( + bid.energy_rate, + self.initial_data["price"] / self.initial_data["energy"], + rel_tol=1e-06, + ) assert bid.buyer.name == "buyer" assert bid.buyer.uuid == self.initial_data["buyer"].uuid assert bid.buyer.origin is None assert bid.buyer.origin_uuid is None def test_hash(self): - bid = Bid( - **self.initial_data - ) - assert bid.__hash__() == hash(bid.id) + bid = Bid(**self.initial_data) + assert hash(bid) == hash(bid.id) def test_repr(self): - bid = Bid( - **self.initial_data + bid = Bid(**self.initial_data) + assert ( + repr(bid) == f"" ) - assert (repr(bid) == - f"") def test_str(self): - bid = Bid( - **self.initial_data + bid = Bid(**self.initial_data) + assert ( + str(bid) == f"{{{bid.id!s:.6s}}} [origin: {bid.buyer.origin}] [{bid.buyer.name}] " + f"{bid.energy} kWh @ {bid.price} {bid.energy_rate}" ) - assert (str(bid) == - f"{{{bid.id!s:.6s}}} [origin: {bid.buyer.origin}] [{bid.buyer.name}] " - f"{bid.energy} kWh @ {bid.price} {bid.energy_rate}") def test_serializable_dict(self): - bid = Bid( - **self.initial_data - ) + bid = Bid(**self.initial_data) assert bid.serializable_dict() == { "type": "Bid", @@ -455,7 +485,7 @@ def test_serializable_dict(self): "original_price": bid.original_price, "creation_time": datetime_to_string_incl_seconds(bid.creation_time), "buyer": bid.buyer.serializable_dict(), - "time_slot": datetime_to_string_incl_seconds(bid.time_slot) + "time_slot": datetime_to_string_incl_seconds(bid.time_slot), } def test_from_dict(self): @@ -465,30 +495,35 @@ def test_from_dict(self): assert Bid.from_dict(bid.serializable_dict()) == bid def test_eq(self): - bid = Bid( - **self.initial_data - ) - other_bid = Bid( - **self.initial_data - ) + bid = Bid(**self.initial_data) + other_bid = Bid(**self.initial_data) assert bid == other_bid other_bid.id = "other_id" assert bid != other_bid def test_csv_values(self): - bid = Bid( - **self.initial_data - ) + bid = Bid(**self.initial_data) rate = round(bid.energy_rate, 4) - assert bid.csv_values() == (bid.creation_time, rate, bid.energy, bid.price, bid.buyer.name, - bid.buyer.origin) + assert bid.csv_values() == ( + bid.creation_time, + rate, + bid.energy, + bid.price, + bid.buyer.name, + bid.buyer.origin, + ) @staticmethod def test_csv_fields(): - assert (Bid.csv_fields() == - ("creation_time", "rate [ct./kWh]", "energy [kWh]", "price [ct.]", "buyer", - "buyer origin")) + assert Bid.csv_fields() == ( + "creation_time", + "rate [ct./kWh]", + "energy [kWh]", + "price [ct.]", + "buyer", + "buyer origin", + ) def test_accumulated_grid_fees(self): bid = Bid(**self.initial_data) @@ -498,20 +533,16 @@ def test_accumulated_grid_fees(self): class TestTradeBidOfferInfo: @staticmethod def test_to_json_string(): - trade_bid_offer_info = TradeBidOfferInfo( - 1, 1, 1, 1, 1 + trade_bid_offer_info = TradeBidOfferInfo(1, 1, 1, 1, 1) + assert trade_bid_offer_info.to_json_string() == json.dumps( + trade_bid_offer_info.serializable_dict() ) - assert (trade_bid_offer_info.to_json_string() == - json.dumps(trade_bid_offer_info.serializable_dict())) @staticmethod def test_from_json(): - trade_bid_offer_info = TradeBidOfferInfo( - 1, 1, 1, 1, 1 - ) + trade_bid_offer_info = TradeBidOfferInfo(1, 1, 1, 1, 1) trade_bid_offer_info_json = trade_bid_offer_info.to_json_string() - assert (TradeBidOfferInfo.from_json(trade_bid_offer_info_json) == - trade_bid_offer_info) + assert TradeBidOfferInfo.from_json(trade_bid_offer_info_json) == trade_bid_offer_info class TestTrade: @@ -526,42 +557,53 @@ def setup_method(self): "buyer": self._buyer, "traded_energy": 1, "trade_price": 1, - "matching_requirements": {"requirement": "value"}} + "matching_requirements": {"requirement": "value"}, + } def test_str(self): trade = Trade(**self.initial_data) - assert (str(trade) == - f"{{{trade.id!s:.6s}}} [origin: {trade.seller.origin} -> {trade.buyer.origin}] " - f"[{trade.seller.name} -> {trade.buyer.name}] {trade.traded_energy} kWh" - f" @ {trade.trade_price} {round(trade.trade_rate, 8)} " - f"{trade.match_details['offer'].id} [fee: {trade.fee_price} cts.] " - f"{trade.matching_requirements or ''}") + assert ( + str(trade) + == f"{{{trade.id!s:.6s}}} [origin: {trade.seller.origin} -> {trade.buyer.origin}] " + f"[{trade.seller.name} -> {trade.buyer.name}] {trade.traded_energy} kWh" + f" @ {trade.trade_price} {round(trade.trade_rate, 8)} " + f"{trade.match_details['offer'].id} [fee: {trade.fee_price} cts.] " + f"{trade.matching_requirements or ''}" + ) @staticmethod def test_csv_fields(): assert Trade.csv_fields() == ( - "creation_time", "rate [ct./kWh]", "energy [kWh]", "seller", "seller origin", "buyer", - "buyer origin") + "creation_time", + "rate [ct./kWh]", + "energy [kWh]", + "seller", + "seller origin", + "buyer", + "buyer origin", + ) def test_csv_values(self): trade = Trade(**self.initial_data) rate = round(trade.trade_rate, 4) - assert (trade.csv_values() == - (trade.creation_time, rate, trade.traded_energy, trade.seller.name, - trade.seller.origin, trade.buyer.name, trade.buyer.origin)) + assert trade.csv_values() == ( + trade.creation_time, + rate, + trade.traded_energy, + trade.seller.name, + trade.seller.origin, + trade.buyer.name, + trade.buyer.origin, + ) def test_to_json_string(self): trade = Trade(**self.initial_data) assert trade.to_json_string() == json.dumps(trade.serializable_dict()) def test_from_json(self): - trade = Trade( - **self.initial_data - ) + trade = Trade(**self.initial_data) trade.residual = deepcopy(trade.match_details["offer"]) - trade.offer_bid_trade_info = TradeBidOfferInfo( - 1, 1, 1, 1, 1 - ) + trade.offer_bid_trade_info = TradeBidOfferInfo(1, 1, 1, 1, 1) trade_json_str = trade.to_json_string() assert Trade.from_json(trade_json_str) == trade @@ -569,17 +611,14 @@ def test_from_json(self): @pytest.mark.parametrize("time_stamp", [None, DEFAULT_DATETIME]) def test_from_json_deals_with_time_stamps_correctly(self, time_stamp): updated_initial_data = self.initial_data - updated_initial_data.update({"time_slot": time_stamp, - "creation_time": time_stamp}) + updated_initial_data.update({"time_slot": time_stamp, "creation_time": time_stamp}) trade = Trade(**updated_initial_data) trade_json = trade.to_json_string() trade = Trade.from_json(trade_json) assert trade.creation_time == time_stamp def test_is_bid_trade(self): - trade = Trade( - **self.initial_data - ) + trade = Trade(**self.initial_data) assert trade.is_bid_trade is False trade.match_details["bid"] = Bid("id", DateTime.now(), 1, 2, self._buyer) @@ -587,9 +626,7 @@ def test_is_bid_trade(self): assert trade.is_bid_trade is True def test_is_offer_trade(self): - trade = Trade( - **self.initial_data - ) + trade = Trade(**self.initial_data) assert trade.is_offer_trade is True trade.match_details["bid"] = Bid("id", DateTime.now(), 1, 2, self._buyer) @@ -617,7 +654,7 @@ def test_serializable_dict(self): "trade_price": 1, "fee_price": 2, "creation_time": DEFAULT_DATETIME, - "time_slot": DEFAULT_DATETIME + "time_slot": DEFAULT_DATETIME, } ) assert trade.serializable_dict() == { @@ -634,18 +671,18 @@ def test_serializable_dict(self): "name": trade.buyer.name, "uuid": trade.buyer.uuid, "origin": trade.buyer.origin, - "origin_uuid": trade.buyer.origin_uuid + "origin_uuid": trade.buyer.origin_uuid, }, "seller": { "name": trade.seller.name, "uuid": trade.seller.uuid, "origin": trade.seller.origin, - "origin_uuid": trade.seller.origin_uuid + "origin_uuid": trade.seller.origin_uuid, }, "fee_price": trade.fee_price, "creation_time": datetime_to_string_incl_seconds(trade.creation_time), "time_slot": datetime_to_string_incl_seconds(trade.creation_time), - "offer_bid_trade_info": None + "offer_bid_trade_info": None, } @@ -658,39 +695,41 @@ def setup_method(self): "energy": 30, "original_price": 8, "seller": TraderDetails("seller", str(uuid.uuid4())), - "time_slot": DEFAULT_DATETIME + "time_slot": DEFAULT_DATETIME, } def test_repr(self): offer = BalancingOffer(**self.initial_data) - assert (repr(offer) == - f"") + assert ( + repr(offer) == f"" + ) def test_str(self): offer = BalancingOffer(**self.initial_data) - assert (str(offer) == - f"") + assert ( + str(offer) == f"" + ) class TestBalancingTrade(TestTrade): def test_str(self): trade = BalancingTrade(**self.initial_data) - assert (str(trade) == - f"{{{trade.id!s:.6s}}} [{trade.seller.name} -> {trade.buyer.name}] " - f"{trade.traded_energy} kWh @ {trade.trade_price}" - f" {trade.trade_rate} {trade.match_details['offer'].id} ") + assert ( + str(trade) == f"{{{trade.id!s:.6s}}} [{trade.seller.name} -> {trade.buyer.name}] " + f"{trade.traded_energy} kWh @ {trade.trade_price}" + f" {trade.trade_rate} {trade.match_details['offer'].id} " + ) class TestClearing: @staticmethod def test_serializable_dict(): clearing = Clearing(energy=1, rate=1) - assert clearing.serializable_dict() == { - "energy": 1, "rate": 1} + assert clearing.serializable_dict() == {"energy": 1, "rate": 1} class TestMarketClearingState: