Skip to content

Time-in-Force & Sessions

Objective

Understand how Time-in-Force (TIF) controls order lifetime across session phases, and how to use the scheduler to drive the exchange through a full trading day.

Background reading

Prerequisites

  • Chapters 01–05 completed.
  • pm-engine, at least one trader gateway and the operator console running. Exercise 1 runs pm-scheduler once to show the phases; after that the chapter controls them by hand (see the Freeze/Advance Procedure below).

Background

Session phases progress as:

PRE_OPEN → OPENING_AUCTION → CONTINUOUS → CLOSING_AUCTION → CLOSED

Different TIF values control when orders are active and when they expire.

TIF Meaning Valid During
DAY Lives until the end of the trading day (expires at CLOSED) PRE_OPEN, OPENING_AUCTION, CONTINUOUS, CLOSING_AUCTION
GTC Good-Till-Cancelled; survives session boundaries Any
ATO At-The-Open only; participates in opening auction OPENING_AUCTION
ATC At-The-Close only; participates in closing auction CLOSING_AUCTION

A DAY order's \"end of session\" is not the same as \"the engine process exits\"

The table above describes the session-scheduler's rule: pm-scheduler driving the session into CLOSED cancels every resting DAY order at that moment, published as order.expired. That is unchanged.

Separately, an engine restart is not itself a day boundary. If you stop and restart pm-engine mid-session (a config reload, a crash recovery) while still on the same business day, a resting DAY order now survives that restart, exactly like a GTC order would — it is only discarded, at the next startup, once its own timestamp is from a prior business day. See Persistence for the full restart-vs-session-close distinction, and chapter 16 — Persistence & Recovery for a hands-on exercise.

Exercise 1: Observe Session Phases

pm-scheduler --now replays the whole day, and the engine only accepts the transitions in order, so it can show you every phase only when it starts from CLOSED. If SESSION_STATUS in the operator console says anything else (after chapter 05 it says CONTINUOUS), close the day by hand first:

[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSING_AUCTION
[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSED

Entering CLOSED expires every resting DAY order, which includes the market makers' quotes from chapter 02, so the books are empty afterwards. That is fine for the rest of this chapter and for chapter 07, which build their own orders. Now start the scheduler in rapid-fire mode to see all transitions quickly:

pm-scheduler --now --delay 5

Watch the transitions (--delay 5 leaves five seconds between them, long enough to query the phase after each one; the whole day takes about 25 seconds). The trader console does not subscribe to session broadcasts — it only answers an explicit SESSION query — so use the operator console, which does:

[GW_ADMIN|ADMIN]> SESSION_STATUS

Run it after each transition, or watch the audit trail in another terminal for the session.state events. The audit logger prints in batches (at least every 10 seconds), so a line can appear a few seconds after the transition. It also prints order-book checkpoints at every phase change, so filter for the lines you want:

pm-audit --terminal | grep '\[session.state\]'
[2026-10-31T11:40:59.5+00:00] [session.state] [seq=3 eseq=40 msg=… cause=… chain=…] {"state": "OPENING_AUCTION", "prev_state": "PRE_OPEN"}
[2026-10-31T11:41:01.5+00:00] [session.state] [seq=4 eseq=52 msg=… cause=… chain=…] {"state": "CONTINUOUS", "prev_state": "OPENING_AUCTION"}

Checkpoint: you see at least one session state transition.

Freeze/Advance Procedure (Deterministic Session Control)

The exercises below need the exchange sitting in a specific, known phase before you place an order — otherwise a TIF exercise may pass or fail purely by timing luck. Instead of running pm-scheduler continuously, use the admin gateway to freeze on a phase and advance only when you are ready:

  1. Do not start pm-scheduler --now (which free-runs through all phases). Start the engine and gateways as usual, but leave the scheduler stopped. With sessions_enabled: true and no scheduler, the engine starts in CLOSED and stays there — so every order is rejected with "Market is closed" until you move it yourself in step 2. That is expected, not a fault.
  2. From the admin gateway, move to the exact phase you need for the next exercise. The engine only accepts the transitions in the session sequence, and from CLOSED the only legal move is to PRE_OPEN — so to reach CONTINUOUS go in two steps:
[GW_ADMIN|ADMIN]> SESSION|STATE=PRE_OPEN
[GW_ADMIN|ADMIN]> SESSION|STATE=CONTINUOUS

An illegal jump (for example CLOSED straight to CONTINUOUS) is silently ignored by the engine, and the console just prints TIMEOUT with no reason. If you see that, run SESSION_STATUS and check which phase you are really in.

  1. Confirm the change took effect with:
[GW_ADMIN|ADMIN]> SESSION_STATUS
  1. Run the TIF exercise's orders while frozen in that phase.
  2. When the exercise calls for a transition (e.g., DAY expiry at CLOSED, or ATO/ATC at auction phases), advance deliberately:
[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSING_AUCTION
[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSED

Using SESSION|STATE= this way means each exercise below runs in a reproducible phase instead of racing a free-running scheduler.

Exercise 2: DAY Order Expiry

During CONTINUOUS (freeze there per the procedure above: the scheduler's day in Exercise 1 ended in CLOSED, so send SESSION|STATE=PRE_OPEN and then SESSION|STATE=CONTINUOUS), place a DAY order:

[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=148.00|TIF=DAY

Advance the session to CLOSED. The engine also accepts CONTINUOUS → CLOSED directly, but this exercise goes through the closing auction, the way a real day ends:

[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSING_AUCTION
[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSED

Watch the trader console right after the second transition: the order is reported EXPIRED.

Operational note: DAY is best for intraday intent where stale overnight orders must not persist.

Checkpoint: DAY order expired at session close.

Exercise 3: GTC Order Survives

Exercise 2 left the market CLOSED, where every order is rejected. Open a new day first (SESSION|STATE=PRE_OPEN, then SESSION|STATE=CONTINUOUS) and place a GTC order:

[TRADER01]> NEW|SYM=MSFT|SIDE=BUY|TYPE=LIMIT|QTY=50|PRICE=415.00|TIF=GTC

Close the day again (SESSION|STATE=CLOSING_AUCTION, then SESSION|STATE=CLOSED). This order survives the CLOSED phase and will still be resting when the next session opens. ORDERS on TRADER01 now shows the DAY order from Exercise 2 as EXPIRED and this one still NEW.

Operational note: GTC is used for standing instructions that remain valid until explicitly cancelled.

Checkpoint: after the session reaches CLOSED, ORDERS still lists the GTC order as NEW.

Exercise 4: ATO Order

The market is CLOSED after Exercise 3. Open a new day to PRE_OPEN (SESSION|STATE=PRE_OPEN), then freeze in OPENING_AUCTION ([GW_ADMIN|ADMIN]> SESSION|STATE=OPENING_AUCTION — from PRE_OPEN, this is a valid direct transition), and place an At-The-Open order:

[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=200|PRICE=150.50|TIF=ATO

ATO is rejected during PRE_OPEN, not just outside auction hours

The engine only accepts TIF=ATO orders while the session state is exactly OPENING_AUCTION — PRE_OPEN accepts orders generally (it is not CLOSED), but an ATO order submitted while still in PRE_OPEN is rejected with "ATO orders only accepted during opening auction". Make sure you have actually transitioned to OPENING_AUCTION (confirm with SESSION_STATUS) before running this exercise.

This order only participates in the opening auction. If not filled during the auction, it expires the moment the session leaves OPENING_AUCTION (e.g. when CONTINUOUS begins).

Checkpoint: ATO order participates in the auction or expires.

Exercise 5: ATC Order

Exercise 4 left the session in OPENING_AUCTION; move on to CONTINUOUS (SESSION|STATE=CONTINUOUS). First try an At-The-Close order during CONTINUOUS:

[TRADER01]> NEW|SYM=AAPL|SIDE=SELL|TYPE=LIMIT|QTY=100|PRICE=149.00|TIF=ATC

It is rejected: ATC orders only accepted during closing auction. An ATC order is not parked until the close — it can only be entered during the closing auction. Freeze there and send it again:

[GW_ADMIN|ADMIN]> SESSION|STATE=CLOSING_AUCTION
[TRADER01]> NEW|SYM=AAPL|SIDE=SELL|TYPE=LIMIT|QTY=100|PRICE=149.00|TIF=ATC

Now it is accepted and waits in the auction; it can only trade when the closing auction uncrosses, on the move to CLOSED.

Checkpoint: ATC is rejected in CONTINUOUS and accepted in CLOSING_AUCTION.

Exercise 6: Rejected TIF

Exercise 5 left the session in CLOSING_AUCTION. Take it through CLOSED and a fresh PRE_OPEN back to CONTINUOUS (SESSION|STATE=CLOSED, SESSION|STATE=PRE_OPEN, SESSION|STATE=CONTINUOUS), then try submitting an ATO order:

[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TIF=ATO

Expected: rejection — ATO orders only accepted during opening auction. The phase must be exactly OPENING_AUCTION; PRE_OPEN is not enough, even though PRE_OPEN accepts orders generally. This matches the note in the summary below and the rule you saw in Exercise 4.

Checkpoint: engine rejects out-of-phase TIF correctly.

Summary

Phase Accepts Order expiry at this boundary
PRE_OPEN DAY, GTC (not ATO — see Exercise 4) —
OPENING_AUCTION DAY, GTC, ATO ATO (unfilled, on exit to any other phase)
CONTINUOUS DAY, GTC (not ATO or ATC) —
CLOSING_AUCTION DAY, GTC, ATC ATC (unfilled, on exit to CLOSED)
CLOSED — (no new orders accepted) DAY (all, on entry to CLOSED)

Notes:

  • The four values above are the whole of TIF=: DAY, GTC, ATO, ATC. IOC and FOK are order types (TYPE=), not time-in-force values — a common confusion, since all six describe "how long does this live".
  • ATO is only accepted while the session state is exactly OPENING_AUCTION — not during PRE_OPEN, even though PRE_OPEN accepts orders generally.
  • ATC is only accepted while the session state is exactly CLOSING_AUCTION.
  • Unfilled ATO/ATC orders are expired the moment the session leaves their respective auction phase (not on any other transition). The order.expired event reports Opening auction ended for ATO and Closing auction ended for ATC.
  • All resting DAY orders (regardless of when they were placed) are expired the moment the session transitions into CLOSED, with reason Trading day ended.

Reflection

Why does GTC survive every phase transition while DAY does not? If you were running an overnight risk desk, what would go wrong if all your hedge orders were accidentally submitted as DAY instead of GTC?

Further Reading

Next: 07 — Auctions