Lesson 1.1.4 · 60 min
Order types
The order type decides what is sacrificed: price, execution or control. This lesson codes a small matching engine against NOVA's book, simulates three stops on a gap day and models the life cycle of an order as a state machine.
- Module
- 1.1 · How markets work
- Duration
- 60 min
- Prerequisites
- 1.1.2 · Sessions, auctions, holidays and calendars, 1.1.3 · Order book: bid, ask, spread and depth
- Author
- Jesús Cuesta
- Updated
Level 1 lessons
1.1 · How markets work
1.2 · Asset classes
- 1.2.1 Stocks: splits, dividends and corporate actions
- 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
- 1.2.3 Futures: specifications, margin, roll and contango
- 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
- 1.2.5 Options: calls, puts, intrinsic value and time value
- 1.2.6 Crypto: spot, perpetuals, funding and custody
- 1.2.7 Fixed income and interest rates; CFDs and leveraged products
1.3 · Market data sources
- 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
- 1.3.2 Free market data sources
- 1.3.3 Retail and prosumer market data APIs
- 1.3.4 Professional market data providers
- 1.3.5 Crypto and on-chain data
- 1.3.6 Brokers as market data sources
- 1.3.7 Market data licensing and terms of use
- 1.3.8 How to choose a market data provider
1.4 · Financial data engineering
- 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
- 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
- 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
- 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
- 1.4.5 Building bars: time, tick, volume, dollar and imbalance
- 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
- 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
- 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
1.5 · Fundamental and macro data
1.6 · Financial exploratory analysis
1.7 · Programmed technical analysis
Price or execution: the dilemma of every order
An order tells the broker what to buy or sell, how much and under what conditions. Every variant that exists comes from one basic decision:
- Market order. Execute now, at whatever price there is. It guarantees execution (if there is a counterparty) and does not guarantee the price.
- Limit order. Execute only at a price equal to or better than the limit. It guarantees the price and does not guarantee execution.
A limit buy above the best ask is marketable: it is executed immediately, like a market order, but with a cap. A limit buy below the best bid stays in the book, at the end of the queue at its price, and waits (lesson 1.1.3). The other types add to these two a condition (a trigger price, a time, a minimum size) or a relationship with other orders.
The figure places each type relative to the book. Limit orders wait on the “good” side of the price: buys below, sells above. Stops wait on the “bad” side: a sell stop sits below the bid and is triggered if the price falls to it; a buy stop, above the ask. Market orders do not wait: they cross the spread.
A twenty-line matching engine
The best way to understand an order is to execute it. This engine walks the levels of one side of the book in priority order and returns the fills. It uses NOVA's L2 snapshot at 15:00 New York time from lesson 1.1.3, with the best ask at 52.75.
import pandas as pd
l2 = pd.read_parquet("nova_libro_l2.parquet")
foto = l2[l2["ts"] == "2025-06-02 19:00:00+00:00"]
asks = foto[foto["lado"] == "ask"].sort_values("precio")
bids = foto[foto["lado"] == "bid"].sort_values("precio",
ascending=False)
lado_venta = list(zip(asks["precio"], asks["tamano"]))
lado_compra = list(zip(bids["precio"], bids["tamano"]))
medio = (asks["precio"].iloc[0] + bids["precio"].iloc[0]) / 2
lado_venta[:4][(52.75, 1870), (52.76, 9900), (52.77, 13280), (52.78, 17322)]def recorre(niveles, cantidad, limite=None, compra=True):
"""Execute against `niveles` [(price, size)] up to `cantidad`.
With `limite`, do not go past that price.
Return (fills, remainder)."""
ejecuciones, resto = [], cantidad
for precio, tam in niveles:
if limite is not None and (precio > limite if compra
else precio < limite):
break
q = min(resto, tam)
ejecuciones.append((precio, q))
resto -= q
if resto == 0:
break
return ejecuciones, resto
def resumen(ejecuciones, medio):
q = sum(c for _, c in ejecuciones)
pm = sum(p * c for p, c in ejecuciones) / q
return {"acciones": q, "niveles": len(ejecuciones),
"precio_medio": round(float(pm), 4),
"sobre_medio_cent": round(float(100 * abs(pm - medio)), 2)}
for n in (2_000, 30_000):
ejec, resto = recorre(lado_venta, n)
print(f"market buy of {n}:", resumen(ejec, medio))market buy of 2000: {'acciones': 2000, 'niveles': 2, 'precio_medio': 52.7507, 'sobre_medio_cent': 0.56}
market buy of 30000: {'acciones': 30000, 'niveles': 4, 'precio_medio': 52.7671, 'sobre_medio_cent': 2.21}A market buy of 2,000 shares is filled with the whole first level (1,870 shares at 52.75) and 130 from the second, at 52.76: average price 52.7507, 0.56 cents above the mid. One of 30,000 walks four levels and pays on average 52.7671: 2.21 cents over the mid, almost four times more per share. That is the order's impact, the cost that grows with size (lesson 1.1.5 turns it into a curve):
Time in force: DAY, GTC, IOC and FOK
Besides the price, an order carries a time in force, which says how long it lives:
| Time in force | What it does | When to use it |
|---|---|---|
| DAY | Lives until the end of the session and expires | Default at almost every broker; daily systems |
| GTC | Lives until it is executed or cancelled (brokers usually cap it at a number of days) | Stops and targets of multi-day positions |
| IOC | Executes what it can right away and cancels the rest | Taking liquidity without leaving a trace in the book |
| FOK | Everything right away or nothing | When a partial fill is useless (pairs, hedges) |
| On open (OPG, MOO, LOO) | Only takes part in the opening auction | Systems that decide with the previous close |
| On close (MOC, LOC) | Only takes part in the closing auction | Rebalancing, matching the closing price |
With the previous engine, IOC and FOK take two lines:
def limitada(niveles, cantidad, limite, validez, compra=True):
ejec, resto = recorre(niveles, cantidad, limite, compra)
if validez == "FOK" and resto > 0:
return [], cantidad, "cancelled: not enough"
if validez == "IOC":
estado = "filled" if resto == 0 else f"cancels {resto}"
return ejec, resto, estado
return ejec, resto, f"{resto} rest in the book"
for validez in ("IOC", "FOK", "DAY"):
ejec, resto, estado = limitada(lado_venta, 15_000, 52.76, validez)
print(validez, "fills", sum(q for _, q in ejec), "·", estado)IOC fills 11770 · cancels 3230
FOK fills 0 · cancelled: not enough
DAY fills 11770 · 3230 rest in the bookAt 52.76 or better there are only 11,770 shares. The IOC takes them and cancels the 3,230 that are missing; the FOK executes nothing; the DAY executes the same as the IOC and leaves a buy of 3,230 shares at 52.76 in the book, which becomes the new best bid: it has crossed the spread and now defines it.
On-open and on-close orders are matched in the auctions (lesson 1.1.2). On the NYSE there are market variants (MOO, MOC) and limit variants (LOO, LOC); MOC and LOC orders can be sent, modified or cancelled until 15:50, and after that only those that reduce the published imbalance are accepted1Note 1 · NYSE, “NYSE opening and closing auctions” fact sheet (accessed on October 21, 2026)..
Stop, stop-limit and trailing through a gap
A stop is a dormant order with a trigger price. A sell stop at 62.50 does nothing while the price is above it; when a trade prints at 62.50 or lower, it becomes a market order. A stop-limit becomes a limit order: “if it falls to 62.50, sell, but not below 62.30”. A trailing stop moves the trigger behind the price: it stays, for example, 2.5% below the high reached since the entry, and only moves up.
The difference between the three matters in gaps: when the price jumps from one level to another without going through the levels in between, typically from a close to the next open after news. NOVA reported earnings after the close on Thursday, October 24, 2024 and on Friday it opened 5.1% lower:
diario = pd.read_csv("nova_diario_sin_ajustar.csv",
parse_dates=["fecha"])
diario["hueco_pct"] = 100 * (diario["apertura"]
/ diario["cierre"].shift() - 1)
tramo = diario[diario["fecha"].between("2024-10-21", "2024-10-28")]
tramo.set_index("fecha").round(2) apertura maximo minimo cierre volumen hueco_pct
fecha
2024-10-21 63.27 64.32 62.90 64.30 13820466 0.02
2024-10-22 64.26 64.47 63.74 64.07 9126443 -0.06
2024-10-23 63.93 64.67 63.76 64.19 8728289 -0.22
2024-10-24 64.11 64.60 63.21 63.40 9874612 -0.12
2024-10-25 60.14 60.87 59.51 59.59 51399629 -5.14
2024-10-28 59.25 59.33 57.11 58.02 18793763 -0.57A system buys at the close of October 21, at 64.30, and protects the position in three ways: a stop at 62.50, a 62.50/62.30 stop-limit valid for the day and a 2.5% trailing stop. The 5-minute bars let you simulate when each one is triggered. The rule for executing a stop on bars is the honest one: if the bar opens below the stop, the sell goes out at the open; if it opens above and its low touches it, it goes out at the stop (and even that is optimistic: in reality, at the first price traded afterwards).
barras = pd.read_parquet("nova_barras_5min.parquet")
barras = (barras[barras["ts"] > "2024-10-21 20:00"]
.reset_index(drop=True))
entrada = 64.30
def stop_venta(barras, stop):
for b in barras.itertuples():
if b.minimo <= stop:
return b.ts, min(b.apertura, stop)
return None, None
def trailing_venta(barras, entrada, pct):
maximo = entrada
for b in barras.itertuples():
stop = maximo * (1 - pct)
if b.minimo <= stop:
return b.ts, min(b.apertura, stop), stop
maximo = max(maximo, b.maximo)
return None, None, None
def stop_limit_venta(barras, stop, limite, validez="DAY"):
disparo = None
for b in barras.itertuples():
if disparo is None and b.minimo <= stop:
disparo = b.ts
if b.apertura > stop: # no gap: goes out at the stop
return disparo, b.ts, stop
if disparo is not None:
if validez == "DAY" and b.ts.date() > disparo.date():
return disparo, None, None # expired unexecuted
if b.maximo >= limite:
return disparo, b.ts, max(limite, b.apertura)
return disparo, None, None
ts, px = stop_venta(barras, 62.50)
print("stop 62.50: trigger", ts, "· sell at", px)
ts, px, nivel = trailing_venta(barras, entrada, 0.025)
print(f"trailing 2.5%: trigger {ts} · level {nivel:.2f}",
f"· sell at {px}")
d, ts, px = stop_limit_venta(barras, 62.50, 62.30)
print("stop-limit DAY: trigger", d, "· sell:", px)stop 62.50: trigger 2024-10-25 13:30:00+00:00 · sell at 60.14
trailing 2.5%: trigger 2024-10-25 13:30:00+00:00 · level 63.05 · sell at 60.14
stop-limit DAY: trigger 2024-10-25 13:30:00+00:00 · sell: NoneAll three are triggered in the same bar, the one at the open of October 25 (13:30 UTC, 9:30 New York time):
- The stop at 62.50 sells at 60.14, the open: $2.36 per share below its price. The stop is not a guaranteed price; it is the condition for a market order to be born.
- The trailing stop, which had raised its level to 63.05 with the 64.67 high of October 23, sells exactly the same, at 60.14. In a gap, a tighter stop is no help at all.
- The stop-limit is triggered, but its 62.30 limit is $1.43 above the highest price of the day, 60.87. At 16:00 it expires unexecuted and the position stays open, unprotected.
The figure at the top draws it bar by bar. And what if the stop-limit were GTC?
disparo, ts, px = stop_limit_venta(barras, 62.50, 62.30, validez="GTC")
print("executed", ts, "at", px)
entre = barras[(barras["ts"] > disparo) & (barras["ts"] < ts)]
print("low while waiting:", entre["minimo"].min(),
"on", entre.loc[entre["minimo"].idxmin(), "ts"].date())executed 2025-01-16 16:55:00+00:00 at 62.3
low while waiting: 53.51 on 2025-01-03It is executed almost three months later, on January 16, 2025, when NOVA returns to 62.30. Along the way the stock reaches 53.51 on January 3, almost $9 below the limit. For a portfolio this is the opposite of protection: the stop-limit avoids selling badly in the gap in exchange for not selling at all while the price keeps falling.
The life cycle of an order
An order does not simply go from “sent” to “filled”. The broker and the market send back states, and a real system has to react to each one: a partial fill leaves a position different from the one planned; a rejection (for insufficient funds, a price out of range, a closed market) means there is no position.
Mermaid source
stateDiagram-v2 [*] --> New New --> Accepted New --> Rejected Accepted --> Partial Accepted --> Filled Accepted --> Cancelled Accepted --> Expired Partial --> Filled Partial --> Cancelled Partial --> Expired Filled --> [*] Cancelled --> [*] Rejected --> [*] Expired --> [*]
Three states are final as far as the order is concerned, though not as far as the position is: an order cancelled or expired after a partial fill has left shares in the account. Modeling it as a state machine avoids the most common mistakes, such as cancelling an order that was already filled or counting a fill twice:
TRANSICIONES = {
"nueva": {"aceptada", "rechazada"},
"aceptada": {"parcial", "ejecutada", "cancelada", "expirada"},
"parcial": {"parcial", "ejecutada", "cancelada", "expirada"},
}
class Orden:
def __init__(self, cantidad):
self.cantidad, self.ejecutada = cantidad, 0
self.estado, self.historial = "nueva", ["nueva"]
def pasa(self, estado):
if estado not in TRANSICIONES.get(self.estado, set()):
raise ValueError(f"cannot go from {self.estado} to {estado}")
self.estado = estado
self.historial.append(estado)
def ejecuta(self, q):
if self.ejecutada + q > self.cantidad:
raise ValueError("fill larger than the order")
self.ejecutada += q
lleno = self.ejecutada == self.cantidad
self.pasa("ejecutada" if lleno else "parcial")
# the IOC for 15,000 shares at 52.76 against the 15:00 book
o = Orden(15_000)
o.pasa("aceptada")
for _, q in recorre(lado_venta, 15_000, limite=52.76)[0]:
o.ejecuta(q)
o.pasa("cancelada") # IOC: the rest is cancelled
print(" -> ".join(o.historial), "·", o.ejecutada, "shares")nueva -> aceptada -> parcial -> parcial -> cancelada · 11770 sharesThe states are code keys, so they print in Spanish: nueva (new), aceptada (accepted), parcial (partial),
ejecutada (filled), cancelada (cancelled). A filled order cannot be cancelled. If the system tries (because it
received the confirmation late, for example), the state machine stops it instead of letting the error reach the
account:
o2 = Orden(2_000)
o2.pasa("aceptada")
o2.ejecuta(2_000)
o2.pasa("cancelada")ValueError: cannot go from ejecutada to canceladaLinked orders: bracket and OCO
An OCO (one cancels the other) is two linked orders: when one is executed, the system cancels the other. The typical combination is a profit target (limit sell above) and a stop loss (stop sell below). A bracket adds the entry: the OCO pair is only activated when the entry is executed.
Mermaid source
flowchart TB E[Limit entry] -->|filled| A[Activates OCO] A --> TP[Limit target] A --> SL[Stop loss] TP -->|executed| C1[Cancels the stop] SL -->|executed| C2[Cancels target]
Having the broker manage the link has an advantage: if the system goes down, the exit orders remain on the server. The risk lies in partial fills: if the target is half executed, the stop has to be reduced to the remaining quantity, and not every broker does it the same way. A system that sends brackets must read what its broker does and check it in a paper trading account.
Other orders that show up in the data
- Iceberg. A limit order with a visible part and a hidden reserve that is replenished (lesson 1.1.3).
- Hidden. A limit order that is not visible at all in the book; it usually loses priority to visible orders at the same price.
- Pegged. Its price follows the best bid, the best ask or the midpoint; it is what is matched in the dark pool of lesson 1.1.1.
- Broker algorithms (TWAP, VWAP). They are not market order types, but programs that split a large order into many small ones over the day. They are the source of many of the 100-share trades on the tape.
| Type | Guarantees | Main risk | Typical use in a system |
|---|---|---|---|
| Market | Execution | Price, especially with a thin book | Exits that cannot wait |
| Limit | Price | Not being executed | Patient entries, liquidity provision |
| Stop | Trigger | Gap: execution far from the stop | Stop loss |
| Stop-limit | Price after the trigger | Being unable to exit | Breakout entries with a cap |
| Trailing | Follows the price | Same as the stop | Protecting profits |
| MOC / LOC | Closing price | Imbalance; sending before the cutoff time | Rebalancing, at-the-close systems |
| IOC / FOK | Leaves no trace | Partial or no execution | Taking liquidity in a controlled way |
| Bracket / OCO | Automatic exit | Badly linked partials | Positions with a target and a stop |
Exercises
- Compute the average price and the levels walked by a market sell of 25,000 shares against the 15:00 book and compare it with a buy of the same size. Why do they not cost the same?
Solution
for lado, niveles, nombre in ((False, lado_compra, "sell"),
(True, lado_venta, "buy")):
ejec, resto = recorre(niveles, 25_000, compra=lado)
print(nombre, resumen(ejec, medio))sell {'acciones': 25000, 'niveles': 3, 'precio_medio': 52.7345, 'sobre_medio_cent': 1.05}
buy {'acciones': 25000, 'niveles': 3, 'precio_medio': 52.7645, 'sobre_medio_cent': 1.95}Both walk three levels, but the sell pays 1.05 cents per share over the mid and the buy 1.95, almost double. The reason lies in the first level: the sell finds 13,432 shares at the best bid, half a cent from the mid, and the buy only 1,870 at the best ask, so most of the buy is executed at the second and third levels (lesson 1.1.3). If the sell were for 40,000 shares, things would change: beyond the third buy level there is hardly anything. An asymmetric book charges buyers and sellers differently, and the asymmetry changes with size.
- On June 2, 2025 a system buys NOVA at the 9:30 open (open of the first bar) with a bracket: target 0.5% above and stop 0.5% below. With the 5-minute bars, which one is executed first? What would you do if the same bar touched both?
Solution
todas = pd.read_parquet("nova_barras_5min.parquet")
dia = todas[todas["ts"].dt.date == pd.Timestamp("2025-06-02").date()]
ent = dia["apertura"].iloc[0]
tp, sl = round(ent * 1.005, 2), round(ent * 0.995, 2)
print("entry", ent, "· target", tp, "· stop", sl)
for b in dia.itertuples():
toca_tp, toca_sl = b.maximo >= tp, b.minimo <= sl
if toca_tp or toca_sl:
print(b.ts.strftime("%H:%M UTC"), "target" if toca_tp else "",
"stop" if toca_sl else "")
breakentry 53.35 · target 53.62 · stop 53.08
13:55 UTC stopThe stop is hit in the 13:55 UTC bar (9:55 New York time), 25 minutes after the entry, and the OCO cancels the
target. The first bar that touches one of the two levels decides the exit. If the same bar touched both, with bars you
cannot know which came first: the prudent rule is to assume the stop (the worst case) or to drop down to
higher-resolution data, such as the trades in nova_operaciones.parquet.
- A daily system enters at the open with market orders and exits with a 3% GTC stop. Propose a better set of orders and justify each change.
Solution
Entry: an on-open order (OPG or MOO) instead of a market order at 9:30, to be matched in the auction, at a single price and with plenty of liquidity, rather than in the first seconds of continuous trading, when the spread is wider (lesson 1.1.3). If a cap is wanted, an LOO. Exit: the stop can remain a stop (not a stop-limit), because what matters is getting out; but the backtest must simulate it with the open on gaps. If the strategy also has a target, link target and stop in an OCO managed by the broker, so that the exit does not depend on the system being alive. And in every case, handle partial fills and rejections with the state machine.
Footnotes
-
NYSE, “NYSE opening and closing auctions” fact sheet (accessed on October 21, 2026). ↩
Each answer is folded under its question
Self-assessment
What does a market order guarantee and what does it not?
- AIt guarantees the price, not the execution
- BIt guarantees execution if there is a counterparty, not the price
- CIt guarantees both
- DIt guarantees neither
Show answer
Answer B. It guarantees execution if there is a counterparty, not the price
A market order is executed against whatever is in the book, level by level, until it is complete. In NOVA's 15:00 book, a market buy of 30,000 shares pays on average 52.7671, almost two cents above the best ask.
An IOC limit buy for 15,000 shares at 52.76 arrives at a book with 1,870 shares at 52.75 and 9,900 at 52.76. What happens?
- ANothing is executed
- B15,000 are executed, the last ones at 52.77
- C11,770 shares are executed and the rest is cancelled
- D11,770 are executed and the rest stays in the book
Show answer
Answer C. 11,770 shares are executed and the rest is cancelled
IOC (immediate or cancel) executes what it can right away within the limit and cancels the rest. With FOK (fill or kill) nothing would be executed, because there are not 15,000 shares at 52.76 or better.
On October 25, 2024 NOVA opens at 60.14 after closing at 63.40. What does a stop-limit sell with stop 62.50 and limit 62.30, valid for the day, do?
- AIt is executed at 62.30
- BIt is executed at 60.14
- CIt is not triggered
- DIt is triggered but not executed: the price does not return to 62.30 for the whole session and the order expires
Show answer
Answer D. It is triggered but not executed: the price does not return to 62.30 for the whole session and the order expires
The stop is triggered by the first trade below 62.50, which is the open at 60.14. A limit sell at 62.30 is then born, which is only executed at 62.30 or higher. The day's high is 60.87, so the order expires unexecuted and the position stays open.
What is an OCO order?
- ATwo linked orders: when one is executed, the other is cancelled
- BAn order executed at the close
- CAn order that is only executed in full
- DA hidden order
Show answer
Answer A. Two linked orders: when one is executed, the other is cancelled
OCO (*one cancels the other*) links two orders, typically a profit target and a stop. A bracket is an entry with an exit OCO that is activated when the entry is executed.
A system decides with the day's close and wants to execute in the NYSE closing auction. Which order, and when?
- AA market order at 16:00
- BAn MOC (or LOC) sent before 15:50, with the signal computed from data before that time
- CA stop at the closing price
- DA GTC
Show answer
Answer B. An MOC (or LOC) sent before 15:50, with the signal computed from data before that time
NYSE on-close orders are accepted, modified or cancelled until 15:50; after that only those that reduce the published imbalance are allowed. The signal cannot use the closing price, which does not yet exist when the order has to be sent (lesson 1.1.2).
Key points
- Every order chooses between certainty of execution (market) and certainty of price (limit). The others are combinations of these two with a condition.
- A market order walks the book: its cost grows with size. For NOVA, 2,000 shares pay 0.56 cents over the mid and 30,000, 2.21.
- A stop is a dormant market order: in a gap it is executed at the first available price, far from the stop. A stop-limit protects the price, but may not be executed and leave the position open.
- Time in force matters as much as the type: DAY, GTC, IOC, FOK, on open or on close.
- Linked orders (bracket, OCO) and the life cycle (partial, cancelled, expired) are what a real system has to manage, and what a backtest usually ignores.