def __init__( self, panel_count: int = 1, initial_selling_rate: float = ConstSettings.GeneralSettings. DEFAULT_MARKET_MAKER_RATE, final_selling_rate: float = ConstSettings.PVSettings. FINAL_SELLING_RATE, fit_to_limit: bool = True, update_interval=duration( minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), energy_rate_decrease_per_update: float = ConstSettings. GeneralSettings.ENERGY_RATE_DECREASE_PER_UPDATE, max_panel_power_W: float = None, use_market_maker_rate: bool = False): """ :param panel_count: Number of solar panels for this PV plant :param initial_selling_rate: Upper Threshold for PV offers :param final_selling_rate: Lower Threshold for PV offers :param fit_to_limit: Linear curve following initial_selling_rate & initial_selling_rate :param update_interval: Interval after which PV will update its offer :param energy_rate_decrease_per_update: Slope of PV Offer change per update :param max_panel_power_W: """ # If use_market_maker_rate is true, overwrite initial_selling_rate to market maker rate if use_market_maker_rate: initial_selling_rate = GlobalConfig.market_maker_rate try: validate_pv_device(panel_count=panel_count, max_panel_power_W=max_panel_power_W) except D3ADeviceException as e: raise D3ADeviceException(str(e)) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BaseStrategy.__init__(self) self.offer_update = UpdateFrequencyMixin( initial_selling_rate, final_selling_rate, fit_to_limit, energy_rate_decrease_per_update, update_interval) for time_slot in generate_market_slot_list(): try: validate_pv_device( initial_selling_rate=self.offer_update. initial_rate[time_slot], final_selling_rate=self.offer_update.final_rate[time_slot]) except D3ADeviceException as e: raise D3ADeviceException(str(e)) self.panel_count = panel_count self.final_selling_rate = final_selling_rate self.max_panel_power_W = max_panel_power_W self.energy_production_forecast_kWh = {} # type: Dict[Time, float] self.state = PVState()
def _init_price_update(self, update_interval, initial_selling_rate, final_selling_rate, use_market_maker_rate, fit_to_limit, energy_rate_decrease_per_update): if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) self.final_selling_rate = final_selling_rate self.use_market_maker_rate = use_market_maker_rate validate_pv_device_price(fit_to_limit=fit_to_limit, energy_rate_decrease_per_update=energy_rate_decrease_per_update) self.offer_update = UpdateFrequencyMixin(initial_selling_rate, final_selling_rate, fit_to_limit, energy_rate_decrease_per_update, update_interval)
def __init__(self, panel_count: int = 1, initial_selling_rate: float = ConstSettings.GeneralSettings. DEFAULT_MARKET_MAKER_RATE, final_selling_rate: float = ConstSettings.PVSettings. SELLING_RATE_RANGE.final, fit_to_limit: bool = True, update_interval=None, energy_rate_decrease_per_update=None, max_panel_power_W: float = None, use_market_maker_rate: bool = False): """ :param panel_count: Number of solar panels for this PV plant :param initial_selling_rate: Upper Threshold for PV offers :param final_selling_rate: Lower Threshold for PV offers :param fit_to_limit: Linear curve following initial_selling_rate & initial_selling_rate :param update_interval: Interval after which PV will update its offer :param energy_rate_decrease_per_update: Slope of PV Offer change per update :param max_panel_power_W: """ if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) self.use_market_maker_rate = use_market_maker_rate validate_pv_device( panel_count=panel_count, max_panel_power_W=max_panel_power_W, fit_to_limit=fit_to_limit, energy_rate_decrease_per_update=energy_rate_decrease_per_update) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BaseStrategy.__init__(self) self.offer_update = UpdateFrequencyMixin( initial_selling_rate, final_selling_rate, fit_to_limit, energy_rate_decrease_per_update, update_interval) self.panel_count = panel_count self.final_selling_rate = final_selling_rate self.max_panel_power_W = max_panel_power_W self.energy_production_forecast_kWh = {} # type: Dict[Time, float] self.state = PVState()
def _init_price_update(self, fit_to_limit, energy_rate_increase_per_update, update_interval, use_market_maker_rate, initial_buying_rate, final_buying_rate): validate_load_device_price(fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update) if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) self.use_market_maker_rate = use_market_maker_rate if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = \ UpdateFrequencyMixin(initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min) self.fit_to_limit = fit_to_limit
def __init__( self, avg_power_W, hrs_per_day=None, hrs_of_day=None, fit_to_limit=True, energy_rate_increase_per_update=1, update_interval=duration( minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), initial_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.INITIAL_BUYING_RATE, final_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.FINAL_BUYING_RATE, balancing_energy_ratio: tuple = ( ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), use_market_maker_rate: bool = False): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device :param hrs_per_day: Daily energy usage :param hrs_of_day: hours of day energy is needed :param fit_to_limit: if set to True, it will make a linear curve following following initial_buying_rate & final_buying_rate :param energy_rate_increase_per_update: Slope of Load bids change per update :param update_interval: Interval after which Load will update its offer :param initial_buying_rate: Starting point of load's preferred buying rate :param final_buying_rate: Ending point of load's preferred buying rate :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ # If use_market_maker_rate is true, overwrite final_buying_rate to market maker rate if use_market_maker_rate: final_buying_rate = GlobalConfig.market_maker_rate if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = \ UpdateFrequencyMixin(initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min) try: validate_load_device(avg_power_W=avg_power_W, hrs_per_day=hrs_per_day, hrs_of_day=hrs_of_day) except D3ADeviceException as e: raise D3ADeviceException(str(e)) for time_slot in generate_market_slot_list(): rate_change = self.bid_update.energy_rate_change_per_update[ time_slot] try: validate_load_device( initial_buying_rate=self.bid_update. initial_rate[time_slot], final_buying_rate=self.bid_update.final_rate[time_slot], energy_rate_increase_per_update=rate_change) except D3ADeviceException as e: raise D3ADeviceException(str(e)) self.state = LoadState() self.avg_power_W = avg_power_W # consolidated_cycle is KWh energy consumed for the entire year self.daily_energy_required = None # Energy consumed during the day ideally should not exceed daily_energy_required self.energy_per_slot_Wh = None self.energy_requirement_Wh = {} # type: Dict[Time, float] self.hrs_per_day = {} # type: Dict[int, int] self.assign_hours_of_per_day(hrs_of_day, hrs_per_day) self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)
class LoadHoursStrategy(BidEnabledStrategy): parameters = ('avg_power_W', 'hrs_per_day', 'hrs_of_day', 'fit_to_limit', 'energy_rate_increase_per_update', 'update_interval', 'initial_buying_rate', 'final_buying_rate', 'balancing_energy_ratio', 'use_market_maker_rate') def __init__( self, avg_power_W, hrs_per_day=None, hrs_of_day=None, fit_to_limit=True, energy_rate_increase_per_update=1, update_interval=duration( minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), initial_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.INITIAL_BUYING_RATE, final_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.FINAL_BUYING_RATE, balancing_energy_ratio: tuple = ( ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), use_market_maker_rate: bool = False): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device :param hrs_per_day: Daily energy usage :param hrs_of_day: hours of day energy is needed :param fit_to_limit: if set to True, it will make a linear curve following following initial_buying_rate & final_buying_rate :param energy_rate_increase_per_update: Slope of Load bids change per update :param update_interval: Interval after which Load will update its offer :param initial_buying_rate: Starting point of load's preferred buying rate :param final_buying_rate: Ending point of load's preferred buying rate :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ # If use_market_maker_rate is true, overwrite final_buying_rate to market maker rate if use_market_maker_rate: final_buying_rate = GlobalConfig.market_maker_rate if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = \ UpdateFrequencyMixin(initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min) try: validate_load_device(avg_power_W=avg_power_W, hrs_per_day=hrs_per_day, hrs_of_day=hrs_of_day) except D3ADeviceException as e: raise D3ADeviceException(str(e)) for time_slot in generate_market_slot_list(): rate_change = self.bid_update.energy_rate_change_per_update[ time_slot] try: validate_load_device( initial_buying_rate=self.bid_update. initial_rate[time_slot], final_buying_rate=self.bid_update.final_rate[time_slot], energy_rate_increase_per_update=rate_change) except D3ADeviceException as e: raise D3ADeviceException(str(e)) self.state = LoadState() self.avg_power_W = avg_power_W # consolidated_cycle is KWh energy consumed for the entire year self.daily_energy_required = None # Energy consumed during the day ideally should not exceed daily_energy_required self.energy_per_slot_Wh = None self.energy_requirement_Wh = {} # type: Dict[Time, float] self.hrs_per_day = {} # type: Dict[int, int] self.assign_hours_of_per_day(hrs_of_day, hrs_per_day) self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) @property def active_markets(self): return [ market for market in self.area.all_markets if self._is_market_active(market) ] def _is_market_active(self, market): return self._allowed_operating_hours(market.time_slot) and \ is_market_in_simulation_duration(self.area.config, market) and \ (not self.area.current_market or market.time_slot >= self.area.current_market.time_slot) def assign_hours_of_per_day(self, hrs_of_day, hrs_per_day): if hrs_of_day is None: hrs_of_day = list(range(24)) # be a parameter on the constructor or if we want to deal in percentages if hrs_per_day is None: hrs_per_day = len(hrs_of_day) if hrs_of_day is None: hrs_of_day = list(range(24)) self.hrs_of_day = hrs_of_day self._initial_hrs_per_day = hrs_per_day if not all([0 <= h <= 23 for h in hrs_of_day]): raise ValueError( "Hrs_of_day list should contain integers between 0 and 23.") if len(hrs_of_day) < hrs_per_day: raise ValueError( "Length of list 'hrs_of_day' must be greater equal 'hrs_per_day'" ) def assign_energy_requirement(self, avg_power_W): self.energy_per_slot_Wh = ( avg_power_W / (duration(hours=1) / self.area.config.slot_length)) for slot_time in generate_market_slot_list(area=self.area): if self._allowed_operating_hours( slot_time) and slot_time >= self.area.now: self.energy_requirement_Wh[slot_time] = self.energy_per_slot_Wh self.state.desired_energy_Wh[ slot_time] = self.energy_per_slot_Wh def event_activate(self): self.bid_update.update_on_activate() self.hrs_per_day = { day: self._initial_hrs_per_day for day in range(self.area.config.sim_duration.days + 1) } self._simulation_start_timestamp = self.area.now self.assign_energy_requirement(self.avg_power_W) self._set_alternative_pricing_scheme() def area_reconfigure_event(self, avg_power_W=None, hrs_per_day=None, hrs_of_day=None, final_buying_rate=None): if hrs_per_day is not None or hrs_of_day is not None: self.assign_hours_of_per_day(hrs_of_day, hrs_per_day) self.hrs_per_day = { day: self._initial_hrs_per_day for day in range(self.area.config.sim_duration.days + 1) } if avg_power_W is not None: self.avg_power_W = avg_power_W self.assign_energy_requirement(avg_power_W) if final_buying_rate is not None: self.bid_update.final_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, final_buying_rate) def _find_acceptable_offer(self, market): offers = market.most_affordable_offers return random.choice(offers) def _one_sided_market_event_tick(self, market): try: if len(market.sorted_offers) < 1: return acceptable_offer = self._find_acceptable_offer(market) current_day = self._get_day_of_timestamp(market.time_slot) if acceptable_offer and \ self.hrs_per_day[current_day] > FLOATING_POINT_TOLERANCE and \ round(acceptable_offer.price / acceptable_offer.energy, 8) <= \ self.bid_update.final_rate[market.time_slot]: max_energy = self.energy_requirement_Wh[ market.time_slot] / 1000.0 if max_energy < FLOATING_POINT_TOLERANCE: return if acceptable_offer.energy > max_energy: self.accept_offer(market, acceptable_offer, energy=max_energy, buyer_origin=self.owner.name) self.energy_requirement_Wh[market.time_slot] = 0 self.hrs_per_day[current_day] -= self._operating_hours( max_energy) else: self.accept_offer(market, acceptable_offer, buyer_origin=self.owner.name) self.energy_requirement_Wh[market.time_slot] -= \ acceptable_offer.energy * 1000.0 self.hrs_per_day[current_day] -= self._operating_hours( acceptable_offer.energy) except MarketException: self.log.exception("An Error occurred while buying an offer") def _get_day_of_timestamp(self, time_slot): return (time_slot - self._simulation_start_timestamp).days def _double_sided_market_event_tick(self, market): self.bid_update.update_posted_bids_over_ticks(market, self) def event_tick(self): for market in self.active_markets: if market.time_slot not in self.energy_requirement_Wh: continue if self.energy_requirement_Wh[market.time_slot] <= 0: continue if ConstSettings.IAASettings.MARKET_TYPE == 1: self._one_sided_market_event_tick(market) elif ConstSettings.IAASettings.MARKET_TYPE == 2 or \ ConstSettings.IAASettings.MARKET_TYPE == 3: self._double_sided_market_event_tick(market) def event_offer(self, *, market_id, offer): super().event_offer(market_id=market_id, offer=offer) market = self.area.get_future_market_from_id(market_id) if market.time_slot in self.energy_requirement_Wh and \ self._is_market_active(market) and \ self.energy_requirement_Wh[market.time_slot] > FLOATING_POINT_TOLERANCE: if ConstSettings.IAASettings.MARKET_TYPE == 1: self._one_sided_market_event_tick(market) def _allowed_operating_hours(self, time): return time.hour in self.hrs_of_day def _operating_hours(self, energy): return (((energy * 1000) / self.energy_per_slot_Wh) * (self.area.config.slot_length / duration(hours=1))) def _set_alternative_pricing_scheme(self): if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0: for time_slot in generate_market_slot_list(): final_rate = self.area.config.market_maker_rate[time_slot] self.bid_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=final_rate) def event_market_cycle(self): super().event_market_cycle() for market in self.active_markets: current_day = self._get_day_of_timestamp(market.time_slot) if self.hrs_per_day[current_day] <= FLOATING_POINT_TOLERANCE: self.energy_requirement_Wh[market.time_slot] = 0.0 self.state.desired_energy_Wh[market.time_slot] = 0.0 if ConstSettings.IAASettings.MARKET_TYPE == 2 or \ ConstSettings.IAASettings.MARKET_TYPE == 3: if self.energy_requirement_Wh[market.time_slot] > 0: if self.is_eligible_for_balancing_market: bid_energy = \ self.energy_requirement_Wh[market.time_slot] - \ self.balancing_energy_ratio.demand * \ self.state.desired_energy_Wh[market.time_slot] else: bid_energy = self.energy_requirement_Wh[ market.time_slot] if not self.are_bids_posted(market.id): self.post_first_bid(market, bid_energy) else: self.bid_update.update_market_cycle_bids(self) def event_balancing_market_cycle(self): for market in self.active_markets: self._demand_balancing_offer(market) def event_bid_traded(self, *, market_id, bid_trade): super().event_bid_traded(market_id=market_id, bid_trade=bid_trade) market = self.area.get_future_market_from_id(market_id) if bid_trade.offer.buyer == self.owner.name: self.energy_requirement_Wh[ market.time_slot] -= bid_trade.offer.energy * 1000.0 self.hrs_per_day[self._get_day_of_timestamp(market.time_slot)] -= \ self._operating_hours(bid_trade.offer.energy) assert self.energy_requirement_Wh[market.time_slot] >= -FLOATING_POINT_TOLERANCE, \ f"Energy requirement for load {self.owner.name} fell below zero " \ f"({self.energy_requirement_Wh[market.time_slot]})." def event_trade(self, *, market_id, trade): market = self.area.get_future_market_from_id(market_id) assert market is not None if ConstSettings.BalancingSettings.FLEXIBLE_LOADS_SUPPORT: # Load can only put supply_balancing_offers only when there is a trade in spot_market self._supply_balancing_offer(market, trade) super().event_trade(market_id=market_id, trade=trade) # committing to increase its consumption when required def _demand_balancing_offer(self, market): if not self.is_eligible_for_balancing_market: return ramp_up_energy = \ self.balancing_energy_ratio.demand * \ self.state.desired_energy_Wh[market.time_slot] self.energy_requirement_Wh[market.time_slot] -= ramp_up_energy ramp_up_price = DeviceRegistry.REGISTRY[ self.owner.name][0] * ramp_up_energy if ramp_up_energy != 0 and ramp_up_price != 0: self.area.get_balancing_market(market.time_slot). \ balancing_offer(ramp_up_price, -ramp_up_energy, self.owner.name) # committing to reduce its consumption when required def _supply_balancing_offer(self, market, trade): if not self.is_eligible_for_balancing_market: return if trade.buyer != self.owner.name: return ramp_down_energy = self.balancing_energy_ratio.supply * trade.offer.energy ramp_down_price = DeviceRegistry.REGISTRY[ self.owner.name][1] * ramp_down_energy self.area.get_balancing_market(market.time_slot).balancing_offer( ramp_down_price, ramp_down_energy, self.owner.name)
class PVStrategy(BaseStrategy): parameters = ('panel_count', 'initial_selling_rate', 'final_selling_rate', 'fit_to_limit', 'update_interval', 'energy_rate_decrease_per_update', 'max_panel_power_W', 'use_market_maker_rate') def __init__(self, panel_count: int = 1, initial_selling_rate: float = ConstSettings.GeneralSettings. DEFAULT_MARKET_MAKER_RATE, final_selling_rate: float = ConstSettings.PVSettings. SELLING_RATE_RANGE.final, fit_to_limit: bool = True, update_interval=None, energy_rate_decrease_per_update=None, max_panel_power_W: float = None, use_market_maker_rate: bool = False): """ :param panel_count: Number of solar panels for this PV plant :param initial_selling_rate: Upper Threshold for PV offers :param final_selling_rate: Lower Threshold for PV offers :param fit_to_limit: Linear curve following initial_selling_rate & initial_selling_rate :param update_interval: Interval after which PV will update its offer :param energy_rate_decrease_per_update: Slope of PV Offer change per update :param max_panel_power_W: """ super().__init__() validate_pv_device_energy(panel_count=panel_count, max_panel_power_W=max_panel_power_W) self.panel_count = panel_count self.max_panel_power_W = max_panel_power_W self.energy_production_forecast_kWh = {} # type: Dict[Time, float] self.state = PVState() self._init_price_update(update_interval, initial_selling_rate, final_selling_rate, use_market_maker_rate, fit_to_limit, energy_rate_decrease_per_update) def _init_price_update(self, update_interval, initial_selling_rate, final_selling_rate, use_market_maker_rate, fit_to_limit, energy_rate_decrease_per_update): if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) self.final_selling_rate = final_selling_rate self.use_market_maker_rate = use_market_maker_rate validate_pv_device_price( fit_to_limit=fit_to_limit, energy_rate_decrease_per_update=energy_rate_decrease_per_update) self.offer_update = UpdateFrequencyMixin( initial_selling_rate, final_selling_rate, fit_to_limit, energy_rate_decrease_per_update, update_interval) def area_reconfigure_event(self, validate=True, **kwargs): assert all(k in self.parameters for k in kwargs.keys()) self._area_reconfigure_prices(validate, **kwargs) validate_pv_device_energy(**kwargs) self.produced_energy_forecast_kWh() for name, value in kwargs.items(): setattr(self, name, value) def _area_reconfigure_prices(self, validate=True, **kwargs): if validate: validate_pv_device_price(**kwargs) if 'initial_selling_rate' in kwargs and kwargs[ 'initial_selling_rate'] is not None: self.offer_update.initial_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['initial_selling_rate']) if 'final_selling_rate' in kwargs and kwargs[ 'final_selling_rate'] is not None: self.offer_update.final_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['final_selling_rate']) self._validate_rates() self.offer_update.update_offer(self) def _validate_rates(self): for time_slot in generate_market_slot_list(): validate_pv_device_price( initial_selling_rate=self.offer_update.initial_rate[time_slot], final_selling_rate=self.offer_update.final_rate[time_slot]) def event_activate(self): self.event_activate_price() self.event_activate_energy() def event_activate_price(self): # If use_market_maker_rate is true, overwrite initial_selling_rate to market maker rate if self.use_market_maker_rate: self.area_reconfigure_event( initial_selling_rate=GlobalConfig.market_maker_rate, validate=False) self._validate_rates() # Calculating the produced energy self._set_alternative_pricing_scheme() self.offer_update.update_on_activate() def event_activate_energy(self): if self.max_panel_power_W is None: self.max_panel_power_W = self.area.config.max_panel_power_W self.produced_energy_forecast_kWh() def event_tick(self): self.offer_update.update_offer(self) self.offer_update.increment_update_counter_all_markets(self) def produced_energy_forecast_kWh(self): # This forecast ist based on the real PV system data provided by enphase # They can be found in the tools folder # A fit of a gaussian function to those data results in a formula Energy(time) for slot_time in generate_market_slot_list(area=self.area): if slot_time >= self.area.now: difference_to_midnight_in_minutes = \ slot_time.diff(self.area.now.start_of("day")).in_minutes() % (60 * 24) self.energy_production_forecast_kWh[slot_time] = \ self.gaussian_energy_forecast_kWh( difference_to_midnight_in_minutes) * self.panel_count self.state.available_energy_kWh[slot_time] = \ self.energy_production_forecast_kWh[slot_time] assert self.energy_production_forecast_kWh[slot_time] >= 0.0 def gaussian_energy_forecast_kWh(self, time_in_minutes=0): # The sun rises at approx 6:30 and sets at 18hr # time_in_minutes is the difference in time to midnight # Clamp to day range time_in_minutes %= 60 * 24 if (8 * 60) > time_in_minutes or time_in_minutes > (16.5 * 60): gauss_forecast = 0 else: gauss_forecast = self.max_panel_power_W * math.exp( # time/5 is needed because we only have one data set per 5 minutes (-(((round(time_in_minutes / 5, 0)) - 147.2) / 38.60)**2)) # /1000 is needed to convert Wh into kWh w_to_wh_factor = (self.area.config.slot_length / duration(hours=1)) return round((gauss_forecast / 1000) * w_to_wh_factor, 4) def event_market_cycle(self): super().event_market_cycle() self.event_market_cycle_price() def event_market_cycle_price(self): self.offer_update.update_market_cycle_offers(self) # Iterate over all markets open in the future for market in self.area.all_markets: assert self.state.available_energy_kWh[ market.time_slot] >= -FLOATING_POINT_TOLERANCE if self.state.available_energy_kWh[market.time_slot] > 0: offer_price = \ self.offer_update.initial_rate[market.time_slot] * \ self.state.available_energy_kWh[market.time_slot] offer = market.offer( offer_price, self.state.available_energy_kWh[market.time_slot], self.owner.name, original_offer_price=offer_price, seller_origin=self.owner.name) self.offers.post(offer, market.id) def event_trade(self, *, market_id, trade): super().event_trade(market_id=market_id, trade=trade) market = self.area.get_future_market_from_id(market_id) if market is None: return if trade.seller == self.owner.name: self.state.available_energy_kWh[ market.time_slot] -= trade.offer.energy def _set_alternative_pricing_scheme(self): if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0: if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 1: for time_slot in generate_market_slot_list(): self.offer_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=0) elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 2: for time_slot in generate_market_slot_list(): rate = \ self.area.config.market_maker_rate[time_slot] * \ ConstSettings.IAASettings.AlternativePricing.FEED_IN_TARIFF_PERCENTAGE / \ 100 self.offer_update.reassign_mixin_arguments( time_slot, initial_rate=rate, final_rate=rate) elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 3: for time_slot in generate_market_slot_list(): rate = self.area.config.market_maker_rate[time_slot] self.offer_update.reassign_mixin_arguments( time_slot, initial_rate=rate, final_rate=rate) else: raise MarketException
def __init__( self, initial_soc: float = StorageSettings.MIN_ALLOWED_SOC, min_allowed_soc=StorageSettings.MIN_ALLOWED_SOC, battery_capacity_kWh: float = StorageSettings.CAPACITY, max_abs_battery_power_kW: float = StorageSettings.MAX_ABS_POWER, cap_price_strategy: bool = False, initial_selling_rate: Union[ float, dict] = StorageSettings.SELLING_RATE_RANGE.initial, final_selling_rate: Union[ float, dict] = StorageSettings.SELLING_RATE_RANGE.final, initial_buying_rate: Union[ float, dict] = StorageSettings.BUYING_RATE_RANGE.initial, final_buying_rate: Union[ float, dict] = StorageSettings.BUYING_RATE_RANGE.final, loss_per_hour=StorageSettings.LOSS_PER_HOUR, loss_function=StorageSettings.LOSS_FUNCTION, fit_to_limit=True, energy_rate_increase_per_update=None, energy_rate_decrease_per_update=None, update_interval=None, initial_energy_origin: Enum = ESSEnergyOrigin.EXTERNAL, balancing_energy_ratio: tuple = ( BalancingSettings.OFFER_DEMAND_RATIO, BalancingSettings.OFFER_SUPPLY_RATIO)): if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) if min_allowed_soc is None: min_allowed_soc = StorageSettings.MIN_ALLOWED_SOC self.initial_soc = initial_soc validate_storage_device( initial_soc=initial_soc, min_allowed_soc=min_allowed_soc, battery_capacity_kWh=battery_capacity_kWh, max_abs_battery_power_kW=max_abs_battery_power_kW, loss_per_hour=loss_per_hour, loss_function=loss_function, fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update, energy_rate_decrease_per_update=energy_rate_decrease_per_update) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.offer_update = \ UpdateFrequencyMixin(initial_rate=initial_selling_rate, final_rate=final_selling_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_decrease_per_update, update_interval=update_interval) for time_slot in generate_market_slot_list(): validate_storage_device( initial_selling_rate=self.offer_update.initial_rate[time_slot], final_selling_rate=self.offer_update.final_rate[time_slot]) self.bid_update = \ UpdateFrequencyMixin( initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min ) for time_slot in generate_market_slot_list(): validate_storage_device( initial_buying_rate=self.bid_update.initial_rate[time_slot], final_buying_rate=self.bid_update.final_rate[time_slot]) self.state = \ StorageState(initial_soc=initial_soc, initial_energy_origin=initial_energy_origin, capacity=battery_capacity_kWh, max_abs_battery_power_kW=max_abs_battery_power_kW, loss_per_hour=loss_per_hour, loss_function=loss_function, min_allowed_soc=min_allowed_soc) self.cap_price_strategy = cap_price_strategy self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)
class StorageStrategy(BidEnabledStrategy): parameters = ('initial_soc', 'min_allowed_soc', 'battery_capacity_kWh', 'max_abs_battery_power_kW', 'cap_price_strategy', 'initial_selling_rate', 'final_selling_rate', 'initial_buying_rate', 'final_buying_rate', 'fit_to_limit', 'energy_rate_increase_per_update', 'energy_rate_decrease_per_update', 'update_interval', 'initial_energy_origin', 'balancing_energy_ratio') def __init__( self, initial_soc: float = StorageSettings.MIN_ALLOWED_SOC, min_allowed_soc=StorageSettings.MIN_ALLOWED_SOC, battery_capacity_kWh: float = StorageSettings.CAPACITY, max_abs_battery_power_kW: float = StorageSettings.MAX_ABS_POWER, cap_price_strategy: bool = False, initial_selling_rate: Union[ float, dict] = StorageSettings.SELLING_RATE_RANGE.initial, final_selling_rate: Union[ float, dict] = StorageSettings.SELLING_RATE_RANGE.final, initial_buying_rate: Union[ float, dict] = StorageSettings.BUYING_RATE_RANGE.initial, final_buying_rate: Union[ float, dict] = StorageSettings.BUYING_RATE_RANGE.final, loss_per_hour=StorageSettings.LOSS_PER_HOUR, loss_function=StorageSettings.LOSS_FUNCTION, fit_to_limit=True, energy_rate_increase_per_update=None, energy_rate_decrease_per_update=None, update_interval=None, initial_energy_origin: Enum = ESSEnergyOrigin.EXTERNAL, balancing_energy_ratio: tuple = ( BalancingSettings.OFFER_DEMAND_RATIO, BalancingSettings.OFFER_SUPPLY_RATIO)): if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) if min_allowed_soc is None: min_allowed_soc = StorageSettings.MIN_ALLOWED_SOC self.initial_soc = initial_soc validate_storage_device( initial_soc=initial_soc, min_allowed_soc=min_allowed_soc, battery_capacity_kWh=battery_capacity_kWh, max_abs_battery_power_kW=max_abs_battery_power_kW, loss_per_hour=loss_per_hour, loss_function=loss_function, fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update, energy_rate_decrease_per_update=energy_rate_decrease_per_update) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.offer_update = \ UpdateFrequencyMixin(initial_rate=initial_selling_rate, final_rate=final_selling_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_decrease_per_update, update_interval=update_interval) for time_slot in generate_market_slot_list(): validate_storage_device( initial_selling_rate=self.offer_update.initial_rate[time_slot], final_selling_rate=self.offer_update.final_rate[time_slot]) self.bid_update = \ UpdateFrequencyMixin( initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min ) for time_slot in generate_market_slot_list(): validate_storage_device( initial_buying_rate=self.bid_update.initial_rate[time_slot], final_buying_rate=self.bid_update.final_rate[time_slot]) self.state = \ StorageState(initial_soc=initial_soc, initial_energy_origin=initial_energy_origin, capacity=battery_capacity_kWh, max_abs_battery_power_kW=max_abs_battery_power_kW, loss_per_hour=loss_per_hour, loss_function=loss_function, min_allowed_soc=min_allowed_soc) self.cap_price_strategy = cap_price_strategy self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) def _area_reconfigure_prices(self, **kwargs): if key_in_dict_and_not_none(kwargs, 'initial_selling_rate'): self.offer_update.initial_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['initial_selling_rate']) if key_in_dict_and_not_none(kwargs, 'final_selling_rate'): self.offer_update.final_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['final_selling_rate']) if key_in_dict_and_not_none(kwargs, 'initial_buying_rate'): self.bid_update.initial_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['initial_buying_rate']) if key_in_dict_and_not_none(kwargs, 'final_buying_rate'): self.bid_update.final_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['final_buying_rate']) if key_in_dict_and_not_none(kwargs, 'energy_rate_decrease_per_update'): self.offer_update.energy_rate_change_per_update = \ read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs['energy_rate_decrease_per_update']) if key_in_dict_and_not_none(kwargs, 'energy_rate_increase_per_update'): self.bid_update.energy_rate_change_per_update = \ read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs['energy_rate_increase_per_update']) if key_in_dict_and_not_none(kwargs, 'fit_to_limit'): self.bid_update.fit_to_limit = kwargs['fit_to_limit'] self.offer_update.fit_to_limit = kwargs['fit_to_limit'] if key_in_dict_and_not_none(kwargs, 'update_interval'): if isinstance(kwargs['update_interval'], int): update_interval = duration(minutes=kwargs['update_interval']) else: update_interval = kwargs['update_interval'] self.bid_update.update_interval = update_interval self.offer_update.update_interval = update_interval def area_reconfigure_event(self, **kwargs): self._area_reconfigure_prices(**kwargs) self.offer_update.update_on_activate() self.bid_update.update_on_activate() self._validate_rates() def _validate_rates(self): for time_slot in generate_market_slot_list(): bid_rate_change = None if self.bid_update.fit_to_limit else \ self.bid_update.energy_rate_change_per_update[time_slot] offer_rate_change = None if self.offer_update.fit_to_limit else \ self.offer_update.energy_rate_change_per_update[time_slot] validate_storage_device( initial_selling_rate=self.offer_update.initial_rate[time_slot], final_selling_rate=self.offer_update.final_rate[time_slot], initial_buying_rate=self.bid_update.initial_rate[time_slot], final_buying_rate=self.bid_update.final_rate[time_slot], energy_rate_increase_per_update=bid_rate_change, energy_rate_decrease_per_update=offer_rate_change, fit_to_limit=self.bid_update.fit_to_limit, update_interval=self.bid_update.update_interval) def event_on_disabled_area(self): self.state.calculate_soc_for_time_slot(self.area.next_market.time_slot) def event_activate_price(self): self.offer_update.update_on_activate() self.bid_update.update_on_activate() self._set_alternative_pricing_scheme() def event_activate_energy(self): self.state.set_battery_energy_per_slot(self.area.config.slot_length) def event_activate(self): self.event_activate_energy() self.event_activate_price() def _set_alternative_pricing_scheme(self): if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0: if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 1: for time_slot in generate_market_slot_list(): self.bid_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=0) self.offer_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=0) elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 2: for time_slot in generate_market_slot_list(): rate = \ self.area.config.market_maker_rate[time_slot] * \ ConstSettings.IAASettings.AlternativePricing.FEED_IN_TARIFF_PERCENTAGE / \ 100 self.bid_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=rate) self.offer_update.reassign_mixin_arguments( time_slot, initial_rate=rate, final_rate=rate) elif ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME == 3: for time_slot in generate_market_slot_list(): rate = self.area.config.market_maker_rate[time_slot] self.bid_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=rate) self.offer_update.reassign_mixin_arguments( time_slot, initial_rate=rate, final_rate=rate) else: raise MarketException @staticmethod def _validate_constructor_arguments(initial_soc=None, min_allowed_soc=None, battery_capacity_kWh=None, max_abs_battery_power_kW=None, initial_selling_rate=None, final_selling_rate=None, initial_buying_rate=None, final_buying_rate=None, energy_rate_change_per_update=None): if battery_capacity_kWh is not None and battery_capacity_kWh < 0: raise ValueError("Battery capacity should be a positive integer") if max_abs_battery_power_kW is not None and max_abs_battery_power_kW < 0: raise ValueError( "Battery Power rating must be a positive integer.") if initial_soc is not None and 0 < initial_soc > 100: raise ValueError("initial SOC must be in between 0-100 %") if min_allowed_soc is not None and 0 < min_allowed_soc > 100: raise ValueError("initial SOC must be in between 0-100 %") if initial_soc is not None and min_allowed_soc is not None and \ initial_soc < min_allowed_soc: raise ValueError( "Initial charge must be more than the minimum allowed soc.") if initial_selling_rate is not None and initial_selling_rate < 0: raise ValueError("Initial selling rate must be greater equal 0.") if final_selling_rate is not None: if type(final_selling_rate) is float and final_selling_rate < 0: raise ValueError("Final selling rate must be greater equal 0.") elif type(final_selling_rate) is dict and \ any(rate < 0 for _, rate in final_selling_rate.items()): raise ValueError("Final selling rate must be greater equal 0.") if initial_selling_rate is not None and final_selling_rate is not None: initial_selling_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, initial_selling_rate) final_selling_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, final_selling_rate) if any(initial_selling_rate[hour] < final_selling_rate[hour] for hour, _ in initial_selling_rate.items()): raise ValueError( "Initial selling rate must be greater than final selling rate." ) if initial_buying_rate is not None and initial_buying_rate < 0: raise ValueError("Initial buying rate must be greater equal 0.") if final_buying_rate is not None: final_buying_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, final_buying_rate) if any(rate < 0 for _, rate in final_buying_rate.items()): raise ValueError("Final buying rate must be greater equal 0.") if initial_buying_rate is not None and final_buying_rate is not None: initial_buying_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, initial_buying_rate) final_buying_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, final_buying_rate) if any(initial_buying_rate[hour] > final_buying_rate[hour] for hour, _ in initial_buying_rate.items()): raise ValueError( "Initial buying rate must be less than final buying rate.") if final_selling_rate is not None and final_buying_rate is not None: final_selling_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, final_selling_rate) final_buying_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, final_buying_rate) if any(final_buying_rate[hour] >= final_selling_rate[hour] for hour, _ in final_selling_rate.items()): raise ValueError( "final_buying_rate should be higher than final_selling_rate." ) if energy_rate_change_per_update is not None and energy_rate_change_per_update < 0: raise ValueError( "energy_rate_change_per_update should be a non-negative value." ) def event_tick(self): self.state.clamp_energy_to_buy_kWh( [ma.time_slot for ma in self.area.all_markets]) for market in self.area.all_markets: if ConstSettings.IAASettings.MARKET_TYPE == 2 or \ ConstSettings.IAASettings.MARKET_TYPE == 3: self.state.clamp_energy_to_buy_kWh( [ma.time_slot for ma in self.area.all_markets]) if self.are_bids_posted(market.id): self.bid_update.update_posted_bids_over_ticks(market, self) else: energy_kWh = self.state.energy_to_buy_dict[ market.time_slot] if energy_kWh > 0: first_bid = self.post_first_bid( market, energy_kWh * 1000.0) if first_bid is not None: self.state.offered_buy_kWh[ market.time_slot] += first_bid.energy self.state.tick(self.area, market.time_slot) if self.cap_price_strategy is False: self.offer_update.update_offer(self) self.bid_update.increment_update_counter_all_markets(self) if self.offer_update.increment_update_counter_all_markets(self): for market in self.area.all_markets: self.buy_energy(market) def event_trade(self, *, market_id, trade): market = self.area.get_future_market_from_id(market_id) super().event_trade(market_id=market_id, trade=trade) self.assert_if_trade_bid_price_is_too_high(market, trade) self.assert_if_trade_offer_price_is_too_low(market_id, trade) if trade.buyer == self.owner.name: self._track_energy_bought_type(trade) if trade.offer.seller == self.owner.name: self._track_energy_sell_type(trade) self.state.pledged_sell_kWh[market.time_slot] += trade.offer.energy self.state.offered_sell_kWh[market.time_slot] -= trade.offer.energy def _is_local(self, trade): for child in self.area.children: if child.name == trade.seller: return True # ESS Energy being utilized based on FIRST-IN FIRST-OUT mechanism def _track_energy_sell_type(self, trade): energy = trade.offer.energy while limit_float_precision(energy) > 0: first_in_energy_with_origin = self.state.get_used_storage_share[0] if energy >= first_in_energy_with_origin.value: energy -= first_in_energy_with_origin.value self.state.get_used_storage_share.pop(0) elif energy < first_in_energy_with_origin.value: residual = first_in_energy_with_origin.value - energy self.state._used_storage_share[0] = \ EnergyOrigin(first_in_energy_with_origin.origin, residual) energy = 0 def _track_energy_bought_type(self, trade): if area_name_from_area_or_iaa_name(trade.seller) == self.area.name: self.state.update_used_storage_share(trade.offer.energy, ESSEnergyOrigin.EXTERNAL) elif self._is_local(trade): self.state.update_used_storage_share(trade.offer.energy, ESSEnergyOrigin.LOCAL) else: self.state.update_used_storage_share(trade.offer.energy, ESSEnergyOrigin.UNKNOWN) def event_bid_traded(self, *, market_id, bid_trade): super().event_bid_traded(market_id=market_id, bid_trade=bid_trade) market = self.area.get_future_market_from_id(market_id) if bid_trade.offer.buyer == self.owner.name: self._track_energy_bought_type(bid_trade) self.state.pledged_buy_kWh[ market.time_slot] += bid_trade.offer.energy self.state.offered_buy_kWh[ market.time_slot] -= bid_trade.offer.energy def event_market_cycle(self): super().event_market_cycle() self.offer_update.update_market_cycle_offers(self) for market in self.area.all_markets[:-1]: self.bid_update.update_counter[market.time_slot] = 0 current_market = self.area.next_market past_market = self.area.last_past_market self.state.market_cycle( past_market.time_slot if past_market else current_market.time_slot, current_market.time_slot) if self.state.used_storage > 0: self.sell_energy() if ConstSettings.IAASettings.MARKET_TYPE == 2 or \ ConstSettings.IAASettings.MARKET_TYPE == 3: self.state.clamp_energy_to_buy_kWh([current_market.time_slot]) self.bid_update.update_market_cycle_bids(self) energy_kWh = self.state.energy_to_buy_dict[ current_market.time_slot] if energy_kWh > 0: self.post_first_bid(current_market, energy_kWh * 1000.0) self.state.offered_buy_kWh[ current_market.time_slot] += energy_kWh def event_balancing_market_cycle(self): if not self.is_eligible_for_balancing_market: return current_market = self.area.next_market free_storage = self.state.free_storage(current_market.time_slot) if free_storage > 0: charge_energy = self.balancing_energy_ratio.demand * free_storage charge_price = DeviceRegistry.REGISTRY[ self.owner.name][0] * charge_energy if charge_energy != 0 and charge_price != 0: # committing to start charging when required self.area.get_balancing_market(self.area.now).balancing_offer( charge_price, -charge_energy, self.owner.name) if self.state.used_storage > 0: discharge_energy = self.balancing_energy_ratio.supply * self.state.used_storage discharge_price = DeviceRegistry.REGISTRY[ self.owner.name][1] * discharge_energy # committing to start discharging when required if discharge_energy != 0 and discharge_price != 0: self.area.get_balancing_market(self.area.now).balancing_offer( discharge_price, discharge_energy, self.owner.name) def _try_to_buy_offer(self, offer, market, max_affordable_offer_rate): if offer.seller == self.owner.name: # Don't buy our own offer return # Check if the price is cheap enough if offer.energy_rate > max_affordable_offer_rate: # Can early return here, because the offers are sorted according to energy rate # therefore the following offers will be more expensive return True alt_pricing_settings = ConstSettings.IAASettings.AlternativePricing if offer.seller == alt_pricing_settings.ALT_PRICING_MARKET_MAKER_NAME \ and alt_pricing_settings.PRICING_SCHEME != 0: # don't buy from IAA if alternative pricing scheme is activated return try: self.state.clamp_energy_to_buy_kWh( [ma.time_slot for ma in self.area.all_markets]) max_energy = min(offer.energy, self.state.energy_to_buy_dict[market.time_slot]) if not self.state.has_battery_reached_max_power( -max_energy, market.time_slot): self.state.pledged_buy_kWh[market.time_slot] += max_energy self.accept_offer(market, offer, energy=max_energy, buyer_origin=self.owner.name) return except MarketException: # Offer already gone etc., try next one. return def buy_energy(self, market, offer=None): if not market: return if self.state.has_battery_reached_max_power(-FLOATING_POINT_TOLERANCE, market.time_slot): return max_affordable_offer_rate = min( self.bid_update.get_updated_rate(market.time_slot), self.bid_update.final_rate[market.time_slot]) # Check if storage has free capacity if self.state.free_storage(market.time_slot) <= 0.0: return if offer: self._try_to_buy_offer(offer, market, max_affordable_offer_rate) else: for offer in market.sorted_offers: if self._try_to_buy_offer(offer, market, max_affordable_offer_rate) is False: return def sell_energy(self): markets_to_sell = self.select_market_to_sell() energy_sell_dict = self.state.clamp_energy_to_sell_kWh( [ma.time_slot for ma in markets_to_sell]) for market in markets_to_sell: selling_rate = self.calculate_selling_rate(market) energy = energy_sell_dict[market.time_slot] if not self.state.has_battery_reached_max_power( energy, market.time_slot): if energy > 0.0: offer = market.offer(price=energy * selling_rate, energy=energy, seller=self.owner.name, original_offer_price=energy * selling_rate, seller_origin=self.owner.name) self.offers.post(offer, market.id) self.state.offered_sell_kWh[ market.time_slot] += offer.energy def select_market_to_sell(self): if StorageSettings.SELL_ON_MOST_EXPENSIVE_MARKET: # Sell on the most expensive market try: max_rate = 0.0 most_expensive_market = self.area.all_markets[0] for market in self.area.all_markets: if len(market.sorted_offers) > 0 and \ market.sorted_offers[0].energy_rate > max_rate: max_rate = market.sorted_offers[ 0].price / market.sorted_offers[0].energy most_expensive_market = market except IndexError: try: most_expensive_market = self.area.current_market except StopIteration: return return [most_expensive_market] else: return self.area.all_markets def calculate_selling_rate(self, market): if self.cap_price_strategy is True: return self.capacity_dependant_sell_rate(market) else: return self.offer_update.initial_rate[market.time_slot] def capacity_dependant_sell_rate(self, market): if self.state.charge_history[market.time_slot] == "-": soc = self.state.used_storage / self.state.capacity else: soc = self.state.charge_history[market.time_slot] / 100.0 max_selling_rate = self.offer_update.initial_rate[market.time_slot] min_selling_rate = self.offer_update.final_rate[market.time_slot] if max_selling_rate < min_selling_rate: return min_selling_rate else: return max_selling_rate - (max_selling_rate - min_selling_rate) * soc def event_offer(self, *, market_id, offer): super().event_offer(market_id=market_id, offer=offer) if ConstSettings.IAASettings.MARKET_TYPE == 1: market = self.area.get_future_market_from_id(market_id) if offer.id in market.offers and \ offer.seller != self.owner.name and \ offer.seller != self.area.name: self.buy_energy(market, offer)