Perp: After-Hours Leverage
Sai Perp supports an after-hours risk control for scheduled markets. It reduces effective exposure for live positions carried through a market close, where ordinary trading, continuous liquidation, and fresh exit pricing are not available in the same way as during open market hours.
This page reflects the behavior merged in sai-perps main through the
after-hours leverage rollout and follow-up fixes.
Product Rule
Section titled “Product Rule”Markets enabled in AFTER_HOURS_MARKETS use the global cap stored in
AFTER_HOURS_LEVERAGE for live trades that were carried through the latest
scheduled close.
effective_leverage = min(Trade.leverage, AFTER_HOURS_LEVERAGE)The cap applies to filled TradeType::Trade positions only. Pending limit or
stop orders are not clamped because they are not live exposure until triggered.
Ordinary user opens and voluntary closes remain blocked while a market is closed. The after-hours trigger path is a maintenance action: it can liquidate or normalize risk, but it does not reopen the market for discretionary exits.
The relevant storage is:
| State | Type | Purpose |
|---|---|---|
AFTER_HOURS_LEVERAGE | Item<Decimal> | Global effective leverage cap for configured after-hours markets |
AFTER_HOURS_MARKETS | Map<MarketIndex, bool> | Markets where the after-hours cap applies |
MarketInfo.block_last_closed | Option<u64> | Block where the market last entered Closed |
MarketInfo.block_last_opened | Option<u64> | Block where the market last re-entered Open |
MarketInfo.last_close_price | Option<Decimal> | Best-effort close snapshot kept as market metadata |
last_close_price is not the settlement price for after-hours risk checks.
After-hours liquidation and normalization use the oracle price returned by the
normal price query.
Eligibility
Section titled “Eligibility”get_effective_leverage returns stored Trade.leverage unless all of these are
true:
- The trade is open and has
trade_type == TradeType::Trade. - The market is enabled in
AFTER_HOURS_MARKETS. block_last_closedandblock_last_openedare initialized.- The trade was not opened or risk-modified in the latest reopen block or later.
Trade.leverageis aboveAFTER_HOURS_LEVERAGE.
The reopen-block boundary is inclusive: a trade with
TradeInfo.created_block >= MarketInfo.block_last_opened keeps its stored
leverage.
Trigger Behavior
Section titled “Trigger Behavior”The contract can route TriggerTrade to after-hours risk handling. The
after-hours path:
- Loads the returned oracle price from the normal price query.
- Checks liquidation first at the stored-leverage liquidation price.
- If liquidation is required, follows the liquidation close path.
- If liquidation is not required, reduces stored leverage to the effective cap.
- Calls
process_closing_feeson the removed position-size delta with order typeafter_hours_risk_trigger. - Updates open interest and emits
sai/perp/trigger_trade/after_hours.
No catalyst reward is paid on normalization. The caller is helping synchronize risk, not executing a trader’s TP/SL order.
If the normalization fee consumes the remaining collateral, the contract closes the trade after the fee and risk updates.
Oracle Freshness
Section titled “Oracle Freshness”After-hours liquidation and normalization intentionally use the returned oracle price even when the market has been closed long enough for the normal freshness window to expire. The narrow exception exists so high-leverage carried exposure can still be reduced while scheduled markets are closed.
Ordinary opens, voluntary closes, and normal TP/SL trigger paths still use the
normal market-state and freshness guards. They do not fall back to
last_close_price.
Query Semantics
Section titled “Query Semantics”Contract queries preserve stored state and expose derived risk state:
get_tradereturns storedTrade, including storedTrade.leverage.get_trade_datareturnsneeds_after_hours_trigger: truewhen the trade is eligible for after-hours normalization.get_trade_pnlevaluates carried trades using effective leverage.GetAfterHoursLeverage,IsAfterHoursMarket, andListAfterHoursMarketsexpose the active cap and enabled markets.
Clients should not infer after-hours state from stored leverage alone. Use the
derived query fields when deciding whether a keeper or UI refresh should submit
TriggerTrade.
Lazy Cleanup Before Mutations
Section titled “Lazy Cleanup Before Mutations”When a user later mutates a carried trade whose stored leverage is above the after-hours effective cap, the contract first materializes the after-hours normalization. The requested mutation then operates on the normalized trade.
This guard runs before leverage updates, TP/SL updates, and position-size increases. Position-size decreases still prioritize liquidation checks before the resize proceeds.