nexa-bidkit
Day-ahead and intraday auction bid generation library for European power markets. DataFrame-first API for curve construction, block bids, and portfolio management, with EUPHEMIA-compatible validation and Nord Pool serialisation.
Python 3.11+ MIT License 1.0.0
pip install nexa-bidkit
Why this exists
Building bids for European power auctions is complex. The EUPHEMIA algorithm supports block bids, linked orders, exclusive groups, and merit-order curves, each with specific constraints. Getting the domain model wrong means rejected bids or unintended market exposure.
nexa-bidkit provides:
- DataFrame-first curve construction — build price-quantity curves directly from your forecast or optimisation output
- Correct EUPHEMIA bid type modelling — block bids, linked orders, exclusive groups
- Pre-submission validation catching constraint violations before they reach the exchange
- 15-minute MTU support natively across all bid types
- Nord Pool serialisation — converts your bids to Nord Pool Auction API payloads
Quick start
Build a supply curve from a DataFrame
import pandas as pd
from decimal import Decimal
from datetime import datetime
from zoneinfo import ZoneInfo
from nexa_bidkit import CurveType, MTUDuration, MTUInterval, from_dataframe
# Your bid data
df = pd.DataFrame({
"price": [10.5, 25.0, 45.0, 80.0],
"volume": [50, 100, 75, 25],
})
# Define the MTU (15-minute interval)
mtu = MTUInterval.from_start(
datetime(2026, 4, 1, 13, 0, tzinfo=ZoneInfo("Europe/Oslo")),
MTUDuration.QUARTER_HOURLY
)
curve = from_dataframe(df, curve_type=CurveType.SUPPLY, mtu=mtu)
print(f"Total volume: {curve.total_volume} MW")
print(f"Price range: {curve.min_price} - {curve.max_price} EUR/MWh")
Build block bids
from decimal import Decimal
from datetime import datetime
from zoneinfo import ZoneInfo
from nexa_bidkit import (
BiddingZone, Direction, DeliveryPeriod, MTUDuration,
block_bid, indivisible_block_bid, linked_block_bid, exclusive_group,
)
# Four-hour delivery window, 15-minute MTUs
delivery = DeliveryPeriod(
start=datetime(2026, 4, 1, 10, 0, tzinfo=ZoneInfo("Europe/Oslo")),
end=datetime(2026, 4, 1, 14, 0, tzinfo=ZoneInfo("Europe/Oslo")),
duration=MTUDuration.QUARTER_HOURLY,
)
# Divisible block bid — accepts partial fill down to 50%
peak_bid = block_bid(
bidding_zone=BiddingZone.NO1,
direction=Direction.SELL,
delivery_period=delivery,
price=Decimal("55.0"),
volume=Decimal("100"),
min_acceptance_ratio=Decimal("0.5"),
)
# Indivisible — must run, all-or-nothing
must_run = indivisible_block_bid(
bidding_zone=BiddingZone.NO1,
direction=Direction.SELL,
delivery_period=delivery,
price=Decimal("25.0"),
volume=Decimal("50"),
)
# Linked bid — only accepted if parent is accepted
ramp_up = linked_block_bid(
parent_bid_id=must_run.bid_id,
bidding_zone=BiddingZone.NO1,
direction=Direction.SELL,
delivery_period=delivery,
price=Decimal("35.0"),
volume=Decimal("25"),
)
# Exclusive group — at most one of these is accepted
option_a = block_bid(BiddingZone.NO1, Direction.SELL, delivery, Decimal("40.0"), Decimal("150"))
option_b = block_bid(BiddingZone.NO1, Direction.SELL, delivery, Decimal("45.0"), Decimal("120"))
options = exclusive_group([option_a, option_b])
Supported bid types
Key concepts
Price-quantity curves
Merit-order curves represent supply or demand. Supply curves are sorted ascending by price (cheapest generation first); demand curves descending.
from nexa_bidkit import merge_curves, clip_curve, filter_zero_volume, aggregate_by_price
# Combine bids from multiple plants
portfolio = merge_curves(
[plant1_curve, plant2_curve],
aggregation="sum"
)
# Clean and constrain
portfolio = filter_zero_volume(portfolio)
portfolio = clip_curve(
portfolio,
min_price=Decimal("0"),
max_volume=Decimal("500"),
)
portfolio = aggregate_by_price(portfolio) # Consolidate for smaller message size
Order book
Manage a portfolio of bids across zones and sessions:
from nexa_bidkit import (
create_order_book, add_bid, add_bids,
get_bids_by_zone, count_bids, total_volume_by_zone,
update_all_statuses, orders_to_dataframe, BidStatus, BiddingZone,
)
book = create_order_book()
book = add_bids(book, [must_run, peak_bid, ramp_up])
no1_bids = get_bids_by_zone(book, BiddingZone.NO1)
volumes = total_volume_by_zone(book)
print(f"NO1 volume: {volumes[BiddingZone.NO1]} MW")
# Export to DataFrame for analysis
df = orders_to_dataframe(book)
Validation
Validate bids against EUPHEMIA rules before submission:
from nexa_bidkit import (
validate_bid, validate_bids, validate_order_book_for_submission,
get_validation_summary, EuphemiaValidationError,
)
from datetime import datetime
from zoneinfo import ZoneInfo
# Validate a single bid
try:
validate_bid(peak_bid)
except EuphemiaValidationError as e:
print(f"EUPHEMIA compliance error: {e}")
# Batch validation
results = validate_bids([must_run, peak_bid, ramp_up])
summary = get_validation_summary(results)
print(f"Pass rate: {summary['pass_rate']:.1f}%")
# Full pre-submission check (includes gate closure)
gate_closure = datetime(2026, 3, 31, 12, 0, tzinfo=ZoneInfo("Europe/Oslo"))
validate_order_book_for_submission(book, gate_closure_time=gate_closure)
Validation enforces:
- EUPHEMIA rules: maximum curve steps (200), block duration limits
- Data quality: minimum volumes (0.1 MW), reasonable price increments
- Temporal constraints: gate closure deadlines, delivery periods within auction day
Nord Pool serialisation
Convert your bids to Nord Pool Auction API payloads. You supply a ContractIdResolver callable that maps (MTUInterval, BiddingZone) to Nord Pool contract IDs — call Nord Pool's products API to populate this at runtime.
from nexa_bidkit.nordpool import (
simple_bid_to_curve_order,
block_bid_to_block_list,
order_book_to_nord_pool,
)
def resolve_contract(mtu, zone):
# Call Nord Pool products API in production
hour = mtu.start.hour
return f"{zone.value}-{hour}"
# Convert a full order book
submission = order_book_to_nord_pool(
book,
auction_id="DA-2026-04-01",
portfolio="my-portfolio",
contract_id_resolver=resolve_contract,
)
print(f"Curve orders: {len(submission.curve_orders)}")
print(f"Block orders: {len(submission.block_orders)}")
print(f"Linked block orders: {len(submission.linked_block_orders)}")
print(f"Exclusive groups: {len(submission.exclusive_group_orders)}")
Examples
Four Jupyter notebooks covering real-world scenarios are included in the repository:
Premium: Hosted Bid Optimisation
Coming Soon
For teams that want optimal bid portfolio construction:
- MILP optimisation engine given asset constraints and price forecasts
- Cloud-hosted solver with sub-minute solve times
- Excel add-in for non-coding desk users
Pricing starts at EUR 99/month. Learn more.