battery_optimizer_app.handlers#

Functions

create_updated_request_for_melmilboa(...)

This function updates the given wrapped_problem to run a mel mil boa optimization. This includes: - updating the initial soe with the soe at the end of the boa. - overwriting the battery marketed of the running SP with the remaining baseline power - overwriting the energy throughput by dynamic services according to the remaining time of the SP - set the battery_marketed values to 0 for the settlement periods during which the BOA is active - extension of the optimization horizon to meet the intended period of optimization after the BOA.

get_afrr_energy(problem, timestamp_afrr, ...)

Compute the optimal aFRR (automatic Frequency Restoration Reserve) energy position for a single settlement period and derive blocked intraday counter capacities.

get_last_afrr_energy_counter_tradeable_timestamp(...)

get_optimizer_and_result_class(wrapped_problem)

handle_compute_bod_request(wrapped_problem)

handle_optimize_afrr_energy_request(...[, ...])

handle_optimize_melmil_after_boa_request(...)

This handles the MEL MIL optimization with BOAs.

handle_optimize_melmil_now_request(...)

This handles MELMIL optimization for a given expected SoC level at a given timestamp for the respective SP until the end of the SP.

handle_optimize_melmil_request(wrapped_problem)

This handles the MEL MIL optimization for full settlement periods without BOAs.

handle_optimize_pnl_request(wrapped_problem, ...)

This function returns an optimization result for the PNL case.

handle_request(message, request_id)

Based on the type of problem, it chooses the correct optimization.

solve_mip_then_ip(problem, request[, ...])

Note: DS = dynamic services, i.e. any or all of DC, DM, DR.

battery_optimizer_app.handlers.create_updated_request_for_melmilboa(wrapped_problem)#

This function updates the given wrapped_problem to run a mel mil boa optimization. This includes:

  • updating the initial soe with the soe at the end of the boa.

  • overwriting the battery marketed of the running SP with the remaining baseline power

  • overwriting the energy throughput by dynamic services according to the remaining time of the SP

  • set the battery_marketed values to 0 for the settlement periods during which the BOA is active

  • extension of the optimization horizon to meet the intended period of optimization after the BOA

battery_optimizer_app.handlers.get_afrr_energy(problem, timestamp_afrr, afrr_energy_market, premium_per_MWh, premium_upper_bound_per_MWh)#

Compute the optimal aFRR (automatic Frequency Restoration Reserve) energy position for a single settlement period and derive blocked intraday counter capacities.

This helper performs a focused re-optimization of an existing solved (or solvable) DE battery problem where exactly one timestamp (timestamp_afrr) is exposed to either the positive or negative aFRR energy market while later timestamps (after the activation delay) may participate in the intraday market for counter deals. The function intentionally restructures the market set, price forecast, and battery marketed data to isolate the competition between aFRR energy, intraday counter trades and imbalance penalties, then runs a fresh optimization to read out the marginal volumes and prices.

Algorithm overview (high level steps):

  1. Infer direction (POS / NEG) and configure side-specific variable & column names.

  2. Copy the current optimization request from problem and rewrite the market participation map so: * Only the aFRR energy market is active at timestamp_afrr. * No markets are active until an activation buffer of two settlement lengths has passed. * Intraday market is enabled from the earliest counter timestamp onwards to allow counter deals.

  3. Build a placeholder aFRR energy price that is intentionally larger than any intraday price but still comfortably below imbalance penalty costs. Rationale: Ensures hierarchy of economic attractiveness -> imbalance >> aFRR placeholder > intraday, so optimizer always prefers fulfilling requested aFRR volume before exploiting intraday spread.

  4. Inject placeholder price for the target timestamp & market and set the aFRR energy column to NA for other timestamps (no trading).

  5. Block one side of the intraday orderbook by overwriting the respective bucket list with a single artificial price bucket that has zero available volume (extreme price + volume 0). This prevents speculative P&L optimization on that side while still allowing necessary counter trades. Rationale: We only want intraday transactions that offset energy introduced by aFRR activation; full intraday optimization would otherwise distort intended prioritization.

  6. Zero out marketed battery positions for the opposite aFRR direction (drop column) to avoid double counting or artificial supply/demand.

  7. Re-instantiate a new DEBatteryOptimizer with modified data and solve.

  8. Extract full result and delta (counter trades) DataFrames; compute VWAP (volume-weighted average price) for intraday counter deals if any volume occurred; else set base price to NaN.

  9. Derive blocked capacities structure (time_from/time_to/volume/price) from intraday delta DataFrame for compatibility with downstream continuous position schemas.

  10. Return an aFRREnergyPosition encapsulating aFRR volume, computed prices, metadata, and blocked intraday capacities.

Edge cases & behaviors: - If no intraday counter volume was executed (total converted MWh == 0) the resulting VWAP is NaN. - Placeholder price selection falls back to (min imbalance penalty / 10) if both bucket max price and price forecast maxima are unavailable. This keeps it positive and below imbalance signals. - Raises ValueError if an unknown market type is passed. - Market mapping logic covers all branching paths; the final else in loop is defensive.

Placeholder price decision order: 1. Use problem.asset.price_buckets_max_price * 2 if available and > 0. 2. Else use twice the maximum observed forecast price. 3. Else degrade to a fraction of minimal imbalance penalty cost. Rationale: Guarantee ordering aFRR > intraday but << imbalance to preserve penalty deterrence.

Performance considerations: - Runs a full MILP re-solve per timestamp & direction invocation; upstream caller should batch or cache where appropriate. - Uses DataFrame round-trip conversions only where needed (price forecast, buckets, marketed battery).

Parameters:
  • problem (DEBatteryOptimizer) – A fully configured (and potentially already solved) DE optimizer instance whose data acts as the template for the focused re-optimization.

  • timestamp_afrr (datetime) – The settlement period timestamp at which aFRR energy activation is considered.

  • afrr_energy_market (MarketType) – Either MarketType.AFRR_ENERGY_POS or MarketType.AFRR_ENERGY_NEG indicating direction.

  • premium_per_MWh (float) – External premium to add on top of the intraday counter VWAP to form final aFRR clearing price.

  • premium_upper_bound_per_MWh (float)

Returns:

Structured result containing: start timestamp, optimized aFRR volume in MW, base counter VWAP, premium applied, final price, market enum, optimization metadata (DataFrame) and formatted blocked intraday capacities list.

Return type:

aFRREnergyPosition

Raises:

ValueError – If afrr_energy_market is not one of the expected aFRR energy market types or if unexpected timestamp logic path is encountered (defensive branch).

Notes

Rationale: Separating this logic allows precise measurement of marginal aFRR opportunity cost against intraday without contamination from broader multi-market strategy present in the original problem. The artificial market & price shaping enforces deterministic prioritization while still permitting necessary counter balancing trades.

battery_optimizer_app.handlers.get_last_afrr_energy_counter_tradeable_timestamp(afrr_timestamp, problem)#
Parameters:
battery_optimizer_app.handlers.get_optimizer_and_result_class(wrapped_problem)#
Parameters:

wrapped_problem (APIWrappedBatteryOptimizer)

battery_optimizer_app.handlers.handle_compute_bod_request(wrapped_problem)#
Parameters:

wrapped_problem (APIWrappedBodRequest)

Return type:

BodResult

battery_optimizer_app.handlers.handle_optimize_afrr_energy_request(wrapped_problem, request_id=None)#
Parameters:
Return type:

aFRREnergyResult

battery_optimizer_app.handlers.handle_optimize_melmil_after_boa_request(wrapped_problem)#

This handles the MEL MIL optimization with BOAs. In addition to the request without BOAs, the request contains boa data and baseline data. The BOA data contains timestamp and power of at least one boa. The latest timestamp corresponds to the end of the BOA. The baseline data contains a timeseries of power and expected soc level.

The end of the boa is not necessarily during the end of a settlement period. However, the optimizer works in full settlement period granularity. The settlement period of the BOA end is obtained. The remaining baseline activities of this settlement period after the BOA are stretched to a full settlement period. The energy flow resulting from the baseline is obtained. With the simplification, that this energy flows during a whole settlement period, the resulting power is calculated. As a workaround, the battery marketed data of epex30 is overwritten.

The initial SoC level is set to the expected SoC level of the baseline data at the end of the BOA.

In case the BOA is ending after the first given settlement period, the mel mil optimization horizon is extended.

MEL and MIL optimization are run successively and both outcomes are returned for the time period after the latest BOA.

Parameters:

wrapped_problem (APIWrappedMelMilBoaRequest)

Return type:

MELMILResult

battery_optimizer_app.handlers.handle_optimize_melmil_now_request(wrapped_problem)#

This handles MELMIL optimization for a given expected SoC level at a given timestamp for the respective SP until the end of the SP. This means, only a single MEL MIL pair in MELMILResult is returned.

In short: We update the usual PNL request. The initial soc is the expected soc_now, the battery_marketed and dynamic service energy throughput values are reduced by the remaining fraction of the SP. Dynamic service MW values stay the same (because they block mel mil independent of the remaining time). MELMIL optimization is run. The returned time is overwritten by timestamp now to match the logic of the optimization.

Parameters:

wrapped_problem (APIWrappedMelMilNowRequest)

Return type:

MELMILResult

battery_optimizer_app.handlers.handle_optimize_melmil_request(wrapped_problem)#

This handles the MEL MIL optimization for full settlement periods without BOAs. MEL and MIL optimization are run successively and both outcomes are returned.

Args:

wrapped_problem: the request structure of this mel mil request matches the one of the PNL request

Returns:

MELMILResult: optimized values for both MEL and MIL

Parameters:

wrapped_problem (APIWrappedMelMilRequest)

Return type:

MELMILResult

battery_optimizer_app.handlers.handle_optimize_pnl_request(wrapped_problem, request_id, return_dataframe=False, return_problem=False)#

This function returns an optimization result for the PNL case.

Args:

wrapped_problem (APIWrappedBatteryOptimizer): the problem to solve request_id (str): request id

Returns:

Result: the result of the optimization

Parameters:
Return type:

DEResult | UKResult | DataFrame

battery_optimizer_app.handlers.handle_request(message, request_id)#

Based on the type of problem, it chooses the correct optimization. This would be equivalent to different API entries, but using a generic Kafka client that listens to different groups?

Args:
message (dict, APIWrappedBodRequest, APIWrappedBatteryOptimizer, APIWrappedMelMilRequest, APIWrappedMelMilBoaRequest):

Received message with all information needed to run optimization

request_id (str): Request ID

Returns:

result (Result, BodResult): the result of running the optimization for the requested type of problem

Parameters:
Return type:

DEResult | UKResult | BodResult | MELMILResult

battery_optimizer_app.handlers.solve_mip_then_ip(problem, request, return_dataframe=False, return_rerun_problem=False)#

Note: DS = dynamic services, i.e. any or all of DC, DM, DR

This utility function is used for optimization requests that optimize for a commodity market AND DS. Commodity markets are optimized as floating point variables that are rounded after the optimization run to the nearest increment these markets can actually be accessed at - i.e. for N2EX and EPEX 100 kW or 0.1 MW. When optimizing both a commodity market and DS, then rounding the result may lead to inconsistent DS results in edge cases: E.g. an N2EX buy position may come out of the optimization as 1.04 MW with DSL based off the resulting SoE at 2 MW. When rounding the N2EX buy position to 1.0 MW (rounded to the nearest increment of 100 kW) DSL at 2 MW may not be feasible anymore due to a slightly lowered SoE in the “rounded state of the asset”.

To avoid this, here we run the full problem (in our example N2EX + DS optimization) and input the rounded N2EX result into a DS-only optimization run based off of the original optimization request.

Parameters:

problem (UKBatteryOptimizer)