Closing the Day: Settlement (pm-eod)¶
Objective¶
Close a trading day the way an exchange operator does, using pm-eod:
- settle every trader's result for the day
- produce the statements and the payment instructions
- check the books add up to zero
- recover from the mistakes that happen in practice: a missed day, a broken roster file, and trading after the close
Background reading
- Closing the Day: Settlement, Statements and Housekeeping
— the concepts (equity, settlement, the central pool, zero-sum) and every
message
pm-eodcan print - Your End-of-Day Statement — the statement as a trader reads it
Prerequisites¶
- Chapters 01–19 completed. Chapter 12 (
pm-clearing) matters most here: settlement reads whatpm-clearingrecorded. - The engine running with
TRADER01,TRADER02,GW_ADMINand the market makers from chapter 02 connected, andpm-clearingrunning. If chapter 19 leftTRADER02kicked, start a new console for it, and re-send aQUOTEfromMM_AAPL_01if its quote was cancelled: Exercise 5 needs liquidity on AAPL. - No
pm-schedulerrunning. You move the session phases yourself, as in earlier chapters. A running--dailyscheduler would close the day at its own time. pm-eodon yourPATH. Trypm-eod --version; if it is missing in a source checkout, runpoetry installonce.
Background¶
At the end of each trading day, pm-eod run does five things:
- Checks that the day has really closed and that the clearing books agree.
- Works out each trader's result for the day: the change in their equity (realized + unrealized P&L) since the close of the previous settled day.
- Adds that amount to the trader's running balance in
eod.db. - Writes one statement per trader, a summary and a payments file under
$EDUMATCHER_DATA_DIR/settlement/<date>/. - Tidies up: compresses old logs, takes backups, and archives any audit
days
pm-auditmissed.
All amounts together always add up to exactly zero, because every trade has a buyer and a seller.
On a production exchange, pm-scheduler --on-close "pm-eod run --pending" runs
this by itself 30 seconds after every close. In this chapter you run every step
by hand, so that you see each one.
The trading date
pm-eod labels each day with its trading date: the calendar date in the
exchange's timezone (locale.session_timezone, default Europe/Stockholm).
Set it once in the shell you use for this chapter:
Exercise 1: Check the starting point¶
Confirm that pm-clearing is healthy and the market is open:
If the session is not CONTINUOUS, open it as in chapter 01 (SESSION|STATE=PRE_OPEN,
then SESSION|STATE=CONTINUOUS).
Why the market must be open before Exercise 2
If you left the market CLOSED at the end of an earlier chapter today,
today counts as a closed trading day. The next step would then settle
today before you have traded in this chapter. Open the market first.
Checkpoint: pm-clearing-cli health reports
health: OK and the session is CONTINUOUS.
Exercise 2: Clear the backlog¶
The earlier chapters closed trading days while pm-clearing was running, so
those days are probably still waiting to be settled. Look:
The table of runs is empty the first time. If any days are waiting, a line on the error stream lists them:
Settle them, oldest first:
[output]
2026-11-04:
[ok] gate: closed, snapshot present, reconciled
[ok] settle: settled
[ok] report: wrote .../settlement/2026-11-04 (no roster: statements are keyed by gateway id)
[ok] audit: nothing pending
[ok] logs: no log directory
[ok] backup: backed up clearing.db, eod.db (…); not present: stats.db
Each line is one step: gate, settle and report are the money, and audit,
logs and backup are the housekeeping. Your dates, sizes and
not present: list will differ. nothing pending instead of the block above
means there was no backlog.
If the backlog run stops on an old day
--pending stops at the first day that fails and says
stopping: later dates wait for <date>, because days are settled in order.
Look the [FAIL] message up in the operator chapter's
Troubleshooting
table. On a training machine, an old day whose clearing data is damaged
can be cleared away with pm-opctl-cli clear --state (with everything
stopped). That deletes all working state (clearing history, GTC orders,
statistics) but keeps eod.db, so the balances settled so far survive.
See Starting over for the full reset
matrix and the --all data-loss warning.
Checkpoint: pm-eod run --pending ended
without a [FAIL] line, and a second pm-eod run --pending prints
nothing pending.
Exercise 3: Ask too early¶
Try to settle today while the market is still open:
The gate is the first step, and it protects the money. A day is closed only
when its latest session phase is CLOSED and the market was open earlier that
day. --dry-run runs the gate and the calculation and writes nothing, so it is
always safe to try.
Checkpoint: the gate refuses today, and
echo $? right after the command prints 1.
Exercise 4: Write the roster¶
pm-eod knows traders only by gateway id. The roster connects ids to
people. Create $EDUMATCHER_DATA_DIR/ref_data/roster.yaml:
traders:
- gateway: TRADER01
name: Alice
email: alice@example.org
opening_balance: 1000.00
- gateway: TRADER02
name: Bob
email: bob@example.org
opening_balance: 1000.00
Leave the market makers out on purpose: you will see what pm-eod does with a
gateway that is not in the roster.
Checkpoint: the file exists, and you can
say what opening_balance does: it is the starting point of the trader's
running balance, added whenever a balance is shown.
Exercise 5: Trade a day¶
Give both traders something to settle. TRADER01 buys and keeps a position; TRADER02 buys and sells part of it again:
[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=MARKET|QTY=200
[TRADER02]> NEW|SYM=AAPL|SIDE=BUY|TYPE=MARKET|QTY=100
[TRADER02]> NEW|SYM=AAPL|SIDE=SELL|TYPE=MARKET|QTY=50
Look at their positions and P&L before the close:
Predict, before going on: whose day result will be positive and whose negative? (Market orders pay the spread to the market makers, so a trader who bought and sold at once is usually a little down.)
Checkpoint: both traders show filled trades
in pm-clearing-cli trades --date "$D", and you have written down a prediction.
Exercise 6: Close the day and settle it¶
1. Close the session from the operator console:
2. See the close snapshot. At the close pm-clearing saves every position
at the closing price. This is what is settled:
[output]
trade_date | gateway_id | symbol | net_qty | mark_price | tick_decimals | realized_pnl | unrealized_pnl
-----------+------------+--------+---------+------------+---------------+--------------+---------------
2026-11-05 | TRADER01 | AAPL | 200 | 149 | 2 | -106.667 | -213.333
3. Dry run. Now the gate passes, and you see the amounts before anything is written:
[output]
2026-11-05:
[ok] gate: closed, snapshot present, reconciled
[ok] settle: would settle: MM_AAPL_01 +275.00, TRADER01 -320.00, TRADER02 +45.00
4. Check one number by hand. A trader's amount is the sum over all their
symbols of realized_pnl + unrealized_pnl in today's snapshot, minus the same
sum at the close of the previous settled day (pm-eod status lists the
settled days; with none, it is 0). In the example above, TRADER01 has
−106.667 + (−213.333) = −320.00 today. Repeat this for TRADER01 with your own
numbers, using pm-clearing-cli snapshots --gateway TRADER01 --date <that day>
for the earlier day.
5. Settle:
A plain --pending is enough: today is now the only closed day that is
waiting.
6. Check the books:
--check should print OK - every settled date sums to zero. Add up today's
amounts from the dry run yourself: they also come to 0.00.
Checkpoint: your hand calculation matches
TRADER01's amount, the run had no [FAIL] line, and --check says OK. Was your
prediction from Exercise 5 right?
Exercise 7: Read what the traders receive¶
ls "$EDUMATCHER_DATA_DIR/settlement/$D" "$EDUMATCHER_DATA_DIR/settlement/$D/statements"
cat "$EDUMATCHER_DATA_DIR/settlement/$D/statements/TRADER01.md"
cat "$EDUMATCHER_DATA_DIR/settlement/$D/instructions.csv"
cat "$EDUMATCHER_DATA_DIR/settlement/$D/leaderboard.csv"
cat "$EDUMATCHER_DATA_DIR/settlement/$D/summary.md"
Answer from the files:
- What is TRADER01's headline sentence, and does its amount match the dry run?
- Does TRADER01's Open positions at the close agree with
POSin TRADER01's console? - In
instructions.csv, which traderspayand whichreceive? Whose balance includes the 1000.00 opening balance? - How does the market maker's statement differ from TRADER01's, and which
warning names it? (Look at the end of
summary.md.)
Checkpoint: you can answer all four from the files alone.
Exercise 8: Run it again¶
Settlement must never count a day twice. Run today again:
The settle line now says already settled. The report and housekeeping run
again: the statements are written afresh, and the amounts are the stored ones,
not new ones. pm-eod status shows today with status ok.
Checkpoint: pm-eod balances shows the same
numbers as after Exercise 6.
Exercise 9: A broken roster¶
Introduce a typical mistake: change TRADER01's opening balance to
1000.005 (three decimals) and run today again:
[output]
2026-11-05:
[ok] gate: closed, snapshot present, reconciled
[ok] settle: already settled
[FAIL] report: .../ref_data/roster.yaml: line 2: opening_balance has more than 2 decimals
...
The error names the line. The settlement is safe in the ledger; only the statements could not be written. Now look at the day as a monitoring system would:
Today's status is report-failed, and status -q (the health check) returns
1. Fix the file back to 1000.00 and run the day again, without
--only:
Why not --only report?
pm-eod run --date "$D" --only report would rewrite the statements too, but
a run with --only is never recorded, so the day would stay
report-failed and the health check red. A full run records ok.
Checkpoint: the health check went from exit
code 1 back to 0, and today's status is ok again.
Exercise 10: Trading after the close¶
Sometimes trading resumes on a date that is already settled, for example when a class runs over. Reopen the market and trade. Buy rather than sell here: Exercise 5 ended with a sell at the market makers' bid, so selling again at that same price would not move the closing price and the new amounts would equal the old ones. A buy trades at the offer, which moves it:
[GW_ADMIN|ADMIN]> SESSION|STATE=PRE_OPEN
[GW_ADMIN|ADMIN]> SESSION|STATE=CONTINUOUS
[MM_AAPL_01]> QUOTE|SYM=AAPL|BID=149.95|ASK=150.05|BID_QTY=500|ASK_QTY=500|TIF=DAY|QUOTE_ID=AAPL-LATE-001
[TRADER02]> NEW|SYM=AAPL|SIDE=BUY|TYPE=MARKET|QTY=50
[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSED
Now ask pm-eod to catch up:
It says nothing pending. There is one settlement per trading date, and
today's is already done. The afternoon's trading is in the new close snapshot,
but not in the ledger. See the difference:
The dry run computes today again from the new snapshot and shows different
amounts. To replace today's settlement with them, use --redo:
[output]
2026-11-05:
[ok] gate: closed, snapshot present, reconciled
[ok] settle: redone, dropped 2026-11-05
...
--redo drops every later day too
Balances are a running total, so --redo deletes the chosen date's
settlement and every later date's and recomputes the chosen one. The
later dates are settled again by the next pm-eod run --pending. On a real
exchange, always do a --dry-run first.
Checkpoint: after --redo the balances
include the afternoon's trade, --check still says OK, and you can explain why
--pending did not pick the afternoon up by itself.
Summary¶
You can now:
- Settle a trading day with
pm-eod, and know what the gate checks before any money is written. - Check a trader's amount by hand from the close snapshots, and check that the day sums to zero.
- Read the statement,
instructions.csv, the leaderboard and the summary. - Clear a backlog with
--pending, recover from a broken roster without leaving the dayreport-failed, and recompute a day with--redo.
Reflection¶
- Settlement pays out paper profits every day, not only profits locked in by selling. Why is that safer for an exchange than waiting until each position is closed? (The operator chapter's Why settle paper profits at all? box is a hint.)
- The engine announces
CLOSEDwhen it starts. Why mustpm-eodnot treat that as the day's close? What would a trader's statement say if the exchange were started in the morning andpm-eodsettled the day at once? pm-eodrefuses to settle a day while an earlier closed day is still waiting. What would go wrong with the balances if it allowed it?
Further Reading¶
- Closing the Day — the scheduler hook, running without a scheduler, backups and restore, troubleshooting
- Your End-of-Day Statement
pm-eodreference- Positions and P&L