Connecting to EPEX SPOT M7: the work nobody quotes you for
"Connect to M7" tends to show up as one line on the plan. A couple of sprints, slotted in next to the strategy work that is the actual reason for the project. The exchange publishes an API, you write a client against it, orders go out and fills come back. On paper it's a small part of the whole thing.
It's also usually where the estimate goes wrong. What follows is roughly everything that line turns into once you start building, so you can weigh building against buying with the real scope in view instead of the one on the slide.
The connection is the easy part
M7 isn't one interface. Exchange management and trade fills go over AMQP as XML, while live market data, deltas, and order execution arrive over WebSocket as Protobuf. Two protocols, two client libraries, and a single identity spanning both.
The part that catches people is that the two protocols aren't independent. The market-data feed can't authenticate on its own. The exchange hands you its credential in the reply to a successful order-session login. So market data cannot flow until order management is up, and a startup you might reasonably have drawn as two parallel connections is actually a chain. Draw it wrong and you get a client that waits forever on a step it thinks is independent.
The exchange can also end the session on its own terms. A logout you never asked for arrives as an ordinary message on the channel, and a client that treats it as a network blip will reconnect straight into a state nobody has reasoned about. The safe response is to stop, raise an alert, and come back up cleanly under supervision. You have to build that path before the day you need it.
This is the most predictable and easiest part of the build.
The order book
On connect, M7 sends a snapshot and then a stream of updates. Every message carries a sequence number, one higher than the last for its delivery area and product. If one goes missing or arrives twice, your local book has drifted from what the exchange sees. Nothing warns you. You notice later, the first time you quote at a price that isn't actually there.
You can't patch a gap. There's no replay to request and no way to fill the hole, so the only honest recovery is to drop the stream, flag the feed as out of sync, and rebuild from a fresh snapshot. A gap in one product tears down the feed for all of them. That sounds heavy-handed until you consider the alternative, which is trading on a book you can't vouch for.
Liveness is the part people underestimate. If you decide the feed is healthy because updates keep arriving, a dead connection looks the same as a market that has simply gone quiet. You'll either stop trading when nothing is wrong or keep trading on a feed that died twenty minutes ago. Feed health has to come off multiple independent heartbeats, tracked separately from whether the market is doing anything. The failures we've seen all landed on slow days, not busy ones, and that makes sense: on a slow day you can't tell a quiet market from a dead feed.
And the book is only as good as your knowledge of what's in it. Contract definitions come from a request that covers a limited time window. The book carries contracts beyond that window, so you receive orders on contracts you don't yet have a definition for: no product, no duration, no delivery period. Dropping them hides real liquidity. Showing them unlabelled confuses traders. Fetching their definitions on demand is one more asynchronous dependency sitting inside what looked like a simple feed.
Prices go negative, and that breaks more than you'd think
Power is one of the few markets where the price is routinely below zero. When there's more wind and sun on the system than demand, being paid to consume is normal, and any code that assumes a price is positive has bugs waiting in it.
The obvious ones are easy. The subtle one is arithmetic. If you mark positions off the midpoint of the book and do it in integers, as you should, rounding truncates toward zero. The direction of your rounding error then flips as the mid crosses zero. Every unrealized PnL number you show carries a small systematic bias, one way on ordinary days and the other way on negative-price days. It isn't large. It also never shows up in testing, because nobody writes the test at minus forty euros.
Then there's zero itself. Zero is a real, tradable, perfectly ordinary price in this market, so it can't double as your "no price here" sentinel, which is exactly what it wants to be in every data structure you'll reach for. Whether a book has a usable price has to be answered by asking whether quantity is resting on both sides, never by asking whether the price is non-zero. Get that backwards and your marks disappear during exactly the market conditions your traders most want to see them.
The money is integers, and the order of operations matters
Floating point has no business near a position that settles in cash, so prices and quantities both arrive as scaled integers and everything downstream is fixed-point. That decision is easy. Living with it is less so.
Weighted-average cost is where it bites. Compute the average, then multiply by the quantity you're closing, and the truncation happens before the multiply. The error then scales with the size of the position instead of staying inside one unit. Carry the full numerator in a wider integer and divide last, and it doesn't. The two versions look equally reasonable in a code review and differ by real money on a large close.
Pre-trade risk
The exchange won't enforce your limits for you, so those checks run locally, before each order leaves.
Position can't be a single net number, because net lets a big buy hide behind a resting sell. You track worst-case exposure on each side instead, assuming everything resting fills. You also need a rule for when you're already over a limit, because the naive check rejects everything, including the orders that would reduce your exposure. That is the worst possible moment to lose the ability to trade.
Cash is more involved, because EPEX clears through ECC and their rules are specific. There are two separate limits: one in euros covering every delivery area except Great Britain, and one in pounds for Great Britain alone. Both reset at the 16:00 CET booking cut, but only on ECC business days, which follow the TARGET2 payment calendar rather than the weekday. A Friday window runs through to Monday, and at Easter a single window stretches from Thursday to Tuesday. A buy at a positive price or a sell at a negative price consumes limit. An order that would pay you reserves nothing, so unfilled sell proceeds can't fund new buys. Except in Great Britain, where every sell also carries a delivery-risk add-on per megawatt-hour that ECC revises by circular, so a British sell consumes limit even when it makes money. The exposure is asymmetric by design, differently per currency, and coding it symmetrically is the intuitive mistake.
The exchange does report where you stand, but its figure lags yours and can disagree with it across a rollover. So you need your own ledger as the source of truth, plus a policy for when the two disagree: when it's lag, when it's a real divergence, and what you do about it. Get any of this slightly wrong and your local limit will disagree with what the clearer enforces. You find out at clearing time instead of at submission, which is a bad time to find out.
Self-trade prevention is its own job. M7 will let one of your orders cross another and only flag it afterwards, so avoiding it means checking before you send, including against orders you fired a few milliseconds ago that haven't been acknowledged yet.
Concurrency is where the real work is
A risk check is only correct if nothing changes between the check and the send. With real flow, something always does.
Two orders sent at the same moment can each pass the limit check and breach it together, unless the check and the reservation are one serialized step. That much is well known. The harder cases come from everything else that moves a reservation.
A modify is the worst of them. It changes the order's exposure, so it has to release the old reservation and take the new one in one step. If the exchange refuses the modify, the old reservation has to come back exactly as it was. Meanwhile a fill on the same order can land between your modify going out and the reply coming back, and the modify is then being applied to a quantity that no longer exists. Every combination of modify, fill, cancel and refusal needs a defined result, and the two orderings you didn't think of are the ones that happen in production.
Hibernated orders reactivating, iceberg slices refilling and orders being cancelled by the exchange at gate closure all move exposure without your code having sent anything. Each one is a path into the same ledger, and each one has to take the same lock.
Then there's the order in flight. You reserved cash for it and sent it, and no reply has come. You can't hold that reservation forever, or a single lost reply slowly strangles your trading capacity. You also can't release it on a timer, because the order may be resting at the exchange after all. Somebody has to decide how long a claim lives, what happens when it expires, and what the system does when the late reply finally arrives for a claim it already gave up on. Whichever way you choose, it has to fail closed.
M7 also throttles order entry across two rolling windows and reports where you stand against each of them. Consuming that is simple. Deciding what your system does as it approaches a ceiling is a trading-policy question, not an engineering one, and somebody has to answer it before the code can be written.
Knowing what the exchange actually did
The first response to a submission is a receipt saying the message arrived. That is not the order resting on the book. Count the receipt as confirmation and your state is wrong for a stretch you don't control.
The exchange can also refuse an order after the receipt, and some of those refusals come back without the client order id you'd use to match them. An error you can't attribute is an order you can't account for.
Modifications aren't edits. A modify can be a cancel and a re-create, so one price change produces a small burst of events, possibly out of order, under a new order id that points back to the old one. Revision numbers don't help as much as you'd hope, because a partial fill bumps the revision too. You can't confirm that your command landed by watching the revision change, or by watching the old order disappear. You have to read what the exchange says it did on each report and stitch the history back into one coherent order. That is fiddlier than anything else in the integration.
Reconnecting
Reconnecting the socket is easy. The work is deciding what you can still trust once you're back.
Your open orders come back as a snapshot, which replaces whatever you thought you had instead of reconciling against it. There's no diff and no report of what changed while you were away. Trades are re-requested over a look-back window, and that window is finite. An outage longer than it leaves a hole in your own trade history with nothing to detect it, so somebody has to decide what the system does when it has been gone too long.
While the feed is degraded you refuse new orders but keep letting cancels through, because reducing exposure has to work even when everything else is down. That distinction has to be built deliberately. The natural implementation is one health check gating everything, which locks you out of the one action you most need.
The hard case is narrower. An order goes out, the connection drops before any reply, and now it may or may not be resting at the exchange. No state in anyone's order model honestly represents that, and both convenient answers are wrong. Assume it's live and you double up when you retry. Assume it's dead and you're carrying risk you don't know about. The only way out is the next snapshot from the exchange, and until it arrives you hold a position you cannot describe, with a cash reservation you can neither keep nor release with confidence.
The exchange doesn't know who your traders are
One more thing that surprises desks. M7 authenticates your firm, not the person at the keyboard. If you have more than one trader or more than one strategy, the exchange doesn't know the difference and won't keep track of it for you.
Every bit of attribution you need is yours to build: who sent this, which desk owns this position, what this trader's PnL looks like, who gets to see this order. All of it sits on an identity model that doesn't have the concept. Because it's the basis of your audit trail, it has to survive a round trip through the exchange and come back on the private feed intact, including for orders a colleague placed from another session. For a single-trader shop this is nothing. For anything larger it's a system in its own right, and it's rarely on the plan.
When building it yourself makes sense
There are real reasons to build. Maybe you have engineers who can own it for years as M7 keeps changing under them. Maybe the infrastructure is part of your edge and you'd rather it didn't sit outside the building. Or you run a single delivery area with a strategy simple enough that most of the above doesn't apply. In any of those cases, building is a sound call.
The failure mode is budgeting it as a couple of sprints of connectivity work. What you're actually taking on is a concurrent, stateful system with its own on-call and an upkeep bill that outlives the person who wrote the first version. Price it as the connection layer and you've priced the smallest part of the job.
None of this is the point of your desk. The strategy is, and the M7 layer is part of what it costs to run one: a cost larger than the single line suggests, and one that keeps running well past launch. Whether you build it or hand it to something that already deals with it depends on your team and your constraints. Mostly it helps to go in knowing its real size.
Voltnir is what buying it looks like. Everything in this piece is a problem it already handles.