When to Use
Use this skill when a passive, liquidity-providing strategy submits Post-Only limit orders
(LIMIT_MAKER, GTX, post_only, ParticipateDoNotInitiate) into a book that can move
between the moment you read the BBO and the moment your order reaches the matching engine —
CPI and payroll prints, open and close auctions, crypto liquidation cascades.
The core problem is not that a crossing Post-Only order gets rejected. It is that "Post-Only" means three different things across venues, and only one of them is a harmless rejection:
- Rejected — Binance Spot
LIMIT_MAKER, Coinbasepost_only. - Executed as a taker at the resting order's price — Nasdaq / BX / PSX. You pay taker fees, forfeit the maker rebate, and remove the liquidity you meant to provide.
- Silently re-priced by the matching engine — Deribit's default
post_only, which rests your order at a price you never chose.
So a client that submits a crossing Post-Only order and assumes "the venue will just reject
it" is wrong on two of the three venue families, and expensively wrong on one. This engine
makes the passivity and tick-alignment decision locally, before the message leaves, which
is the only guarantee that holds across all three. Sourced venue table in
references/standards.md.
When NOT to Use
- As a substitute for reading your venue's rulebook. The engine prevents your client from emitting a crossing price. It cannot stop a venue from filling, sliding, or rejecting an order for reasons it does not model (self-match prevention, price bands, credit, entitlements).
- For cross-venue / NBBO compliance. It sees one
best_bid/best_askpair. A price that is passive on the target venue can still lock or cross a protected quotation elsewhere — seeus-reg-nms-order-protection-rule-compliance. - When you actually want the fill. Repricing to the passive boundary is a decision to not
trade now. If the objective is completion under a deadline, use an execution algorithm that
is allowed to cross — see
implementation-shortfall-minimization. - As a latency fix. The decision is correct for the snapshot handed to it. If quotes
routinely change while your order is in flight, the answer is colocation, a faster quote
loop, or a wider
fast_market_offset_ticks— not more reprice attempts. - For pegged orders. A continuously re-evaluated reference price is a different mechanism;
see
peg-order-types-for-passive-execution.
Prerequisites
- Top-of-book snapshot per symbol:
symbol,best_bid,best_ask, andtick_sizesourced from venue reference data at runtime — not a hard-coded constant. US NMS increments are assigned per symbol and are subject to regime change; crypto ticks vary by orders of magnitude across pairs. - Optional
market_velocity_ticks_per_secfor fast-market classification. - Order spec:
order_id,side('BUY'/'SELL'),quantity(> 0),desired_price(> 0), and thereprice_attemptsalready spent on this order cycle. Config(max_reprice_attempts=3, fast_market_velocity_threshold=20.0, fast_market_offset_ticks=0)— all three are library defaults, not venue mandates.
Workflow
-
Validate the snapshot. Non-finite, zero, or negative prices and ticks raise. A
NaNbest bid is a feed defect, not a market condition, and must not be quietly priced around. -
Check the attempt budget first. If
reprice_attempts >= max_reprice_attempts, returnREPRICE_ATTEMPTS_EXCEEDED/action='HOLD'and send nothing.- Decision point: cancel or stand down — do not reset the counter and re-enter. An order that keeps crossing is telling you the level is moving faster than your quote loop.
-
Detect a locked or crossed book. If
best_bid >= best_askthere is no passive price on either side (repricing a BUY tobest_bidstill sits at or abovebest_ask).- Decision point: this is a
HOLD, not a reprice, and it consumes no attempt. A crossed book often means a stale or gapped feed — validate before trusting the next snapshot.
- Decision point: this is a
-
Test crossing at the inclusive boundary. A BUY takes at
desired_price >= best_ask; a SELL takes atdesired_price <= best_bid. An order resting strictly inside the spread is already passive and is left untouched — pulling it to the touch would surrender price improvement for nothing. -
Reprice to the passive boundary, then apply the fast-market offset. BUY →
best_bid, SELL →best_ask; then movefast_market_offset_ticksfurther from the touch if the book is classified fast.- Decision point: the offset is a trade-off, not free safety. Joining the touch
maximises fill probability and is the price most likely to be crossed while in flight;
backing off cuts both. Default
0; raise only on measured evidence of in-flight re-crossing.
- Decision point: the offset is a trade-off, not free safety. Joining the touch
maximises fill probability and is the price most likely to be crossed while in flight;
backing off cuts both. Default
-
Align to the tick, away from the touch. Floor a BUY, ceil a SELL, quantized with
Decimal.- Decision point: never round to nearest. Nearest-rounding moves a BUY up onto the ask and a SELL down onto the bid — it manufactures the exact order this skill prevents.
-
Re-assert passivity after alignment. A coarse tick, an off-tick quote, or a sub-tick bid can still yield a marketable or non-positive price. On violation return
NO_VALID_PASSIVE_PRICE/action='HOLD'. -
Carry the attempt count forward. The engine does not mutate the order. Rebuild it with
report.next_attempt(order)before resubmitting, or the cap never engages. -
Submit only when
report.action == 'SUBMIT', and persist the report as the audit trail.
Full procedure: see
references/workflows.md. Standards and sourced venue behaviour: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Assuming Post-Only cannot take liquidity. On Nasdaq, BX and PSX a Post-Only order that would cross the book "will be executed at the price of the resting order." The protective flag you believed you had is, on that venue family, a price-improvement flag. Verify per venue before relying on it.
- Rounding the repriced limit to the nearest tick. With
best_bid = 100.00,best_ask = 100.10andtick = 0.05, a passive BUY at100.08rounds to100.10— the best ask. The tick-alignment step, added to prevent rejections, becomes the thing that causes them. Floor BUYs, ceil SELLs. - Rounding prices to a fixed number of decimals.
round(price, 4)on a0.00000001-tick pair returns0.0. A price-formatting shortcut that works on dollar-denominated equities silently destroys crypto prices. - Float arithmetic for tick alignment.
round(price / tick) * tickreintroduces binary representation error and produces off-tick prices that fail venue validation outright — Binance'sPRICE_FILTERrequiresprice % tickSize == 0. - Repricing into a locked or crossed book. When
best_bid >= best_ask, "reprice the BUY to the bid" produces a price at or above the ask. The repricing logic confidently emits the crossing order it was written to prevent. - A churn cap that never engages. Reading
reprice_attemptswithout ever incrementing and carrying it forward gives an unbounded loop wearing a bounded loop's API. Verify the cap in a resubmission loop, not a single call. - Assuming rejected orders are free. Binance documents that rejected orders "might or
might not" update the
ORDERSrate limit, and the rate-limit decrement is earned by filling — which a rejected Post-Only order never does. Breaches return429/-1015and escalate to IP bans lasting from 2 minutes to 3 days. - Ignoring the queue-position cost. Every reprice is a new order at the back of the queue
at that level. A loop that keeps its price passive can still have a terrible fill rate; see
queue-position-modeling-for-passive-orders. - Treating a malformed
sideas passive. Defaulting an unrecognised side to "accepted, no reprice" turns a typo into an unchecked live order. Unknown sides raise.
Verification
- Instantiate
FastMarketPostOnlyRepricer(). Withbest_bid = 100.00,best_ask = 100.10,tick_size = 0.05, submit a BUY at100.08: expectfinal_limit_price == 100.05(floored, strictly below the ask), not100.10. Mirror with a SELL at100.02: expect100.05, strictly above the bid. - Crossing reprice: BUY at
60015.0against60000.0 / 60010.0at 25 ticks/sec ⇒POST_ONLY_PASSIVE_REPRICED,final_limit_price == 60000.0,is_fast_markettrue. - Precision: BUY at
0.00002460against0.00002451 / 0.00002455withtick_size = 0.00000001⇒final_limit_price == 0.00002451, exactly on-tick. - Churn cap: loop
report = engine.process_order(market, order); order = report.next_attempt(order)five times against a permanently crossing price ⇒ threePOST_ONLY_PASSIVE_REPRICEDthenREPRICE_ATTEMPTS_EXCEEDEDwithaction == 'HOLD'. - Locked book:
best_bid == best_ask == 60000.0⇒BOOK_LOCKED_OR_CROSSED,action == 'HOLD', and no attempt consumed. - Negative checks:
tick_size <= 0,NaN/Infprices,quantity <= 0,desired_price <= 0, an unrecognisedside, and a negativereprice_attemptsmust each raiseValueError. - Run
python -m unittest discover -s skills/post-only-limit-repricing-under-fast-markets/scriptsand confirm a 100% pass rate.