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's 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. Order management runs over AMQP and market data over WebSocket, so you're on two protocols and two client libraries from the outset, with a single identity spanning both. They don't even share a serialization format, one side is XML, the other protobuf, so you're writing and testing two codecs before a single order moves.
That identity is more than a username and password. There's a mutual-TLS client certificate, your M7 credential, and a one-time code that regenerates on every connect, and all of it has to be right each time the client reconnects, which is more often than you'd expect.
The part that catches people is that the two protocols aren't independent. The market-data feed can't authenticate on its own. Its credential is handed to you by the exchange in the reply to a successful order-session login. So market data cannot flow until order management is up, and a startup sequence you might reasonably have drawn as two parallel connections is actually a chain. Draw it wrong and the failure is a client that sits waiting forever on a step it thinks is independent, which is a much harder thing to diagnose than an error.
The TLS trust store alone can cost you a day. It has to work on your laptop, in CI, and on a locked-down production box where the system certificate bundle is missing or stale, which means layering a bundled root set, whatever the OS has, and an operator-supplied bundle for the private or corporate CAs the OS doesn't know about. And you have to do it twice: the two protocols configure trust through different APIs and want the roots in different forms, so wiring one doesn't wire the other. Miss that and you get market data trusting a corporate CA while order management rejects the same chain as an unknown issuer, on the same box, in the same process. There are also two exchange endpoints to fail between when one drops.
This all comes to about two weeks, and it's the most predictable and easy stretch of the whole 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, and 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.
A gap isn't something you can patch over. 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. That means a gap in one product tears down the feed for all of them, which 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, and 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 a heartbeat, tracked separately from whether the market is doing anything. The failures I've seen all landed on slow days rather than busy ones, which makes sense: a quiet market and a dead feed look identical, and a slow day is when you can't tell them apart.
There's also a modelling decision hiding in here that's easy to make badly. If you store the book as aggregated price levels, you've thrown away the information you need to apply the next update, because updates arrive per order, and a removal is signaled by an update carrying zero quantity rather than by any kind of delete message. Aggregate too early and you spend the rest of the project trying to reconstruct what you discarded.
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 price is a positive number has bugs waiting in it.
The obvious ones are easy. The subtle one is arithmetic. If you're marking positions off the midpoint of the book and doing it in integers, as you should, then rounding truncates toward zero, which means the direction of your rounding error flips sign as the mid crosses zero. Every unrealized PnL number you show carries a small systematic bias, in one direction on ordinary days and the other direction on negative-price days. It's not 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, traceable, 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 there's quantity resting on both sides, never by asking whether the price is non-zero. Get that backwards and your marks quietly disappear during precisely 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 anywhere 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, so the error 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.
On top of that, every contract has a duration. A quarter-hour contract is a quarter of the value of the hourly at the same price per megawatt-hour, and a block is a multiple. That scaling factor has to be threaded through every valuation path you have, and the day someone adds a new product type is the day you find out which paths you missed.
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 what happens when you're already over a limit, because the naive check rejects everything including the orders that would reduce your exposure, which is the worst possible moment to stop being able to trade.
Cash is more involved, because EPEX clears through ECC and their rules are specific about it. EUR and GBP are separate pools with no conversion between them, and they don't even roll over on the same days, because UK bank holidays close the payment system on dates the euro calendar doesn't recognize. The limit resets on the clearing calendar, so a Friday window runs through to Monday. An order that makes money reserves nothing, so unfilled sell proceeds can't fund new buys. The exposure is asymmetric by design, and coding it symmetrically is the intuitive mistake. Get your local version of any of that slightly wrong and it will disagree with what the clearer actually enforces, which you find out at clearing time rather than at submission, and that is a bad time to find out.
Self-trade prevention is its own job. M7 will happily let one of your orders cross another and only flag it after the fact, so avoiding it means checking before you send, including against orders you fired a few milliseconds ago that haven't been acknowledged yet. There's a race condition in here too: two orders sent at once can each pass the limit check and breach it together, unless the check and the write are serialized. You won't see that in a demo, only once there's real flow through the system.
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 not, and it isn't really an engineering question, it's a trading-policy question somebody has to answer before you can write the code.
Reconnecting
Reconnecting the socket itself is easy. The work is in 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 rather than reconciling against it. There's no diff, 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's been gone too long.
While the feed is degraded you refuse new orders but keep letting cancels through, since reducing exposure has to work even when the rest 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 genuinely hard case is narrower and nastier. An order goes out, the connection drops before any reply, and now it may or may not be resting at the exchange. There is no state in anyone's order model that honestly represents that, and both convenient answers are wrong, because if you assume it's live you double up when you retry, and if you assume it's dead 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 have a position you cannot describe.
Two smaller things stay hidden until you're in production. The first response you get to a submission is just a receipt saying the message arrived, which is not the same as the order resting on the book. Count the receipt as a fill and your state is wrong for a stretch you don't control. And modifications aren't edits, a modify is a cancel and a re-create, so a single price change produces a small burst of events that can arrive out of order, and stitching them back into one coherent order history is fiddlier than anything else in the integration.
Contracts expire, but the PnL doesn't
Delivery ends and the contract stops trading. What you do with it then is a question the API doesn't answer for you.
You can't just delete it. A trader who locked in a profit during the last hour before delivery still expects that to show up in their day, and in the product roll-up, after the contract is gone. But you also can't leave it sitting in the book, because a stale contract with a stale last price will keep marking as though it's live and quietly inventing unrealized PnL on something that already settled.
So expiry is a sequence rather than an event: stop trading it, then later clear the book and the marks while keeping enough of the contract to attribute the realized money correctly. Neither half is difficult. Knowing you needed both is the part that comes from having run it through a delivery day.
The exchange doesn't know who your traders are
One more thing that surprises desks. M7 authenticates your firm, not the individual sitting at the keyboard. If you have more than one trader, or more than one strategy, the exchange has no concept of that distinction and will not maintain it for you.
Which means every bit of attribution you need is yours to build: who sent this, which desk owns this position, what this trader's PnL actually looks like, who gets to see this order. All of it sits on top of an identity model that doesn't have the concept. And since it's the basis of your audit trail, it has to survive round-tripping through the exchange and coming back on the private feed intact. 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 it. Maybe you have engineers who can own the thing 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 and a strategy simple enough that most of the above just doesn't apply to you. Any of those, and 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 system with its own on-call and an upkeep bill that outlives the person who wrote the first version of it. Price that as the connection layer and you've priced the smallest part of the job.
None of this is the point of your desk, of course. The strategy is, and the M7 layer is just part of what it costs to run one, a cost that's larger than the single line suggests and that keeps running well past launch. Whether you build it or hand it to something that already deals with it comes down to your team and your constraints. Mostly it just helps to go in knowing the real size of it.
Voltnir is what buying it looks like. Everything in this piece is a problem it already handles.