When to Use
Use this skill in algorithmic execution engines, market-making algorithms, and futures risk managers that still hold or trade a contract inside its final weeks. Liquidity leaves an expiring futures contract before it expires: CME designates the Equity Index roll date as the Monday preceding the third Friday of the expiration month, and from that date the second-nearest expiration is identified as the lead month. For practically the whole of expiry week the contract the strategy is still in is no longer where the depth is — the resting size thins and the quoted spread widens while the position is open.
The engine reads one microstructure snapshot of the expiring contract (symbol, days_to_expiration, bid_ask_spread_ticks, top_of_book_depth_qty, baseline_average_depth_qty, is_quadruple_witching_week) and returns the execution constraints that follow from it: market orders blocked on a wide spread, a size haircut on a thinned book or a quarterly witching week, entries blocked and a roll mandated at the expiration cutoff, and escalation once the contract has stopped trading.
When NOT to Use
- As the roll mechanism. This engine says that a roll is due; it does not evaluate roll triggers, build the calendar spread, or route it. See
futures-contract-roll-automation. - As a market-impact model.
top_of_book_depth_qtyis a single level. A halved order can still sweep several levels of a drained book, and the haircut will not tell you so. Size against impact withliquidity-adjusted-position-sizingand validate the fill assumption withexecution-realistic-simulation. - On a contract that has already stopped trading. Past Last Trading Day the engine returns
EXPIRED_ESCALATEand refuses to describe any executable size. There is no automated remedy — the leg cannot be lifted and the position is heading to settlement. - As a general volatility overlay. The trigger here is the calendar, not the tape. For a volatility-shock overlay that applies away from expiry, see
adaptive-execution-under-volatility-spikes. - For options pin risk. Short options near the strike on expiration Friday are a different exposure with a different deadline; see
options-pin-risk-management-at-expiry. - Without calibrating the defaults. No regulator or exchange mandates a spread ceiling, a depth haircut, or a roll cutoff. Every threshold in this engine is a library default — see
references/standards.md.
Prerequisites
- A microstructure snapshot of the expiring contract, with units the engine cannot infer and will not guess:
days_to_expirationin business days to Last Trading Day.0= today is the final session; negative = the contract has stopped trading.bid_ask_spread_ticks= $(\text{ask} - \text{bid}) / \text{tick size}$ for this product. A tick is product-specific — E-mini S&P 500 futures trade in 0.25 index-point ticks, CME Single Stock futures in 0.01 points ($$1.00$ per tick) — so a threshold in ticks is a different currency amount on every contract.top_of_book_depth_qtyandbaseline_average_depth_qtymeasured with the same depth convention (both near-side resting size, or both bid+ask). Mixing conventions silently rescales the ratio.
- A normal-market depth baseline for the same contract, strictly positive. There is no fallback: an absent baseline raises rather than being clamped.
- A separately owned roll path and a human escalation route — the engine reports, it does not act.
Workflow
- Validate before comparing. Every threshold in this engine is a
>or<test, andNaNloses all of them. An unvalidated engine given aNaNspread answers "market orders are fine", and given a zero baseline answers "the book is deep" — the least restrictive report from the data it understands least.FuturesOrderBookState.validate()raises on non-finite, negative, crossed, or absent inputs. - Expiration audit — runs first, because it can void the rest.
days_to_expiration < 0$\implies$EXPIRED_ESCALATE: quantity capped at $0$, market orders and entries blocked, no roll mandated, logged at CRITICAL. Decision point: do not report a roll as the remedy here — telling an agent to roll a leg that no longer trades produces an order the venue will reject and hides a position that needs a human.days_to_expiration <= mandatory_roll_dbe_cutoff(inclusive) $\implies$MANDATORY_ROLL_REQUIRED: entries blocked, roll mandated, contract still tradable.DBE = 0is the final session, so it is a roll, not an escalation.- Decision point: the default cutoff of 2 business days is later than the market's own roll. CME's designated Equity Index roll date sits roughly four business days before expiration, so a cutoff of 2 fires after liquidity has already migrated. Calibrate per product rather than inheriting the default.
- Microstructure audit — evaluated in every branch, not only the restricted one.
- Spread:
bid_ask_spread_ticks > max_spread_ticks_threshold(strict; exactly at the threshold is not wide) $\implies$ market orders blocked, limit-only execution. - Depth: $\text{ratio} = \text{top_of_book_depth_qty} / \text{baseline_average_depth_qty}$;
ratio < min_depth_ratio_threshold(strict) $\implies$ size haircut. - Quad-witching week $\implies$ size haircut. Decision point: this flag is a policy override, not a measurement. It halves size on a tight, deep book too. Set it deliberately, and do not read a haircut caused by it as evidence that the book was thin —
is_depth_thinnedis the field that says that. - Decision point: a thin book does not block market orders. The spread is the immediate cost of crossing; one depth level is not a bound on market-order cost, so overloading the block with a depth signal would imply protection the engine cannot give. Thin books are answered with size.
- Spread:
- Execution size adjustment: $\text{AdjustedQty} = \lfloor \text{BaseOrderQty} \times \text{HaircutFactor} \rfloor$ — floored, never rounded, so the cap can never exceed the budget. Decision point: when the floor lands on $0$,
is_order_size_suppressedis set. That means do not send the order, not "send quantity 0" — most venues reject a zero-quantity order and some reuse the quantity field. - Read the whole report, not the status.
restriction_reasonscarries every condition that fired,is_spread_wide/is_depth_thinned/depth_ratiocarry the book state, andadjusted_max_order_qtyis a cap on a permitted order — whenis_new_entry_allowedisFalsethe only permitted orders are risk-reducing, and the cap does not re-authorise an entry.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- A safeguard that fails open on missing data:
float('nan') > 2.0isFalse, so aNaNspread reads as "not wide" and permits market orders; aNaNor zero-clamped depth baseline reads as "deep" and cancels the haircut. The controls switch themselves off exactly when the feed breaks. Validate every field before the first comparison. - Using market orders in a thinning expiry book: sending market orders once the spread has widened, and paying the widened spread on every child order.
- Reading
adjusted_max_order_qtyas permission to trade: it is a size cap on an order the caller is otherwise permitted to send. AtMANDATORY_ROLL_REQUIREDandEXPIRED_ESCALATEnew entries are blocked, whatever the cap says. - Treating quarterly witching as one uniform event: on the third Friday the expiring E-mini S&P 500 future stops trading at 9:30 a.m. ET and settles to the S&P 500 Special Opening Quotation, while CME Single Stock futures trade until 3:00 p.m. CT and settle to the cash close. A "close out before the bell" rule protects the single stock future and misses the index future by an entire session.
- Assuming "quad witching" still means the same four products: US single stock futures ceased trading when OneChicago closed in September 2020, and the day was more accurately called triple witching until CME relisted single stock futures on 27 July 2026. Treat the flag as "a quarterly third-Friday expiration week", not as a count of instrument classes.
- Rolling on the engine's default cutoff: 2 business days is this library's default and sits after CME's own designated roll date. Nothing about it is mandated, and inheriting it means rolling into the liquidity that has already left.
- Failing to enforce entry bans near expiry: opening a new position in a contract that is inside the roll cutoff, then discovering the exit has to be executed in the thinnest book of the cycle.
- Comparing depth against a baseline measured differently: a top-of-book baseline against a bid+ask snapshot halves the ratio and triggers a permanent haircut; the reverse suppresses it permanently.
- Sizing on top-of-book depth alone: the haircut is a discipline, not an impact model. A halved order can still clear several levels of a drained book.
Verification
- Instantiate
FuturesExpiryRiskHandlerEngine()(defaults: spread 2.0 ticks, depth ratio 0.30, DBE cutoff 2, haircut 0.50). Input a liquid contract (DBE=15, spread 1.0, depth 1000 against baseline 1000): verifyNORMAL_EXECUTION,size_haircut_factor == 1.0,adjusted_max_order_qty == 100on a base of 100, andrestriction_reasons == []. - Input
DBE=4, spread 3.5, depth 200 against baseline 1000, quad-witchingTrue: verifyEXPIRY_WEEK_RESTRICTED,is_market_orders_allowed is False,depth_ratio == 0.20,adjusted_max_order_qty == 50, and thatrestriction_reasonscontains all three ofWIDE_SPREAD,THIN_TOP_OF_BOOK_DEPTH,QUAD_WITCHING_WEEK. - Boundary checks: a spread of exactly 2.0 is not wide; a depth ratio of exactly 0.30 (300/1000) is not thinned;
DBE == 2mandates a roll whileDBE == 3does not. - Escalation regression:
DBE = -1must returnEXPIRED_ESCALATEwithadjusted_max_order_qty == 0,is_mandatory_roll_required is False, andrequires_manual_escalation is True. - Fail-open regressions: a
NaN/infspread, aNaNdepth, a zero or negative depth baseline, a crossed (negative) spread, a negative depth, a non-integerdays_to_expiration, and a non-positive or non-integerbase_order_qtymust each raiseValueErrorrather than producing a permissive report. Out-of-range constructor thresholds must also raise. - Sizing:
floor(7 × 0.50) == 3(not 4), and a base quantity of 1 under a 50% haircut setsis_order_size_suppressed. - Run
python -m unittest discover -s skills/futures-expiry-week-liquidity-and-volatility-handling/scriptsand confirm a 100% pass rate.