From 5363bcf21842acd2d2ab8bf90f69e40324747a20 Mon Sep 17 00:00:00 2001 From: hannesdiedrich Date: Tue, 20 Jan 2026 11:02:47 +0100 Subject: [PATCH] GSYE-900: Add Decimal to pay_as_bid_matching algorithm in order to correctly compare selected_energy to FLOATING_POINT_TOLERANCE --- gsy_framework/data_classes.py | 8 ++-- .../pay_as_bid_matching_algorithm.py | 42 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/gsy_framework/data_classes.py b/gsy_framework/data_classes.py index 8b409ae8..08a8e8db 100644 --- a/gsy_framework/data_classes.py +++ b/gsy_framework/data_classes.py @@ -28,7 +28,7 @@ from dataclasses import dataclass, field, asdict from math import isclose from typing import Dict, Optional, Tuple, Union - +from decimal import Decimal from pendulum import DateTime from gsy_framework.constants_limits import ( @@ -641,7 +641,7 @@ class BidOfferMatch: market_id: str time_slot: str bid: Bid.serializable_dict - selected_energy: float + selected_energy: Decimal offer: Offer.serializable_dict trade_rate: float matching_requirements: Optional[Dict] = None @@ -657,7 +657,7 @@ def serializable_dict(self) -> Dict: "time_slot": self.time_slot, "bid": self.bid, "offer": self.offer, - "selected_energy": self.selected_energy, + "selected_energy": float(self.selected_energy), "trade_rate": self.trade_rate, "matching_requirements": self.matching_requirements, } @@ -685,7 +685,7 @@ def is_valid_dict(cls, bid_offer_match: Dict) -> bool: is_valid = False elif not isinstance(bid_offer_match["market_id"], str): is_valid = False - elif not isinstance(bid_offer_match["selected_energy"], (int, float)): + elif not isinstance(bid_offer_match["selected_energy"], (int, float, Decimal)): is_valid = False elif not isinstance(bid_offer_match["trade_rate"], (int, float)): is_valid = False diff --git a/gsy_framework/matching_algorithms/pay_as_bid_matching_algorithm.py b/gsy_framework/matching_algorithms/pay_as_bid_matching_algorithm.py index 54f2f25d..6f6531cf 100644 --- a/gsy_framework/matching_algorithms/pay_as_bid_matching_algorithm.py +++ b/gsy_framework/matching_algorithms/pay_as_bid_matching_algorithm.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import Dict, List, Optional from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE @@ -16,8 +17,9 @@ class PayAsBidMatchingAlgorithm(BaseMatchingAlgorithm): """ @staticmethod - def _match_one_bid_one_offer(offer: Dict, bid: Dict, available_order_energy: Dict, - market_id: str, time_slot: str) -> Optional[BidOfferMatch]: + def _match_one_bid_one_offer( + offer: Dict, bid: Dict, available_order_energy: Dict, market_id: str, time_slot: str + ) -> Optional[BidOfferMatch]: """ Try to match one bid with one offer, and at the same time update the dict with the already selected order energy in order to be able to reuse the same order in future @@ -27,31 +29,33 @@ def _match_one_bid_one_offer(offer: Dict, bid: Dict, available_order_energy: Dic return None if bid["id"] not in available_order_energy: - available_order_energy[bid["id"]] = bid["energy"] + available_order_energy[bid["id"]] = Decimal(bid["energy"]) if offer["id"] not in available_order_energy: - available_order_energy[offer["id"]] = offer["energy"] + available_order_energy[offer["id"]] = Decimal(offer["energy"]) offer_energy = available_order_energy[offer["id"]] bid_energy = available_order_energy[bid["id"]] - selected_energy = min(offer_energy, bid_energy) + selected_energy = Decimal(min(offer_energy, bid_energy)) if selected_energy <= FLOATING_POINT_TOLERANCE: return None available_order_energy[bid["id"]] -= selected_energy available_order_energy[offer["id"]] -= selected_energy - assert all(v >= -FLOATING_POINT_TOLERANCE - for v in available_order_energy.values()) + assert all(v >= -FLOATING_POINT_TOLERANCE for v in available_order_energy.values()) return BidOfferMatch( market_id=market_id, time_slot=time_slot, - bid=bid, offer=offer, + bid=bid, + offer=offer, selected_energy=selected_energy, - trade_rate=bid.get("energy_rate")) + trade_rate=bid.get("energy_rate"), + ) @classmethod def _calculate_bid_offer_matches_for_one_market_timeslot( - cls, market_id: str, time_slot: str, data: Dict) -> List[BidOfferMatch]: + cls, market_id: str, time_slot: str, data: Dict + ) -> List[BidOfferMatch]: """ Calculate all possible matches for one market slot. """ @@ -74,8 +78,10 @@ def _calculate_bid_offer_matches_for_one_market_timeslot( if possible_match: bid_offer_matches.append(possible_match) - if (offer["id"] in available_order_energy and - available_order_energy[offer["id"]] <= FLOATING_POINT_TOLERANCE): + if ( + offer["id"] in available_order_energy + and available_order_energy[offer["id"]] <= FLOATING_POINT_TOLERANCE + ): break return bid_offer_matches @@ -84,9 +90,9 @@ def get_matches_recommendations(cls, matching_data: Dict) -> List: bid_offer_matches = [] for market_id, time_slot_data in matching_data.items(): for time_slot, data in time_slot_data.items(): - bid_offer_matches.extend(cls._calculate_bid_offer_matches_for_one_market_timeslot( - market_id, time_slot, data - )) - return [ - match.serializable_dict() for match in bid_offer_matches - ] + bid_offer_matches.extend( + cls._calculate_bid_offer_matches_for_one_market_timeslot( + market_id, time_slot, data + ) + ) + return [match.serializable_dict() for match in bid_offer_matches]