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 lrvx
exec = lrvx.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.
Reduce-only orders bypass the pre-trade gates¶
reduce_only / flatten / close_position orders skip the risk
manager, the kill switch, and the order validator by design — on
both BacktestRunner and the live Runner. The gate only fires on
entry-type orders (no flags.reduceOnly). This is deliberate: if a
strategy holds a position and a tightened risk cap would now reject
the entry, the exit still has to go through, or the strategy is
stuck in the position it can no longer afford to hold.
The consequence is that tripping the kill switch, or any other
pre-trade gate, does not by itself stop reduce-only order flow —
only new exposure. If you need to block exits too during a halt,
add a separate guard rather than relying on the gate; setting
flags.reduceOnly = true (or calling close_position /
Strategy.close_position) is exactly the escape hatch this bypass
exists for.
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
lrvx_simulated_executor_submit_order_exfor the extended-flag path. The legacysubmit_orderstays byte-compatible.