Extended time-in-force and reduce-only¶
The simulator now honours four order flags that real venues enforce on submit. Strategies built against GTC and POST_ONLY get the same behaviour as before; the new flags add coverage for the order types professional venues (Binance UM, Deribit, Bybit options) accept.
Flags¶
| Flag | Behaviour at submit |
|---|---|
gtc (default) |
Rests until cancelled or filled. |
ioc |
Take whatever crosses now, cancel the remainder. Never rests. |
fok |
Atomic: fill the entire order at the best price or reject with fok_not_fillable. No partial fills. |
gtd |
Like gtc, plus auto-cancel at expires_at_ns. Expiry fires on the next market event after the deadline. |
post_only |
Reject any limit that would cross. Same as before this task. |
reduce_only (orthogonal flag) |
Order may only reduce the open position. Rejected if it would open/grow; truncated if it would overshoot flat. |
The TOB liquidity assumption for FOK is a simplification — only the best level is consulted. Deeper-walk FOK is a follow-up.
FOK mode (any-price vs single-price)¶
Real venues differ on what FOK means when crossing liquidity sits at a different price than the order's limit:
| Mode | Behaviour | Used by |
|---|---|---|
any_price |
Fill if TOB qty ≥ order qty and TOB crosses the limit. | Default; most crypto venues |
single_price |
Fill only if TOB price equals the order's limit and TOB qty ≥ order qty. | CME, Eurex, most US equities |
Single-price rejects with fok_unfillable when crossing liquidity
sits at a more aggressive level than the limit. any_price accepts
that same scenario.
Apply from a strategy¶
"""Use FOK / IOC / GTD / reduce-only with SimulatedExecutor."""
import flox_py as flox
exec = flox.SimulatedExecutor()
exec.set_queue_model("tob", 1)
exec.on_best_levels(symbol=1, bid_price=50_000.0, bid_qty=5.0,
ask_price=50_001.0, ask_qty=5.0)
# FOK: fully fill or reject. Asks 5.0 available, request 1.0 → fills.
exec.submit_order(id=1, side="buy", price=50_001.0, quantity=1.0,
type="limit", symbol=1, tif="fok")
# IOC: take what crosses now, cancel remainder.
exec.submit_order(id=2, side="buy", price=50_001.0, quantity=0.5,
type="limit", symbol=1, tif="ioc")
# GTD: rests like GTC but auto-cancels at the absolute deadline.
expires = 5_000_000_000 # 5 seconds since simulator start
exec.submit_order(id=3, side="buy", price=49_500.0, quantity=1.0,
type="limit", symbol=1, tif="gtd",
expires_at_ns=expires)
# reduce_only: rejects if it would open / grow the position. After the
# two buys above we are long 1.5; a reduce-only sell shrinks toward
# flat. A reduce-only buy would be rejected.
exec.submit_order(id=4, side="sell", price=50_000.0, quantity=0.5,
type="limit", symbol=1, tif="ioc", reduce_only=True)
Reduce-only mechanics¶
The simulator maintains a per-symbol net position internally,
updated in executeFill. A reduce-only submit is evaluated against
that net:
- Flat → reject with
reduce_only. - Same side as the position → reject (would grow).
- Opposite side that exactly reduces or zeros → accepted.
- Opposite side that would flip the sign → truncated to flat.
The truncation is applied before the order enters the book / queue tracker, so the rest of the lifecycle (fills, queue position, etc.) operates on the truncated quantity.
GTD expiry timing¶
expires_at_ns is an absolute timestamp in the simulator's clock
domain. The expiry fires at the next market event (book update /
trade) past the deadline — there is no internal tick-driven
scheduler. For deterministic expiry at exact times, advance the
clock and fire any market event after the deadline.
Notes¶
- POST_ONLY already rejected crossing limits before this task; the behaviour is unchanged.
- IOC partial fills emit
PARTIALLY_FILLEDfor the taken portion; the unfilled remainder emitsCANCELEDwithout a free-text reason field on cancel (the TIF tag is on the order itself). - The C ABI ships
flox_simulated_executor_submit_order_exfor the extended-flag path. The legacysubmit_orderstays byte-compatible.