Skip to main content

Python library for building, validating, and serializing mFRR energy activation market bids for the Nordic TSOs

Project description

nexa-mfrr-nordic-eam

CI codecov Python 3.11+

This project is a work in progress. The API, documentation, and feature set are under active development and subject to change. If you want to get involved, receive progress updates, or have feedback, please open an issue or contact the repo admin.

Python library for submitting mFRR energy activation market bids to the Nordic TSOs (Statnett, Fingrid, Energinet, Svenska kraftnat).

Built for the 75% who connect via API and build their own.

Implementation status

Module Status Notes
types.py ✅ Done All enums + Pydantic models (BidTimeSeriesModel, BidDocumentModel, etc.)
exceptions.py ✅ Done NexaMFRREAMError, InvalidMTUError, NaiveDatetimeError, BidValidationError
config.py ✅ Done Global MARI mode, configure(), get_mari_mode()
timing.py ✅ Done MTU, GateClosure, gate_closure(), current_mtu(), mtu_range(), evaluate_conditional_availability()
__init__.py ✅ Done Public re-exports including Bid, BidDocument, BuiltBidDocument
bids/simple.py ✅ Done Bid factory + SimpleBidBuilder with fluent API
bids/validation.py ✅ Done Common + TSO-configurable validation rules
bids/complex.py 🔲 Planned Exclusive, inclusive, multipart group builders
bids/linked.py ✅ Done TechnicalLink builder; conditional link methods on SimpleBidBuilder
xml/namespaces.py ✅ Done NBM and IEC namespace URI constants
xml/serialize.py ✅ Done Pydantic models → CIM XML (XSD-compliant element ordering)
xml/deserialize.py 🔲 Planned CIM XML → Pydantic models
tso/base.py ✅ Done TSOConfig strategy dataclass
tso/statnett.py ✅ Done Statnett (NO) configuration
tso/fingrid.py 🔲 Planned Fingrid (FI) configuration
tso/energinet.py 🔲 Planned Energinet (DK) configuration
tso/svk.py ✅ Done Svenska kraftnat (SE) configuration
documents/reserve_bid.py ✅ Done BidDocument factory + BidDocumentBuilder + BuiltBidDocument
documents/activation.py 🔲 Planned Activation parser + response builder
documents/acknowledgement.py 🔲 Planned ACK parser
documents/bid_availability.py 🔲 Planned Availability parser
documents/allocation_result.py 🔲 Planned Allocation result parser
heartbeat.py 🔲 Planned Heartbeat detection + response
pandas.py 🔲 Planned DataFrame → Bid conversion
examples/ ✅ Done Jupyter notebooks: Statnett daily bid preparation (GS tax); SVK technically + conditionally linked bids

What this does

This library handles the full BSP workflow for the Nordic mFRR energy activation market.

Implemented today:

  • Build simple bids - Divisible and indivisible bids with full attribute support (volume, price, resource, product type, duration constraints)
  • Technically linked bids - TechnicalLink builder groups bids across MTUs under a shared link ID to prevent double-activation
  • Conditionally linked bids - .conditionally_available(), .conditionally_unavailable(), .link_to() on the bid builder; all three condition codes (A55, A56, A67)
  • TSO configuration - Statnett (NO) and Svenska kraftnat (SE) fully configured; Fingrid and Energinet planned
  • Validate before you send - Common and TSO-configurable validation rules, pre-MARI and post-MARI price limits
  • Serialize to CIM XML - Generates compliant ReserveBid_MarketDocument XML per the NBM ReserveBid schema with strict XSD element ordering
  • Timing helpers - Gate closure calculations, MTU boundaries, DST handling, MARI vs pre-MARI timing

Planned:

  • Complex bid groups - Exclusive, inclusive, and multipart group builders (bids/complex.py)
  • Parse TSO responses - Acknowledgements, activation orders, bid availability reports, allocation results
  • Handle activations - Build activation response documents, track activation state
  • Heartbeat responder - Automatic heartbeat processing for Statnett and Svenska kraftnat
  • Pandas integration - Build bid portfolios from DataFrames
  • Deserializer - Parse incoming CIM XML back to Pydantic models

What it does not do: manage your ECP/EDX endpoint. That is infrastructure you deploy and operate separately (see ECP/EDX Setup below). This library generates the XML documents you send through your endpoint.

Installation

pip install nexa-mfrr-nordic-eam

With Pandas support:

pip install nexa-mfrr-nordic-eam[pandas]

Quick start

Note: Simple bids, technically linked bids, conditional links, document building, serialization, and timing helpers are implemented. Examples for complex bids (MultipartGroup, ExclusiveGroup), activation parsing, and Pandas integration show the intended API and will work once those modules are complete.

Submit a simple divisible bid to Statnett

from datetime import datetime, timezone
from nexa_mfrr_eam import (
    Bid, BidDocument, Direction, MarketProductType,
    BiddingZone, TSO, MARIMode,
)

# Create a simple divisible up-regulation bid
bid = (
    Bid.up(volume_mw=50, price_eur=85.50)
    .divisible(min_volume_mw=10)
    .for_mtu("2026-03-21T10:00Z")
    .resource("NOKG90901", coding_scheme="NNO")  # Statnett regulation object
    .product_type(MarketProductType.SCHEDULED_AND_DIRECT)  # A07
    .build()
)

# Wrap in a document targeting Statnett
doc = (
    BidDocument(tso=TSO.STATNETT)
    .sender(party_id="9999909919920", coding_scheme="A10")  # GS1
    .add_bid(bid)
    .build()
)

# Validate against Statnett-specific rules (pre-MARI timing)
errors = doc.validate(mari_mode=MARIMode.PRE_MARI)
if errors:
    for e in errors:
        print(f"  {e}")
else:
    xml_bytes = doc.to_xml()
    # Send xml_bytes via your ECP endpoint to Statnett

Build a multipart bid

Not yet implemented. MultipartGroup is planned in bids/complex.py.

Multipart bids are groups of simple bids at different price levels for the same MTU. If the higher-priced component is accepted, all cheaper components must also be accepted.

from nexa_mfrr_eam import MultipartGroup  # coming soon

group = (
    MultipartGroup.up(bidding_zone=BiddingZone.NO2)
    .for_mtu("2026-03-21T10:00Z")
    .resource("NOKG90901", coding_scheme="NNO")
    .add_component(volume_mw=20, price_eur=50.00)
    .add_component(volume_mw=15, price_eur=75.00)
    .add_component(volume_mw=10, price_eur=120.00)
    .build()
)

doc = (
    BidDocument(tso=TSO.STATNETT)
    .sender(party_id="9999909919920", coding_scheme="A10")
    .add_group(group)
    .build()
)

Build an exclusive group

Not yet implemented. ExclusiveGroup is planned in bids/complex.py.

Only one bid in the group can be activated. Useful when you have alternative resources that cannot run simultaneously.

from nexa_mfrr_eam import ExclusiveGroup  # coming soon

group = (
    ExclusiveGroup(bidding_zone=BiddingZone.NO2)
    .for_mtu("2026-03-21T10:00Z")
    .add_bid(
        Bid.up(volume_mw=30, price_eur=60.00)
        .indivisible()
        .resource("NOKG90901", coding_scheme="NNO")
    )
    .add_bid(
        Bid.up(volume_mw=50, price_eur=80.00)
        .divisible(min_volume_mw=10)
        .resource("NOKG90902", coding_scheme="NNO")
    )
    .product_type(MarketProductType.SCHEDULED_ONLY)
    .build()
)

Build technically linked bids across MTUs

Technical linking ensures a resource is not double-activated when a bid in one MTU is activated via direct activation and is still ramping during the next MTU.

from nexa_mfrr_eam import TechnicalLink

link = (
    TechnicalLink(bidding_zone=BiddingZone.SE3)
    .resource("REG-OBJ-SE-001", coding_scheme="A01")  # EIC
    .add_mtu(
        mtu="2026-03-21T10:00Z",
        direction=Direction.UP,
        volume_mw=25,
        price_eur=90.00,
        product_type=MarketProductType.SCHEDULED_AND_DIRECT,
    )
    .add_mtu(
        mtu="2026-03-21T10:15Z",
        direction=Direction.UP,
        volume_mw=25,
        price_eur=90.00,
        product_type=MarketProductType.SCHEDULED_AND_DIRECT,
    )
    .add_mtu(
        mtu="2026-03-21T10:30Z",
        direction=Direction.UP,
        volume_mw=25,
        price_eur=90.00,
        product_type=MarketProductType.SCHEDULED_AND_DIRECT,
    )
    .build()
)

Build conditional links

Conditional linking adjusts bid availability in QH0 based on activation outcomes in QH-1 or QH-2.

from nexa_mfrr_eam import ConditionalStatus

# "Make my bid in 10:30 unavailable if the linked bid in 10:15 was activated"
bid_qh_minus_1 = (
    Bid.up(volume_mw=30, price_eur=70.00)
    .divisible(min_volume_mw=5)
    .for_mtu("2026-03-21T10:15Z")
    .resource("NOKG90901", coding_scheme="NNO")
    .build()
)

bid_qh_0 = (
    Bid.up(volume_mw=30, price_eur=70.00)
    .divisible(min_volume_mw=5)
    .for_mtu("2026-03-21T10:30Z")
    .resource("NOKG90901", coding_scheme="NNO")
    .conditionally_available()  # A65
    .link_to(bid_qh_minus_1, status=ConditionalStatus.NOT_AVAILABLE_IF_ACTIVATED)  # A55
    .build()
)

National attributes: Statnett period shift

Not yet implemented. .period_shift() on SimpleBidBuilder is planned. Period-shift-only bids using MarketProductType.PERIOD_SHIFT_ONLY (Z01) are already supported via .product_type().

from nexa_mfrr_eam import PeriodShiftPosition

# Standard product bid that can also be used for period shift (coming soon)
bid = (
    Bid.up(volume_mw=20, price_eur=65.00)
    .indivisible()
    .for_mtu("2026-03-21T10:00Z")
    .resource("NOKG90901", coding_scheme="NNO")
    .period_shift(PeriodShiftPosition.END_OF_PERIOD)  # Z65 – not yet implemented
    .build()
)

# Period-shift-only bid (no standard product participation) – works today
ps_bid = (
    Bid.up(volume_mw=15)  # No price for period shift only
    .indivisible()
    .for_mtu("2026-03-21T10:00Z")
    .resource("NOKG90901", coding_scheme="NNO")
    .product_type(MarketProductType.PERIOD_SHIFT_ONLY)  # Z01
    .build()
)

National attributes: maximum duration and resting time

from nexa_mfrr_eam import TechnicalLink, Direction

# A hydro unit that can only run for 90 minutes and needs 60 minutes rest
link = (
    TechnicalLink(bidding_zone=BiddingZone.NO2)
    .resource("NOKG-HYDRO-01", coding_scheme="NNO")
    .max_duration(minutes=90)
    .resting_time(minutes=60)
    .add_mtu("2026-03-21T10:00Z", Direction.UP, 40, 55.00)
    .add_mtu("2026-03-21T10:15Z", Direction.UP, 40, 55.00)
    .add_mtu("2026-03-21T10:30Z", Direction.UP, 40, 55.00)
    .add_mtu("2026-03-21T10:45Z", Direction.UP, 40, 55.00)
    .add_mtu("2026-03-21T11:00Z", Direction.UP, 40, 55.00)
    .add_mtu("2026-03-21T11:15Z", Direction.UP, 40, 55.00)
    .build()
)

National attributes: faster activation (Statnett)

# A battery that can ramp in 2 minutes (FAT = 3 min including 1 min prep)
bid = (
    Bid.up(volume_mw=10, price_eur=200.00)
    .indivisible()
    .for_mtu("2026-03-21T10:00Z")
    .resource("NOKG-BATT-01", coding_scheme="NNO")
    .product_type(MarketProductType.SCHEDULED_AND_DIRECT)
    .faster_activation(minutes=3)  # PT3M
    .build()
)

Energinet: direct activation (local model)

Denmark uses a different direct activation model where the DA bid and its linked bid in the next quarter are two separate bids with potentially different prices.

import uuid

# Direct activatable bid for current QH
da_bid = (
    Bid.up(volume_mw=25, price_eur=100.00)
    .divisible(min_volume_mw=5)
    .for_mtu("2026-03-21T10:00Z")
    .resource("DK1-RES-001", coding_scheme="A01")
    .product_type(MarketProductType.SCHEDULED_AND_DIRECT)
    .technical_link(link_id := str(uuid.uuid4()))
    .build()
)

# Linked bid for next QH (can have different price/volume) – shares the same link ID
next_qh_bid = (
    Bid.up(volume_mw=30, price_eur=95.00)
    .divisible(min_volume_mw=5)
    .for_mtu("2026-03-21T10:15Z")
    .resource("DK1-RES-001", coding_scheme="A01")
    .product_type(MarketProductType.SCHEDULED_AND_DIRECT)
    .technical_link(link_id)
    .build()
)

Statnett non-standard bids: mFRR-D (disturbance reserve)

Not yet implemented. .non_standard() on TechnicalLink is planned. The NonStandardType enum and Reason model are already defined in types.py.

from nexa_mfrr_eam import NonStandardType

link = (
    TechnicalLink(bidding_zone=BiddingZone.NO2)
    .resource("NOKG-DFR-01", coding_scheme="NNO")
    .non_standard(NonStandardType.DISTURBANCE_RESERVE)  # Z74 – not yet implemented
    .add_mtu("2026-03-21T10:00Z", Direction.UP, 100, 45.00)
    .add_mtu("2026-03-21T10:15Z", Direction.UP, 100, 45.00)
    .build()
)

Build a bid portfolio from a Pandas DataFrame

import pandas as pd
from nexa_mfrr_eam.pandas import bids_from_dataframe

df = pd.DataFrame({
    "mtu_start":  pd.to_datetime(["2026-03-21T10:00Z", "2026-03-21T10:15Z", "2026-03-21T10:30Z"]),
    "direction":  ["up", "up", "up"],
    "volume_mw":  [50, 45, 55],
    "price_eur":  [72.30, 74.10, 69.50],
    "min_volume":  [10, 10, 10],
    "resource":   ["NOKG90901"] * 3,
})

bids = bids_from_dataframe(
    df,
    bidding_zone=BiddingZone.NO2,
    resource_coding_scheme="NNO",
    product_type=MarketProductType.SCHEDULED_AND_DIRECT,
    # Automatically creates a technical link across consecutive MTUs
    technical_link=True,
)

doc = (
    BidDocument(tso=TSO.STATNETT)
    .sender(party_id="9999909919920", coding_scheme="A10")
    .add_bids(bids)
    .build()
)

Handling activation orders

Not yet implemented. ActivationDocument is planned in documents/activation.py.

from nexa_mfrr_eam import ActivationDocument  # coming soon

# Parse the activation order XML received from your ECP endpoint
order = ActivationDocument.from_xml(activation_xml_bytes)

# Check if it is a heartbeat
if order.is_heartbeat:
    # Respond to heartbeat (required for Statnett and Svenska kraftnat)
    response = order.heartbeat_response(status="activated")
    response_xml = response.to_xml()
    # Send response_xml back via ECP
else:
    # Real activation order
    for ts in order.time_series:
        print(f"Bid {ts.bid_id}: {ts.quantity_mw} MW {ts.direction}")
        print(f"  Period: {ts.start} to {ts.end}")
        print(f"  Type: {order.activation_type}")  # scheduled, direct, period_shift, etc.

    # Build response - confirm all activations
    response = order.respond_all_activated()

    # Or selectively mark some as unavailable
    response = (
        order.build_response()
        .activate(ts.bid_id for ts in order.time_series[:2])
        .unavailable(
            order.time_series[2].bid_id,
            reason="B59",  # Unavailability of reserve providing unit
            text="Generator tripped",
        )
        .build()
    )

    response_xml = response.to_xml()
    # Send via ECP within 2 minutes

Parsing allocation results

Not yet implemented. AllocationResult is planned in documents/allocation_result.py.

from nexa_mfrr_eam import AllocationResult  # coming soon

result = AllocationResult.from_xml(allocation_xml_bytes)

for ts in result.time_series:
    print(f"Bid {ts.bid_id}")
    print(f"  Activated: {ts.quantity_mw} MW at {ts.price_eur} EUR/MWh")
    print(f"  Direction: {ts.direction}")
    print(f"  Reason: {ts.activation_reasons}")  # e.g. ["B49", "Z58"] = Balancing + Scheduled

MARI mode toggle

Product characteristics change when connecting to MARI. Toggle this globally or per-validation:

from nexa_mfrr_eam import MARIMode, configure

# Set globally
configure(mari_mode=MARIMode.POST_MARI)

# Or per-validation
errors = doc.validate(mari_mode=MARIMode.POST_MARI)

# Key differences:
# - BSP GCT moves from QH-45 to QH-25
# - Max price moves from 10,000 to 15,000 EUR/MWh (later 99,999)
# - Timing changes for TSO GCT, AOF run, etc.

Timing and DX helpers

Gate closure calculator

from nexa_mfrr_eam.timing import gate_closure, MARIMode

# When do I need to submit bids for a given MTU?
mtu_start = datetime(2026, 3, 21, 10, 0, tzinfo=timezone.utc)

gc = gate_closure(mtu_start, mari_mode=MARIMode.PRE_MARI)
print(f"BSP GCT (BEGCT): {gc.bsp_gct}")        # 09:15:00 UTC (QH-45)
print(f"TSO GCT: {gc.tso_gct}")                  # 09:45:00 UTC (QH-15)
print(f"Activation orders at: {gc.activation}")   # 09:52:30 UTC (QH-7.5)
print(f"Is gate open now? {gc.is_gate_open()}")

gc_mari = gate_closure(mtu_start, mari_mode=MARIMode.POST_MARI)
print(f"BSP GCT (MARI): {gc_mari.bsp_gct}")     # 09:35:00 UTC (QH-25)

MTU boundaries

from nexa_mfrr_eam.timing import current_mtu, mtu_range

now = datetime.now(timezone.utc)
mtu = current_mtu(now)
print(f"Current MTU: {mtu.start} to {mtu.end}")

# Generate MTU start times for a date range
mtus = mtu_range("2026-03-21T00:00Z", "2026-03-22T00:00Z")
print(f"MTUs in day: {len(mtus)}")  # 96

# DST-aware: handles 23-hour and 25-hour days
mtus_short = mtu_range("2026-03-29T00:00Z", "2026-03-30T00:00Z", tz="CET")
print(f"MTUs on DST transition day: {len(mtus_short)}")  # 92

Conditional link availability evaluator

After the AOF runs, determine if your conditional bids are available:

from nexa_mfrr_eam.timing import evaluate_conditional_availability

# Your bid in QH0 is conditionally available (A65) with link:
# "Not available if linked bid in QH-1 was activated" (A55)
is_available = evaluate_conditional_availability(
    bid_status="A65",  # conditionally available
    links=[
        {"linked_bid_activated": True, "condition": "A55"},
    ],
)
print(f"Bid available: {is_available}")  # False

Bid document size checker

The max is 4000 time series per message (2000 for Fingrid) and 100 messages per MTU:

from nexa_mfrr_eam import BidDocument

doc = (
    BidDocument(tso=TSO.STATNETT)
    .sender(party_id="9999909919920", coding_scheme="A10")
    .add_bids(my_large_bid_list)
    .build()
)

print(f"Bids in document: {doc.time_series_count}")
errors = doc.validate(mari_mode=MARIMode.PRE_MARI)
# errors will include a message if the TSO limit is exceeded

Note: Auto-splitting into multiple documents (.split()) is not yet implemented. For now, partition your bid list manually before calling .add_bids().

Schema notes

The library targets the NBM (Nordic Balancing Model) variant of the IEC 62325-451-7 ReserveBid schema. There are some important details to be aware of:

Schema versions and namespaces: The vendored XSD uses namespace urn:iec62325:ediel:nbm:reservebiddocument:7:2 while the Statnett example XML uses namespace urn:iec62325.351:tc57wg16:451-7:reservebiddocument:7:2. The implementation guide references schema version 7.4. The library handles all known namespace variants during parsing and uses the NBM namespace for serialization by default (configurable per TSO).

Element naming: The XSD uses quantity_Measure_Unit.name and energyPrice_Measure_Unit.name (without "ment" suffix). The implementation guide text inconsistently uses quantity_Measurement_Unit.name. The library follows the XSD and example XML.

Status element structure: In the XML, the status element contains a nested <value> element, not a flat string. For example: <status><value>A06</value></status>.

Resource coding schemes: The registeredResource.mRID element requires a codingScheme attribute. Known schemes: A01 (EIC), NNO (Norwegian national / NOKG codes), NSE (Swedish national). The Statnett example uses NNO.

Sender/receiver coding schemes: Supported schemes per the implementation guide section 6.6: A01 (EIC), A10 (GS1), NSE (Swedish national). The Statnett example uses A10 (GS1) for the sender.

Denmark-specific schema: The implementation guide notes that Denmark "currently uses a specific version of the schema." The fields mktPSRType.psrType (mandatory for DK) and Note (optional, DK only) are not present in the standard NBM XSD. Request the Denmark-specific schema from Energinet.

Optional fields in XSD not discussed in the implementation guide: The XSD includes several optional elements: blockBid, priority, stepIncrementQuantity, validity_Period.timeInterval, original_MarketProduct.marketProductType, provider_MarketParticipant.mRID, price_Measure_Unit.name, ProcuredFor_MarketParticipant, SharedWith_MarketParticipant, ExchangedWith_MarketParticipant, AvailableBiddingZone_Domain, and marketAgreement.*. These may be relevant for future MARI integration or capacity market cross-references.

XSD cardinality vs implementation guide: The XSD allows multiple Period elements per BidTimeSeries and multiple Point elements per Series_Period. The implementation guide restricts both to exactly one. The library enforces the implementation guide restriction by default.

TSO-specific feature matrix

Feature Statnett (NO) Fingrid (FI) Energinet (DK) Svenska kraftnat (SE)
Min bid volume 10 MW (5-9 MW exception) 1 MW 1 MW 1 MW
Exclusive groups Yes Yes Yes Yes
Inclusive groups Yes Yes No No
Multipart bids Yes Yes Yes Yes
Technical linking Yes Yes Yes Yes
Conditional linking Yes (incl. Z04 for period shift) Yes (incl. inclusive bids) Yes (no A71/A72) Yes
Max duration Yes No Yes Yes
Resting time Yes No Yes Yes
Period shift Yes No No No
Faster activation Yes No No No
Slower activation No No Yes No
Non-standard (mFRR-D) Yes No No No
Non-standard (other) Yes No No Yes (overbelastning)
Heartbeat Yes (T-12, T-7.5, T-3) No No Yes (every 5 min)
Direct activation Yes Yes Local model (separate bids) Yes
Change product type A05 <-> A07 A05 <-> A07 No A05 <-> A07
Cut-off time for msgs 15 min None specified None specified 6 min
Locational info Yes (NOKG/NOG) Yes (resource) Yes (substations) Yes
Voluntary bid ID No Yes (Reason A95) No No
Sender coding scheme A01, A10 A01, A10 A01, A10 A01, A10, NSE
Resource coding scheme NNO A01 A01 A01, NSE
Fallback portal FiftyWeb Vaksi Web BRP Self Service Portal FiftyWeb

ECP/EDX setup

This library generates the CIM XML documents. To actually send them, you need an ECP/EDX endpoint deployed in the Nordic Energy Messaging (NEM) network.

This is infrastructure you must set up with your TSO. The process is:

  1. Register as a BSP with your connecting TSO (Statnett, Fingrid, Energinet, or Svenska kraftnat)
  2. Request ECP/EDX documentation from your TSO
  3. Deploy an ECP endpoint - ENTSO-E software, available as Docker images from the ENTSO-E Docker Hub
  4. Register your endpoint with your TSO's Central Directory (CD)
  5. Configure message paths - download from ediel.org for your TSO:
  6. Test connectivity in NEM-TEST/PREPROD before going to NEM-PROD

ECP supports multiple integration channels: AMQP(S), MADES Web Service, and FSSF (File System Shared Folder). EDX adds SFTP, SCP, and additional web service options. Third-party managed service providers (e.g. Tietoevry's BIX EIX) can operate the endpoint on your behalf.

The NEX (Nordic ECP/EDX Group) installation guide is available at ediel.org.

Integration pattern

# Pseudocode - your actual integration depends on your ECP setup
from nexa_mfrr_eam import BidDocument, TSO

doc = BidDocument(tso=TSO.STATNETT).sender(party_id="...", coding_scheme="A10").add_bid(bid).build()
xml_bytes = doc.to_xml()

# Option 1: Write to ECP's FSSF inbox folder
Path("/ecp/outbox/").write_bytes(xml_bytes)

# Option 2: Post via AMQP to your local ECP broker
channel.basic_publish(exchange="", routing_key="ecp.outbox", body=xml_bytes)

# Option 3: Use MADES web service
# POST to your ECP endpoint's MADES interface

Bidding zone EIC codes

The library maps these automatically, but for reference:

Zone EIC Code Country
NO1 10YNO-1--------2 Norway
NO2 10YNO-2--------T Norway
NO3 10YNO-3--------J Norway
NO4 10YNO-4--------9 Norway
NO5 10Y1001A1001A48H Norway
SE1 10Y1001A1001A44P Sweden
SE2 10Y1001A1001A45N Sweden
SE3 10Y1001A1001A46L Sweden
SE4 10Y1001A1001A47J Sweden
DK1 10YDK-1--------W Denmark
DK2 10YDK-2--------M Denmark
FI 10YFI-1--------U Finland

Control area EIC codes (used in document-level domain.mRID):

TSO Control Area EIC
Energinet 10Y1001A1001A796
Fingrid 10YFI-1--------U
Statnett 10YNO-0--------C
Svenska kraftnat 10YSE-1--------K

Nordic Market Area (used in bid-level acquiring_Domain.mRID): 10Y1001A1001A91G

TSO receiver EIC codes (used in receiver_MarketParticipant.mRID):

TSO Receiver EIC
Statnett 10X1001A1001A38Y
Svenska kraftnat 10X1001A1001A418

(Fingrid and Energinet receiver EICs should be confirmed with each TSO during onboarding.)

What you need that this library cannot provide

The following information is not publicly documented in the mFRR EAM implementation guide and must be obtained directly from your connecting TSO:

  1. ECP/EDX endpoint setup credentials and certificates - your TSO issues registration keystores and passwords
  2. ECP endpoint URLs / broker addresses - provided per TSO during onboarding, not published
  3. Test environment access - NEM-TEST/PREPROD endpoints, separate from production
  4. Your BSP party ID - either an EIC code (A01) or GS1 number (A10), depending on TSO
  5. Resource object codes - Statnett uses NNO coding scheme (NOKG/NOG codes); other TSOs use EIC or national identifiers
  6. Denmark's specific ReserveBid schema version - the implementation guide notes Denmark uses a specific version; request from Energinet
  7. NMEG additional code lists - referenced in the implementation guide but not included; request from your TSO
  8. Service codes for mFRR EAM on ECP - the addressing convention (e.g. SERVICE-<code>) for mFRR EAM specifically
  9. National Terms and Conditions - each TSO publishes specific T&Cs that BSPs must comply with
  10. Fingrid IP whitelisting requirements - Fingrid only allows Nordic IPs plus Azure West Europe; contact them if your endpoint is elsewhere
  11. TSO receiver EIC codes - the Statnett example uses 10X1001A1001A38Y; confirm the others with each TSO

Example XML messages are referenced as available at nordicbalancingmodel.net but are separate downloads.

Project structure

nexa-mfrr-nordic-eam/
  examples/
    statnett_bid_preparation.ipynb  # Statnett daily bid prep with GS tax
    svk_linked_bids.ipynb           # SVK technically + conditionally linked bids
    data/                           # Sample asset definitions and DA prices
  src/nexa_mfrr_eam/
    __init__.py              # Public API re-exports
    types.py                 # Enums, Pydantic models for all bid types
    config.py                # MARI mode, TSO configuration
    exceptions.py            # Typed exceptions
    bids/
      __init__.py
      simple.py              # Simple bid builder
      complex.py             # Exclusive, inclusive, multipart group builders
      linked.py              # Technical and conditional link builders
      validation.py          # Common + TSO-specific validation rules
    documents/
      __init__.py
      reserve_bid.py         # ReserveBid_MarketDocument builder + serializer
      activation.py          # Activation_MarketDocument parser + response builder
      bid_availability.py    # BidAvailability_MarketDocument parser
      allocation_result.py   # ReserveAllocationResult_MarketDocument parser
      acknowledgement.py     # Acknowledgement_MarketDocument parser
    xml/
      __init__.py
      namespaces.py          # Namespace URI handling (NBM, IEC, per-TSO variants)
      serialize.py           # Pydantic models -> CIM XML
      deserialize.py         # CIM XML -> Pydantic models
      schemas/               # XSD files for validation (vendored)
    tso/
      __init__.py
      base.py                # Base TSO configuration and rules
      energinet.py           # DK-specific rules, DA model, schema version
      fingrid.py             # FI-specific rules, voluntary bid ID
      statnett.py            # NO-specific rules, period shift, mFRR-D, heartbeat
      svk.py                 # SE-specific rules, overbelastning, heartbeat
    timing.py                # MTU calc, gate closures, DST, MARI timing
    heartbeat.py             # Heartbeat detection + response generation
    pandas.py                # DataFrame -> Bid conversion utilities
  tests/
    conftest.py
    fixtures/                # Example XML files
    test_bids.py
    test_config.py
    test_documents.py
    test_linked.py
    test_timing.py
    test_xml_serialize.py

Contributing

See CONTRIBUTING.md. Issues and PRs welcome.

This project follows trunk-based development with a protected main branch and squash-only merges.

License

MIT

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nexa_mfrr_nordic_eam-0.3.0b1.tar.gz (39.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nexa_mfrr_nordic_eam-0.3.0b1-py3-none-any.whl (39.1 kB view details)

Uploaded Python 3

File details

Details for the file nexa_mfrr_nordic_eam-0.3.0b1.tar.gz.

File metadata

  • Download URL: nexa_mfrr_nordic_eam-0.3.0b1.tar.gz
  • Upload date:
  • Size: 39.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for nexa_mfrr_nordic_eam-0.3.0b1.tar.gz
Algorithm Hash digest
SHA256 6dfc7ef50644ffbebc0d32e6d40ad119ac4f481dcd9c3b2aa04d284642f16358
MD5 8d68ea0999e19c20663ddf81bd6486ba
BLAKE2b-256 20fefc6dec48dd76d787f0c43efbc7c3e0555fcba491256e7e60ce9af227d529

See more details on using hashes here.

Provenance

The following attestation bundles were made for nexa_mfrr_nordic_eam-0.3.0b1.tar.gz:

Publisher: publish.yml on phasenexa/nexa-mfrr-nordic-eam

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nexa_mfrr_nordic_eam-0.3.0b1-py3-none-any.whl.

File metadata

File hashes

Hashes for nexa_mfrr_nordic_eam-0.3.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 4f870c9628a0f5d866324bb39047cf249597914d106b4902d9544c8741d68168
MD5 7583672f4e189aa87f8621fafe3960ff
BLAKE2b-256 4dc63f06bf50a9816d249357acccb318c50399c62ad21e1663c252c8c8b8509f

See more details on using hashes here.

Provenance

The following attestation bundles were made for nexa_mfrr_nordic_eam-0.3.0b1-py3-none-any.whl:

Publisher: publish.yml on phasenexa/nexa-mfrr-nordic-eam

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page