Skip to main content

rustrade_execution/exchange/mock/
venue.rs

1//! The simulated venue's state machine, independent of any transport that drives it.
2
3use crate::{
4    AccountEventKind, InstrumentAccountSnapshot, UnindexedAccountEvent, UnindexedAccountSnapshot,
5    balance::{AssetBalance, MarginDetails},
6    borrow_capacity::BorrowCapacity,
7    cash_flow::{CashFlow, CashFlowId, CashFlowKind, InterestTiming},
8    client::mock::MockExecutionConfig,
9    error::{ApiError, BorrowReject, BorrowRejectReason, UnindexedApiError, UnindexedOrderError},
10    exchange::mock::{
11        account::{AccountState, BalanceInsufficient, Committed, Credit, Debit, Delivery, Lending},
12        orders::{AlreadyResting, OpenOrder, Reservation, RestingOrder},
13    },
14    fee::{FeeAssetConfig, FeeModel, FeeModelConfig, Liquidity, SpotFeeAsset},
15    fill::{FillContext, FillModel, MarketOrderTiming, NextBarPrice, StaleMarketPolicy},
16    holding_cost::{HeldPosition, HoldingCostModel},
17    margin::{InterestDue, MarginEffect, MarginInterestModel, SpotMarginConfig},
18    margin_debt::{MarginDebtKind, MarginDebtUpdate},
19    market::{MarketBar, MarketDepth, MarketObservation, MarketSnapshot},
20    order::{
21        Order, OrderKind, TimeInForce, UnindexedInactiveOrder, UnindexedOrder, UnindexedOrderKey,
22        id::{ClientOrderId, OrderId, VenueOrderId},
23        request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
24        state::{Cancelled, Expired, Filled, Open, OrderState, UnindexedOrderState},
25    },
26    position::{Position, PositionReport},
27    shortability::ShortabilityProvider,
28    slippage::Slippage,
29    trade::{AssetFees, TimeSource, Trade, TradeId, TradeOrigin},
30};
31use chrono::{DateTime, Utc};
32use fnv::{FnvHashMap, FnvHashSet};
33use itertools::Itertools;
34use rust_decimal::Decimal;
35use rustrade_instrument::{
36    Side, Underlying,
37    asset::name::AssetNameExchange,
38    exchange::ExchangeId,
39    instrument::{Instrument, kind::InstrumentKind, name::InstrumentNameExchange},
40};
41use rustrade_integration::collection::snapshot::Snapshot;
42use smol_str::ToSmolStr;
43use std::{sync::Arc, time::Duration};
44use tracing::warn;
45
46mod liquidation;
47mod spot_pairs;
48
49use spot_pairs::{SpotPair, SpotPairs};
50
51/// Everything one request obliges a driver to deliver, in the order it must deliver it.
52///
53/// # Ordering obligation
54/// Every event in `events` must reach the client **before** `response`, and in the order given. A
55/// [`SimulatedVenue`] balance is an absolute restatement, not a delta: successive fills report
56/// `9_999_500`, then `9_999_000`, then `9_998_500`. Delivering two of them out of order therefore
57/// does not merely reorder history, it leaves the client holding the wrong balance.
58#[derive(Debug)]
59pub struct VenueOutcome<Response> {
60    /// Account events this request produced, in the order they must be delivered.
61    pub events: Vec<UnindexedAccountEvent>,
62    /// The response owed to whoever made the request.
63    pub response: Response,
64}
65
66/// Response type of [`SimulatedVenue::open_order`].
67pub type OpenOutcome = VenueOutcome<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>;
68
69/// Response type of [`SimulatedVenue::cancel_order`].
70pub type CancelOutcome = VenueOutcome<UnindexedOrderResponseCancel>;
71
72/// A venue's view of one instrument: what it is worth, how much of it is left to take, and when
73/// that view was last replaced.
74///
75/// The instant is carried alongside the prices rather than inside them because a
76/// [`MarketSnapshot`] says what the market is, not when it was observed — and a matching engine
77/// needs both: an order resting since `T` may only be matched against a market at or after `T`.
78/// The remaining depth is here for the same reason and one more: it is not even a property of the
79/// market, but of what this account has already taken out of it.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub struct VenueInstrumentMarket {
82    /// Prices as of [`time_exchange`](Self::time_exchange).
83    pub snapshot: MarketSnapshot,
84
85    /// What is **left** of the size reported with [`snapshot`](Self::snapshot), after every taker
86    /// fill this venue has struck against it.
87    ///
88    /// Reset wholesale by [`apply_market`](SimulatedVenue::apply_market), because a new
89    /// observation is a new statement of what is on offer rather than an increment to the old
90    /// one. Between two such observations it only ever falls, which is what stops two market
91    /// orders arriving in the same tick from each taking the whole of a size that was only ever
92    /// displayed once. Reading a size does not consume it; filling against it does.
93    pub depth: MarketDepth,
94
95    /// When this view was last replaced.
96    pub time_exchange: DateTime<Utc>,
97
98    /// The most recent bar this instrument was fed, or `None` if it was never fed one.
99    ///
100    /// Replaced only by an observation that carries a bar, so a tick between two bars leaves it
101    /// standing. What [`MarketOrderTiming::NextBar`] needs to have seen before it accepts a market
102    /// order to hold for the next one.
103    pub bar: Option<MarketBar>,
104
105    /// When this instrument was last observed [`fresh`](MarketObservation::fresh), or `None` if it
106    /// never was.
107    ///
108    /// Moved only by a fresh observation, so a bar carrying a price forward with zero volume leaves
109    /// it standing while it replaces [`snapshot`](Self::snapshot). What
110    /// [`StaleMarketPolicy::Wait`] measures a market's age from.
111    pub fresh_at: Option<DateTime<Utc>>,
112}
113
114/// Which price source a [`SimulatedVenue`] has, and therefore which orders it can accept.
115///
116/// A venue's regime is fixed at construction and is the gate on [`OrderKind::Limit`]. Without it a
117/// limit order placed through a driver with no market feed would be accepted and then rest forever
118/// with nothing that could ever match it — accepted-and-silently-never-filled, which is worse than
119/// a flat rejection naming the reason.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum VenueRegime {
122    /// No market feed: a market order is priced from the snapshot its own request carries, and
123    /// nothing can rest, so [`OrderKind::Limit`] is rejected.
124    ///
125    /// The regime of [`SimulatedVenue::new`], and what [`MockExchange::new`](super::MockExchange::new)
126    /// drives.
127    RequestPriced,
128
129    /// A driver feeds [`apply_market`](SimulatedVenue::apply_market), so an order can rest and be
130    /// matched against that feed later — and **every** fill, market orders included, is priced
131    /// from that feed rather than from the snapshot a request carried.
132    ///
133    /// The regime of [`SimulatedVenue::new_market_driven`], and what `SimRunner` and
134    /// [`MockExchange::new_market_driven`](super::MockExchange::new_market_driven) drive.
135    MarketDriven,
136}
137
138/// Simulated venue state machine: synchronous, transport-free and latency-free.
139///
140/// Fills every market order immediately, rests limit orders that the market has not reached, and
141/// keeps its own balance ledger. It has no channels and never sleeps, so a driver decides *when*
142/// its output reaches a client while the venue decides *what* that output is and in which order it
143/// must arrive — see [`VenueOutcome`].
144///
145/// # Which [`InstrumentKind`]s it accounts for
146/// [`Spot`](InstrumentKind::Spot) and [`Cfd`](InstrumentKind::Cfd). A spot fill exchanges the two
147/// underlying assets; a CFD fill is cash-settled in the quote asset in both directions, since there
148/// is nothing to deliver and a short is a margin position rather than a stock loan — see
149/// `# How a CFD is accounted`. Both carry the instrument's `contract_size` into the notional and
150/// the fee, so this ledger and the engine's position accounting cannot disagree by that multiplier.
151///
152/// # How a spot fill is accounted
153/// A spot fill exchanges the two assets. Which asset its fee is charged in is
154/// [`spot_fee_asset`](Self::spot_fee_asset), a [`SpotFeeAsset`] rule; the amount comes from
155/// [`fee_model`](Self::fee_model), priced in quote. Under the default,
156/// [`SpotFeeAsset::Quote`], the fee is charged in quote on both sides:
157/// - **A buy** pays `price × quantity` plus the fee in quote, and receives `quantity` of base.
158/// - **A sell** delivers `quantity` of base, and receives `price × quantity` less the fee in quote.
159///
160/// Under [`SpotFeeAsset::Received`], a sell is the same, and **a buy** pays `price × quantity` in
161/// quote and receives `quantity` less the fee in base, the quote fee divided by the price. A fill
162/// at a price that is not positive cannot be converted, so its fee is in quote.
163///
164/// A fill whose fee exceeds what it receives, which only a flat per-unit fee on a near-worthless
165/// fill can produce, takes the excess from the balance of the asset it receives, and is refused
166/// if that cannot cover it.
167///
168/// With [`MockExecutionConfig::fee_asset`], a fill instead pays its fee, discounted, in a third
169/// asset such as BNB, out of that asset's free balance and apart from what it exchanges: a buy
170/// pays `price × quantity` in quote and receives `quantity` of base, and a sell delivers
171/// `quantity` of base and receives `price × quantity`. It falls back to the rule above when that
172/// balance is short, the asset has no price, or the instrument trades the asset itself. See
173/// [`FeeAssetConfig`]. A fill paying in the fee asset restates that balance after the two it
174/// exchanged.
175///
176/// The fee the fill reports in [`Trade::fees`] is the one the ledger charged, in the asset it
177/// charged. So the base a fill moves is its quantity less any fee it reports in base, and the
178/// engine's position, which nets a base fee from a spot quantity, agrees with this ledger's base
179/// balance. A fill restates both balances it moved, the one it paid with first, before the
180/// [`Trade`].
181///
182/// # Spot margin
183/// By default this venue is a cash account: a spot order must be paid for with what the account
184/// holds, so a sell of base it lacks is refused. Configured with
185/// [`MockExecutionConfig::spot_margin`], it is a cross margin account instead. An order's
186/// [`MarginEffect`] (its request's, else the configuration's) decides what it does to the loans: a
187/// `Borrow` is lent the shortfall of the asset it pays with when the venue accepts it, or that
188/// asset's configured minimum borrow if it is more, and the venue holds the loan against it and
189/// repays it if the order ends unfilled; a `Repay` repays
190/// loans, interest first, from what its fill delivers; a `NoSideEffect` does neither. Every
191/// balance then reports its loan in [`Balance::margin`], and the account's
192/// holding of an asset is its `total` less what it owes. See
193/// [`SpotMarginConfig`] for how much it lends and what it does not model.
194///
195/// A restatement whose loan differs from the one last reported for its asset is followed at once
196/// by an [`AccountEventKind::MarginDebt`] stating the new debt, at the restatement's time, as a
197/// live margin venue streams one: [`MarginDebtKind::Borrow`] when principal rose,
198/// [`MarginDebtKind::Repay`] when principal or interest fell, and
199/// [`MarginDebtKind::Interest`] when only interest rose. The loans a configuration seeds are the
200/// debt it starts from, and are not reported.
201///
202/// Its loans are free unless it is given a [`MarginInterestModel`] through
203/// [`with_margin_interest`](Self::with_margin_interest). Then each loan is charged when it is
204/// taken, and at each of the model's boundaries while principal is owed in its asset:
205/// - **At borrow**, on the amount lent, when the order that borrows is accepted. The balance the
206///   order restates includes the interest. The [`AccountEventKind::CashFlow`] reporting it comes
207///   after the trade the loan pays for, once the position it funds exists, so that a consumer
208///   attributing the charge to that position (see `CashFlowAttributionStrategy` in the `rustrade`
209///   crate) finds it: `[balance.., trade, cash flow]`. An order that borrows when it rests on the
210///   book, or is held for a later bar or a fresh market, has traded nothing yet, so its cash flow
211///   waits: it is reported after the order's first fill, or with the order's end if it never fills
212///   (a cancel, an expiry, or a release it cannot afford). It is stamped at that instant, not at
213///   the charge, since a consumer applies no flow stamped before the position it belongs to.
214/// - **At a boundary**, on the principal owed when it falls, as `[balance, cash flow]` stamped at the
215///   boundary, when a driver moves the clock to or past it, as for holding costs (see
216///   `# Holding costs`). A loan taken at a boundary's instant is charged at borrow, not again at
217///   that boundary.
218///
219/// The interest is added to the loan, so `total` and `free` are unchanged and what a fill
220/// delivers repays it first. Each cash flow is a
221/// [`CashFlowKind::MarginInterest`], in the borrowed asset, negative, with no instrument, since a
222/// cross margin loan belongs to the account rather than to a position. A charge on a new loan
223/// carries [`InterestTiming::OnBorrow`] and one at a boundary [`InterestTiming::Periodic`]. A venue that books such
224/// interest at the start of its hour stamps it earlier than this venue does.
225///
226/// It liquidates the account only when the configuration says so, with
227/// [`SpotMarginConfig::with_liquidation`]: then, after each input, an account whose margin level
228/// has fallen to the configured level has its open orders cancelled and every debt closed by
229/// forced trades, reported as [`TradeOrigin::Liquidation`] fills, and is charged a fee. See
230/// [`SpotMarginLiquidation`](crate::margin::SpotMarginLiquidation).
231///
232/// [`Balance::margin`]: crate::balance::Balance::margin
233///
234/// # How a CFD is accounted
235/// This venue keeps one net position per CFD instrument, in contracts, and splits every fill
236/// against it: the part that opposes the position **closes** it, and the rest **opens** one. A
237/// single fill can do both, flipping a long into a short.
238/// - **Opening** posts the full notional, `price × quantity × contract_size`, as margin. This
239///   venue models no leverage, so that is the conservative reading, and the requirement a spot buy
240///   already carries.
241/// - **Closing** pays back the margin of the quantity closed at its average entry, together with
242///   the realised PnL: `closed × contract_size × (price − entry)` for a long, and the negative of
243///   that for a short. The proceeds count towards what the same fill must pay, so a flip is funded
244///   by the position it closes, and an account that opened with its whole balance can close.
245/// - **The fee** is charged on the whole quantity.
246///
247/// Over a round trip the balance therefore moves by the realised PnL less the fees, as the
248/// engine's own `Position` books it. The entry averages as the engine's does: an increase moves
249/// it to the volume-weighted average, a reduction keeps it, and a flip enters at the fill price.
250/// The position is reported by [`account_snapshot`](Self::account_snapshot), and a position that
251/// a configured `initial_state` reports for a CFD is where this venue starts.
252///
253/// Not modelled: liquidation of a CFD position, even on a venue that liquidates its spot margin
254/// account. A short closed at more than twice its entry has lost more than its margin, and pays
255/// the shortfall as a debit. Carry is charged only when configured: see
256/// `# Holding costs`.
257///
258/// [`Perpetual`](InstrumentKind::Perpetual), [`Future`](InstrumentKind::Future) and
259/// [`Option`](InstrumentKind::Option) need funding, margin and expiry settlement, none of which this
260/// venue models. They are **rejected at [`open_order`](Self::open_order)** with
261/// [`ApiError::InstrumentInvalid`] rather than filled. The check lives here, not only in the
262/// `rustrade` builder, because this type and its constructors are public: a consumer that
263/// constructs one directly bypasses every upstream gate, and the alternative to
264/// rejecting is filling a derivative as if it were deliverable stock.
265///
266/// # Holding costs
267/// By default this venue charges no carrying cost, so a CFD position is held for free and the
268/// balances and PnL of a run leave carry out. Configured with
269/// [`with_holding_costs`](Self::with_holding_costs), it charges each CFD position what its
270/// [`HoldingCostModel`]s say holding it costs: funding, a borrow fee, overnight financing.
271/// - **When.** At each model's boundaries, as the clock reaches them. This venue has no timer, so
272///   a boundary is charged when a driver next moves the clock to or past it, through
273///   [`advance_time`](Self::advance_time) or [`apply_market`](Self::apply_market). Several
274///   boundaries passed at once are each charged once, in time order, however far the clock
275///   jumped, and a clock moved back charges nothing (see [`advance_time`](Self::advance_time)). A
276///   driver that moves the clock to [`next_deadline`](Self::next_deadline) charges each one when
277///   it falls, as `SimRunner` does.
278///   Charging starts at the instant the clock first moves, so a position a configured
279///   `initial_state` seeds is charged from there, and never for the time before the run.
280/// - **What.** The position held across the boundary, valued at the mid of this venue's market,
281///   else its last price, else its entry price where there is no market, which is warned of
282///   once. A position opened at a boundary's instant is not charged for it; one closed at that
283///   instant is. Nothing is charged for part of a period: a position opened and closed between
284///   two boundaries pays nothing, as one held past no venue cutoff would.
285/// - **How.** Each charge moves the instrument's quote asset and is reported as
286///   `[balance, cash flow]`: the restated balance, then an
287///   [`AccountEventKind::CashFlow`] attributed to the instrument, both stamped at the boundary,
288///   with a [`CashFlowId`] from this venue's own sequence. The engine adds the flow to the
289///   position's `carry`, as it does a live venue's. A charge the account cannot cover is still
290///   made, and takes `free` below zero, with a WARN on the charge that does so, since this venue
291///   liquidates no CFD position.
292///
293/// # Locates and the short-sale restriction
294/// This venue lends no stock: a spot sell delivers base the account must hold, so a short sale of
295/// a spot instrument is refused with [`ApiError::BalanceInsufficient`].
296///
297/// A CFD short is allowed without limit by default. Configured with
298/// [`with_shortability`](Self::with_shortability), this venue asks its [`ShortabilityProvider`]
299/// about every order that would open or increase one, at the venue's clock, and refuses it with
300/// [`ApiError::BorrowRejected`] for the first of these that applies:
301/// - [`NotShortable`](BorrowRejectReason::NotShortable): the provider says the instrument is not
302///   shortable, or that it is delisted at or before the venue's clock
303///   ([`Shortability::delisting_at`](crate::shortability::Shortability::delisting_at)).
304/// - [`InventoryUnavailable`](BorrowRejectReason::InventoryUnavailable): the short would exceed
305///   what the provider says is available. That is the lender's total, so the account's own short
306///   counts against it, and so does every resting sell, as if it had filled. The refusal carries
307///   what is left in [`BorrowReject::available`].
308/// - [`ShortSaleRestricted`](BorrowRejectReason::ShortSaleRestricted): a short-sale restriction
309///   is in effect and the order is marketable: a market order, or a sell limit at or below the
310///   best bid (the last price, on a feed with no book). A limit above the bid rests, and so does
311///   any sell limit when there is neither a bid nor a last price. A market order the
312///   [`FillModel`] cannot price, such as one against a bid-only book under
313///   [`MidpointFillModel`](crate::fill::MidpointFillModel), is refused as unpriceable, as it would
314///   be unrestricted.
315///
316/// Only the short part of an order is gated: a flip from a long is measured by the short it
317/// opens, and a buy, or a sell that only reduces a long once every resting sell has filled, is
318/// never refused. Unknown values allow
319/// the short. An order is checked once, on arrival: a resting order fills whatever the provider
320/// says later, and the easy-to-borrow flag and fee rate are not read here, since a borrow fee is
321/// charged by a [`HoldingCostModel`].
322///
323/// # ⚠️ Caller obligations and known limitations
324/// - **Fund the quote asset of every instrument traded.** Every debit is quote-denominated except a
325///   spot sell, which debits base, and the excess fee of a fill whose fee exceeds what it receives
326///   (see `# How a spot fill is accounted`). A missing balance to debit is a **panic**, not an error: the
327///   balances are this venue's own fixture, so an absent one is a mis-specified test rather than a
328///   runtime condition. A margin account that lends the debit is the exception: it opens the
329///   balance with the loan (see `# Spot margin`). A CFD settling in an account currency that is
330///   not the quote asset still needs the **quote** asset funded — see below.
331/// - **`CfdContract::settlement_asset` is not settled in.** A CFD routinely cash-settles in an
332///   account currency that is not the quote asset (a GBP account trading a USD-quoted index), which
333///   requires a quote→settlement conversion rate. This venue has no rate source and will not invent
334///   one, so it debits and credits the quote asset and leaves the currency dimension unmodelled. The
335///   field is carried on the instrument so its description stays faithful, and the engine's own
336///   `InstrumentKind` — not this copy — drives PnL. A backtest whose result depends on the
337///   settlement currency needs a real execution client.
338/// - **The asset a spot fill receives is created at zero if the account never held it,** so only
339///   the paying asset of a first order needs funding. A sell of base the account holds none of is
340///   still a missing balance, since it is a debit.
341/// - **Which order kinds are accepted depends on the [`VenueRegime`].** [`OrderKind::Market`]
342///   always; [`OrderKind::Limit`] only on a [`MarketDriven`](VenueRegime::MarketDriven) venue;
343///   every other kind never.
344/// - **A resting order seeded by `initial_state` reserves nothing, because this venue never took
345///   it.** An account snapshot says an order is resting and says what the balances are, but not
346///   which order any held portion belongs to. Such an order still matches, and its fill debits the
347///   ledger then — so the seeded balances must cover it, exactly as they must cover a market order.
348///   They cannot? For spot that is a mis-specified fixture, and it **panics**, in the same class
349///   as an absent balance; a CFD order is instead cancelled at fill, as described under the
350///   reserved balances below. Cancelling one releases nothing and restates no balance, and neither does
351///   retiring one at its deadline. Such an order may also arrive **part-filled**, carrying an
352///   [`Open::filled_quantity`] this venue did not produce: only the remainder ever trades here,
353///   and a fill that completes it reports no `avg_price`, because the price of the part that
354///   traded elsewhere is not something this venue can know. An order **this** venue part-filled
355///   before resting is the same shape, and reports no `avg_price` for the same reason — the two
356///   fills were struck at different prices and this state carries one of them.
357/// - **A CFD position seeded by `initial_state` is where the venue's ledger starts, and must carry
358///   an entry price.** Closing it credits the margin and realised PnL measured from that price, so
359///   a position with `entry_price: None` is a mis-specified fixture, and construction **panics**.
360///
361/// # How an order is priced
362///
363/// Every fill is priced from **one** market, and which market that is depends only on the
364/// [`VenueRegime`]: this venue's own on a [`MarketDriven`](VenueRegime::MarketDriven) one, and the
365/// snapshot the request carried
366/// ([`RequestOpen::market`](crate::order::request::RequestOpen::market)) on a
367/// [`RequestPriced`](VenueRegime::RequestPriced) one. A limit order is only ever accepted on a
368/// `MarketDriven` venue, so it is always priced from the venue's own market; a market order follows
369/// the regime.
370///
371/// That a `MarketDriven` venue prices a market order from its own book — rather than from the
372/// sender's view at decision time — is what makes a driver's request latency price-relevant instead
373/// of a pure delivery delay. The market may move between an order being decided and arriving, and a
374/// fill is struck against the book it arrives to. `RequestOpen::market` is then decision-time
375/// provenance and nothing else, which is what it is documented to be, and the difference between it
376/// and the fill is implementation shortfall — a quantity that is identically zero when the two are
377/// the same snapshot.
378///
379/// A `MarketDriven` venue that has not been fed an instrument **rejects** a market order in it as
380/// unpriceable rather than falling back to the requester's snapshot: a venue with no market for an
381/// instrument cannot fill a market order in it, and saying so is the observable failure. Silently
382/// pricing from the requester's view would make the fill depend on which of the two happened to
383/// hold a price, and would re-admit the very `RequestPriced` behaviour the regime exists to
384/// distinguish.
385///
386/// Given that market, the three cases are:
387/// - **A market order**, and **a limit order marketable on arrival** — priced by the [`FillModel`],
388///   then moved against the order by the configured
389///   [`slippage`](crate::client::mock::MockExecutionConfig::slippage). A limit order is then
390///   *clamped* to its own limit: a buy never fills above it, a sell never below. Both are charged
391///   [`Liquidity::Taker`].
392/// - **A resting limit order** — fills at its own limit price exactly, and never reaches the
393///   [`FillModel`] or the slippage. Deriving a better price from the book would credit price
394///   improvement no resting order can obtain: a maker is paid the price it quoted, and improvement
395///   accrues to the aggressor that crossed it. Charged [`Liquidity::Maker`].
396/// - **A market order held for the next bar**, under
397///   [`MarketOrderTiming::NextBar`] — priced like a market order, but from the next bar's
398///   reference price rather than the market it arrived to: the [`FillModel`] is handed that price
399///   as the only one there is, and the slippage moves it. Charged [`Liquidity::Taker`].
400/// - **An order held on a stale market**, under
401///   [`StaleMarketPolicy::Wait`] — priced as if it arrived just after the fresh observation that
402///   releases it, by the rules above: a market order as a market order, a limit order as one
403///   marketable on arrival if that market crosses it and as a resting one if not.
404///
405/// The marketable and resting rules agree exactly at the crossing point — an order arriving when
406/// `best_ask == limit` fills at the limit either way — and diverge only where the order genuinely
407/// is, or is not, marketable. Marketability is judged on `best_ask`/`best_bid`, falling back to
408/// `last_price` when the feed supplies no book: with the default `LastPriceFillModel` and a
409/// trades-only feed there is no book at all, so a book-only rule would never match anything.
410///
411/// # What matching does not model
412/// - **No queue position.** Nothing rests ahead of this account's orders. [`OpenOrders`] ranks
413///   them by price then arrival, so *which* of them fills first is reproducible, but they are
414///   matched against a book with no other participants in it — an order that crosses is never
415///   waiting behind anyone.
416/// - **Only a taker is capped by size.** An order aggressing on arrival fills at most what
417///   [`VenueInstrumentMarket::depth`] says is on offer, and draws that down as it takes it. A
418///   **resting** order is not capped: when the market reaches it, it fills its whole remaining
419///   quantity at its limit however little traded there. Capping it honestly would need the volume
420///   that actually printed through the limit, which this venue's feed does not carry — a
421///   [`MarketSnapshot`] is a view of the market, not a record of what traded — and capping it by
422///   the size on the *opposite* side would be a number shaped like a cap that measures nothing a
423///   cap should.
424/// - **A capped taker's remainder is a maker, and makers are not capped.** So an order the book
425///   could only partly fill rests the rest, and the very next tick that still crosses it fills
426///   that remainder in full. The cap bounds what one order takes from one observation; it does not
427///   make liquidity scarce for longer than that.
428/// - **Absent size means unlimited, not unfillable.** A feed with no sizes — trades only, candles,
429///   a price-only export, or a [`RequestPriced`](VenueRegime::RequestPriced) venue, which has no
430///   market at all — caps nothing. See [`MarketDepth`].
431/// - **Depth is this account's own bound, not the market's.** It is reset by each
432///   [`apply_market`](Self::apply_market) rather than accumulated, so it stops two of this
433///   account's orders double-spending one observation's size and claims nothing about anyone
434///   else's.
435///
436/// # Which [`TimeInForce`] this venue honours
437/// An unsupported one is **rejected**, never quietly treated as good-until-cancelled: that would
438/// leave an order working that its sender asked to have cancelled.
439///
440/// | [`TimeInForce`] | [`OrderKind::Limit`] | [`OrderKind::Market`] |
441/// |---|---|---|
442/// | `GoodUntilCancelled { post_only: false }` | rests, or fills if marketable | fills |
443/// | `GoodUntilCancelled { post_only: true }` | rests, or **cancels** if marketable | **rejected** |
444/// | `ImmediateOrCancel` / `FillOrKill` | fills if marketable, else **cancels** | fills |
445/// | `GoodTillDate { expiry }` | rests until `expiry`, then **expires** | fills |
446/// | `GoodUntilEndOfDay` | **rejected** | fills |
447/// | `AtOpen` / `AtClose` | **rejected** | **rejected** |
448///
449/// A market order works only on arrival, which honours every time in force that bounds *how long*
450/// an order works — so most of the table is moot for one. (It fills in full wherever the book has
451/// the size; what it cannot fill is cancelled, since it has no limit price to rest at.) The two
452/// exceptions are real:
453/// `post_only` on a market order is a promise never to take liquidity, which is all a market order
454/// does; and `AtOpen`/`AtClose` say *when* an order executes rather than how long it lasts, so
455/// honouring them needs a session calendar this venue has not got. Filling one on arrival would be
456/// a wrong fill rather than an ignored flag.
457///
458/// ## A deadline is swept, not scheduled
459/// This venue has no timer. A [`GoodTillDate`](TimeInForce::GoodTillDate) order is retired when a
460/// driver next calls [`advance_time`](Self::advance_time) or [`apply_market`](Self::apply_market),
461/// whichever comes first, and the sweep spans every instrument rather than only the one that
462/// ticked. A deadline falling after the last such call is never reached at all — see
463/// [`advance_time`](Self::advance_time) for what that means for a caller. A driver that keeps a
464/// timeline of its own can advance the clock to each [`next_deadline`](Self::next_deadline) as it
465/// falls, as `SimRunner` does, so that a deadline is reached at its own instant.
466///
467/// A deadline is an unconditional cutoff. An order reaching it is retired even if the very tick
468/// that reached it would have crossed it, and an order that arrives already past its deadline is
469/// retired without resting and without trading — otherwise whether an expired order traded would
470/// depend on where the book happened to be.
471///
472/// # A client order id names one working order at a time
473/// An open request whose [`ClientOrderId`] already names an order resting here is **rejected** with
474/// [`ApiError::DuplicateClientOrderId`], whatever its kind, and the resting order is untouched: its
475/// price, queue position, deadline and reservation stay as they were. Only an order that rests
476/// holds its id; one that fills or is cancelled on arrival never does.
477///
478/// Once an order has ended its id is free, and the next order under it replaces what this venue
479/// remembers of the earlier one. [`orders_ended`](Self::orders_ended), the rejection of a cancel
480/// and [`account_snapshot`](Self::account_snapshot) therefore report the latest order under each
481/// id.
482///
483/// The check follows the static ones on kind, time in force and instrument, so a malformed request
484/// is reported as malformed, and comes before anything that touches the ledger.
485///
486/// [`OpenOrders`]: super::orders::OpenOrders
487///
488/// # Reserved balances
489/// The ledger distinguishes held from spendable: `free` is what an order may draw on, `total` is
490/// what the account holds, and the difference is held against something. An `initial_state` copied
491/// from a live account with margin reserved is therefore usable as configured — it was rejected
492/// outright while every order filled on arrival and the ledger could not represent the state.
493///
494/// Every arrival is **one** commitment against the ledger ([`AccountState::commit`]) and therefore
495/// **one** balance restatement, whichever of the three shapes it has: an order that fills outright
496/// settles and holds nothing; one that rests holds and settles nothing; one the book fills in part
497/// does both at once, settling what traded and holding against the remainder it leaves resting. An
498/// order that rests emits one more restatement later, when the market reaches it and the hold is
499/// settled — never an intermediate balance the account did not hold. A configured reservation is
500/// carried through untouched: settling lowers `total` by the settled amount and leaves the rest
501/// held.
502///
503/// **An order is funded whole or refused whole.** The two legs of a part-filled arrival are asked
504/// for together, so an account that cannot cover both has the order rejected with nothing moved.
505/// It is not filled for the part it could afford: choosing that would be this venue substituting a
506/// disposition for the one the sender asked for. Note the consequence, which is easy to be
507/// surprised by — a capped order costs *more* than the same order filling outright, because its
508/// remainder is held at the order's own limit while the fill struck a better price, so an order
509/// can be refused for a size the book was willing to sell it.
510///
511/// For spot, the reservation is **exactly** what the fill will settle, not a conservative
512/// over-estimate: both come from one computation at the order's own limit and its own liquidity
513/// side. A conservative reservation would have to be released and re-debited on the fill,
514/// producing two or three restatements for one fill and reporting balances the account never
515/// held, while [`Balance::used`](crate::balance::Balance::used) misreported for the order's whole
516/// life.
517///
518/// A resting CFD order holds only what its fill would cost **net of what the fill pays back**,
519/// computed against the position when it rests. An order that only reduces a position therefore
520/// holds nothing. The position can move before the order fills — another order may close what this
521/// one was going to — so a CFD fill is costed again when it fills: its hold is released and the
522/// fill settled afresh, still as one balance restatement. If the account can no longer afford it,
523/// the order is **cancelled** rather than filled, as a venue re-checking margin at fill time would
524/// cancel it. That includes an order a configured `initial_state` seeded, which for an ordinary
525/// spot order would be a panic.
526///
527/// A resting spot order holds what it pays with, and nothing for a fee that comes out of what it
528/// receives. A fill whose fee exceeds what it receives takes the excess from the asset it receives,
529/// so it is costed afresh when it fills, as a CFD order is, and cancelled, seeded or not, if that
530/// asset cannot cover the excess.
531///
532/// [`AccountState::commit`]: crate::exchange::mock::account::AccountState::commit
533#[derive(Debug)]
534pub struct SimulatedVenue {
535    pub exchange: ExchangeId,
536    pub fee_model: FeeModelConfig,
537    /// Which asset a spot fill's fee is charged in. See `# How a spot fill is accounted`.
538    pub spot_fee_asset: SpotFeeAsset,
539    /// Prices each taker fill. Built from the configuration's
540    /// [`fill_model`](MockExecutionConfig::fill_model) unless replaced by
541    /// [`with_fill_model`](Self::with_fill_model).
542    fill_model: Arc<dyn FillModel>,
543    /// Moves each taker fill against the order, from its own seeded stream. Private: the stream's
544    /// position is what makes a run reproducible from its seed, so nothing else may draw from it.
545    slippage: Slippage,
546    /// Every instrument this venue trades, as constructed. Private, and never changed after
547    /// construction: [`spot_pairs`](Self::spot_pairs) indexes it, and resting orders were accepted
548    /// against it. Read through [`instruments`](Self::instruments).
549    instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
550    /// The spot instruments of [`instruments`](Self::instruments), by the two assets each pairs.
551    /// What values an account and picks a liquidation's legs without scanning every instrument.
552    spot_pairs: SpotPairs,
553    pub account: AccountState,
554    /// This venue's own view of each instrument it trades, as of the last
555    /// [`apply_market`](Self::apply_market).
556    ///
557    /// Empty unless a driver feeds it — see [`apply_market`](Self::apply_market) for which drivers
558    /// do. Read through [`market`](Self::market).
559    market: FnvHashMap<InstrumentNameExchange, VenueInstrumentMarket>,
560    /// The net position of each cash-settled instrument this account holds one in.
561    ///
562    /// What decides how much of a CFD fill closes a position, returning its margin and realising
563    /// its PnL, rather than posting new margin. Flat instruments have no entry. Private: the
564    /// ledger's credits are computed from it, so editing it would pay out margin that was never
565    /// posted. Reported through [`account_snapshot`](Self::account_snapshot).
566    positions: FnvHashMap<InstrumentNameExchange, NetPosition>,
567    /// Monotone `OrderId` source. Private: resetting it mints duplicate ids, which a consumer
568    /// keying a position on an order would silently merge.
569    ///
570    /// Counts every order this venue booked, including those that retired without trading, so it
571    /// is also what [`order_sequence`](Self::order_sequence) reports.
572    order_sequence: u64,
573    /// The `OrderId` the first booked order is minted with; each later one counts up from it.
574    ///
575    /// Zero, unless the seeded account state already carries a decimal id, in which case it is one
576    /// past the highest such id, so no minted id can repeat a seeded one. A seeded id that is not
577    /// decimal can never equal a minted one, so it is left out. `u128` so that one past a seeded
578    /// `u64::MAX`, and every id counted up from it, stays representable.
579    order_id_start: u128,
580    /// Monotone `TradeId` source, independent of [`order_sequence`](Self::order_sequence).
581    ///
582    /// Separate because one order can print more than once — a taker the book fills in part rests
583    /// the remainder and prints again when it is crossed — so an id derived from the order's would
584    /// collide between the two. A real venue mints trade ids independently for the same reason;
585    /// the link back to the order is [`Trade::order_id`], which is typed rather than parsed.
586    trade_sequence: u64,
587    /// Read via [`time_exchange`](Self::time_exchange), and set only by
588    /// [`advance_time`](Self::advance_time), which never sets it below
589    /// [`accrued_through`](Self::accrued_through). An [`apply_market`](Self::apply_market) since
590    /// can have raised that mark past it, until the next `advance_time` catches up.
591    time_exchange_latest: DateTime<Utc>,
592    /// Which price source this venue has. Private: it is fixed at construction, and a consumer
593    /// flipping it on a venue already holding resting orders would leave them unmatchable with no
594    /// way to find out. Read through [`regime`](Self::regime).
595    regime: VenueRegime,
596    /// What holding a position costs over time. Empty unless set by
597    /// [`with_holding_costs`](Self::with_holding_costs), and then fixed: swapping a model under
598    /// a position mid-run would leave its charges to date priced by one and the rest by another.
599    holding_costs: Vec<Arc<dyn HoldingCostModel>>,
600    /// The instant up to which every holding-cost and interest boundary has been charged, or
601    /// `None` before this venue's clock first moved.
602    ///
603    /// A boundary is charged when the clock first reaches it and never again, which is what makes
604    /// a charge idempotent however often the clock is moved to one instant. It starts unset
605    /// rather than at the epoch, so a venue charges from the first instant it is driven to, not
606    /// for every boundary since 1970.
607    ///
608    /// Both inputs raise it and nothing lowers it, so it is also the latest instant this venue's
609    /// clock has reached, which both inputs hold the instant they are given to — see
610    /// [`not_before_reached`](Self::not_before_reached).
611    accrued_through: Option<DateTime<Utc>>,
612    /// The earliest holding-cost boundary after [`accrued_through`](Self::accrued_through) of any
613    /// position held, or `None` if there is none.
614    ///
615    /// Cached because both [`accrue`](Self::accrue), on every input, and
616    /// [`next_deadline`](Self::next_deadline) need it, and asking every model for its next
617    /// boundary — a time-zone conversion for a daily cutoff — on every market tick would cost a
618    /// long backtest real time. It can change only when a position does or a boundary is charged,
619    /// so those are where it is recomputed.
620    next_charge: Option<DateTime<Utc>>,
621    /// Monotone [`CashFlowId`] source for the charges this venue posts.
622    cash_flow_sequence: u64,
623    /// Whether a holding cost has already been valued at a position's entry price for want of a
624    /// market, which is warned of once per venue rather than at every boundary.
625    valued_at_entry: bool,
626    /// What this venue may lend for a CFD short, or `None` to lend without limit. Set by
627    /// [`with_shortability`](Self::with_shortability).
628    shortability: Option<Arc<dyn ShortabilityProvider>>,
629    /// When a market order fills: on arrival, or held for its instrument's next bar. Fixed at
630    /// construction from [`MockExecutionConfig::market_order_timing`]: switching it while orders
631    /// are held would leave them waiting for a bar that no longer releases them.
632    market_order_timing: MarketOrderTiming,
633    /// What an order does on a stale market. Fixed at construction from
634    /// [`MockExecutionConfig::stale_market`], for the same reason as
635    /// [`market_order_timing`](Self::market_order_timing).
636    stale_market: StaleMarketPolicy,
637    /// What this venue lends a spot order, or `None` for a cash account. Fixed at construction
638    /// from [`MockExecutionConfig::spot_margin`]: turning it off would strand the loans it made.
639    spot_margin: Option<SpotMarginConfig>,
640    /// The third asset this venue charges spot fees in, or `None` to charge every fee under
641    /// [`spot_fee_asset`](Self::spot_fee_asset). Fixed at construction from
642    /// [`MockExecutionConfig::fee_asset`].
643    fee_asset: Option<FeeAssetConfig>,
644    /// What this venue's loans cost in interest. `None` unless set by
645    /// [`with_margin_interest`](Self::with_margin_interest), and then fixed, for the same reason
646    /// as [`holding_costs`](Self::holding_costs).
647    margin_interest: Option<Arc<dyn MarginInterestModel>>,
648    /// The earliest interest boundary after [`accrued_through`](Self::accrued_through) of any
649    /// asset the account owes principal in, or `None` if there is none.
650    ///
651    /// Cached, like [`next_charge`](Self::next_charge), and recomputed when a boundary is charged
652    /// or when [`AccountState::loans_changed`] says which assets owe principal may have changed.
653    next_interest: Option<DateTime<Utc>>,
654    /// The [`AccountState::loans_changed`] count [`next_interest`](Self::next_interest) was
655    /// computed at.
656    next_interest_loans: u64,
657    /// The debt last reported for each asset of a spot-margin account, so that a balance
658    /// restatement whose debt differs is followed by an [`AccountEventKind::MarginDebt`]. Seeded
659    /// with the debt the account starts with.
660    reported_debt: FnvHashMap<AssetNameExchange, MarginDetails>,
661    /// Where a spot margin account stands against its configured
662    /// [`SpotMarginLiquidation`](crate::margin::SpotMarginLiquidation): whether a liquidation is
663    /// under way, and whether a margin call is outstanding.
664    liquidation: liquidation::LiquidationState,
665}
666
667impl SimulatedVenue {
668    /// A venue with no market feed, which prices each market order from the snapshot its own
669    /// request carries and accepts no [`OrderKind::Limit`].
670    ///
671    /// See [`VenueRegime::RequestPriced`], and [`new_market_driven`](Self::new_market_driven) for
672    /// the regime that accepts limits.
673    ///
674    /// # Panics
675    /// - If `initial_state` holds an open CFD position with no entry price: see the caller
676    ///   obligations on [`SimulatedVenue`].
677    /// - If `initial_state` lists one client order id more than once among its open and cancelled
678    ///   orders: see [`DuplicateSeededOrder`](super::account::DuplicateSeededOrder).
679    pub fn new(
680        config: &MockExecutionConfig,
681        instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
682    ) -> Self {
683        Self::with_regime(config, instruments, VenueRegime::RequestPriced)
684    }
685
686    /// A venue whose driver feeds [`apply_market`](Self::apply_market), so limit orders are
687    /// accepted and a resting one is matched against that feed.
688    ///
689    /// Constructing one and then never calling `apply_market` produces a venue that accepts limit
690    /// orders and can never match them, which is the state the regime exists to prevent — so this
691    /// constructor is a driver's assertion that it will feed the venue, not a configuration knob.
692    ///
693    /// See [`VenueRegime::MarketDriven`].
694    ///
695    /// # Panics
696    /// - If `initial_state` holds an open CFD position with no entry price: see the caller
697    ///   obligations on [`SimulatedVenue`].
698    /// - If `initial_state` lists one client order id more than once among its open and cancelled
699    ///   orders: see [`DuplicateSeededOrder`](super::account::DuplicateSeededOrder).
700    pub fn new_market_driven(
701        config: &MockExecutionConfig,
702        instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
703    ) -> Self {
704        Self::with_regime(config, instruments, VenueRegime::MarketDriven)
705    }
706
707    fn with_regime(
708        config: &MockExecutionConfig,
709        instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
710        regime: VenueRegime,
711    ) -> Self {
712        let positions = seeded_positions(&config.initial_state, &instruments);
713        let mut account = match AccountState::try_from(config.initial_state.clone()) {
714            Ok(account) => account,
715            Err(error) => panic!("invalid MockExecutionConfig::initial_state: {error}"),
716        };
717        if config.spot_margin.is_some() {
718            account.enable_margin();
719        }
720        let order_id_start = first_unseeded_order_id(&account);
721        let reported_debt = account
722            .balances()
723            .filter_map(|balance| Some((balance.asset.clone(), balance.balance.margin?)))
724            .collect();
725
726        Self {
727            exchange: config.mocked_exchange,
728            fee_model: config.fee_model,
729            spot_fee_asset: config.spot_fee_asset,
730            fill_model: Arc::new(config.fill_model),
731            slippage: Slippage::new(config.slippage),
732            spot_pairs: SpotPairs::new(&instruments),
733            instruments,
734            account,
735            market: FnvHashMap::default(),
736            positions,
737            order_sequence: 0,
738            order_id_start,
739            trade_sequence: 0,
740            time_exchange_latest: Default::default(),
741            regime,
742            holding_costs: Vec::new(),
743            accrued_through: None,
744            next_charge: None,
745            cash_flow_sequence: 0,
746            valued_at_entry: false,
747            shortability: None,
748            market_order_timing: config.market_order_timing,
749            stale_market: config.stale_market,
750            spot_margin: config.spot_margin.clone(),
751            fee_asset: config.fee_asset.clone(),
752            margin_interest: None,
753            next_interest: None,
754            next_interest_loans: 0,
755            reported_debt,
756            liquidation: Default::default(),
757        }
758    }
759
760    /// This venue, charging each open position what `models` say holding it costs.
761    ///
762    /// Each model is asked for its boundaries on every instrument this venue holds a position in,
763    /// and charges the position held across each one, as described on [`HoldingCostModel`]. Only
764    /// a cash-settled position is charged: a spot holding is a balance on this venue, not a
765    /// position. See the type's `# Holding costs` for when a charge is made and how it is
766    /// reported.
767    #[must_use]
768    pub fn with_holding_costs(mut self, models: Vec<Arc<dyn HoldingCostModel>>) -> Self {
769        self.holding_costs = models;
770        self.refresh_next_charge();
771        self
772    }
773
774    /// This venue, charging interest on its spot margin loans as `model` says.
775    ///
776    /// Each loan is charged when it is taken, on the amount lent, and at each of the model's
777    /// boundaries while the account owes principal in its asset, on that principal, as described
778    /// on [`MarginInterestModel`]. See the type's `# Spot margin` for how a charge is reported.
779    ///
780    /// # Panics
781    /// If this venue has no [`MockExecutionConfig::spot_margin`]: a cash account takes no loans,
782    /// so a model given to one would silently charge nothing. Or if `model` breaks its contract
783    /// with a boundary not after the instant it was asked from, for a loan the account already
784    /// owes.
785    #[must_use]
786    pub fn with_margin_interest(mut self, model: Arc<dyn MarginInterestModel>) -> Self {
787        assert!(
788            self.spot_margin.is_some(),
789            "SimulatedVenue given a margin interest model without MockExecutionConfig::spot_margin: \
790             a cash account takes no loans to charge"
791        );
792        self.margin_interest = Some(model);
793        self.refresh_next_interest();
794        self
795    }
796
797    /// This venue, pricing each taker fill with `model` rather than the configured
798    /// [`fill_model`](MockExecutionConfig::fill_model).
799    ///
800    /// The way to plug in a model of your own, such as a size-aware impact model reading
801    /// [`FillContext::quantity`]. The configured
802    /// [`slippage`](MockExecutionConfig::slippage) still applies to whatever price it returns. See
803    /// [`FillModel`] for what an implementation must uphold.
804    ///
805    /// The model is not part of the configuration, so a serialised configuration no longer says
806    /// what priced the fills: its `fill_model` is unused. A venue that
807    /// [`MockExchange::new`](super::MockExchange::new) drives is built from its configuration
808    /// alone, so it takes no custom model.
809    #[must_use]
810    pub fn with_fill_model(mut self, model: Arc<dyn FillModel>) -> Self {
811        self.fill_model = model;
812        self
813    }
814
815    /// This venue, refusing a CFD short that `provider` does not allow.
816    ///
817    /// Asked on the arrival of every order that would open or increase a short, as described in
818    /// the type's `# Locates and the short-sale restriction`.
819    #[must_use]
820    pub fn with_shortability(mut self, provider: Arc<dyn ShortabilityProvider>) -> Self {
821        self.shortability = Some(provider);
822        self
823    }
824
825    /// The next instant at which this venue has something to do of its own accord: an open order
826    /// reaching its deadline, a holding-cost boundary of a position it holds, or an interest
827    /// boundary of a loan it made.
828    ///
829    /// For a driver with a timeline of its own, such as `SimRunner`, which moves the clock to this
830    /// instant so that an expiry or a charge happens when it is due rather than when the next
831    /// market event or request happens to arrive. `None` when nothing is due.
832    ///
833    /// Recompute it after anything that moves this venue: an order, a fill, or the clock. A
834    /// holding-cost or interest boundary is reported only once the venue's clock has first moved,
835    /// since that is where charging starts.
836    pub fn next_deadline(&self) -> Option<DateTime<Utc>> {
837        let expiry = self.account.orders().next_expiry();
838        expiry
839            .into_iter()
840            .chain(self.next_charge)
841            .chain(self.next_interest)
842            .min()
843    }
844
845    /// Recomputes [`next_charge`](Self::next_charge) from the positions held now.
846    fn refresh_next_charge(&mut self) {
847        self.next_charge = self.accrued_through.and_then(|after| {
848            self.positions
849                .keys()
850                .flat_map(|instrument| {
851                    self.holding_costs
852                        .iter()
853                        .filter_map(move |model| next_boundary(model.as_ref(), instrument, after))
854                })
855                .min()
856        });
857    }
858
859    /// Recomputes [`next_interest`](Self::next_interest) from the loans owed now.
860    fn refresh_next_interest(&mut self) {
861        self.next_interest_loans = self.account.loans_changed();
862        let Some(model) = self.margin_interest.as_deref() else {
863            return;
864        };
865        self.next_interest = self.accrued_through.and_then(|after| {
866            self.account
867                .loans()
868                .filter_map(|(asset, _)| next_interest_boundary(model, asset, after))
869                .min()
870        });
871    }
872
873    /// Recomputes [`next_interest`](Self::next_interest) if the assets owing principal may have
874    /// changed since it was last computed. Called at the end of every input that can lend or
875    /// repay.
876    fn refresh_next_interest_if_loans_changed(&mut self) {
877        if self.account.loans_changed() != self.next_interest_loans {
878            self.refresh_next_interest();
879        }
880    }
881
882    /// Which price source this venue has, and so which order kinds it accepts.
883    pub fn regime(&self) -> VenueRegime {
884        self.regime
885    }
886
887    /// Sets the instant this venue stamps on everything it produces from now on, retiring any
888    /// order whose deadline that instant has reached.
889    ///
890    /// The caller owns the latency model: a driver simulating a network delay passes an instant
891    /// already offset by it. The venue itself models no delay.
892    ///
893    /// # Deadlines are swept from here and from [`apply_market`](Self::apply_market), never on a
894    /// clock of their own
895    /// A [`TimeInForce::GoodTillDate`] order must stop working at the instant it says, not at
896    /// whenever the next market tick happens to arrive -- otherwise a cancel sent after the
897    /// nominal deadline would succeed against an order that should already have been retired, and
898    /// an instrument that stops ticking would hold its expired orders open indefinitely. So both
899    /// of this venue's inputs sweep.
900    ///
901    /// The sweep is **not** scoped to any instrument: a deadline is a property of the clock, not of
902    /// a book. An order on a quiet instrument is retired by activity anywhere on the venue.
903    ///
904    /// ## The residual limitation, stated plainly
905    /// This venue has no timer. Nothing retires an order until a driver next advances the clock or
906    /// feeds a market, so a deadline falling after the last such call is never reached at all. A
907    /// driver that stops driving leaves orders working past their deadline, and that is a property
908    /// of the simulation rather than something a caller can configure away. A driver that wants
909    /// each deadline reached at its own instant advances the clock to
910    /// [`next_deadline`](Self::next_deadline).
911    ///
912    /// # Returns the expiries it caused, which a driver must deliver
913    /// Per retired order the events are `[balance, order]` -- the released reservation, then the
914    /// terminal [`Expired`] snapshot -- or `[order]` alone for an order this venue never took a
915    /// reservation for, exactly as [`cancel_order`](Self::cancel_order) reports one.
916    ///
917    /// `#[must_use]` because a driver that advances the clock and drops the result has silently
918    /// eaten the expiries, leaving its client holding orders the venue no longer has and a balance
919    /// it has already released.
920    ///
921    /// # And the holding costs and interest it reached
922    /// Every holding-cost and interest boundary between the previous instant and this one is
923    /// charged first, in time order, each as `[balance, cash flow]` stamped at its boundary — see
924    /// the type's `# Holding costs` and `# Spot margin`. They precede the expiries, which are
925    /// stamped at `time_exchange`.
926    ///
927    /// # And the liquidation it caused
928    /// Last, a spot margin account configured with a
929    /// [`SpotMarginLiquidation`](crate::margin::SpotMarginLiquidation) is checked at
930    /// `time_exchange`, and the notices, cancels, forced trades and fee that check causes follow
931    /// everything else, stamped there too.
932    ///
933    /// # The clock never moves back
934    /// An instant earlier than the latest this venue has reached, through this call or through
935    /// [`apply_market`](Self::apply_market), is taken as that latest instant. It charges nothing
936    /// again, and the balances it restamps and whatever a request after it produces are stamped no
937    /// earlier than what this venue has already reported. A consumer keeping the latest stamp of
938    /// each balance would otherwise drop such a restatement as older than one it holds.
939    ///
940    /// The venue clamps the instant itself, so this holds whichever driver calls it, including one
941    /// stamping requests from a wall clock that stepped back. A request that arrives behind a later
942    /// market tick is therefore stamped at that tick. [`apply_market`](Self::apply_market) clamps
943    /// its tick the same way.
944    ///
945    /// # A forward jump charges every boundary it crosses
946    /// However far one call moves the clock, each boundary in between is charged once, at its own
947    /// stamp, on what was held and owed across it: a live paper venue whose host resumes after
948    /// hours asleep charges every hour it slept through. That is what a venue that kept running
949    /// would have charged, since funding and interest fall due at a boundary whether or not anyone
950    /// is watching. Each is valued at the market as it stood before the jump, since this venue saw
951    /// none in between.
952    ///
953    /// # Panics
954    /// If a configured [`HoldingCostModel`] or [`MarginInterestModel`] breaks its contract: a
955    /// boundary not after the instant it was asked from, or a negative interest.
956    #[must_use]
957    pub fn advance_time(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
958        let time_exchange = self.not_before_reached(time_exchange);
959        // Charged before the balances are restamped, so each restatement it emits keeps its own
960        // boundary's stamp and the ledger still ends stamped at `time_exchange`.
961        let mut events = self.accrue(time_exchange);
962        self.time_exchange_latest = time_exchange;
963        self.account.update_time_exchange(time_exchange);
964        events.append(&mut self.sweep_expired(time_exchange));
965        self.check_margin(time_exchange, &mut events);
966        self.refresh_next_interest_if_loans_changed();
967        self.report_debt_changes(&mut events);
968        events
969    }
970
971    /// `time_exchange`, or the latest instant this venue has reached if that is later, which is
972    /// how both inputs keep the clock from moving back. See `advance_time`'s
973    /// `# The clock never moves back`.
974    fn not_before_reached(&self, time_exchange: DateTime<Utc>) -> DateTime<Utc> {
975        self.accrued_through
976            .map_or(time_exchange, |reached| reached.max(time_exchange))
977    }
978
979    /// Charges every holding-cost and interest boundary in `(accrued_through, now]`, the first
980    /// against the position held across it and the second against the principal owed when it falls,
981    /// returning `[balance, cash flow]` per charge. In boundary order, and at one boundary the
982    /// holding costs, by instrument then model, before the interest, by asset.
983    ///
984    /// The positions and the principal owed cannot have changed since `accrued_through`: this
985    /// venue's state moves only on its inputs, and each input charges what is due before it fills
986    /// anything. So what is held and owed now is what was held and owed across every boundary
987    /// being charged, and a gap of several boundaries is caught up exactly. Interest charged at
988    /// one boundary is not principal, so it does not change what the next one charges.
989    ///
990    /// # Panics
991    /// If a model returns a boundary that is not after the instant it was asked from, or a
992    /// negative interest — see the contracts of [`HoldingCostModel`] and [`MarginInterestModel`].
993    fn accrue(&mut self, now: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
994        let after = match self.accrued_through {
995            // The clock's first move: charging starts here.
996            None => {
997                self.accrued_through = Some(now);
998                self.refresh_next_charge();
999                self.refresh_next_interest();
1000                return Vec::new();
1001            }
1002            Some(after) if now <= after => return Vec::new(),
1003            Some(after) => after,
1004        };
1005        // Every input refreshes this as it ends, but the ledger is public, so one changed since is
1006        // caught here, while the cache is still computed from `after`.
1007        self.refresh_next_interest_if_loans_changed();
1008        self.accrued_through = Some(now);
1009
1010        // Nothing falls in `(after, now]`, so the first boundary after `now` is still the first
1011        // after `after`, and the caches stay right.
1012        let falls = |next: Option<DateTime<Utc>>| next.is_some_and(|due| due <= now);
1013        if !falls(self.next_charge) && !falls(self.next_interest) {
1014            return Vec::new();
1015        }
1016
1017        let mut due = Vec::new();
1018        for instrument in self.positions.keys() {
1019            for (index, model) in self.holding_costs.iter().enumerate() {
1020                let boundaries = boundaries_through(after, now, |from| {
1021                    next_boundary(model.as_ref(), instrument, from)
1022                });
1023                due.extend(
1024                    boundaries.map(|boundary| (boundary, Due::Holding(instrument.clone(), index))),
1025                );
1026            }
1027        }
1028        if let Some(model) = self.margin_interest.as_deref() {
1029            for (asset, principal) in self.account.loans() {
1030                let boundaries = boundaries_through(after, now, |from| {
1031                    next_interest_boundary(model, asset, from)
1032                });
1033                due.extend(
1034                    boundaries.map(|boundary| (boundary, Due::Interest(asset.clone(), principal))),
1035                );
1036            }
1037        }
1038        // Positions and balances are hashed, so their iteration order is arbitrary: sorted, so
1039        // one run's charges arrive in the same order as another's.
1040        due.sort_unstable();
1041
1042        let mut events = Vec::with_capacity(due.len() * 2);
1043        for (boundary, due) in due {
1044            match due {
1045                Due::Holding(instrument, index) => {
1046                    self.charge_holding_cost(boundary, &instrument, index, &mut events);
1047                }
1048                Due::Interest(asset, principal) => {
1049                    self.charge_interest_boundary(boundary, &asset, principal, &mut events);
1050                }
1051            }
1052        }
1053        self.refresh_next_charge();
1054        self.refresh_next_interest();
1055        events
1056    }
1057
1058    /// Charges the interest on `principal` of `asset`, owed when `boundary` falls, pushing
1059    /// `[balance, cash flow]` onto `events`, or nothing if the model charges nothing.
1060    fn charge_interest_boundary(
1061        &mut self,
1062        boundary: DateTime<Utc>,
1063        asset: &AssetNameExchange,
1064        principal: Decimal,
1065        events: &mut Vec<UnindexedAccountEvent>,
1066    ) {
1067        if let Some((balance, flow)) =
1068            self.charge_interest(asset, principal, InterestDue::Boundary(boundary))
1069        {
1070            events.push(self.build_account_event(Snapshot(balance)));
1071            events.push(self.build_account_event(AccountEventKind::CashFlow(flow)));
1072        }
1073    }
1074
1075    /// Charges a loan of `principal` of `asset` what the interest model says is `due`, returning
1076    /// the restated balance and the cash flow that reports the charge, stamped when it was due.
1077    /// `None` when there is no model, or it charges nothing.
1078    ///
1079    /// # Panics
1080    /// If the model charges a negative interest — see [`MarginInterestModel`]'s contract.
1081    fn charge_interest(
1082        &mut self,
1083        asset: &AssetNameExchange,
1084        principal: Decimal,
1085        due: InterestDue,
1086    ) -> Option<(
1087        AssetBalance<AssetNameExchange>,
1088        CashFlow<AssetNameExchange, InstrumentNameExchange>,
1089    )> {
1090        let model = self.margin_interest.as_deref()?;
1091        let charge = model.charge(asset, principal, due)?;
1092        assert!(
1093            charge.interest >= Decimal::ZERO,
1094            "MarginInterestModel {model:?} charged negative interest {} on {principal} {asset} \
1095             due {due:?}",
1096            charge.interest
1097        );
1098        if charge.interest.is_zero() {
1099            return None;
1100        }
1101
1102        let time = due.time();
1103        let balance = self.account.charge_interest(asset, charge.interest, time);
1104        let id = CashFlowId::new(self.cash_flow_sequence.to_smolstr());
1105        self.cash_flow_sequence += 1;
1106
1107        let flow = CashFlow::new(
1108            CashFlowKind::MarginInterest {
1109                rate: charge.rate,
1110                principal: Some(principal),
1111                timing: Some(match due {
1112                    InterestDue::Borrow(_) => InterestTiming::OnBorrow,
1113                    InterestDue::Boundary(_) => InterestTiming::Periodic,
1114                }),
1115            },
1116            asset.clone(),
1117            -charge.interest,
1118            None,
1119            time,
1120            Some(id),
1121        );
1122        Some((balance, flow))
1123    }
1124
1125    /// Charges one model's cost of the position in `instrument` at `boundary`, pushing
1126    /// `[balance, cash flow]` onto `events`, or nothing if the model charges nothing.
1127    fn charge_holding_cost(
1128        &mut self,
1129        boundary: DateTime<Utc>,
1130        instrument: &InstrumentNameExchange,
1131        model: usize,
1132        events: &mut Vec<UnindexedAccountEvent>,
1133    ) {
1134        let (Some(position), Some(terms)) = (
1135            self.positions.get(instrument).copied(),
1136            self.instruments.get(instrument),
1137        ) else {
1138            return;
1139        };
1140        let quote = terms.underlying.quote.clone();
1141        let contract_size = terms.kind.contract_size();
1142        let price = self.valuation(instrument, position);
1143
1144        let held = HeldPosition::new(
1145            instrument,
1146            position.quantity,
1147            contract_size,
1148            position.entry,
1149            price,
1150        );
1151        let Some(charge) = self.holding_costs[model].charge(&held, boundary) else {
1152            return;
1153        };
1154        if charge.amount.is_zero() {
1155            return;
1156        }
1157
1158        let balance = self.account.charge(&quote, charge.amount, boundary);
1159        // Warned when `free` goes negative, not at every charge while it stays there: an
1160        // underwater account charged hourly would otherwise warn every simulated hour. Each charge
1161        // is still reported as a cash flow.
1162        let free = balance.balance.free;
1163        if free.is_sign_negative() && !(free - charge.amount).is_sign_negative() {
1164            warn!(
1165                exchange = %self.exchange,
1166                %instrument,
1167                asset = %quote,
1168                amount = %charge.amount,
1169                free = %balance.balance.free,
1170                %boundary,
1171                "SimulatedVenue charged a holding cost the account could not cover: free balance \
1172                 is negative, and this venue liquidates no CFD position"
1173            );
1174        }
1175
1176        let id = CashFlowId::new(self.cash_flow_sequence.to_smolstr());
1177        self.cash_flow_sequence += 1;
1178
1179        events.push(self.build_account_event(Snapshot(balance)));
1180        events.push(
1181            self.build_account_event(AccountEventKind::CashFlow(CashFlow::new(
1182                charge.kind,
1183                quote,
1184                charge.amount,
1185                Some(instrument.clone()),
1186                boundary,
1187                Some(id),
1188            ))),
1189        );
1190    }
1191
1192    /// The price a holding cost values `position` in `instrument` at: the mid of this venue's
1193    /// market, else its last price, else the position's entry price.
1194    ///
1195    /// The entry price is a degraded valuation, used only where this venue has no market for the
1196    /// instrument, as on a [`RequestPriced`](VenueRegime::RequestPriced) venue. It is warned of
1197    /// once.
1198    fn valuation(&mut self, instrument: &InstrumentNameExchange, position: NetPosition) -> Decimal {
1199        let price = self
1200            .market(instrument)
1201            .and_then(|market| mid_or_last(&market.snapshot));
1202
1203        price.unwrap_or_else(|| {
1204            if !self.valued_at_entry {
1205                self.valued_at_entry = true;
1206                warn!(
1207                    exchange = %self.exchange,
1208                    %instrument,
1209                    entry = %position.entry,
1210                    "SimulatedVenue has no market price to value a holding cost at, so values it \
1211                     at the position's entry price; warned once per venue"
1212                );
1213            }
1214            position.entry
1215        })
1216    }
1217
1218    /// The [`MarginEffect`] an order takes: `requested`, else the spot margin configuration's
1219    /// default, and [`NoSideEffect`](MarginEffect::NoSideEffect) on a cash account, which neither
1220    /// borrows nor repays.
1221    fn margin_effect(&self, requested: Option<MarginEffect>) -> MarginEffect {
1222        match &self.spot_margin {
1223            Some(config) => requested.unwrap_or(config.margin_effect()),
1224            None => MarginEffect::NoSideEffect,
1225        }
1226    }
1227
1228    /// Why an order carrying `requested` must be refused unsent, or `None` if this venue honours
1229    /// it: a CFD never borrows or repays, so with spot margin on it takes `NoSideEffect` alone, and
1230    /// a cash account takes `NoSideEffect` alone for the same reason.
1231    fn refused_margin_effect(
1232        &self,
1233        requested: Option<MarginEffect>,
1234        terms: &InstrumentTerms,
1235    ) -> Option<MarginEffect> {
1236        let borrows_here = self.spot_margin.is_some() && !terms.cash_settled;
1237        requested.filter(|effect| !borrows_here && *effect != MarginEffect::NoSideEffect)
1238    }
1239
1240    /// Gives `reservation` back to `free`, repaying the loan its order took when it was placed if
1241    /// the spot margin configuration repays at cancel, and hands back the interest that loan was
1242    /// charged, for the order's end to report. See [`AccountState::release_reservation`].
1243    fn release_reservation(
1244        &mut self,
1245        reservation: Reservation,
1246        time_exchange: DateTime<Utc>,
1247    ) -> Released {
1248        let repays = self.repays_at_cancel();
1249        let balance = self
1250            .account
1251            .release_reservation(&reservation, repays, time_exchange);
1252        Released {
1253            balance,
1254            interest: reservation.unreported_interest,
1255        }
1256    }
1257
1258    /// Whether a loan its order no longer needs is repaid: the spot margin configuration's
1259    /// [`auto_repay_at_cancel`](SpotMarginConfig::auto_repay_at_cancel).
1260    fn repays_at_cancel(&self) -> bool {
1261        self.spot_margin
1262            .as_ref()
1263            .is_some_and(SpotMarginConfig::auto_repay_at_cancel)
1264    }
1265
1266    /// The most a margin account may borrow of the asset `debit` pays with, for an arrival on an
1267    /// instrument with `terms`: see [`SpotMarginConfig`]'s `# How much it lends`.
1268    ///
1269    /// Zero unless spot margin lends, the order's `effect` is [`Borrow`](MarginEffect::Borrow),
1270    /// the instrument is spot, no liquidation is under way, and `free` falls short of the debit,
1271    /// so a venue that lends nothing, or an order that needs no loan, values nothing.
1272    /// `market` is the snapshot the order is priced from, used to value an asset on an instrument
1273    /// this venue holds no market for, as on a [`RequestPriced`](VenueRegime::RequestPriced)
1274    /// venue.
1275    fn borrowable(
1276        &self,
1277        debit: &Debit,
1278        effect: MarginEffect,
1279        terms: &InstrumentTerms,
1280        market: Option<&MarketSnapshot>,
1281    ) -> Decimal {
1282        let Some(config) = self.spot_margin.as_ref() else {
1283            return Decimal::ZERO;
1284        };
1285        if effect != MarginEffect::Borrow
1286            || terms.cash_settled
1287            || self.liquidation.under_way()
1288            || self.account.free(&debit.asset) >= debit.settled + debit.reserved
1289        {
1290            return Decimal::ZERO;
1291        }
1292
1293        let priced = market.map(|market| (&terms.underlying, market));
1294        // A debt this venue cannot value could be any size, so it lends nothing more.
1295        self.leverage_room(config, &debit.asset, priced)
1296            .unwrap_or(Decimal::ZERO)
1297    }
1298
1299    /// How much more of `asset` the leverage limit of `config` lets this account borrow, whatever
1300    /// order would borrow it: `(max_leverage − 1) · equity − debt`, never negative, in units of
1301    /// `asset`. See [`SpotMarginConfig`]'s `# How much it lends`.
1302    ///
1303    /// `None` if this venue cannot value `asset`, or a debt the account owes. `priced` is as for
1304    /// [`price_in`](Self::price_in). Shared by an order's [`borrowable`](Self::borrowable) and the
1305    /// [`borrow_capacity`](Self::borrow_capacity) query, so the two agree.
1306    fn leverage_room(
1307        &self,
1308        config: &SpotMarginConfig,
1309        asset: &AssetNameExchange,
1310        priced: Option<(&Underlying<AssetNameExchange>, &MarketSnapshot)>,
1311    ) -> Option<Decimal> {
1312        let asset_price = self.price_in(asset, config.valuation_asset(), priced)?;
1313        let Valued { equity, debt } = self.valued(config, MarginCheck::Borrow, priced)?;
1314
1315        let headroom = (config.max_leverage() - Decimal::ONE) * equity - debt;
1316        // `price_in` never values an asset at zero, so the division is safe.
1317        Some(headroom.max(Decimal::ZERO) / asset_price)
1318    }
1319
1320    /// The rejection for an arrival the account refused, an order on `side`.
1321    ///
1322    /// [`ApiError::BorrowRejected`] with [`BorrowRejectReason::BorrowLimitExceeded`] when the
1323    /// asset's borrow cap is the limit that refused ([`BalanceInsufficient::max_borrow`]), as a
1324    /// live Binance margin client reports Binance's `-3006`; [`ApiError::BalanceInsufficient`]
1325    /// otherwise. Both messages say how much the venue would lend, and name the minimum borrow and
1326    /// the cap when they are why. A sell borrows the base asset, the units its order is sized in,
1327    /// so its [`BorrowReject::available`] is what the cap left, or zero when that is below the
1328    /// asset's minimum borrow, which no loan may be under. A buy borrows the quote asset, and
1329    /// leaves it unset.
1330    fn insufficient_error(
1331        &self,
1332        insufficient: BalanceInsufficient,
1333        side: Side,
1334    ) -> UnindexedApiError {
1335        let BalanceInsufficient {
1336            asset,
1337            free,
1338            required,
1339            borrowable,
1340            min_borrow,
1341            max_borrow,
1342        } = insufficient;
1343        let mut message =
1344            format!("Available Balance: {free}, Required Balance inc. fees: {required}");
1345        if let Some(borrowable) = borrowable {
1346            message.push_str(&format!(", Borrowable: {borrowable}"));
1347        }
1348        if let Some(min_borrow) = min_borrow {
1349            message.push_str(&format!(", Minimum Borrow: {min_borrow}"));
1350        }
1351        let Some(max_borrow) = max_borrow else {
1352            return ApiError::BalanceInsufficient(Some(asset), message);
1353        };
1354
1355        let message = format!("{asset} borrow exceeds Max Borrow: {max_borrow}; {message}");
1356        let reject = BorrowReject::new(BorrowRejectReason::BorrowLimitExceeded, None, message);
1357        let reject = match (side, borrowable) {
1358            (Side::Sell, Some(left)) => {
1359                let least = self
1360                    .spot_margin
1361                    .as_ref()
1362                    .map_or(Decimal::ZERO, |config| config.min_borrow(&asset));
1363                reject.with_available(if left < least { Decimal::ZERO } else { left })
1364            }
1365            _ => reject,
1366        };
1367        ApiError::BorrowRejected(reject)
1368    }
1369
1370    /// The account's equity and debt, valued in `config`'s valuation asset as
1371    /// [`SpotMarginConfig`]'s `# How much it lends` says, with each asset's positive net weighted
1372    /// by its collateral ratio for `check`, or `None` if it owes an asset this venue cannot value.
1373    ///
1374    /// `priced` is as for [`price_in`](Self::price_in). Shared by the leverage limit and the
1375    /// liquidation check, so the two value an account alike but for the ratios.
1376    fn valued(
1377        &self,
1378        config: &SpotMarginConfig,
1379        check: MarginCheck,
1380        priced: Option<(&Underlying<AssetNameExchange>, &MarketSnapshot)>,
1381    ) -> Option<Valued> {
1382        let mut valued = Valued::default();
1383        for balance in self.account.balances() {
1384            let owed = owed(balance);
1385            let Some(price) = self.price_in(&balance.asset, config.valuation_asset(), priced)
1386            else {
1387                if owed > Decimal::ZERO {
1388                    return None;
1389                }
1390                continue;
1391            };
1392            let net = balance.balance.net_asset();
1393            // Only what the account holds beyond what it owes is weighted: the rest counts in
1394            // full, as does the net of an asset it owes more of than it holds.
1395            let ratio = match net > Decimal::ZERO {
1396                true => check.collateral_ratio(config, &balance.asset),
1397                false => Decimal::ONE,
1398            };
1399            valued.equity += net * ratio * price;
1400            valued.debt += owed * price;
1401        }
1402        Some(valued)
1403    }
1404
1405    /// What one unit of `asset` is worth in `valuation`: one for `valuation` itself, else the mid
1406    /// or last price of a spot instrument pairing the two, in either direction. `None` if no
1407    /// instrument this venue trades has a positive price for the pair.
1408    ///
1409    /// `priced` is a pair with its snapshot, used when this venue holds no market for it. Of
1410    /// several instruments pairing the two, the one first by name is used, so a run values the
1411    /// same way every time.
1412    fn price_in(
1413        &self,
1414        asset: &AssetNameExchange,
1415        valuation: &AssetNameExchange,
1416        priced: Option<(&Underlying<AssetNameExchange>, &MarketSnapshot)>,
1417    ) -> Option<Decimal> {
1418        if asset == valuation {
1419            return Some(Decimal::ONE);
1420        }
1421
1422        // `priced` applies to an instrument whose underlying it names: `asset`/`valuation`, or the
1423        // reverse for an inverse pair.
1424        let snapshot_of = |pair: &SpotPair| {
1425            self.market(&pair.instrument)
1426                .map(|market| market.snapshot)
1427                .or_else(|| {
1428                    let (underlying, snapshot) = priced?;
1429                    let (base, quote) = match pair.inverse {
1430                        true => (valuation, asset),
1431                        false => (asset, valuation),
1432                    };
1433                    (underlying.base == *base && underlying.quote == *quote).then_some(*snapshot)
1434                })
1435        };
1436
1437        // Ordered by name, so the first that prices is the one first by name.
1438        self.spot_pairs
1439            .between(asset, valuation)
1440            .iter()
1441            .find_map(|pair| {
1442                let price = mid_or_last(&snapshot_of(pair)?)?;
1443                // A price of zero values nothing, and the inverse would divide by it.
1444                if price <= Decimal::ZERO {
1445                    return None;
1446                }
1447                Some(match pair.inverse {
1448                    true => Decimal::ONE / price,
1449                    false => price,
1450                })
1451            })
1452    }
1453
1454    /// Retires every order whose deadline `time_exchange` has reached, releasing what it held.
1455    ///
1456    /// Shared by this venue's two inputs so that a deadline means the same thing whichever one
1457    /// reaches it first -- see [`advance_time`](Self::advance_time).
1458    ///
1459    /// Deadlines are collected before anything is retired, because retiring removes from the very
1460    /// index the sweep walks. [`OpenOrders::expired_as_of`] yields them by deadline then
1461    /// [`ClientOrderId`], so the events are in the same order on every run.
1462    ///
1463    /// [`OpenOrders::expired_as_of`]: super::orders::OpenOrders::expired_as_of
1464    fn sweep_expired(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
1465        let expired = self.account.orders().expired_as_of(time_exchange);
1466
1467        if expired.is_empty() {
1468            return Vec::new();
1469        }
1470
1471        let mut events = Vec::with_capacity(expired.len() * 2);
1472
1473        for cid in expired {
1474            // Collected from this same index with nothing in between, so this always finds it.
1475            if let Some(resting) = self.account.remove_order(&cid) {
1476                events.extend(self.retire_expired(resting, time_exchange));
1477            }
1478        }
1479
1480        events
1481    }
1482
1483    /// Retires an order already taken off the book as expired, returning `[balance, order]`, or
1484    /// `[order]` for one the venue took no reservation for. A margin order that borrowed when it
1485    /// rested reports the interest it was charged after the balance, as `[balance, cash flow..,
1486    /// order]`: it never traded, so this is the last chance to report it.
1487    fn retire_expired(
1488        &mut self,
1489        RestingOrder { order, reservation }: RestingOrder,
1490        time_exchange: DateTime<Utc>,
1491    ) -> Vec<UnindexedAccountEvent> {
1492        let flows = reservation
1493            .as_ref()
1494            .map_or(0, |reservation| reservation.unreported_interest.len());
1495        let mut events = Vec::with_capacity(2 + flows);
1496
1497        // Released before the terminal snapshot, so a consumer applying them in order never sees
1498        // an order retired against a balance that still holds its reservation. An order the venue
1499        // took nothing for releases nothing -- see `OpenOrders`.
1500        if let Some(reservation) = reservation {
1501            let Released { balance, interest } =
1502                self.release_reservation(reservation, time_exchange);
1503            events.push(self.build_account_event(Snapshot(balance)));
1504            self.push_interest(&mut events, interest, time_exchange);
1505        }
1506
1507        let expired_id = order.state.id.or_client_id(&order.key.cid);
1508        let expired_order = Order {
1509            key: order.key,
1510            side: order.side,
1511            price: order.price,
1512            quantity: order.quantity,
1513            kind: order.kind,
1514            time_in_force: order.time_in_force,
1515            state: Expired {
1516                id: expired_id,
1517                time_exchange,
1518                filled_quantity: Some(order.state.filled_quantity),
1519            },
1520        };
1521
1522        self.account.ack_expired(expired_order.clone());
1523
1524        events.push(self.build_account_event(Snapshot(UnindexedOrder::from(expired_order))));
1525        events
1526    }
1527
1528    /// The instant [`advance_time`](Self::advance_time) last set, which stamps what a request
1529    /// produces. The Unix epoch until `advance_time` is first called.
1530    pub fn time_exchange(&self) -> DateTime<Utc> {
1531        self.time_exchange_latest
1532    }
1533
1534    /// Records this venue's own view of `instrument` as of `time_exchange`, from `observation`.
1535    ///
1536    /// Its [`snapshot`](MarketObservation::snapshot) **replaces** whatever was held: it is a
1537    /// complete view of the instrument, not a partial update, so a field that is `None` means the
1538    /// feed supplies no such price rather than "unchanged". Whoever calls this owns the state
1539    /// machine that folds a stream of market events into that view.
1540    ///
1541    /// # The [`depth`](MarketObservation::depth) replaces what is left, it does not add to it
1542    /// [`MarketDepth`] is the size on offer as of this observation, and it is stored as the size
1543    /// still *available* — drawn down by every taker fill struck against it until the next call
1544    /// replaces it. That is the only thing that stops two market orders arriving between two
1545    /// observations from each taking the whole of a size that was displayed once: reading a
1546    /// stored size does not consume it, so without the draw-down both would see it undiminished.
1547    ///
1548    /// It bounds this account's own aggression within one observation and claims nothing more. It
1549    /// is not queue position against the rest of the market, which stays unmodelled — see the
1550    /// type's `# What matching does not model`.
1551    ///
1552    /// A feed with no sizes passes [`MarketDepth::UNKNOWN`], which caps nothing. See
1553    /// [`MarketDepth`] for why that, and not "nothing is available", is what absent size means.
1554    ///
1555    /// # Not every driver supplies this
1556    /// A venue only has market state if something feeds it one. `SimRunner` does, routing each
1557    /// source market event to the venues trading that instrument before the `Engine` sees it, and
1558    /// so does [`MockExchange::new_market_driven`](super::MockExchange::new_market_driven), from a
1559    /// live feed. A request-priced venue, which [`MockExchange::new`](super::MockExchange::new)
1560    /// drives, is fed nothing, since a backtest feeding it the market alongside requests would
1561    /// re-create the look-ahead hazard that motivated the split, so it reports
1562    /// [`market`](Self::market) as `None` forever. That difference is a property of the regime,
1563    /// not an accident of wiring.
1564    ///
1565    /// # What it prices
1566    /// Everything this venue fills. It is the market a **resting** order is matched against, the
1567    /// one a limit order's marketability is judged on, and — on a
1568    /// [`MarketDriven`](VenueRegime::MarketDriven) venue, which is the only kind that has one — the
1569    /// one a **market** order is priced from. See the type's `# How an order is priced`.
1570    ///
1571    /// So feeding this is not optional for a `MarketDriven` venue: an instrument it has never been
1572    /// fed cannot fill a market order, and says so rather than reaching for the requester's
1573    /// snapshot.
1574    ///
1575    /// # Returns the fills it caused, which a driver must deliver
1576    /// Applying a market is what makes a resting order fill, so this returns every account event
1577    /// those fills produced, in the order they must be delivered — the same obligation
1578    /// [`VenueOutcome`] carries for a request. Per filled order the events are
1579    /// `[balance, trade, order]`: the trade precedes the terminal order snapshot, because a
1580    /// consumer routing a fill to a position needs the order still to be live when the trade
1581    /// arrives.
1582    ///
1583    /// They are preceded by the holding costs of every boundary since the clock last moved and the
1584    /// expiries this tick reached, exactly as [`advance_time`](Self::advance_time) reports them.
1585    /// The costs are valued at the market as it stood before this tick replaced it. They are
1586    /// followed by what the margin check at the end of the tick causes, as for `advance_time`,
1587    /// valued at the market this tick set.
1588    ///
1589    /// # A bar releases the orders held for it
1590    /// Under [`MarketOrderTiming::NextBar`], an observation carrying a
1591    /// [`bar`](MarketObservation::bar) releases every order held on `instrument`, earliest first,
1592    /// stamped `time_exchange`: each market order filled at the bar's reference price as a taker,
1593    /// and each limit order held on a stale market decided as if it arrived to that price. Their
1594    /// events follow the expiries and precede the resting fills, since a held order trades at the
1595    /// start of the bar and a resting one is crossed during it.
1596    /// [`fills_ahead_of`](Self::fills_ahead_of) says beforehand whether an observation will
1597    /// release anything this way.
1598    ///
1599    /// Under [`StaleMarketPolicy::Wait`], a bar that is not [`fresh`](MarketObservation::fresh)
1600    /// releases nothing, and cancels the immediate-or-cancel and fill-or-kill orders held for it.
1601    ///
1602    /// # A fresh observation releases the orders waiting for one
1603    /// Under [`StaleMarketPolicy::Wait`] with [`MarketOrderTiming::OnArrival`], a
1604    /// [`fresh`](MarketObservation::fresh) observation releases every order that arrived on a
1605    /// stale `instrument`, earliest first, each decided as if it had arrived just after this
1606    /// observation, stamped `time_exchange`: a market order fills, and a limit order trades what
1607    /// the market crosses and rests the rest at its limit. Their events follow the resting fills,
1608    /// which the observation crossed first, so a limit order resting on release is not crossed by
1609    /// the observation that released it.
1610    ///
1611    /// # An observation that is not fresh crosses no resting order
1612    /// Under [`StaleMarketPolicy::Wait`], an observation that is not
1613    /// [`fresh`](MarketObservation::fresh) still moves the price the venue holds, but fills no
1614    /// resting order, however far it moves: it is not evidence that anything traded there.
1615    ///
1616    /// `#[must_use]` because a driver that applies the market and drops the result has silently
1617    /// eaten the fills, leaving its client holding an order the venue no longer has.
1618    ///
1619    /// # The clock never moves back
1620    /// A `time_exchange` earlier than the latest instant this venue has reached, through this call
1621    /// or through [`advance_time`](Self::advance_time), is taken as that latest instant, as
1622    /// `advance_time` takes one. It charges nothing again, and everything the tick stamps is
1623    /// stamped no earlier than what this venue has already reported: the market it stores, the
1624    /// instant it counts as [`fresh`](MarketObservation::fresh) from, the expiries it sweeps, and
1625    /// the fills it causes with the balances they restate. A consumer keeping the latest stamp of
1626    /// each balance would otherwise drop a fill's restatement as older than one it holds.
1627    ///
1628    /// The venue clamps the instant itself, so this holds whichever driver calls it. A driver that
1629    /// feeds its ticks in order, never behind an instant it has already advanced to, sees no
1630    /// difference.
1631    ///
1632    /// # Panics
1633    /// If a configured [`HoldingCostModel`] or [`MarginInterestModel`] breaks its contract: a
1634    /// boundary not after the instant it was asked from, or a negative interest.
1635    #[must_use]
1636    pub fn apply_market(
1637        &mut self,
1638        instrument: &InstrumentNameExchange,
1639        observation: MarketObservation,
1640        time_exchange: DateTime<Utc>,
1641    ) -> Vec<UnindexedAccountEvent> {
1642        let time_exchange = self.not_before_reached(time_exchange);
1643        // Charged before the snapshot is replaced, so a boundary this tick passed is valued at the
1644        // market as of that boundary rather than at a price observed after it.
1645        let mut events = self.accrue(time_exchange);
1646
1647        let MarketObservation {
1648            snapshot,
1649            depth,
1650            bar,
1651            fresh,
1652        } = observation;
1653        let entry = self.market.entry(instrument.clone()).or_default();
1654        entry.snapshot = snapshot;
1655        entry.depth = depth;
1656        entry.time_exchange = time_exchange;
1657        if bar.is_some() {
1658            entry.bar = bar;
1659        }
1660        if fresh {
1661            entry.fresh_at = Some(time_exchange);
1662        }
1663
1664        // Swept before matching, so an order whose deadline this tick has reached cannot trade on
1665        // the very tick that retires it -- see `advance_time`.
1666        events.append(&mut self.sweep_expired(time_exchange));
1667        // Under `Wait`, an observation that shows nothing trading moves the price but fills no one.
1668        let crosses_resting = fresh || !matches!(self.stale_market, StaleMarketPolicy::Wait { .. });
1669        match self.market_order_timing {
1670            MarketOrderTiming::NextBar(price) => {
1671                if let Some(bar) = bar {
1672                    if self.bar_releases(&observation) {
1673                        events.append(&mut self.release(
1674                            instrument,
1675                            Release::Bar(&bar, price),
1676                            time_exchange,
1677                        ));
1678                    } else {
1679                        events.append(&mut self.cancel_held_immediate(instrument, time_exchange));
1680                    }
1681                }
1682                if crosses_resting {
1683                    events.append(&mut self.match_resting(instrument, snapshot, time_exchange));
1684                }
1685            }
1686            MarketOrderTiming::OnArrival => {
1687                if crosses_resting {
1688                    events.append(&mut self.match_resting(instrument, snapshot, time_exchange));
1689                }
1690                // Only `Wait` holds an order under `OnArrival`, so no other policy has one to look
1691                // up.
1692                if fresh && matches!(self.stale_market, StaleMarketPolicy::Wait { .. }) {
1693                    events.append(&mut self.release(
1694                        instrument,
1695                        Release::Market(&snapshot),
1696                        time_exchange,
1697                    ));
1698                }
1699            }
1700        }
1701        self.check_margin(time_exchange, &mut events);
1702        self.refresh_next_interest_if_loans_changed();
1703        self.report_debt_changes(&mut events);
1704        events
1705    }
1706
1707    /// Whether applying `observation` to `instrument` releases orders held for its bar,
1708    /// whose fills a driver must deliver **ahead of** the observation itself — see
1709    /// [`MarketOrderTiming::NextBar`].
1710    ///
1711    /// Asked before [`apply_market`](Self::apply_market), since applying the observation is what
1712    /// empties the hold. `false` for a fill released by a fresh observation under
1713    /// [`StaleMarketPolicy::Wait`] alone, which follows its observation as a resting fill does.
1714    pub fn fills_ahead_of(
1715        &self,
1716        instrument: &InstrumentNameExchange,
1717        observation: &MarketObservation,
1718    ) -> bool {
1719        matches!(self.market_order_timing, MarketOrderTiming::NextBar(_))
1720            && self.bar_releases(observation)
1721            && self.account.orders().holds(instrument)
1722    }
1723
1724    /// Whether `observation` carries a bar that releases the orders held for one: any bar,
1725    /// unless [`StaleMarketPolicy::Wait`] asks for one something traded in.
1726    fn bar_releases(&self, observation: &MarketObservation) -> bool {
1727        observation.bar.is_some()
1728            && (observation.fresh || !matches!(self.stale_market, StaleMarketPolicy::Wait { .. }))
1729    }
1730
1731    /// Retires every order still held as expired, filled nothing, stamped `time_exchange`:
1732    /// the data has ended before the bar or the fresh observation it was waiting for.
1733    ///
1734    /// For a driver at the end of its data, so that no order it accepted is left unanswered. Per
1735    /// order the events are `[balance, order]`, the released hold then the terminal [`Expired`]
1736    /// snapshot, as [`advance_time`](Self::advance_time) reports a deadline it reached. Pass the
1737    /// last instant of the run: this moves no clock and charges no holding cost.
1738    ///
1739    /// # Panics
1740    /// If the configured [`MarginInterestModel`] breaks its contract with a boundary not after the
1741    /// instant it was asked from, when the venue next looks for one after a loan is repaid.
1742    #[must_use]
1743    pub fn expire_held(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
1744        let held = self.account.orders().held();
1745        let mut events = Vec::with_capacity(held.len() * 2);
1746        for cid in held {
1747            // Collected from this same index with nothing in between, so this always finds it.
1748            if let Some(resting) = self.account.remove_order(&cid) {
1749                events.extend(self.retire_expired(resting, time_exchange));
1750            }
1751        }
1752        self.refresh_next_interest_if_loans_changed();
1753        self.report_debt_changes(&mut events);
1754        events
1755    }
1756
1757    /// Fills every resting order on `instrument` that `snapshot` has moved to or through.
1758    ///
1759    /// Walks each side best-first and stops at the first order that does not cross, which
1760    /// [`OpenOrders::resting`]'s price-time ordering makes correct: the crossing test is monotone
1761    /// in price down a side, so the first order that fails it is followed only by worse ones.
1762    ///
1763    /// # What crosses is the remainder
1764    /// A crossing order fills what is **left** of it: its quantity less whatever
1765    /// [`Open::filled_quantity`] says has already traded. That is its whole quantity for every
1766    /// order this venue books itself, which fills nothing before it rests -- but not for one a
1767    /// configured `initial_state` seeds, which may arrive carrying a live account's partial fill.
1768    /// Settling and printing the whole quantity of such an order would double-count the part that
1769    /// traded elsewhere, in the balance ledger and in the trade ledger alike.
1770    ///
1771    /// [`OpenOrders::resting`]: super::orders::OpenOrders::resting
1772    fn match_resting(
1773        &mut self,
1774        instrument: &InstrumentNameExchange,
1775        snapshot: MarketSnapshot,
1776        time_exchange: DateTime<Utc>,
1777    ) -> Vec<UnindexedAccountEvent> {
1778        // Collected before anything is filled: matching removes from the very book it walks, and
1779        // each entry carries its own limit so the fill loop never re-reads an order it just took
1780        // off the book. `resting` only yields queued orders, and only a priced order is queued.
1781        let mut crossing: Vec<(ClientOrderId, Decimal)> = Vec::new();
1782        for side in [Side::Buy, Side::Sell] {
1783            crossing.extend(
1784                self.account
1785                    .orders()
1786                    .resting(instrument, side)
1787                    .take_while(|order| {
1788                        order
1789                            .price
1790                            .is_some_and(|limit| crosses(side, limit, &snapshot))
1791                    })
1792                    .filter_map(|order| Some((order.key.cid.clone(), order.price?))),
1793            );
1794        }
1795
1796        if crossing.is_empty() {
1797            return Vec::new();
1798        }
1799
1800        // One lookup for the whole instrument: every crossing order is on it by construction.
1801        let terms = match self.instrument_terms(instrument) {
1802            Ok(terms) => terms,
1803            // Unreachable: an order cannot rest on an instrument this venue rejected at open, and
1804            // the instruments never change after construction. Should that ever break, dropping
1805            // the fills silently is the one thing this method must not do.
1806            Err(error) => panic!(
1807                "SimulatedVenue holds resting orders on an instrument it cannot price: {error}"
1808            ),
1809        };
1810
1811        let mut events = Vec::with_capacity(crossing.len() * 4);
1812
1813        for (cid, limit) in crossing {
1814            // Collected from this same book with nothing in between, so this always finds it.
1815            let Some(RestingOrder { order, reservation }) = self.account.remove_order(&cid) else {
1816                continue;
1817            };
1818
1819            // Only what has not already traded crosses. An order can reach this book carrying a
1820            // filled quantity -- a configured `initial_state` seeds one straight out of a live
1821            // account -- and settling or printing the whole of it again would charge the ledger a
1822            // second time for a portion that traded before this venue ever held the order.
1823            let remaining = order.state.quantity_remaining(order.quantity);
1824
1825            debug_assert!(
1826                remaining > Decimal::ZERO,
1827                "resting order {cid} arrived with {} of {} already filled, so crossing it trades \
1828                 nothing: an order with no quantity left to trade does not belong on the book",
1829                order.state.filled_quantity,
1830                order.quantity
1831            );
1832
1833            // A resting order fills at its own limit, never at a price derived from the book --
1834            // see the type's `# How an order is priced` -- and takes no liquidity, so it is
1835            // charged the maker rate. A cash-settled one is priced against the position as it
1836            // stands now, which may not be the one it rested against.
1837            let settlement = self.fill_settlement(
1838                &terms,
1839                self.net_position(&terms, instrument),
1840                order.side,
1841                remaining,
1842                limit,
1843                Liquidity::Maker,
1844            );
1845
1846            let mut committed = match self.settle_resting(
1847                &order,
1848                &terms,
1849                reservation,
1850                &settlement,
1851                time_exchange,
1852            ) {
1853                RestingSettlement::Settled(committed) => committed,
1854                RestingSettlement::Unaffordable { released } => {
1855                    events.extend(self.cancel_unfilled(order, released, time_exchange));
1856                    continue;
1857                }
1858            };
1859            self.take_fee_apart(&settlement, &mut committed, time_exchange);
1860
1861            let trade = self.print_fill(
1862                &terms,
1863                &order,
1864                Fill {
1865                    quantity: remaining,
1866                    price: limit,
1867                },
1868                settlement.fees,
1869                time_exchange,
1870            );
1871            let order_id = trade.order_id.clone();
1872
1873            // The order is terminal, and says so as `Inactive(FullyFilled)` rather than as an
1874            // `Open` carrying a complete fill. Both denote the same fact, but only this one can
1875            // carry the price it filled at.
1876            //
1877            // `filled_quantity` is the order's whole quantity -- what it has done in total, not
1878            // what this cross did -- while `avg_price` is reported only when this venue struck
1879            // every fill behind it, which is exactly an order that reached the book with nothing
1880            // done. An order that arrived part-filled took that part at a price this venue never
1881            // saw, so it reports no average rather than passing one fill's price off as the mean
1882            // of two. That is what `Filled::avg_price` being optional is for.
1883            let filled = Order {
1884                key: order.key,
1885                side: order.side,
1886                price: order.price,
1887                quantity: order.quantity,
1888                kind: order.kind,
1889                time_in_force: order.time_in_force,
1890                state: Filled::new(
1891                    order_id,
1892                    time_exchange,
1893                    order.quantity,
1894                    order.state.filled_quantity.is_zero().then_some(limit),
1895                ),
1896            };
1897            self.account.ack_filled(filled.clone());
1898            let filled = filled.map_state(OrderState::fully_filled);
1899
1900            self.push_committed(&mut events, committed, Some(trade));
1901            events.push(self.build_account_event(Snapshot(filled)));
1902        }
1903
1904        events
1905    }
1906
1907    /// Releases every order held on `instrument`, earliest arrival first, priced from `release`:
1908    /// the bar it was held for under [`MarketOrderTiming::NextBar`], or the market a fresh
1909    /// observation left under [`StaleMarketPolicy::Wait`]. A market order fills, and a limit order
1910    /// is decided as if it arrived to that market — see
1911    /// [`release_held_limit`](Self::release_held_limit).
1912    fn release(
1913        &mut self,
1914        instrument: &InstrumentNameExchange,
1915        release: Release<'_>,
1916        time_exchange: DateTime<Utc>,
1917    ) -> Vec<UnindexedAccountEvent> {
1918        let held = self.account.take_held(instrument);
1919        if held.is_empty() {
1920            return Vec::new();
1921        }
1922
1923        let terms = match self.instrument_terms(instrument) {
1924            Ok(terms) => terms,
1925            // Unreachable, as in `match_resting`: an order is held only on an instrument this
1926            // venue accepted it for.
1927            Err(error) => {
1928                panic!("SimulatedVenue holds orders on an instrument it cannot price: {error}")
1929            }
1930        };
1931
1932        let mut events = Vec::with_capacity(held.len() * 4);
1933        for cid in held {
1934            // Handed out by the held index with nothing in between, so this always finds it.
1935            let Some(resting) = self.account.remove_order(&cid) else {
1936                continue;
1937            };
1938            // Only a limit order is held with a price: `hold_on_arrival` drops a market order's.
1939            debug_assert_eq!(
1940                resting.order.kind == OrderKind::Limit,
1941                resting.order.price.is_some(),
1942                "SimulatedVenue held {} as {:?} with price {:?}: only a limit order is held with \
1943                 a price",
1944                resting.order.key.cid,
1945                resting.order.kind,
1946                resting.order.price
1947            );
1948            events.extend(match resting.order.price {
1949                Some(limit) => {
1950                    self.release_held_limit(resting, limit, release, &terms, time_exchange)
1951                }
1952                None => self.fill_held(resting, release, &terms, time_exchange),
1953            });
1954        }
1955        events
1956    }
1957
1958    /// Cancels every immediate-or-cancel and fill-or-kill market order held on `instrument` for a
1959    /// bar that has arrived with nothing traded in it, leaving the rest held for the next one — see
1960    /// [`StaleMarketPolicy::Wait`]. Per order the events are `[balance, cash flow.., order]`, the
1961    /// cash flows reporting the interest its hold was charged, if it borrowed — see
1962    /// [`cancel_unfilled`](Self::cancel_unfilled).
1963    fn cancel_held_immediate(
1964        &mut self,
1965        instrument: &InstrumentNameExchange,
1966        time_exchange: DateTime<Utc>,
1967    ) -> Vec<UnindexedAccountEvent> {
1968        let orders = self.account.orders();
1969        let immediate = orders
1970            .held_on(instrument)
1971            .iter()
1972            .filter(|cid| {
1973                orders.get(cid).is_some_and(|order| {
1974                    matches!(
1975                        order.time_in_force,
1976                        TimeInForce::ImmediateOrCancel | TimeInForce::FillOrKill
1977                    )
1978                })
1979            })
1980            .cloned()
1981            .collect::<Vec<_>>();
1982
1983        let mut events = Vec::with_capacity(immediate.len() * 2);
1984        for cid in immediate {
1985            // Collected from this same index with nothing in between, so this always finds it.
1986            if let Some(RestingOrder { order, reservation }) = self.account.remove_order(&cid) {
1987                let released = reservation
1988                    .map(|reservation| self.release_reservation(reservation, time_exchange));
1989                events.extend(self.cancel_unfilled(order, released, time_exchange));
1990            }
1991        }
1992        events
1993    }
1994
1995    /// Fills one held market order, as a taker arriving at the price `release` gives it, and
1996    /// returns `[balance.., trade, cash flow.., order]`, or `[balance, cash flow.., order]` for one
1997    /// it cancels, the cash flows reporting the interest its loans were charged, if any.
1998    ///
1999    /// What the order would cost was held when it arrived, at the price the venue held then. The
2000    /// release's price is another, so the hold is released and the fill costed afresh, as a
2001    /// resting CFD fill is, and an order the account can no longer afford is cancelled — see
2002    /// [`settle_resting`](Self::settle_resting). So is one the release's market cannot price.
2003    fn fill_held(
2004        &mut self,
2005        RestingOrder { order, reservation }: RestingOrder,
2006        release: Release<'_>,
2007        terms: &InstrumentTerms,
2008        time_exchange: DateTime<Utc>,
2009    ) -> Vec<UnindexedAccountEvent> {
2010        let side = order.side;
2011        let quantity = order.state.quantity_remaining(order.quantity);
2012
2013        let reference = match release {
2014            Release::Bar(bar, price) => {
2015                // The bar's price is handed to the model as the only price there is, so a model
2016                // reading the last price prices from it, and the configured slippage moves it, as
2017                // for any taker. A model that needs a book cannot price from it and is given the
2018                // reference itself.
2019                let reference = price.reference(side, bar);
2020                let at_reference = MarketSnapshot::from_last_price(Some(reference));
2021                Some(
2022                    self.fill_model
2023                        .fill_price(&FillContext::new(side, quantity, &at_reference))
2024                        .unwrap_or(reference),
2025                )
2026            }
2027            // Priced exactly as an order arriving now would be.
2028            Release::Market(market) => self
2029                .fill_model
2030                .fill_price(&FillContext::new(side, quantity, market)),
2031        };
2032        // A fresh observation leaving a market the model cannot price for this side: a quote with
2033        // only the other side of the book, say. On arrival that order would be refused as
2034        // unpriceable; having been accepted, it is cancelled.
2035        let Some(reference) = reference else {
2036            let released =
2037                reservation.map(|reservation| self.release_reservation(reservation, time_exchange));
2038            return self.cancel_unfilled(order, released, time_exchange);
2039        };
2040
2041        // What the releasing observation has to give this order, which is all of it wherever no
2042        // size is known -- as on every candle feed.
2043        let filled_quantity = match self.depth_available(&order.key.instrument, side) {
2044            Some(available) => quantity.min(available),
2045            None => quantity,
2046        };
2047        // Its time in force decides what becomes of a part the observation cannot fill, as on
2048        // arrival: a deadline already reached was swept before it released anything.
2049        let kill = order.time_in_force == TimeInForce::FillOrKill;
2050        if filled_quantity <= Decimal::ZERO || (filled_quantity < quantity && kill) {
2051            let released =
2052                reservation.map(|reservation| self.release_reservation(reservation, time_exchange));
2053            return self.cancel_unfilled(order, released, time_exchange);
2054        }
2055
2056        // Drawn only now that the order is known to trade, and handed back if the ledger refuses
2057        // it, as in `fill_on_arrival`.
2058        let slippage_before = self.slippage.clone();
2059        let fill_price = self.slippage.apply(side, reference);
2060
2061        let settlement = self.fill_settlement(
2062            terms,
2063            self.net_position(terms, &order.key.instrument),
2064            side,
2065            filled_quantity,
2066            fill_price,
2067            Liquidity::Taker,
2068        );
2069
2070        let mut committed =
2071            match self.settle_resting_afresh(terms, reservation, &settlement, time_exchange) {
2072                RestingSettlement::Settled(committed) => committed,
2073                RestingSettlement::Unaffordable { released } => {
2074                    self.slippage = slippage_before;
2075                    return self.cancel_unfilled(order, released, time_exchange);
2076                }
2077            };
2078        self.take_fee_apart(&settlement, &mut committed, time_exchange);
2079
2080        self.consume_depth(&order.key.instrument, side, filled_quantity);
2081        let fill = Fill {
2082            quantity: filled_quantity,
2083            price: fill_price,
2084        };
2085        let trade = self.print_fill(terms, &order, fill, settlement.fees, time_exchange);
2086
2087        // Filled outright, or cancelled carrying what the observation's size allowed: a market
2088        // order has no limit to rest a remainder at, as on arrival.
2089        let terminal = if filled_quantity == quantity {
2090            let filled = Order {
2091                key: order.key,
2092                side,
2093                price: order.price,
2094                quantity: order.quantity,
2095                kind: order.kind,
2096                time_in_force: order.time_in_force,
2097                state: Filled::new(
2098                    trade.order_id.clone(),
2099                    time_exchange,
2100                    order.quantity,
2101                    Some(fill_price),
2102                ),
2103            };
2104            self.account.ack_filled(filled.clone());
2105            filled.map_state(OrderState::fully_filled)
2106        } else {
2107            let cancelled = Order {
2108                key: order.key,
2109                side,
2110                price: order.price,
2111                quantity: order.quantity,
2112                kind: order.kind,
2113                time_in_force: order.time_in_force,
2114                state: Cancelled {
2115                    id: trade.order_id.clone(),
2116                    time_exchange,
2117                    filled_quantity: Some(filled_quantity),
2118                },
2119            };
2120            self.account.ack_cancelled(cancelled.clone());
2121            UnindexedOrder::from(cancelled)
2122        };
2123
2124        let mut events = Vec::with_capacity(4);
2125        self.push_committed(&mut events, committed, Some(trade));
2126        events.push(self.build_account_event(Snapshot(terminal)));
2127        events
2128    }
2129
2130    /// Releases one limit order held on a stale market, as if it had arrived to the market
2131    /// `release` gives it, and returns `[balance.., trade, cash flow.., order]` for one that
2132    /// trades, `[balance, order]` for one that joins the book whole, and `[balance, cash flow..,
2133    /// order]` for one it cancels. The cash flows report the interest its loans were charged, if
2134    /// any. One that joins the book whole has traded nothing, so its reservation keeps them.
2135    ///
2136    /// Decided against that market as an arriving limit order is decided against the market it
2137    /// arrives to:
2138    /// - one the market crosses trades as a taker, priced by the venue's [`FillModel`], moved by
2139    ///   its slippage and capped at its limit, up to the size the market has, and what is left of
2140    ///   it joins the book at its limit;
2141    /// - one the market does not cross joins the book whole, at its limit;
2142    /// - a post-only one the market crosses is cancelled, having traded nothing.
2143    ///
2144    /// A bar's reference price is handed over as the only price there is, as for a held market
2145    /// order — see [`fill_held`](Self::fill_held).
2146    ///
2147    /// What the order would cost was held when it arrived, at the market the venue held then. The
2148    /// hold is released and the order costed afresh, its fill and what it rests as one
2149    /// requirement, as on arrival, so an order the account can no longer afford is cancelled.
2150    /// Whatever joins the book is stamped `time_exchange`, which is when it joined the queue.
2151    fn release_held_limit(
2152        &mut self,
2153        RestingOrder { order, reservation }: RestingOrder,
2154        limit: Decimal,
2155        release: Release<'_>,
2156        terms: &InstrumentTerms,
2157        time_exchange: DateTime<Utc>,
2158    ) -> Vec<UnindexedAccountEvent> {
2159        let side = order.side;
2160        let quantity = order.state.quantity_remaining(order.quantity);
2161
2162        let (market, reference) = match release {
2163            Release::Bar(bar, price) => {
2164                let reference = price.reference(side, bar);
2165                (
2166                    MarketSnapshot::from_last_price(Some(reference)),
2167                    Some(reference),
2168                )
2169            }
2170            Release::Market(market) => (*market, None),
2171        };
2172
2173        let marketable = crosses(side, limit, &market);
2174        let post_only = matches!(
2175            order.time_in_force,
2176            TimeInForce::GoodUntilCancelled { post_only: true }
2177        );
2178        if marketable && post_only {
2179            let released =
2180                reservation.map(|reservation| self.release_reservation(reservation, time_exchange));
2181            return self.cancel_unfilled(order, released, time_exchange);
2182        }
2183
2184        // What the market has to give this order: nothing if it does not cross, and all of it
2185        // wherever no size is known.
2186        let filled_quantity = match self.depth_available(&order.key.instrument, side) {
2187            _ if !marketable => Decimal::ZERO,
2188            Some(available) => quantity.min(available),
2189            None => quantity,
2190        };
2191        let unfilled = quantity - filled_quantity;
2192
2193        // Drawn only if the order trades, and handed back if the ledger refuses it, as in
2194        // `fill_on_arrival`.
2195        let slippage_before = self.slippage.clone();
2196        let position = self.net_position(terms, &order.key.instrument);
2197        let fill = (filled_quantity > Decimal::ZERO).then(|| {
2198            // A crossed market prices it, but a model that needs more of a book than the market
2199            // has falls back to the bar's reference price, or to the limit, as on arrival.
2200            let price = self
2201                .fill_model
2202                .fill_price(&FillContext::new(side, quantity, &market))
2203                .or(reference)
2204                .unwrap_or(limit);
2205            let price = clamp_to_limit(side, self.slippage.apply(side, price), limit);
2206            let settlement = self.fill_settlement(
2207                terms,
2208                position,
2209                side,
2210                filled_quantity,
2211                price,
2212                Liquidity::Taker,
2213            );
2214            (price, settlement)
2215        });
2216
2217        // What resting the rest will cost, priced as the fill that settles it will be: at the
2218        // limit, as the maker, against the position the fill leaves.
2219        let held = (unfilled > Decimal::ZERO).then(|| {
2220            let position = match &fill {
2221                Some((price, _)) => position.after(side, filled_quantity, *price),
2222                None => position,
2223            };
2224            self.settlement(terms, position, side, unfilled, limit, Liquidity::Maker)
2225        });
2226
2227        let (debit, credit) = match (&fill, &held) {
2228            (Some((_, fill)), Some(held)) => (fill.settled_holding(held), Some(&fill.credit)),
2229            (Some((_, fill)), None) => (fill.settled(), Some(&fill.credit)),
2230            (None, Some(held)) => (held.reserved(), None),
2231            (None, None) => unreachable!(
2232                "SimulatedVenue released {} with nothing to trade or rest: it holds only an order \
2233                 with a positive quantity, which `open_order_inner` checks, and such an order \
2234                 either trades or rests some of it",
2235                order.key.cid
2236            ),
2237        };
2238        let effect = reservation.as_ref().map_or_else(
2239            || self.margin_effect(None),
2240            |reservation| reservation.effect,
2241        );
2242        let (mut committed, loan, excess_borrowed) =
2243            match self.commit_afresh(terms, reservation, &debit, credit, time_exchange) {
2244                Recommitted::Committed {
2245                    commitment,
2246                    loan,
2247                    excess_borrowed,
2248                } => (commitment, loan, excess_borrowed),
2249                Recommitted::Unaffordable { released } => {
2250                    self.slippage = slippage_before;
2251                    return self.cancel_unfilled(order, released, time_exchange);
2252                }
2253            };
2254
2255        let mut events = Vec::with_capacity(4);
2256        let mut filled_total = order.state.filled_quantity;
2257        let mut trade = None;
2258        if let Some((price, settlement)) = fill {
2259            self.take_fee_apart(&settlement, &mut committed, time_exchange);
2260            self.consume_depth(&order.key.instrument, side, filled_quantity);
2261            let fill = Fill {
2262                quantity: filled_quantity,
2263                price,
2264            };
2265            trade = Some(self.print_fill(terms, &order, fill, settlement.fees, time_exchange));
2266            filled_total += filled_quantity;
2267        }
2268
2269        let terminal = match held {
2270            // What is left joins the book, carrying what has already traded, holding the loan
2271            // this release left the order with, as a remainder rested on arrival does.
2272            Some(held) => {
2273                let open = Open::new(order.state.id.clone(), time_exchange, filled_total);
2274                let rested = Order {
2275                    key: order.key,
2276                    side,
2277                    price: order.price,
2278                    quantity: order.quantity,
2279                    kind: order.kind,
2280                    time_in_force: order.time_in_force,
2281                    state: open,
2282                };
2283                // A release that traded reports every charge after its trade. One that did not
2284                // leaves them with no position to belong to, so they wait with the remainder, as
2285                // on arrival.
2286                let unreported_interest = match trade {
2287                    Some(_) => Vec::new(),
2288                    None => {
2289                        let mut held = std::mem::take(&mut committed.held_interest);
2290                        held.extend(committed.interest.take().map(|flow| *flow));
2291                        held
2292                    }
2293                };
2294                self.book_rested(
2295                    rested.clone(),
2296                    Reservation {
2297                        borrowed: loan.min(held.requirement()),
2298                        excess_borrowed,
2299                        amount: held.requirement(),
2300                        asset: held.asset,
2301                        effect,
2302                        unreported_interest,
2303                    },
2304                );
2305                rested.map_state(OrderState::active)
2306            }
2307            // Filled outright by this one trade, which carries the order's id: a held order has
2308            // traded nothing before its release, so the trade's price is the mean of its fills.
2309            None => {
2310                let Some(trade) = &trade else {
2311                    unreachable!("an order with nothing left to rest has traded");
2312                };
2313                let filled = Order {
2314                    key: order.key,
2315                    side,
2316                    price: order.price,
2317                    quantity: order.quantity,
2318                    kind: order.kind,
2319                    time_in_force: order.time_in_force,
2320                    state: Filled::new(
2321                        trade.order_id.clone(),
2322                        time_exchange,
2323                        order.quantity,
2324                        Some(trade.price),
2325                    ),
2326                };
2327                self.account.ack_filled(filled.clone());
2328                filled.map_state(OrderState::fully_filled)
2329            }
2330        };
2331
2332        self.push_committed(&mut events, committed, trade);
2333        events.push(self.build_account_event(Snapshot(terminal)));
2334        events
2335    }
2336
2337    /// Records `fill` of `order`, an order this venue already holds, and returns the trade it
2338    /// printed, acknowledged in the ledger.
2339    ///
2340    /// The trade's cumulative is everything the order had already done plus `fill`.
2341    fn print_fill(
2342        &mut self,
2343        terms: &InstrumentTerms,
2344        order: &OpenOrder,
2345        fill: Fill,
2346        fees: AssetFees<AssetNameExchange>,
2347        time_exchange: DateTime<Utc>,
2348    ) -> Trade<AssetNameExchange, InstrumentNameExchange> {
2349        let Fill { quantity, price } = fill;
2350        self.record_fill(terms, &order.key.instrument, order.side, fill);
2351
2352        let trade = Trade {
2353            id: self.trade_id_sequence_fetch_add(),
2354            order_id: order.state.id.or_client_id(&order.key.cid),
2355            instrument: order.key.instrument.clone(),
2356            strategy: order.key.strategy.clone(),
2357            time_exchange,
2358            side: order.side,
2359            price,
2360            quantity,
2361            order_filled_quantity: Some(order.state.filled_quantity + quantity),
2362            fees,
2363            origin: TradeOrigin::Order,
2364            position_side: None,
2365            client_order_id: Some(order.key.cid.clone()),
2366            time_source: TimeSource::Venue,
2367        };
2368
2369        self.account.ack_trade(trade.clone());
2370        trade
2371    }
2372
2373    /// Moves the balances one resting fill pays with and receives, and returns the restatements it
2374    /// owes.
2375    ///
2376    /// # A cash-settled fill is costed again when it fills
2377    /// What a CFD order holds was computed against the position when it rested, and the position
2378    /// may have moved since: another order may have closed what this one was going to close, so
2379    /// this one now opens a position instead. Its hold is therefore released and the fill costed
2380    /// afresh, as one ledger move with one restatement. If the account can no longer afford it,
2381    /// the result is [`RestingSettlement::Unaffordable`] and the order is cancelled, as a venue
2382    /// re-checking margin at fill time would cancel it, rather than filled with balance the account
2383    /// does not have. A spot fill whose fee exceeds what it receives is the same: its hold covers
2384    /// what it pays with, not the excess fee taken from the asset it receives, so it is checked
2385    /// afresh.
2386    ///
2387    /// # Panics
2388    /// Panics if a spot order the venue holds nothing against cannot be afforded when it fills —
2389    /// see this type's caller obligations on `initial_state` — unless its fee exceeds what it
2390    /// receives, which is checked afresh and cancelled like a CFD order.
2391    fn settle_resting(
2392        &mut self,
2393        order: &OpenOrder,
2394        terms: &InstrumentTerms,
2395        reservation: Option<Reservation>,
2396        settlement: &Settlement,
2397        time_exchange: DateTime<Utc>,
2398    ) -> RestingSettlement {
2399        let delivery = match &settlement.credit {
2400            // A fill whose fee is taken apart costs less than its order holds, the cost of its
2401            // fee falling back, whenever that fee would have been on top of what it pays with.
2402            _ if settlement.fee_apart => {
2403                return self.settle_resting_afresh(terms, reservation, settlement, time_exchange);
2404            }
2405            Credit::Delivery(delivery) if !delivery.amount.is_sign_negative() => delivery,
2406            // A cash-settled fill, or a spot fill whose fee exceeds what it receives.
2407            Credit::PaidBack(_) | Credit::Delivery(_) => {
2408                return self.settle_resting_afresh(terms, reservation, settlement, time_exchange);
2409            }
2410        };
2411
2412        // An order seeded by `initial_state` holds nothing, and takes the venue's default effect.
2413        let effect = reservation.as_ref().map_or_else(
2414            || self.margin_effect(None),
2415            |reservation| reservation.effect,
2416        );
2417        let Some(Reservation {
2418            asset,
2419            amount,
2420            unreported_interest,
2421            ..
2422        }) = reservation
2423        else {
2424            // Seeded by a configured `initial_state`, so nothing is held against it and the fill
2425            // debits now. An account that cannot cover it is a mis-specified fixture, in the same
2426            // class as an absent balance, and gets the same treatment, unless a margin account
2427            // lends what it lacks.
2428            let debit = settlement.settled();
2429            return RestingSettlement::Settled(
2430                self.commit(
2431                    &debit,
2432                    Some(&settlement.credit),
2433                    effect,
2434                    terms,
2435                    None,
2436                    time_exchange,
2437                )
2438                .unwrap_or_else(|insufficient| {
2439                    panic!(
2440                        "SimulatedVenue cannot afford the fill of resting order {}, which was \
2441                             seeded by `initial_state` and so reserved nothing: {} of {} free, {} \
2442                             required",
2443                        order.key.cid, insufficient.free, insufficient.asset, insufficient.required
2444                    )
2445                }),
2446            );
2447        };
2448
2449        // Reserve exactly what will be settled, then settle exactly what was reserved: the client
2450        // sees one balance restatement per asset, and never a balance the account did not hold.
2451        // Both sides are the unfilled remainder, priced at the order's own limit as the maker --
2452        // `rest_order` holds for it and this settles it, so the two agree by construction rather
2453        // than by two formulas coinciding. Only spot reaches this: what a spot fill costs does not
2454        // depend on a position, so nothing can have moved it since the order rested.
2455        debug_assert!(
2456            asset == settlement.asset && amount == settlement.amount,
2457            "resting order {} reserved {amount} of {asset} but its fill settles {} of {}: the \
2458             reservation and the fill disagree, so a balance the account never held would be \
2459             reported",
2460            order.key.cid,
2461            settlement.amount,
2462            settlement.asset
2463        );
2464
2465        // A loan taken when the order was placed stays owed: it paid for this fill, and the
2466        // interest it was charged is reported after it.
2467        let paid = self.account.settle(&asset, amount, time_exchange);
2468        let received = self.account.deliver(delivery, effect, time_exchange);
2469        RestingSettlement::Settled(Commitment {
2470            held_interest: unreported_interest,
2471            ..Commitment::from(Committed {
2472                paid,
2473                received: Some(received),
2474                borrowed: Decimal::ZERO,
2475                excess_borrowed: Decimal::ZERO,
2476            })
2477        })
2478    }
2479
2480    /// Releases a resting fill's hold and settles the fill afresh, restating each asset it moved
2481    /// once, or reports it unaffordable. See [`settle_resting`](Self::settle_resting).
2482    ///
2483    /// On a margin account, a loan the order took when it was placed stays owed while the fill is
2484    /// costed afresh, which may borrow more. A fill that costs less than was held repays the part
2485    /// of the loan it no longer needs, and an order that turns out unaffordable repays all of it,
2486    /// as a cancelled order does.
2487    fn settle_resting_afresh(
2488        &mut self,
2489        terms: &InstrumentTerms,
2490        reservation: Option<Reservation>,
2491        settlement: &Settlement,
2492        time_exchange: DateTime<Utc>,
2493    ) -> RestingSettlement {
2494        match self.commit_afresh(
2495            terms,
2496            reservation,
2497            &settlement.settled(),
2498            Some(&settlement.credit),
2499            time_exchange,
2500        ) {
2501            Recommitted::Committed { commitment, .. } => RestingSettlement::Settled(commitment),
2502            Recommitted::Unaffordable { released } => RestingSettlement::Unaffordable { released },
2503        }
2504    }
2505
2506    /// Releases an order's hold and commits `debit` and `credit` afresh in its place, restating
2507    /// each asset it moved once, or reports the account cannot cover them. See
2508    /// [`settle_resting_afresh`](Self::settle_resting_afresh) and [`Recommitted`].
2509    fn commit_afresh(
2510        &mut self,
2511        terms: &InstrumentTerms,
2512        mut reservation: Option<Reservation>,
2513        debit: &Debit,
2514        credit: Option<&Credit>,
2515        time_exchange: DateTime<Utc>,
2516    ) -> Recommitted {
2517        // Reported after the fill this commits, or with the cancel if the account cannot cover
2518        // it: either way the order's hold is done with.
2519        let held_interest = reservation
2520            .as_mut()
2521            .map(|reservation| std::mem::take(&mut reservation.unreported_interest))
2522            .unwrap_or_default();
2523        let effect = reservation.as_ref().map_or_else(
2524            || self.margin_effect(None),
2525            |reservation| reservation.effect,
2526        );
2527        let released = reservation.map(|reservation| {
2528            let balance =
2529                self.account
2530                    .release(&reservation.asset, reservation.amount, time_exchange);
2531            (reservation, balance)
2532        });
2533
2534        let repays_at_cancel = self.repays_at_cancel();
2535        match self.commit(debit, credit, effect, terms, None, time_exchange) {
2536            Ok(mut commitment) => {
2537                let mut loan = commitment.committed.shortfall_borrowed();
2538                let mut excess_borrowed = commitment.committed.excess_borrowed;
2539                if let Some((reservation, _)) = &released {
2540                    loan += reservation.borrowed;
2541                    excess_borrowed += reservation.excess_borrowed;
2542                }
2543                // A fill that cost less than was held needs less of the loan; the account's own
2544                // funds pay first, so the unused loan is the smaller of the loan and the surplus.
2545                // It is repaid only if the venue repays what its orders no longer need.
2546                if let Some((reservation, _)) = released.filter(|_| repays_at_cancel) {
2547                    let surplus = reservation.amount - (debit.settled + debit.reserved);
2548                    let unused = reservation.borrowed.min(surplus);
2549                    if unused > Decimal::ZERO {
2550                        debug_assert_eq!(commitment.committed.paid.asset, reservation.asset);
2551                        commitment.committed.paid =
2552                            self.account
2553                                .repay_loan(&reservation.asset, unused, time_exchange);
2554                        loan -= unused;
2555                    }
2556                }
2557                commitment.held_interest = held_interest;
2558                Recommitted::Committed {
2559                    commitment,
2560                    loan,
2561                    excess_borrowed,
2562                }
2563            }
2564            Err(_) => Recommitted::Unaffordable {
2565                released: released.map(|(reservation, balance)| Released {
2566                    balance: match !repays_at_cancel || reservation.loan().is_zero() {
2567                        true => balance,
2568                        false => self.account.repay_loan(
2569                            &reservation.asset,
2570                            reservation.loan(),
2571                            time_exchange,
2572                        ),
2573                    },
2574                    interest: held_interest,
2575                }),
2576            },
2577        }
2578    }
2579
2580    /// Appends the balances one commitment restated, the asset paid with first, then `trade`, the
2581    /// fill they paid for, then the cash flows reporting the interest its loans were charged: what
2582    /// the order's hold was charged, then what this commitment borrowed.
2583    ///
2584    /// The balances come before the fill that spent them, and the interest after it, once the
2585    /// position the loan funds exists, so a consumer attributing the interest to that position
2586    /// finds it. The balances already include the interest, so a consumer applying the events in
2587    /// order never holds more than the account does. With no `trade` the interest follows the
2588    /// balances, so nothing a caller left in `commitment` goes unreported.
2589    fn push_committed(
2590        &self,
2591        events: &mut Vec<UnindexedAccountEvent>,
2592        commitment: Commitment,
2593        trade: Option<Trade<AssetNameExchange, InstrumentNameExchange>>,
2594    ) {
2595        let Commitment {
2596            committed: Committed { paid, received, .. },
2597            interest,
2598            held_interest,
2599            fee,
2600        } = commitment;
2601        // The instant the interest is reported: the fill's, or with no fill the restatement's.
2602        let reported_at = trade
2603            .as_ref()
2604            .map_or(paid.time_exchange, |trade| trade.time_exchange);
2605        events.push(self.build_account_event(Snapshot(paid)));
2606        if let Some(received) = received {
2607            events.push(self.build_account_event(Snapshot(received)));
2608        }
2609        if let Some(fee) = fee {
2610            events.push(self.build_account_event(Snapshot(*fee)));
2611        }
2612        if let Some(trade) = trade {
2613            events.push(self.build_account_event(trade));
2614        }
2615        self.push_interest(
2616            events,
2617            held_interest.into_iter().chain(interest.map(|flow| *flow)),
2618            reported_at,
2619        );
2620    }
2621
2622    /// Appends the cash flows reporting `interest` a loan was charged, each stamped
2623    /// `time_exchange`, the instant it is reported. See [`Reservation::unreported_interest`] for
2624    /// why a charge can be reported later than it was made.
2625    fn push_interest(
2626        &self,
2627        events: &mut Vec<UnindexedAccountEvent>,
2628        interest: impl IntoIterator<Item = CashFlow<AssetNameExchange, InstrumentNameExchange>>,
2629        time_exchange: DateTime<Utc>,
2630    ) {
2631        events.extend(interest.into_iter().map(|mut flow| {
2632            flow.time_exchange = time_exchange;
2633            self.build_account_event(AccountEventKind::CashFlow(flow))
2634        }));
2635    }
2636
2637    /// Takes a fill's fee out of its asset's free balance when `settlement` says it is
2638    /// [taken apart](Settlement::fee_apart), adding that balance's restatement to `commitment`.
2639    fn take_fee_apart(
2640        &mut self,
2641        settlement: &Settlement,
2642        commitment: &mut Commitment,
2643        time_exchange: DateTime<Utc>,
2644    ) {
2645        // A fee discounted to nothing takes nothing, from a balance the account may never have
2646        // held.
2647        if !settlement.fee_apart || settlement.fees.fees.is_zero() {
2648            return;
2649        }
2650        // `fee_in_fee_asset` saw the free balance cover the fee before the fill moved anything,
2651        // and the fill exchanges neither its base nor its quote for the fee asset, nor lends it,
2652        // so the balance still covers it.
2653        commitment.fee = Some(Box::new(self.account.charge(
2654            &settlement.fees.asset,
2655            -settlement.fees.fees,
2656            time_exchange,
2657        )));
2658    }
2659
2660    /// Commits one arrival's whole ledger effect through [`AccountState::commit`], lending what
2661    /// [`borrowable`](Self::borrowable) and the asset's borrow cap allow, and at least the asset's
2662    /// minimum borrow, to an order whose `effect` borrows, then charges interest on whatever it
2663    /// lent.
2664    ///
2665    /// The interest is charged at `time_exchange` on the amount lent, so the restated
2666    /// [`Committed::paid`] includes it, and [`Commitment::interest`] reports it.
2667    ///
2668    /// # Errors
2669    /// As [`AccountState::commit`]: the ledger is left untouched and nothing is charged.
2670    fn commit(
2671        &mut self,
2672        debit: &Debit,
2673        credit: Option<&Credit>,
2674        effect: MarginEffect,
2675        terms: &InstrumentTerms,
2676        market: Option<&MarketSnapshot>,
2677        time_exchange: DateTime<Utc>,
2678    ) -> Result<Commitment, BalanceInsufficient> {
2679        let lending = Lending {
2680            borrowable: self.borrowable(debit, effect, terms, market),
2681            min_borrow: self
2682                .spot_margin
2683                .as_ref()
2684                .map_or(Decimal::ZERO, |config| config.min_borrow(&debit.asset)),
2685            max_borrow: self
2686                .spot_margin
2687                .as_ref()
2688                .and_then(|config| config.max_borrow(&debit.asset)),
2689        };
2690        let mut committed = self
2691            .account
2692            .commit(debit, credit, effect, lending, time_exchange)?;
2693
2694        let interest = match committed.borrowed > Decimal::ZERO {
2695            true => self
2696                .charge_interest(
2697                    &debit.asset,
2698                    committed.borrowed,
2699                    InterestDue::Borrow(time_exchange),
2700                )
2701                .map(|(balance, flow)| {
2702                    committed.paid = balance;
2703                    Box::new(flow)
2704                }),
2705            false => None,
2706        };
2707        Ok(Commitment {
2708            interest,
2709            ..Commitment::from(committed)
2710        })
2711    }
2712
2713    /// Cancels an order the venue held and will not fill, and reports it: a resting order whose
2714    /// fill the account can no longer afford, or a held market order its bar could not fill.
2715    ///
2716    /// The released hold restates first, so a consumer applying the events in order never sees the
2717    /// order retired against a balance that still holds its reservation. Interest a loan for the
2718    /// order was charged when it was placed follows it, reported now since the order never traded:
2719    /// `[balance, cash flow.., order]`.
2720    fn cancel_unfilled(
2721        &mut self,
2722        order: OpenOrder,
2723        released: Option<Released>,
2724        time_exchange: DateTime<Utc>,
2725    ) -> Vec<UnindexedAccountEvent> {
2726        let flows = released
2727            .as_ref()
2728            .map_or(0, |released| released.interest.len());
2729        let mut events = Vec::with_capacity(2 + flows);
2730        if let Some(Released { balance, interest }) = released {
2731            events.push(self.build_account_event(Snapshot(balance)));
2732            self.push_interest(&mut events, interest, time_exchange);
2733        }
2734
2735        let id = order.state.id.or_client_id(&order.key.cid);
2736        let cancelled = Order {
2737            key: order.key,
2738            side: order.side,
2739            price: order.price,
2740            quantity: order.quantity,
2741            kind: order.kind,
2742            time_in_force: order.time_in_force,
2743            state: Cancelled {
2744                id,
2745                time_exchange,
2746                filled_quantity: Some(order.state.filled_quantity),
2747            },
2748        };
2749        self.account.ack_cancelled(cancelled.clone());
2750        events.push(self.build_account_event(Snapshot(UnindexedOrder::from(cancelled))));
2751
2752        events
2753    }
2754
2755    /// Every instrument this venue trades, keyed by its exchange name, exactly as it was
2756    /// constructed with.
2757    ///
2758    /// Read only: the set of instruments is fixed for the venue's life. To trade a different set,
2759    /// construct a new venue.
2760    pub fn instruments(
2761        &self,
2762    ) -> &FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
2763        &self.instruments
2764    }
2765
2766    /// The instrument this venue trades under `name`, or `None` if it trades none by that name.
2767    pub fn instrument(
2768        &self,
2769        name: &InstrumentNameExchange,
2770    ) -> Option<&Instrument<ExchangeId, AssetNameExchange>> {
2771        self.instruments.get(name)
2772    }
2773
2774    /// This venue's view of `instrument`, or `None` if nothing has fed it one.
2775    pub fn market(&self, instrument: &InstrumentNameExchange) -> Option<&VenueInstrumentMarket> {
2776        self.market.get(instrument)
2777    }
2778
2779    /// Number of orders this venue has booked.
2780    ///
2781    /// Also how far the next `OrderId` is past the first one this venue minted, which is `0` unless
2782    /// the seeded account state carried a decimal id: then the first is one past the highest such
2783    /// id, so a minted id never repeats a seeded one.
2784    ///
2785    /// Counts every order it gave an id to, including those that retired without ever trading. It
2786    /// is **not** a count of trades — one order can print more than once, and trade ids are minted
2787    /// from a sequence of their own.
2788    pub fn order_sequence(&self) -> u64 {
2789        self.order_sequence
2790    }
2791
2792    pub fn balances(&self, assets: &[AssetNameExchange]) -> Vec<AssetBalance<AssetNameExchange>> {
2793        // Empty slice means "return all" (consistent with account_snapshot behavior).
2794        self.account
2795            .balances()
2796            .filter(|balance| assets.is_empty() || assets.contains(&balance.asset))
2797            .cloned()
2798            .collect()
2799    }
2800
2801    /// How much more this account can borrow now for an order on `instrument` on `side`: what
2802    /// [`BorrowCapacityClient::fetch_borrow_capacity`] asks a simulated venue.
2803    ///
2804    /// A sell borrows `instrument`'s base asset, a buy its quote asset, which the result names.
2805    /// [`borrowable_now`](BorrowCapacity::borrowable_now) is the lesser of what the leverage limit
2806    /// and the asset's borrow cap leave, as [`SpotMarginConfig`]'s `# How much it lends` says.
2807    /// It is zero when that is below the asset's minimum borrow, which no loan may be under, and
2808    /// while a liquidation is under way, when this venue lends nothing.
2809    /// [`account_limit`](BorrowCapacity::account_limit) is the asset's cap, unset when it has none.
2810    ///
2811    /// It is the room to borrow alone. The account's free balance of the asset is not added,
2812    /// though an order spends that before it borrows. Resting orders need no adjustment: what they
2813    /// borrowed when placed is in the debt, and what they hold is still in the equity.
2814    ///
2815    /// # Valued without an order's market
2816    /// The query carries no market snapshot, so only the markets this venue holds value the
2817    /// account. When they cannot value the asset, or any balance the account holds or owes,
2818    /// `borrowable_now` is `None`, unknown, where an order, which brings its own market, may still
2819    /// be lent. A cap that leaves nothing, or less than the minimum borrow, still makes it zero.
2820    /// A [`RequestPriced`](VenueRegime::RequestPriced) venue holds no market, so it can answer
2821    /// only for an account holding nothing but its valuation asset, asked about that asset.
2822    ///
2823    /// # Errors
2824    /// [`ApiError::InstrumentInvalid`] for an instrument this venue does not trade, and for one it
2825    /// never lends against: every instrument on a cash account (no spot margin configuration),
2826    /// and a cash-settled one on a margin account.
2827    ///
2828    /// [`BorrowCapacityClient::fetch_borrow_capacity`]: crate::client::BorrowCapacityClient::fetch_borrow_capacity
2829    pub fn borrow_capacity(
2830        &self,
2831        instrument: &InstrumentNameExchange,
2832        side: Side,
2833    ) -> Result<BorrowCapacity, UnindexedApiError> {
2834        let terms = self.instrument_terms(instrument)?;
2835        let config = match &self.spot_margin {
2836            Some(config) if !terms.cash_settled => config,
2837            Some(_) => {
2838                return Err(ApiError::InstrumentInvalid(
2839                    instrument.clone(),
2840                    format!(
2841                        "SimulatedVenue lends nothing against {instrument}: it is cash-settled"
2842                    ),
2843                ));
2844            }
2845            None => {
2846                return Err(ApiError::InstrumentInvalid(
2847                    instrument.clone(),
2848                    format!(
2849                        "SimulatedVenue lends nothing against {instrument}: it has no spot margin \
2850                         configuration"
2851                    ),
2852                ));
2853            }
2854        };
2855        let asset = match side {
2856            Side::Buy => &terms.underlying.quote,
2857            Side::Sell => &terms.underlying.base,
2858        };
2859
2860        let mut capacity = BorrowCapacity::new(asset.clone());
2861        let cap = config.max_borrow(asset);
2862        if let Some(cap) = cap {
2863            capacity = capacity.with_account_limit(cap);
2864        }
2865        let cap_room = cap.map(|cap| self.account.cap_room(asset, cap));
2866        let min_borrow = config.min_borrow(asset);
2867
2868        // The cap alone rules a loan out when it leaves none, or less than the least that may be
2869        // lent, whatever the leverage limit would leave.
2870        let ruled_out = self.liquidation.under_way()
2871            || cap_room.is_some_and(|room| room.is_zero() || room < min_borrow);
2872        let room = match ruled_out {
2873            true => Some(Decimal::ZERO),
2874            false => self
2875                .values_every_holding(config.valuation_asset())
2876                .then(|| self.leverage_room(config, asset, None))
2877                .flatten()
2878                .map(|room| cap_room.map_or(room, |cap_room| cap_room.min(room))),
2879        };
2880        if let Some(room) = room {
2881            capacity = capacity.with_borrowable_now(match room < min_borrow {
2882                true => Decimal::ZERO,
2883                false => room,
2884            });
2885        }
2886        Ok(capacity)
2887    }
2888
2889    /// Whether the markets this venue holds value, in `valuation`, every asset the account holds
2890    /// any of. The leverage limit leaves a holding it cannot value out of the equity, which
2891    /// understates what an order may borrow, but a query that cannot value one does not know.
2892    /// A debt it cannot value already stops [`leverage_room`](Self::leverage_room).
2893    fn values_every_holding(&self, valuation: &AssetNameExchange) -> bool {
2894        self.account.balances().all(|balance| {
2895            balance.balance.total.is_zero()
2896                || self.price_in(&balance.asset, valuation, None).is_some()
2897        })
2898    }
2899
2900    pub fn orders_open(
2901        &self,
2902        instruments: &[InstrumentNameExchange],
2903    ) -> Vec<Order<ExchangeId, InstrumentNameExchange, Open>> {
2904        // Empty slice means "return all" (consistent with account_snapshot behavior).
2905        self.account
2906            .orders_open()
2907            .filter(|order| instruments.is_empty() || instruments.contains(&order.key.instrument))
2908            .cloned()
2909            .collect()
2910    }
2911
2912    /// How each of `orders` ended, for those that have: filled, cancelled (including the
2913    /// unfilled remainder of a market order) or expired at its own deadline.
2914    ///
2915    /// An order still open is omitted, and so is one this venue never held, including one it
2916    /// rejected on arrival, whose rejection was the answer to its open request. An empty `orders`
2917    /// returns nothing, not every ended order.
2918    ///
2919    /// Each order is found by its client order id, which this venue keys every order on, and is
2920    /// returned under the key it was asked for. A key naming a different instrument from the one
2921    /// the order traded is treated as unknown, and an id asked about more than once is reported
2922    /// once, under the first key that finds it.
2923    pub fn orders_ended(&self, orders: &[UnindexedOrderKey]) -> Vec<UnindexedInactiveOrder> {
2924        let mut reported = FnvHashSet::default();
2925        orders
2926            .iter()
2927            .filter_map(|key| {
2928                let order = self
2929                    .account
2930                    .order_ended(&key.cid)
2931                    .filter(|order| order.key.instrument == key.instrument)?;
2932                reported.insert(&key.cid).then(|| Order {
2933                    key: key.clone(),
2934                    ..order
2935                })
2936            })
2937            .collect()
2938    }
2939
2940    pub fn trades(
2941        &self,
2942        time_since: DateTime<Utc>,
2943    ) -> Vec<Trade<AssetNameExchange, InstrumentNameExchange>> {
2944        self.account.trades(time_since).cloned().collect()
2945    }
2946
2947    /// Books an open against the ledger and reports everything it owes the client.
2948    ///
2949    /// The venue books its own fill: the `Trade` is acknowledged into the ledger here, so a driver
2950    /// cannot forget to. A filled open reports `[balance, trade]` -- `[paid, received, trade]` for
2951    /// spot, which moves two assets -- mirroring a real venue where the trade is booked before the
2952    /// order can be reported `FullyFilled`; an open that rests reports `[balance]` alone -- the
2953    /// reservation now held against it -- and no trade, because nothing traded.
2954    ///
2955    /// A Market order has no limit price, and any price it carries is ignored, so the venue prices
2956    /// it from whichever market its [`VenueRegime`] gives it -- its own on a
2957    /// [`MarketDriven`](VenueRegime::MarketDriven) venue, and
2958    /// [`RequestOpen::market`](crate::order::request::RequestOpen::market) on a
2959    /// [`RequestPriced`](VenueRegime::RequestPriced) one. A Limit order is always judged and priced
2960    /// against this venue's own market; see the type's `# How an order is priced`.
2961    ///
2962    /// An order whose quantity is zero or negative is rejected, moving nothing, whatever the
2963    /// market: with an [`ApiError::OrderRejected`] naming the quantity, unless its kind or time in
2964    /// force is refused first.
2965    ///
2966    /// # Panics
2967    /// If a configured [`HoldingCostModel`] or [`MarginInterestModel`] breaks its contract: a
2968    /// boundary not after the instant it was asked from, or a negative interest.
2969    pub fn open_order(
2970        &mut self,
2971        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
2972    ) -> OpenOutcome {
2973        // Chosen here so one path holds one snapshot: threading both through would allow two copies
2974        // to disagree, with nothing to arbitrate.
2975        let market = self.pricing_market(&request);
2976        let (response, notifications) = self.open_order_inner(request, market);
2977
2978        // Everything is already committed -- the trade acknowledged, the order recorded or booked,
2979        // the ledger moved. All that is left is to package it in the order a driver must deliver
2980        // it: the balance that pays for a fill before the fill that spent it, and the interest a
2981        // loan for it was charged after it, once the position it funds exists.
2982        let mut events = match notifications {
2983            Some(OpenOrderNotifications::Filled { commitment, trade }) => {
2984                let mut events = Vec::with_capacity(3);
2985                self.push_committed(&mut events, commitment, Some(trade));
2986                events
2987            }
2988            // Already on the book: `open_order_inner` put it there, holding the reservation this
2989            // balance restates.
2990            // Any interest a loan for it was charged waits in its reservation until it trades or
2991            // ends: see `Reservation::unreported_interest`.
2992            Some(OpenOrderNotifications::Rested { balance }) => {
2993                vec![self.build_account_event(balance)]
2994            }
2995            None => Vec::new(),
2996        };
2997        self.refresh_next_interest_if_loans_changed();
2998        self.report_debt_changes(&mut events);
2999
3000        VenueOutcome { events, response }
3001    }
3002
3003    /// Takes a resting order off the book, releasing whatever is held against it.
3004    ///
3005    /// Returns `[balance]` and a [`Cancelled`] carrying the quantity filled before the cancel
3006    /// arrived -- which is whatever the order had done: nothing for one that rested untouched, the
3007    /// taker part for one the book could only partly fill on arrival, and whatever a configured
3008    /// `initial_state` seeded, carried through rather than added to. An order the venue holds
3009    /// nothing against releases nothing and reports no balance; see this type's note on
3010    /// `initial_state`.
3011    ///
3012    /// # A cancel that finds nothing says which nothing it found
3013    /// A cancel racing its own order's fill is the reason this distinguishes three cases rather
3014    /// than rejecting flatly. With a non-zero `to_venue` latency the fill can win, and
3015    /// [`ApiError::OrderAlreadyFullyFilled`] is what tells the caller to reconcile against the fill
3016    /// rather than retry. An order cancelled twice gets [`ApiError::OrderAlreadyCancelled`], and
3017    /// only an id this venue has never booked is rejected as unknown.
3018    ///
3019    /// # Panics
3020    /// If the configured [`MarginInterestModel`] breaks its contract with a boundary not after the
3021    /// instant it was asked from, when the venue next looks for one after a loan is repaid.
3022    pub fn cancel_order(
3023        &mut self,
3024        request: OrderRequestCancel<ExchangeId, InstrumentNameExchange>,
3025    ) -> CancelOutcome {
3026        let time_exchange = self.time_exchange();
3027
3028        let Some(RestingOrder { order, reservation }) = self.account.remove_order(&request.key.cid)
3029        else {
3030            return VenueOutcome {
3031                events: Vec::new(),
3032                response: UnindexedOrderResponseCancel {
3033                    state: Err(self.cancel_missing_reason(&request.key.cid)),
3034                    key: request.key,
3035                },
3036            };
3037        };
3038
3039        // Released before the response is built, so a request arriving after this one sees the
3040        // freed balance regardless of when a driver delivers the restatement.
3041        // Interest a loan for it was charged when it was placed is reported now, since it never
3042        // traded.
3043        let mut events = match reservation {
3044            Some(reservation) => {
3045                let Released { balance, interest } =
3046                    self.release_reservation(reservation, time_exchange);
3047                let mut events = vec![self.build_account_event(Snapshot(balance))];
3048                self.push_interest(&mut events, interest, time_exchange);
3049                events
3050            }
3051            None => Vec::new(),
3052        };
3053        self.refresh_next_interest_if_loans_changed();
3054        self.report_debt_changes(&mut events);
3055
3056        let cancelled = Cancelled {
3057            id: order.state.id.or_client_id(&order.key.cid),
3058            time_exchange,
3059            filled_quantity: Some(order.state.filled_quantity),
3060        };
3061
3062        self.account.ack_cancelled(Order {
3063            key: order.key,
3064            side: order.side,
3065            price: order.price,
3066            quantity: order.quantity,
3067            kind: order.kind,
3068            time_in_force: order.time_in_force,
3069            state: cancelled.clone(),
3070        });
3071
3072        VenueOutcome {
3073            events,
3074            response: UnindexedOrderResponseCancel {
3075                key: request.key,
3076                state: Ok(cancelled),
3077            },
3078        }
3079    }
3080
3081    /// Follows each balance restatement in `events` whose debt differs from the debt last reported
3082    /// for its asset with an [`AccountEventKind::MarginDebt`] stating the new debt, at the
3083    /// restatement's time. Nothing on an account that is not a spot-margin one.
3084    ///
3085    /// The debt is read off the restatements this venue already emits, so every path that moves a
3086    /// loan reports it without each one doing so itself. What changed is read from how: principal
3087    /// that rose is a borrow, principal or interest that fell a repay, and interest that rose
3088    /// alone an interest charge.
3089    fn report_debt_changes(&mut self, events: &mut Vec<UnindexedAccountEvent>) {
3090        if self.spot_margin.is_none() {
3091            return;
3092        }
3093        let mut index = 0;
3094        while index < events.len() {
3095            index += 1;
3096            let AccountEventKind::BalanceSnapshot(Snapshot(balance)) = &events[index - 1].kind
3097            else {
3098                continue;
3099            };
3100            let Some(debt) = balance.balance.margin else {
3101                continue;
3102            };
3103            let before = self
3104                .reported_debt
3105                .insert(balance.asset.clone(), debt)
3106                .unwrap_or_default();
3107            if before == debt {
3108                continue;
3109            }
3110            let kind = if debt.borrowed > before.borrowed {
3111                MarginDebtKind::Borrow
3112            } else if debt.borrowed < before.borrowed || debt.interest < before.interest {
3113                MarginDebtKind::Repay
3114            } else {
3115                MarginDebtKind::Interest
3116            };
3117            let update = MarginDebtUpdate::new(
3118                balance.asset.clone(),
3119                None,
3120                debt,
3121                kind,
3122                kind.to_smolstr(),
3123                balance.time_exchange,
3124            );
3125            events.insert(
3126                index,
3127                self.build_account_event(AccountEventKind::MarginDebt(update)),
3128            );
3129        }
3130    }
3131
3132    /// Why a cancel found no resting order under `cid`.
3133    fn cancel_missing_reason(&self, cid: &ClientOrderId) -> UnindexedOrderError {
3134        if self.account.is_filled(cid) {
3135            ApiError::OrderAlreadyFullyFilled.into()
3136        } else if self.account.is_cancelled(cid) {
3137            ApiError::OrderAlreadyCancelled.into()
3138        } else if self.account.is_expired(cid) {
3139            ApiError::OrderAlreadyExpired.into()
3140        } else {
3141            UnindexedOrderError::Rejected(ApiError::OrderRejected(format!(
3142                "SimulatedVenue is not holding an open order with {cid}"
3143            )))
3144        }
3145    }
3146
3147    pub fn account_snapshot(&self) -> UnindexedAccountSnapshot {
3148        let balances = self.account.balances().cloned().collect();
3149
3150        let orders_open = self
3151            .account
3152            .orders_open()
3153            .cloned()
3154            .map(UnindexedOrder::from);
3155
3156        let orders_cancelled = self
3157            .account
3158            .orders_cancelled()
3159            .cloned()
3160            .map(UnindexedOrder::from);
3161
3162        let orders_expired = self
3163            .account
3164            .orders_expired()
3165            .cloned()
3166            .map(UnindexedOrder::from);
3167
3168        let orders_all = orders_open.chain(orders_cancelled).chain(orders_expired);
3169        // Sorted on `(instrument, cid)` rather than `instrument` alone: the sort is unstable, so a
3170        // key shared by several orders leaves their relative order unspecified, and a snapshot that
3171        // lists the same account's orders in a different order on each run is not comparable
3172        // between runs. `cid` is unique per order, so the extended key is total.
3173        let orders_all = orders_all
3174            .sorted_unstable_by_key(|order| (order.key.instrument.clone(), order.key.cid.clone()));
3175        let orders_by_instrument = orders_all.chunk_by(|order| order.key.instrument.clone());
3176
3177        let mut orders_by_instrument: FnvHashMap<_, Vec<_>> = orders_by_instrument
3178            .into_iter()
3179            .map(|(instrument, orders)| (instrument, orders.collect()))
3180            .collect();
3181
3182        // Every instrument with an order, and every cash-settled instrument whether or not it has
3183        // one: a CFD's position is reported even while it is flat, which is what lets a consumer
3184        // tell "holds none" from "not reported".
3185        let mut instruments: Vec<_> = self
3186            .instruments
3187            .iter()
3188            .filter_map(|(name, instrument)| {
3189                let orders = orders_by_instrument.remove(name);
3190                let position = self.position_report(name, instrument);
3191                if orders.is_none() && position == PositionReport::Unreported {
3192                    return None;
3193                }
3194                Some(InstrumentAccountSnapshot {
3195                    instrument: name.clone(),
3196                    orders: orders.unwrap_or_default(),
3197                    // Read from the venue's own book, so every open order is here under the id it
3198                    // was placed with. Cancelled and expired orders are listed too, which only adds
3199                    // to what a consumer can see.
3200                    orders_complete: true,
3201                    position,
3202                    isolated: None,
3203                })
3204            })
3205            .collect();
3206
3207        // Orders a configured `initial_state` seeded on an instrument this venue does not trade,
3208        // which it still holds and so still reports.
3209        instruments.extend(
3210            orders_by_instrument
3211                .into_iter()
3212                .map(|(instrument, orders)| InstrumentAccountSnapshot {
3213                    instrument,
3214                    orders,
3215                    orders_complete: true,
3216                    position: PositionReport::Unreported,
3217                    isolated: None,
3218                }),
3219        );
3220        instruments.sort_unstable_by(|a, b| a.instrument.cmp(&b.instrument));
3221
3222        UnindexedAccountSnapshot {
3223            exchange: self.exchange,
3224            balances,
3225            instruments,
3226        }
3227    }
3228
3229    /// The position this venue reports for `instrument`.
3230    ///
3231    /// A cash-settled instrument reports its net position, [`Flat`](PositionReport::Flat) when it
3232    /// holds none. A spot holding is an asset balance on this venue rather than a position, so a
3233    /// spot instrument reports [`Unreported`](PositionReport::Unreported).
3234    fn position_report(
3235        &self,
3236        name: &InstrumentNameExchange,
3237        instrument: &Instrument<ExchangeId, AssetNameExchange>,
3238    ) -> PositionReport {
3239        if !matches!(instrument.kind, InstrumentKind::Cfd(_)) {
3240            return PositionReport::Unreported;
3241        }
3242
3243        let position = self.positions.get(name).copied().unwrap_or_default();
3244        PositionReport::from_position(Position::new(
3245            position.quantity,
3246            (!position.quantity.is_zero()).then_some(position.entry),
3247            None,
3248            None,
3249            None,
3250            None,
3251            None,
3252            self.time_exchange(),
3253        ))
3254    }
3255
3256    /// Decides one open's fate, moves the balance it pays with, and commits nothing else.
3257    ///
3258    /// Kept separate from [`open_order`](Self::open_order) rather than inlined: this body has
3259    /// several early returns, and the ordering contract -- emit the balance before the trade it
3260    /// paid for -- is the part a reader needs to find. Splitting keeps that contract in a short
3261    /// caller instead of at the end of a long one.
3262    ///
3263    /// Everything an arrival commits is committed *here*, by whichever function decides the
3264    /// order's fate: [`fill_on_arrival`](Self::fill_on_arrival) acknowledges its own trade and
3265    /// records what the order became, exactly as [`cancel_on_arrival`](Self::cancel_on_arrival),
3266    /// [`expire_on_arrival`](Self::expire_on_arrival) and [`rest_order`](Self::rest_order) already
3267    /// do. The caller is left owning event order and nothing else.
3268    ///
3269    /// `market` is the snapshot the request carried, and is used only by an order that is priced
3270    /// from it -- see the type's `# How an order is priced`.
3271    /// The market an arriving order is priced against, which is the venue's own wherever it has
3272    /// one.
3273    ///
3274    /// # Why the regime decides, and why there is no fallback
3275    /// A [`MarketDriven`](VenueRegime::MarketDriven) venue owns a book, and a fill is priced from
3276    /// the book the order reaches -- that is what a venue *is*. Its driver routes each market event
3277    /// to it before the client that will react sees it, and books the request at the instant it
3278    /// arrives, so this snapshot is the market as of that arrival: neither stale, nor from a future
3279    /// the sender could not have seen.
3280    ///
3281    /// [`RequestPriced`](VenueRegime::RequestPriced) has no book at all, so the snapshot the
3282    /// request carried is its only price source. That is the whole of the difference, and it is why
3283    /// a request-priced `MockExchange`'s results do not move.
3284    ///
3285    /// A `MarketDriven` venue that has not been fed this instrument answers `None` rather than
3286    /// falling back to the request's snapshot. A venue with no market for an instrument cannot fill
3287    /// a market order in it, and saying so is the observable failure; silently pricing from the
3288    /// requester's view would make the fill depend on which of the two happened to hold a price,
3289    /// and would re-admit exactly the request-priced behaviour the regime exists to distinguish.
3290    /// [`fill_on_arrival`](Self::fill_on_arrival) reports it as an unpriceable order, naming the
3291    /// absent snapshot.
3292    fn pricing_market(
3293        &self,
3294        request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3295    ) -> Option<MarketSnapshot> {
3296        match self.regime {
3297            VenueRegime::RequestPriced => request.state.market,
3298            // `VenueInstrumentMarket` is `Copy`, so this leaves no borrow outstanding.
3299            VenueRegime::MarketDriven => self
3300                .market(&request.key.instrument)
3301                .map(|market| market.snapshot),
3302        }
3303    }
3304
3305    fn open_order_inner(
3306        &mut self,
3307        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3308        market: Option<MarketSnapshot>,
3309    ) -> (
3310        Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
3311        Option<OpenOrderNotifications>,
3312    ) {
3313        if let Err(error) = self.validate_order_kind_supported(request.state.kind) {
3314            return (build_open_order_err_response(request, error), None);
3315        }
3316
3317        if let Err(error) =
3318            validate_time_in_force_supported(request.state.kind, request.state.time_in_force)
3319        {
3320            return (build_open_order_err_response(request, error), None);
3321        }
3322
3323        // An order for nothing, or for less than nothing, is refused as a venue refuses it. Every
3324        // path below takes the quantity as positive: a held order, for one, is released only to
3325        // trade or rest some of it.
3326        if request.state.quantity <= Decimal::ZERO {
3327            let reason = format!(
3328                "cannot open {} for {}: quantity {} is not positive",
3329                request.key.instrument, request.key.exchange, request.state.quantity
3330            );
3331            return (
3332                build_open_order_err_response(
3333                    request,
3334                    UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
3335                ),
3336                None,
3337            );
3338        }
3339
3340        // Read out before the `&mut self` balance borrows below.
3341        let terms = match self.instrument_terms(&request.key.instrument) {
3342            Ok(terms) => terms,
3343            Err(error) => return (build_open_order_err_response(request, error), None),
3344        };
3345
3346        // Before anything touches the ledger: an effect this venue cannot honour is refused rather
3347        // than placed without it, which would borrow, or not repay, against the caller's intent.
3348        if let Some(effect) = self.refused_margin_effect(request.state.margin_effect, &terms) {
3349            return (
3350                build_open_order_err_response(
3351                    request,
3352                    UnindexedOrderError::UnsupportedMarginEffect(effect),
3353                ),
3354                None,
3355            );
3356        }
3357
3358        // Before anything touches the ledger, and for every kind: see the type's note on client
3359        // order ids. It is also what makes a refused booking in `book_rested` unreachable.
3360        if self.account.orders().contains(&request.key.cid) {
3361            let reason = format!(
3362                "{} already names an order resting on {}",
3363                request.key.cid, request.key.exchange
3364            );
3365            return (
3366                build_open_order_err_response(request, ApiError::DuplicateClientOrderId(reason)),
3367                None,
3368            );
3369        }
3370
3371        // Before anything touches the ledger: a refused short leaves no trace on it.
3372        if let Err(error) = self.check_shortability(&request, &terms, market.as_ref()) {
3373            return (build_open_order_err_response(request, error), None);
3374        }
3375
3376        let now = self.time_exchange();
3377
3378        // Only a limit order's price is a limit, and a limit order without one has nothing to rest
3379        // at or bound a fill by. Refused before the stale-market gate below, so that the gate
3380        // never holds an order this venue would refuse anyway.
3381        let limit = match request.state.kind {
3382            OrderKind::Limit => match request.state.price {
3383                Some(limit) => Some(limit),
3384                None => {
3385                    let reason = format!(
3386                        "cannot open {} for {}: OrderKind::Limit carries no limit price",
3387                        request.key.instrument, request.key.exchange
3388                    );
3389                    return (
3390                        build_open_order_err_response(
3391                            request,
3392                            UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
3393                        ),
3394                        None,
3395                    );
3396                }
3397            },
3398            _ => None,
3399        };
3400
3401        // Under `StaleMarketPolicy::Wait`, an order arriving on a stale market is handled as if it
3402        // arrived just after the instrument's next fresh observation: held until then, or refused
3403        // if its time in force will not wait. An order already past its own deadline is not asked,
3404        // since it expires below whatever the market is doing.
3405        if let StaleMarketPolicy::Wait { max_age } = self.stale_market
3406            && !matches!(
3407                request.state.time_in_force,
3408                TimeInForce::GoodTillDate { expiry } if now >= expiry
3409            )
3410        {
3411            match self.market_age(&request.key.instrument, max_age, now) {
3412                MarketAge::NoFeed => {
3413                    let reason = format!(
3414                        "cannot accept {} on {}: StaleMarketPolicy::Wait needs a market feed to \
3415                         measure {}'s age from, and this venue has none",
3416                        request.key.cid, request.key.exchange, request.key.instrument
3417                    );
3418                    return (
3419                        build_open_order_err_response(
3420                            request,
3421                            UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
3422                        ),
3423                        None,
3424                    );
3425                }
3426                MarketAge::Stale { fresh_at } => {
3427                    if matches!(
3428                        request.state.time_in_force,
3429                        TimeInForce::ImmediateOrCancel | TimeInForce::FillOrKill
3430                    ) {
3431                        let last = fresh_at.map_or_else(
3432                            || "never observed fresh".to_owned(),
3433                            |fresh_at| format!("last observed fresh at {fresh_at}"),
3434                        );
3435                        let reason = format!(
3436                            "cannot fill {} on {}: {}'s market is stale under \
3437                             StaleMarketPolicy::Wait (max_age {max_age:?}; {last}), and \
3438                             TimeInForce::{} does not wait for a fresh observation",
3439                            request.key.cid,
3440                            request.key.exchange,
3441                            request.key.instrument,
3442                            request.state.time_in_force
3443                        );
3444                        return (
3445                            build_open_order_err_response(
3446                                request,
3447                                UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
3448                            ),
3449                            None,
3450                        );
3451                    }
3452                    return self.hold_on_arrival(request, market, &terms);
3453                }
3454                MarketAge::Fresh | MarketAge::Unpriced => {}
3455            }
3456        }
3457
3458        // `None` for every kind but a limit order, each of which is marketable on arrival by
3459        // definition.
3460        let Some(limit) = limit else {
3461            // Only the two aggressing arms an order with no limit price can reach, and `Expire`,
3462            // are reachable: `Rest` and `FillAndRest` need somewhere to rest, which such an order
3463            // has not got, and `CancelUnfilled` needs `post_only`, which the static gate already
3464            // refused on a market order. The remaining arms retire the order unfilled rather than
3465            // asserting, because filling an order this function could not account for is the one
3466            // wrong answer.
3467            //
3468            // The two aggressing arms differ in what becomes of a remainder the book could not
3469            // fill: `ImmediateOrCancel` keeps what it got and retires the rest, `FillOrKill`
3470            // would rather trade nothing -- see `Remainder`.
3471            //
3472            // Under `MarketOrderTiming::NextBar`, both aggressing arms wait for the bar instead,
3473            // and their time in force is applied when it fills them.
3474            return match disposition(request.state.kind, request.state.time_in_force, true, now) {
3475                Disposition::FillAndCancel | Disposition::FillOrNothing
3476                    if matches!(self.market_order_timing, MarketOrderTiming::NextBar(_)) =>
3477                {
3478                    self.hold_on_arrival(request, market, &terms)
3479                }
3480                Disposition::FillAndCancel => {
3481                    self.fill_on_arrival(request, market, &terms, Remainder::Cancel)
3482                }
3483                Disposition::FillOrNothing => {
3484                    self.fill_on_arrival(request, market, &terms, Remainder::Kill)
3485                }
3486                Disposition::Expire => self.expire_on_arrival(request, now),
3487                Disposition::FillAndRest | Disposition::CancelUnfilled | Disposition::Rest => {
3488                    self.cancel_on_arrival(request, now)
3489                }
3490            };
3491        };
3492
3493        // The same snapshot the order will be priced from, so marketability and price cannot be
3494        // decided against two different views of the book. `validate_order_kind_supported` has
3495        // already refused a limit order on a `RequestPriced` venue, so on this path
3496        // `pricing_market` is this venue's own market by construction.
3497        let marketable = market.is_some_and(|market| crosses(request.state.side, limit, &market));
3498
3499        match disposition(
3500            request.state.kind,
3501            request.state.time_in_force,
3502            marketable,
3503            now,
3504        ) {
3505            // The three differ only in what becomes of a remainder the book could not fill, which
3506            // is exactly what they are asked here -- see `Remainder`.
3507            Disposition::FillAndRest => {
3508                self.fill_on_arrival(request, market, &terms, Remainder::Rest { limit })
3509            }
3510            Disposition::FillAndCancel => {
3511                self.fill_on_arrival(request, market, &terms, Remainder::Cancel)
3512            }
3513            Disposition::FillOrNothing => {
3514                self.fill_on_arrival(request, market, &terms, Remainder::Kill)
3515            }
3516            // Nothing has traded, so the whole quantity is what rests.
3517            Disposition::Rest => self.rest_order(request, limit, &terms, Decimal::ZERO),
3518            Disposition::CancelUnfilled => self.cancel_on_arrival(request, now),
3519            Disposition::Expire => self.expire_on_arrival(request, now),
3520        }
3521    }
3522
3523    /// How old `instrument`'s market is at `now`, measured against `max_age` — see
3524    /// [`StaleMarketPolicy::Wait`].
3525    fn market_age(
3526        &self,
3527        instrument: &InstrumentNameExchange,
3528        max_age: Duration,
3529        now: DateTime<Utc>,
3530    ) -> MarketAge {
3531        if self.regime == VenueRegime::RequestPriced {
3532            return MarketAge::NoFeed;
3533        }
3534        let Some(market) = self
3535            .market(instrument)
3536            .filter(|market| !market.snapshot.is_empty())
3537        else {
3538            return MarketAge::Unpriced;
3539        };
3540        let Some(fresh_at) = market.fresh_at else {
3541            return MarketAge::Stale { fresh_at: None };
3542        };
3543        // A `max_age` too long for a `TimeDelta` is longer than any run, so nothing is stale.
3544        let stale = chrono::TimeDelta::from_std(max_age)
3545            .is_ok_and(|max_age| now.signed_duration_since(fresh_at) > max_age);
3546        if stale {
3547            MarketAge::Stale {
3548                fresh_at: Some(fresh_at),
3549            }
3550        } else {
3551            MarketAge::Fresh
3552        }
3553    }
3554
3555    /// Accepts an order to fill later, holding what it would cost at the venue's current market:
3556    /// a market order on its instrument's next bar under [`MarketOrderTiming::NextBar`], and any
3557    /// order arriving on a stale market on its next fresh observation under
3558    /// [`StaleMarketPolicy::Wait`] (or, combined with `NextBar`, its next fresh bar).
3559    ///
3560    /// What it would cost is what it would cost arriving now: a market order, or a limit order the
3561    /// market crosses, as a taker at the price the [`FillModel`] gives, capped at a limit order's
3562    /// limit; a limit order the market does not cross, or a post-only one, at its limit as the
3563    /// maker it would rest as.
3564    ///
3565    /// Refused instead, with nothing moved, when the order waits for a bar and the instrument has
3566    /// never been fed one (there is no next bar this venue can know to wait for), when the current
3567    /// market prices nothing for a market order, and when the account cannot cover the hold. The
3568    /// last two are what an arriving order would be refused for too, so a strategy is refused the
3569    /// same orders whether or not they wait.
3570    fn hold_on_arrival(
3571        &mut self,
3572        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3573        market: Option<MarketSnapshot>,
3574        terms: &InstrumentTerms,
3575    ) -> (
3576        Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
3577        Option<OpenOrderNotifications>,
3578    ) {
3579        if matches!(self.market_order_timing, MarketOrderTiming::NextBar(_))
3580            && self
3581                .market(&request.key.instrument)
3582                .and_then(|market| market.bar)
3583                .is_none()
3584        {
3585            let reason = format!(
3586                "cannot hold {} on {} for {}'s next bar: MarketOrderTiming::NextBar holds it for \
3587                 one and needs a bar feed, and this venue has been fed no bar for it",
3588                request.key.cid, request.key.exchange, request.key.instrument
3589            );
3590            return (
3591                build_open_order_err_response(
3592                    request,
3593                    UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
3594                ),
3595                None,
3596            );
3597        }
3598
3599        let side = request.state.side;
3600        let limit = match request.state.kind {
3601            OrderKind::Limit => request.state.price,
3602            _ => None,
3603        };
3604        let post_only = matches!(
3605            request.state.time_in_force,
3606            TimeInForce::GoodUntilCancelled { post_only: true }
3607        );
3608
3609        // The price the order would fill at now, unslipped: the hold is an estimate, released and
3610        // costed afresh when the order is released, so it takes no draw from the slippage stream.
3611        let taker_estimate = || {
3612            market.and_then(|market| {
3613                self.fill_model
3614                    .fill_price(&FillContext::new(side, request.state.quantity, &market))
3615            })
3616        };
3617        let (estimate, liquidity) = match limit {
3618            Some(limit)
3619                if !post_only && market.is_some_and(|market| crosses(side, limit, &market)) =>
3620            {
3621                // A crossed market prices it, but a model that needs more of a book than the
3622                // market has falls back to the limit, as on arrival.
3623                let estimate = taker_estimate().unwrap_or(limit);
3624                (
3625                    Some(clamp_to_limit(side, estimate, limit)),
3626                    Liquidity::Taker,
3627                )
3628            }
3629            Some(limit) => (Some(limit), Liquidity::Maker),
3630            None => (taker_estimate(), Liquidity::Taker),
3631        };
3632        let Some(estimate) = estimate else {
3633            let reason = format!(
3634                "cannot price {} for {}: no market price available yet and OrderKind::{:?} carries \
3635                 no limit price",
3636                request.key.instrument, request.key.exchange, request.state.kind
3637            );
3638            return (
3639                build_open_order_err_response(
3640                    request,
3641                    UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
3642                ),
3643                None,
3644            );
3645        };
3646
3647        let time_exchange = self.time_exchange();
3648        let settlement = self.settlement(
3649            terms,
3650            self.net_position(terms, &request.key.instrument),
3651            side,
3652            request.state.quantity,
3653            estimate,
3654            liquidity,
3655        );
3656
3657        let debit = settlement.reserved();
3658        let effect = self.margin_effect(request.state.margin_effect);
3659        let (committed, interest) =
3660            match self.commit(&debit, None, effect, terms, market.as_ref(), time_exchange) {
3661                // A fresh commitment holds no earlier order's interest, so `held_interest` is
3662                // empty.
3663                Ok(Commitment {
3664                    committed,
3665                    interest,
3666                    ..
3667                }) => (committed, interest),
3668                Err(insufficient) => {
3669                    return (
3670                        build_open_order_err_response(
3671                            request,
3672                            self.insufficient_error(insufficient, side),
3673                        ),
3674                        None,
3675                    );
3676                }
3677            };
3678
3679        let open = Open::new(
3680            VenueOrderId::Assigned(self.order_id_sequence_fetch_add()),
3681            time_exchange,
3682            Decimal::ZERO,
3683        );
3684        // A market order is held with no price whatever the request carried, since a price on one
3685        // is ignored, and a limit order with its limit, which it rests at once released.
3686        let order = Order {
3687            key: request.key,
3688            side,
3689            price: limit,
3690            quantity: request.state.quantity,
3691            kind: request.state.kind,
3692            time_in_force: request.state.time_in_force,
3693            state: open,
3694        };
3695
3696        // Nothing traded, so the shortfall it was lent is all held against the order, and the
3697        // interest that loan was charged waits with it for a position to belong to.
3698        let reservation = Reservation {
3699            amount: settlement.requirement(),
3700            asset: settlement.asset,
3701            borrowed: committed.shortfall_borrowed(),
3702            excess_borrowed: committed.excess_borrowed,
3703            effect,
3704            unreported_interest: interest.map(|flow| vec![*flow]).unwrap_or_default(),
3705        };
3706        if let Err(AlreadyResting(refused)) = self.account.hold(order.clone(), Some(reservation)) {
3707            unreachable!(
3708                "SimulatedVenue held {} over a resting order: open_order_inner checks first",
3709                refused.order.key.cid
3710            );
3711        }
3712
3713        (
3714            order.map_state(OrderState::active),
3715            Some(OpenOrderNotifications::Rested {
3716                balance: Snapshot(committed.paid),
3717            }),
3718        )
3719    }
3720
3721    /// Refuses an order that would open or increase a CFD short which the configured
3722    /// [`ShortabilityProvider`] does not allow; see the type's
3723    /// `# Locates and the short-sale restriction`.
3724    ///
3725    /// The short an order opens is measured as if every resting sell on the instrument filled
3726    /// first, so that a short this venue has already accepted on the book counts against what is
3727    /// left to lend, and an order whose short part is nothing — a buy, or a sell that only reduces
3728    /// a long — passes untouched. `market` is the snapshot the order will be priced from, so
3729    /// whether it is marketable is judged against the book it would fill on.
3730    fn check_shortability(
3731        &self,
3732        request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3733        terms: &InstrumentTerms,
3734        market: Option<&MarketSnapshot>,
3735    ) -> Result<(), UnindexedApiError> {
3736        let Some(provider) = &self.shortability else {
3737            return Ok(());
3738        };
3739        if !terms.cash_settled || request.state.side != Side::Sell {
3740            return Ok(());
3741        }
3742
3743        let instrument = &request.key.instrument;
3744        let resting_sells: Decimal = self
3745            .account
3746            .orders_open()
3747            .filter(|order| order.key.instrument == *instrument && order.side == Side::Sell)
3748            .map(|order| order.state.quantity_remaining(order.quantity))
3749            .sum();
3750        // The position left if every resting sell fills, before and after this order does too.
3751        let committed = self.net_position(terms, instrument).quantity - resting_sells;
3752        let short_before = (-committed).max(Decimal::ZERO);
3753        let short_after = (request.state.quantity - committed).max(Decimal::ZERO);
3754        if short_after <= short_before {
3755            return Ok(());
3756        }
3757
3758        let now = self.time_exchange();
3759        let exchange = request.key.exchange;
3760        let shortability = provider.shortability(instrument, now);
3761
3762        if let Some(delisting_at) = shortability.delisting_at
3763            && delisting_at <= now
3764        {
3765            return Err(ApiError::BorrowRejected(BorrowReject::new(
3766                BorrowRejectReason::NotShortable,
3767                None,
3768                format!(
3769                    "{instrument} cannot be sold short on {exchange} at {now}: it is delisted at \
3770                     {delisting_at}"
3771                ),
3772            )));
3773        }
3774        if shortability.shortable == Some(false) {
3775            return Err(ApiError::BorrowRejected(BorrowReject::new(
3776                BorrowRejectReason::NotShortable,
3777                None,
3778                format!("{instrument} cannot be sold short on {exchange} at {now}"),
3779            )));
3780        }
3781
3782        if let Some(available) = shortability.available
3783            && short_after > available
3784        {
3785            let message = format!(
3786                "selling {} {instrument} on {exchange} would take its short, resting sells \
3787                 included, to {short_after}, past the {available} there is to borrow at {now}",
3788                request.state.quantity
3789            );
3790            let left = (available - short_before).max(Decimal::ZERO);
3791            return Err(ApiError::BorrowRejected(
3792                BorrowReject::new(BorrowRejectReason::InventoryUnavailable, None, message)
3793                    .with_available(left),
3794            ));
3795        }
3796
3797        // A limit is judged against what a sell trades against, the bid, else the last price, as
3798        // `crosses` judges it. Every other kind this venue accepts takes whatever the book offers,
3799        // at the price the fill model gives it. When there is nothing to judge against, an order
3800        // that would take from the book is refused as unpriceable instead, which names the real
3801        // cause.
3802        let marketable = || {
3803            market.is_some_and(|market| match request.state.kind {
3804                OrderKind::Limit => request
3805                    .state
3806                    .price
3807                    .is_some_and(|limit| crosses(Side::Sell, limit, market)),
3808                _ => self
3809                    .fill_model
3810                    .fill_price(&FillContext::new(
3811                        Side::Sell,
3812                        request.state.quantity,
3813                        market,
3814                    ))
3815                    .is_some(),
3816            })
3817        };
3818        if provider.short_sale_restricted(instrument, now) && marketable() {
3819            return Err(ApiError::BorrowRejected(BorrowReject::new(
3820                BorrowRejectReason::ShortSaleRestricted,
3821                None,
3822                format!(
3823                    "{instrument} is under a short-sale restriction on {exchange} at {now}: a \
3824                     short sale must be priced above the best bid"
3825                ),
3826            )));
3827        }
3828
3829        Ok(())
3830    }
3831
3832    /// Retires an order that asked to work only on arrival and could not, having traded nothing.
3833    ///
3834    /// Reached by a non-marketable [`TimeInForce::ImmediateOrCancel`] or
3835    /// [`TimeInForce::FillOrKill`], and by a `post_only` order that would have taken liquidity.
3836    /// Nothing was reserved and nothing traded, so this owes no balance restatement -- only the
3837    /// terminal response.
3838    fn cancel_on_arrival(
3839        &mut self,
3840        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3841        now: DateTime<Utc>,
3842    ) -> (
3843        Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
3844        Option<OpenOrderNotifications>,
3845    ) {
3846        let cancelled = Cancelled {
3847            id: self.order_id_sequence_fetch_add(),
3848            time_exchange: now,
3849            filled_quantity: Some(Decimal::ZERO),
3850        };
3851
3852        self.account.ack_cancelled(Order {
3853            key: request.key.clone(),
3854            side: request.state.side,
3855            price: request.state.price,
3856            quantity: request.state.quantity,
3857            kind: request.state.kind,
3858            time_in_force: request.state.time_in_force,
3859            state: cancelled.clone(),
3860        });
3861
3862        (
3863            Order {
3864                key: request.key,
3865                side: request.state.side,
3866                price: request.state.price,
3867                quantity: request.state.quantity,
3868                kind: request.state.kind,
3869                time_in_force: request.state.time_in_force,
3870                state: OrderState::inactive(cancelled),
3871            },
3872            None,
3873        )
3874    }
3875
3876    /// Retires an order whose [`TimeInForce::GoodTillDate`] deadline had already passed when it
3877    /// arrived.
3878    ///
3879    /// Such an order is never rested, and never traded -- not even when the book would have filled
3880    /// it, since an order past its deadline has stopped working and whether it *could* have traded
3881    /// is not a question this venue asks. With no independent clock, deadlines are swept only when
3882    /// a driver advances it or feeds it a market, so an order accepted past its own deadline would
3883    /// work until something else happened to touch the venue -- and if nothing did, for the rest of
3884    /// the run. Retiring it here makes the deadline mean what it says.
3885    ///
3886    /// Reported as expired rather than rejected because nothing was wrong with the request: its
3887    /// price, size and funding were never examined, and calling it a rejection would tell a
3888    /// consumer's statistics the venue refused an order it merely found too late.
3889    fn expire_on_arrival(
3890        &mut self,
3891        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3892        now: DateTime<Utc>,
3893    ) -> (
3894        Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
3895        Option<OpenOrderNotifications>,
3896    ) {
3897        let expired = Expired {
3898            id: self.order_id_sequence_fetch_add(),
3899            time_exchange: now,
3900            filled_quantity: Some(Decimal::ZERO),
3901        };
3902
3903        self.account.ack_expired(Order {
3904            key: request.key.clone(),
3905            side: request.state.side,
3906            price: request.state.price,
3907            quantity: request.state.quantity,
3908            kind: request.state.kind,
3909            time_in_force: request.state.time_in_force,
3910            state: expired.clone(),
3911        });
3912
3913        (
3914            Order {
3915                key: request.key,
3916                side: request.state.side,
3917                price: request.state.price,
3918                quantity: request.state.quantity,
3919                kind: request.state.kind,
3920                time_in_force: request.state.time_in_force,
3921                state: OrderState::expired(expired),
3922            },
3923            None,
3924        )
3925    }
3926
3927    /// Trades an order against the book the moment it arrives, for as much of it as the book has.
3928    ///
3929    /// # What the book has is what it fills
3930    /// The fill is capped at the size on offer to this side
3931    /// ([`VenueInstrumentMarket::depth`]), and that size is drawn down by what this fill takes, so
3932    /// a second order arriving before the next [`apply_market`](Self::apply_market) sees only what
3933    /// the first left. A feed supplying no size caps nothing — see [`MarketDepth`].
3934    ///
3935    /// `remainder` decides what becomes of the quantity the book could not fill, and it is the
3936    /// order's own time in force that chose it — see [`Remainder`] and [`Disposition`].
3937    ///
3938    /// # An order is funded whole or refused whole
3939    /// An order that fills in part and rests the rest owes the ledger two things at one instant: a
3940    /// settlement for what traded and a hold against what did not. They are asked for as one
3941    /// requirement ([`AccountState::commit`]), so an account that cannot cover both has the order
3942    /// refused with nothing moved — rather than filled for the part it could afford, which would
3943    /// be this venue choosing a disposition the sender did not ask for.
3944    ///
3945    /// That makes a capped order *dearer* than the same order filling outright, because the
3946    /// remainder is held at the order's own limit while the fill struck a better price. An order
3947    /// can therefore be refused for a size the book was willing to sell it.
3948    ///
3949    /// [`VenueInstrumentMarket::depth`]: VenueInstrumentMarket::depth
3950    /// [`AccountState::commit`]: crate::exchange::mock::account::AccountState::commit
3951    fn fill_on_arrival(
3952        &mut self,
3953        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
3954        market: Option<MarketSnapshot>,
3955        terms: &InstrumentTerms,
3956        remainder: Remainder,
3957    ) -> (
3958        Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
3959        Option<OpenOrderNotifications>,
3960    ) {
3961        let market_supplied = market.is_some();
3962        let market = market.unwrap_or_default();
3963
3964        // Only a limit order's price is a limit. Any other kind's is ignored, as the live clients
3965        // ignore a price sent with a market order: it neither prices the fill nor bounds it.
3966        let limit = match request.state.kind {
3967            OrderKind::Limit => request.state.price,
3968            _ => None,
3969        };
3970
3971        // The order's own price is deliberately NOT handed to the model. A limit constrains the
3972        // result, not the pricing: a model reads the book and returns where a taker prints, and
3973        // this venue is what bounds that by the order's terms. See `FillModel`'s contract.
3974        let maybe_reference = self
3975            .fill_model
3976            .fill_price(&FillContext::new(
3977                request.state.side,
3978                request.state.quantity,
3979                &market,
3980            ))
3981            // For a market order this is `.or(None)`, whatever price it carries. For a marketable
3982            // limit it is the last resort that makes the rejection below unreachable: an order
3983            // whose limit the market crossed necessarily has one.
3984            .or(limit);
3985
3986        // No price anywhere. Ordinary user data -- a cold start, a thin instrument, a subscription
3987        // that was never made, or a caller that supplied no snapshot at all -- not a violated
3988        // internal invariant, so it is rejected rather than panicked on.
3989        //
3990        // A panic here is also strictly less informative. The async driver runs as a spawned task:
3991        // killing it drops the request receiver, every later order comes back as
3992        // `ConnectivityError::ExchangeOffline`, and the run completes over the whole dataset
3993        // having filled nothing, reporting only a `JoinError` at teardown.
3994        //
3995        // The two causes get different messages because they have different fixes: an absent
3996        // snapshot is a wiring problem in the caller, an empty one is a timing problem in the data.
3997        let Some(reference) = maybe_reference else {
3998            let cause = if market_supplied {
3999                "no market price available yet"
4000            } else {
4001                "the request carried no market snapshot (RequestOpen::market was None)"
4002            };
4003            let reason = format!(
4004                "cannot price {} for {}: {cause} and OrderKind::{:?} carries no limit price",
4005                request.key.instrument, request.key.exchange, request.state.kind
4006            );
4007            return (
4008                build_open_order_err_response(
4009                    request,
4010                    UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
4011                ),
4012                None,
4013            );
4014        };
4015
4016        let time_exchange = self.time_exchange();
4017        let side = request.state.side;
4018
4019        // What the book has to give this order, which is all of it wherever no size is known.
4020        let filled_quantity = match self.depth_available(&request.key.instrument, side) {
4021            Some(available) => request.state.quantity.min(available),
4022            None => request.state.quantity,
4023        };
4024        let unfilled = request.state.quantity - filled_quantity;
4025
4026        // Nothing trades: either the book has no size left to give, or the order would rather
4027        // trade nothing than trade in part. `FillOrKill` is the second case and the only one --
4028        // this is where it stops coinciding with `ImmediateOrCancel`, which takes what it can get.
4029        // The whole order then meets the fate its remainder would have.
4030        if filled_quantity <= Decimal::ZERO || (unfilled > Decimal::ZERO && remainder.is_kill()) {
4031            return match remainder {
4032                // Nothing has traded, so the whole quantity is what rests.
4033                Remainder::Rest { limit } => self.rest_order(request, limit, terms, Decimal::ZERO),
4034                Remainder::Cancel | Remainder::Kill => {
4035                    self.cancel_on_arrival(request, time_exchange)
4036                }
4037            };
4038        }
4039
4040        // Slipped only now that the order is known to trade, so the stream advances exactly once
4041        // per fill, and an order that is cancelled or rests whole takes no draw. One the ledger
4042        // refuses below hands its draw back, which needs the stream as it was.
4043        let slippage_before = self.slippage.clone();
4044        let fill_price = self.slippage.apply(side, reference);
4045
4046        // The clamp, and the only place a limit price bounds a fill. A taker crossing the spread
4047        // can be filled better than its limit but never worse, so a buy never prints above it and
4048        // a sell never below. Applied here rather than inside the model because it is a property of
4049        // the order's terms, not of how the market is read, and after the slippage so that no
4050        // rate, however large, fills an order past its limit.
4051        let fill_price = match limit {
4052            Some(limit) => clamp_to_limit(side, fill_price, limit),
4053            None => fill_price,
4054        };
4055
4056        // Marketable on arrival, so this fill takes liquidity and is charged the taker rate.
4057        let position = self.net_position(terms, &request.key.instrument);
4058        let fill = self.fill_settlement(
4059            terms,
4060            position,
4061            side,
4062            filled_quantity,
4063            fill_price,
4064            Liquidity::Taker,
4065        );
4066
4067        // What holding the remainder will cost, priced exactly as the fill that settles it will be
4068        // -- the unfilled quantity, at the order's own limit, as the maker, against the position
4069        // this fill leaves. Computed before anything moves, so both legs are asked of the ledger as
4070        // one requirement.
4071        let held = match remainder {
4072            Remainder::Rest { limit } if unfilled > Decimal::ZERO => Some(self.settlement(
4073                terms,
4074                position.after(side, filled_quantity, fill_price),
4075                side,
4076                unfilled,
4077                limit,
4078                Liquidity::Maker,
4079            )),
4080            _ => None,
4081        };
4082
4083        // One commitment for the whole arrival. A configured `total != free` (an `initial_state`
4084        // copied from a live account with margin reserved) is carried through untouched rather
4085        // than rejected: the ledger represents a held amount, so there is nothing left to refuse.
4086        let debit = match &held {
4087            Some(held) => fill.settled_holding(held),
4088            None => fill.settled(),
4089        };
4090
4091        let effect = self.margin_effect(request.state.margin_effect);
4092        let mut commitment = match self.commit(
4093            &debit,
4094            Some(&fill.credit),
4095            effect,
4096            terms,
4097            Some(&market),
4098            time_exchange,
4099        ) {
4100            Ok(commitment) => commitment,
4101            Err(insufficient) => {
4102                // Refused, so it never traded: the next fill draws the rate it would have drawn had
4103                // this order never been sent.
4104                self.slippage = slippage_before;
4105                return (
4106                    build_open_order_err_response(
4107                        request,
4108                        self.insufficient_error(insufficient, side),
4109                    ),
4110                    None,
4111                );
4112            }
4113        };
4114
4115        self.take_fee_apart(&fill, &mut commitment, time_exchange);
4116
4117        // Taken off the book only now that it is paid for, so a refused order consumes nothing.
4118        self.consume_depth(&request.key.instrument, side, filled_quantity);
4119        self.record_fill(
4120            terms,
4121            &request.key.instrument,
4122            side,
4123            Fill {
4124                quantity: filled_quantity,
4125                price: fill_price,
4126            },
4127        );
4128
4129        let order_id = self.order_id_sequence_fetch_add();
4130        let trade = Trade {
4131            id: self.trade_id_sequence_fetch_add(),
4132            order_id: order_id.clone(),
4133            instrument: request.key.instrument.clone(),
4134            strategy: request.key.strategy.clone(),
4135            time_exchange,
4136            side,
4137            price: fill_price,
4138            quantity: filled_quantity,
4139            // An arriving order that aggresses has no prior executions, so this fill is its
4140            // whole cumulative.
4141            order_filled_quantity: Some(filled_quantity),
4142            fees: fill.fees,
4143            origin: TradeOrigin::Order,
4144            position_side: None,
4145            client_order_id: Some(request.key.cid.clone()),
4146            time_source: TimeSource::Venue,
4147        };
4148
4149        // Booked here rather than by the caller, so that whatever this order became is decided in
4150        // one place: every other arrival already acknowledges its own outcome.
4151        self.account.ack_trade(trade.clone());
4152
4153        let state = self.retire_filled(
4154            &request,
4155            order_id,
4156            Fill {
4157                quantity: filled_quantity,
4158                price: fill_price,
4159            },
4160            held,
4161            &commitment.committed,
4162            time_exchange,
4163        );
4164
4165        (
4166            Order {
4167                key: request.key,
4168                side,
4169                price: request.state.price,
4170                quantity: request.state.quantity,
4171                kind: request.state.kind,
4172                time_in_force: request.state.time_in_force,
4173                state,
4174            },
4175            Some(OpenOrderNotifications::Filled { commitment, trade }),
4176        )
4177    }
4178
4179    /// Records what an order that traded on arrival became, and reports the state it answers with.
4180    ///
4181    /// The three outcomes are the three things a taker fill can leave behind: nothing, a remainder
4182    /// with nowhere to wait, or a remainder on the book. `committed` says what a margin account
4183    /// lent the arrival, part of which a remainder on the book holds.
4184    fn retire_filled(
4185        &mut self,
4186        request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
4187        order_id: OrderId,
4188        fill: Fill,
4189        held: Option<Settlement>,
4190        committed: &Committed,
4191        time_exchange: DateTime<Utc>,
4192    ) -> UnindexedOrderState {
4193        // Spelled out per arm rather than shared: a closure would be monomorphic in the state
4194        // type, and the two arms that record an order record two different ones.
4195        match held {
4196            // The book could not fill all of it and the remainder waits at the order's own limit,
4197            // carrying what has already traded so `Open::quantity_remaining` stays true for the
4198            // rest of its life -- which is what `match_resting` settles and prints.
4199            Some(held) => {
4200                let open = Open::new(
4201                    VenueOrderId::Assigned(order_id),
4202                    time_exchange,
4203                    fill.quantity,
4204                );
4205                self.book_rested(
4206                    Order {
4207                        key: request.key.clone(),
4208                        side: request.state.side,
4209                        price: request.state.price,
4210                        quantity: request.state.quantity,
4211                        kind: request.state.kind,
4212                        time_in_force: request.state.time_in_force,
4213                        state: open.clone(),
4214                    },
4215                    Reservation {
4216                        // The fill spent the account's own balance first, so whatever was lent of
4217                        // the shortfall is held against the remainder.
4218                        borrowed: committed.shortfall_borrowed().min(held.requirement()),
4219                        excess_borrowed: committed.excess_borrowed,
4220                        amount: held.requirement(),
4221                        asset: held.asset,
4222                        effect: self.margin_effect(request.state.margin_effect),
4223                        // The arrival traded, so the interest its loan was charged is reported
4224                        // after that trade, the remainder's share with it.
4225                        unreported_interest: Vec::new(),
4226                    },
4227                );
4228
4229                OrderState::active(open)
4230            }
4231
4232            // Either it filled outright, or what is left has nowhere to wait.
4233            None if fill.quantity == request.state.quantity => {
4234                // One fill is the whole of this order, so its price is also the mean of every
4235                // fill behind the total -- which is exactly when `avg_price` may be reported.
4236                let filled = Filled::new(
4237                    order_id,
4238                    time_exchange,
4239                    request.state.quantity,
4240                    Some(fill.price),
4241                );
4242                self.account.ack_filled(Order {
4243                    key: request.key.clone(),
4244                    side: request.state.side,
4245                    price: request.state.price,
4246                    quantity: request.state.quantity,
4247                    kind: request.state.kind,
4248                    time_in_force: request.state.time_in_force,
4249                    state: filled.clone(),
4250                });
4251
4252                OrderState::fully_filled(filled)
4253            }
4254
4255            // A market order has no limit price, so its remainder has nothing to rest at and
4256            // retires carrying what did trade -- which is what `Cancelled::filled_quantity` is for,
4257            // and the first time this venue makes it non-zero.
4258            None => {
4259                let cancelled = Cancelled {
4260                    id: order_id,
4261                    time_exchange,
4262                    filled_quantity: Some(fill.quantity),
4263                };
4264                self.account.ack_cancelled(Order {
4265                    key: request.key.clone(),
4266                    side: request.state.side,
4267                    price: request.state.price,
4268                    quantity: request.state.quantity,
4269                    kind: request.state.kind,
4270                    time_in_force: request.state.time_in_force,
4271                    state: cancelled.clone(),
4272                });
4273
4274                OrderState::inactive(cancelled)
4275            }
4276        }
4277    }
4278
4279    /// What is left to take at the best price on the far side of `side`, where a feed says.
4280    fn depth_available(&self, instrument: &InstrumentNameExchange, side: Side) -> Option<Decimal> {
4281        self.market
4282            .get(instrument)
4283            .and_then(|market| market.depth.available_to(side))
4284    }
4285
4286    /// Draws down what an aggressor on `side` can still take, by what this one just took.
4287    fn consume_depth(
4288        &mut self,
4289        instrument: &InstrumentNameExchange,
4290        side: Side,
4291        quantity: Decimal,
4292    ) {
4293        if let Some(market) = self.market.get_mut(instrument) {
4294            market.depth.consume(side, quantity);
4295        }
4296    }
4297
4298    /// Puts `order` on the book holding `reservation`.
4299    ///
4300    /// # Panics
4301    /// If an order is already resting under its id. `open_order_inner` refuses such an open before
4302    /// anything else, under the same `&mut self`, so the book is vacant under this id by
4303    /// construction, and a refusal here would be a bug in this venue with no correct reply left
4304    /// to give.
4305    fn book_rested(&mut self, order: OpenOrder, reservation: Reservation) {
4306        if let Err(AlreadyResting(refused)) = self.account.book(order, Some(reservation)) {
4307            unreachable!(
4308                "SimulatedVenue booked {} over a resting order: open_order_inner checks first",
4309                refused.order.key.cid
4310            );
4311        }
4312    }
4313
4314    /// Puts what is left of an order onto the book, holding what that remainder's fill will cost.
4315    ///
4316    /// `filled_quantity` is how much of `request` has already traded, and everything here is
4317    /// computed from what is **left** of it: the reservation covers the unfilled remainder alone,
4318    /// and the [`Open`] state carries the filled part so that [`Open::quantity_remaining`] stays
4319    /// true for the rest of the order's life -- which is what
4320    /// [`match_resting`](Self::match_resting) settles and prints when the book reaches it.
4321    /// Reserving for the whole quantity would hold balance against a portion that has already
4322    /// settled, and the fill that eventually released it would settle less than was held.
4323    ///
4324    /// The reservation is what the fill will settle, computed once here and settled unchanged --
4325    /// never a conservative over-estimate. A conservative reservation would have to be released and
4326    /// re-debited on the fill, and [`AccountState::commit`]'s contract is that an arrival restates
4327    /// each asset it moves exactly **once**; the extra restatements would report balances the
4328    /// account never held, and [`Balance::used`](crate::balance::Balance::used) would misreport
4329    /// for the order's whole life.
4330    ///
4331    /// [`AccountState::commit`]: crate::exchange::mock::account::AccountState::commit
4332    fn rest_order(
4333        &mut self,
4334        request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
4335        limit: Decimal,
4336        terms: &InstrumentTerms,
4337        filled_quantity: Decimal,
4338    ) -> (
4339        Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
4340        Option<OpenOrderNotifications>,
4341    ) {
4342        let time_exchange = self.time_exchange();
4343
4344        // Priced and charged exactly as the fill will be: the unfilled remainder, at the order's
4345        // own limit, as the maker, against the position as it stands now.
4346        let settlement = self.settlement(
4347            terms,
4348            self.net_position(terms, &request.key.instrument),
4349            request.state.side,
4350            request.state.quantity - filled_quantity,
4351            limit,
4352            Liquidity::Maker,
4353        );
4354
4355        let debit = settlement.reserved();
4356        let market = self
4357            .market(&request.key.instrument)
4358            .map(|market| market.snapshot);
4359        let effect = self.margin_effect(request.state.margin_effect);
4360        let (committed, interest) =
4361            match self.commit(&debit, None, effect, terms, market.as_ref(), time_exchange) {
4362                // Nothing traded, so nothing is delivered: the one balance is the hold. A fresh
4363                // commitment holds no earlier order's interest, so `held_interest` is empty.
4364                Ok(Commitment {
4365                    committed,
4366                    interest,
4367                    ..
4368                }) => (committed, interest),
4369                Err(insufficient) => {
4370                    let error = self.insufficient_error(insufficient, request.state.side);
4371                    return (build_open_order_err_response(request, error), None);
4372                }
4373            };
4374
4375        // The order rests carrying whatever it has already done, so the book, the client and this
4376        // venue's own later arithmetic all read the same remainder off it.
4377        let open = Open::new(
4378            VenueOrderId::Assigned(self.order_id_sequence_fetch_add()),
4379            time_exchange,
4380            filled_quantity,
4381        );
4382
4383        self.book_rested(
4384            Order {
4385                key: request.key.clone(),
4386                side: request.state.side,
4387                price: request.state.price,
4388                quantity: request.state.quantity,
4389                kind: request.state.kind,
4390                time_in_force: request.state.time_in_force,
4391                state: open.clone(),
4392            },
4393            Reservation {
4394                amount: settlement.requirement(),
4395                asset: settlement.asset,
4396                borrowed: committed.shortfall_borrowed(),
4397                excess_borrowed: committed.excess_borrowed,
4398                effect,
4399                // Nothing traded: the interest waits for a position to belong to.
4400                unreported_interest: interest.map(|flow| vec![*flow]).unwrap_or_default(),
4401            },
4402        );
4403
4404        let order_response = Order {
4405            key: request.key,
4406            side: request.state.side,
4407            price: request.state.price,
4408            quantity: request.state.quantity,
4409            kind: request.state.kind,
4410            time_in_force: request.state.time_in_force,
4411            state: OrderState::active(open),
4412        };
4413
4414        (
4415            order_response,
4416            Some(OpenOrderNotifications::Rested {
4417                balance: Snapshot(committed.paid),
4418            }),
4419        )
4420    }
4421
4422    /// The instrument facts one order's settlement is computed from, read out once.
4423    fn instrument_terms(
4424        &self,
4425        instrument: &InstrumentNameExchange,
4426    ) -> Result<InstrumentTerms, UnindexedApiError> {
4427        let instrument = self.find_instrument_data(instrument)?;
4428        let cash_settled = Self::settlement_of_supported_kind(instrument)?;
4429
4430        Ok(InstrumentTerms {
4431            underlying: instrument.underlying.clone(),
4432            contract_size: instrument.kind.contract_size(),
4433            cash_settled,
4434        })
4435    }
4436
4437    /// The net position an order on `instrument` trades against. Flat for a cash-settled
4438    /// instrument this account holds no position in, and for every spot instrument, which skips
4439    /// the lookup: a spot settlement never reads it.
4440    fn net_position(
4441        &self,
4442        terms: &InstrumentTerms,
4443        instrument: &InstrumentNameExchange,
4444    ) -> NetPosition {
4445        if !terms.cash_settled {
4446            return NetPosition::default();
4447        }
4448        self.positions.get(instrument).copied().unwrap_or_default()
4449    }
4450
4451    /// Moves `instrument`'s net position by a fill, if it is cash-settled. A spot fill moves the
4452    /// ledger's balances and nothing here.
4453    fn record_fill(
4454        &mut self,
4455        terms: &InstrumentTerms,
4456        instrument: &InstrumentNameExchange,
4457        side: Side,
4458        fill: Fill,
4459    ) {
4460        if !terms.cash_settled {
4461            return;
4462        }
4463
4464        // One lookup for a position already held, and the name cloned only to open one.
4465        let opened_or_closed = match self.positions.get_mut(instrument) {
4466            Some(position) => {
4467                *position = position.after(side, fill.quantity, fill.price);
4468                let closed = position.quantity.is_zero();
4469                if closed {
4470                    self.positions.remove(instrument);
4471                }
4472                closed
4473            }
4474            None => {
4475                let position = NetPosition::default().after(side, fill.quantity, fill.price);
4476                let opened = !position.quantity.is_zero();
4477                if opened {
4478                    self.positions.insert(instrument.clone(), position);
4479                }
4480                opened
4481            }
4482        };
4483
4484        // A position opened or closed has boundaries of its own, or no longer has any. One that
4485        // only changed size keeps its boundaries, which depend on the instrument alone.
4486        if opened_or_closed && !self.holding_costs.is_empty() {
4487            self.refresh_next_charge();
4488        }
4489    }
4490
4491    /// What one order pays with at `price`, how much of it including the fee, and what it pays
4492    /// back.
4493    ///
4494    /// The single place this venue decides an order's cost. A reservation and the fill that settles
4495    /// it both come from here, with the same `price` and `liquidity`, which is what makes "reserve
4496    /// exactly what will be settled" true by construction rather than by two formulas agreeing.
4497    /// For a cash-settled instrument the cost also depends on `position`, the net position the
4498    /// order trades against, which can change between an order resting and filling; see
4499    /// [`settle_resting`](Self::settle_resting).
4500    ///
4501    /// Both the notional and the fee carry the instrument's `contract_size` multiplier, which is
4502    /// `Decimal::ONE` for `Spot` and a real per-point multiplier for a `Cfd`. Dropping it here while
4503    /// the engine applies it to PnL and fees (`InstrumentState::update_from_trade`) would make this
4504    /// ledger and the engine's position accounting disagree by exactly that factor -- balances
4505    /// moving 1x while PnL moves 25x, silently, with every balance-derived return, drawdown and
4506    /// Sharpe wrong by the same factor. Which models actually consult the multiplier is theirs to
4507    /// decide -- `PercentageFeeModel` scales by it, `PerContractFeeModel` deliberately does not --
4508    /// and passing it is what keeps that decision in the models rather than making this a second
4509    /// place the multiplier can be lost.
4510    fn settlement(
4511        &self,
4512        terms: &InstrumentTerms,
4513        position: NetPosition,
4514        side: Side,
4515        quantity: Decimal,
4516        price: Decimal,
4517        liquidity: Liquidity,
4518    ) -> Settlement {
4519        let fees_quote =
4520            self.fee_model
4521                .compute_fee(price, quantity.abs(), terms.contract_size, liquidity);
4522        self.settlement_with_fee(terms, position, side, quantity, price, fees_quote)
4523    }
4524
4525    /// The settlement of a fill, as [`settlement`](Self::settlement) gives it, unless its fee is
4526    /// charged in the [`FeeAssetConfig`] asset: then the fill exchanges its two assets free of fee,
4527    /// and its fee, in that asset, is [taken apart](Settlement::fee_apart) when it settles.
4528    ///
4529    /// Only a fill uses this. What an order holds while it works is its
4530    /// [`settlement`](Self::settlement), which is what its fill costs when the fee falls back, and
4531    /// never less than when it does not.
4532    fn fill_settlement(
4533        &self,
4534        terms: &InstrumentTerms,
4535        position: NetPosition,
4536        side: Side,
4537        quantity: Decimal,
4538        price: Decimal,
4539        liquidity: Liquidity,
4540    ) -> Settlement {
4541        let settlement = self.settlement(terms, position, side, quantity, price, liquidity);
4542        match self.fee_in_fee_asset(terms, &settlement.fees) {
4543            Some(fees) => Settlement {
4544                fees,
4545                fee_apart: true,
4546                ..self.settlement_with_fee(terms, position, side, quantity, price, Decimal::ZERO)
4547            },
4548            None => settlement,
4549        }
4550    }
4551
4552    /// `fees`, a fill's fee in quote, as the [`FeeAssetConfig`] asset charges it: discounted, and
4553    /// converted at this venue's price for that asset in the fill's quote. `None` when the fill
4554    /// falls back to the [`SpotFeeAsset`] rule, as `FeeAssetConfig`'s `# When it falls back`
4555    /// lists, including when the asset's free balance is short of the fee.
4556    fn fee_in_fee_asset(
4557        &self,
4558        terms: &InstrumentTerms,
4559        fees: &AssetFees<AssetNameExchange>,
4560    ) -> Option<AssetFees<AssetNameExchange>> {
4561        let config = self.fee_asset.as_ref()?;
4562        let asset = config.asset();
4563        let underlying = &terms.underlying;
4564        let fees_quote = fees.fees_quote?;
4565        if terms.cash_settled
4566            || *asset == underlying.base
4567            || *asset == underlying.quote
4568            || fees_quote <= Decimal::ZERO
4569        {
4570            return None;
4571        }
4572
4573        let price = self.price_in(asset, &underlying.quote, None)?;
4574        let fees_quote = fees_quote * (Decimal::ONE - config.discount());
4575        let fee = fees_quote.checked_div(price)?;
4576        (self.account.free(asset) >= fee)
4577            .then(|| AssetFees::new(asset.clone(), fee, Some(fees_quote)))
4578    }
4579
4580    /// [`settlement`](Self::settlement), with its fee already computed: `fees_quote`, in quote.
4581    fn settlement_with_fee(
4582        &self,
4583        terms: &InstrumentTerms,
4584        position: NetPosition,
4585        side: Side,
4586        quantity: Decimal,
4587        price: Decimal,
4588        fees_quote: Decimal,
4589    ) -> Settlement {
4590        let quantity = quantity.abs();
4591        let fees = AssetFees::new(terms.underlying.quote.clone(), fees_quote, Some(fees_quote));
4592
4593        if terms.cash_settled {
4594            // A CFD is a cash-settled position on a price, not an exchange of the two underlying
4595            // assets: there is nothing to deliver in either direction, so both directions post
4596            // and receive quote. A CFD short is a margin position rather than a stock loan, so --
4597            // unlike a spot sell -- it requires no base inventory, which would otherwise force the
4598            // caller to fund a phantom balance in an index or a commodity to open one.
4599            //
4600            // The fill first closes whatever of `position` it opposes, and opens a position with
4601            // the rest. This venue models no leverage, so opening posts the full notional as
4602            // margin -- the conservative reading, and the requirement a spot buy already carries.
4603            // Closing pays that margin back together with the realised PnL, which for a closed
4604            // quantity `c` against an average entry `e` is `c * cs * (price - e)` for a long and
4605            // `c * cs * (e - price)` for a short. The fee is charged on the whole quantity.
4606            let closing = position.closing(side, quantity);
4607            let opening = quantity - closing;
4608            let open_margin = price * opening * terms.contract_size;
4609            let close_proceeds = closing
4610                * terms.contract_size
4611                * match position.is_long() {
4612                    true => price,
4613                    false => position.entry + position.entry - price,
4614                };
4615
4616            // A short closed at more than twice its entry has lost more than its margin. There is
4617            // no liquidation here to have stopped it, so the shortfall is paid like any other
4618            // debit rather than credited as a negative amount.
4619            let (credit, shortfall) = match close_proceeds.is_sign_negative() {
4620                true => (Decimal::ZERO, -close_proceeds),
4621                false => (close_proceeds, Decimal::ZERO),
4622            };
4623
4624            return Settlement {
4625                asset: terms.underlying.quote.clone(),
4626                amount: open_margin + fees_quote + shortfall,
4627                credit: Credit::PaidBack(credit),
4628                fees,
4629                fee_apart: false,
4630            };
4631        }
4632
4633        // A spot fill exchanges the two assets. Its fee comes out of what a sell receives, in quote,
4634        // and under `SpotFeeAsset::Quote` on top of what a buy pays, so the base it moves is exactly
4635        // its quantity. Under `Received` a buy's fee comes out of the base it receives instead,
4636        // converted at the fill price. That base fee is what the engine nets from its position, so
4637        // the two still agree. The fee each fill reports is the one charged, in the asset charged.
4638        let notional = price * quantity * terms.contract_size;
4639        match side {
4640            Side::Buy
4641                if self.spot_fee_asset == SpotFeeAsset::Received
4642                    && price > Decimal::ZERO
4643                    && !fees_quote.is_zero() =>
4644            {
4645                let fee_base = fees_quote / price;
4646                Settlement {
4647                    asset: terms.underlying.quote.clone(),
4648                    amount: notional,
4649                    credit: Credit::Delivery(Delivery {
4650                        asset: terms.underlying.base.clone(),
4651                        amount: quantity - fee_base,
4652                    }),
4653                    fees: AssetFees::new(terms.underlying.base.clone(), fee_base, Some(fees_quote)),
4654                    fee_apart: false,
4655                }
4656            }
4657            Side::Buy => Settlement {
4658                asset: terms.underlying.quote.clone(),
4659                amount: notional + fees_quote,
4660                credit: Credit::Delivery(Delivery {
4661                    asset: terms.underlying.base.clone(),
4662                    amount: quantity,
4663                }),
4664                fees,
4665                fee_apart: false,
4666            },
4667            Side::Sell => Settlement {
4668                asset: terms.underlying.base.clone(),
4669                amount: quantity,
4670                credit: Credit::Delivery(Delivery {
4671                    asset: terms.underlying.quote.clone(),
4672                    amount: notional - fees_quote,
4673                }),
4674                fees,
4675                fee_apart: false,
4676            },
4677        }
4678    }
4679
4680    /// Whether this venue accepts `order_kind`, which depends on its [`VenueRegime`].
4681    ///
4682    /// [`OrderKind::Market`] is always accepted. [`OrderKind::Limit`] needs somewhere to rest and
4683    /// something to be matched against, so it is accepted only by a
4684    /// [`MarketDriven`](VenueRegime::MarketDriven) venue. Every other kind is rejected outright.
4685    ///
4686    /// # Errors
4687    /// Returns [`ApiError::OrderRejected`] naming the kind, and -- for a limit order refused by the
4688    /// regime -- why the venue cannot hold one.
4689    pub fn validate_order_kind_supported(
4690        &self,
4691        order_kind: OrderKind,
4692    ) -> Result<(), UnindexedOrderError> {
4693        let reason = match (order_kind, self.regime) {
4694            (OrderKind::Market, _) | (OrderKind::Limit, VenueRegime::MarketDriven) => return Ok(()),
4695            (OrderKind::Limit, VenueRegime::RequestPriced) => format!(
4696                "SimulatedVenue does not support OrderKind::{order_kind:?} in \
4697                 VenueRegime::RequestPriced: it has no market feed, so an order that rested could \
4698                 never be matched. Construct it with SimulatedVenue::new_market_driven, and feed \
4699                 it with apply_market"
4700            ),
4701            _ => format!("SimulatedVenue does not support OrderKind::{order_kind:?}"),
4702        };
4703
4704        Err(UnindexedOrderError::Rejected(ApiError::OrderRejected(
4705            reason,
4706        )))
4707    }
4708
4709    /// Returns whether `instrument` is cash-settled, rejecting kinds this venue cannot model.
4710    ///
4711    /// # Why this is enforced here and not only upstream
4712    /// This type and its constructors are public, so a consumer can construct one directly with
4713    /// any instruments and never pass through the `rustrade` builder that screens kinds
4714    /// today. Without this gate a [`InstrumentKind::Perpetual`], [`InstrumentKind::Future`] or
4715    /// [`InstrumentKind::Option`] falls to the physically-settled spot path and is filled as if it
4716    /// were deliverable stock — with its `contract_size` multiplier applied to a delivery that
4717    /// cannot happen, and with no funding, margin or expiry settlement anywhere. That is a wrong
4718    /// backtest, not a missing feature, and it is silent.
4719    ///
4720    /// # Errors
4721    /// Returns [`ApiError::InstrumentInvalid`] for any kind other than [`InstrumentKind::Spot`] or
4722    /// [`InstrumentKind::Cfd`].
4723    fn settlement_of_supported_kind(
4724        instrument: &Instrument<ExchangeId, AssetNameExchange>,
4725    ) -> Result<bool, UnindexedApiError> {
4726        match &instrument.kind {
4727            InstrumentKind::Spot => Ok(false),
4728            InstrumentKind::Cfd(_) => Ok(true),
4729            unsupported => Err(ApiError::InstrumentInvalid(
4730                instrument.name_exchange.clone(),
4731                format!(
4732                    "SimulatedVenue does not support {}; only Spot and Cfd are modelled",
4733                    match unsupported {
4734                        InstrumentKind::Perpetual(_) => "InstrumentKind::Perpetual",
4735                        InstrumentKind::Future(_) => "InstrumentKind::Future",
4736                        InstrumentKind::Option(_) => "InstrumentKind::Option",
4737                        // Unreachable: both are matched above. Spelled out rather than `_` so a new
4738                        // kind is a compile error here instead of a mislabelled rejection.
4739                        InstrumentKind::Spot | InstrumentKind::Cfd(_) => "InstrumentKind",
4740                    }
4741                ),
4742            )),
4743        }
4744    }
4745
4746    pub fn find_instrument_data(
4747        &self,
4748        instrument: &InstrumentNameExchange,
4749    ) -> Result<&Instrument<ExchangeId, AssetNameExchange>, UnindexedApiError> {
4750        self.instruments.get(instrument).ok_or_else(|| {
4751            ApiError::InstrumentInvalid(
4752                instrument.clone(),
4753                format!("SimulatedVenue is not set-up for managing: {instrument}"),
4754            )
4755        })
4756    }
4757
4758    fn order_id_sequence_fetch_add(&mut self) -> OrderId {
4759        let sequence = self.order_sequence;
4760        self.order_sequence += 1;
4761        OrderId((self.order_id_start + u128::from(sequence)).to_smolstr())
4762    }
4763
4764    /// Mints the next `TradeId`, which no other trade from this venue instance carries.
4765    ///
4766    /// Not derived from the order's id: one order can print twice — filling in part on arrival and
4767    /// again when its remainder is crossed — and two trades under one id would be indistinguishable
4768    /// to anything reconciling them. See [`TradeId`] for what a consumer may assume of one.
4769    fn trade_id_sequence_fetch_add(&mut self) -> TradeId {
4770        let sequence = self.trade_sequence;
4771        self.trade_sequence += 1;
4772        TradeId(sequence.to_smolstr())
4773    }
4774
4775    fn build_account_event<Kind>(&self, kind: Kind) -> UnindexedAccountEvent
4776    where
4777        Kind: Into<AccountEventKind<ExchangeId, AssetNameExchange, InstrumentNameExchange>>,
4778    {
4779        UnindexedAccountEvent {
4780            exchange: self.exchange,
4781            kind: kind.into(),
4782        }
4783    }
4784}
4785
4786/// `model`'s first boundary for `instrument` after `after`, checked against its contract.
4787///
4788/// # Panics
4789/// If the boundary is not after `after`. Charging it would charge one instant forever, and a
4790/// driver waking the venue for it would wake it there forever, so it is refused at the first
4791/// read, wherever that is.
4792fn next_boundary(
4793    model: &dyn HoldingCostModel,
4794    instrument: &InstrumentNameExchange,
4795    after: DateTime<Utc>,
4796) -> Option<DateTime<Utc>> {
4797    let boundary = model.next_boundary(instrument, after)?;
4798    assert!(
4799        boundary > after,
4800        "HoldingCostModel {model:?} returned boundary {boundary} for {instrument}, which is not \
4801         after {after}"
4802    );
4803    Some(boundary)
4804}
4805
4806/// `model`'s first interest boundary for `asset` after `after`, checked against its contract.
4807///
4808/// # Panics
4809/// If the boundary is not after `after` — see [`MarginInterestModel`]'s contract.
4810fn next_interest_boundary(
4811    model: &dyn MarginInterestModel,
4812    asset: &AssetNameExchange,
4813    after: DateTime<Utc>,
4814) -> Option<DateTime<Utc>> {
4815    let boundary = model.next_boundary(asset, after)?;
4816    assert!(
4817        boundary > after,
4818        "MarginInterestModel {model:?} returned boundary {boundary} for {asset}, which is not \
4819         after {after}"
4820    );
4821    Some(boundary)
4822}
4823
4824/// Every boundary in `(after, through]`, each found from the one before by `next`.
4825fn boundaries_through(
4826    after: DateTime<Utc>,
4827    through: DateTime<Utc>,
4828    mut next: impl FnMut(DateTime<Utc>) -> Option<DateTime<Utc>>,
4829) -> impl Iterator<Item = DateTime<Utc>> {
4830    std::iter::successors(next(after), move |&boundary| next(boundary))
4831        .take_while(move |&boundary| boundary <= through)
4832}
4833
4834/// One charge that falls due at a boundary, ordered so a run's charges at one instant arrive in
4835/// the same order every time: holding costs first, then interest.
4836#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4837enum Due {
4838    /// The holding cost of the position in an instrument, by the index of its model.
4839    Holding(InstrumentNameExchange, usize),
4840    /// The interest on the principal owed in an asset, with that principal: charging interest
4841    /// does not change it, so it holds for every boundary of one accrual.
4842    Interest(AssetNameExchange, Decimal),
4843}
4844
4845/// The net positions an `initial_state` opens a venue with: one for each cash-settled instrument
4846/// the snapshot reports an open position in.
4847///
4848/// Its balances are taken as they are, so the margin such a position was opened with is treated as
4849/// already posted, and closing it pays that margin back. A position on a spot instrument is not
4850/// seeded: a spot holding is a balance, not a position, on this venue.
4851///
4852/// # Panics
4853/// Panics if a cash-settled position reports no entry price. Closing it credits margin and
4854/// realised PnL computed from the entry, so an absent one is a mis-specified fixture, in the same
4855/// class as an absent balance.
4856fn seeded_positions(
4857    initial_state: &UnindexedAccountSnapshot,
4858    instruments: &FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
4859) -> FnvHashMap<InstrumentNameExchange, NetPosition> {
4860    initial_state
4861        .instruments
4862        .iter()
4863        .filter(|snapshot| {
4864            instruments
4865                .get(&snapshot.instrument)
4866                .is_some_and(|instrument| matches!(instrument.kind, InstrumentKind::Cfd(_)))
4867        })
4868        .filter_map(|snapshot| {
4869            let position = snapshot.position.open()?;
4870            let Some(entry) = position.entry_price else {
4871                panic!(
4872                    "SimulatedVenue's initial_state reports an open position in {} with no entry \
4873                     price, so the margin and PnL its close would credit cannot be computed",
4874                    snapshot.instrument
4875                );
4876            };
4877            Some((
4878                snapshot.instrument.clone(),
4879                NetPosition {
4880                    quantity: position.quantity,
4881                    entry,
4882                },
4883            ))
4884        })
4885        .collect()
4886}
4887
4888/// What `balance` owes: its loan and the interest on it, zero on a cash account.
4889fn owed(balance: &AssetBalance<AssetNameExchange>) -> Decimal {
4890    balance
4891        .balance
4892        .margin
4893        .map_or(Decimal::ZERO, |margin| margin.owed())
4894}
4895
4896/// Which check a [`SimulatedVenue::valued`] call values the account for, and so which of
4897/// [`SpotMarginConfig`]'s collateral ratios weights its equity.
4898#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4899enum MarginCheck {
4900    /// The leverage limit: how much the venue lends.
4901    Borrow,
4902    /// The margin level a liquidation checks.
4903    Liquidation,
4904}
4905
4906impl MarginCheck {
4907    /// The fraction of `asset`'s positive net that counts as equity for this check.
4908    fn collateral_ratio(self, config: &SpotMarginConfig, asset: &AssetNameExchange) -> Decimal {
4909        match self {
4910            Self::Borrow => config.borrow_collateral_ratio(asset),
4911            Self::Liquidation => config.liquidation_collateral_ratio(asset),
4912        }
4913    }
4914}
4915
4916/// A margin account's equity and debt, in its valuation asset. See [`SimulatedVenue::valued`].
4917#[derive(Debug, Clone, Copy, Default)]
4918struct Valued {
4919    /// The value of what the account holds, net of what it owes, each asset's positive net
4920    /// weighted by its collateral ratio.
4921    equity: Decimal,
4922    /// The value of what it owes, loans plus interest.
4923    debt: Decimal,
4924}
4925
4926/// The mid of `snapshot`'s best bid and ask, else its last price.
4927fn mid_or_last(snapshot: &MarketSnapshot) -> Option<Decimal> {
4928    match (snapshot.best_bid, snapshot.best_ask) {
4929        (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
4930        _ => snapshot.last_price,
4931    }
4932}
4933
4934/// Whether the market has moved to or through `limit` for an order resting on `side`.
4935///
4936/// Judged on the far side of the book -- an offer for a buy, a bid for a sell -- because that is
4937/// what an order on this side trades against. A feed supplying no book at all falls back to
4938/// `last_price`, which is what makes a trades-only feed able to match anything; a feed that does
4939/// supply a book is never second-guessed by it, since a price *between* the two sides crosses
4940/// neither.
4941fn crosses(side: Side, limit: Decimal, market: &MarketSnapshot) -> bool {
4942    match side {
4943        Side::Buy => match market.best_ask {
4944            Some(best_ask) => best_ask <= limit,
4945            None => market.last_price.is_some_and(|last| last <= limit),
4946        },
4947        Side::Sell => match market.best_bid {
4948            Some(best_bid) => best_bid >= limit,
4949            None => market.last_price.is_some_and(|last| last >= limit),
4950        },
4951    }
4952}
4953
4954/// `price` bounded by `limit` for an order on `side`: a buy never above it, a sell never below.
4955fn clamp_to_limit(side: Side, price: Decimal, limit: Decimal) -> Decimal {
4956    match side {
4957        Side::Buy => price.min(limit),
4958        Side::Sell => price.max(limit),
4959    }
4960}
4961
4962/// Whether this venue can honour `time_in_force` for an order of `order_kind` **at all**.
4963///
4964/// This is the static half of the time-in-force decision: it asks only whether the pair could ever
4965/// be honoured, never what the order should then do. What a supported time in force *does* depends
4966/// on whether the order is marketable when it arrives, which is not known here -- see
4967/// [`Disposition`].
4968///
4969/// # Most of a time in force is moot for an order that works only on arrival
4970/// A [`OrderKind::Market`] order trades the instant it arrives and never afterwards, which
4971/// satisfies every time in force that governs *how long* an order works: there is no later instant
4972/// at which it could still be working, and a remainder the book could not fill has no limit price
4973/// to wait at, so it is cancelled rather than kept.
4974///
4975/// [`TimeInForce::AtOpen`] and [`TimeInForce::AtClose`] are not of that kind. They govern *when* an
4976/// order executes, not how long it lasts, and honouring them needs a session calendar this venue
4977/// has not got. Accepting one would fill it immediately, at a price that is not the auction it
4978/// asked for -- a wrong fill rather than an ignored flag. This library takes them seriously
4979/// elsewhere: its IBKR client turns them into real market-on-close and limit-on-close orders, which
4980/// is correct only because a real venue has the calendar. So they are refused here for the same
4981/// reason on both order kinds.
4982///
4983/// [`TimeInForce::GoodUntilCancelled`] with `post_only` is likewise refused on a market order, as
4984/// a promise never to take liquidity is one a market order cannot keep.
4985///
4986/// # Errors
4987/// Returns [`ApiError::OrderRejected`] naming what it would take to honour the pair. Rejecting is
4988/// the point: treating an unsupported time in force as `GoodUntilCancelled` would silently leave an
4989/// order working that its sender asked to have cancelled, and treating an unsupported one as
4990/// "fill now" answers a question the sender did not ask.
4991fn validate_time_in_force_supported(
4992    order_kind: OrderKind,
4993    time_in_force: TimeInForce,
4994) -> Result<(), UnindexedOrderError> {
4995    const NO_CALENDAR: &str = "a session calendar, which this venue does not have";
4996
4997    let missing = match (order_kind, time_in_force) {
4998        // Governs *when* an order executes, not how long it works, whatever the kind.
4999        (_, TimeInForce::AtOpen | TimeInForce::AtClose) => NO_CALENDAR,
5000
5001        // A market order takes liquidity by definition, so it cannot promise not to.
5002        (OrderKind::Market, TimeInForce::GoodUntilCancelled { post_only: true }) => {
5003            "an order that both takes liquidity and refuses to"
5004        }
5005
5006        // Every other time in force bounds how long an order works, which is moot for one that
5007        // fills in full on arrival.
5008        (OrderKind::Market, _) => return Ok(()),
5009
5010        (OrderKind::Limit, TimeInForce::GoodUntilEndOfDay) => NO_CALENDAR,
5011
5012        // Honoured -- what each one then does is `Disposition`'s decision, not this one.
5013        (
5014            OrderKind::Limit,
5015            TimeInForce::GoodUntilCancelled { .. }
5016            | TimeInForce::ImmediateOrCancel
5017            | TimeInForce::FillOrKill
5018            | TimeInForce::GoodTillDate { .. },
5019        ) => return Ok(()),
5020
5021        // Every other kind is rejected before this gate is reached.
5022        (_, _) => return Ok(()),
5023    };
5024
5025    Err(UnindexedOrderError::Rejected(ApiError::OrderRejected(
5026        format!(
5027            "SimulatedVenue does not support TimeInForce::{time_in_force} on \
5028             OrderKind::{order_kind:?}: it does not model {missing}"
5029        ),
5030    )))
5031}
5032
5033/// What a supported time in force makes one arriving order do.
5034///
5035/// The marketability-dependent half of the time-in-force decision, split from
5036/// [`validate_time_in_force_supported`] because three of the four time in forces this venue
5037/// honours on a limit order need context that gate cannot see: `post_only`, `ImmediateOrCancel`
5038/// and `FillOrKill` all turn on whether the order is marketable when it arrives, and
5039/// `GoodTillDate` turns on the clock.
5040///
5041/// # Three ways to aggress, which differ only over a remainder
5042/// What an aggressing order's time in force really decides is the fate of the quantity the book
5043/// could not fill: it rests, or it retires, or -- for [`TimeInForce::FillOrKill`] -- nothing trades
5044/// at all. Which of them an order asked for is a property of the order rather than of what the
5045/// book happened to hold, so all three are decided here and the answer is handed to
5046/// [`fill_on_arrival`](SimulatedVenue::fill_on_arrival) as a [`Remainder`].
5047#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5048enum Disposition {
5049    /// Trade against the book now as the aggressor, resting whatever does not fill.
5050    FillAndRest,
5051    /// Trade against the book now as the aggressor, retiring whatever does not fill.
5052    FillAndCancel,
5053    /// Trade against the book now as the aggressor, but only if it fills in full: an order that
5054    /// could fill only in part trades nothing at all.
5055    FillOrNothing,
5056    /// Join the book and wait to be crossed.
5057    Rest,
5058    /// Retire immediately having traded nothing, because the order asked to work only now and
5059    /// could not: a non-marketable `ImmediateOrCancel` or `FillOrKill`, or a `post_only` order
5060    /// that would have taken liquidity.
5061    CancelUnfilled,
5062    /// Retire immediately having traded nothing, because the order's own deadline has passed.
5063    Expire,
5064}
5065
5066/// Decide what one order does, given whether it is marketable and what time it is.
5067///
5068/// `marketable` is [`crosses`] for a limit order, and `true` for every other kind this venue
5069/// accepts -- each of those is marketable on arrival by definition, which is what lets one function
5070/// answer for both.
5071///
5072/// `order_kind` decides one thing only: whether an unfilled remainder has anywhere to go. An order
5073/// carrying no limit price has nothing to wait at, so a market order aggresses and retires whatever
5074/// is left of it, whichever time in force it carries.
5075///
5076/// # Immediate-or-cancel and fill-or-kill differ only when the book is too thin
5077/// Both aggress on arrival and neither waits, so while the book can fill an order whole they are
5078/// the same instruction. They part exactly where a fill is capped by the size on offer:
5079/// [`Disposition::FillAndCancel`] takes what there is and retires the rest, while
5080/// [`Disposition::FillOrNothing`] would rather trade nothing than trade in part, and so trades
5081/// nothing. Which one an order gets is decided here; what they then do with the remainder is
5082/// [`Remainder`]'s.
5083fn disposition(
5084    order_kind: OrderKind,
5085    time_in_force: TimeInForce,
5086    marketable: bool,
5087    now: DateTime<Utc>,
5088) -> Disposition {
5089    match time_in_force {
5090        // Post-only means "never take liquidity", so being marketable is what disqualifies it.
5091        TimeInForce::GoodUntilCancelled { post_only: true } if marketable => {
5092            Disposition::CancelUnfilled
5093        }
5094
5095        // A deadline already reached retires the order whatever the book is doing. Checked before
5096        // marketability so that a deadline is an unconditional cutoff rather than one an order can
5097        // trade its way past at the very instant it expires.
5098        TimeInForce::GoodTillDate { expiry } if now >= expiry => Disposition::Expire,
5099
5100        // The one time in force that would rather trade nothing than trade in part, whatever the
5101        // order kind.
5102        TimeInForce::FillOrKill if marketable => Disposition::FillOrNothing,
5103
5104        // Every other aggressing order keeps what it could not fill only if it has somewhere to
5105        // keep it: a limit price to wait at, and a time in force that asked to wait. `post_only`
5106        // is not among them here -- a marketable one was disqualified above.
5107        _ if marketable => match (order_kind, time_in_force) {
5108            (
5109                OrderKind::Limit,
5110                TimeInForce::GoodUntilCancelled { .. } | TimeInForce::GoodTillDate { .. },
5111            ) => Disposition::FillAndRest,
5112            _ => Disposition::FillAndCancel,
5113        },
5114
5115        TimeInForce::ImmediateOrCancel | TimeInForce::FillOrKill => Disposition::CancelUnfilled,
5116
5117        _ => Disposition::Rest,
5118    }
5119}
5120
5121/// How old an instrument's market is when an order arrives — see
5122/// [`StaleMarketPolicy::Wait`].
5123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5124enum MarketAge {
5125    /// The venue has no market feed, so no age to measure.
5126    NoFeed,
5127    /// The venue holds no price for the instrument: not stale, but unpriceable, which the order's
5128    /// usual path reports.
5129    Unpriced,
5130    /// Observed fresh within the policy's `max_age`.
5131    Fresh,
5132    /// Priced, but last observed fresh at `fresh_at`, longer ago than the policy's `max_age`, or
5133    /// never.
5134    Stale { fresh_at: Option<DateTime<Utc>> },
5135}
5136
5137/// What a held order is priced from when it is released.
5138#[derive(Debug, Clone, Copy)]
5139enum Release<'a> {
5140    /// The bar it was held for, at the reference price its [`MarketOrderTiming::NextBar`] picks.
5141    Bar(&'a MarketBar, NextBarPrice),
5142    /// The market a fresh observation left, which it was waiting for under
5143    /// [`StaleMarketPolicy::Wait`].
5144    Market(&'a MarketSnapshot),
5145}
5146
5147/// What becomes of the quantity an arriving order's book could not fill.
5148///
5149/// The half of a [`Disposition`] that [`fill_on_arrival`](SimulatedVenue::fill_on_arrival) needs,
5150/// derived at the two call sites that can reach it. Carrying the limit price *here* rather than
5151/// re-reading [`RequestOpen::price`](crate::order::request::RequestOpen::price) is what makes
5152/// "rest what is left" unrepresentable for an order that has nowhere to rest it: a market order
5153/// has no limit price, so it cannot construct [`Rest`](Self::Rest) at all.
5154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5155enum Remainder {
5156    /// Joins the book at this limit and waits to be crossed.
5157    Rest { limit: Decimal },
5158    /// Retires as [`Cancelled`], carrying whatever did trade.
5159    Cancel,
5160    /// Nothing trades at all. An order that cannot be filled in full would rather be filled not at
5161    /// all, so there is never a remainder for this arm to dispose of -- only a whole order to
5162    /// retire untouched.
5163    Kill,
5164}
5165
5166impl Remainder {
5167    /// Whether a remainder is refused rather than disposed of, which is what makes
5168    /// [`TimeInForce::FillOrKill`] differ from [`TimeInForce::ImmediateOrCancel`].
5169    fn is_kill(self) -> bool {
5170        matches!(self, Self::Kill)
5171    }
5172}
5173
5174fn build_open_order_err_response<E>(
5175    request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
5176    error: E,
5177) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>
5178where
5179    E: Into<UnindexedOrderError>,
5180{
5181    Order {
5182        key: request.key,
5183        side: request.state.side,
5184        price: request.state.price,
5185        quantity: request.state.quantity,
5186        kind: request.state.kind,
5187        time_in_force: request.state.time_in_force,
5188        state: OrderState::inactive(error.into()),
5189    }
5190}
5191
5192/// What one open owes the client, before it is committed and packaged into events.
5193///
5194/// Internal to [`SimulatedVenue::open_order`]: it exists only to keep the ordering contract --
5195/// emit the balance before the trade it paid for -- visible in one short place rather than at the
5196/// end of the pricing body. Nothing here is uncommitted; it is what to *say*, not what to do.
5197///
5198/// An enum rather than an optional trade because the two outcomes owe different things and a
5199/// missing trade is not an absent field: a rested open has a balance and *no* trade, and nothing
5200/// about it is incomplete.
5201///
5202/// # One arrival restates each asset once
5203/// Including the arrival that both settles a fill and holds against the remainder it leaves
5204/// resting. Those are one event at the venue and one commitment in the ledger
5205/// ([`AccountState::commit`]), so they restate the asset paid with once, and a spot fill the asset
5206/// it received once more — see that method for why an intermediate would report a balance the
5207/// account never held.
5208///
5209/// [`AccountState::commit`]: crate::exchange::mock::account::AccountState::commit
5210// `large_enum_variant`: boxing the `Trade` would trade one move of ~300 bytes for a heap
5211// allocation on every fill, which is the wrong way round for a backtest that does millions of
5212// them. The value is built once per order and destructured immediately, and the struct this
5213// replaced moved the same bytes on both paths -- the enum only makes the rested one smaller.
5214#[allow(clippy::large_enum_variant)]
5215#[derive(Debug)]
5216enum OpenOrderNotifications {
5217    /// The open filled on arrival: the balances it moved, and the trade it printed.
5218    Filled {
5219        commitment: Commitment,
5220        trade: Trade<AssetNameExchange, InstrumentNameExchange>,
5221    },
5222    /// The open rests on the book, or is held for its bar: the balance now held against it, and
5223    /// nothing traded. Interest charged on what a margin account lent it is held back in its
5224    /// [`Reservation::unreported_interest`], since no position exists yet for it to belong to.
5225    Rested {
5226        balance: Snapshot<AssetBalance<AssetNameExchange>>,
5227    },
5228}
5229
5230/// What one arrival committed on this venue: the balances the ledger restated, and the interest
5231/// charged on what a margin account lent it, if any.
5232#[derive(Debug)]
5233struct Commitment {
5234    committed: Committed,
5235    /// The cash flow reporting the interest charged on [`Committed::borrowed`] when it was lent,
5236    /// which [`Committed::paid`] already includes.
5237    ///
5238    /// Boxed because only a borrowing arrival on a margin account charges one: an unboxed flow
5239    /// would more than double every commitment, and every [`RestingSettlement`] carrying one, to
5240    /// serve the rare arrival that has it.
5241    interest: Option<Box<CashFlow<AssetNameExchange, InstrumentNameExchange>>>,
5242    /// The interest a resting or held order's loan was charged when the order was placed, which
5243    /// its hold's balance already included, handed over from its
5244    /// [`Reservation::unreported_interest`] to be reported after the fill.
5245    held_interest: Vec<CashFlow<AssetNameExchange, InstrumentNameExchange>>,
5246    /// The fee asset's balance, restated when a fill's fee was
5247    /// [taken apart](Settlement::fee_apart) from it.
5248    ///
5249    /// Boxed for the reason [`interest`](Self::interest) is: only a fill charged in a configured
5250    /// fee asset restates one.
5251    fee: Option<Box<AssetBalance<AssetNameExchange>>>,
5252}
5253
5254impl From<Committed> for Commitment {
5255    fn from(committed: Committed) -> Self {
5256        Self {
5257            committed,
5258            interest: None,
5259            held_interest: Vec::new(),
5260            fee: None,
5261        }
5262    }
5263}
5264
5265/// What releasing an order's hold restated, and the interest its loan was charged when the order
5266/// was placed, which the order's end now reports.
5267#[derive(Debug)]
5268#[must_use = "its interest must be reported, or carried into the order's next reservation"]
5269struct Released {
5270    balance: AssetBalance<AssetNameExchange>,
5271    /// See [`Reservation::unreported_interest`].
5272    interest: Vec<CashFlow<AssetNameExchange, InstrumentNameExchange>>,
5273}
5274
5275/// The instrument facts one order's settlement is computed from.
5276///
5277/// Read out of [`SimulatedVenue::instruments`] once per request, before any `&mut self` borrow of
5278/// the ledger, so pricing never holds a borrow the balance moves need.
5279#[derive(Debug, Clone)]
5280struct InstrumentTerms {
5281    underlying: Underlying<AssetNameExchange>,
5282    contract_size: Decimal,
5283    cash_settled: bool,
5284}
5285
5286/// What settling one resting fill came to.
5287#[derive(Debug)]
5288enum RestingSettlement {
5289    /// The fill settled, restating these balances.
5290    Settled(Commitment),
5291    /// A fill the account can no longer afford, so the order is cancelled instead: a cash-settled
5292    /// one, or a spot fill whose fee exceeds what it receives by more than that asset's balance
5293    /// holds.
5294    /// Carries what its released hold restated, if it held anything.
5295    Unaffordable { released: Option<Released> },
5296}
5297
5298/// What committing an order afresh, in place of the hold it was released from, came to — see
5299/// `SimulatedVenue::commit_afresh`.
5300// `large_enum_variant`: as for `OpenOrderNotifications`, boxing the `Commitment` would add a heap
5301// allocation to every fill costed afresh to save one move of a value that is returned once and
5302// destructured at once.
5303#[allow(clippy::large_enum_variant)]
5304enum Recommitted {
5305    /// Committed, restating these balances. `loan` is how much of the account's loans the order
5306    /// accounts for afterwards: what its hold had borrowed, less what this repaid, plus what this
5307    /// borrowed of its shortfall. A caller holding part of what it committed against a remainder
5308    /// holds that loan against it, so a later cancel repays it. `excess_borrowed` is what minimum
5309    /// borrows lent the order beyond its shortfalls, its hold's and this one's, which a remainder
5310    /// carries too.
5311    Committed {
5312        commitment: Commitment,
5313        loan: Decimal,
5314        excess_borrowed: Decimal,
5315    },
5316    /// The account cannot cover it. Carries what the released hold restated, if it held anything.
5317    Unaffordable { released: Option<Released> },
5318}
5319
5320/// How much of an arriving order traded, and at what price.
5321///
5322/// One value because the two are decided together and used together: the quantity is what the
5323/// book had to give, and the price is what it gave it at.
5324#[derive(Debug, Clone, Copy)]
5325struct Fill {
5326    quantity: Decimal,
5327    price: Decimal,
5328}
5329
5330/// What one order pays with, how much of it, the fee charged, and what the fill gives back.
5331#[derive(Debug, Clone)]
5332struct Settlement {
5333    /// Quote for a buy or a CFD, base for a spot sell.
5334    asset: AssetNameExchange,
5335    /// Denominated in [`asset`](Self::asset), inclusive of any fee charged in it.
5336    amount: Decimal,
5337    /// What the fill gives the account. For a CFD, the margin and realised PnL of the part that
5338    /// closes a position, [paid back](Credit::PaidBack) into [`asset`](Self::asset). For spot, a
5339    /// [delivery](Credit::Delivery) in the other asset: the base a buy receives, or the quote a
5340    /// sell receives, net of any fee charged in it.
5341    credit: Credit,
5342    /// In the asset the fee is charged in: quote, base for a buy under
5343    /// [`SpotFeeAsset::Received`], or the [`FeeAssetConfig`] asset. Its `fees_quote` is always the
5344    /// quote fee charged, after any discount.
5345    fees: AssetFees<AssetNameExchange>,
5346    /// Whether [`fees`](Self::fees) is taken apart from the fill: out of its own asset's free
5347    /// balance when the fill settles, and in neither [`amount`](Self::amount) nor
5348    /// [`credit`](Self::credit). Only a fill charged in the [`FeeAssetConfig`] asset, which is
5349    /// neither of the two assets it exchanges.
5350    fee_apart: bool,
5351}
5352
5353impl Settlement {
5354    /// What must be held for this settlement while the order it belongs to is still working.
5355    ///
5356    /// The amount net of what the fill will pay back into the asset held, and never negative. An
5357    /// order that only reduces a CFD position is paid more by its fill than it costs, so it holds
5358    /// nothing: the account that opened a position with all of its balance can still close it. A
5359    /// spot fill pays back in the other asset, which funds nothing, so this is its whole amount.
5360    fn requirement(&self) -> Decimal {
5361        match &self.credit {
5362            Credit::PaidBack(credit) => (self.amount - credit).max(Decimal::ZERO),
5363            Credit::Delivery(_) => self.amount,
5364        }
5365    }
5366
5367    /// The whole of this settlement leaves the account, because the whole of it traded.
5368    fn settled(&self) -> Debit {
5369        Debit {
5370            asset: self.asset.clone(),
5371            settled: self.amount,
5372            reserved: Decimal::ZERO,
5373        }
5374    }
5375
5376    /// The whole of this settlement's [`requirement`](Self::requirement) is held, because the
5377    /// order it belongs to is still working.
5378    fn reserved(&self) -> Debit {
5379        Debit {
5380            asset: self.asset.clone(),
5381            settled: Decimal::ZERO,
5382            reserved: self.requirement(),
5383        }
5384    }
5385
5386    /// This settlement traded and `remainder`'s [`requirement`](Self::requirement) is held
5387    /// against what is still working: one order that the book filled in part and that rested the
5388    /// rest.
5389    ///
5390    /// # Panics
5391    /// Debug-asserts that both settlements pay with the same asset. They come from one order, so
5392    /// they share its side and its instrument, and [`SimulatedVenue::settlement`] decides the
5393    /// asset from nothing else — a disagreement would mean one order paying with two assets.
5394    fn settled_holding(&self, remainder: &Settlement) -> Debit {
5395        debug_assert!(
5396            self.asset == remainder.asset,
5397            "one order settled {} of {} and reserved {} of {}: an order pays with one asset, so \
5398             two settlements of it cannot name two",
5399            self.amount,
5400            self.asset,
5401            remainder.amount,
5402            remainder.asset
5403        );
5404
5405        Debit {
5406            asset: self.asset.clone(),
5407            settled: self.amount,
5408            reserved: remainder.requirement(),
5409        }
5410    }
5411}
5412
5413/// A cash-settled instrument's net position on this venue: what a CFD fill closes before it
5414/// opens anything.
5415///
5416/// Kept per instrument, in contracts, whatever position mode the client keeps its own book in. A
5417/// CFD venue nets one account's trades in one instrument, which is what decides how much of a fill
5418/// returns margin and realises PnL rather than posting new margin.
5419#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5420struct NetPosition {
5421    /// Signed quantity in contracts: positive long, negative short, zero flat.
5422    quantity: Decimal,
5423    /// Volume-weighted average price the open `quantity` was entered at. Zero while flat.
5424    ///
5425    /// Rounded to `Decimal` precision when an average does not terminate, so a later close can
5426    /// return a margin that differs from the one posted in the last of its 28 digits.
5427    entry: Decimal,
5428}
5429
5430impl NetPosition {
5431    fn is_long(&self) -> bool {
5432        self.quantity.is_sign_positive() && !self.quantity.is_zero()
5433    }
5434
5435    /// How much of a fill of `quantity` on `side` closes this position. The rest of it opens one.
5436    fn closing(&self, side: Side, quantity: Decimal) -> Decimal {
5437        let opposes = match side {
5438            Side::Buy => self.quantity.is_sign_negative() && !self.quantity.is_zero(),
5439            Side::Sell => self.is_long(),
5440        };
5441        match opposes {
5442            true => quantity.min(self.quantity.abs()),
5443            false => Decimal::ZERO,
5444        }
5445    }
5446
5447    /// This position after a fill of `quantity` on `side` at `price`.
5448    ///
5449    /// Averaged as the `rustrade` engine averages its own `Position`: an increase moves the entry
5450    /// to the volume-weighted average, a reduction keeps it, and a fill that opens from flat or
5451    /// flips through zero enters what is left at the fill price.
5452    fn after(self, side: Side, quantity: Decimal, price: Decimal) -> Self {
5453        let signed = match side {
5454            Side::Buy => quantity,
5455            Side::Sell => -quantity,
5456        };
5457        let next = self.quantity + signed;
5458
5459        let entry = if next.is_zero() {
5460            Decimal::ZERO
5461        } else if self.quantity.is_zero()
5462            || next.is_sign_negative() != self.quantity.is_sign_negative()
5463        {
5464            price
5465        } else if signed.is_sign_negative() == self.quantity.is_sign_negative() {
5466            (self.entry * self.quantity.abs() + price * quantity) / next.abs()
5467        } else {
5468            self.entry
5469        };
5470
5471        Self {
5472            quantity: next,
5473            entry,
5474        }
5475    }
5476}
5477
5478/// The id a venue seeded with `account` mints its first order with: one past the highest decimal
5479/// `OrderId` among the seeded orders, or `0` if none carries one.
5480///
5481/// Open and cancelled orders both count, which is everything an account snapshot seeds. Each was
5482/// given its id by the venue the state describes, so reusing one would make two orders
5483/// indistinguishable to anything keyed on it.
5484fn first_unseeded_order_id(account: &AccountState) -> u128 {
5485    let open = account
5486        .orders_open()
5487        .filter_map(|order| match &order.state.id {
5488            VenueOrderId::Assigned(id) => Some(id),
5489            VenueOrderId::ClientAssigned => None,
5490        });
5491    let cancelled = account.orders_cancelled().map(|order| &order.state.id);
5492
5493    open.chain(cancelled)
5494        .filter_map(|id| id.0.parse::<u64>().ok())
5495        .max()
5496        .map_or(0, |highest| u128::from(highest) + 1)
5497}
5498
5499#[cfg(test)]
5500#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics on bad input are acceptable
5501mod tests {
5502    use super::*;
5503    use crate::{
5504        balance::{Balance, MarginDetails},
5505        cash_flow::CashFlowKind,
5506        error::OrderError,
5507        exchange::mock::{
5508            fixtures::*,
5509            orders::{OpenOrders, as_open},
5510        },
5511        fee::{PerContractFeeModel, PercentageFeeModel},
5512        fill::{BidAskFillModel, NextBarPrice, SimFillConfig},
5513        holding_cost::{
5514            AccrualDays, BorrowFeeModel, DailyAccrual, DayCount, FinancingModel, FinancingRates,
5515            FundingModel, HoldingCharge, RateSeries, WeekendsOnly,
5516        },
5517        margin::{InterestCharge, InterestSchedule, MarginEffect, PeriodicMarginInterest},
5518        order::{
5519            OrderEvent, OrderKey, TimeInForce,
5520            id::{ClientOrderId, StrategyId},
5521            request::RequestCancel,
5522            state::{ActiveOrderState, InactiveOrderState},
5523        },
5524        shortability::{Shortability, ShortabilityTable},
5525        slippage::SlippageConfig,
5526    };
5527    use chrono::{NaiveTime, TimeDelta};
5528    use rustrade_instrument::{
5529        Underlying,
5530        instrument::{
5531            kind::cfd::CfdContract, name::InstrumentNameInternal, quote::InstrumentQuoteAsset,
5532        },
5533    };
5534
5535    fn make_venue(btc: &str, usdt: &str) -> SimulatedVenue {
5536        make_venue_with_fee(btc, usdt, FeeModelConfig::default())
5537    }
5538
5539    fn make_venue_with_fee(btc: &str, usdt: &str, fee_model: FeeModelConfig) -> SimulatedVenue {
5540        SimulatedVenue::new(&spot_config(btc, usdt, fee_model), spot_instruments())
5541    }
5542
5543    /// Maker 2bp against taker 10bp, so a fill charged the wrong side is off by 5x rather than by a
5544    /// rounding difference.
5545    fn maker_taker_fee() -> FeeModelConfig {
5546        FeeModelConfig::Percentage(PercentageFeeModel::maker_taker(d("0.0002"), d("0.001")))
5547    }
5548
5549    /// A venue fed by a driver, and so the only regime that accepts limit orders.
5550    fn make_market_venue(btc: &str, usdt: &str) -> SimulatedVenue {
5551        make_market_venue_with(btc, usdt, maker_taker_fee(), SimFillConfig::default())
5552    }
5553
5554    fn make_market_venue_with(
5555        btc: &str,
5556        usdt: &str,
5557        fee_model: FeeModelConfig,
5558        fill_model: SimFillConfig,
5559    ) -> SimulatedVenue {
5560        SimulatedVenue::new_market_driven(
5561            &spot_config_with_fill(btc, usdt, fee_model, fill_model),
5562            spot_instruments(),
5563        )
5564    }
5565
5566    /// `events` without the margin debt updates a spot-margin venue follows a restatement with,
5567    /// for a test about the other events and their order.
5568    fn sans_debt(events: &[UnindexedAccountEvent]) -> Vec<&UnindexedAccountEvent> {
5569        events
5570            .iter()
5571            .filter(|event| !matches!(event.kind, AccountEventKind::MarginDebt(_)))
5572            .collect()
5573    }
5574
5575    /// The balance restatement a venue owes, read off the event it emitted.
5576    fn balance_of(event: &UnindexedAccountEvent) -> &AssetBalance<AssetNameExchange> {
5577        match &event.kind {
5578            AccountEventKind::BalanceSnapshot(balance) => &balance.0,
5579            other => panic!("expected a balance restatement, got: {other:?}"),
5580        }
5581    }
5582
5583    fn trade_of(
5584        event: &UnindexedAccountEvent,
5585    ) -> &Trade<AssetNameExchange, InstrumentNameExchange> {
5586        match &event.kind {
5587            AccountEventKind::Trade(trade) => trade,
5588            other => panic!("expected a trade, got: {other:?}"),
5589        }
5590    }
5591
5592    fn order_of(
5593        event: &UnindexedAccountEvent,
5594    ) -> &Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
5595        match &event.kind {
5596            AccountEventKind::OrderSnapshot(order) => &order.0,
5597            other => panic!("expected an order snapshot, got: {other:?}"),
5598        }
5599    }
5600
5601    /// The `Open` a rested order reports, or a panic naming what it reported instead.
5602    fn rested_open(outcome: &OpenOutcome) -> &Open {
5603        match &outcome.response.state {
5604            OrderState::Active(ActiveOrderState::Open(open)) => open,
5605            other => panic!("expected a resting open order, got: {other:?}"),
5606        }
5607    }
5608
5609    /// The price a filled order reports having filled at.
5610    fn filled_price(
5611        order: &Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
5612    ) -> Decimal {
5613        match &order.state {
5614            OrderState::Inactive(InactiveOrderState::FullyFilled(filled)) => filled
5615                .avg_price
5616                .expect("a filled order reports the price it filled at"),
5617            other => panic!("expected a fully filled order, got: {other:?}"),
5618        }
5619    }
5620
5621    /// Rests one buy limit, returning the venue holding it and what it told its client.
5622    ///
5623    /// The market is set first and does not cross the order, which is what makes it rest rather
5624    /// than fill on arrival.
5625    fn venue_resting_one_buy(limit: &str) -> (SimulatedVenue, OpenOutcome) {
5626        let mut venue = make_market_venue("10", "1000000");
5627        advance(&mut venue, time(1));
5628
5629        assert!(
5630            venue
5631                .apply_market(
5632                    &instrument_name(),
5633                    MarketObservation::new(book("49000", "49100")),
5634                    time(1)
5635                )
5636                .is_empty(),
5637            "an empty book fills nothing"
5638        );
5639
5640        let outcome = venue.open_order(limit_request("resting", Side::Buy, "1", limit, gtc()));
5641        assert_eq!(
5642            outcome.events.len(),
5643            1,
5644            "a rested order restates one balance and books no trade"
5645        );
5646
5647        (venue, outcome)
5648    }
5649
5650    /// Advances `venue`, asserting the advance retired nothing.
5651    ///
5652    /// Every existing test predates deadlines and holds only `GoodUntilCancelled` orders, so a
5653    /// sweep that produced anything would mean an order retired that nothing asked to expire.
5654    /// Asserting that beats discarding the value: it is the same one line, and it pins the
5655    /// property instead of hiding it.
5656    fn advance(venue: &mut SimulatedVenue, time_exchange: DateTime<Utc>) {
5657        assert!(
5658            venue.advance_time(time_exchange).is_empty(),
5659            "advancing to {time_exchange} retired an order this test never gave a deadline"
5660        );
5661    }
5662
5663    fn time(seconds: i64) -> DateTime<Utc> {
5664        DateTime::<Utc>::from_timestamp(1_700_000_000 + seconds, 0).expect("representable instant")
5665    }
5666
5667    /// The balance snapshot a filled open owes its client, read back off the venue's own output.
5668    ///
5669    /// The assertions below go through [`VenueOutcome::events`] rather than any internal, so they
5670    /// pin what a driver actually delivers — and, by indexing, the order it must deliver it in.
5671    fn filled_balance(outcome: &OpenOutcome) -> &AssetBalance<AssetNameExchange> {
5672        match sans_debt(&outcome.events).first().map(|event| &event.kind) {
5673            Some(AccountEventKind::BalanceSnapshot(balance)) => &balance.0,
5674            other => panic!("a filled open must emit its balance first, got: {other:?}"),
5675        }
5676    }
5677
5678    /// The balance a filled spot open received, which follows the one it paid with.
5679    fn filled_received(outcome: &OpenOutcome) -> &AssetBalance<AssetNameExchange> {
5680        match sans_debt(&outcome.events).get(1).map(|event| &event.kind) {
5681            Some(AccountEventKind::BalanceSnapshot(balance)) => &balance.0,
5682            other => panic!("a filled spot open must emit what it received second, got: {other:?}"),
5683        }
5684    }
5685
5686    /// The trade a filled open owes its client, which must follow every balance it moved, and the
5687    /// interest charged on what it borrowed.
5688    fn filled_trade(outcome: &OpenOutcome) -> &Trade<AssetNameExchange, InstrumentNameExchange> {
5689        let Some(at) = outcome
5690            .events
5691            .iter()
5692            .position(|event| matches!(event.kind, AccountEventKind::Trade(_)))
5693        else {
5694            panic!(
5695                "a filled open must emit its trade, got: {:?}",
5696                outcome.events
5697            );
5698        };
5699        assert!(
5700            at > 0
5701                && outcome.events[..at].iter().all(|event| matches!(
5702                    event.kind,
5703                    AccountEventKind::BalanceSnapshot(_)
5704                        | AccountEventKind::CashFlow(_)
5705                        | AccountEventKind::MarginDebt(_)
5706                )),
5707            "a filled open must emit its balances before its trade, got: {:?}",
5708            outcome.events
5709        );
5710        trade_of(&outcome.events[at])
5711    }
5712
5713    /// The venue's ordering obligation, asserted without any transport in the way.
5714    ///
5715    /// Two separate claims, because a driver needs both: each fill reports its balances before its
5716    /// trade, and successive fills report strictly decreasing balances. A balance here is an
5717    /// absolute restatement rather than a delta, so a driver that reorders them does not merely
5718    /// reorder history — it leaves the client holding the wrong number.
5719    #[test]
5720    fn a_filled_open_reports_its_balance_before_its_trade_and_debits_monotonically() {
5721        const FILLS: usize = 4;
5722
5723        let mut venue = make_venue("100", "10000000");
5724        let mut balances = Vec::with_capacity(FILLS);
5725
5726        for _ in 0..FILLS {
5727            let outcome = venue.open_order(buy_request("0.01", market_prices("50000")));
5728
5729            assert_eq!(
5730                outcome.events.len(),
5731                3,
5732                "a spot fill owes the balance it paid with, the one it received, and one trade"
5733            );
5734            // The accessors assert their own positions, so reaching here is the order assertion.
5735            let balance = filled_balance(&outcome);
5736            let received = filled_received(&outcome);
5737            let trade = filled_trade(&outcome);
5738
5739            assert_eq!(balance.asset, quote(), "a spot buy debits quote");
5740            assert_eq!(received.asset, base(), "and credits base");
5741            assert_eq!(trade.price, d("50000"));
5742            balances.push(balance.balance.total);
5743        }
5744
5745        // 0.01 * 50 000 = 500 per fill, from a 10 000 000 start.
5746        let expected = (1..=FILLS)
5747            .map(|nth| d("10000000") - d("500") * Decimal::from(nth))
5748            .collect::<Vec<_>>();
5749        assert_eq!(
5750            balances, expected,
5751            "successive fills must restate a strictly decreasing balance"
5752        );
5753    }
5754
5755    /// The fill is in the ledger the moment `open_order` returns, not when its events are delivered.
5756    ///
5757    /// The venue acknowledges its own trade, so the ledger cannot disagree with the events a driver
5758    /// is still holding: a later request sees the fill regardless of when the driver gets around to
5759    /// emitting it. Splitting that across the seam — book here, acknowledge in the driver — is what
5760    /// let one driver forget.
5761    #[test]
5762    fn a_filled_open_is_in_the_ledger_before_its_events_are_delivered() {
5763        let mut venue = make_venue("100", "10000000");
5764        let before = venue.time_exchange();
5765
5766        let outcome = venue.open_order(buy_request("0.01", market_prices("50000")));
5767        assert_eq!(outcome.events.len(), 3, "the fill must owe events");
5768
5769        // Nothing has delivered `outcome`, yet the ledger already reflects it.
5770        let trades = venue.trades(before);
5771        assert_eq!(trades.len(), 1, "the venue must acknowledge its own trade");
5772        assert_eq!(trades[0].price, d("50000"));
5773    }
5774
5775    /// `advance_time` sets the instant everything a request produces after it is stamped with.
5776    #[test]
5777    fn a_fill_is_stamped_with_the_instant_the_venue_was_last_advanced() {
5778        let mut venue = make_venue("100", "10000000");
5779        let time_exchange = "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
5780
5781        advance(&mut venue, time_exchange);
5782        assert_eq!(venue.time_exchange(), time_exchange);
5783
5784        let outcome = venue.open_order(buy_request("0.01", market_prices("50000")));
5785        assert_eq!(filled_trade(&outcome).time_exchange, time_exchange);
5786        assert_eq!(filled_balance(&outcome).time_exchange, time_exchange);
5787    }
5788
5789    /// A run may start before 1970: the first instant is taken as given, not held at the epoch
5790    /// the clock reads before it first moves.
5791    #[test]
5792    fn the_first_instant_may_precede_the_epoch() {
5793        let mut venue = make_venue("100", "10000000");
5794        let start = "1965-03-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
5795
5796        advance(&mut venue, start);
5797
5798        assert_eq!(venue.time_exchange(), start);
5799    }
5800
5801    /// A request arriving behind a market tick the venue has already applied is stamped at the
5802    /// tick, so nothing it reports is older than what the tick could have reported.
5803    #[test]
5804    fn a_request_behind_a_later_market_tick_is_stamped_at_the_tick() {
5805        let mut venue = make_cfd_venue_charging("200000", Vec::new());
5806        let tick = time(10);
5807        let fills = venue.apply_market(
5808            &cfd_instrument_name(),
5809            MarketObservation::new(MarketSnapshot {
5810                best_bid: Some(d("5000")),
5811                best_ask: Some(d("5000")),
5812                last_price: Some(d("5000")),
5813            }),
5814            tick,
5815        );
5816        assert!(fills.is_empty());
5817
5818        assert!(venue.advance_time(time(5)).is_empty());
5819        assert_eq!(venue.time_exchange(), tick, "the clock is held at the tick");
5820
5821        let outcome = venue.open_order(cfd_request(Side::Buy, "1", None));
5822        assert_eq!(filled_trade(&outcome).time_exchange, tick);
5823        assert_eq!(filled_balance(&outcome).time_exchange, tick);
5824    }
5825
5826    /// A market tick behind an instant the venue has already reached is applied at that instant,
5827    /// so the fill it causes, the balances it restates and the market it stores are stamped no
5828    /// earlier than what the venue has already reported.
5829    #[test]
5830    fn a_market_tick_behind_the_clock_is_applied_at_the_clock() {
5831        let (mut venue, _) = venue_resting_one_buy("48000");
5832        let reached = time(10);
5833        advance(&mut venue, reached);
5834
5835        let events = venue.apply_market(
5836            &instrument_name(),
5837            MarketObservation::new(book("47800", "47900")),
5838            time(5),
5839        );
5840
5841        assert_eq!(
5842            events.len(),
5843            4,
5844            "the tick still crosses the resting order: two balances, a trade, an order"
5845        );
5846        assert_eq!(balance_of(&events[0]).time_exchange, reached);
5847        assert_eq!(balance_of(&events[1]).time_exchange, reached);
5848        assert_eq!(trade_of(&events[2]).time_exchange, reached);
5849        assert!(
5850            venue
5851                .account
5852                .balances()
5853                .all(|balance| balance.time_exchange == reached),
5854            "no balance is restamped earlier than the venue has reported"
5855        );
5856        let market = venue.market(&instrument_name()).unwrap();
5857        assert_eq!(market.time_exchange, reached);
5858        assert_eq!(market.fresh_at, Some(reached));
5859        assert_eq!(venue.time_exchange(), reached);
5860    }
5861
5862    /// A resting order's arrival stamp survives the clock moving past it.
5863    ///
5864    /// `advance_time` used to rewrite `time_exchange` on every open order. Nothing rested, so the
5865    /// only orders it could reach were those an `initial_state` seeded — and it moved them to
5866    /// whenever the clock last ticked. Arrival order is half of price-time priority, so an order
5867    /// whose arrival instant is rewritten on every event has no arrival instant at all.
5868    #[test]
5869    fn advancing_the_clock_does_not_restamp_a_resting_order() {
5870        let arrived = "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
5871        let much_later = "2025-06-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
5872
5873        let mut venue = make_venue("100", "10000000");
5874        venue.account = AccountState::new(
5875            venue
5876                .account
5877                .balances()
5878                .cloned()
5879                .map(|balance| (balance.asset.clone(), balance))
5880                .collect(),
5881            {
5882                let mut book = OpenOrders::default();
5883                book.insert(
5884                    Order {
5885                        key: OrderKey {
5886                            exchange: EXCHANGE,
5887                            instrument: instrument_name(),
5888                            strategy: StrategyId::new("test"),
5889                            cid: ClientOrderId::new("resting"),
5890                        },
5891                        side: Side::Buy,
5892                        price: Some(d("50000")),
5893                        quantity: d("1"),
5894                        kind: OrderKind::Limit,
5895                        time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
5896                        state: Open {
5897                            id: VenueOrderId::Assigned(OrderId::new("resting")),
5898                            time_exchange: arrived,
5899                            filled_quantity: Decimal::ZERO,
5900                        },
5901                    },
5902                    None,
5903                )
5904                .unwrap();
5905                book
5906            },
5907            Default::default(),
5908            Vec::new(),
5909        );
5910
5911        advance(&mut venue, much_later);
5912
5913        let resting = venue.orders_open(&[]);
5914        assert_eq!(resting.len(), 1, "the seeded order must still be open");
5915        assert_eq!(
5916            resting[0].state.time_exchange, arrived,
5917            "an order does not become a different order because time passed"
5918        );
5919
5920        // Balances do move: a venue restates them as of now, and they carry no ordering role.
5921        let usdt = venue.balances(&[quote()]);
5922        assert_eq!(usdt[0].time_exchange, much_later);
5923    }
5924
5925    /// A cancel for an id the venue never booked is rejected as unknown, and the rejection is the
5926    /// venue's rather than any driver's.
5927    ///
5928    /// Answering here — rather than in each driver — is what stops two drivers from carrying two
5929    /// copies of the same policy, and the response is the one the request's channel asks for.
5930    #[test]
5931    fn a_cancel_for_an_unknown_order_is_rejected_as_unknown() {
5932        let mut venue = make_venue("100", "10000000");
5933        let key = buy_request("1", market_prices("50000")).key;
5934
5935        let outcome = venue.cancel_order(OrderEvent {
5936            key: key.clone(),
5937            state: RequestCancel { id: None },
5938        });
5939
5940        assert!(
5941            outcome.events.is_empty(),
5942            "a rejected cancel moves nothing, so it owes no events"
5943        );
5944        assert_eq!(
5945            outcome.response.key, key,
5946            "the response must name its request"
5947        );
5948        match outcome.response.state {
5949            Err(OrderError::Rejected(ApiError::OrderRejected(ref reason))) => {
5950                assert!(
5951                    reason.contains("test-cid"),
5952                    "the rejection must name the order it could not find, got: {reason}"
5953                );
5954            }
5955            ref other => panic!("expected a rejected cancel, got: {other:?}"),
5956        }
5957
5958        // Nothing was cancelled, so nothing may appear in the ledger either.
5959        assert!(
5960            venue.account_snapshot().instruments.is_empty(),
5961            "a rejected cancel must not enter the ledger"
5962        );
5963    }
5964
5965    /// `contract_size` of the CFD fixture below, as a per-point multiplier a real index CFD carries.
5966    const CFD_CONTRACT_SIZE: &str = "25";
5967
5968    fn cfd_instrument_name() -> InstrumentNameExchange {
5969        InstrumentNameExchange::new("spx500_usd")
5970    }
5971
5972    /// A USD-quoted index CFD settling in **GBP** — the case that makes the settlement asset differ
5973    /// from the quote asset — with **only the quote asset funded** and no `spx500` balance at all.
5974    /// An index is not deliverable, so requiring base inventory to short one would be unfundable.
5975    fn make_cfd_venue(usd: &str, fee_model: FeeModelConfig) -> SimulatedVenue {
5976        make_cfd_venue_with_balances(
5977            vec![funded(AssetNameExchange::new("usd"), d(usd))],
5978            fee_model,
5979        )
5980    }
5981
5982    fn make_cfd_venue_with_balances(
5983        balances: Vec<AssetBalance<AssetNameExchange>>,
5984        fee_model: FeeModelConfig,
5985    ) -> SimulatedVenue {
5986        SimulatedVenue::new(
5987            &config_from_balances(balances, fee_model),
5988            instruments_of(cfd_instrument()),
5989        )
5990    }
5991
5992    fn cfd_instrument() -> Instrument<ExchangeId, AssetNameExchange> {
5993        Instrument {
5994            exchange: EXCHANGE,
5995            name_internal: InstrumentNameInternal::new("spx500_usd"),
5996            name_exchange: cfd_instrument_name(),
5997            underlying: Underlying {
5998                base: AssetNameExchange::new("spx500"),
5999                quote: AssetNameExchange::new("usd"),
6000            },
6001            quote: InstrumentQuoteAsset::UnderlyingQuote,
6002            kind: InstrumentKind::Cfd(CfdContract {
6003                contract_size: d(CFD_CONTRACT_SIZE),
6004                settlement_asset: AssetNameExchange::new("gbp"),
6005            }),
6006            spec: None,
6007            data_venue: None,
6008        }
6009    }
6010
6011    fn cfd_request(
6012        side: Side,
6013        quantity: &str,
6014        market: Option<MarketSnapshot>,
6015    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
6016        request(cfd_instrument_name(), side, quantity, market)
6017    }
6018
6019    /// The multiplier must reach the ledger, or balances move 1x while the engine's PnL moves
6020    /// `contract_size`x — the same fill accounted two different ways.
6021    #[test]
6022    fn cfd_buy_debits_the_contract_size_scaled_notional() {
6023        let mut venue = make_cfd_venue("200000", FeeModelConfig::default());
6024
6025        let outcome = venue.open_order(cfd_request(Side::Buy, "1", market_prices("5000")));
6026
6027        assert!(
6028            outcome.response.state.is_accepted(),
6029            "cfd buy should fill: {:?}",
6030            outcome.response.state
6031        );
6032        assert_eq!(outcome.events.len(), 2);
6033
6034        // 1 contract * 5000 * 25 = 125,000 of true notional, not the unmultiplied 5,000.
6035        let usd = venue
6036            .account
6037            .balance_mut(&AssetNameExchange::new("usd"))
6038            .unwrap();
6039        assert_eq!(usd.balance.free, d("200000") - d("125000"));
6040        assert_eq!(usd.balance.total, usd.balance.free);
6041    }
6042
6043    /// A CFD short is a margin position, not a stock loan: it must not require inventory in an
6044    /// index that cannot be held. This fixture funds no `spx500` balance at all, so a base debit
6045    /// would panic on the missing balance rather than merely reject.
6046    #[test]
6047    fn cfd_sell_needs_no_base_inventory_and_debits_quote() {
6048        let mut venue = make_cfd_venue("200000", FeeModelConfig::default());
6049
6050        let outcome = venue.open_order(cfd_request(Side::Sell, "1", market_prices("5000")));
6051
6052        assert!(
6053            outcome.response.state.is_accepted(),
6054            "cfd short should fill without base inventory: {:?}",
6055            outcome.response.state
6056        );
6057        let balance = filled_balance(&outcome);
6058        assert_eq!(
6059            balance.asset,
6060            AssetNameExchange::new("usd"),
6061            "a cash-settled short debits quote, not base"
6062        );
6063        assert_eq!(balance.balance.free, d("75000"));
6064    }
6065
6066    /// The scaled notional is what the balance check tests, so an account that could fund the
6067    /// unmultiplied order must still be rejected.
6068    #[test]
6069    fn cfd_buy_is_rejected_when_only_the_unscaled_notional_is_funded() {
6070        let mut venue = make_cfd_venue("10000", FeeModelConfig::default());
6071
6072        let outcome = venue.open_order(cfd_request(Side::Buy, "1", market_prices("5000")));
6073
6074        assert!(outcome.events.is_empty());
6075        match outcome.response.state {
6076            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
6077                ApiError::BalanceInsufficient(Some(ref asset), _),
6078            ))) => {
6079                assert_eq!(*asset, AssetNameExchange::new("usd"));
6080            }
6081            other => panic!("expected BalanceInsufficient, got: {other:?}"),
6082        }
6083    }
6084
6085    /// Fees stay quote-denominated on both sides of a CFD, are computed on the `contract_size`
6086    /// scaled notional, and are debited on top of it.
6087    ///
6088    /// Every amount below is hard-coded rather than read back from the event: an assertion that
6089    /// subtracts the reported fee from its own expected balance is satisfied by *any* fee,
6090    /// including the unscaled one this exists to rule out.
6091    #[test]
6092    fn cfd_fee_is_quote_denominated_on_both_sides() {
6093        for side in [Side::Buy, Side::Sell] {
6094            let mut venue = make_cfd_venue(
6095                "200000",
6096                // 0.1% of the scaled notional.
6097                FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001"))),
6098            );
6099
6100            let outcome = venue.open_order(cfd_request(side, "1", market_prices("5000")));
6101
6102            assert!(
6103                outcome.response.state.is_accepted(),
6104                "{side:?} should fill: {:?}",
6105                outcome.response.state
6106            );
6107            assert_eq!(
6108                filled_trade(&outcome).fees.asset,
6109                AssetNameExchange::new("usd"),
6110                "{side:?} fee asset"
6111            );
6112
6113            // 0.001 * 5000 * 1 * 25. The unscaled answer is 5, so this pins the multiplier.
6114            assert_eq!(
6115                filled_trade(&outcome).fees.fees,
6116                d("125"),
6117                "{side:?} fee must be 0.1% of the contract_size-scaled notional"
6118            );
6119            assert_eq!(
6120                filled_balance(&outcome).balance.free,
6121                d("200000") - d("125000") - d("125"),
6122                "{side:?} debit must be notional + fee"
6123            );
6124        }
6125    }
6126
6127    /// The documented caller obligation: the **quote** asset must be funded, even when the CFD
6128    /// settles in another currency. This mock has no conversion rate, so it cannot fall back to the
6129    /// settlement asset, and an unfunded quote balance is a mis-specified fixture rather than a
6130    /// runtime condition.
6131    #[test]
6132    #[should_panic(expected = "SimulatedVenue has Balance for all configured Instrument assets")]
6133    fn cfd_panics_when_the_quote_asset_is_unfunded() {
6134        // A realistically funded GBP account: the settlement asset is present, the quote asset is
6135        // not. The mock cannot convert between them, so this is a mis-specified fixture.
6136        let mut venue = make_cfd_venue_with_balances(
6137            vec![funded(AssetNameExchange::new("gbp"), d("200000"))],
6138            FeeModelConfig::default(),
6139        );
6140
6141        let _ = venue.open_order(cfd_request(Side::Buy, "1", market_prices("5000")));
6142    }
6143
6144    // --- A CFD is a net position: closing pays back what opening posted -----------------------
6145
6146    fn usd() -> AssetNameExchange {
6147        AssetNameExchange::new("usd")
6148    }
6149
6150    fn free_usd(venue: &SimulatedVenue) -> Decimal {
6151        venue.balances(&[usd()]).remove(0).balance.free
6152    }
6153
6154    fn total_usd(venue: &SimulatedVenue) -> Decimal {
6155        venue.balances(&[usd()]).remove(0).balance.total
6156    }
6157
6158    /// Fills a CFD market order at `price`, asserting it was accepted.
6159    fn cfd_fill(
6160        venue: &mut SimulatedVenue,
6161        side: Side,
6162        quantity: &str,
6163        price: &str,
6164    ) -> OpenOutcome {
6165        let outcome = venue.open_order(cfd_request(side, quantity, market_prices(price)));
6166        assert!(
6167            outcome.response.state.is_accepted(),
6168            "{side:?} {quantity} @ {price} should fill: {:?}",
6169            outcome.response.state
6170        );
6171        outcome
6172    }
6173
6174    /// The venue's reported CFD position as `(quantity, entry)`, or `None` while flat.
6175    fn cfd_position(venue: &SimulatedVenue) -> Option<(Decimal, Option<Decimal>)> {
6176        let snapshot = venue.account_snapshot();
6177        let Some(instrument) = snapshot
6178            .instruments
6179            .iter()
6180            .find(|instrument| instrument.instrument == cfd_instrument_name())
6181        else {
6182            panic!("a CFD instrument is always in the snapshot, flat or not");
6183        };
6184        match &instrument.position {
6185            PositionReport::Open(position) => Some((position.quantity, position.entry_price)),
6186            PositionReport::Flat => None,
6187            PositionReport::Unreported => panic!("a CFD position is reported, not Unreported"),
6188        }
6189    }
6190
6191    /// The bug this ledger fixes: an account that opens with most of its balance can close, and a
6192    /// round trip moves the balance by the realised PnL alone. 1 contract × 25 × the price move.
6193    #[test]
6194    fn a_cfd_round_trip_returns_the_margin_and_settles_the_pnl() {
6195        // (opening side, open price, close price, balance after the round trip)
6196        for (side, open, close, end) in [
6197            (Side::Buy, "5000", "5100", "152500"),  // long that wins 2,500
6198            (Side::Sell, "5000", "4900", "152500"), // short that wins 2,500
6199            (Side::Sell, "5000", "5200", "145000"), // short that loses 5,000
6200        ] {
6201            let mut venue = make_cfd_venue("150000", FeeModelConfig::default());
6202
6203            cfd_fill(&mut venue, side, "1", open);
6204            assert_eq!(
6205                free_usd(&venue),
6206                d("25000"),
6207                "{side:?}: opening posts 125,000"
6208            );
6209
6210            let closing = match side {
6211                Side::Buy => Side::Sell,
6212                Side::Sell => Side::Buy,
6213            };
6214            let outcome = cfd_fill(&mut venue, closing, "1", close);
6215
6216            assert_eq!(
6217                filled_balance(&outcome).balance.free,
6218                d(end),
6219                "{side:?} @ {close}"
6220            );
6221            assert_eq!(total_usd(&venue), d(end), "{side:?}: nothing is left held");
6222            assert_eq!(
6223                cfd_position(&venue),
6224                None,
6225                "{side:?}: the round trip is flat"
6226            );
6227        }
6228    }
6229
6230    /// One sell of 3 against a long of 1 closes the long and opens a short of 2, and the long it
6231    /// closes helps fund the short it opens.
6232    #[test]
6233    fn a_cfd_flip_closes_the_position_and_opens_the_rest_in_one_fill() {
6234        let mut venue = make_cfd_venue("300000", FeeModelConfig::default());
6235        cfd_fill(&mut venue, Side::Buy, "1", "5000");
6236        assert_eq!(free_usd(&venue), d("175000"));
6237
6238        // Closing 1 pays back 1 × 25 × 5100 = 127,500; opening 2 posts 2 × 25 × 5100 = 255,000.
6239        // 175,000 alone could not post 255,000, so the close is what funds it.
6240        cfd_fill(&mut venue, Side::Sell, "3", "5100");
6241
6242        assert_eq!(free_usd(&venue), d("175000") + d("127500") - d("255000"));
6243        assert_eq!(cfd_position(&venue), Some((d("-2"), Some(d("5100")))));
6244    }
6245
6246    #[test]
6247    fn a_partial_cfd_reduce_keeps_the_entry_and_pays_back_its_share() {
6248        let mut venue = make_cfd_venue("300000", FeeModelConfig::default());
6249        cfd_fill(&mut venue, Side::Buy, "2", "5000");
6250        assert_eq!(free_usd(&venue), d("50000"));
6251
6252        cfd_fill(&mut venue, Side::Sell, "1", "5200");
6253
6254        // 1 × 25 × 5200: the 125,000 posted for it plus 5,000 of PnL.
6255        assert_eq!(free_usd(&venue), d("180000"));
6256        assert_eq!(
6257            cfd_position(&venue),
6258            Some((d("2") - d("1"), Some(d("5000"))))
6259        );
6260    }
6261
6262    #[test]
6263    fn increasing_a_cfd_position_averages_its_entry() {
6264        let mut venue = make_cfd_venue("300000", FeeModelConfig::default());
6265        cfd_fill(&mut venue, Side::Buy, "1", "5000");
6266        cfd_fill(&mut venue, Side::Buy, "1", "5200");
6267
6268        assert_eq!(free_usd(&venue), d("300000") - d("125000") - d("130000"));
6269        assert_eq!(cfd_position(&venue), Some((d("2"), Some(d("5100")))));
6270    }
6271
6272    /// Fees stay on the whole quantity whichever way it splits, so a round trip costs the PnL and
6273    /// both fees and nothing else.
6274    #[test]
6275    fn a_cfd_close_is_charged_its_fee_on_top_of_the_proceeds() {
6276        let mut venue = make_cfd_venue(
6277            "200000",
6278            FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001"))),
6279        );
6280        cfd_fill(&mut venue, Side::Buy, "1", "5000"); // fee 125
6281        let outcome = cfd_fill(&mut venue, Side::Sell, "1", "5100"); // fee 127.5
6282
6283        assert_eq!(filled_trade(&outcome).fees.fees, d("127.5"));
6284        assert_eq!(
6285            free_usd(&venue),
6286            d("200000") + d("2500") - d("125") - d("127.5")
6287        );
6288    }
6289
6290    /// A short that more than doubles has lost more than its margin. Nothing liquidated it, so the
6291    /// shortfall is paid rather than credited as a negative amount.
6292    #[test]
6293    fn a_cfd_short_that_loses_more_than_its_margin_pays_the_shortfall() {
6294        let mut venue = make_cfd_venue("200000", FeeModelConfig::default());
6295        cfd_fill(&mut venue, Side::Sell, "1", "1000"); // posts 25,000
6296        cfd_fill(&mut venue, Side::Buy, "1", "2500"); // loses 37,500
6297
6298        assert_eq!(free_usd(&venue), d("200000") - d("37500"));
6299        assert_eq!(cfd_position(&venue), None);
6300    }
6301
6302    #[test]
6303    fn a_spot_instrument_still_reports_no_position() {
6304        let mut venue = make_venue("10", "1000000");
6305        let _ = venue.open_order(request(
6306            instrument_name(),
6307            Side::Buy,
6308            "1",
6309            market_prices("50000"),
6310        ));
6311
6312        let snapshot = venue.account_snapshot();
6313        assert!(
6314            snapshot
6315                .instruments
6316                .iter()
6317                .all(|instrument| instrument.position == PositionReport::Unreported),
6318            "a spot holding is a balance, not a position"
6319        );
6320    }
6321
6322    #[test]
6323    fn a_cfd_position_in_the_initial_state_is_where_the_venue_starts() {
6324        let config = MockExecutionConfig::new(
6325            EXCHANGE,
6326            UnindexedAccountSnapshot {
6327                exchange: EXCHANGE,
6328                balances: vec![funded(usd(), d("10000"))],
6329                instruments: vec![InstrumentAccountSnapshot {
6330                    instrument: cfd_instrument_name(),
6331                    orders: vec![],
6332                    orders_complete: true,
6333                    position: PositionReport::from_position(Position::new(
6334                        d("-1"),
6335                        Some(d("5000")),
6336                        None,
6337                        None,
6338                        None,
6339                        None,
6340                        None,
6341                        time(0),
6342                    )),
6343                    isolated: None,
6344                }],
6345            },
6346            0,
6347            FeeModelConfig::default(),
6348            SimFillConfig::default(),
6349            SpotFeeAsset::default(),
6350            crate::slippage::SlippageConfig::default(),
6351            crate::fill::MarketOrderTiming::default(),
6352            crate::fill::StaleMarketPolicy::default(),
6353            None,
6354            None,
6355        );
6356        let mut venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
6357        assert_eq!(cfd_position(&venue), Some((d("-1"), Some(d("5000")))));
6358
6359        // Its margin counts as posted, so closing it pays that back: 25 × (2 × 5000 − 4800).
6360        cfd_fill(&mut venue, Side::Buy, "1", "4800");
6361        assert_eq!(free_usd(&venue), d("10000") + d("130000"));
6362    }
6363
6364    #[test]
6365    #[should_panic(expected = "no entry price")]
6366    fn a_cfd_position_in_the_initial_state_without_an_entry_price_panics() {
6367        let config = MockExecutionConfig::new(
6368            EXCHANGE,
6369            UnindexedAccountSnapshot {
6370                exchange: EXCHANGE,
6371                balances: vec![funded(usd(), d("10000"))],
6372                instruments: vec![InstrumentAccountSnapshot {
6373                    instrument: cfd_instrument_name(),
6374                    orders: vec![],
6375                    orders_complete: true,
6376                    position: PositionReport::from_position(Position::new(
6377                        d("-1"),
6378                        None,
6379                        None,
6380                        None,
6381                        None,
6382                        None,
6383                        None,
6384                        time(0),
6385                    )),
6386                    isolated: None,
6387                }],
6388            },
6389            0,
6390            FeeModelConfig::default(),
6391            SimFillConfig::default(),
6392            SpotFeeAsset::default(),
6393            crate::slippage::SlippageConfig::default(),
6394            crate::fill::MarketOrderTiming::default(),
6395            crate::fill::StaleMarketPolicy::default(),
6396            None,
6397            None,
6398        );
6399        let _ = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
6400    }
6401
6402    // --- A resting CFD order holds only what it opens ------------------------------------------
6403
6404    fn make_cfd_market_venue(usd: &str) -> SimulatedVenue {
6405        let mut venue = SimulatedVenue::new_market_driven(
6406            &config_from_balances(vec![funded(self::usd(), d(usd))], FeeModelConfig::default()),
6407            instruments_of(cfd_instrument()),
6408        );
6409        advance(&mut venue, time(1));
6410        venue
6411    }
6412
6413    fn cfd_limit(
6414        cid: &str,
6415        side: Side,
6416        quantity: &str,
6417        price: &str,
6418    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
6419        let mut request = limit_request(cid, side, quantity, price, gtc());
6420        request.key.instrument = cfd_instrument_name();
6421        request
6422    }
6423
6424    /// Feeds the CFD a market with a book and a trade price, returning whatever it filled.
6425    fn cfd_market(
6426        venue: &mut SimulatedVenue,
6427        bid: &str,
6428        ask: &str,
6429        last: &str,
6430    ) -> Vec<UnindexedAccountEvent> {
6431        let time_exchange = venue.time_exchange();
6432        venue.apply_market(
6433            &cfd_instrument_name(),
6434            MarketObservation::new(MarketSnapshot {
6435                best_bid: Some(d(bid)),
6436                best_ask: Some(d(ask)),
6437                last_price: Some(d(last)),
6438            }),
6439            time_exchange,
6440        )
6441    }
6442
6443    fn cfd_market_fill(venue: &mut SimulatedVenue, side: Side, quantity: &str) {
6444        let outcome = venue.open_order(cfd_request(side, quantity, None));
6445        assert!(
6446            outcome.response.state.is_accepted(),
6447            "{side:?} {quantity} should fill: {:?}",
6448            outcome.response.state
6449        );
6450    }
6451
6452    /// An account fully deployed in a long can still rest the order that closes it, which holds
6453    /// nothing, and the close pays the margin back when it fills.
6454    #[test]
6455    fn a_resting_order_that_only_reduces_a_cfd_position_holds_nothing() {
6456        let mut venue = make_cfd_market_venue("125000");
6457        assert!(cfd_market(&mut venue, "4990", "5000", "5000").is_empty());
6458        cfd_market_fill(&mut venue, Side::Buy, "1");
6459        assert_eq!(
6460            free_usd(&venue),
6461            Decimal::ZERO,
6462            "the long took the whole balance"
6463        );
6464
6465        let outcome = venue.open_order(cfd_limit("close", Side::Sell, "1", "5200"));
6466        assert!(rested_open(&outcome).filled_quantity.is_zero());
6467        assert_eq!(
6468            total_usd(&venue) - free_usd(&venue),
6469            Decimal::ZERO,
6470            "nothing is held"
6471        );
6472
6473        let events = cfd_market(&mut venue, "5200", "5210", "5200");
6474        assert_eq!(balance_of(&events[0]).balance.free, d("130000"));
6475        assert_eq!(filled_price(order_of(&events[2])), d("5200"));
6476        assert_eq!(cfd_position(&venue), None);
6477    }
6478
6479    #[test]
6480    fn a_resting_order_that_opens_a_cfd_position_holds_its_margin_until_cancelled() {
6481        let mut venue = make_cfd_market_venue("200000");
6482        assert!(cfd_market(&mut venue, "4990", "5000", "5000").is_empty());
6483
6484        let _ = venue.open_order(cfd_limit("open", Side::Buy, "1", "4800"));
6485        assert_eq!(free_usd(&venue), d("200000") - d("120000"));
6486        assert_eq!(total_usd(&venue), d("200000"));
6487
6488        let cancelled = venue.cancel_order(OrderRequestCancel {
6489            key: cfd_limit("open", Side::Buy, "1", "4800").key,
6490            state: RequestCancel { id: None },
6491        });
6492        assert!(cancelled.response.state.is_ok());
6493        assert_eq!(free_usd(&venue), d("200000"));
6494    }
6495
6496    /// The position a resting order was costed against can be gone by the time it fills: here a
6497    /// market sell closes the long the resting sell was going to close, so the resting sell now
6498    /// opens a short. It is costed again at the fill and posts that short's margin.
6499    #[test]
6500    fn a_resting_cfd_order_is_costed_against_the_position_when_it_fills() {
6501        let mut venue = make_cfd_market_venue("300000");
6502        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
6503        cfd_market_fill(&mut venue, Side::Buy, "1"); // posts 125,000
6504
6505        let _ = venue.open_order(cfd_limit("close", Side::Sell, "1", "5200"));
6506        assert_eq!(
6507            free_usd(&venue),
6508            d("175000"),
6509            "a reducing order holds nothing"
6510        );
6511
6512        cfd_market_fill(&mut venue, Side::Sell, "1"); // closes the long at 5,000
6513        assert_eq!(free_usd(&venue), d("300000"));
6514        assert_eq!(cfd_position(&venue), None);
6515
6516        let events = cfd_market(&mut venue, "5200", "5210", "5200");
6517        assert_eq!(
6518            balance_of(&events[0]).balance.free,
6519            d("300000") - d("130000"),
6520            "the resting sell now opens a short of 1 at 5,200"
6521        );
6522        assert_eq!(cfd_position(&venue), Some((d("-1"), Some(d("5200")))));
6523    }
6524
6525    /// The same, for an account that cannot afford the short the order now opens. It is cancelled
6526    /// rather than filled with balance the account does not have.
6527    #[test]
6528    fn a_resting_cfd_order_the_account_can_no_longer_afford_is_cancelled() {
6529        let mut venue = make_cfd_market_venue("126000");
6530        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
6531        cfd_market_fill(&mut venue, Side::Buy, "1");
6532        let _ = venue.open_order(cfd_limit("close", Side::Sell, "1", "5200"));
6533        cfd_market_fill(&mut venue, Side::Sell, "1");
6534        assert_eq!(free_usd(&venue), d("126000"));
6535
6536        let events = cfd_market(&mut venue, "5200", "5210", "5200");
6537
6538        // Its released hold (nothing, since it only reduced when it rested) restates first, and
6539        // the cancelled order comes last.
6540        let Some((last, released)) = events.split_last() else {
6541            panic!("an unaffordable fill reports its cancelled order");
6542        };
6543        assert!(
6544            matches!(
6545                order_of(last).state,
6546                OrderState::Inactive(InactiveOrderState::Cancelled(_))
6547            ),
6548            "{:?}",
6549            order_of(last).state
6550        );
6551        assert!(
6552            released
6553                .iter()
6554                .all(|event| balance_of(event).balance.free == d("126000")),
6555            "no balance moved"
6556        );
6557        assert_eq!(free_usd(&venue), d("126000"), "nothing moved");
6558        assert_eq!(cfd_position(&venue), None);
6559        assert!(venue.account.is_cancelled(&ClientOrderId::new("close")));
6560    }
6561
6562    /// The same, for an order that holds something: its hold is given back before the order is
6563    /// cancelled, and nothing else moves. The resting sell of 3 closes the long of 1 and opens a
6564    /// short of 2, so it holds the 2's margin net of the 1's proceeds. Once a market sell has
6565    /// closed the long, its fill would open a short of 3, which the account cannot cover.
6566    #[test]
6567    fn a_resting_cfd_order_cancelled_as_unaffordable_releases_its_hold() {
6568        let mut venue = make_cfd_market_venue("260000");
6569        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
6570        cfd_market_fill(&mut venue, Side::Buy, "1"); // posts 125,000
6571
6572        let _ = venue.open_order(cfd_limit("flip", Side::Sell, "3", "5200"));
6573        assert_eq!(
6574            free_usd(&venue),
6575            d("260000") - d("125000") - (d("260000") - d("130000")),
6576            "holds the short's 260,000 net of the long's 130,000"
6577        );
6578
6579        cfd_market_fill(&mut venue, Side::Sell, "1"); // closes the long at 5,000
6580        assert_eq!(free_usd(&venue), d("130000"));
6581        assert_eq!(total_usd(&venue), d("260000"));
6582
6583        let events = cfd_market(&mut venue, "5200", "5210", "5200");
6584
6585        let Some((last, released)) = events.split_last() else {
6586            panic!("an unaffordable fill reports its cancelled order");
6587        };
6588        assert!(
6589            matches!(
6590                order_of(last).state,
6591                OrderState::Inactive(InactiveOrderState::Cancelled(_))
6592            ),
6593            "{:?}",
6594            order_of(last).state
6595        );
6596        let [released] = released else {
6597            panic!("exactly the released hold restates: {released:?}");
6598        };
6599        assert_eq!(
6600            balance_of(released).balance.free,
6601            d("260000"),
6602            "the hold is given back"
6603        );
6604        assert_eq!(free_usd(&venue), d("260000"));
6605        assert_eq!(total_usd(&venue), d("260000"), "nothing settled");
6606        assert_eq!(cfd_position(&venue), None);
6607        assert!(venue.account.is_cancelled(&ClientOrderId::new("flip")));
6608    }
6609
6610    #[test]
6611    fn sell_order_decrements_base_and_credits_quote_proceeds() {
6612        let mut venue = make_venue("1.0", "10000");
6613        let initial_usdt = d("10000");
6614
6615        let outcome = venue.open_order(sell_request("0.5", market_prices("50000")));
6616
6617        assert!(
6618            outcome.response.state.is_accepted(),
6619            "sell should succeed: {:?}",
6620            outcome.response.state
6621        );
6622        assert_eq!(outcome.events.len(), 3, "a successful sell must notify");
6623        assert_eq!(
6624            filled_balance(&outcome).asset,
6625            base(),
6626            "a sell pays with base"
6627        );
6628        assert_eq!(
6629            filled_received(&outcome).asset,
6630            quote(),
6631            "and receives quote"
6632        );
6633
6634        // Base (BTC) must be decremented by the quantity sold.
6635        let btc = venue.account.balance_mut(&base()).unwrap();
6636        assert_eq!(
6637            btc.balance.free,
6638            d("0.5"),
6639            "base balance should decrease by quantity sold"
6640        );
6641
6642        // Quote (USDT) is credited the proceeds: 0.5 * 50000, with no fee in this test.
6643        let usdt = venue.account.balance_mut(&quote()).unwrap();
6644        assert_eq!(
6645            usdt.balance.free,
6646            initial_usdt + d("25000"),
6647            "quote balance should rise by the proceeds"
6648        );
6649    }
6650
6651    /// A derivative must be rejected, not filled down the physically-settled spot path.
6652    ///
6653    /// The venue is constructed directly with the derivative on purpose: that is exactly the route
6654    /// a consumer takes when it builds a venue itself rather than through the `rustrade` builder,
6655    /// and it is the route that had no gate on it.
6656    #[test]
6657    fn unsupported_instrument_kinds_are_rejected_rather_than_filled_as_spot() {
6658        use rustrade_instrument::instrument::kind::{
6659            future::FutureContract,
6660            option::{OptionContract, OptionExercise, OptionKind},
6661            perpetual::PerpetualContract,
6662        };
6663
6664        let settlement = AssetNameExchange::new("USDT");
6665        let expiry = Utc::now();
6666
6667        let unsupported = [
6668            InstrumentKind::Perpetual(PerpetualContract {
6669                contract_size: d("10"),
6670                settlement_asset: settlement.clone(),
6671            }),
6672            InstrumentKind::Future(FutureContract {
6673                contract_size: d("10"),
6674                settlement_asset: settlement.clone(),
6675                expiry,
6676            }),
6677            InstrumentKind::Option(OptionContract {
6678                contract_size: d("10"),
6679                settlement_asset: settlement.clone(),
6680                kind: OptionKind::Call,
6681                exercise: OptionExercise::European,
6682                expiry,
6683                strike: d("50000"),
6684            }),
6685        ];
6686
6687        for kind in unsupported {
6688            // Amply funded: the rejection must come from the kind, not from a balance shortfall.
6689            let mut instruments = spot_instruments();
6690            instruments.get_mut(&instrument_name()).unwrap().kind = kind.clone();
6691            let config = spot_config("10", "10000000", FeeModelConfig::default());
6692            let mut venue = SimulatedVenue::new(&config, instruments);
6693
6694            let outcome = venue.open_order(buy_request("1.0", market_prices("50000")));
6695
6696            assert!(
6697                outcome.events.is_empty(),
6698                "{kind:?} must produce no account events"
6699            );
6700            assert!(
6701                matches!(
6702                    outcome.response.state,
6703                    OrderState::Inactive(InactiveOrderState::OpenFailed(
6704                        UnindexedOrderError::Connectivity(_) | UnindexedOrderError::Rejected(_)
6705                    ))
6706                ),
6707                "{kind:?} must be rejected, got {:?}",
6708                outcome.response.state
6709            );
6710
6711            // And the ledger must be untouched -- a rejected order that still moved cash would be
6712            // worse than one that filled.
6713            let usdt = venue.account.balance_mut(&quote()).unwrap();
6714            assert_eq!(
6715                usdt.balance.free,
6716                d("10000000"),
6717                "{kind:?} must not move the quote balance"
6718            );
6719        }
6720    }
6721
6722    #[test]
6723    fn sell_order_insufficient_balance_names_base_asset() {
6724        // Regression guard for a sell-side balance bug: previously `balance_mut(&underlying.quote)`
6725        // was called for sells, so BalanceInsufficient would name the quote asset (USDT) instead of
6726        // the base (BTC).
6727        let mut venue = make_venue("0.1", "10000");
6728
6729        // Selling 1 BTC but only 0.1 available.
6730        let outcome = venue.open_order(sell_request("1.0", market_prices("50000")));
6731
6732        assert!(
6733            outcome.events.is_empty(),
6734            "failed order must produce no account events"
6735        );
6736        match outcome.response.state {
6737            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
6738                ApiError::BalanceInsufficient(Some(ref asset), _),
6739            ))) => {
6740                assert_eq!(
6741                    *asset,
6742                    base(),
6743                    "BalanceInsufficient must name the base asset (BTC), not the quote (USDT)"
6744                );
6745            }
6746            other => panic!("expected BalanceInsufficient, got: {other:?}"),
6747        }
6748    }
6749
6750    #[test]
6751    fn bid_ask_fill_model_fills_at_ask_price_and_deducts_correct_balance() {
6752        let mut venue = make_venue("0", "10000"); // 0 BTC, 10 000 USDT
6753        venue.fill_model = Arc::new(SimFillConfig::BidAsk(BidAskFillModel));
6754
6755        let market = Some(MarketSnapshot {
6756            best_bid: Some(d("99.5")),
6757            best_ask: Some(d("100.5")),
6758            last_price: Some(d("100.0")),
6759        });
6760
6761        // Market buy of 1 BTC; last_price 100 is only a fallback for when the fill model returns
6762        // None — BidAsk returns best_ask, so it is not used.
6763        let outcome = venue.open_order(buy_request("1", market));
6764
6765        assert!(
6766            outcome.response.state.is_accepted(),
6767            "buy should succeed: {:?}",
6768            outcome.response.state
6769        );
6770
6771        // BidAskFillModel: market buy fills at best_ask = 100.5, not last_price 100.0.
6772        assert_eq!(
6773            filled_trade(&outcome).price,
6774            d("100.5"),
6775            "fill price must be best_ask"
6776        );
6777
6778        // Balance deduction: 1 * 100.5 = 100.5 USDT; fee_model = Zero.
6779        let usdt = venue.account.balance_mut(&quote()).unwrap();
6780        assert_eq!(
6781            usdt.balance.free,
6782            d("9899.5"),
6783            "quote balance must decrease by fill_price * qty"
6784        );
6785    }
6786
6787    // --- Slippage ---
6788
6789    /// A funded spot config moving every taker fill by `slippage`.
6790    fn slipped_config(slippage: SlippageConfig) -> MockExecutionConfig {
6791        MockExecutionConfig {
6792            slippage,
6793            ..spot_config("10", "1000000", FeeModelConfig::default())
6794        }
6795    }
6796
6797    fn last(price: &str) -> Option<MarketSnapshot> {
6798        Some(MarketSnapshot {
6799            last_price: Some(d(price)),
6800            ..MarketSnapshot::default()
6801        })
6802    }
6803
6804    fn uniform(seed: u64) -> SlippageConfig {
6805        SlippageConfig::uniform(d("0.01"), d("0.025"), seed).unwrap()
6806    }
6807
6808    #[test]
6809    fn a_fixed_slippage_moves_a_market_fill_against_the_order_and_settles_that_price() {
6810        let config = slipped_config(SlippageConfig::fixed(d("0.01")).unwrap());
6811        let mut venue = SimulatedVenue::new(&config, spot_instruments());
6812
6813        let bought = venue.open_order(buy_request("2", last("100")));
6814        assert_eq!(filled_trade(&bought).price, d("101"), "a buy pays 1% above");
6815        assert_eq!(
6816            venue.account.balance_mut(&quote()).unwrap().balance.free,
6817            d("999798"),
6818            "the quote debited is the slipped price times the quantity"
6819        );
6820
6821        let sold = venue.open_order(sell_request("2", last("100")));
6822        assert_eq!(
6823            filled_trade(&sold).price,
6824            d("99"),
6825            "a sell receives 1% below"
6826        );
6827        assert_eq!(
6828            venue.account.balance_mut(&quote()).unwrap().balance.free,
6829            d("999996"),
6830            "the round trip cost the 1% on each side"
6831        );
6832    }
6833
6834    /// The clamp comes after the slippage, so no rate fills a limit order past its own limit.
6835    #[test]
6836    fn a_slipped_marketable_limit_never_fills_past_its_limit() {
6837        let fill = |rate: &str, side: Side, limit: &str| {
6838            let config = slipped_config(SlippageConfig::fixed(d(rate)).unwrap());
6839            let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
6840            advance(&mut venue, time(1));
6841            let fed = venue.apply_market(
6842                &instrument_name(),
6843                MarketObservation::new(book("49000", "49100")),
6844                time(1),
6845            );
6846            assert!(fed.is_empty(), "an empty book fills nothing");
6847            let outcome = venue.open_order(limit_request("cid", side, "1", limit, gtc()));
6848            filled_trade(&outcome).price
6849        };
6850
6851        assert_eq!(
6852            fill("0.01", Side::Buy, "49200"),
6853            d("49200"),
6854            "49591 slipped, clamped to the limit"
6855        );
6856        assert_eq!(
6857            fill("0.01", Side::Sell, "48800"),
6858            d("48800"),
6859            "48510 slipped, clamped to the limit"
6860        );
6861        assert_eq!(
6862            fill("0.001", Side::Buy, "49200"),
6863            d("49149.1"),
6864            "inside the limit, the slipped price stands"
6865        );
6866    }
6867
6868    /// A resting order is the maker, so it fills at its own limit and leaves the stream alone.
6869    #[test]
6870    fn a_resting_fill_is_not_slipped_and_takes_no_draw() {
6871        let config = MockExecutionConfig {
6872            slippage: uniform(5),
6873            ..spot_config("10", "1000000", maker_taker_fee())
6874        };
6875        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
6876        advance(&mut venue, time(1));
6877        let fed = venue.apply_market(
6878            &instrument_name(),
6879            MarketObservation::new(book("49000", "49100")),
6880            time(1),
6881        );
6882        assert!(fed.is_empty(), "an empty book fills nothing");
6883        let rested = venue.open_order(limit_request("resting", Side::Buy, "1", "48000", gtc()));
6884        rested_open(&rested);
6885        assert_eq!(
6886            venue.slippage,
6887            Slippage::new(config.slippage),
6888            "resting drew nothing"
6889        );
6890
6891        advance(&mut venue, time(2));
6892        let events = venue.apply_market(
6893            &instrument_name(),
6894            MarketObservation::new(book("47800", "47900")),
6895            time(2),
6896        );
6897        assert_eq!(trade_of(&events[2]).price, d("48000"));
6898        assert_eq!(
6899            venue.slippage,
6900            Slippage::new(config.slippage),
6901            "the maker's fill drew nothing"
6902        );
6903    }
6904
6905    /// A marketable order the book has no size for trades nothing, whether it then rests or is
6906    /// cancelled, so it takes no draw.
6907    #[test]
6908    fn an_order_the_book_cannot_fill_takes_no_draw() {
6909        let config = slipped_config(uniform(3));
6910        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
6911        advance(&mut venue, time(1));
6912        let fed = venue.apply_market(
6913            &instrument_name(),
6914            MarketObservation::new(book("47800", "47900"))
6915                .with_depth(MarketDepth::new(Some(d("1")), Some(Decimal::ZERO))),
6916            time(1),
6917        );
6918        assert!(fed.is_empty(), "an empty book fills nothing");
6919
6920        let rested = venue.open_order(limit_request("rests", Side::Buy, "1", "48000", gtc()));
6921        assert_eq!(rested_open(&rested).filled_quantity, Decimal::ZERO);
6922        let cancelled = venue.open_order(limit_request(
6923            "cancels",
6924            Side::Buy,
6925            "1",
6926            "48000",
6927            TimeInForce::ImmediateOrCancel,
6928        ));
6929        assert!(
6930            matches!(
6931                cancelled.response.state,
6932                OrderState::Inactive(InactiveOrderState::Cancelled(_))
6933            ),
6934            "{:?}",
6935            cancelled.response.state
6936        );
6937
6938        assert_eq!(venue.slippage, Slippage::new(config.slippage));
6939    }
6940
6941    /// A funded spot venue on a book offering `depth`, moving every taker fill by `slippage`.
6942    fn slipped_venue_offering(
6943        usdt: &str,
6944        slippage: SlippageConfig,
6945        depth: MarketDepth,
6946    ) -> SimulatedVenue {
6947        let config = MockExecutionConfig {
6948            slippage,
6949            ..spot_config("10", usdt, FeeModelConfig::default())
6950        };
6951        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
6952        advance(&mut venue, time(1));
6953        let fed = venue.apply_market(
6954            &instrument_name(),
6955            MarketObservation::new(book("47800", "47900")).with_depth(depth),
6956            time(1),
6957        );
6958        assert!(fed.is_empty(), "an empty book fills nothing");
6959        venue
6960    }
6961
6962    /// The book fills 1 of 3: that fill is slipped, with one draw, and the remainder rests at the
6963    /// order's own limit.
6964    #[test]
6965    fn a_part_filled_order_is_slipped_once_and_rests_the_rest_at_its_limit() {
6966        let mut venue = slipped_venue_offering("1000000", uniform(8), sized("1"));
6967        let mut expected = Slippage::new(uniform(8));
6968        let price = expected.apply(Side::Buy, d("47900"));
6969
6970        let outcome = venue.open_order(limit_request("part", Side::Buy, "3", "50000", gtc()));
6971        let trade = filled_trade(&outcome);
6972        assert_eq!((trade.quantity, trade.price), (d("1"), price));
6973        assert_eq!(rested_open(&outcome).filled_quantity, d("1"));
6974        assert_eq!(venue.slippage, expected, "one fill, one draw");
6975        assert_eq!(
6976            venue
6977                .account
6978                .orders()
6979                .resting(&instrument_name(), Side::Buy)
6980                .map(|order| order.price)
6981                .collect::<Vec<_>>(),
6982            vec![Some(d("50000"))],
6983            "the remainder rests at its limit, unslipped"
6984        );
6985    }
6986
6987    /// A fill-or-kill the book can only part fill trades nothing, so it takes no draw.
6988    #[test]
6989    fn a_killed_order_takes_no_draw() {
6990        let mut venue = slipped_venue_offering("1000000", uniform(8), sized("1"));
6991        let outcome = venue.open_order(limit_request(
6992            "kill",
6993            Side::Buy,
6994            "3",
6995            "50000",
6996            TimeInForce::FillOrKill,
6997        ));
6998        assert!(
6999            matches!(
7000                outcome.response.state,
7001                OrderState::Inactive(InactiveOrderState::Cancelled(_))
7002            ),
7003            "{:?}",
7004            outcome.response.state
7005        );
7006        assert_eq!(venue.slippage, Slippage::new(uniform(8)));
7007    }
7008
7009    /// The fill is affordable, its remainder's hold is not: refused whole, and the draw handed back.
7010    #[test]
7011    fn a_part_filled_order_refused_for_its_hold_hands_back_its_draw() {
7012        let mut venue = slipped_venue_offering("60000", uniform(8), sized("1"));
7013        let outcome = venue.open_order(limit_request("split", Side::Buy, "3", "50000", gtc()));
7014        assert!(
7015            matches!(
7016                outcome.response.state,
7017                OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
7018                    ApiError::BalanceInsufficient(..)
7019                )))
7020            ),
7021            "{:?}",
7022            outcome.response.state
7023        );
7024        assert_eq!(venue.slippage, Slippage::new(uniform(8)));
7025    }
7026
7027    /// An order the ledger refuses never traded, so the next fill gets the rate it would have got
7028    /// had the refused order never been sent.
7029    #[test]
7030    fn a_refused_order_hands_back_its_draw() {
7031        let config = slipped_config(uniform(11));
7032        let mut venue = SimulatedVenue::new(&config, spot_instruments());
7033
7034        let refused = venue.open_order(buy_request("1000000", last("100")));
7035        assert!(
7036            matches!(
7037                refused.response.state,
7038                OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
7039                    ApiError::BalanceInsufficient(..)
7040                )))
7041            ),
7042            "{:?}",
7043            refused.response.state
7044        );
7045        assert_eq!(venue.slippage, Slippage::new(config.slippage));
7046
7047        let mut fresh = SimulatedVenue::new(&config, spot_instruments());
7048        assert_eq!(
7049            filled_trade(&venue.open_order(buy_request("1", last("100")))).price,
7050            filled_trade(&fresh.open_order(buy_request("1", last("100")))).price,
7051        );
7052    }
7053
7054    /// The prices of a sequence of fills, alternating buys and sells, on a venue built from
7055    /// `config`.
7056    fn slipped_prices(config: &MockExecutionConfig) -> Vec<Decimal> {
7057        let mut venue = SimulatedVenue::new(config, spot_instruments());
7058        (0..20)
7059            .map(|i| {
7060                let request = if i % 2 == 0 {
7061                    buy_request("0.1", last("100"))
7062                } else {
7063                    sell_request("0.1", last("100"))
7064                };
7065                filled_trade(&venue.open_order(request)).price
7066            })
7067            .collect()
7068    }
7069
7070    #[test]
7071    fn a_seeded_venue_reproduces_its_prices_within_the_range() {
7072        let prices = slipped_prices(&slipped_config(uniform(42)));
7073        assert_eq!(prices, slipped_prices(&slipped_config(uniform(42))));
7074        assert_ne!(prices, slipped_prices(&slipped_config(uniform(43))));
7075
7076        for (i, price) in prices.iter().enumerate() {
7077            // Buys pay 1% to 2.5% above 100, sells receive 1% to 2.5% below.
7078            let (low, high) = if i % 2 == 0 {
7079                (d("101"), d("102.5"))
7080            } else {
7081                (d("97.5"), d("99"))
7082            };
7083            assert!(
7084                low <= *price && *price <= high,
7085                "fill {i} at {price} outside [{low}, {high}]"
7086            );
7087        }
7088    }
7089
7090    /// A zero rate never draws and never multiplies, so it is the unslipped price exactly, scale
7091    /// included.
7092    #[test]
7093    fn a_zero_rate_is_identical_to_no_slippage() {
7094        let none = slipped_prices(&slipped_config(SlippageConfig::none()));
7095        let zero = slipped_prices(&slipped_config(
7096            SlippageConfig::uniform(Decimal::ZERO, Decimal::ZERO, 42).unwrap(),
7097        ));
7098        assert_eq!(
7099            none.iter().map(Decimal::to_string).collect::<Vec<_>>(),
7100            zero.iter().map(Decimal::to_string).collect::<Vec<_>>()
7101        );
7102    }
7103
7104    /// Prices a fill one unit of price above the last trade for each unit of quantity.
7105    #[derive(Debug)]
7106    struct SizeImpact;
7107
7108    impl FillModel for SizeImpact {
7109        fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
7110            fill.market.last_price.map(|last| last + fill.quantity)
7111        }
7112    }
7113
7114    /// A custom model prices from the order's quantity, and the configured slippage still
7115    /// applies to what it returns.
7116    #[test]
7117    fn a_custom_fill_model_sees_the_quantity_and_is_still_slipped() {
7118        let config = slipped_config(SlippageConfig::fixed(d("0.01")).unwrap());
7119        let mut venue =
7120            SimulatedVenue::new(&config, spot_instruments()).with_fill_model(Arc::new(SizeImpact));
7121
7122        let outcome = venue.open_order(buy_request("3", last("100")));
7123        assert_eq!(
7124            filled_trade(&outcome).price,
7125            d("104.03"),
7126            "(100 + 3) x 1.01"
7127        );
7128    }
7129
7130    #[test]
7131    fn percentage_fee_model_deducts_correct_fee_on_buy() {
7132        // 0.1% fee rate
7133        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7134        let mut venue = make_venue_with_fee("0", "10000", fee_model);
7135
7136        // Buy 10 BTC at price 100 USDT each
7137        // Notional = 10 * 100 = 1000 USDT
7138        // Fee = 1000 * 0.001 = 1 USDT
7139        // Total deducted = 1000 + 1 = 1001 USDT
7140        let outcome = venue.open_order(buy_request("10", market_prices("100")));
7141
7142        assert!(
7143            outcome.response.state.is_accepted(),
7144            "buy should succeed: {:?}",
7145            outcome.response.state
7146        );
7147
7148        // Trade must report fee in quote denomination
7149        assert_eq!(
7150            filled_trade(&outcome).fees.fees,
7151            d("1"),
7152            "trade fee must be 1 USDT"
7153        );
7154
7155        // Quote balance: 10000 - 1001 = 8999
7156        let usdt = venue.account.balance_mut(&quote()).unwrap();
7157        assert_eq!(
7158            usdt.balance.free,
7159            d("8999"),
7160            "quote balance must decrease by notional + fee"
7161        );
7162    }
7163
7164    #[test]
7165    fn percentage_fee_model_deducts_correct_fee_on_sell() {
7166        // 0.1% fee rate
7167        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7168        let mut venue = make_venue_with_fee("10", "0", fee_model);
7169
7170        // Sell 1 BTC at price 100 USDT
7171        // Notional = 1 * 100 = 100 USDT
7172        // Fee (quote) = 100 * 0.001 = 0.1 USDT, out of the proceeds
7173        // Base deducted = 1 BTC; quote credited = 100 - 0.1 = 99.9 USDT
7174        let outcome = venue.open_order(sell_request("1", market_prices("100")));
7175
7176        assert!(
7177            outcome.response.state.is_accepted(),
7178            "sell should succeed: {:?}",
7179            outcome.response.state
7180        );
7181
7182        // Trade must report fee in quote denomination
7183        assert_eq!(
7184            filled_trade(&outcome).fees.fees,
7185            d("0.1"),
7186            "trade fee must be 0.1 USDT"
7187        );
7188
7189        // Base balance: 10 - 1 = 9, so the base moved equals the quantity the engine counts
7190        let btc = venue.account.balance_mut(&base()).unwrap();
7191        assert_eq!(
7192            btc.balance.free,
7193            d("9"),
7194            "base balance must decrease by the quantity alone"
7195        );
7196
7197        // Quote balance: 0 + 100 - 0.1 = 99.9, so the fee charged is the fee reported
7198        let usdt = venue.account.balance_mut(&quote()).unwrap();
7199        assert_eq!(
7200            usdt.balance.free,
7201            d("99.9"),
7202            "quote balance must rise by the proceeds less the fee"
7203        );
7204    }
7205
7206    /// A spot fill exchanges the two assets: a round trip gives back the base it bought, and the
7207    /// quote moves by the price difference less both fees, which is what the engine books as
7208    /// realised PnL. Before a spot fill credited what it received, the base never arrived and the
7209    /// sell's proceeds never left.
7210    #[test]
7211    fn a_spot_round_trip_returns_the_base_and_moves_quote_by_pnl_less_fees() {
7212        // 0.1% fee rate
7213        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7214        let mut venue = make_venue_with_fee("0", "1000", fee_model);
7215
7216        let buy = venue.open_order(buy_request("1", market_prices("100")));
7217        // 1000 - (100 + 0.1 fee)
7218        assert_eq!(
7219            filled_balance(&buy).balance,
7220            Balance::new(d("899.9"), d("899.9"))
7221        );
7222        assert_eq!(filled_received(&buy).balance, Balance::new(d("1"), d("1")));
7223
7224        let sell = venue.open_order(sell_request("1", market_prices("110")));
7225        assert_eq!(filled_balance(&sell).balance, Balance::new(d("0"), d("0")));
7226        // 899.9 + (110 - 0.11 fee)
7227        assert_eq!(
7228            filled_received(&sell).balance,
7229            Balance::new(d("1009.79"), d("1009.79"))
7230        );
7231
7232        let fees = filled_trade(&buy).fees.fees + filled_trade(&sell).fees.fees;
7233        assert_eq!(
7234            d("1009.79") - d("1000"),
7235            d("10") - fees,
7236            "quote moves by the realised PnL less the fees the trades report"
7237        );
7238    }
7239
7240    /// The asset a fill receives is created at zero if the account never held it, so a backtest
7241    /// funds only what its first order pays with.
7242    #[test]
7243    fn a_buy_creates_the_base_balance_the_account_never_held() {
7244        let mut venue = SimulatedVenue::new(
7245            &config_from_balances(vec![funded(quote(), d("1000"))], FeeModelConfig::default()),
7246            spot_instruments(),
7247        );
7248
7249        let buy = venue.open_order(buy_request("2", market_prices("100")));
7250        let received = filled_received(&buy);
7251        assert_eq!(received.asset, base());
7252        assert_eq!(received.balance, Balance::new(d("2"), d("2")));
7253
7254        let sell = venue.open_order(sell_request("2", market_prices("100")));
7255        assert!(
7256            sell.response.state.is_accepted(),
7257            "the base the buy received can be sold: {:?}",
7258            sell.response.state
7259        );
7260        assert_eq!(filled_balance(&sell).balance, Balance::new(d("0"), d("0")));
7261        assert_eq!(
7262            filled_received(&sell).balance,
7263            Balance::new(d("1000"), d("1000"))
7264        );
7265    }
7266
7267    /// A per-unit commission, as a broker charges on shares, is a quote amount, so a sell pays it
7268    /// out of its proceeds rather than in base converted at the price.
7269    #[test]
7270    fn a_per_unit_commission_on_a_spot_sell_comes_out_of_the_proceeds() {
7271        let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
7272            commission_per_contract: d("0.5"),
7273        });
7274        let mut venue = make_venue_with_fee("10", "0", fee_model);
7275
7276        let sell = venue.open_order(sell_request("2", market_prices("100")));
7277
7278        assert_eq!(filled_trade(&sell).fees.fees, d("1"));
7279        assert_eq!(filled_balance(&sell).balance, Balance::new(d("8"), d("8")));
7280        assert_eq!(
7281            filled_received(&sell).balance,
7282            Balance::new(d("199"), d("199"))
7283        );
7284    }
7285
7286    /// A sell whose fee exceeds its proceeds takes the excess from quote, and is refused with
7287    /// nothing moved when quote cannot cover it.
7288    #[test]
7289    fn a_sell_whose_fee_exceeds_its_proceeds_needs_the_excess_in_quote() {
7290        let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
7291            commission_per_contract: d("2"),
7292        });
7293
7294        // Proceeds 1 * 1 = 1, fee 2: the sell owes 1 quote on top of the base it delivers.
7295        let mut short_of_quote = make_venue_with_fee("10", "0", fee_model);
7296        let refused = short_of_quote.open_order(sell_request("1", market_prices("1")));
7297        match refused.response.state {
7298            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
7299                ApiError::BalanceInsufficient(Some(asset), _),
7300            ))) => assert_eq!(asset, quote(), "the asset short is quote, not base"),
7301            other => panic!("expected BalanceInsufficient, got: {other:?}"),
7302        }
7303        assert!(refused.events.is_empty(), "a refusal moves nothing");
7304        assert_eq!(
7305            short_of_quote.account.balance_mut(&base()).unwrap().balance,
7306            Balance::new(d("10"), d("10"))
7307        );
7308
7309        let mut covered = make_venue_with_fee("10", "5", fee_model);
7310        let filled = covered.open_order(sell_request("1", market_prices("1")));
7311        assert_eq!(
7312            filled_balance(&filled).balance,
7313            Balance::new(d("9"), d("9"))
7314        );
7315        assert_eq!(
7316            filled_received(&filled).balance,
7317            Balance::new(d("4"), d("4"))
7318        );
7319    }
7320
7321    /// The same sell resting: its hold is the base it delivers, so when it crosses with quote
7322    /// unable to cover the excess fee it is cancelled, releasing the base, rather than filled.
7323    #[test]
7324    fn a_resting_sell_whose_fee_exceeds_its_proceeds_is_cancelled_when_quote_cannot_cover() {
7325        let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
7326            commission_per_contract: d("2"),
7327        });
7328        let mut venue = make_market_venue_with("10", "0", fee_model, SimFillConfig::default());
7329        advance(&mut venue, time(1));
7330        assert!(
7331            venue
7332                .apply_market(
7333                    &instrument_name(),
7334                    MarketObservation::new(book("0.5", "0.6")),
7335                    time(1)
7336                )
7337                .is_empty()
7338        );
7339
7340        let rested = venue.open_order(limit_request("s", Side::Sell, "1", "1", gtc()));
7341        assert_eq!(rested.events.len(), 1, "a 0.5 bid does not reach 1");
7342        assert_eq!(
7343            balance_of(&rested.events[0]).balance,
7344            Balance::new(d("10"), d("9"))
7345        );
7346
7347        advance(&mut venue, time(2));
7348        let events = venue.apply_market(
7349            &instrument_name(),
7350            MarketObservation::new(book("1.1", "1.2")),
7351            time(2),
7352        );
7353
7354        let [released, cancelled] = events.as_slice() else {
7355            panic!("expected the released hold, then the cancel: {events:?}");
7356        };
7357        assert_eq!(balance_of(released).balance, Balance::new(d("10"), d("10")));
7358        assert!(
7359            matches!(
7360                order_of(cancelled).state,
7361                OrderState::Inactive(InactiveOrderState::Cancelled(_))
7362            ),
7363            "{:?}",
7364            order_of(cancelled).state
7365        );
7366        assert!(venue.trades(time(0)).is_empty(), "nothing traded");
7367    }
7368
7369    /// When quote can cover the excess fee, the same resting sell fills: its base hold is released
7370    /// and the fill settled afresh, still restating each asset once.
7371    #[test]
7372    fn a_resting_sell_whose_fee_exceeds_its_proceeds_fills_when_quote_covers_it() {
7373        let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
7374            commission_per_contract: d("2"),
7375        });
7376        let mut venue = make_market_venue_with("10", "5", fee_model, SimFillConfig::default());
7377        advance(&mut venue, time(1));
7378        assert!(
7379            venue
7380                .apply_market(
7381                    &instrument_name(),
7382                    MarketObservation::new(book("0.5", "0.6")),
7383                    time(1)
7384                )
7385                .is_empty()
7386        );
7387        let rested = venue.open_order(limit_request("s", Side::Sell, "1", "1", gtc()));
7388        assert_eq!(rested.events.len(), 1, "a 0.5 bid does not reach 1");
7389
7390        advance(&mut venue, time(2));
7391        let events = venue.apply_market(
7392            &instrument_name(),
7393            MarketObservation::new(book("1.1", "1.2")),
7394            time(2),
7395        );
7396
7397        assert_eq!(
7398            events.len(),
7399            4,
7400            "the base paid, the quote received, the trade, then the terminal order"
7401        );
7402        assert_eq!(balance_of(&events[0]).balance, Balance::new(d("9"), d("9")));
7403        // 5 + proceeds of 1 - the fee of 2.
7404        assert_eq!(balance_of(&events[1]).balance, Balance::new(d("4"), d("4")));
7405        assert_eq!(trade_of(&events[2]).fees.fees, d("2"));
7406        assert!(
7407            matches!(
7408                order_of(&events[3]).state,
7409                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
7410            ),
7411            "{:?}",
7412            order_of(&events[3]).state
7413        );
7414    }
7415
7416    // --- SpotFeeAsset::Received: a buy's fee comes out of the base it receives -----------------
7417
7418    fn received(mut venue: SimulatedVenue) -> SimulatedVenue {
7419        venue.spot_fee_asset = SpotFeeAsset::Received;
7420        venue
7421    }
7422
7423    /// A buy pays its notional alone, and receives its quantity less the fee converted to base at
7424    /// the fill price. The fill reports that base fee, with the quote fee it came from.
7425    #[test]
7426    fn received_takes_a_buys_fee_from_the_base_it_receives() {
7427        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7428        let mut venue = received(make_venue_with_fee("0", "10000", fee_model));
7429
7430        // Notional 10 * 100 = 1000, fee 1 quote = 0.01 base.
7431        let outcome = venue.open_order(buy_request("10", market_prices("100")));
7432
7433        assert_eq!(
7434            filled_balance(&outcome).balance,
7435            Balance::new(d("9000"), d("9000"))
7436        );
7437        assert_eq!(filled_received(&outcome).asset, base());
7438        assert_eq!(
7439            filled_received(&outcome).balance,
7440            Balance::new(d("9.99"), d("9.99"))
7441        );
7442        assert_eq!(
7443            filled_trade(&outcome).fees,
7444            AssetFees::new(base(), d("0.01"), Some(d("1")))
7445        );
7446    }
7447
7448    /// A sell's fee is in quote, out of its proceeds, under either rule.
7449    #[test]
7450    fn received_takes_a_sells_fee_from_its_proceeds() {
7451        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7452        let mut venue = received(make_venue_with_fee("10", "0", fee_model));
7453
7454        let outcome = venue.open_order(sell_request("1", market_prices("100")));
7455
7456        assert_eq!(
7457            filled_balance(&outcome).balance,
7458            Balance::new(d("9"), d("9"))
7459        );
7460        assert_eq!(
7461            filled_received(&outcome).balance,
7462            Balance::new(d("99.9"), d("99.9"))
7463        );
7464        assert_eq!(
7465            filled_trade(&outcome).fees,
7466            AssetFees::new(quote(), d("0.1"), Some(d("0.1")))
7467        );
7468    }
7469
7470    /// A buy whose base fee exceeds its quantity takes the excess from base, and is refused with
7471    /// nothing moved when base cannot cover it.
7472    #[test]
7473    fn a_received_buy_whose_fee_exceeds_its_quantity_needs_the_excess_in_base() {
7474        let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
7475            commission_per_contract: d("2"),
7476        });
7477
7478        // Notional 1 * 1 = 1, fee 2 quote = 2 base: the buy owes 1 base on top of the 1 it receives.
7479        let mut short_of_base = received(make_venue_with_fee("0", "10", fee_model));
7480        let refused = short_of_base.open_order(buy_request("1", market_prices("1")));
7481        match refused.response.state {
7482            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
7483                ApiError::BalanceInsufficient(Some(asset), _),
7484            ))) => assert_eq!(asset, base(), "the asset short is base, not quote"),
7485            other => panic!("expected BalanceInsufficient, got: {other:?}"),
7486        }
7487        assert!(refused.events.is_empty(), "a refusal moves nothing");
7488        assert_eq!(
7489            short_of_base.account.balance_mut(&quote()).unwrap().balance,
7490            Balance::new(d("10"), d("10"))
7491        );
7492
7493        let mut covered = received(make_venue_with_fee("5", "10", fee_model));
7494        let filled = covered.open_order(buy_request("1", market_prices("1")));
7495        assert_eq!(
7496            filled_balance(&filled).balance,
7497            Balance::new(d("9"), d("9"))
7498        );
7499        assert_eq!(
7500            filled_received(&filled).balance,
7501            Balance::new(d("4"), d("4"))
7502        );
7503    }
7504
7505    /// A resting buy holds its notional alone, since its fee comes out of the base it receives,
7506    /// and its fill settles exactly what it held.
7507    #[test]
7508    fn a_resting_received_buy_holds_only_its_notional() {
7509        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7510        let mut venue = received(make_market_venue_with(
7511            "0",
7512            "10000",
7513            fee_model,
7514            SimFillConfig::default(),
7515        ));
7516        advance(&mut venue, time(1));
7517        assert!(
7518            venue
7519                .apply_market(
7520                    &instrument_name(),
7521                    MarketObservation::new(book("101", "102")),
7522                    time(1)
7523                )
7524                .is_empty()
7525        );
7526
7527        let rested = venue.open_order(limit_request("b", Side::Buy, "10", "100", gtc()));
7528        assert_eq!(rested.events.len(), 1, "a 102 ask does not reach 100");
7529        assert_eq!(
7530            balance_of(&rested.events[0]).balance,
7531            Balance::new(d("10000"), d("9000"))
7532        );
7533
7534        advance(&mut venue, time(2));
7535        let events = venue.apply_market(
7536            &instrument_name(),
7537            MarketObservation::new(book("98", "99")),
7538            time(2),
7539        );
7540
7541        assert_eq!(
7542            events.len(),
7543            4,
7544            "the quote paid, the base received, the trade, then the terminal order"
7545        );
7546        assert_eq!(
7547            balance_of(&events[0]).balance,
7548            Balance::new(d("9000"), d("9000"))
7549        );
7550        assert_eq!(
7551            balance_of(&events[1]).balance,
7552            Balance::new(d("9.99"), d("9.99"))
7553        );
7554        assert_eq!(
7555            trade_of(&events[2]).fees,
7556            AssetFees::new(base(), d("0.01"), Some(d("1")))
7557        );
7558    }
7559
7560    /// A fee cannot be converted to base at a price that is not positive, so such a buy is
7561    /// charged in quote, as under the default rule.
7562    #[test]
7563    fn a_received_buy_at_a_zero_price_is_charged_in_quote() {
7564        let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
7565            commission_per_contract: d("1"),
7566        });
7567        let mut venue = received(make_venue_with_fee("0", "10", fee_model));
7568
7569        let outcome = venue.open_order(buy_request("1", market_prices("0")));
7570
7571        assert_eq!(
7572            filled_balance(&outcome).balance,
7573            Balance::new(d("9"), d("9"))
7574        );
7575        assert_eq!(
7576            filled_received(&outcome).balance,
7577            Balance::new(d("1"), d("1"))
7578        );
7579        assert_eq!(filled_trade(&outcome).fees.asset, quote());
7580    }
7581
7582    #[test]
7583    fn percentage_fee_with_zero_price_returns_zero_fee() {
7584        // Edge case: if fill_price is zero, fee computation must not divide by zero
7585        let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
7586        let mut venue = make_venue_with_fee("10", "0", fee_model);
7587
7588        // Sell 1 BTC at price 0 (degenerate case)
7589        // Fee (quote) = 0 * 0.001 * 1 = 0
7590        // Fee (base) = guarded by is_zero() check, returns 0
7591        let outcome = venue.open_order(sell_request("1", market_prices("0")));
7592
7593        assert!(
7594            outcome.response.state.is_accepted(),
7595            "sell at zero price should succeed: {:?}",
7596            outcome.response.state
7597        );
7598
7599        // Fee must be zero (not NaN or panic from division by zero)
7600        assert_eq!(
7601            filled_trade(&outcome).fees.fees,
7602            Decimal::ZERO,
7603            "fee must be zero when price is zero"
7604        );
7605
7606        // Base balance: 10 - 1 = 9 (no fee deducted)
7607        let btc = venue.account.balance_mut(&base()).unwrap();
7608        assert_eq!(
7609            btc.balance.free,
7610            d("9"),
7611            "base balance must decrease by quantity only"
7612        );
7613    }
7614
7615    /// A Market order the venue cannot price is rejected, not panicked on.
7616    ///
7617    /// A Market order has no limit price of its own to fall back on, so with no usable
7618    /// snapshot there is no price anywhere. This used to hit an `expect`. Because the async driver
7619    /// runs as a spawned task, that killed the exchange, dropped the request receiver, and turned
7620    /// every later order into `ExchangeOffline` -- so a backtest ran to completion over the whole
7621    /// dataset having filled nothing and surfaced only a `JoinError` at teardown.
7622    ///
7623    /// Both unpriceable cases are covered, because they mean different things: `None` is a caller
7624    /// that sampled no snapshot at all, `Some(empty)` is a cold start where one was sampled and the
7625    /// instrument had no price yet.
7626    #[test]
7627    fn a_market_order_that_cannot_be_priced_is_rejected_rather_than_panicking() {
7628        for market in [None, Some(MarketSnapshot::default())] {
7629            let mut venue = make_venue("10", "10000000");
7630
7631            let outcome = venue.open_order(buy_request("1.0", market));
7632
7633            assert!(
7634                outcome.events.is_empty(),
7635                "a rejected order must not notify a fill (market={market:?})"
7636            );
7637            assert!(
7638                matches!(
7639                    outcome.response.state,
7640                    OrderState::Inactive(InactiveOrderState::OpenFailed(
7641                        UnindexedOrderError::Rejected(_)
7642                    ))
7643                ),
7644                "expected a rejection, got {:?} (market={market:?})",
7645                outcome.response.state
7646            );
7647
7648            let usd = venue.account.balance_mut(&quote()).unwrap();
7649            assert_eq!(
7650                usd.balance.free,
7651                d("10000000"),
7652                "a rejected order must not move the balance (market={market:?})"
7653            );
7654        }
7655    }
7656
7657    /// A market order carrying a price, as `RequestOpen::price` documents it should not.
7658    fn priced_market_order(
7659        side: Side,
7660        price: &str,
7661        market: Option<MarketSnapshot>,
7662    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
7663        let mut request = request(instrument_name(), side, "1", market);
7664        request.state.price = Some(d(price));
7665        request
7666    }
7667
7668    /// A price on a market order is ignored, as the live clients ignore it, so it is no fallback:
7669    /// with nothing to price from, the order is still unpriceable rather than filled at the price
7670    /// its sender happened to stamp.
7671    ///
7672    /// Both regimes can reach the fallback: a request-priced venue sent no usable snapshot, and a
7673    /// market-driven venue never fed the instrument.
7674    #[test]
7675    fn a_market_order_is_never_filled_at_a_price_it_carries() {
7676        let mut market_driven = make_market_venue("10", "10000000");
7677        advance(&mut market_driven, time(1));
7678
7679        let cases = [
7680            ("request-priced", make_venue("10", "10000000"), None),
7681            (
7682                "request-priced",
7683                make_venue("10", "10000000"),
7684                Some(MarketSnapshot::default()),
7685            ),
7686            ("market-driven", market_driven, None),
7687        ];
7688
7689        for (regime, mut venue, market) in cases {
7690            let outcome = venue.open_order(priced_market_order(Side::Buy, "1", market));
7691
7692            assert!(
7693                matches!(
7694                    outcome.response.state,
7695                    OrderState::Inactive(InactiveOrderState::OpenFailed(
7696                        UnindexedOrderError::Rejected(ApiError::OrderRejected(_))
7697                    ))
7698                ),
7699                "expected the unpriceable rejection, got {:?} ({regime}, market={market:?})",
7700                outcome.response.state
7701            );
7702            assert!(
7703                outcome.events.is_empty(),
7704                "nothing filled ({regime}, market={market:?})"
7705            );
7706        }
7707    }
7708
7709    /// Nor does a price on a market order bound its fill, as a limit would.
7710    #[test]
7711    fn a_market_order_is_not_clamped_by_a_price_it_carries() {
7712        for (side, stray) in [(Side::Buy, "90"), (Side::Sell, "110")] {
7713            let mut venue = make_venue("10", "10000000");
7714            let market = Some(MarketSnapshot {
7715                last_price: Some(d("100")),
7716                ..MarketSnapshot::default()
7717            });
7718
7719            let outcome = venue.open_order(priced_market_order(side, stray, market));
7720
7721            assert_eq!(
7722                filled_trade(&outcome).price,
7723                d("100"),
7724                "a {side:?} fills at the market, not clamped to its stray {stray}"
7725            );
7726        }
7727    }
7728
7729    /// The rejection names which of the two unpriceable causes occurred, because their fixes
7730    /// differ: an absent snapshot is a wiring problem in the caller, an empty one is a timing
7731    /// problem in the data.
7732    #[test]
7733    fn an_unpriceable_rejection_distinguishes_an_absent_snapshot_from_an_empty_one() {
7734        let reason_of = |market| {
7735            let mut venue = make_venue("10", "10000000");
7736            match venue.open_order(buy_request("1.0", market)).response.state {
7737                OrderState::Inactive(InactiveOrderState::OpenFailed(
7738                    UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
7739                )) => reason,
7740                other => panic!("expected an OrderRejected, got {other:?}"),
7741            }
7742        };
7743
7744        assert!(
7745            reason_of(None).contains("no market snapshot"),
7746            "an absent snapshot must be named as such, got: {}",
7747            reason_of(None)
7748        );
7749        assert!(
7750            reason_of(Some(MarketSnapshot::default())).contains("no market price available yet"),
7751            "an empty snapshot must read as a cold start, got: {}",
7752            reason_of(Some(MarketSnapshot::default()))
7753        );
7754    }
7755
7756    /// A Market order fills at the snapshot the request carried, and at nothing else.
7757    ///
7758    /// With the default `LastPriceFillModel` the fill price is the snapshot's `last_price`, so this
7759    /// pins that a market order priced purely from `RequestOpen::market` both fills and fills at
7760    /// the right price.
7761    #[test]
7762    fn a_market_order_fills_at_the_price_its_snapshot_carried() {
7763        let mut venue = make_venue("0", "10000");
7764
7765        let outcome = venue.open_order(buy_request(
7766            "1.0",
7767            Some(MarketSnapshot::from_last_price(Some(d("100")))),
7768        ));
7769
7770        let OrderState::Inactive(InactiveOrderState::FullyFilled(ref filled)) =
7771            outcome.response.state
7772        else {
7773            panic!("expected a filled order, got {:?}", outcome.response.state)
7774        };
7775        assert_eq!(
7776            filled.avg_price,
7777            Some(d("100")),
7778            "a market order must fill at its snapshot's last price"
7779        );
7780        assert_eq!(filled.filled_quantity, d("1.0"));
7781
7782        assert_eq!(filled_trade(&outcome).price, d("100"));
7783
7784        let usd = venue.account.balance_mut(&quote()).unwrap();
7785        assert_eq!(
7786            usd.balance.free,
7787            d("9900"),
7788            "a 1 BTC buy at 100 must debit 100 quote"
7789        );
7790    }
7791
7792    /// A configured reservation is honoured, not refused.
7793    ///
7794    /// `total != free` is ordinary user configuration -- an `initial_state` copied from a live
7795    /// account with margin reserved. It was once an `assert_eq!` that panicked the exchange task on
7796    /// the first order, then a rejection, because a ledger where every order filled on arrival had
7797    /// no way to represent an amount held back. It has one now: the order draws on `free`, and the
7798    /// held amount is still held afterwards.
7799    #[test]
7800    fn a_configured_quote_reservation_survives_a_fill() {
7801        let mut venue = make_venue("10", "10000000");
7802        venue.account.balance_mut(&quote()).unwrap().balance.total = d("20000000");
7803
7804        let outcome = venue.open_order(buy_request("1.0", market_prices("50000")));
7805
7806        assert!(
7807            matches!(outcome.response.state, OrderState::Inactive(_)),
7808            "a market order fills on arrival, got {:?}",
7809            outcome.response.state
7810        );
7811
7812        let usdt = venue.account.balance_mut(&quote()).unwrap().balance;
7813        assert_eq!(
7814            usdt.free,
7815            d("10000000") - d("50000"),
7816            "the fill draws the notional from the spendable side"
7817        );
7818        assert_eq!(
7819            usdt.total - usdt.free,
7820            d("10000000"),
7821            "the configured reservation must be untouched by an unrelated fill"
7822        );
7823    }
7824
7825    /// The sell path debits the base asset, so the reservation must survive there too.
7826    #[test]
7827    fn a_configured_base_reservation_survives_a_fill() {
7828        let mut venue = make_venue("10", "10000000");
7829        venue.account.balance_mut(&base()).unwrap().balance.total = d("20");
7830
7831        let outcome = venue.open_order(sell_request("1.0", market_prices("50000")));
7832
7833        assert!(
7834            matches!(outcome.response.state, OrderState::Inactive(_)),
7835            "a market order fills on arrival, got {:?}",
7836            outcome.response.state
7837        );
7838
7839        let btc = venue.account.balance_mut(&base()).unwrap().balance;
7840        assert_eq!(
7841            btc.free,
7842            d("10") - d("1"),
7843            "the fill delivers one base unit from the spendable side"
7844        );
7845        assert_eq!(
7846            btc.total - btc.free,
7847            d("10"),
7848            "the configured reservation must be untouched by an unrelated fill"
7849        );
7850    }
7851
7852    /// An order larger than `free` is refused even when `total` would cover it.
7853    ///
7854    /// The whole point of a reservation is that the held portion is not spendable. Sizing against
7855    /// `total` would let one order spend what another is already holding.
7856    #[test]
7857    fn a_reservation_is_not_spendable() {
7858        let mut venue = make_venue("10", "100");
7859        // free 100, total 10_000_000: only 100 may be spent.
7860        venue.account.balance_mut(&quote()).unwrap().balance.total = d("10000000");
7861
7862        let outcome = venue.open_order(buy_request("1.0", market_prices("50000")));
7863
7864        assert!(
7865            matches!(
7866                outcome.response.state,
7867                OrderState::Inactive(InactiveOrderState::OpenFailed(
7868                    UnindexedOrderError::Rejected(ApiError::BalanceInsufficient(_, _))
7869                ))
7870            ),
7871            "expected an insufficient-balance rejection, got {:?}",
7872            outcome.response.state
7873        );
7874        assert!(
7875            outcome.events.is_empty(),
7876            "a refused order moves nothing, so it restates nothing"
7877        );
7878
7879        let usdt = venue.account.balance_mut(&quote()).unwrap().balance;
7880        assert_eq!(usdt.free, d("100"), "a refused reserve must not move free");
7881        assert_eq!(
7882            usdt.total,
7883            d("10000000"),
7884            "a refused reserve must not move total"
7885        );
7886    }
7887
7888    /// Orders come back in a total order, so two runs of one backtest produce one snapshot.
7889    ///
7890    /// The snapshot sorts with an *unstable* sort over a `FnvHashMap`'s values. Keyed on
7891    /// `instrument` alone every order on one instrument ties, leaving their relative order
7892    /// unspecified -- so a golden hash over the snapshot is a flake, and comparing two runs of the
7893    /// same backtest compares two different orderings. `cid` is unique per order, which makes the
7894    /// key total.
7895    #[test]
7896    fn an_account_snapshot_orders_one_instruments_orders_deterministically() {
7897        // Enough orders that a hash map's own iteration order is vanishingly unlikely to be sorted
7898        // by accident -- with three, a passing assertion would say nothing.
7899        const CIDS: [&str; 8] = ["h", "d", "a", "g", "c", "f", "b", "e"];
7900
7901        let resting = |cid: &str| UnindexedOrder {
7902            key: OrderKey {
7903                exchange: EXCHANGE,
7904                instrument: instrument_name(),
7905                strategy: StrategyId::new("test"),
7906                cid: ClientOrderId::new(cid),
7907            },
7908            side: Side::Buy,
7909            price: Some(d("50000")),
7910            quantity: d("1"),
7911            kind: OrderKind::Limit,
7912            time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
7913            state: OrderState::active(Open {
7914                id: VenueOrderId::Assigned(OrderId::new(cid)),
7915                time_exchange: Default::default(),
7916                filled_quantity: Decimal::ZERO,
7917            }),
7918        };
7919
7920        let mut config = spot_config("10", "10000000", FeeModelConfig::default());
7921        config.initial_state.instruments = vec![InstrumentAccountSnapshot {
7922            instrument: instrument_name(),
7923            orders: CIDS.into_iter().map(resting).collect(),
7924            orders_complete: true,
7925            position: PositionReport::Unreported,
7926            isolated: None,
7927        }];
7928
7929        let venue = SimulatedVenue::new(&config, spot_instruments());
7930
7931        let cids = venue
7932            .account_snapshot()
7933            .instruments
7934            .into_iter()
7935            .flat_map(|instrument| instrument.orders)
7936            .map(|order| order.key.cid)
7937            .collect::<Vec<_>>();
7938
7939        let mut expected = CIDS.map(ClientOrderId::new).to_vec();
7940        expected.sort();
7941        assert_eq!(
7942            cids, expected,
7943            "orders on one instrument must be ordered by a key no two of them share"
7944        );
7945    }
7946
7947    /// A venue seeded with orders carrying decimal ids mints its own past the highest of them, so
7948    /// no minted id repeats a seeded one. Anything keyed on the id alone — a position under
7949    /// `OmsMode::Hedging`, say — would otherwise merge the two orders.
7950    #[test]
7951    fn minted_order_ids_start_past_the_highest_decimal_id_seeded() {
7952        let resting = |cid: &str, id: &str| UnindexedOrder {
7953            key: OrderKey {
7954                exchange: EXCHANGE,
7955                instrument: instrument_name(),
7956                strategy: StrategyId::new("test"),
7957                cid: ClientOrderId::new(cid),
7958            },
7959            side: Side::Buy,
7960            price: Some(d("50000")),
7961            quantity: d("1"),
7962            kind: OrderKind::Limit,
7963            time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
7964            state: OrderState::active(Open {
7965                id: VenueOrderId::Assigned(OrderId::new(id)),
7966                time_exchange: Default::default(),
7967                filled_quantity: Decimal::ZERO,
7968            }),
7969        };
7970
7971        let seeded_venue = |orders| {
7972            let mut config = spot_config("10", "10000000", FeeModelConfig::default());
7973            config.initial_state.instruments = vec![InstrumentAccountSnapshot {
7974                instrument: instrument_name(),
7975                orders,
7976                orders_complete: true,
7977                position: PositionReport::Unreported,
7978                isolated: None,
7979            }];
7980            SimulatedVenue::new(&config, spot_instruments())
7981        };
7982
7983        // A non-decimal id can never equal a minted one, however it sorts.
7984        let mut venue = seeded_venue(vec![
7985            resting("a", "3"),
7986            resting("b", "1"),
7987            resting("c", "zzz"),
7988        ]);
7989        assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("4"));
7990        assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("5"));
7991        // Still a count of the orders this venue booked, not an id.
7992        assert_eq!(venue.order_sequence(), 2);
7993
7994        // A seeded cancelled order's id is the venue's too, and counts the same way.
7995        let mut cancelled = resting("d", "9");
7996        cancelled.state = OrderState::inactive(Cancelled::new(
7997            OrderId::new("9"),
7998            Default::default(),
7999            Some(Decimal::ZERO),
8000        ));
8001        let mut venue = seeded_venue(vec![resting("a", "3"), cancelled]);
8002        assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("10"));
8003
8004        // With only a non-decimal id seeded, ids count from zero as they always have.
8005        let mut venue = seeded_venue(vec![resting("c", "zzz")]);
8006        assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("0"));
8007    }
8008
8009    /// With nothing seeded, ids count from zero as they always have.
8010    #[test]
8011    fn minted_order_ids_start_at_zero_when_nothing_is_seeded() {
8012        let config = spot_config("10", "10000000", FeeModelConfig::default());
8013        let mut venue = SimulatedVenue::new(&config, spot_instruments());
8014
8015        assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("0"));
8016    }
8017
8018    /// A seeded `u64::MAX` leaves the next id one past it, rather than overflowing.
8019    #[test]
8020    fn a_seeded_u64_max_order_id_does_not_overflow_the_next() {
8021        let mut account = AccountState::try_from(
8022            spot_config("10", "10000000", FeeModelConfig::default()).initial_state,
8023        )
8024        .unwrap();
8025        let mut seeded = seeded_part_filled("48000", "1", "0");
8026        seeded.state.id = VenueOrderId::Assigned(OrderId::new(u64::MAX.to_string()));
8027        assert!(
8028            account.book(seeded, None).is_ok(),
8029            "the book is vacant under this id"
8030        );
8031
8032        assert_eq!(first_unseeded_order_id(&account), u128::from(u64::MAX) + 1);
8033    }
8034
8035    #[test]
8036    fn an_order_for_a_quantity_that_is_not_positive_is_rejected_before_the_ledger_moves() {
8037        for request in [
8038            buy_request("0", market_prices("50000")),
8039            sell_request("-1", market_prices("50000")),
8040        ] {
8041            let mut venue = make_venue("100", "10000000");
8042            let quantity = request.state.quantity;
8043
8044            let outcome = venue.open_order(request);
8045
8046            assert_eq!(
8047                rejection_reason(&outcome),
8048                format!(
8049                    "cannot open {} for {EXCHANGE}: quantity {quantity} is not positive",
8050                    instrument_name()
8051                )
8052            );
8053            assert!(outcome.events.is_empty(), "a refused order moves nothing");
8054            assert!(venue.trades(time(0)).is_empty(), "and nothing traded");
8055            assert!(venue.orders_open(&[]).is_empty(), "or rests");
8056            assert_eq!(free_quote(&venue), d("10000000"));
8057            assert_eq!(total_base(&venue), d("100"));
8058        }
8059
8060        // Its kind is refused first: this venue holds no limit order, whatever its quantity.
8061        let mut venue = make_venue("100", "10000000");
8062        let outcome = venue.open_order(limit_request("limit", Side::Buy, "0", "50000", gtc()));
8063        let reason = rejection_reason(&outcome);
8064        assert!(
8065            reason.contains("does not support OrderKind::Limit"),
8066            "{reason}"
8067        );
8068    }
8069
8070    /// A [`RequestPriced`](VenueRegime::RequestPriced) venue rejects a limit order on its kind,
8071    /// before any fill model is consulted.
8072    ///
8073    /// The clamp that bounds a fill by the order's own terms lives on the market-driven path, so
8074    /// this venue must never reach a pricing decision for an order it cannot hold. The snapshot is
8075    /// chosen so a lapse would be unmistakable: `MidpointFillModel` would price this buy at 50,000
8076    /// against a limit of 40,000 — a fill 25% above the order's own limit — so a `Filled` response
8077    /// here means the gate stopped holding, not merely that a rejection message changed.
8078    #[test]
8079    fn a_limit_order_is_rejected_before_a_fill_model_is_consulted() {
8080        let mut venue = make_venue("100", "10000000");
8081        venue.fill_model = Arc::new(SimFillConfig::Midpoint(crate::fill::MidpointFillModel));
8082
8083        let mut request = buy_request("1", market_prices("50000"));
8084        request.state.kind = OrderKind::Limit;
8085        request.state.price = Some(d("40000"));
8086
8087        let outcome = venue.open_order(request);
8088
8089        assert!(
8090            outcome.events.is_empty(),
8091            "a rejected order moves no balance and books no trade"
8092        );
8093        match outcome.response.state {
8094            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
8095                ApiError::OrderRejected(ref reason),
8096            ))) => assert!(
8097                reason.contains("does not support OrderKind::Limit"),
8098                "the rejection must name the unsupported kind, got: {reason}"
8099            ),
8100            other => panic!("a limit order must be rejected by kind, got: {other:?}"),
8101        }
8102    }
8103
8104    // --- Stage 3b: limit orders -------------------------------------------------------------
8105
8106    /// The regime gate, and the reason it exists: a venue with no feed could never match a resting
8107    /// order, so accepting one would be accepted-and-silently-never-filled.
8108    #[test]
8109    fn a_request_priced_venue_rejects_a_limit_order_and_says_what_would_accept_one() {
8110        let mut venue = make_venue("10", "1000000");
8111        assert_eq!(venue.regime(), VenueRegime::RequestPriced);
8112
8113        let outcome = venue.open_order(limit_request("a", Side::Buy, "1", "48000", gtc()));
8114
8115        assert!(
8116            outcome.events.is_empty(),
8117            "a rejected order moves no balance"
8118        );
8119        match outcome.response.state {
8120            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
8121                ApiError::OrderRejected(ref reason),
8122            ))) => {
8123                assert!(
8124                    reason.contains("new_market_driven"),
8125                    "the rejection must name the constructor that would accept it, got: {reason}"
8126                );
8127            }
8128            ref other => panic!("a limit order must be rejected by the regime, got: {other:?}"),
8129        }
8130    }
8131
8132    /// A limit order that the market has not reached rests, and the venue holds exactly what its
8133    /// fill will cost — no more, so no release-and-re-debit is ever needed.
8134    #[test]
8135    fn a_limit_order_that_does_not_cross_rests_and_reserves_what_its_fill_will_cost() {
8136        let (venue, rested) = venue_resting_one_buy("48000");
8137
8138        // 48000 notional + 2bp maker fee on it.
8139        let reserved = d("48009.6");
8140
8141        let balance = venue.balances(&[quote()]).remove(0);
8142        assert_eq!(
8143            balance.balance.total,
8144            d("1000000"),
8145            "resting an order spends nothing: `total` moves only on a fill"
8146        );
8147        assert_eq!(
8148            balance.balance.free,
8149            d("1000000") - reserved,
8150            "`free` falls by the notional plus the MAKER fee the fill will charge"
8151        );
8152        assert_eq!(
8153            balance.balance.used(),
8154            reserved,
8155            "the held portion is what a client sees as committed to the order"
8156        );
8157
8158        assert_eq!(
8159            rested_open(&rested).filled_quantity,
8160            Decimal::ZERO,
8161            "the client is told an order is working with none of its quantity done"
8162        );
8163
8164        let open = venue
8165            .orders_open(&[instrument_name()])
8166            .pop()
8167            .expect("the order is on the book");
8168        assert_eq!(open.price, Some(d("48000")));
8169        assert_eq!(
8170            open.state,
8171            *rested_open(&rested),
8172            "the book and the response describe the same order"
8173        );
8174    }
8175
8176    /// A resting order is the passive side, so it is paid the price it quoted. Price improvement
8177    /// accrues to the aggressor that crossed it, never to the maker.
8178    #[test]
8179    fn a_resting_order_fills_at_its_own_limit_and_is_never_improved_by_the_book() {
8180        let (mut venue, _) = venue_resting_one_buy("48000");
8181        advance(&mut venue, time(2));
8182
8183        // Straight through the limit: the offer is 100 better than the order asked for.
8184        let events = venue.apply_market(
8185            &instrument_name(),
8186            MarketObservation::new(book("47800", "47900")),
8187            time(2),
8188        );
8189
8190        assert_eq!(
8191            events.len(),
8192            4,
8193            "one filled spot order owes two balances, a trade and a terminal order snapshot"
8194        );
8195
8196        let trade = trade_of(&events[2]);
8197        assert_eq!(
8198            trade.price,
8199            d("48000"),
8200            "a resting order fills at its own limit; 47900 would credit it improvement no maker \
8201             can obtain"
8202        );
8203        assert_eq!(
8204            trade.fees.fees,
8205            d("9.6"),
8206            "2bp MAKER on 48000, not 10bp taker: the resting side supplied liquidity"
8207        );
8208
8209        assert_eq!(
8210            filled_price(order_of(&events[3])),
8211            d("48000"),
8212            "the terminal snapshot reports the same price as the trade"
8213        );
8214    }
8215
8216    /// The trade precedes the terminal order snapshot, which is not merely cosmetic: a consumer
8217    /// routing a fill to a position needs the order still to be live when the trade arrives.
8218    #[test]
8219    fn a_resting_fill_reports_its_trade_before_the_order_that_produced_it() {
8220        let (mut venue, _) = venue_resting_one_buy("48000");
8221        advance(&mut venue, time(2));
8222
8223        let events = venue.apply_market(
8224            &instrument_name(),
8225            MarketObservation::new(book("47800", "47900")),
8226            time(2),
8227        );
8228
8229        assert_eq!(
8230            balance_of(&events[0]).asset,
8231            quote(),
8232            "the balance the fill paid with comes first"
8233        );
8234        assert_eq!(
8235            balance_of(&events[1]).asset,
8236            base(),
8237            "then the balance it received"
8238        );
8239        assert!(
8240            matches!(events[2].kind, AccountEventKind::Trade(_)),
8241            "then the trade, got: {:?}",
8242            events[2].kind
8243        );
8244        assert!(
8245            matches!(events[3].kind, AccountEventKind::OrderSnapshot(_)),
8246            "and only then the order it terminated, got: {:?}",
8247            events[3].kind
8248        );
8249    }
8250
8251    /// A completed order is reported as `Inactive(FullyFilled)` rather than as an `Open` carrying a
8252    /// complete fill. Both denote the same fact; only this one can carry the price it filled at.
8253    #[test]
8254    fn a_resting_fill_reports_its_order_as_terminal_rather_than_as_a_complete_open() {
8255        let (mut venue, _) = venue_resting_one_buy("48000");
8256        advance(&mut venue, time(2));
8257
8258        let events = venue.apply_market(
8259            &instrument_name(),
8260            MarketObservation::new(book("47800", "47900")),
8261            time(2),
8262        );
8263
8264        assert!(
8265            matches!(
8266                order_of(&events[3]).state,
8267                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
8268            ),
8269            "got: {:?}",
8270            order_of(&events[3]).state
8271        );
8272        assert!(
8273            venue.orders_open(&[]).is_empty(),
8274            "a filled order leaves the book"
8275        );
8276    }
8277
8278    /// Reserve exactly what will be settled: the client sees one restatement per asset a fill
8279    /// moves, and never a balance the account did not hold.
8280    #[test]
8281    fn a_resting_order_settles_exactly_what_it_reserved() {
8282        let (mut venue, _) = venue_resting_one_buy("48000");
8283        advance(&mut venue, time(2));
8284
8285        let reserved = d("48009.6");
8286        let free_while_resting = venue.balances(&[quote()]).remove(0).balance.free;
8287
8288        let events = venue.apply_market(
8289            &instrument_name(),
8290            MarketObservation::new(book("47800", "47900")),
8291            time(2),
8292        );
8293
8294        let balance = balance_of(&events[0]);
8295        assert_eq!(
8296            balance.balance.total,
8297            d("1000000") - reserved,
8298            "`total` falls by exactly what was held"
8299        );
8300        assert_eq!(
8301            balance.balance.free, free_while_resting,
8302            "`free` was already reduced when the order rested, so the fill does not move it again"
8303        );
8304        assert_eq!(
8305            balance.balance.used(),
8306            Decimal::ZERO,
8307            "nothing is held any more: the order that held it is gone"
8308        );
8309    }
8310
8311    // --- An order trades only what is left of it ---------------------------------------------
8312
8313    /// An open buy at `price` that reached the book with `filled` of `quantity` already done, as a
8314    /// configured `initial_state` copied from a live account may seed one.
8315    fn seeded_part_filled(price: &str, quantity: &str, filled: &str) -> OpenOrder {
8316        as_open(Order {
8317            key: OrderKey {
8318                exchange: EXCHANGE,
8319                instrument: instrument_name(),
8320                strategy: StrategyId::new("test"),
8321                cid: ClientOrderId::new("part_filled"),
8322            },
8323            side: Side::Buy,
8324            price: Some(d(price)),
8325            quantity: d(quantity),
8326            kind: OrderKind::Limit,
8327            time_in_force: gtc(),
8328            state: OrderState::active(Open::new(
8329                VenueOrderId::Assigned(OrderId::new("part_filled")),
8330                time(1),
8331                d(filled),
8332            )),
8333        })
8334        .expect("the seeded order is Open")
8335    }
8336
8337    /// A seeded order holds nothing, so an account that cannot pay for its fill is a mis-specified
8338    /// fixture, and the venue says so rather than cancelling the order as unaffordable.
8339    #[test]
8340    #[should_panic(expected = "cannot afford the fill of resting order part_filled")]
8341    fn a_seeded_order_the_account_cannot_afford_panics_when_it_fills() {
8342        let mut venue = make_market_venue("10", "100");
8343        advance(&mut venue, time(1));
8344        assert!(
8345            venue
8346                .account
8347                .book(seeded_part_filled("48000", "1", "0"), None)
8348                .is_ok()
8349        );
8350
8351        advance(&mut venue, time(2));
8352        let _ = venue.apply_market(
8353            &instrument_name(),
8354            MarketObservation::new(book("47800", "47900")),
8355            time(2),
8356        );
8357    }
8358
8359    /// An order that reached the book part-filled trades only the rest of it.
8360    ///
8361    /// Settling and printing its whole quantity would charge the ledger a second time for a
8362    /// portion that traded before this venue ever held the order, and report a trade of a size
8363    /// that never happened. The two readings are far enough apart to be unmistakable: the whole
8364    /// order costs 48,009.6 of quote and its remainder 28,805.76.
8365    #[test]
8366    fn a_part_filled_order_on_the_book_settles_and_prints_only_its_remainder() {
8367        let mut venue = make_market_venue("10", "1000000");
8368        advance(&mut venue, time(1));
8369
8370        assert!(
8371            venue
8372                .account
8373                .book(
8374                    seeded_part_filled("48000", "1", "0.4"),
8375                    // As `initial_state` seeds one: the venue never took anything for it.
8376                    None,
8377                )
8378                .is_ok(),
8379            "the book is vacant under this id"
8380        );
8381
8382        advance(&mut venue, time(2));
8383        let events = venue.apply_market(
8384            &instrument_name(),
8385            MarketObservation::new(book("47800", "47900")),
8386            time(2),
8387        );
8388
8389        assert_eq!(
8390            events.len(),
8391            4,
8392            "one filled spot order owes two balances, a trade and a terminal order snapshot"
8393        );
8394
8395        let trade = trade_of(&events[2]);
8396        assert_eq!(
8397            trade.quantity,
8398            d("0.6"),
8399            "0.6 is what was left to trade; printing 1 would report a fill that did not happen"
8400        );
8401        assert_eq!(
8402            trade.fees.fees,
8403            d("5.76"),
8404            "2bp maker on 0.6 at 48000, not on the whole quantity"
8405        );
8406        assert_eq!(
8407            trade.order_filled_quantity,
8408            Some(d("1")),
8409            "the order's cumulative is the 0.4 it arrived with plus the 0.6 this cross traded"
8410        );
8411
8412        assert_eq!(
8413            balance_of(&events[0]).balance.total,
8414            d("1000000") - d("28805.76"),
8415            "the fill costs what the remainder costs; 48009.6 would pay a second time for the 0.4 \
8416             that traded elsewhere"
8417        );
8418    }
8419
8420    /// A fill that completes a part-filled order reports no average price.
8421    ///
8422    /// This venue struck one of the two fills behind it and never saw the other, so the limit it
8423    /// filled at is this fill's price rather than the mean of both — and
8424    /// [`Filled::avg_price`](crate::order::state::Filled::avg_price) is optional for exactly that.
8425    /// A consumer needing the mean has the trades to compute it from. An order that reached the
8426    /// book with nothing done still reports the price it filled at, which
8427    /// `a_resting_order_fills_at_its_own_limit_and_is_never_improved_by_the_book` pins.
8428    #[test]
8429    fn a_fill_completing_a_part_filled_order_reports_no_average_price() {
8430        let mut venue = make_market_venue("10", "1000000");
8431        advance(&mut venue, time(1));
8432
8433        assert!(
8434            venue
8435                .account
8436                .book(seeded_part_filled("48000", "1", "0.4"), None)
8437                .is_ok(),
8438            "the book is vacant under this id"
8439        );
8440
8441        advance(&mut venue, time(2));
8442        let events = venue.apply_market(
8443            &instrument_name(),
8444            MarketObservation::new(book("47800", "47900")),
8445            time(2),
8446        );
8447
8448        let filled = match &order_of(&events[3]).state {
8449            OrderState::Inactive(InactiveOrderState::FullyFilled(filled)) => filled,
8450            other => panic!("expected a fully filled order, got: {other:?}"),
8451        };
8452
8453        assert_eq!(
8454            filled.filled_quantity,
8455            d("1"),
8456            "the order has now done its whole quantity, which is what this field reports"
8457        );
8458        assert_eq!(
8459            filled.avg_price, None,
8460            "48000 is one of the two prices behind that quantity, not their mean"
8461        );
8462    }
8463
8464    /// Resting the remainder of a part-filled order holds what is left of it, and the cross that
8465    /// completes it settles exactly that.
8466    ///
8467    /// `rest_order` is reached directly because nothing routes a part-filled order to it yet: this
8468    /// venue caps no fill, so an order that aggresses on arrival fills in full and never rests a
8469    /// remainder. The arithmetic is pinned all the same, because the two halves have to agree —
8470    /// holding against the whole quantity would reserve balance for a portion that has already
8471    /// settled, and the fill that released it would then settle less than was held.
8472    #[test]
8473    fn resting_a_part_filled_order_reserves_and_settles_exactly_its_remainder() {
8474        let mut venue = make_market_venue("10", "1000000");
8475        advance(&mut venue, time(1));
8476
8477        let terms = venue
8478            .instrument_terms(&instrument_name())
8479            .expect("the spot fixture is priceable");
8480
8481        let (response, notifications) = venue.rest_order(
8482            limit_request("part_filled", Side::Buy, "1", "48000", gtc()),
8483            d("48000"),
8484            &terms,
8485            d("0.4"),
8486        );
8487
8488        match &response.state {
8489            OrderState::Active(ActiveOrderState::Open(open)) => assert_eq!(
8490                open.filled_quantity,
8491                d("0.4"),
8492                "the order rests carrying what it has already done"
8493            ),
8494            other => panic!("expected a resting open order, got: {other:?}"),
8495        }
8496
8497        match notifications {
8498            Some(OpenOrderNotifications::Rested { balance, .. }) => assert_eq!(
8499                balance.0.balance.free,
8500                d("1000000") - d("28805.76"),
8501                "0.6 at 48000 plus 2bp maker is held; 48009.6 would hold against the 0.4 that has \
8502                 already settled"
8503            ),
8504            other => panic!("a rested order restates one balance, got: {other:?}"),
8505        }
8506
8507        advance(&mut venue, time(2));
8508        let events = venue.apply_market(
8509            &instrument_name(),
8510            MarketObservation::new(book("47800", "47900")),
8511            time(2),
8512        );
8513
8514        assert_eq!(events.len(), 4);
8515        assert_eq!(
8516            trade_of(&events[2]).quantity,
8517            d("0.6"),
8518            "the cross prints the remainder that was held for"
8519        );
8520        assert_eq!(
8521            balance_of(&events[0]).balance.total,
8522            d("1000000") - d("28805.76"),
8523            "and settles exactly that: one restatement, with nothing left over to release"
8524        );
8525        assert_eq!(
8526            balance_of(&events[0]).balance.used(),
8527            Decimal::ZERO,
8528            "nothing is held any more: the order that held it is gone"
8529        );
8530    }
8531
8532    /// Which of the three ways to aggress an order's terms asked for.
8533    ///
8534    /// Asserted on [`Disposition`] directly because nothing observable separates them yet: this
8535    /// venue caps no fill, so every arm below fills in full and leaves no remainder to dispose of.
8536    /// What an order asked for is a property of the order rather than of what the book held, and a
8537    /// size cap is what would make the difference visible — fill-or-kill refusing the partial fill
8538    /// immediate-or-cancel accepts, and a good-until-cancelled limit resting what neither keeps.
8539    #[test]
8540    fn a_marketable_order_aggresses_the_way_its_own_terms_asked_for() {
8541        let now = time(1);
8542        let gtd = TimeInForce::GoodTillDate { expiry: time(9) };
8543
8544        assert_eq!(
8545            disposition(OrderKind::Limit, gtc(), true, now),
8546            Disposition::FillAndRest,
8547            "a limit order with somewhere to wait keeps whatever it could not fill"
8548        );
8549        assert_eq!(
8550            disposition(OrderKind::Limit, gtd, true, now),
8551            Disposition::FillAndRest,
8552            "a deadline it has not reached is still somewhere to wait"
8553        );
8554        assert_eq!(
8555            disposition(OrderKind::Limit, TimeInForce::ImmediateOrCancel, true, now),
8556            Disposition::FillAndCancel,
8557            "immediate-or-cancel asked not to wait"
8558        );
8559        assert_eq!(
8560            disposition(OrderKind::Limit, TimeInForce::FillOrKill, true, now),
8561            Disposition::FillOrNothing,
8562            "fill-or-kill would rather trade nothing than trade in part"
8563        );
8564        assert_eq!(
8565            disposition(OrderKind::Market, gtc(), true, now),
8566            Disposition::FillAndCancel,
8567            "an order carrying no limit price has nothing to wait at, whatever its time in force"
8568        );
8569        assert_eq!(
8570            disposition(OrderKind::Market, gtd, true, now),
8571            Disposition::FillAndCancel,
8572            "including one whose deadline is still ahead of it"
8573        );
8574        assert_eq!(
8575            disposition(OrderKind::Market, TimeInForce::FillOrKill, true, now),
8576            Disposition::FillOrNothing,
8577            "and all-or-nothing stays all-or-nothing whatever the kind"
8578        );
8579    }
8580
8581    /// A limit order the market has already reached is the aggressor, so it takes the book's price
8582    /// bounded by its own limit, and pays the taker rate.
8583    #[test]
8584    fn a_marketable_limit_order_fills_on_arrival_at_the_book_bounded_by_its_limit() {
8585        let mut venue = make_market_venue_with(
8586            "10",
8587            "1000000",
8588            maker_taker_fee(),
8589            SimFillConfig::BidAsk(BidAskFillModel),
8590        );
8591        advance(&mut venue, time(1));
8592        assert!(
8593            venue
8594                .apply_market(
8595                    &instrument_name(),
8596                    MarketObservation::new(book("47800", "47900")),
8597                    time(1)
8598                )
8599                .is_empty()
8600        );
8601
8602        // Willing to pay 48000; the offer is 47900, so it prints at the offer.
8603        let outcome = venue.open_order(limit_request("taker", Side::Buy, "1", "48000", gtc()));
8604
8605        assert_eq!(
8606            outcome.events.len(),
8607            3,
8608            "a filled open owes both balances, then the trade"
8609        );
8610        let trade = filled_trade(&outcome);
8611        assert_eq!(
8612            trade.price,
8613            d("47900"),
8614            "the aggressor takes the offer, which is better than its limit"
8615        );
8616        assert_eq!(
8617            trade.fees.fees,
8618            d("47.9"),
8619            "10bp TAKER on 47900: it crossed the spread rather than resting"
8620        );
8621        assert!(
8622            venue.orders_open(&[]).is_empty(),
8623            "it filled on arrival, so it never rested"
8624        );
8625    }
8626
8627    /// The clamp, on the **default** fill model. Marketability is judged on the book while the
8628    /// price comes from the model, and the two are separate readings of a sampled snapshot — so the
8629    /// model can return a price the order never agreed to pay. The order's own terms bound it.
8630    ///
8631    /// A clamp against [`MidpointFillModel`] or [`BidAskFillModel`] would be vacuous: both derive
8632    /// the price from the very side of the book that decided the order was marketable, so neither
8633    /// can exceed the limit that side already crossed.
8634    #[test]
8635    fn a_marketable_limit_order_never_fills_worse_than_its_limit() {
8636        let mut venue = make_market_venue_with(
8637            "10",
8638            "1000000",
8639            FeeModelConfig::default(),
8640            // `LastPriceFillModel`, which reads `last_price` and never consults the book.
8641            SimFillConfig::default(),
8642        );
8643        advance(&mut venue, time(1));
8644
8645        // The offer is inside the limit, so the order is marketable -- but the trade price the
8646        // model reads is well above it.
8647        let stale = MarketSnapshot {
8648            best_bid: Some(d("46900")),
8649            best_ask: Some(d("47000")),
8650            last_price: Some(d("48000")),
8651        };
8652        assert!(
8653            venue
8654                .apply_market(&instrument_name(), MarketObservation::new(stale), time(1))
8655                .is_empty()
8656        );
8657
8658        let outcome = venue.open_order(limit_request("clamped", Side::Buy, "1", "47100", gtc()));
8659
8660        assert_eq!(
8661            filled_trade(&outcome).price,
8662            d("47100"),
8663            "the model priced it at 48000, which the order never agreed to pay"
8664        );
8665    }
8666
8667    /// The two pricing rules are different rules, and this is the one point at which they must give
8668    /// the same answer. If they diverge here, one of them is wrong.
8669    #[test]
8670    fn the_two_pricing_rules_agree_where_the_book_meets_the_limit() {
8671        let limit = "48000";
8672        let touching = || book("47900", "48000");
8673
8674        // Rule 1: marketable on arrival. The book is already at the limit when the order arrives.
8675        let mut aggressor = make_market_venue_with(
8676            "10",
8677            "1000000",
8678            maker_taker_fee(),
8679            SimFillConfig::BidAsk(BidAskFillModel),
8680        );
8681        advance(&mut aggressor, time(1));
8682        assert!(
8683            aggressor
8684                .apply_market(
8685                    &instrument_name(),
8686                    MarketObservation::new(touching()),
8687                    time(1)
8688                )
8689                .is_empty()
8690        );
8691        let on_arrival = aggressor.open_order(limit_request("a", Side::Buy, "1", limit, gtc()));
8692        let arrival_price = filled_trade(&on_arrival).price;
8693
8694        // Rule 2: rested first, crossed later by a book that reaches exactly the same price.
8695        let mut maker = make_market_venue_with(
8696            "10",
8697            "1000000",
8698            maker_taker_fee(),
8699            SimFillConfig::BidAsk(BidAskFillModel),
8700        );
8701        advance(&mut maker, time(1));
8702        assert!(
8703            maker
8704                .apply_market(
8705                    &instrument_name(),
8706                    MarketObservation::new(book("49000", "49100")),
8707                    time(1)
8708                )
8709                .is_empty()
8710        );
8711        let rested = maker.open_order(limit_request("b", Side::Buy, "1", limit, gtc()));
8712        assert_eq!(rested.events.len(), 1, "it rested rather than filling");
8713        advance(&mut maker, time(2));
8714        let resting_events = maker.apply_market(
8715            &instrument_name(),
8716            MarketObservation::new(touching()),
8717            time(2),
8718        );
8719        let resting_price = trade_of(&resting_events[2]).price;
8720
8721        assert_eq!(
8722            arrival_price,
8723            d(limit),
8724            "an order arriving when the book is at its limit fills at the limit"
8725        );
8726        assert_eq!(
8727            resting_price, arrival_price,
8728            "the two pricing rules must agree exactly at the crossing point; they may differ only \
8729             where the order genuinely is, or is not, marketable"
8730        );
8731    }
8732
8733    /// Matching takes orders in the order the book holds them and stops at the first that does not
8734    /// cross — not at the first it happens to iterate over.
8735    #[test]
8736    fn matching_fills_in_price_time_order_and_stops_at_the_first_order_that_does_not_cross() {
8737        let mut venue = make_market_venue("10", "1000000");
8738        advance(&mut venue, time(1));
8739        assert!(
8740            venue
8741                .apply_market(
8742                    &instrument_name(),
8743                    MarketObservation::new(book("49000", "49100")),
8744                    time(1)
8745                )
8746                .is_empty()
8747        );
8748
8749        // Best bid last, to prove the fill order is the book's rather than the insertion order.
8750        for (cid, price) in [("worst", "47000"), ("mid", "48000"), ("best", "48500")] {
8751            let outcome = venue.open_order(limit_request(cid, Side::Buy, "1", price, gtc()));
8752            assert_eq!(outcome.events.len(), 1, "{cid} must rest");
8753        }
8754
8755        advance(&mut venue, time(2));
8756        // Crosses `best` and `mid`, leaves `worst` alone.
8757        let events = venue.apply_market(
8758            &instrument_name(),
8759            MarketObservation::new(book("47900", "48000")),
8760            time(2),
8761        );
8762
8763        let filled: Vec<_> = events
8764            .iter()
8765            .filter_map(|event| match &event.kind {
8766                AccountEventKind::Trade(trade) => Some(trade.price),
8767                _ => None,
8768            })
8769            .collect();
8770
8771        assert_eq!(
8772            filled,
8773            vec![d("48500"), d("48000")],
8774            "best bid first, and each at its own limit"
8775        );
8776        assert_eq!(
8777            venue
8778                .orders_open(&[])
8779                .into_iter()
8780                .map(|order| order.key.cid)
8781                .collect::<Vec<_>>(),
8782            vec![ClientOrderId::new("worst")],
8783            "the order the market did not reach is untouched"
8784        );
8785    }
8786
8787    /// A trades-only feed has no book at all, so marketability falls back to the trade price. A
8788    /// book-only rule would never match anything on such a feed.
8789    #[test]
8790    fn a_feed_with_no_book_matches_on_the_trade_price() {
8791        let mut venue = make_market_venue("10", "1000000");
8792        advance(&mut venue, time(1));
8793        assert!(
8794            venue
8795                .apply_market(
8796                    &instrument_name(),
8797                    MarketObservation::new(traded("49000")),
8798                    time(1)
8799                )
8800                .is_empty()
8801        );
8802
8803        let outcome = venue.open_order(limit_request("t", Side::Buy, "1", "48000", gtc()));
8804        assert_eq!(outcome.events.len(), 1, "49000 traded does not reach 48000");
8805
8806        advance(&mut venue, time(2));
8807        let events = venue.apply_market(
8808            &instrument_name(),
8809            MarketObservation::new(traded("47500")),
8810            time(2),
8811        );
8812
8813        assert_eq!(events.len(), 4, "a print through the limit fills it");
8814        assert_eq!(trade_of(&events[2]).price, d("48000"));
8815    }
8816
8817    /// A price between the two sides of a book crosses neither, so a supplied book is never
8818    /// second-guessed by `last_price`.
8819    #[test]
8820    fn a_price_inside_the_spread_does_not_cross_a_resting_order() {
8821        let (mut venue, _) = venue_resting_one_buy("48000");
8822        advance(&mut venue, time(2));
8823
8824        // The microprice sits inside the spread and below the limit; the offer does not.
8825        let inside = MarketSnapshot {
8826            best_bid: Some(d("47900")),
8827            best_ask: Some(d("48100")),
8828            last_price: Some(d("47950")),
8829        };
8830
8831        assert!(
8832            venue
8833                .apply_market(&instrument_name(), MarketObservation::new(inside), time(2))
8834                .is_empty(),
8835            "the offer is 48100, so a 48000 bid is not crossed however the trade price reads"
8836        );
8837        assert_eq!(
8838            venue.orders_open(&[]).len(),
8839            1,
8840            "the order is still resting"
8841        );
8842    }
8843
8844    /// A sell rests against the bid, pays with base, and settles the base it committed.
8845    #[test]
8846    fn a_resting_sell_reserves_base_and_settles_it() {
8847        let mut venue = make_market_venue("10", "1000000");
8848        advance(&mut venue, time(1));
8849        assert!(
8850            venue
8851                .apply_market(
8852                    &instrument_name(),
8853                    MarketObservation::new(book("47000", "47100")),
8854                    time(1)
8855                )
8856                .is_empty()
8857        );
8858
8859        let outcome = venue.open_order(limit_request("s", Side::Sell, "1", "48000", gtc()));
8860        assert_eq!(outcome.events.len(), 1, "47000 bid does not reach 48000");
8861        assert_eq!(
8862            balance_of(&outcome.events[0]).asset,
8863            base(),
8864            "a spot sell delivers the base asset, so that is what is held"
8865        );
8866
8867        advance(&mut venue, time(2));
8868        let events = venue.apply_market(
8869            &instrument_name(),
8870            MarketObservation::new(book("48100", "48200")),
8871            time(2),
8872        );
8873
8874        assert_eq!(trade_of(&events[2]).price, d("48000"), "at its own limit");
8875        let balance = balance_of(&events[0]);
8876        assert_eq!(balance.asset, base());
8877        // 1 BTC delivered, and nothing more: the fee is charged in quote.
8878        assert_eq!(balance.balance.total, d("10") - d("1"));
8879        let received = balance_of(&events[1]);
8880        assert_eq!(received.asset, quote());
8881        // 1 BTC at its 48000 limit, less the 2bp maker fee of 9.6.
8882        assert_eq!(received.balance.total, d("1000000") + d("48000") - d("9.6"));
8883    }
8884
8885    /// A maker rebate is a negative fee, so the amount held is less than the notional and settling
8886    /// it leaves more behind than the notional alone would.
8887    #[test]
8888    fn a_maker_rebate_is_held_and_settled_as_a_smaller_amount_than_the_notional() {
8889        let rebate =
8890            FeeModelConfig::Percentage(PercentageFeeModel::maker_taker(d("-0.0001"), d("0.001")));
8891        let mut venue = make_market_venue_with("10", "1000000", rebate, SimFillConfig::default());
8892        advance(&mut venue, time(1));
8893        assert!(
8894            venue
8895                .apply_market(
8896                    &instrument_name(),
8897                    MarketObservation::new(book("49000", "49100")),
8898                    time(1)
8899                )
8900                .is_empty()
8901        );
8902
8903        let outcome = venue.open_order(limit_request("r", Side::Buy, "1", "48000", gtc()));
8904
8905        // 48000 notional less a 1bp rebate on it.
8906        let held = d("48000") - d("4.8");
8907        assert_eq!(
8908            balance_of(&outcome.events[0]).balance.used(),
8909            held,
8910            "a rebate means less is held than the notional"
8911        );
8912
8913        advance(&mut venue, time(2));
8914        let events = venue.apply_market(
8915            &instrument_name(),
8916            MarketObservation::new(book("47800", "47900")),
8917            time(2),
8918        );
8919
8920        assert_eq!(
8921            balance_of(&events[0]).balance.total,
8922            d("1000000") - held,
8923            "and exactly that less is settled, so the rebate is kept"
8924        );
8925        assert_eq!(trade_of(&events[2]).fees.fees, d("-4.8"));
8926    }
8927
8928    /// An order the account cannot afford never reaches the book, and the ledger is untouched.
8929    #[test]
8930    fn a_limit_order_that_cannot_be_afforded_is_rejected_without_resting() {
8931        let mut venue = make_market_venue("10", "1000");
8932        advance(&mut venue, time(1));
8933        assert!(
8934            venue
8935                .apply_market(
8936                    &instrument_name(),
8937                    MarketObservation::new(book("49000", "49100")),
8938                    time(1)
8939                )
8940                .is_empty()
8941        );
8942
8943        let outcome = venue.open_order(limit_request("poor", Side::Buy, "1", "48000", gtc()));
8944
8945        assert!(
8946            outcome.events.is_empty(),
8947            "a refused reservation moves nothing"
8948        );
8949        assert!(matches!(
8950            outcome.response.state,
8951            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
8952                ApiError::BalanceInsufficient(..)
8953            )))
8954        ));
8955        assert!(venue.orders_open(&[]).is_empty(), "it never rested");
8956        assert_eq!(
8957            venue.balances(&[quote()]).remove(0).balance.free,
8958            d("1000"),
8959            "the ledger is left exactly as it was"
8960        );
8961    }
8962
8963    // --- Stage 3b: cancel -------------------------------------------------------------------
8964
8965    /// Cancelling gives the held balance back and reports the quantity done, which this venue can
8966    /// only ever report as zero because it models no partial fills.
8967    #[test]
8968    fn a_cancel_releases_the_reservation_and_restates_the_balance() {
8969        let (mut venue, _) = venue_resting_one_buy("48000");
8970        advance(&mut venue, time(2));
8971
8972        let outcome = venue.cancel_order(OrderEvent {
8973            key: OrderKey {
8974                exchange: EXCHANGE,
8975                instrument: instrument_name(),
8976                strategy: StrategyId::new("test"),
8977                cid: ClientOrderId::new("resting"),
8978            },
8979            state: RequestCancel { id: None },
8980        });
8981
8982        assert_eq!(outcome.events.len(), 1, "a cancel restates one balance");
8983        let balance = balance_of(&outcome.events[0]);
8984        assert_eq!(
8985            balance.balance.free,
8986            d("1000000"),
8987            "everything held against the order is spendable again"
8988        );
8989        assert_eq!(
8990            balance.balance.total,
8991            d("1000000"),
8992            "nothing ever left the account, so `total` never moved"
8993        );
8994
8995        let cancelled = outcome.response.state.expect("the order was on the book");
8996        assert_eq!(cancelled.filled_quantity, Some(Decimal::ZERO));
8997        assert_eq!(cancelled.time_exchange, time(2));
8998        assert!(venue.orders_open(&[]).is_empty());
8999    }
9000
9001    /// The cancel-vs-fill race, which a non-zero `to_venue` latency makes reachable. The caller is
9002    /// told to reconcile against the fill rather than to retry.
9003    #[test]
9004    fn a_cancel_that_loses_the_race_to_its_own_fill_reports_the_fill() {
9005        let (mut venue, _) = venue_resting_one_buy("48000");
9006        advance(&mut venue, time(2));
9007        let events = venue.apply_market(
9008            &instrument_name(),
9009            MarketObservation::new(book("47800", "47900")),
9010            time(2),
9011        );
9012        assert_eq!(events.len(), 4, "the order filled first");
9013
9014        let outcome = venue.cancel_order(OrderEvent {
9015            key: OrderKey {
9016                exchange: EXCHANGE,
9017                instrument: instrument_name(),
9018                strategy: StrategyId::new("test"),
9019                cid: ClientOrderId::new("resting"),
9020            },
9021            state: RequestCancel { id: None },
9022        });
9023
9024        assert!(outcome.events.is_empty(), "a failed cancel moves nothing");
9025        assert!(
9026            matches!(
9027                outcome.response.state,
9028                Err(OrderError::Rejected(ApiError::OrderAlreadyFullyFilled))
9029            ),
9030            "got: {:?}",
9031            outcome.response.state
9032        );
9033    }
9034
9035    /// A second cancel is a state conflict rather than an unknown order, and says so.
9036    #[test]
9037    fn a_second_cancel_reports_the_order_as_already_cancelled() {
9038        let (mut venue, _) = venue_resting_one_buy("48000");
9039        let cancel = || OrderEvent {
9040            key: OrderKey {
9041                exchange: EXCHANGE,
9042                instrument: instrument_name(),
9043                strategy: StrategyId::new("test"),
9044                cid: ClientOrderId::new("resting"),
9045            },
9046            state: RequestCancel { id: None },
9047        };
9048
9049        assert!(venue.cancel_order(cancel()).response.state.is_ok());
9050        let second = venue.cancel_order(cancel());
9051
9052        assert!(
9053            matches!(
9054                second.response.state,
9055                Err(OrderError::Rejected(ApiError::OrderAlreadyCancelled))
9056            ),
9057            "got: {:?}",
9058            second.response.state
9059        );
9060    }
9061
9062    /// A cancelled order is reported by a later snapshot, so a client reconciling against the venue
9063    /// sees the terminal state rather than an order that merely vanished.
9064    #[test]
9065    fn a_cancelled_order_enters_the_account_snapshot() {
9066        let (mut venue, _) = venue_resting_one_buy("48000");
9067        venue.cancel_order(OrderEvent {
9068            key: OrderKey {
9069                exchange: EXCHANGE,
9070                instrument: instrument_name(),
9071                strategy: StrategyId::new("test"),
9072                cid: ClientOrderId::new("resting"),
9073            },
9074            state: RequestCancel { id: None },
9075        });
9076
9077        let snapshot = venue.account_snapshot();
9078        let orders = &snapshot.instruments[0].orders;
9079        assert_eq!(orders.len(), 1);
9080        assert!(matches!(
9081            orders[0].state,
9082            OrderState::Inactive(InactiveOrderState::Cancelled(_))
9083        ));
9084    }
9085
9086    // --- Stage 3b: time in force ------------------------------------------------------------
9087
9088    /// Every time in force this venue cannot honour is rejected rather than treated as
9089    /// `GoodUntilCancelled`, which would silently leave an order working that its sender asked to
9090    /// have cancelled.
9091    #[test]
9092    fn a_limit_order_is_rejected_for_every_time_in_force_this_venue_cannot_honour() {
9093        let unsupported = [
9094            TimeInForce::GoodUntilEndOfDay,
9095            TimeInForce::AtOpen,
9096            TimeInForce::AtClose,
9097        ];
9098
9099        for time_in_force in unsupported {
9100            let mut venue = make_market_venue("10", "1000000");
9101            advance(&mut venue, time(1));
9102            assert!(
9103                venue
9104                    .apply_market(
9105                        &instrument_name(),
9106                        MarketObservation::new(book("49000", "49100")),
9107                        time(1)
9108                    )
9109                    .is_empty()
9110            );
9111
9112            let outcome =
9113                venue.open_order(limit_request("tif", Side::Buy, "1", "48000", time_in_force));
9114
9115            assert!(
9116                outcome.events.is_empty(),
9117                "{time_in_force} must move no balance"
9118            );
9119            assert!(
9120                venue.orders_open(&[]).is_empty(),
9121                "{time_in_force} must not rest"
9122            );
9123            match outcome.response.state {
9124                OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
9125                    ApiError::OrderRejected(ref reason),
9126                ))) => assert!(
9127                    reason.contains("does not model"),
9128                    "the rejection must say what is missing, got: {reason}"
9129                ),
9130                ref other => panic!("{time_in_force} must be rejected, got: {other:?}"),
9131            }
9132        }
9133    }
9134
9135    /// A time in force that only bounds *how long* an order works is moot for one that fills in
9136    /// full on arrival, and must not stop a market order filling.
9137    #[test]
9138    fn a_market_order_is_accepted_for_every_time_in_force_that_only_bounds_how_long_it_works() {
9139        for time_in_force in [
9140            TimeInForce::GoodUntilCancelled { post_only: false },
9141            TimeInForce::ImmediateOrCancel,
9142            TimeInForce::FillOrKill,
9143            TimeInForce::GoodTillDate { expiry: time(9) },
9144            TimeInForce::GoodUntilEndOfDay,
9145        ] {
9146            let mut venue = make_venue("10", "1000000");
9147            let mut request = buy_request("1", market_prices("48000"));
9148            request.state.time_in_force = time_in_force;
9149
9150            let outcome = venue.open_order(request);
9151
9152            assert!(
9153                matches!(
9154                    outcome.response.state,
9155                    OrderState::Inactive(InactiveOrderState::FullyFilled(_))
9156                ),
9157                "{time_in_force} on a market order must still fill, got: {:?}",
9158                outcome.response.state
9159            );
9160        }
9161    }
9162
9163    /// The two kinds of time in force a market order cannot keep, as opposed to the many it makes
9164    /// moot.
9165    ///
9166    /// `AtOpen` and `AtClose` say *when* an order executes rather than how long it works, and
9167    /// honouring them needs a session calendar. Filling one on arrival would answer a question its
9168    /// sender did not ask, at a price that is not the auction it wanted — this library's own IBKR
9169    /// client turns them into real market-on-close orders, so they are not a flag to be ignored.
9170    /// `post_only` is refused because a market order takes liquidity by definition.
9171    #[test]
9172    fn a_market_order_is_rejected_for_a_time_in_force_it_cannot_keep() {
9173        for time_in_force in [
9174            TimeInForce::GoodUntilCancelled { post_only: true },
9175            TimeInForce::AtOpen,
9176            TimeInForce::AtClose,
9177        ] {
9178            let mut venue = make_venue("10", "1000000");
9179            let mut request = buy_request("1", market_prices("48000"));
9180            request.state.time_in_force = time_in_force;
9181
9182            let outcome = venue.open_order(request);
9183
9184            assert!(
9185                outcome.events.is_empty(),
9186                "{time_in_force} must move no balance"
9187            );
9188            match outcome.response.state {
9189                OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
9190                    ApiError::OrderRejected(ref reason),
9191                ))) => assert!(
9192                    reason.contains("does not model"),
9193                    "the rejection must say what is missing, got: {reason}"
9194                ),
9195                ref other => panic!("{time_in_force} must be rejected, got: {other:?}"),
9196            }
9197        }
9198    }
9199
9200    // --- Stage 3b-ii: post-only -------------------------------------------------------------
9201
9202    /// Post-only means "never take liquidity", so being marketable is what disqualifies the order.
9203    ///
9204    /// It retires having traded nothing rather than being rejected: the request was valid, and the
9205    /// venue did exactly what `post_only` asked of it.
9206    #[test]
9207    fn a_post_only_order_that_would_take_liquidity_is_cancelled_having_traded_nothing() {
9208        let mut venue = make_market_venue("10", "1000000");
9209        advance(&mut venue, time(1));
9210        assert!(
9211            venue
9212                .apply_market(
9213                    &instrument_name(),
9214                    MarketObservation::new(book("49000", "49100")),
9215                    time(1)
9216                )
9217                .is_empty()
9218        );
9219
9220        // Buying at 50000 crosses an ask of 49100, so this order would be the aggressor.
9221        let outcome = venue.open_order(limit_request(
9222            "post",
9223            Side::Buy,
9224            "1",
9225            "50000",
9226            TimeInForce::GoodUntilCancelled { post_only: true },
9227        ));
9228
9229        assert!(outcome.events.is_empty(), "nothing was reserved or traded");
9230        assert!(venue.orders_open(&[]).is_empty(), "it must not rest");
9231        assert!(venue.trades(time(0)).is_empty(), "it must not trade");
9232
9233        match outcome.response.state {
9234            OrderState::Inactive(InactiveOrderState::Cancelled(ref cancelled)) => {
9235                assert_eq!(cancelled.filled_quantity, Some(Decimal::ZERO))
9236            }
9237            ref other => panic!("a marketable post-only order must be cancelled, got: {other:?}"),
9238        }
9239    }
9240
9241    /// The same order that does *not* cross is exactly what post-only is for, and rests normally.
9242    #[test]
9243    fn a_post_only_order_that_would_rest_is_accepted_and_reserves() {
9244        let mut venue = make_market_venue("10", "1000000");
9245        advance(&mut venue, time(1));
9246        assert!(
9247            venue
9248                .apply_market(
9249                    &instrument_name(),
9250                    MarketObservation::new(book("49000", "49100")),
9251                    time(1)
9252                )
9253                .is_empty()
9254        );
9255
9256        let outcome = venue.open_order(limit_request(
9257            "post",
9258            Side::Buy,
9259            "1",
9260            "48000",
9261            TimeInForce::GoodUntilCancelled { post_only: true },
9262        ));
9263
9264        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
9265        assert_eq!(venue.orders_open(&[]).len(), 1);
9266        assert_eq!(
9267            balance_of(&outcome.events[0]).balance.free,
9268            d("1000000") - d("48000") - d("9.6"),
9269            "resting post-only reserves the notional plus the 2bp maker fee its fill will charge"
9270        );
9271    }
9272
9273    // --- Stage 3b-ii: immediate-or-cancel and fill-or-kill -----------------------------------
9274
9275    /// Both retire unfilled when the book is not there to trade against, and neither rests.
9276    ///
9277    /// They coincide because this venue models no order size — see `disposition`.
9278    #[test]
9279    fn a_non_marketable_immediate_order_is_cancelled_having_traded_nothing() {
9280        for time_in_force in [TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill] {
9281            let mut venue = make_market_venue("10", "1000000");
9282            advance(&mut venue, time(1));
9283            assert!(
9284                venue
9285                    .apply_market(
9286                        &instrument_name(),
9287                        MarketObservation::new(book("49000", "49100")),
9288                        time(1)
9289                    )
9290                    .is_empty()
9291            );
9292
9293            let outcome =
9294                venue.open_order(limit_request("ioc", Side::Buy, "1", "48000", time_in_force));
9295
9296            assert!(
9297                outcome.events.is_empty(),
9298                "{time_in_force} must move no balance"
9299            );
9300            assert!(
9301                venue.orders_open(&[]).is_empty(),
9302                "{time_in_force} must not rest"
9303            );
9304            match outcome.response.state {
9305                OrderState::Inactive(InactiveOrderState::Cancelled(ref cancelled)) => assert_eq!(
9306                    cancelled.filled_quantity,
9307                    Some(Decimal::ZERO),
9308                    "{time_in_force} traded nothing"
9309                ),
9310                ref other => panic!("{time_in_force} must be cancelled, got: {other:?}"),
9311            }
9312        }
9313    }
9314
9315    /// Marketable, and both fill in full against the book as the aggressor.
9316    #[test]
9317    fn a_marketable_immediate_order_fills_in_full() {
9318        for time_in_force in [TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill] {
9319            let mut venue = make_market_venue("10", "1000000");
9320            advance(&mut venue, time(1));
9321            assert!(
9322                venue
9323                    .apply_market(
9324                        &instrument_name(),
9325                        MarketObservation::new(traded("49000")),
9326                        time(1)
9327                    )
9328                    .is_empty()
9329            );
9330
9331            let outcome =
9332                venue.open_order(limit_request("ioc", Side::Buy, "1", "50000", time_in_force));
9333
9334            assert!(
9335                matches!(
9336                    outcome.response.state,
9337                    OrderState::Inactive(InactiveOrderState::FullyFilled(_))
9338                ),
9339                "{time_in_force} must fill, got: {:?}",
9340                outcome.response.state
9341            );
9342        }
9343    }
9344
9345    // --- Stage 3b-ii: deadlines -------------------------------------------------------------
9346
9347    /// A deadline in the future rests like any other order, and nothing retires it early.
9348    #[test]
9349    fn a_good_till_date_order_rests_until_its_deadline() {
9350        let mut venue = make_market_venue("10", "1000000");
9351        advance(&mut venue, time(1));
9352
9353        let outcome = venue.open_order(limit_request(
9354            "gtd",
9355            Side::Buy,
9356            "1",
9357            "48000",
9358            TimeInForce::GoodTillDate { expiry: time(9) },
9359        ));
9360
9361        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
9362
9363        // Short of the deadline, so the sweep must find nothing.
9364        advance(&mut venue, time(8));
9365        assert_eq!(venue.orders_open(&[]).len(), 1, "still working at time(8)");
9366    }
9367
9368    /// Advancing to the deadline retires the order, releases what it held, and says so in order.
9369    #[test]
9370    fn advancing_to_a_deadline_releases_the_reservation_then_reports_the_order() {
9371        let mut venue = make_market_venue("10", "1000000");
9372        advance(&mut venue, time(1));
9373
9374        let outcome = venue.open_order(limit_request(
9375            "gtd",
9376            Side::Buy,
9377            "1",
9378            "48000",
9379            TimeInForce::GoodTillDate { expiry: time(9) },
9380        ));
9381        let reserved = balance_of(&outcome.events[0]).balance.free;
9382
9383        let events = venue.advance_time(time(9));
9384
9385        assert_eq!(events.len(), 2, "one release and one terminal snapshot");
9386        assert_eq!(
9387            balance_of(&events[0]).balance.free,
9388            reserved + d("48000") + d("9.6"),
9389            "the release restates exactly what resting took"
9390        );
9391
9392        let order = order_of(&events[1]);
9393        match order.state {
9394            OrderState::Inactive(InactiveOrderState::Expired(ref expired)) => {
9395                assert_eq!(expired.time_exchange, time(9));
9396                assert_eq!(expired.filled_quantity, Some(Decimal::ZERO));
9397            }
9398            ref other => panic!("the order must be reported expired, got: {other:?}"),
9399        }
9400
9401        assert!(venue.orders_open(&[]).is_empty(), "it must leave the book");
9402    }
9403
9404    /// The deadline is reached *at* its instant, not after it.
9405    ///
9406    /// The boundary is the whole contract: a caller reading "good till `T`" must be able to say
9407    /// whether the order is working at `T`, and this pins the answer to "no".
9408    #[test]
9409    fn a_deadline_is_reached_at_its_instant_not_after_it() {
9410        let mut venue = make_market_venue("10", "1000000");
9411        advance(&mut venue, time(1));
9412
9413        let _ = venue.open_order(limit_request(
9414            "gtd",
9415            Side::Buy,
9416            "1",
9417            "48000",
9418            TimeInForce::GoodTillDate { expiry: time(5) },
9419        ));
9420
9421        // One second short: still working.
9422        advance(&mut venue, time(4));
9423        assert_eq!(venue.orders_open(&[]).len(), 1);
9424
9425        // Exactly the stated instant: retired.
9426        assert_eq!(venue.advance_time(time(5)).len(), 2);
9427        assert!(venue.orders_open(&[]).is_empty());
9428    }
9429
9430    /// A tick that both reaches a deadline and crosses the order retires it rather than filling it.
9431    ///
9432    /// The deadline is an unconditional cutoff: whether an order trades at the very instant it
9433    /// expires must not depend on whether the market happened to cross there.
9434    #[test]
9435    fn a_deadline_is_swept_before_the_tick_that_would_have_filled_it() {
9436        let mut venue = make_market_venue("10", "1000000");
9437        advance(&mut venue, time(1));
9438
9439        let _ = venue.open_order(limit_request(
9440            "gtd",
9441            Side::Buy,
9442            "1",
9443            "48000",
9444            TimeInForce::GoodTillDate { expiry: time(5) },
9445        ));
9446
9447        // An ask of 47000 crosses a limit of 48000, so this tick would have filled the order — but
9448        // it also reaches the deadline.
9449        let events = venue.apply_market(
9450            &instrument_name(),
9451            MarketObservation::new(book("46900", "47000")),
9452            time(5),
9453        );
9454
9455        assert_eq!(events.len(), 2, "a release and an expiry, not a fill");
9456        assert!(
9457            venue.trades(time(0)).is_empty(),
9458            "an expired order must not trade on the tick that retired it"
9459        );
9460        assert!(matches!(
9461            order_of(&events[1]).state,
9462            OrderState::Inactive(InactiveOrderState::Expired(_))
9463        ));
9464    }
9465
9466    /// An order whose deadline has already passed on arrival never reaches the book.
9467    ///
9468    /// With no independent clock, an order accepted past its own deadline would work until
9469    /// something else happened to touch the venue — and if nothing did, for the rest of the run.
9470    #[test]
9471    fn a_good_till_date_order_that_has_already_expired_is_never_rested() {
9472        let mut venue = make_market_venue("10", "1000000");
9473        advance(&mut venue, time(9));
9474
9475        let outcome = venue.open_order(limit_request(
9476            "stale",
9477            Side::Buy,
9478            "1",
9479            "48000",
9480            TimeInForce::GoodTillDate { expiry: time(5) },
9481        ));
9482
9483        assert!(outcome.events.is_empty(), "nothing was reserved");
9484        assert!(venue.orders_open(&[]).is_empty(), "it must not rest");
9485        match outcome.response.state {
9486            OrderState::Inactive(InactiveOrderState::Expired(ref expired)) => {
9487                assert_eq!(expired.time_exchange, time(9), "stamped when it was found");
9488                assert_eq!(expired.filled_quantity, Some(Decimal::ZERO));
9489            }
9490            ref other => panic!("a stale deadline must expire, not {other:?}"),
9491        }
9492    }
9493
9494    /// A stale deadline retires the order even when the book would have filled it.
9495    ///
9496    /// The same unconditional cutoff as on the tick path: an order whose deadline has elapsed has
9497    /// stopped working, so whether it *could* have traded is not a question the venue asks. The
9498    /// alternative would execute an order its sender had already asked to be finished with, and
9499    /// make the outcome depend on where the book happened to be.
9500    #[test]
9501    fn a_stale_deadline_retires_an_order_the_book_would_have_filled() {
9502        let mut venue = make_market_venue("10", "1000000");
9503        advance(&mut venue, time(9));
9504        assert!(
9505            venue
9506                .apply_market(
9507                    &instrument_name(),
9508                    MarketObservation::new(traded("47000")),
9509                    time(9)
9510                )
9511                .is_empty()
9512        );
9513
9514        let outcome = venue.open_order(limit_request(
9515            "stale",
9516            Side::Buy,
9517            "1",
9518            "48000",
9519            TimeInForce::GoodTillDate { expiry: time(5) },
9520        ));
9521
9522        assert!(
9523            matches!(
9524                outcome.response.state,
9525                OrderState::Inactive(InactiveOrderState::Expired(_))
9526            ),
9527            "a marketable order past its deadline must expire, not fill: {:?}",
9528            outcome.response.state
9529        );
9530        assert!(
9531            venue.trades(time(0)).is_empty(),
9532            "it must not trade on its way out"
9533        );
9534    }
9535
9536    /// A cancel arriving after its order's deadline is told *why* it found nothing.
9537    #[test]
9538    fn a_cancel_after_a_deadline_is_answered_already_expired() {
9539        let mut venue = make_market_venue("10", "1000000");
9540        advance(&mut venue, time(1));
9541
9542        let _ = venue.open_order(limit_request(
9543            "gtd",
9544            Side::Buy,
9545            "1",
9546            "48000",
9547            TimeInForce::GoodTillDate { expiry: time(5) },
9548        ));
9549        assert_eq!(venue.advance_time(time(6)).len(), 2);
9550
9551        let outcome = venue.cancel_order(OrderEvent {
9552            key: OrderKey {
9553                exchange: EXCHANGE,
9554                instrument: instrument_name(),
9555                strategy: StrategyId::new("test"),
9556                cid: ClientOrderId::new("gtd"),
9557            },
9558            state: RequestCancel { id: None },
9559        });
9560
9561        assert_eq!(
9562            outcome.response.state,
9563            Err(OrderError::Rejected(ApiError::OrderAlreadyExpired)),
9564            "a cancel that lost to a deadline must not read as an unknown order"
9565        );
9566    }
9567
9568    /// An expired order is still part of the account, and a later snapshot reports it.
9569    #[test]
9570    fn an_expired_order_enters_the_account_snapshot() {
9571        let mut venue = make_market_venue("10", "1000000");
9572        advance(&mut venue, time(1));
9573
9574        let _ = venue.open_order(limit_request(
9575            "gtd",
9576            Side::Buy,
9577            "1",
9578            "48000",
9579            TimeInForce::GoodTillDate { expiry: time(5) },
9580        ));
9581        assert_eq!(venue.advance_time(time(5)).len(), 2);
9582
9583        let snapshot = venue.account_snapshot();
9584        let orders = &snapshot.instruments[0].orders;
9585        assert_eq!(orders.len(), 1);
9586        assert!(matches!(
9587            orders[0].state,
9588            OrderState::Inactive(InactiveOrderState::Expired(_))
9589        ));
9590    }
9591
9592    /// Deadlines are swept in deadline order, so a run's events do not depend on map iteration.
9593    #[test]
9594    fn deadlines_are_swept_earliest_first() {
9595        let mut venue = make_market_venue("10", "1000000");
9596        advance(&mut venue, time(1));
9597
9598        // Booked latest-deadline first, so insertion order is the opposite of sweep order.
9599        for (cid, expiry) in [("late", time(7)), ("early", time(5)), ("middle", time(6))] {
9600            let _ = venue.open_order(limit_request(
9601                cid,
9602                Side::Buy,
9603                "1",
9604                "48000",
9605                TimeInForce::GoodTillDate { expiry },
9606            ));
9607        }
9608
9609        let events = venue.advance_time(time(9));
9610
9611        // Each retirement is a release then a snapshot, so the orders are at the odd indices.
9612        let swept: Vec<_> = events
9613            .iter()
9614            .skip(1)
9615            .step_by(2)
9616            .map(|event| order_of(event).key.cid.0.to_string())
9617            .collect();
9618
9619        assert_eq!(swept, ["early", "middle", "late"]);
9620    }
9621
9622    /// A deadline is a property of the clock, not of a book: an order on an instrument that never
9623    /// ticks again is still retired by activity elsewhere on the venue.
9624    #[test]
9625    fn a_deadline_on_a_quiet_instrument_is_swept_by_a_tick_on_another() {
9626        let mut venue = make_market_venue("10", "1000000");
9627        advance(&mut venue, time(1));
9628
9629        let _ = venue.open_order(limit_request(
9630            "gtd",
9631            Side::Buy,
9632            "1",
9633            "48000",
9634            TimeInForce::GoodTillDate { expiry: time(5) },
9635        ));
9636
9637        // A market for an instrument this venue holds no order on.
9638        let elsewhere = InstrumentNameExchange::new("eth_usdt");
9639        let events = venue.apply_market(
9640            &elsewhere,
9641            MarketObservation::new(book("2000", "2001")),
9642            time(6),
9643        );
9644
9645        assert_eq!(
9646            events.len(),
9647            2,
9648            "the quiet instrument's order still retires"
9649        );
9650        assert!(venue.orders_open(&[]).is_empty());
9651    }
9652
9653    /// An order the venue took nothing for releases nothing, exactly as cancelling one does.
9654    #[test]
9655    fn expiring_an_order_seeded_with_no_reservation_restates_no_balance() {
9656        let expiry = time(5);
9657        let seeded = Order {
9658            key: OrderKey {
9659                exchange: EXCHANGE,
9660                instrument: instrument_name(),
9661                strategy: StrategyId::new("test"),
9662                cid: ClientOrderId::new("seeded"),
9663            },
9664            side: Side::Buy,
9665            price: Some(d("48000")),
9666            quantity: d("1"),
9667            kind: OrderKind::Limit,
9668            time_in_force: TimeInForce::GoodTillDate { expiry },
9669            state: OrderState::active(Open::new(
9670                VenueOrderId::Assigned(OrderId::new("seeded")),
9671                time(1),
9672                Decimal::ZERO,
9673            )),
9674        };
9675
9676        let mut venue = make_market_venue("10", "1000000");
9677        assert!(
9678            venue
9679                .account
9680                .book(
9681                    as_open(seeded).expect("the seeded order is Open"),
9682                    // As `initial_state` seeds one: the venue never took anything for it.
9683                    None,
9684                )
9685                .is_ok(),
9686            "the book is vacant under this id"
9687        );
9688
9689        let events = venue.advance_time(expiry);
9690
9691        assert_eq!(events.len(), 1, "the terminal snapshot alone, no release");
9692        assert!(matches!(
9693            order_of(&events[0]).state,
9694            OrderState::Inactive(InactiveOrderState::Expired(_))
9695        ));
9696    }
9697
9698    // --- Stage 4: market orders are priced from the book they arrive to ---------------------
9699
9700    /// A market-driven venue prices a market order from its own book, ignoring the requester's.
9701    ///
9702    /// The request carries a deliberately different snapshot, so a fill at the venue's price can
9703    /// only have come from the venue's own market — the two are far enough apart that no fill
9704    /// model or rounding could confuse them.
9705    #[test]
9706    fn a_market_driven_venue_prices_a_market_order_from_its_own_book() {
9707        let mut venue = make_market_venue("10", "1000000");
9708        advance(&mut venue, time(1));
9709        assert!(
9710            venue
9711                .apply_market(
9712                    &instrument_name(),
9713                    MarketObservation::new(traded("49000")),
9714                    time(1)
9715                )
9716                .is_empty()
9717        );
9718
9719        // What the sender was looking at when it decided, which is not what the venue holds.
9720        let outcome = venue.open_order(buy_request("1.0", Some(traded("40000"))));
9721
9722        assert_eq!(
9723            filled_price(&outcome.response),
9724            d("49000"),
9725            "the fill is struck against the venue's book, not the snapshot the request carried"
9726        );
9727    }
9728
9729    /// The same request on a request-priced venue still fills at the snapshot it carried.
9730    ///
9731    /// The regime is the whole of the difference, which is why `MockExchange`'s results do not
9732    /// move: it drives a `RequestPriced` venue and reads `market()` as `None` forever.
9733    #[test]
9734    fn a_request_priced_venue_still_prices_a_market_order_from_the_request() {
9735        let mut venue = make_venue("10", "1000000");
9736
9737        let outcome = venue.open_order(buy_request("1.0", Some(traded("40000"))));
9738
9739        assert_eq!(
9740            filled_price(&outcome.response),
9741            d("40000"),
9742            "with no book of its own, the request's snapshot is still the only price source"
9743        );
9744    }
9745
9746    /// A market-driven venue that has never been fed an instrument rejects a market order in it
9747    /// rather than reaching for the requester's snapshot.
9748    ///
9749    /// Falling back would make the fill depend on which of the two happened to hold a price, and
9750    /// would re-admit the request-priced behaviour the regime exists to distinguish. The request
9751    /// here carries a perfectly good snapshot, and is refused anyway.
9752    #[test]
9753    fn a_market_driven_venue_with_no_book_rejects_a_market_order_rather_than_using_the_request() {
9754        let mut venue = make_market_venue("10", "1000000");
9755        advance(&mut venue, time(1));
9756
9757        let outcome = venue.open_order(buy_request("1.0", Some(traded("49000"))));
9758
9759        assert!(outcome.events.is_empty(), "nothing was debited or traded");
9760        match outcome.response.state {
9761            OrderState::Inactive(InactiveOrderState::OpenFailed(
9762                UnindexedOrderError::Rejected(ApiError::OrderRejected(ref reason)),
9763            )) => assert!(
9764                reason.contains("no market snapshot"),
9765                "the rejection must name the venue's absent market, got: {reason}"
9766            ),
9767            ref other => panic!("an unpriceable market order must be rejected, got: {other:?}"),
9768        }
9769    }
9770
9771    // ---------------------------------------------------------------------------------------
9772    // A taker takes only what is on offer.
9773    // ---------------------------------------------------------------------------------------
9774
9775    /// The same size on both sides, so a test need only say how much.
9776    fn sized(amount: &str) -> MarketDepth {
9777        MarketDepth::new(Some(d(amount)), Some(d(amount)))
9778    }
9779
9780    /// A venue fed one book carrying `depth`, priced off that book rather than off a last trade.
9781    ///
9782    /// The offer is 47900 throughout, so a buy limited at 48000 is marketable and prints at 47900.
9783    fn venue_offering(depth: MarketDepth) -> SimulatedVenue {
9784        venue_offering_funded("1000000", depth)
9785    }
9786
9787    fn venue_offering_funded(usdt: &str, depth: MarketDepth) -> SimulatedVenue {
9788        let mut venue = make_market_venue_with(
9789            "10",
9790            usdt,
9791            maker_taker_fee(),
9792            SimFillConfig::BidAsk(BidAskFillModel),
9793        );
9794        advance(&mut venue, time(1));
9795        assert!(
9796            venue
9797                .apply_market(
9798                    &instrument_name(),
9799                    MarketObservation::new(book("47800", "47900")).with_depth(depth),
9800                    time(1)
9801                )
9802                .is_empty(),
9803            "an empty book fills nothing"
9804        );
9805        venue
9806    }
9807
9808    fn free_quote(venue: &SimulatedVenue) -> Decimal {
9809        venue.balances(&[quote()])[0].balance.free
9810    }
9811
9812    fn total_quote(venue: &SimulatedVenue) -> Decimal {
9813        venue.balances(&[quote()])[0].balance.total
9814    }
9815
9816    /// A market order has no limit price, so what the book could not fill has nowhere to wait.
9817    #[test]
9818    fn a_market_order_fills_what_the_book_has_and_cancels_what_it_has_not() {
9819        let mut venue = venue_offering(sized("0.4"));
9820
9821        let outcome = venue.open_order(buy_request("1", None));
9822
9823        assert_eq!(
9824            outcome.events.len(),
9825            3,
9826            "a fill owes both balances, then the trade"
9827        );
9828        let trade = filled_trade(&outcome);
9829        assert_eq!(
9830            trade.quantity,
9831            d("0.4"),
9832            "it took the whole offer and no more"
9833        );
9834        assert_eq!(trade.price, d("47900"));
9835
9836        match &outcome.response.state {
9837            OrderState::Inactive(InactiveOrderState::Cancelled(cancelled)) => assert_eq!(
9838                cancelled.filled_quantity,
9839                Some(d("0.4")),
9840                "the remainder retires carrying what did trade"
9841            ),
9842            other => panic!("a market order's unfillable remainder is cancelled, got: {other:?}"),
9843        }
9844        assert!(
9845            venue.orders_open(&[]).is_empty(),
9846            "a market order has no price to rest the remainder at"
9847        );
9848    }
9849
9850    /// The condition under which the two coincided has expired, which is this test.
9851    #[test]
9852    fn fill_or_kill_refuses_the_partial_fill_immediate_or_cancel_accepts() {
9853        // One book, one size, one order -- only the time in force differs.
9854        let mut immediate = venue_offering(sized("0.4"));
9855        let mut all_or_nothing = venue_offering(sized("0.4"));
9856
9857        let ioc = immediate.open_order(limit_request(
9858            "ioc",
9859            Side::Buy,
9860            "1",
9861            "48000",
9862            TimeInForce::ImmediateOrCancel,
9863        ));
9864        let fok = all_or_nothing.open_order(limit_request(
9865            "fok",
9866            Side::Buy,
9867            "1",
9868            "48000",
9869            TimeInForce::FillOrKill,
9870        ));
9871
9872        assert_eq!(
9873            filled_trade(&ioc).quantity,
9874            d("0.4"),
9875            "immediate-or-cancel keeps what the book could give it"
9876        );
9877
9878        assert!(
9879            fok.events.is_empty(),
9880            "fill-or-kill would rather trade nothing than trade in part"
9881        );
9882        assert!(
9883            all_or_nothing.trades(time(0)).is_empty(),
9884            "and nothing reached the ledger either"
9885        );
9886        assert_eq!(
9887            free_quote(&all_or_nothing),
9888            d("1000000"),
9889            "a killed order moves no balance"
9890        );
9891    }
9892
9893    /// One arrival, one balance -- whatever it did with the part the book could not fill.
9894    #[test]
9895    fn a_capped_limit_order_fills_what_it_can_and_rests_the_remainder() {
9896        let mut venue = venue_offering(sized("0.4"));
9897
9898        let outcome = venue.open_order(limit_request("split", Side::Buy, "1", "48000", gtc()));
9899
9900        assert_eq!(
9901            outcome.events.len(),
9902            3,
9903            "one arrival restates each asset once and prints one trade, even settling and holding \
9904             at once"
9905        );
9906        let trade = filled_trade(&outcome);
9907        assert_eq!(trade.quantity, d("0.4"));
9908        assert_eq!(
9909            trade.price,
9910            d("47900"),
9911            "the part that traded took the offer"
9912        );
9913
9914        let open = rested_open(&outcome);
9915        assert_eq!(
9916            open.filled_quantity,
9917            d("0.4"),
9918            "the remainder rests carrying what has already traded"
9919        );
9920        assert_eq!(open.quantity_remaining(d("1")), d("0.6"));
9921        assert_eq!(venue.orders_open(&[]).len(), 1);
9922
9923        // Settled the taker leg, still holding the maker one: 19179.16 spent, 28805.76 held.
9924        assert_eq!(total_quote(&venue), d("1000000") - d("19179.16"));
9925        assert_eq!(
9926            free_quote(&venue),
9927            d("1000000") - d("19179.16") - d("28805.76")
9928        );
9929    }
9930
9931    /// The sell side of the same arrival: it pays with base and receives quote, so the base it
9932    /// settles and the base it holds for the remainder are one commitment, and the quote it
9933    /// receives for the part that traded funds neither.
9934    #[test]
9935    fn a_capped_spot_sell_settles_and_holds_base_and_receives_quote_for_what_traded() {
9936        let mut venue = venue_offering(sized("0.4"));
9937
9938        let outcome = venue.open_order(limit_request("split", Side::Sell, "1", "47700", gtc()));
9939
9940        assert_eq!(
9941            outcome.events.len(),
9942            3,
9943            "the base paid, the quote received, then the trade"
9944        );
9945        let trade = filled_trade(&outcome);
9946        assert_eq!(trade.quantity, d("0.4"));
9947        assert_eq!(trade.price, d("47800"), "the part that traded took the bid");
9948
9949        // 0.4 delivered, 0.6 held for the remainder resting at its 47700 limit.
9950        let paid = filled_balance(&outcome);
9951        assert_eq!(paid.asset, base());
9952        assert_eq!(paid.balance, Balance::new(d("9.6"), d("9")));
9953
9954        // 0.4 at 47800 is 19120, less the 10bp taker fee of 19.12.
9955        let received = filled_received(&outcome);
9956        assert_eq!(received.asset, quote());
9957        assert_eq!(
9958            received.balance,
9959            Balance::new(d("1019100.88"), d("1019100.88"))
9960        );
9961        assert_eq!(rested_open(&outcome).quantity_remaining(d("1")), d("0.6"));
9962    }
9963
9964    /// An order that fills in part, rests, and is later crossed settles exactly the remainder --
9965    /// twice in total, summing to the original, with no third restatement.
9966    #[test]
9967    fn an_order_filled_in_part_then_crossed_settles_the_remainder_and_no_more() {
9968        let mut venue = venue_offering(sized("0.4"));
9969
9970        let arrival = venue.open_order(limit_request("split", Side::Buy, "1", "48000", gtc()));
9971        let taker = filled_trade(&arrival).clone();
9972
9973        // The market reaches what is resting.
9974        let fills = venue.apply_market(
9975            &instrument_name(),
9976            MarketObservation::new(book("48000", "48000")),
9977            time(2),
9978        );
9979        assert_eq!(
9980            fills.len(),
9981            4,
9982            "a resting fill owes both balances, the trade, the order"
9983        );
9984        let maker = trade_of(&fills[2]);
9985
9986        assert_eq!(maker.quantity, d("0.6"), "only the remainder crosses");
9987        assert_eq!(
9988            maker.price,
9989            d("48000"),
9990            "a maker is paid the price it quoted, not the one the book moved to"
9991        );
9992        assert_eq!(
9993            taker.quantity + maker.quantity,
9994            d("1"),
9995            "the two fills are the whole order, and no more than it"
9996        );
9997
9998        assert_ne!(
9999            taker.id, maker.id,
10000            "one order printing twice must print two distinguishable trades"
10001        );
10002        assert_eq!(
10003            taker.order_id, maker.order_id,
10004            "and both must still say which order they belong to"
10005        );
10006
10007        // 0.4 taken at 47900 plus 10bp, then 0.6 quoted at 48000 plus 2bp. Nothing else moved.
10008        let spent = d("19179.16") + d("28805.76");
10009        assert_eq!(total_quote(&venue), d("1000000") - spent);
10010        assert_eq!(
10011            free_quote(&venue),
10012            d("1000000") - spent,
10013            "the order is done, so nothing is still held against it"
10014        );
10015    }
10016
10017    /// Reading a stored size does not consume it -- only filling against it does.
10018    #[test]
10019    fn a_second_order_on_one_observation_sees_only_what_the_first_left() {
10020        let mut venue = venue_offering(sized("1"));
10021
10022        let first = venue.open_order(limit_request(
10023            "first",
10024            Side::Buy,
10025            "0.6",
10026            "48000",
10027            TimeInForce::ImmediateOrCancel,
10028        ));
10029        assert_eq!(filled_trade(&first).quantity, d("0.6"));
10030
10031        let second = venue.open_order(limit_request(
10032            "second",
10033            Side::Buy,
10034            "0.6",
10035            "48000",
10036            TimeInForce::ImmediateOrCancel,
10037        ));
10038        assert_eq!(
10039            filled_trade(&second).quantity,
10040            d("0.4"),
10041            "the offer was displayed once, so the two of them share it"
10042        );
10043
10044        let third = venue.open_order(limit_request(
10045            "third",
10046            Side::Buy,
10047            "0.6",
10048            "48000",
10049            TimeInForce::ImmediateOrCancel,
10050        ));
10051        assert!(
10052            third.events.is_empty(),
10053            "the offer is exhausted until the next observation replaces it"
10054        );
10055    }
10056
10057    /// A new observation states what is on offer now; it is not an increment on what was.
10058    #[test]
10059    fn a_new_observation_replaces_the_remaining_size_rather_than_adding_to_it() {
10060        let mut venue = venue_offering(sized("1"));
10061
10062        let taken = venue.open_order(limit_request(
10063            "first",
10064            Side::Buy,
10065            "0.6",
10066            "48000",
10067            TimeInForce::ImmediateOrCancel,
10068        ));
10069        assert_eq!(filled_trade(&taken).quantity, d("0.6"));
10070
10071        assert!(
10072            venue
10073                .apply_market(
10074                    &instrument_name(),
10075                    MarketObservation::new(book("47800", "47900")).with_depth(sized("1")),
10076                    time(2)
10077                )
10078                .is_empty()
10079        );
10080
10081        let after = venue.open_order(limit_request(
10082            "second",
10083            Side::Buy,
10084            "1",
10085            "48000",
10086            TimeInForce::ImmediateOrCancel,
10087        ));
10088        assert_eq!(
10089            filled_trade(&after).quantity,
10090            d("1"),
10091            "the fresh observation offers a whole unit again"
10092        );
10093    }
10094
10095    /// No size information is not no size. A price-only feed must keep filling exactly as it did.
10096    #[test]
10097    fn a_feed_supplying_no_size_caps_nothing() {
10098        let mut venue = venue_offering(MarketDepth::UNKNOWN);
10099
10100        let outcome = venue.open_order(limit_request(
10101            "uncapped",
10102            Side::Buy,
10103            "5",
10104            "48000",
10105            TimeInForce::ImmediateOrCancel,
10106        ));
10107
10108        assert_eq!(
10109            filled_trade(&outcome).quantity,
10110            d("5"),
10111            "an absent size caps nothing, however large the order"
10112        );
10113    }
10114
10115    /// A reported *absence* of size is the other thing, and it does stop a fill.
10116    #[test]
10117    fn a_marketable_order_with_nothing_on_offer_rests_rather_than_filling() {
10118        let mut venue = venue_offering(MarketDepth::new(Some(d("1")), Some(Decimal::ZERO)));
10119
10120        let outcome = venue.open_order(limit_request("nothing", Side::Buy, "1", "48000", gtc()));
10121
10122        assert_eq!(
10123            outcome.events.len(),
10124            1,
10125            "nothing traded, so it owes only the hold it took"
10126        );
10127        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
10128    }
10129
10130    /// A capped order costs **more** than the same order filling outright: its remainder is held at
10131    /// the order's own limit while the fill struck a better price. So an account can afford the
10132    /// whole order and still not afford the split -- and it is refused whole rather than filled for
10133    /// the part it could pay for, which would be this venue choosing a disposition nobody asked for.
10134    #[test]
10135    fn an_order_that_cannot_fund_both_legs_is_refused_with_nothing_moved() {
10136        // 47947.90 buys the whole of it at the offer; the split wants 47984.92.
10137        const FUNDED: &str = "47950";
10138
10139        let mut uncapped = venue_offering_funded(FUNDED, MarketDepth::UNKNOWN);
10140        let affordable =
10141            uncapped.open_order(limit_request("whole", Side::Buy, "1", "48000", gtc()));
10142        assert_eq!(
10143            filled_trade(&affordable).quantity,
10144            d("1"),
10145            "uncapped, this very order is affordable and fills outright"
10146        );
10147
10148        let mut venue = venue_offering_funded(FUNDED, sized("0.4"));
10149        let outcome = venue.open_order(limit_request("split", Side::Buy, "1", "48000", gtc()));
10150
10151        assert!(outcome.events.is_empty(), "a refused order moves nothing");
10152        assert!(
10153            matches!(
10154                outcome.response.state,
10155                OrderState::Inactive(InactiveOrderState::OpenFailed(_))
10156            ),
10157            "and says so, rather than filling the part it could afford: {:?}",
10158            outcome.response.state
10159        );
10160        assert_eq!(free_quote(&venue), d(FUNDED), "the ledger is as it was");
10161        assert_eq!(total_quote(&venue), d(FUNDED));
10162        assert!(venue.trades(time(0)).is_empty(), "and nothing traded");
10163        assert!(venue.orders_open(&[]).is_empty());
10164    }
10165
10166    fn is_duplicate_cid(state: &UnindexedOrderState) -> bool {
10167        matches!(
10168            state,
10169            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
10170                ApiError::DuplicateClientOrderId(_)
10171            )))
10172        )
10173    }
10174
10175    /// An open under the id of a resting order is rejected, and the resting order keeps its size,
10176    /// its place and what is held against it.
10177    #[test]
10178    fn an_open_under_a_resting_id_is_rejected_and_the_resting_order_is_untouched() {
10179        let (mut venue, _) = venue_resting_one_buy("48000");
10180        let free = free_quote(&venue);
10181
10182        // Same cid, a different size and price.
10183        let second = venue.open_order(limit_request("resting", Side::Buy, "0.5", "47000", gtc()));
10184
10185        assert!(
10186            is_duplicate_cid(&second.response.state),
10187            "rejected as a duplicate: {:?}",
10188            second.response.state
10189        );
10190        assert!(second.events.is_empty(), "a rejection moves nothing");
10191        let [resting] = &venue.orders_open(&[])[..] else {
10192            panic!("one cid, one resting order");
10193        };
10194        assert_eq!(
10195            (resting.quantity, resting.price),
10196            (d("1"), Some(d("48000"))),
10197            "the resting order is the first one"
10198        );
10199        assert_eq!(
10200            free_quote(&venue),
10201            free,
10202            "its hold is still held, and only its"
10203        );
10204        assert_eq!(total_quote(&venue), d("1000000"));
10205    }
10206
10207    /// The check covers every kind: a market order under a resting id is rejected before it can
10208    /// trade, although it would never have rested itself.
10209    #[test]
10210    fn a_market_order_under_a_resting_id_is_rejected_before_it_trades() {
10211        let (mut venue, _) = venue_resting_one_buy("48000");
10212        let mut market = buy_request("0.1", None);
10213        market.key.cid = ClientOrderId::new("resting");
10214
10215        let outcome = venue.open_order(market);
10216
10217        assert!(
10218            is_duplicate_cid(&outcome.response.state),
10219            "rejected as a duplicate: {:?}",
10220            outcome.response.state
10221        );
10222        assert!(venue.trades(time(0)).is_empty(), "nothing traded");
10223        assert_eq!(venue.orders_open(&[]).len(), 1);
10224    }
10225
10226    /// A malformed request is reported as malformed, even under a resting id.
10227    #[test]
10228    fn a_malformed_open_under_a_resting_id_is_rejected_as_malformed() {
10229        let (mut venue, _) = venue_resting_one_buy("48000");
10230
10231        let outcome = venue.open_order(limit_request(
10232            "resting",
10233            Side::Buy,
10234            "1",
10235            "47000",
10236            TimeInForce::AtOpen,
10237        ));
10238
10239        assert!(
10240            matches!(
10241                outcome.response.state,
10242                OrderState::Inactive(InactiveOrderState::OpenFailed(_))
10243            ) && !is_duplicate_cid(&outcome.response.state),
10244            "rejected on its time in force: {:?}",
10245            outcome.response.state
10246        );
10247    }
10248
10249    /// Once an order is cancelled its id is free, and what the venue reports under the id is the
10250    /// latest order: still working while it rests, cancelled once it is.
10251    #[test]
10252    fn a_cancelled_orders_id_is_free_again_and_the_latest_order_is_reported() {
10253        let (mut venue, _) = venue_resting_one_buy("48000");
10254        let cancel = |venue: &mut SimulatedVenue| {
10255            venue.cancel_order(OrderRequestCancel {
10256                key: limit_request("resting", Side::Buy, "1", "48000", gtc()).key,
10257                state: RequestCancel { id: None },
10258            })
10259        };
10260        assert!(cancel(&mut venue).response.state.is_ok());
10261
10262        let second = venue.open_order(limit_request("resting", Side::Buy, "0.5", "47000", gtc()));
10263
10264        assert!(
10265            matches!(
10266                second.response.state,
10267                OrderState::Active(ActiveOrderState::Open(_))
10268            ),
10269            "the id is free again: {:?}",
10270            second.response.state
10271        );
10272        assert!(
10273            venue.orders_ended(&[key_of("resting")]).is_empty(),
10274            "the order under the id is working, whatever became of the one before it"
10275        );
10276        let snapshot = venue.account_snapshot();
10277        let listed = snapshot
10278            .instruments
10279            .iter()
10280            .flat_map(|instrument| &instrument.orders)
10281            .filter(|order| order.key.cid == ClientOrderId::new("resting"))
10282            .count();
10283        assert_eq!(
10284            listed, 1,
10285            "a snapshot lists the id once, as the working order"
10286        );
10287
10288        assert!(
10289            cancel(&mut venue).response.state.is_ok(),
10290            "and the cancel reaches it"
10291        );
10292        let [ended] = &venue.orders_ended(&[key_of("resting")])[..] else {
10293            panic!("the second order has ended");
10294        };
10295        assert_eq!(ended.quantity, d("0.5"), "the latest order under the id");
10296    }
10297
10298    /// Once an order fills its id is free: an order under it rests, and a cancel reaches that
10299    /// order instead of being refused as "already filled".
10300    #[test]
10301    fn a_filled_orders_id_is_free_again() {
10302        let (mut venue, _) = venue_resting_one_buy("48000");
10303        let filled = venue.open_order(limit_request("reused", Side::Buy, "1", "49100", gtc()));
10304        assert!(
10305            matches!(
10306                filled.response.state,
10307                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
10308            ),
10309            "crosses the ask, so fills on arrival"
10310        );
10311
10312        let second = venue.open_order(limit_request("reused", Side::Buy, "1", "47000", gtc()));
10313        assert!(
10314            matches!(
10315                second.response.state,
10316                OrderState::Active(ActiveOrderState::Open(_))
10317            ),
10318            "the id is free again: {:?}",
10319            second.response.state
10320        );
10321
10322        let cancelled = venue.cancel_order(OrderRequestCancel {
10323            key: limit_request("reused", Side::Buy, "1", "47000", gtc()).key,
10324            state: RequestCancel { id: None },
10325        });
10326        assert!(
10327            cancelled.response.state.is_ok(),
10328            "the cancel reaches the working order: {:?}",
10329            cancelled.response.state
10330        );
10331    }
10332
10333    // --- How an order ended -------------------------------------------------------------------
10334
10335    fn key_of(cid: &str) -> UnindexedOrderKey {
10336        OrderKey {
10337            exchange: EXCHANGE,
10338            instrument: instrument_name(),
10339            strategy: StrategyId::new("asker"),
10340            cid: ClientOrderId::new(cid),
10341        }
10342    }
10343
10344    /// Every way an order can end on this venue is reported, under the key it was asked for, and
10345    /// an order still working or never held is left out.
10346    #[test]
10347    fn orders_ended_reports_each_ended_order_and_omits_the_rest() {
10348        let mut venue = make_market_venue("10", "1000000");
10349        advance(&mut venue, time(1));
10350        assert!(
10351            venue
10352                .apply_market(
10353                    &instrument_name(),
10354                    MarketObservation::new(book("49000", "49100")),
10355                    time(1)
10356                )
10357                .is_empty()
10358        );
10359
10360        let filled = venue.open_order(limit_request("filled", Side::Buy, "1", "49100", gtc()));
10361        assert!(
10362            matches!(
10363                filled.response.state,
10364                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
10365            ),
10366            "crosses the ask, so fills on arrival"
10367        );
10368        let _ = venue.open_order(limit_request("working", Side::Buy, "1", "48000", gtc()));
10369        let _ = venue.open_order(limit_request("cancelled", Side::Buy, "1", "47500", gtc()));
10370        let _ = venue.cancel_order(OrderEvent {
10371            key: key_of("cancelled"),
10372            state: RequestCancel { id: None },
10373        });
10374        let _ = venue.open_order(limit_request(
10375            "expired",
10376            Side::Buy,
10377            "1",
10378            "47000",
10379            TimeInForce::GoodTillDate { expiry: time(9) },
10380        ));
10381        assert_eq!(
10382            venue.advance_time(time(9)).len(),
10383            2,
10384            "the deadline retires it"
10385        );
10386
10387        let asked = ["filled", "working", "cancelled", "expired", "unknown"].map(key_of);
10388        let ended = venue.orders_ended(&asked);
10389
10390        assert_eq!(ended.len(), 3, "exactly the three ended orders: {ended:?}");
10391        assert!(
10392            ended
10393                .iter()
10394                .all(|order| order.key.strategy == StrategyId::new("asker")),
10395            "each comes back under the key it was asked for"
10396        );
10397        let ended_as = |cid: &str| {
10398            let Some(order) = ended
10399                .iter()
10400                .find(|order| order.key.cid == ClientOrderId::new(cid))
10401            else {
10402                panic!("{cid} is reported: {ended:?}");
10403            };
10404            order
10405        };
10406        let (filled, cancelled, expired) = (
10407            ended_as("filled"),
10408            ended_as("cancelled"),
10409            ended_as("expired"),
10410        );
10411        match &filled.state {
10412            InactiveOrderState::FullyFilled(state) => {
10413                assert_eq!(state.filled_quantity, d("1"));
10414                assert_eq!(state.avg_price, Some(d("49100")));
10415            }
10416            other => panic!("filled, got {other:?}"),
10417        }
10418        match &cancelled.state {
10419            InactiveOrderState::Cancelled(state) => {
10420                assert_eq!(state.filled_quantity, Some(Decimal::ZERO))
10421            }
10422            other => panic!("cancelled, got {other:?}"),
10423        }
10424        match &expired.state {
10425            InactiveOrderState::Expired(state) => assert_eq!(state.time_exchange, time(9)),
10426            other => panic!("expired, got {other:?}"),
10427        }
10428
10429        assert!(
10430            venue.orders_ended(&[]).is_empty(),
10431            "an empty request asks about nothing, not about everything"
10432        );
10433    }
10434
10435    /// A market order's unfillable remainder retires as a cancel carrying what did trade, and a
10436    /// lookup reports it so.
10437    #[test]
10438    fn orders_ended_reports_a_market_remainder_as_cancelled_with_its_fill() {
10439        let mut venue = venue_offering(sized("0.4"));
10440        let outcome = venue.open_order(buy_request("1", None));
10441
10442        let ended = venue.orders_ended(std::slice::from_ref(&outcome.response.key));
10443
10444        let [order] = ended.as_slice() else {
10445            panic!("the remainder's order is reported: {ended:?}");
10446        };
10447        match &order.state {
10448            InactiveOrderState::Cancelled(state) => {
10449                assert_eq!(state.filled_quantity, Some(d("0.4")))
10450            }
10451            other => panic!("cancelled carrying its fill, got {other:?}"),
10452        }
10453    }
10454
10455    /// A resting order filled from the book is reported with the price it filled at, and one that
10456    /// reached the book part-filled with no average, as its own fill reported.
10457    #[test]
10458    fn orders_ended_reports_a_resting_fill_and_its_average_price_as_the_fill_did() {
10459        let (mut venue, _) = venue_resting_one_buy("48000");
10460        assert!(
10461            venue
10462                .account
10463                .book(seeded_part_filled("48000", "1", "0.4"), None)
10464                .is_ok(),
10465            "the book is vacant under this id"
10466        );
10467        advance(&mut venue, time(2));
10468        let events = venue.apply_market(
10469            &instrument_name(),
10470            MarketObservation::new(book("47800", "47900")),
10471            time(2),
10472        );
10473        assert_eq!(
10474            events.len(),
10475            8,
10476            "two fills, each two balances, a trade and an order"
10477        );
10478
10479        let ended = venue.orders_ended(&[key_of("resting"), key_of("part_filled")]);
10480
10481        let avg_price_of = |cid: &str| {
10482            let Some(order) = ended
10483                .iter()
10484                .find(|order| order.key.cid == ClientOrderId::new(cid))
10485            else {
10486                panic!("{cid} is reported: {ended:?}");
10487            };
10488            match &order.state {
10489                InactiveOrderState::FullyFilled(filled) => {
10490                    assert_eq!(filled.filled_quantity, d("1"));
10491                    filled.avg_price
10492                }
10493                other => panic!("{cid} filled, got {other:?}"),
10494            }
10495        };
10496        assert_eq!(avg_price_of("resting"), Some(d("48000")));
10497        assert_eq!(
10498            avg_price_of("part_filled"),
10499            None,
10500            "this venue saw only one of the two fills behind it"
10501        );
10502    }
10503
10504    /// An order the configured `initial_state` reports as cancelled is reported as such.
10505    #[test]
10506    fn orders_ended_reports_an_order_the_initial_state_seeded_as_cancelled() {
10507        let seeded = Order {
10508            key: key_of("seeded"),
10509            side: Side::Buy,
10510            price: Some(d("48000")),
10511            quantity: d("1"),
10512            kind: OrderKind::Limit,
10513            time_in_force: gtc(),
10514            state: OrderState::inactive(Cancelled {
10515                id: OrderId::new("seeded"),
10516                time_exchange: time(0),
10517                filled_quantity: Some(d("0.25")),
10518            }),
10519        };
10520        let venue = SimulatedVenue::new(
10521            &spot_config_holding("10", "1000000", seeded),
10522            spot_instruments(),
10523        );
10524
10525        let ended = venue.orders_ended(&[key_of("seeded")]);
10526
10527        let [order] = ended.as_slice() else {
10528            panic!("the seeded cancel is reported: {ended:?}");
10529        };
10530        match &order.state {
10531            InactiveOrderState::Cancelled(cancelled) => {
10532                assert_eq!(cancelled.filled_quantity, Some(d("0.25")))
10533            }
10534            other => panic!("cancelled, got {other:?}"),
10535        }
10536    }
10537
10538    /// An id asked about twice is reported once, under the first key that finds it, and a key
10539    /// naming another instrument does not find the order or stop a later key from finding it.
10540    #[test]
10541    fn orders_ended_reports_an_id_once_and_only_for_its_own_instrument() {
10542        let mut venue = venue_offering(sized("0.4"));
10543        let outcome = venue.open_order(buy_request("1", None));
10544        let key = outcome.response.key.clone();
10545        let other_instrument = OrderKey {
10546            instrument: InstrumentNameExchange::new("ETHUSDT"),
10547            ..key.clone()
10548        };
10549        let second_ask = OrderKey {
10550            strategy: StrategyId::new("second"),
10551            ..key.clone()
10552        };
10553
10554        let ended = venue.orders_ended(&[key.clone(), second_ask]);
10555        let [order] = ended.as_slice() else {
10556            panic!("one entry for one id: {ended:?}");
10557        };
10558        assert_eq!(order.key, key, "the first key that finds it");
10559
10560        let ended = venue.orders_ended(&[other_instrument.clone(), key.clone()]);
10561        let [order] = ended.as_slice() else {
10562            panic!("a key that finds nothing does not stop a later one: {ended:?}");
10563        };
10564        assert_eq!(order.key, key);
10565
10566        assert!(
10567            venue.orders_ended(&[other_instrument]).is_empty(),
10568            "a key for another instrument does not find it"
10569        );
10570    }
10571
10572    // --- Holding costs: charged at each boundary the clock reaches -----------------------------
10573
10574    /// Hourly funding on the CFD at `rate` per hour, so a boundary falls on every whole hour.
10575    fn hourly_funding(rate: &str) -> Arc<dyn HoldingCostModel> {
10576        Arc::new(
10577            FundingModel::new(TimeDelta::hours(1))
10578                .with_rates(cfd_instrument_name(), RateSeries::constant(d(rate))),
10579        )
10580    }
10581
10582    /// The `n`th whole hour after `time(0)`: `time(0)` is 2023-11-14T22:13:20Z.
10583    fn boundary(n: i64) -> DateTime<Utc> {
10584        time(2800 + 3600 * n)
10585    }
10586
10587    /// A market-driven CFD venue charging `models`, its clock first moved to `time(1)`.
10588    fn make_cfd_venue_charging(
10589        usd: &str,
10590        models: Vec<Arc<dyn HoldingCostModel>>,
10591    ) -> SimulatedVenue {
10592        let mut venue = SimulatedVenue::new_market_driven(
10593            &config_from_balances(vec![funded(self::usd(), d(usd))], FeeModelConfig::default()),
10594            instruments_of(cfd_instrument()),
10595        )
10596        .with_holding_costs(models);
10597        advance(&mut venue, time(1));
10598        venue
10599    }
10600
10601    /// The cash flows among `events`, each with the balance restated just before it.
10602    fn charges(
10603        events: &[UnindexedAccountEvent],
10604    ) -> Vec<(
10605        &AssetBalance<AssetNameExchange>,
10606        &CashFlow<AssetNameExchange, InstrumentNameExchange>,
10607    )> {
10608        sans_debt(events)
10609            .chunks(2)
10610            .map(
10611                |pair| match (&pair[0].kind, pair.get(1).map(|event| &event.kind)) {
10612                    (
10613                        AccountEventKind::BalanceSnapshot(balance),
10614                        Some(AccountEventKind::CashFlow(flow)),
10615                    ) => (&balance.0, flow),
10616                    other => panic!("a charge is [balance, cash flow], got: {other:?}"),
10617                },
10618            )
10619            .collect()
10620    }
10621
10622    /// The position is valued at the mid (5000), not the last trade (4000) it filled at: a long
10623    /// of 1 contract × 25 × 5000 pays 0.0001 of it, 12.5, as one restatement and one cash flow,
10624    /// both stamped at the boundary.
10625    #[test]
10626    fn a_position_held_across_a_boundary_is_charged_there_as_balance_then_cash_flow() {
10627        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
10628        assert!(cfd_market(&mut venue, "4990", "5010", "4000").is_empty());
10629        cfd_market_fill(&mut venue, Side::Buy, "1");
10630        assert_eq!(free_usd(&venue), d("100000"));
10631
10632        let events = venue.advance_time(boundary(0));
10633        let charged = charges(&events);
10634
10635        assert_eq!(charged.len(), 1);
10636        let (balance, flow) = charged[0];
10637        assert_eq!(balance.asset, usd());
10638        assert_eq!(balance.balance, Balance::new(d("99987.5"), d("99987.5")));
10639        assert_eq!(balance.time_exchange, boundary(0));
10640        assert_eq!(
10641            *flow,
10642            CashFlow::new(
10643                CashFlowKind::Funding {
10644                    rate: Some(d("0.0001")),
10645                    position_quantity: Some(d("1")),
10646                },
10647                usd(),
10648                d("-12.5"),
10649                Some(cfd_instrument_name()),
10650                boundary(0),
10651                Some(CashFlowId::new("0")),
10652            )
10653        );
10654        assert_eq!(total_usd(&venue), d("99987.5"));
10655    }
10656
10657    /// A clock that jumps over several boundaries charges each one, in time order and at its own
10658    /// stamp, and a clock moved to an instant already charged charges nothing again.
10659    #[test]
10660    fn a_gap_of_several_boundaries_is_caught_up_once_each() {
10661        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
10662        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10663        cfd_market_fill(&mut venue, Side::Sell, "1");
10664
10665        let events = venue.advance_time(boundary(2) + TimeDelta::seconds(1));
10666        let charged = charges(&events);
10667
10668        let stamps: Vec<_> = charged.iter().map(|(_, flow)| flow.time_exchange).collect();
10669        assert_eq!(stamps, vec![boundary(0), boundary(1), boundary(2)]);
10670        let ids: Vec<_> = charged.iter().map(|(_, flow)| flow.id.clone()).collect();
10671        assert_eq!(
10672            ids,
10673            ["0", "1", "2"].map(|id| Some(CashFlowId::new(id))).to_vec()
10674        );
10675        assert!(
10676            charged.iter().all(|(_, flow)| flow.amount == d("12.5")),
10677            "a short receives a positive funding rate"
10678        );
10679        assert_eq!(free_usd(&venue), d("75000") + d("37.5"));
10680
10681        assert!(
10682            venue
10683                .advance_time(boundary(2) + TimeDelta::seconds(1))
10684                .is_empty()
10685        );
10686        assert!(
10687            venue.advance_time(boundary(1)).is_empty(),
10688            "the clock moving back"
10689        );
10690        assert_eq!(free_usd(&venue), d("75000") + d("37.5"));
10691    }
10692
10693    /// An earlier instant is held at the latest one: it charges nothing, the balances keep their
10694    /// stamp, and the next boundary is charged once, as if the earlier instant had never come.
10695    #[test]
10696    fn an_earlier_instant_moves_neither_the_clock_nor_the_accrual_back() {
10697        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
10698        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10699        cfd_market_fill(&mut venue, Side::Sell, "1");
10700        let reached = boundary(1) + TimeDelta::seconds(1);
10701        assert_eq!(charges(&venue.advance_time(reached)).len(), 2);
10702        let free = free_usd(&venue);
10703
10704        assert!(
10705            venue.advance_time(boundary(0)).is_empty(),
10706            "nothing is charged again"
10707        );
10708        assert_eq!(venue.time_exchange(), reached);
10709        assert!(
10710            venue
10711                .account
10712                .balances()
10713                .all(|balance| balance.time_exchange == reached),
10714            "no balance is restamped earlier than the venue has reported"
10715        );
10716        assert_eq!(free_usd(&venue), free);
10717
10718        let events = venue.advance_time(boundary(2));
10719        let stamps: Vec<_> = charges(&events)
10720            .iter()
10721            .map(|(_, flow)| flow.time_exchange)
10722            .collect();
10723        assert_eq!(stamps, vec![boundary(2)]);
10724    }
10725
10726    /// Charging starts where the clock first moves, not at the epoch the clock starts at. A venue
10727    /// with no market values the position at its entry price.
10728    #[test]
10729    fn charging_starts_at_the_first_clock_move_and_values_at_entry_without_a_market() {
10730        let mut venue = make_cfd_venue("200000", FeeModelConfig::default())
10731            .with_holding_costs(vec![hourly_funding("0.0001")]);
10732        cfd_fill(&mut venue, Side::Buy, "1", "4000");
10733
10734        assert!(
10735            venue
10736                .advance_time(boundary(0) + TimeDelta::seconds(1))
10737                .is_empty(),
10738            "the first move charges nothing before it"
10739        );
10740
10741        let events = venue.advance_time(boundary(1));
10742        let charged = charges(&events);
10743        assert_eq!(charged.len(), 1);
10744        // 1 × 25 × 4000 × 0.0001, at the entry price.
10745        assert_eq!(charged[0].1.amount, d("-10"));
10746    }
10747
10748    /// At a boundary's instant the venue charges before it fills: a position opened there is not
10749    /// charged for it, and one closed there is.
10750    #[test]
10751    fn a_position_opened_at_a_boundary_is_not_charged_for_it_and_one_closed_there_is() {
10752        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
10753        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10754
10755        assert!(venue.advance_time(boundary(0)).is_empty());
10756        cfd_market_fill(&mut venue, Side::Buy, "1");
10757
10758        let events = venue.advance_time(boundary(1));
10759        let stamps: Vec<_> = charges(&events)
10760            .iter()
10761            .map(|(_, flow)| flow.time_exchange)
10762            .collect();
10763        assert_eq!(stamps, vec![boundary(1)]);
10764
10765        cfd_market_fill(&mut venue, Side::Sell, "1");
10766        assert!(venue.advance_time(boundary(3)).is_empty(), "flat");
10767    }
10768
10769    /// A tick that passes a boundary charges it at the market as it stood before the tick.
10770    #[test]
10771    fn a_market_tick_past_a_boundary_charges_it_at_the_market_before_the_tick() {
10772        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
10773        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10774        cfd_market_fill(&mut venue, Side::Buy, "1");
10775
10776        let six_thousand = MarketSnapshot {
10777            best_bid: Some(d("6000")),
10778            best_ask: Some(d("6000")),
10779            last_price: Some(d("6000")),
10780        };
10781        let events = venue.apply_market(
10782            &cfd_instrument_name(),
10783            MarketObservation::new(six_thousand),
10784            boundary(0) + TimeDelta::seconds(1),
10785        );
10786
10787        let charged = charges(&events);
10788        assert_eq!(charged.len(), 1);
10789        assert_eq!(charged[0].1.amount, d("-12.5"), "valued at 5000, not 6000");
10790    }
10791
10792    /// A charge the account cannot cover is still made: this venue models no liquidation to stop
10793    /// it, and leaving it out would understate the cost.
10794    #[test]
10795    fn a_charge_the_account_cannot_cover_takes_free_below_zero() {
10796        let mut venue = make_cfd_venue_charging("125000", vec![hourly_funding("0.0001")]);
10797        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10798        cfd_market_fill(&mut venue, Side::Buy, "1");
10799        assert_eq!(free_usd(&venue), Decimal::ZERO);
10800
10801        let events = venue.advance_time(boundary(0));
10802
10803        assert_eq!(
10804            charges(&events)[0].0.balance,
10805            Balance::new(d("-12.5"), d("-12.5"))
10806        );
10807    }
10808
10809    /// Run `f`, returning its value and the message of every WARN it logged on this thread.
10810    fn warnings<T>(f: impl FnOnce() -> T) -> (T, Vec<String>) {
10811        let (value, events) = rustrade_instrument::test_utils::logs::capture(f);
10812        let warnings = events
10813            .iter()
10814            .filter(|event| event.level == tracing::Level::WARN)
10815            .map(|event| event.message().to_owned())
10816            .collect();
10817        (value, warnings)
10818    }
10819
10820    /// An account charged into a negative balance is warned once, on the charge that took it
10821    /// there, not again for each charge while it stays negative, nor for a credit.
10822    #[test]
10823    fn a_negative_balance_is_warned_of_on_the_charge_that_crosses_zero() {
10824        // Paid for three hours, then received while the balance is still below zero.
10825        let rates = RateSeries::new(vec![
10826            (DateTime::<Utc>::MIN_UTC, d("0.0001")),
10827            (boundary(3), d("-0.00001")),
10828        ])
10829        .unwrap();
10830        let funding =
10831            FundingModel::new(TimeDelta::hours(1)).with_rates(cfd_instrument_name(), rates);
10832        let mut venue = make_cfd_venue_charging("125000", vec![Arc::new(funding)]);
10833        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10834        cfd_market_fill(&mut venue, Side::Buy, "1");
10835
10836        let (events, logged) = warnings(|| venue.advance_time(boundary(2)));
10837        assert_eq!(charges(&events).len(), 3, "three charges, all below zero");
10838        assert_eq!(logged.len(), 1, "{logged:?}");
10839        assert!(logged[0].contains("could not cover"), "{logged:?}");
10840
10841        let (events, logged) = warnings(|| venue.advance_time(boundary(3)));
10842        assert_eq!(charges(&events)[0].1.amount, d("1.25"), "the long receives");
10843        assert_eq!(free_usd(&venue), d("-36.25"));
10844        assert!(logged.is_empty(), "a credit is not warned of: {logged:?}");
10845    }
10846
10847    /// A charge of zero is not reported: there is nothing to apply.
10848    #[test]
10849    fn a_zero_charge_is_not_reported() {
10850        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0")]);
10851        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10852        cfd_market_fill(&mut venue, Side::Buy, "1");
10853
10854        assert!(venue.advance_time(boundary(1)).is_empty());
10855    }
10856
10857    /// The next deadline is the earlier of an order's expiry and a held position's boundary, and
10858    /// a flat venue with no expiring order has none.
10859    #[test]
10860    fn the_next_deadline_is_the_earliest_expiry_or_boundary() {
10861        let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
10862        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10863        assert_eq!(venue.next_deadline(), None, "flat, no orders");
10864
10865        cfd_market_fill(&mut venue, Side::Buy, "1");
10866        assert_eq!(venue.next_deadline(), Some(boundary(0)));
10867
10868        // Rests without crossing, and holds nothing, since it only reduces the long.
10869        let expiry = boundary(0) - TimeDelta::seconds(60);
10870        let mut gtd = limit_request(
10871            "gtd",
10872            Side::Sell,
10873            "1",
10874            "6000",
10875            TimeInForce::GoodTillDate { expiry },
10876        );
10877        gtd.key.instrument = cfd_instrument_name();
10878        assert!(venue.open_order(gtd).response.state.is_accepted());
10879        assert_eq!(venue.next_deadline(), Some(expiry));
10880
10881        let events = venue.advance_time(expiry);
10882        assert!(!events.is_empty(), "the order expired");
10883        assert_eq!(venue.next_deadline(), Some(boundary(0)));
10884    }
10885
10886    /// A borrow fee on settlement days and financing on calendar days, both at 21:00 UTC, on a
10887    /// short. `time(1)` is Tuesday 2023-11-14 22:13:21Z, so the first cutoffs are Wednesday's
10888    /// and Thursday's. With T+1, Wednesday's borrow fee covers one day and Thursday's three; the
10889    /// short receives its negative financing rate once a day. At one boundary the models charge
10890    /// in the order they were given.
10891    #[test]
10892    fn a_short_pays_its_borrow_by_settlement_day_and_receives_its_financing() {
10893        let cutoff = NaiveTime::from_hms_opt(21, 0, 0).unwrap();
10894        let borrow = BorrowFeeModel::new(DailyAccrual::new(
10895            cutoff,
10896            Utc,
10897            AccrualDays::Settlement {
10898                calendar: Arc::new(WeekendsOnly),
10899                lag: 1,
10900            },
10901            DayCount::Act360,
10902        ))
10903        .with_rates(cfd_instrument_name(), RateSeries::constant(d("0.36")));
10904        let financing = FinancingModel::new(DailyAccrual::new(
10905            cutoff,
10906            Utc,
10907            AccrualDays::Calendar,
10908            DayCount::Act365,
10909        ))
10910        .with_rates(
10911            cfd_instrument_name(),
10912            FinancingRates::new(
10913                RateSeries::constant(d("0.073")),
10914                RateSeries::constant(d("-0.0365")),
10915            ),
10916        );
10917        let mut venue =
10918            make_cfd_venue_charging("200000", vec![Arc::new(borrow), Arc::new(financing)]);
10919        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
10920        cfd_market_fill(&mut venue, Side::Sell, "1");
10921
10922        let friday = DateTime::parse_from_rfc3339("2023-11-17T00:00:00Z")
10923            .unwrap()
10924            .to_utc();
10925        let events = venue.advance_time(friday);
10926        let charged: Vec<_> = charges(&events)
10927            .iter()
10928            .map(|(_, flow)| (flow.time_exchange, flow.kind.clone(), flow.amount))
10929            .collect();
10930
10931        let wednesday = DateTime::parse_from_rfc3339("2023-11-15T21:00:00Z")
10932            .unwrap()
10933            .to_utc();
10934        let thursday = wednesday + TimeDelta::days(1);
10935        let borrow_fee = CashFlowKind::BorrowFee {
10936            rate: Some(d("0.36")),
10937            quantity: Some(d("25")),
10938        };
10939        let financed = CashFlowKind::Financing {
10940            rate: Some(d("-0.0365")),
10941            notional: Some(d("125000")),
10942        };
10943        // 125,000 notional: 36% / 360 a day is 125, and 3.65% / 365 a day is 12.5.
10944        assert_eq!(
10945            charged,
10946            vec![
10947                (wednesday, borrow_fee.clone(), d("-125")),
10948                (wednesday, financed.clone(), d("12.5")),
10949                (thursday, borrow_fee, d("-375")),
10950                (thursday, financed, d("12.5")),
10951            ]
10952        );
10953    }
10954
10955    /// Instruments charged at one boundary are charged in instrument order, whichever order the
10956    /// venue's positions happen to hash in.
10957    #[test]
10958    fn positions_charged_at_one_boundary_are_charged_in_instrument_order() {
10959        let nas = InstrumentNameExchange::new("nas100_usd");
10960        let dax = InstrumentNameExchange::new("dax40_usd");
10961        let mut instruments = instruments_of(cfd_instrument());
10962        let mut funding = FundingModel::new(TimeDelta::hours(1))
10963            .with_rates(cfd_instrument_name(), RateSeries::constant(d("0.0001")));
10964        for name in [&nas, &dax] {
10965            let mut other = cfd_instrument();
10966            other.name_exchange = name.clone();
10967            instruments.insert(name.clone(), other);
10968            funding = funding.with_rates(name.clone(), RateSeries::constant(d("0.0001")));
10969        }
10970        let mut venue = SimulatedVenue::new_market_driven(
10971            &config_from_balances(vec![funded(usd(), d("1000000"))], FeeModelConfig::default()),
10972            instruments,
10973        )
10974        .with_holding_costs(vec![Arc::new(funding)]);
10975        advance(&mut venue, time(1));
10976
10977        for name in [cfd_instrument_name(), nas.clone(), dax.clone()] {
10978            let fills = venue.apply_market(
10979                &name,
10980                MarketObservation::new(market_prices("5000").unwrap()),
10981                time(1),
10982            );
10983            assert!(fills.is_empty());
10984            let outcome = venue.open_order(request(name, Side::Buy, "1", None));
10985            assert!(outcome.response.state.is_accepted());
10986        }
10987
10988        let events = venue.advance_time(boundary(0));
10989        let order: Vec<_> = charges(&events)
10990            .iter()
10991            .map(|(_, flow)| flow.instrument.clone().unwrap())
10992            .collect();
10993        assert_eq!(order, vec![dax, nas, cfd_instrument_name()]);
10994    }
10995
10996    /// A long flipped to a short before a boundary is charged there as the short it is.
10997    #[test]
10998    fn a_position_flipped_before_a_boundary_is_charged_as_what_it_became() {
10999        let mut venue = make_cfd_venue_charging("400000", vec![hourly_funding("0.0001")]);
11000        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
11001        cfd_market_fill(&mut venue, Side::Buy, "1");
11002        cfd_market_fill(&mut venue, Side::Sell, "2");
11003
11004        let events = venue.advance_time(boundary(0));
11005        let charged = charges(&events);
11006        assert_eq!(charged.len(), 1);
11007        assert_eq!(
11008            charged[0].1.amount,
11009            d("12.5"),
11010            "a short receives a positive rate"
11011        );
11012    }
11013
11014    /// A model breaking its contract is caught at the first read of its boundary, here the cache
11015    /// refresh on opening a position, before any driver could wake the venue for it forever.
11016    #[test]
11017    #[should_panic(expected = "which is not after")]
11018    fn a_model_whose_boundary_does_not_advance_panics_rather_than_charging_forever() {
11019        #[derive(Debug)]
11020        struct Stuck;
11021        impl HoldingCostModel for Stuck {
11022            fn next_boundary(
11023                &self,
11024                _: &InstrumentNameExchange,
11025                after: DateTime<Utc>,
11026            ) -> Option<DateTime<Utc>> {
11027                Some(after)
11028            }
11029            fn charge(&self, _: &HeldPosition<'_>, _: DateTime<Utc>) -> Option<HoldingCharge> {
11030                None
11031            }
11032        }
11033
11034        let mut venue = make_cfd_venue_charging("200000", vec![Arc::new(Stuck)]);
11035        assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
11036        cfd_market_fill(&mut venue, Side::Buy, "1");
11037        let _ = venue.advance_time(time(2));
11038    }
11039
11040    // --- Locates and the short-sale restriction ------------------------------------------------
11041
11042    /// A market-driven CFD venue lending as `table` says, its clock at `time(1)` and its book
11043    /// 4990 / 5000.
11044    fn make_cfd_venue_lending(table: ShortabilityTable) -> SimulatedVenue {
11045        let mut venue = SimulatedVenue::new_market_driven(
11046            &config_from_balances(
11047                vec![funded(self::usd(), d("10000000"))],
11048                FeeModelConfig::default(),
11049            ),
11050            instruments_of(cfd_instrument()),
11051        )
11052        .with_shortability(Arc::new(table));
11053        advance(&mut venue, time(1));
11054        assert!(cfd_market(&mut venue, "4990", "5000", "5000").is_empty());
11055        venue
11056    }
11057
11058    fn lending(shortability: Shortability) -> ShortabilityTable {
11059        ShortabilityTable::new().with_shortability(cfd_instrument_name(), time(0), shortability)
11060    }
11061
11062    /// The refusal an open was answered with, having moved nothing.
11063    fn borrow_rejection(outcome: &OpenOutcome) -> &BorrowReject {
11064        assert!(
11065            outcome.events.is_empty(),
11066            "a refused short moves nothing, so it owes no events"
11067        );
11068        match &outcome.response.state {
11069            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
11070                ApiError::BorrowRejected(reject),
11071            ))) => reject,
11072            other => panic!("expected BorrowRejected, got: {other:?}"),
11073        }
11074    }
11075
11076    fn open_cfd(venue: &mut SimulatedVenue, side: Side, quantity: &str) -> OpenOutcome {
11077        venue.open_order(cfd_request(side, quantity, None))
11078    }
11079
11080    #[test]
11081    fn a_short_of_an_instrument_that_is_not_shortable_is_refused_before_the_ledger_moves() {
11082        let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_shortable(false)));
11083
11084        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11085
11086        let reject = borrow_rejection(&outcome);
11087        assert_eq!(reject.reason, BorrowRejectReason::NotShortable);
11088        assert_eq!(reject.venue_code, None);
11089        assert_eq!(reject.available, None);
11090        assert!(reject.message.contains("spx500_usd"), "{}", reject.message);
11091        assert_eq!(free_usd(&venue), d("10000000"));
11092        assert_eq!(cfd_position(&venue), None);
11093        assert!(venue.orders_open(&[]).is_empty());
11094    }
11095
11096    /// A delisting time at or before the clock refuses the short even when the provider still
11097    /// calls the instrument shortable; one still ahead does not.
11098    #[test]
11099    fn a_short_at_or_after_the_delisting_time_is_refused_and_one_before_it_is_not() {
11100        let delisted = Shortability::new()
11101            .with_shortable(true)
11102            .with_delisting_at(time(1));
11103        let mut venue = make_cfd_venue_lending(lending(delisted));
11104
11105        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11106
11107        let reject = borrow_rejection(&outcome);
11108        assert_eq!(reject.reason, BorrowRejectReason::NotShortable);
11109        assert!(reject.message.contains("delisted"), "{}", reject.message);
11110        assert_eq!(cfd_position(&venue), None);
11111
11112        let ahead = Shortability::new()
11113            .with_shortable(true)
11114            .with_delisting_at(time(2));
11115        let mut venue = make_cfd_venue_lending(lending(ahead));
11116        cfd_market_fill(&mut venue, Side::Sell, "1");
11117        assert!(cfd_position(&venue).is_some(), "not delisted yet");
11118    }
11119
11120    #[test]
11121    fn a_buy_and_a_sell_that_only_reduces_a_long_are_never_refused() {
11122        let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_shortable(false)));
11123
11124        cfd_market_fill(&mut venue, Side::Buy, "2");
11125        cfd_market_fill(&mut venue, Side::Sell, "1");
11126        cfd_market_fill(&mut venue, Side::Sell, "1");
11127        assert_eq!(cfd_position(&venue), None);
11128
11129        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11130        assert_eq!(
11131            borrow_rejection(&outcome).reason,
11132            BorrowRejectReason::NotShortable,
11133            "once flat, the next sell opens a short"
11134        );
11135    }
11136
11137    #[test]
11138    fn a_sell_reducing_a_long_that_resting_sells_already_close_opens_a_short() {
11139        let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_shortable(false)));
11140        cfd_market_fill(&mut venue, Side::Buy, "2");
11141        let rested = venue.open_order(cfd_limit("close", Side::Sell, "2", "5200"));
11142        assert!(rested_open(&rested).filled_quantity.is_zero());
11143
11144        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11145
11146        assert_eq!(
11147            borrow_rejection(&outcome).reason,
11148            BorrowRejectReason::NotShortable
11149        );
11150    }
11151
11152    /// The lender has 5 in total. The account's own short of 2 and its resting sell of 2 leave 1,
11153    /// so a sell of 2 is refused saying so, and a sell of 1 is not.
11154    #[test]
11155    fn a_short_past_what_is_available_net_of_the_own_short_and_resting_sells_is_refused() {
11156        let mut venue = make_cfd_venue_lending(lending(
11157            Shortability::new()
11158                .with_shortable(true)
11159                .with_available(d("5")),
11160        ));
11161        cfd_market_fill(&mut venue, Side::Sell, "2");
11162        let rested = venue.open_order(cfd_limit("rest", Side::Sell, "2", "5200"));
11163        assert!(rested_open(&rested).filled_quantity.is_zero());
11164        let free = free_usd(&venue);
11165
11166        let outcome = open_cfd(&mut venue, Side::Sell, "2");
11167
11168        let reject = borrow_rejection(&outcome);
11169        assert_eq!(reject.reason, BorrowRejectReason::InventoryUnavailable);
11170        assert_eq!(reject.available, Some(d("1")));
11171        assert_eq!(free_usd(&venue), free);
11172        assert_eq!(
11173            cfd_position(&venue).map(|(quantity, _)| quantity),
11174            Some(d("-2"))
11175        );
11176
11177        cfd_market_fill(&mut venue, Side::Sell, "1");
11178        assert_eq!(
11179            cfd_position(&venue).map(|(quantity, _)| quantity),
11180            Some(d("-3"))
11181        );
11182    }
11183
11184    #[test]
11185    fn what_is_available_is_never_reported_below_zero() {
11186        let mut venue = make_cfd_venue_lending(
11187            ShortabilityTable::new()
11188                .with_shortability(cfd_instrument_name(), time(0), Shortability::new())
11189                .with_shortability(
11190                    cfd_instrument_name(),
11191                    time(2),
11192                    Shortability::new().with_available(d("1")),
11193                ),
11194        );
11195        cfd_market_fill(&mut venue, Side::Sell, "3");
11196        advance(&mut venue, time(2));
11197
11198        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11199
11200        let reject = borrow_rejection(&outcome);
11201        assert_eq!(reject.reason, BorrowRejectReason::InventoryUnavailable);
11202        assert_eq!(reject.available, Some(Decimal::ZERO));
11203    }
11204
11205    /// From a long of 5, a sell of 9 opens a short of 4 and a sell of 8 one of 3: only the short
11206    /// part is measured against the 3 available.
11207    #[test]
11208    fn a_flip_is_gated_on_the_short_it_opens_only() {
11209        let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_available(d("3"))));
11210        cfd_market_fill(&mut venue, Side::Buy, "5");
11211
11212        let outcome = open_cfd(&mut venue, Side::Sell, "9");
11213        let reject = borrow_rejection(&outcome);
11214        assert_eq!(reject.reason, BorrowRejectReason::InventoryUnavailable);
11215        assert_eq!(reject.available, Some(d("3")));
11216        assert_eq!(
11217            cfd_position(&venue).map(|(quantity, _)| quantity),
11218            Some(d("5"))
11219        );
11220
11221        cfd_market_fill(&mut venue, Side::Sell, "8");
11222        assert_eq!(
11223            cfd_position(&venue).map(|(quantity, _)| quantity),
11224            Some(d("-3"))
11225        );
11226    }
11227
11228    #[test]
11229    fn a_short_sale_restriction_refuses_a_marketable_short_and_lets_one_above_the_bid_rest() {
11230        let mut venue =
11231            make_cfd_venue_lending(ShortabilityTable::new().with_short_sale_restriction(
11232                cfd_instrument_name(),
11233                time(0),
11234                time(10),
11235            ));
11236
11237        // Reducing a long is not a short sale.
11238        cfd_market_fill(&mut venue, Side::Buy, "1");
11239        cfd_market_fill(&mut venue, Side::Sell, "1");
11240
11241        let market = open_cfd(&mut venue, Side::Sell, "1");
11242        assert_eq!(
11243            borrow_rejection(&market).reason,
11244            BorrowRejectReason::ShortSaleRestricted
11245        );
11246        for (cid, limit) in [("at-bid", "4990"), ("below-bid", "4980")] {
11247            let outcome = venue.open_order(cfd_limit(cid, Side::Sell, "1", limit));
11248            assert_eq!(
11249                borrow_rejection(&outcome).reason,
11250                BorrowRejectReason::ShortSaleRestricted,
11251                "a sell limit at {limit} crosses the 4990 bid"
11252            );
11253        }
11254
11255        let above = venue.open_order(cfd_limit("above-bid", Side::Sell, "1", "4995"));
11256        assert!(rested_open(&above).filled_quantity.is_zero());
11257        assert_eq!(cfd_position(&venue), None);
11258
11259        // The restriction is read at the venue's clock, so it lifts when its window ends.
11260        advance(&mut venue, time(10));
11261        cfd_market_fill(&mut venue, Side::Sell, "1");
11262        assert_eq!(
11263            cfd_position(&venue).map(|(quantity, _)| quantity),
11264            Some(d("-1"))
11265        );
11266    }
11267
11268    /// Whether a market sell is marketable is the fill model's to say, and one that can price it
11269    /// is refused for the restriction.
11270    #[test]
11271    fn a_restricted_market_short_the_fill_model_can_price_is_refused_for_the_restriction() {
11272        let mut venue =
11273            make_cfd_venue_lending(ShortabilityTable::new().with_short_sale_restriction(
11274                cfd_instrument_name(),
11275                time(0),
11276                time(10),
11277            ));
11278        venue.fill_model = Arc::new(SimFillConfig::Midpoint(crate::fill::MidpointFillModel));
11279
11280        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11281
11282        assert_eq!(
11283            borrow_rejection(&outcome).reason,
11284            BorrowRejectReason::ShortSaleRestricted
11285        );
11286    }
11287
11288    #[test]
11289    fn missing_shortability_data_allows_the_short() {
11290        let mut venue = make_cfd_venue_lending(
11291            ShortabilityTable::new()
11292                .with_shortability(
11293                    cfd_instrument_name(),
11294                    time(100),
11295                    Shortability::new().with_shortable(false),
11296                )
11297                .with_shortability(
11298                    InstrumentNameExchange::new("other"),
11299                    time(0),
11300                    Shortability::new().with_shortable(false),
11301                ),
11302        );
11303
11304        cfd_market_fill(&mut venue, Side::Sell, "1");
11305
11306        assert_eq!(
11307            cfd_position(&venue).map(|(quantity, _)| quantity),
11308            Some(d("-1"))
11309        );
11310    }
11311
11312    /// Spot lends nothing either way: a sell of base the account does not hold is short of
11313    /// balance, whatever the provider says about lending.
11314    #[test]
11315    fn a_spot_short_is_refused_for_its_balance_not_by_the_provider() {
11316        let table = ShortabilityTable::new().with_shortability(
11317            instrument_name(),
11318            time(0),
11319            Shortability::new().with_shortable(false),
11320        );
11321        let mut venue = make_venue("0", "10000").with_shortability(Arc::new(table));
11322
11323        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
11324
11325        match outcome.response.state {
11326            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
11327                ApiError::BalanceInsufficient(..),
11328            ))) => {}
11329            other => panic!("expected BalanceInsufficient, got: {other:?}"),
11330        }
11331    }
11332
11333    /// With no price to fill at, a restricted market sell is refused for what it is, unpriceable,
11334    /// naming which of the two unpriceable causes occurred, rather than for a restriction that
11335    /// judges a bid it could not fill against.
11336    #[test]
11337    fn a_restricted_short_with_no_price_is_refused_as_unpriceable() {
11338        let reason_of = |fill_model: SimFillConfig, market| {
11339            let mut venue = make_cfd_venue("10000000", FeeModelConfig::default())
11340                .with_shortability(Arc::new(
11341                    ShortabilityTable::new().with_short_sale_restriction(
11342                        cfd_instrument_name(),
11343                        time(0),
11344                        time(10),
11345                    ),
11346                ));
11347            venue.fill_model = Arc::new(fill_model);
11348            advance(&mut venue, time(1));
11349            match venue
11350                .open_order(cfd_request(Side::Sell, "1", market))
11351                .response
11352                .state
11353            {
11354                OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
11355                    ApiError::OrderRejected(reason),
11356                ))) => reason,
11357                other => panic!("expected the unpriceable OrderRejected, got: {other:?}"),
11358            }
11359        };
11360
11361        let absent = reason_of(SimFillConfig::default(), None);
11362        assert!(absent.contains("no market snapshot"), "{absent}");
11363        let empty = reason_of(SimFillConfig::default(), Some(MarketSnapshot::default()));
11364        assert!(empty.contains("no market price available yet"), "{empty}");
11365
11366        // A bid alone has no midpoint, so `MidpointFillModel` cannot price the sell.
11367        let bid_only = MarketSnapshot {
11368            best_bid: Some(d("4990")),
11369            ..MarketSnapshot::default()
11370        };
11371        let midpoint = reason_of(
11372            SimFillConfig::Midpoint(crate::fill::MidpointFillModel),
11373            Some(bid_only),
11374        );
11375        assert!(
11376            midpoint.contains("no market price available yet"),
11377            "{midpoint}"
11378        );
11379    }
11380
11381    /// The checks apply in their documented order: not shortable before too little to lend,
11382    /// and too little to lend before the restriction.
11383    #[test]
11384    fn the_first_refusal_that_applies_is_the_one_reported() {
11385        let restricted = |shortability| {
11386            lending(shortability).with_short_sale_restriction(
11387                cfd_instrument_name(),
11388                time(0),
11389                time(10),
11390            )
11391        };
11392
11393        let mut venue = make_cfd_venue_lending(restricted(
11394            Shortability::new()
11395                .with_shortable(false)
11396                .with_available(Decimal::ZERO),
11397        ));
11398        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11399        assert_eq!(
11400            borrow_rejection(&outcome).reason,
11401            BorrowRejectReason::NotShortable
11402        );
11403
11404        let mut venue = make_cfd_venue_lending(restricted(
11405            Shortability::new().with_available(Decimal::ZERO),
11406        ));
11407        let outcome = open_cfd(&mut venue, Side::Sell, "1");
11408        assert_eq!(
11409            borrow_rejection(&outcome).reason,
11410            BorrowRejectReason::InventoryUnavailable
11411        );
11412    }
11413
11414    /// A flip from a long opens a short, so a restriction refuses it when it is marketable, while
11415    /// the sell that only closes the same long goes through.
11416    #[test]
11417    fn a_short_sale_restriction_refuses_a_marketable_flip_but_not_a_close() {
11418        let mut venue =
11419            make_cfd_venue_lending(ShortabilityTable::new().with_short_sale_restriction(
11420                cfd_instrument_name(),
11421                time(0),
11422                time(10),
11423            ));
11424        cfd_market_fill(&mut venue, Side::Buy, "2");
11425
11426        let outcome = open_cfd(&mut venue, Side::Sell, "3");
11427        assert_eq!(
11428            borrow_rejection(&outcome).reason,
11429            BorrowRejectReason::ShortSaleRestricted
11430        );
11431        assert_eq!(
11432            cfd_position(&venue).map(|(quantity, _)| quantity),
11433            Some(d("2"))
11434        );
11435
11436        cfd_market_fill(&mut venue, Side::Sell, "2");
11437        assert_eq!(cfd_position(&venue), None);
11438    }
11439
11440    // ---------------------------------------------------------------------------------------
11441    // Market orders held for their instrument's next bar.
11442    // ---------------------------------------------------------------------------------------
11443
11444    fn next_bar_config(usdt: &str, price: NextBarPrice) -> MockExecutionConfig {
11445        MockExecutionConfig {
11446            market_order_timing: MarketOrderTiming::NextBar(price),
11447            ..spot_config("10", usdt, FeeModelConfig::default())
11448        }
11449    }
11450
11451    fn ohlc(open: &str, high: &str, low: &str, close: &str) -> MarketBar {
11452        MarketBar::new(d(open), d(high), d(low), d(close))
11453    }
11454
11455    /// Feeds `bar` at `time(seconds)` as a candle feed does: a last price at its close, no size.
11456    fn feed_bar(
11457        venue: &mut SimulatedVenue,
11458        seconds: i64,
11459        bar: MarketBar,
11460    ) -> Vec<UnindexedAccountEvent> {
11461        feed_bar_sized(venue, seconds, bar, MarketDepth::UNKNOWN)
11462    }
11463
11464    fn feed_bar_sized(
11465        venue: &mut SimulatedVenue,
11466        seconds: i64,
11467        bar: MarketBar,
11468        depth: MarketDepth,
11469    ) -> Vec<UnindexedAccountEvent> {
11470        let snapshot = MarketSnapshot::from_last_price(Some(bar.close));
11471        venue.apply_market(
11472            &instrument_name(),
11473            MarketObservation::new(snapshot)
11474                .with_depth(depth)
11475                .with_bar(Some(bar)),
11476            time(seconds),
11477        )
11478    }
11479
11480    /// A venue holding market orders for the next bar, fed one bar at `time(1)` that closed at 100.
11481    fn venue_holding(price: NextBarPrice, usdt: &str) -> SimulatedVenue {
11482        let mut venue =
11483            SimulatedVenue::new_market_driven(&next_bar_config(usdt, price), spot_instruments());
11484        advance(&mut venue, time(1));
11485        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
11486        venue
11487    }
11488
11489    fn market_order(
11490        cid: &str,
11491        side: Side,
11492        quantity: &str,
11493        time_in_force: TimeInForce,
11494    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
11495        let mut request = request(instrument_name(), side, quantity, None);
11496        request.key.cid = ClientOrderId::new(cid);
11497        request.state.time_in_force = time_in_force;
11498        request
11499    }
11500
11501    fn ioc_buy(cid: &str, quantity: &str) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
11502        market_order(cid, Side::Buy, quantity, TimeInForce::ImmediateOrCancel)
11503    }
11504
11505    fn total_base(venue: &SimulatedVenue) -> Decimal {
11506        venue.balances(&[base()])[0].balance.total
11507    }
11508
11509    fn rejection_reason(outcome: &OpenOutcome) -> &str {
11510        match &outcome.response.state {
11511            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
11512                ApiError::OrderRejected(reason),
11513            ))) => reason,
11514            other => panic!("expected an OrderRejected, got: {other:?}"),
11515        }
11516    }
11517
11518    #[test]
11519    fn a_market_order_held_for_the_next_bar_is_open_and_holds_its_cost_at_the_current_price() {
11520        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11521
11522        let outcome = venue.open_order(ioc_buy("held", "1"));
11523
11524        assert_eq!(
11525            rested_open(&outcome).filled_quantity,
11526            Decimal::ZERO,
11527            "accepted as open, with nothing traded yet"
11528        );
11529        assert_eq!(
11530            outcome.events.len(),
11531            1,
11532            "it owes the one balance its hold restates"
11533        );
11534        assert_eq!(
11535            balance_of(&outcome.events[0]).balance.free,
11536            d("9900"),
11537            "held at the close the order arrived on"
11538        );
11539        assert_eq!(total_quote(&venue), d("10000"), "and nothing paid yet");
11540        assert!(venue.trades(time(0)).is_empty());
11541        assert_eq!(venue.orders_open(&[]).len(), 1);
11542        assert!(venue.account.orders().holds(&instrument_name()));
11543    }
11544
11545    #[test]
11546    fn the_next_bar_fills_a_held_order_at_its_open_and_settles_that_price() {
11547        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11548        let _ = venue.open_order(ioc_buy("held", "1"));
11549
11550        let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
11551
11552        assert_eq!(
11553            events.len(),
11554            4,
11555            "the balance paid, the one received, the trade, then the order: {events:?}"
11556        );
11557        let paid = balance_of(&events[0]);
11558        assert_eq!(paid.asset, quote());
11559        assert_eq!(
11560            (paid.balance.total, paid.balance.free),
11561            (d("9898"), d("9898")),
11562            "the hold is released and the fill settled at the open, not the close it was held at"
11563        );
11564        assert_eq!(balance_of(&events[1]).asset, base());
11565
11566        let trade = trade_of(&events[2]);
11567        assert_eq!(
11568            (trade.price, trade.quantity, trade.time_exchange),
11569            (d("102"), d("1"), time(2)),
11570            "filled at the bar's open, stamped at the bar"
11571        );
11572        assert!(
11573            matches!(
11574                order_of(&events[3]).state,
11575                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
11576            ),
11577            "{:?}",
11578            order_of(&events[3]).state
11579        );
11580
11581        assert!(venue.orders_open(&[]).is_empty());
11582        assert!(!venue.account.orders().holds(&instrument_name()));
11583        assert_eq!(total_base(&venue), d("11"));
11584        assert_eq!(free_quote(&venue), d("9898"));
11585    }
11586
11587    #[test]
11588    fn adverse_extreme_fills_a_held_buy_at_the_bar_high_and_a_sell_at_its_low() {
11589        for (side, expected) in [(Side::Buy, "110"), (Side::Sell, "98")] {
11590            let mut venue = venue_holding(NextBarPrice::AdverseExtreme, "10000");
11591            let _ = venue.open_order(market_order(
11592                "held",
11593                side,
11594                "1",
11595                TimeInForce::ImmediateOrCancel,
11596            ));
11597
11598            let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
11599
11600            assert_eq!(trade_of(&events[2]).price, d(expected), "a {side:?}");
11601        }
11602    }
11603
11604    #[test]
11605    fn a_tick_between_bars_does_not_release_a_held_order() {
11606        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11607        let _ = venue.open_order(ioc_buy("held", "1"));
11608
11609        let ticked = venue.apply_market(
11610            &instrument_name(),
11611            MarketObservation::new(traded("101")),
11612            time(2),
11613        );
11614
11615        assert!(ticked.is_empty(), "a tick is not a bar: {ticked:?}");
11616        assert!(venue.account.orders().holds(&instrument_name()));
11617
11618        let events = feed_bar(&mut venue, 3, ohlc("103", "104", "102", "103"));
11619        assert_eq!(
11620            trade_of(&events[2]).price,
11621            d("103"),
11622            "the next bar fills it"
11623        );
11624    }
11625
11626    #[test]
11627    fn a_held_order_fills_at_the_bar_price_moved_by_the_configured_slippage() {
11628        let config = MockExecutionConfig {
11629            slippage: SlippageConfig::fixed(d("0.01")).unwrap(),
11630            ..next_bar_config("10000", NextBarPrice::Open)
11631        };
11632        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
11633        advance(&mut venue, time(1));
11634        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
11635        let _ = venue.open_order(ioc_buy("held", "1"));
11636
11637        let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
11638
11639        assert_eq!(
11640            trade_of(&events[2]).price,
11641            d("103.02"),
11642            "the open, 1% against the buy"
11643        );
11644    }
11645
11646    #[test]
11647    fn a_market_order_with_no_bar_to_wait_for_is_rejected_naming_the_setting() {
11648        // Fed ticks but never a bar: there is no next bar this venue could know to wait for.
11649        let mut ticked = SimulatedVenue::new_market_driven(
11650            &next_bar_config("10000", NextBarPrice::Open),
11651            spot_instruments(),
11652        );
11653        advance(&mut ticked, time(1));
11654        assert!(
11655            ticked
11656                .apply_market(
11657                    &instrument_name(),
11658                    MarketObservation::new(traded("100")),
11659                    time(1)
11660                )
11661                .is_empty()
11662        );
11663        // No feed at all.
11664        let mut request_priced = SimulatedVenue::new(
11665            &next_bar_config("10000", NextBarPrice::Open),
11666            spot_instruments(),
11667        );
11668
11669        for (venue, request) in [
11670            (&mut ticked, ioc_buy("held", "1")),
11671            (&mut request_priced, buy_request("1", market_prices("100"))),
11672        ] {
11673            let outcome = venue.open_order(request);
11674
11675            assert!(outcome.events.is_empty());
11676            assert!(
11677                rejection_reason(&outcome).contains("MarketOrderTiming::NextBar"),
11678                "{}",
11679                rejection_reason(&outcome)
11680            );
11681            assert_eq!(
11682                free_quote(venue),
11683                d("10000"),
11684                "a refused order holds nothing"
11685            );
11686        }
11687    }
11688
11689    #[test]
11690    fn a_held_order_the_account_cannot_cover_is_refused_on_arrival() {
11691        let mut venue = venue_holding(NextBarPrice::Open, "99");
11692
11693        let outcome = venue.open_order(ioc_buy("held", "1"));
11694
11695        assert!(
11696            matches!(
11697                outcome.response.state,
11698                OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
11699                    ApiError::BalanceInsufficient(..)
11700                )))
11701            ),
11702            "{:?}",
11703            outcome.response.state
11704        );
11705        assert!(!venue.account.orders().holds(&instrument_name()));
11706    }
11707
11708    #[test]
11709    fn a_held_order_is_cancelled_like_a_resting_one_and_releases_its_hold() {
11710        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11711        let request = ioc_buy("held", "1");
11712        let key = request.key.clone();
11713        let _ = venue.open_order(request);
11714
11715        let cancelled = venue.cancel_order(OrderEvent {
11716            key,
11717            state: RequestCancel { id: None },
11718        });
11719
11720        assert!(cancelled.response.state.is_ok());
11721        assert_eq!(free_quote(&venue), d("10000"));
11722        assert!(!venue.account.orders().holds(&instrument_name()));
11723        assert!(
11724            feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108")).is_empty(),
11725            "the next bar has nothing left to fill"
11726        );
11727    }
11728
11729    /// A price on a held market order is ignored, as on arrival: the order is held priceless, so
11730    /// a tick through that price cannot match it as a limit, and a cancel leaves nothing held.
11731    #[test]
11732    fn a_held_market_order_carrying_a_price_is_held_without_it() {
11733        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11734        let request = priced_market_order(Side::Buy, "200", None);
11735        let key = request.key.clone();
11736
11737        let outcome = venue.open_order(request);
11738        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
11739        assert_eq!(outcome.response.price, None, "reported as it is held");
11740        assert_eq!(venue.orders_open(&[])[0].price, None);
11741
11742        let ticked = venue.apply_market(
11743            &instrument_name(),
11744            MarketObservation::new(traded("150")),
11745            time(2),
11746        );
11747        assert!(
11748            ticked.is_empty(),
11749            "a tick below the stray price matches nothing: {ticked:?}"
11750        );
11751
11752        let cancelled = venue.cancel_order(OrderEvent {
11753            key,
11754            state: RequestCancel { id: None },
11755        });
11756        assert!(cancelled.response.state.is_ok());
11757        assert!(!venue.account.orders().holds(&instrument_name()));
11758    }
11759
11760    /// A held order whose deadline the bar reaches is retired by the sweep before the bar can
11761    /// release it, and leaves the held index with it.
11762    #[test]
11763    fn a_held_order_whose_deadline_the_bar_reaches_expires_unfilled() {
11764        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11765        let _ = venue.open_order(market_order(
11766            "gtd",
11767            Side::Buy,
11768            "1",
11769            TimeInForce::GoodTillDate { expiry: time(2) },
11770        ));
11771
11772        let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
11773
11774        assert_eq!(
11775            events.len(),
11776            2,
11777            "the released hold, then the order: {events:?}"
11778        );
11779        assert!(
11780            matches!(
11781                order_of(&events[1]).state,
11782                OrderState::Inactive(InactiveOrderState::Expired(_))
11783            ),
11784            "{:?}",
11785            order_of(&events[1]).state
11786        );
11787        assert!(venue.trades(time(0)).is_empty());
11788        assert_eq!(free_quote(&venue), d("10000"));
11789        assert!(!venue.account.orders().holds(&instrument_name()));
11790    }
11791
11792    /// A held spot sell holds the base it would deliver, and the bar settles exactly that.
11793    #[test]
11794    fn a_held_spot_sell_holds_the_base_it_would_deliver() {
11795        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11796        let free_base = |venue: &SimulatedVenue| venue.balances(&[base()])[0].balance.free;
11797
11798        let _ = venue.open_order(market_order(
11799            "sell",
11800            Side::Sell,
11801            "1",
11802            TimeInForce::ImmediateOrCancel,
11803        ));
11804        assert_eq!(free_base(&venue), d("9"));
11805        assert_eq!(total_base(&venue), d("10"), "and nothing delivered yet");
11806
11807        let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
11808
11809        assert_eq!(trade_of(&events[2]).price, d("102"));
11810        assert_eq!(total_base(&venue), d("9"));
11811        assert_eq!(free_base(&venue), d("9"));
11812    }
11813
11814    #[test]
11815    fn a_held_order_the_account_can_no_longer_afford_at_the_bar_is_cancelled() {
11816        // 100 is held at the close the order arrived on, and the bar opens at 110.
11817        let mut venue = venue_holding(NextBarPrice::Open, "105");
11818        let _ = venue.open_order(ioc_buy("held", "1"));
11819        assert_eq!(free_quote(&venue), d("5"));
11820
11821        let events = feed_bar(&mut venue, 2, ohlc("110", "112", "108", "111"));
11822
11823        assert_eq!(
11824            events.len(),
11825            2,
11826            "the released hold, then the order: {events:?}"
11827        );
11828        assert_eq!(balance_of(&events[0]).balance.free, d("105"));
11829        assert!(
11830            matches!(
11831                &order_of(&events[1]).state,
11832                OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
11833                    if cancelled.filled_quantity == Some(Decimal::ZERO)
11834            ),
11835            "{:?}",
11836            order_of(&events[1]).state
11837        );
11838        assert!(venue.trades(time(0)).is_empty());
11839        assert_eq!(free_quote(&venue), d("105"));
11840    }
11841
11842    #[test]
11843    fn a_bar_with_less_size_than_a_held_order_fills_immediate_or_cancel_in_part_and_kills_fill_or_kill()
11844     {
11845        let bar = ohlc("102", "110", "98", "108");
11846
11847        let mut immediate = venue_holding(NextBarPrice::Open, "10000");
11848        let _ = immediate.open_order(ioc_buy("ioc", "1"));
11849        let events = feed_bar_sized(&mut immediate, 2, bar, sized("0.4"));
11850        assert_eq!(trade_of(&events[2]).quantity, d("0.4"));
11851        assert!(
11852            matches!(
11853                &order_of(&events[3]).state,
11854                OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
11855                    if cancelled.filled_quantity == Some(d("0.4"))
11856            ),
11857            "immediate-or-cancel keeps what the bar gave it: {:?}",
11858            order_of(&events[3]).state
11859        );
11860        assert_eq!(free_quote(&immediate), d("10000") - d("40.8"));
11861
11862        let mut all_or_nothing = venue_holding(NextBarPrice::Open, "10000");
11863        let _ =
11864            all_or_nothing.open_order(market_order("fok", Side::Buy, "1", TimeInForce::FillOrKill));
11865        let events = feed_bar_sized(&mut all_or_nothing, 2, bar, sized("0.4"));
11866        assert_eq!(
11867            events.len(),
11868            2,
11869            "the released hold, then the order: {events:?}"
11870        );
11871        assert!(all_or_nothing.trades(time(0)).is_empty());
11872        assert_eq!(free_quote(&all_or_nothing), d("10000"));
11873    }
11874
11875    /// Fill-or-kill kills only a part: a bar with size for the whole held order fills it.
11876    #[test]
11877    fn a_bar_with_size_for_the_whole_held_order_fills_fill_or_kill() {
11878        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11879        let _ = venue.open_order(market_order("fok", Side::Buy, "1", TimeInForce::FillOrKill));
11880
11881        let events = feed_bar_sized(&mut venue, 2, ohlc("102", "110", "98", "108"), sized("1"));
11882
11883        assert_eq!(trade_of(&events[2]).quantity, d("1"));
11884        assert!(
11885            matches!(
11886                order_of(&events[3]).state,
11887                OrderState::Inactive(InactiveOrderState::FullyFilled(_))
11888            ),
11889            "{:?}",
11890            order_of(&events[3]).state
11891        );
11892    }
11893
11894    #[test]
11895    fn orders_held_on_one_instrument_fill_in_arrival_order_against_one_size() {
11896        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11897        let _ = venue.open_order(ioc_buy("first", "1"));
11898        let _ = venue.open_order(ioc_buy("second", "1"));
11899
11900        let events = feed_bar_sized(&mut venue, 2, ohlc("102", "110", "98", "108"), sized("1.5"));
11901
11902        let fills = events
11903            .iter()
11904            .filter_map(|event| match &event.kind {
11905                AccountEventKind::Trade(trade) => Some((
11906                    trade.client_order_id.clone().unwrap().0.to_string(),
11907                    trade.quantity,
11908                )),
11909                _ => None,
11910            })
11911            .collect::<Vec<_>>();
11912        assert_eq!(
11913            fills,
11914            [
11915                ("first".to_string(), d("1")),
11916                ("second".to_string(), d("0.5"))
11917            ],
11918            "earliest first, and the second gets what the first left"
11919        );
11920    }
11921
11922    #[test]
11923    fn expiring_the_held_orders_retires_each_as_expired_and_releases_its_hold() {
11924        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11925        let _ = venue.open_order(ioc_buy("held", "1"));
11926
11927        let events = venue.expire_held(time(5));
11928
11929        assert_eq!(
11930            events.len(),
11931            2,
11932            "the released hold, then the order: {events:?}"
11933        );
11934        assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
11935        let order = order_of(&events[1]);
11936        assert!(
11937            matches!(
11938                &order.state,
11939                OrderState::Inactive(InactiveOrderState::Expired(expired))
11940                    if expired.filled_quantity == Some(Decimal::ZERO)
11941                        && expired.time_exchange == time(5)
11942            ),
11943            "{:?}",
11944            order.state
11945        );
11946        assert!(venue.orders_open(&[]).is_empty());
11947        assert!(
11948            venue.expire_held(time(6)).is_empty(),
11949            "nothing is held twice"
11950        );
11951        assert!(feed_bar(&mut venue, 7, ohlc("102", "110", "98", "108")).is_empty());
11952    }
11953
11954    #[test]
11955    fn a_limit_order_is_not_held_for_the_next_bar() {
11956        let mut venue = venue_holding(NextBarPrice::Open, "10000");
11957
11958        let outcome = venue.open_order(limit_request("limit", Side::Buy, "1", "101", gtc()));
11959
11960        assert_eq!(
11961            filled_trade(&outcome).price,
11962            d("100"),
11963            "a marketable limit still fills on arrival"
11964        );
11965        assert!(!venue.account.orders().holds(&instrument_name()));
11966    }
11967
11968    // ---------------------------------------------------------------------------------------
11969    // Market orders on a stale market.
11970    // ---------------------------------------------------------------------------------------
11971
11972    fn stale_config(max_age: Duration) -> MockExecutionConfig {
11973        MockExecutionConfig {
11974            stale_market: StaleMarketPolicy::Wait { max_age },
11975            ..spot_config("10", "10000", FeeModelConfig::default())
11976        }
11977    }
11978
11979    /// Ten seconds: a market last observed fresh at `time(1)` is stale from `time(12)`.
11980    const MAX_AGE: Duration = Duration::from_secs(10);
11981
11982    /// A venue waiting out markets more than [`MAX_AGE`] old, fed a trade at 100 at `time(1)`.
11983    fn venue_waiting() -> SimulatedVenue {
11984        let mut venue =
11985            SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
11986        advance(&mut venue, time(1));
11987        assert!(observe(&mut venue, 1, traded("100")).is_empty());
11988        venue
11989    }
11990
11991    /// Feeds a fresh observation of `snapshot` at `time(seconds)`.
11992    fn observe(
11993        venue: &mut SimulatedVenue,
11994        seconds: i64,
11995        snapshot: MarketSnapshot,
11996    ) -> Vec<UnindexedAccountEvent> {
11997        venue.apply_market(
11998            &instrument_name(),
11999            MarketObservation::new(snapshot),
12000            time(seconds),
12001        )
12002    }
12003
12004    /// Feeds an observation of `snapshot` at `time(seconds)` that carries its price forward
12005    /// without anything having traded, as a zero-volume bar does.
12006    fn observe_idle(
12007        venue: &mut SimulatedVenue,
12008        seconds: i64,
12009        snapshot: MarketSnapshot,
12010    ) -> Vec<UnindexedAccountEvent> {
12011        venue.apply_market(
12012            &instrument_name(),
12013            MarketObservation::new(snapshot).with_fresh(false),
12014            time(seconds),
12015        )
12016    }
12017
12018    fn gtc_buy(cid: &str, quantity: &str) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
12019        market_order(cid, Side::Buy, quantity, gtc())
12020    }
12021
12022    fn trades_in(
12023        events: &[UnindexedAccountEvent],
12024    ) -> Vec<&Trade<AssetNameExchange, InstrumentNameExchange>> {
12025        events
12026            .iter()
12027            .filter_map(|event| match &event.kind {
12028                AccountEventKind::Trade(trade) => Some(trade),
12029                _ => None,
12030            })
12031            .collect()
12032    }
12033
12034    #[test]
12035    fn a_market_observed_fresh_within_the_max_age_fills_on_arrival() {
12036        let mut venue = venue_waiting();
12037        // Exactly `MAX_AGE` old is still fresh.
12038        advance(&mut venue, time(11));
12039
12040        let outcome = venue.open_order(gtc_buy("fresh", "1"));
12041
12042        assert_eq!(filled_trade(&outcome).price, d("100"));
12043        assert!(!venue.account.orders().holds(&instrument_name()));
12044    }
12045
12046    #[test]
12047    fn a_market_order_on_a_stale_market_waits_open_holding_its_cost() {
12048        let mut venue = venue_waiting();
12049        advance(&mut venue, time(12));
12050
12051        let outcome = venue.open_order(gtc_buy("waiting", "1"));
12052
12053        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
12054        assert_eq!(
12055            outcome.events.len(),
12056            1,
12057            "it owes the one balance its hold restates"
12058        );
12059        assert_eq!(
12060            balance_of(&outcome.events[0]).balance.free,
12061            d("9900"),
12062            "held at the stale price it arrived on"
12063        );
12064        assert!(venue.trades(time(0)).is_empty());
12065        assert!(venue.account.orders().holds(&instrument_name()));
12066    }
12067
12068    #[test]
12069    fn the_next_fresh_observation_fills_a_waiting_order_at_its_price() {
12070        let mut venue = venue_waiting();
12071        advance(&mut venue, time(12));
12072        let _ = venue.open_order(gtc_buy("waiting", "1"));
12073
12074        let events = observe(&mut venue, 20, traded("105"));
12075
12076        assert_eq!(
12077            events.len(),
12078            4,
12079            "the balance paid, the one received, the trade, then the order: {events:?}"
12080        );
12081        assert_eq!(
12082            balance_of(&events[0]).balance.free,
12083            d("9895"),
12084            "the hold is released and the fill settled at the fresh price"
12085        );
12086        let trade = trade_of(&events[2]);
12087        assert_eq!(
12088            (trade.price, trade.quantity, trade.time_exchange),
12089            (d("105"), d("1"), time(20)),
12090            "filled at the observation's price, stamped at it"
12091        );
12092        assert!(matches!(
12093            order_of(&events[3]).state,
12094            OrderState::Inactive(InactiveOrderState::FullyFilled(_))
12095        ));
12096        assert!(!venue.account.orders().holds(&instrument_name()));
12097        assert_eq!(total_base(&venue), d("11"));
12098    }
12099
12100    #[test]
12101    fn a_waiting_order_fills_after_the_resting_orders_its_observation_crosses() {
12102        let mut venue = venue_waiting();
12103        let _ = venue.open_order(limit_request("resting", Side::Buy, "1", "99", gtc()));
12104        advance(&mut venue, time(12));
12105        let _ = venue.open_order(gtc_buy("waiting", "1"));
12106
12107        let events = observe(&mut venue, 20, traded("98"));
12108
12109        let prices = trades_in(&events)
12110            .iter()
12111            .map(|trade| (trade.client_order_id.clone(), trade.price))
12112            .collect::<Vec<_>>();
12113        assert_eq!(
12114            prices,
12115            vec![
12116                (Some(ClientOrderId::new("resting")), d("99")),
12117                (Some(ClientOrderId::new("waiting")), d("98")),
12118            ],
12119            "the resting order was crossed by the observation, the waiting one arrived after it"
12120        );
12121    }
12122
12123    #[test]
12124    fn an_observation_that_is_not_fresh_neither_refreshes_the_market_nor_releases_an_order() {
12125        let mut venue = venue_waiting();
12126        // Carries a price forward at `time(5)`, but the market was last fresh at `time(1)`.
12127        assert!(observe_idle(&mut venue, 5, traded("101")).is_empty());
12128        advance(&mut venue, time(12));
12129
12130        let outcome = venue.open_order(gtc_buy("waiting", "1"));
12131        assert_eq!(
12132            balance_of(&outcome.events[0]).balance.free,
12133            d("9899"),
12134            "it waits, holding the price the idle observation carried"
12135        );
12136
12137        assert!(observe_idle(&mut venue, 13, traded("102")).is_empty());
12138        assert!(venue.account.orders().holds(&instrument_name()));
12139
12140        let events = observe(&mut venue, 14, traded("103"));
12141        assert_eq!(trade_of(&events[2]).price, d("103"));
12142    }
12143
12144    #[test]
12145    fn a_market_never_observed_fresh_is_stale() {
12146        let mut venue =
12147            SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
12148        advance(&mut venue, time(1));
12149        assert!(observe_idle(&mut venue, 1, traded("100")).is_empty());
12150
12151        let outcome = venue.open_order(ioc_buy("immediate", "1"));
12152        let reason = rejection_reason(&outcome);
12153        assert!(reason.contains("never observed fresh"), "{reason}");
12154
12155        let outcome = venue.open_order(gtc_buy("waiting", "1"));
12156        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
12157    }
12158
12159    #[test]
12160    fn an_immediate_or_cancel_or_fill_or_kill_order_on_a_stale_market_is_rejected_naming_the_policy()
12161     {
12162        for time_in_force in [TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill] {
12163            let mut venue = venue_waiting();
12164            advance(&mut venue, time(12));
12165
12166            let outcome = venue.open_order(market_order("now", Side::Buy, "1", time_in_force));
12167
12168            assert!(outcome.events.is_empty());
12169            let reason = rejection_reason(&outcome);
12170            assert!(
12171                reason.contains("StaleMarketPolicy::Wait")
12172                    && reason.contains(&time(1).to_string())
12173                    && reason.contains(&time_in_force.to_string()),
12174                "{reason}"
12175            );
12176            assert_eq!(
12177                free_quote(&venue),
12178                d("10000"),
12179                "a refused order holds nothing"
12180            );
12181            assert!(!venue.account.orders().holds(&instrument_name()));
12182        }
12183    }
12184
12185    #[test]
12186    fn a_market_order_on_an_unpriced_instrument_is_rejected_as_unpriceable_not_as_stale() {
12187        let mut venue =
12188            SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
12189        advance(&mut venue, time(1));
12190        assert!(observe(&mut venue, 1, MarketSnapshot::default()).is_empty());
12191
12192        for request in [gtc_buy("waiting", "1"), ioc_buy("immediate", "1")] {
12193            let outcome = venue.open_order(request);
12194
12195            let reason = rejection_reason(&outcome);
12196            assert!(
12197                reason.contains("cannot price") && !reason.contains("StaleMarketPolicy"),
12198                "{reason}"
12199            );
12200        }
12201    }
12202
12203    #[test]
12204    fn a_venue_with_no_market_feed_rejects_market_orders_naming_the_policy() {
12205        let mut venue = SimulatedVenue::new(&stale_config(MAX_AGE), spot_instruments());
12206
12207        let outcome = venue.open_order(buy_request("1", market_prices("100")));
12208
12209        assert!(
12210            rejection_reason(&outcome).contains("StaleMarketPolicy::Wait needs a market feed"),
12211            "{}",
12212            rejection_reason(&outcome)
12213        );
12214        assert_eq!(free_quote(&venue), d("10000"));
12215    }
12216
12217    #[test]
12218    fn a_max_age_longer_than_any_run_never_makes_a_market_stale() {
12219        let mut venue =
12220            SimulatedVenue::new_market_driven(&stale_config(Duration::MAX), spot_instruments());
12221        advance(&mut venue, time(1));
12222        assert!(observe(&mut venue, 1, traded("100")).is_empty());
12223        advance(&mut venue, time(1_000_000));
12224
12225        let outcome = venue.open_order(ioc_buy("immediate", "1"));
12226
12227        assert_eq!(filled_trade(&outcome).price, d("100"));
12228    }
12229
12230    #[test]
12231    fn a_waiting_order_the_fresh_market_cannot_price_is_cancelled_and_releases_its_hold() {
12232        let mut venue = venue_waiting();
12233        advance(&mut venue, time(12));
12234        let _ = venue.open_order(gtc_buy("waiting", "1"));
12235
12236        // A bid alone: nothing a buy can be priced from.
12237        let events = observe(
12238            &mut venue,
12239            20,
12240            MarketSnapshot::new(Some(d("99")), None, None),
12241        );
12242
12243        assert_eq!(
12244            events.len(),
12245            2,
12246            "the released hold, then the order: {events:?}"
12247        );
12248        assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
12249        assert!(matches!(
12250            &order_of(&events[1]).state,
12251            OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
12252                if cancelled.filled_quantity == Some(Decimal::ZERO)
12253        ));
12254        assert!(!venue.account.orders().holds(&instrument_name()));
12255    }
12256
12257    #[test]
12258    fn only_a_fresh_observation_releases_a_waiting_order_never_the_clock() {
12259        let mut venue = venue_waiting();
12260        advance(&mut venue, time(12));
12261        let _ = venue.open_order(gtc_buy("waiting", "1"));
12262
12263        advance(&mut venue, time(1_000));
12264
12265        assert!(venue.account.orders().holds(&instrument_name()));
12266        assert!(venue.trades(time(0)).is_empty());
12267    }
12268
12269    #[test]
12270    fn a_waiting_order_still_waiting_at_the_end_of_the_data_expires() {
12271        let mut venue = venue_waiting();
12272        advance(&mut venue, time(12));
12273        let _ = venue.open_order(gtc_buy("waiting", "1"));
12274
12275        let events = venue.expire_held(time(30));
12276
12277        assert_eq!(events.len(), 2, "{events:?}");
12278        assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
12279        assert!(matches!(
12280            order_of(&events[1]).state,
12281            OrderState::Inactive(InactiveOrderState::Expired(_))
12282        ));
12283    }
12284
12285    #[test]
12286    fn only_a_bar_released_under_next_bar_fills_ahead_of_its_observation() {
12287        let bar = ohlc("102", "110", "98", "108");
12288        let fresh_bar = MarketObservation::new(traded("108")).with_bar(Some(bar));
12289        let idle_bar = fresh_bar.with_fresh(false);
12290        let tick = MarketObservation::new(traded("108"));
12291
12292        // Waiting for a fresh observation, which it follows as a resting fill does.
12293        let mut waiting = venue_waiting();
12294        advance(&mut waiting, time(12));
12295        let _ = waiting.open_order(gtc_buy("waiting", "1"));
12296        assert!(waiting.account.orders().holds(&instrument_name()));
12297        assert!(!waiting.fills_ahead_of(&instrument_name(), &fresh_bar));
12298
12299        // Held for the next bar: any bar releases it, but nothing else does.
12300        let mut held = venue_holding(NextBarPrice::Open, "10000");
12301        assert!(
12302            !held.fills_ahead_of(&instrument_name(), &fresh_bar),
12303            "nothing held yet"
12304        );
12305        let _ = held.open_order(ioc_buy("held", "1"));
12306        assert!(held.fills_ahead_of(&instrument_name(), &fresh_bar));
12307        assert!(held.fills_ahead_of(&instrument_name(), &idle_bar));
12308        assert!(!held.fills_ahead_of(&instrument_name(), &tick));
12309
12310        // Both: only a bar something traded in releases it.
12311        let mut both = venue_holding_fresh_bars();
12312        let _ = both.open_order(gtc_buy("held", "1"));
12313        assert!(both.fills_ahead_of(&instrument_name(), &fresh_bar));
12314        assert!(!both.fills_ahead_of(&instrument_name(), &idle_bar));
12315    }
12316
12317    /// A venue holding market orders for the next bar something traded in, fed one at `time(1)`
12318    /// that closed at 100.
12319    fn venue_holding_fresh_bars() -> SimulatedVenue {
12320        let config = MockExecutionConfig {
12321            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
12322            ..next_bar_config("10000", NextBarPrice::Open)
12323        };
12324        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
12325        advance(&mut venue, time(1));
12326        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
12327        venue
12328    }
12329
12330    #[test]
12331    fn under_next_bar_an_idle_bar_cancels_held_immediate_orders_and_leaves_the_rest_held() {
12332        let mut venue = venue_holding_fresh_bars();
12333        let _ = venue.open_order(ioc_buy("immediate", "1"));
12334        let _ = venue.open_order(market_order("all", Side::Buy, "1", TimeInForce::FillOrKill));
12335        let _ = venue.open_order(gtc_buy("patient", "1"));
12336
12337        let bar = ohlc("100", "100", "100", "100");
12338        let snapshot = MarketSnapshot::from_last_price(Some(bar.close));
12339        let events = venue.apply_market(
12340            &instrument_name(),
12341            MarketObservation::new(snapshot)
12342                .with_bar(Some(bar))
12343                .with_fresh(false),
12344            time(2),
12345        );
12346
12347        assert_eq!(
12348            events.len(),
12349            4,
12350            "a released hold and a cancel for each: {events:?}"
12351        );
12352        for (order, cid) in [(&events[1], "immediate"), (&events[3], "all")] {
12353            let order = order_of(order);
12354            assert_eq!(order.key.cid, ClientOrderId::new(cid));
12355            assert!(matches!(
12356                &order.state,
12357                OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
12358                    if cancelled.filled_quantity == Some(Decimal::ZERO)
12359            ));
12360        }
12361        assert_eq!(
12362            free_quote(&venue),
12363            d("9900"),
12364            "only the patient order's hold remains"
12365        );
12366        assert_eq!(
12367            venue.account.orders().held_on(&instrument_name()),
12368            [ClientOrderId::new("patient")]
12369        );
12370
12371        let events = feed_bar(&mut venue, 3, ohlc("103", "104", "102", "103"));
12372        assert_eq!(
12373            trade_of(&events[2]).price,
12374            d("103"),
12375            "the next bar something traded in fills it at its open"
12376        );
12377    }
12378
12379    #[test]
12380    fn under_next_bar_an_immediate_order_on_a_stale_market_is_rejected_and_a_patient_one_held() {
12381        let mut venue = venue_holding_fresh_bars();
12382        advance(&mut venue, time(12));
12383
12384        let outcome = venue.open_order(ioc_buy("immediate", "1"));
12385        assert!(
12386            rejection_reason(&outcome).contains("StaleMarketPolicy::Wait"),
12387            "{}",
12388            rejection_reason(&outcome)
12389        );
12390
12391        let outcome = venue.open_order(gtc_buy("patient", "1"));
12392        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
12393        assert!(venue.account.orders().holds(&instrument_name()));
12394    }
12395
12396    // ---------------------------------------------------------------------------------------
12397    // Limit orders on a stale market.
12398    // ---------------------------------------------------------------------------------------
12399
12400    fn gtc_limit(
12401        cid: &str,
12402        side: Side,
12403        quantity: &str,
12404        limit: &str,
12405    ) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
12406        limit_request(cid, side, quantity, limit, gtc())
12407    }
12408
12409    fn post_only() -> TimeInForce {
12410        TimeInForce::GoodUntilCancelled { post_only: true }
12411    }
12412
12413    /// The orders resting on the book for matching on `side`, best first.
12414    fn resting_on(venue: &SimulatedVenue, side: Side) -> Vec<ClientOrderId> {
12415        venue
12416            .account
12417            .orders()
12418            .resting(&instrument_name(), side)
12419            .map(|order| order.key.cid.clone())
12420            .collect()
12421    }
12422
12423    #[test]
12424    fn a_limit_order_on_a_stale_market_waits_open_holding_what_it_would_cost_arriving_now() {
12425        // The stale market is 100: a buy at 105 would take it, capped at the limit, and a buy at
12426        // 95 would rest at its limit.
12427        for (limit, free) in [("105", "9900"), ("95", "9905")] {
12428            let mut venue = venue_waiting();
12429            advance(&mut venue, time(12));
12430
12431            let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1", limit));
12432
12433            assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
12434            assert_eq!(outcome.events.len(), 1, "{:?}", outcome.events);
12435            assert_eq!(balance_of(&outcome.events[0]).balance.free, d(free));
12436            assert!(venue.trades(time(0)).is_empty());
12437            assert!(venue.account.orders().holds(&instrument_name()));
12438            assert!(
12439                resting_on(&venue, Side::Buy).is_empty(),
12440                "held, so nothing matches it"
12441            );
12442            assert_eq!(
12443                venue
12444                    .account
12445                    .orders()
12446                    .get(&ClientOrderId::new("waiting"))
12447                    .unwrap()
12448                    .price,
12449                Some(d(limit)),
12450                "held with its limit"
12451            );
12452        }
12453    }
12454
12455    #[test]
12456    fn a_held_limit_order_the_fresh_market_crosses_fills_as_a_taker_at_the_fresh_price() {
12457        let mut venue = venue_waiting();
12458        advance(&mut venue, time(12));
12459        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
12460
12461        let events = observe(&mut venue, 20, traded("102"));
12462
12463        assert_eq!(events.len(), 4, "{events:?}");
12464        assert_eq!(balance_of(&events[0]).balance.free, d("9898"));
12465        let trade = trade_of(&events[2]);
12466        assert_eq!(
12467            (trade.price, trade.quantity, trade.time_exchange),
12468            (d("102"), d("1"), time(20)),
12469            "at the fresh market, better than its limit, stamped at the observation"
12470        );
12471        assert!(matches!(
12472            &order_of(&events[3]).state,
12473            OrderState::Inactive(InactiveOrderState::FullyFilled(filled))
12474                if filled.avg_price == Some(d("102"))
12475        ));
12476        assert!(!venue.account.orders().holds(&instrument_name()));
12477        assert!(venue.account.orders().is_empty());
12478        assert_eq!(total_base(&venue), d("11"));
12479    }
12480
12481    #[test]
12482    fn a_held_limit_order_is_capped_at_its_limit_however_far_the_slippage_moves_it() {
12483        let config = MockExecutionConfig {
12484            slippage: SlippageConfig::fixed(d("0.1")).unwrap(),
12485            ..stale_config(MAX_AGE)
12486        };
12487        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
12488        advance(&mut venue, time(1));
12489        assert!(observe(&mut venue, 1, traded("100")).is_empty());
12490        advance(&mut venue, time(12));
12491        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
12492
12493        // 102 slipped by 10% is 112.2, over the limit.
12494        let events = observe(&mut venue, 20, traded("102"));
12495
12496        assert_eq!(trade_of(&events[2]).price, d("105"));
12497    }
12498
12499    #[test]
12500    fn a_held_limit_order_the_fresh_market_does_not_cross_joins_the_book_at_its_limit() {
12501        let mut venue = venue_waiting();
12502        advance(&mut venue, time(12));
12503        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
12504
12505        let events = observe(&mut venue, 20, traded("110"));
12506
12507        assert_eq!(
12508            events.len(),
12509            2,
12510            "the hold restated, then the order: {events:?}"
12511        );
12512        assert_eq!(
12513            balance_of(&events[0]).balance.free,
12514            d("9895"),
12515            "now held at its limit, as the maker it rests as"
12516        );
12517        let order = order_of(&events[1]);
12518        assert!(matches!(
12519            &order.state,
12520            OrderState::Active(ActiveOrderState::Open(open))
12521                if open.time_exchange == time(20) && open.filled_quantity.is_zero()
12522        ));
12523        assert!(!venue.account.orders().holds(&instrument_name()));
12524        assert_eq!(
12525            resting_on(&venue, Side::Buy),
12526            [ClientOrderId::new("waiting")]
12527        );
12528
12529        let events = observe(&mut venue, 21, traded("104"));
12530        assert_eq!(
12531            trade_of(&events[2]).price,
12532            d("105"),
12533            "resting, it fills at its limit"
12534        );
12535    }
12536
12537    #[test]
12538    fn a_limit_order_on_a_stale_market_is_decided_by_the_fresh_market_not_the_stale_one() {
12539        // At the stale 100 a buy at 101 crosses and a buy at 99 does not. The fresh 95 crosses
12540        // both, and both are filled at it: the one that looked passive is not filled worse.
12541        let mut venue = venue_waiting();
12542        advance(&mut venue, time(12));
12543        let _ = venue.open_order(gtc_limit("crossing", Side::Buy, "1", "101"));
12544        let _ = venue.open_order(gtc_limit("passive", Side::Buy, "1", "99"));
12545
12546        let events = observe(&mut venue, 20, traded("95"));
12547
12548        let prices = trades_in(&events)
12549            .iter()
12550            .map(|trade| (trade.client_order_id.clone(), trade.price))
12551            .collect::<Vec<_>>();
12552        assert_eq!(
12553            prices,
12554            vec![
12555                (Some(ClientOrderId::new("crossing")), d("95")),
12556                (Some(ClientOrderId::new("passive")), d("95")),
12557            ],
12558            "earliest arrival first"
12559        );
12560    }
12561
12562    #[test]
12563    fn a_held_limit_order_the_fresh_size_cannot_fill_rests_the_rest_at_its_limit() {
12564        let mut venue = venue_waiting();
12565        advance(&mut venue, time(12));
12566        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
12567
12568        let events = venue.apply_market(
12569            &instrument_name(),
12570            MarketObservation::new(book("101", "102"))
12571                .with_depth(MarketDepth::new(Some(d("1")), Some(d("0.4")))),
12572            time(20),
12573        );
12574
12575        assert_eq!(events.len(), 4, "{events:?}");
12576        assert_eq!(
12577            balance_of(&events[0]).balance.free,
12578            d("9896.20"),
12579            "0.4 paid at 102 and 0.6 held at 105: 10000 - 40.8 - 63"
12580        );
12581        let trade = trade_of(&events[2]);
12582        assert_eq!(
12583            (trade.price, trade.quantity, trade.order_filled_quantity),
12584            (d("102"), d("0.4"), Some(d("0.4")))
12585        );
12586        assert!(matches!(
12587            &order_of(&events[3]).state,
12588            OrderState::Active(ActiveOrderState::Open(open)) if open.filled_quantity == d("0.4")
12589        ));
12590        assert_eq!(
12591            resting_on(&venue, Side::Buy),
12592            [ClientOrderId::new("waiting")]
12593        );
12594
12595        let events = observe(&mut venue, 21, traded("104"));
12596        let trade = trade_of(&events[2]);
12597        assert_eq!(
12598            (trade.price, trade.quantity, trade.order_filled_quantity),
12599            (d("105"), d("0.6"), Some(d("1"))),
12600            "the remainder fills at its limit, as the maker"
12601        );
12602        assert_eq!(free_quote(&venue), d("9896.20"));
12603    }
12604
12605    #[test]
12606    fn a_held_post_only_order_the_fresh_market_crosses_is_cancelled_and_one_it_does_not_rests() {
12607        let mut venue = venue_waiting();
12608        advance(&mut venue, time(12));
12609        let outcome = venue.open_order(limit_request("maker", Side::Buy, "1", "105", post_only()));
12610        assert_eq!(
12611            balance_of(&outcome.events[0]).balance.free,
12612            d("9895"),
12613            "held at its limit, as the maker it promises to be"
12614        );
12615
12616        let events = observe(&mut venue, 20, traded("104"));
12617
12618        assert_eq!(events.len(), 2, "{events:?}");
12619        assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
12620        assert!(matches!(
12621            &order_of(&events[1]).state,
12622            OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
12623                if cancelled.filled_quantity == Some(Decimal::ZERO)
12624        ));
12625
12626        advance(&mut venue, time(40));
12627        let _ = venue.open_order(limit_request("passive", Side::Buy, "1", "100", post_only()));
12628        let events = observe(&mut venue, 41, traded("104"));
12629        assert!(matches!(
12630            &order_of(&events[1]).state,
12631            OrderState::Active(ActiveOrderState::Open(_))
12632        ));
12633        assert_eq!(
12634            resting_on(&venue, Side::Buy),
12635            [ClientOrderId::new("passive")]
12636        );
12637    }
12638
12639    #[test]
12640    fn an_immediate_or_cancel_or_fill_or_kill_limit_on_a_stale_market_is_rejected_whether_or_not_it_crosses()
12641     {
12642        for (time_in_force, limit) in [
12643            (TimeInForce::ImmediateOrCancel, "105"),
12644            (TimeInForce::ImmediateOrCancel, "95"),
12645            (TimeInForce::FillOrKill, "105"),
12646            (TimeInForce::FillOrKill, "95"),
12647        ] {
12648            let mut venue = venue_waiting();
12649            advance(&mut venue, time(12));
12650
12651            let outcome =
12652                venue.open_order(limit_request("now", Side::Buy, "1", limit, time_in_force));
12653
12654            assert!(outcome.events.is_empty());
12655            let reason = rejection_reason(&outcome);
12656            assert!(
12657                reason.contains("StaleMarketPolicy::Wait")
12658                    && reason.contains(&time_in_force.to_string()),
12659                "{reason}"
12660            );
12661            assert!(venue.account.orders().is_empty());
12662        }
12663    }
12664
12665    #[test]
12666    fn a_limit_order_past_its_deadline_on_a_stale_market_expires_rather_than_waits() {
12667        let mut venue = venue_waiting();
12668        advance(&mut venue, time(12));
12669
12670        let outcome = venue.open_order(limit_request(
12671            "late",
12672            Side::Buy,
12673            "1",
12674            "105",
12675            TimeInForce::GoodTillDate { expiry: time(12) },
12676        ));
12677
12678        assert!(matches!(
12679            outcome.response.state,
12680            OrderState::Inactive(InactiveOrderState::Expired(_))
12681        ));
12682        assert!(venue.account.orders().is_empty());
12683    }
12684
12685    #[test]
12686    fn a_held_limit_order_reaching_its_deadline_expires_and_releases_its_hold() {
12687        let mut venue = venue_waiting();
12688        advance(&mut venue, time(12));
12689        let _ = venue.open_order(limit_request(
12690            "dated",
12691            Side::Buy,
12692            "1",
12693            "105",
12694            TimeInForce::GoodTillDate { expiry: time(15) },
12695        ));
12696
12697        let events = observe(&mut venue, 15, traded("102"));
12698
12699        assert!(trades_in(&events).is_empty(), "{events:?}");
12700        assert!(events.iter().any(|event| matches!(
12701            &event.kind,
12702            AccountEventKind::OrderSnapshot(order)
12703                if matches!(order.0.state, OrderState::Inactive(InactiveOrderState::Expired(_)))
12704        )));
12705        assert_eq!(free_quote(&venue), d("10000"));
12706        assert!(venue.account.orders().is_empty());
12707    }
12708
12709    #[test]
12710    fn a_held_limit_order_can_be_cancelled_and_one_still_held_at_the_end_of_the_data_expires() {
12711        let mut venue = venue_waiting();
12712        advance(&mut venue, time(12));
12713        let _ = venue.open_order(gtc_limit("cancelled", Side::Buy, "1", "105"));
12714        let _ = venue.open_order(gtc_limit("expired", Side::Buy, "1", "95"));
12715
12716        let cancelled = venue.cancel_order(OrderRequestCancel {
12717            key: gtc_limit("cancelled", Side::Buy, "1", "105").key,
12718            state: RequestCancel { id: None },
12719        });
12720        assert!(cancelled.response.state.is_ok());
12721        assert_eq!(free_quote(&venue), d("9905"));
12722        assert_eq!(
12723            venue.account.orders().held_on(&instrument_name()),
12724            [ClientOrderId::new("expired")]
12725        );
12726
12727        let events = venue.expire_held(time(30));
12728        assert_eq!(events.len(), 2, "{events:?}");
12729        assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
12730        assert!(matches!(
12731            order_of(&events[1]).state,
12732            OrderState::Inactive(InactiveOrderState::Expired(_))
12733        ));
12734        assert!(venue.account.orders().is_empty());
12735    }
12736
12737    #[test]
12738    fn a_limit_order_on_an_unpriced_instrument_rests_as_it_does_without_the_policy() {
12739        let mut venue =
12740            SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
12741        advance(&mut venue, time(12));
12742
12743        let outcome = venue.open_order(gtc_limit("resting", Side::Buy, "1", "100"));
12744
12745        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
12746        assert!(!venue.account.orders().holds(&instrument_name()));
12747        assert_eq!(
12748            resting_on(&venue, Side::Buy),
12749            [ClientOrderId::new("resting")]
12750        );
12751    }
12752
12753    /// On a fresh market, and on a stale one, where it is refused rather than held for a release
12754    /// with nothing to trade.
12755    #[test]
12756    fn an_order_for_a_quantity_that_is_not_positive_is_refused_rather_than_held() {
12757        for (now, quantity) in [(1, "0"), (1, "-1"), (12, "0"), (12, "-1")] {
12758            for request in [
12759                gtc_limit("limit", Side::Buy, quantity, "105"),
12760                gtc_buy("market", quantity),
12761            ] {
12762                let mut venue = venue_waiting();
12763                advance(&mut venue, time(now));
12764
12765                let outcome = venue.open_order(request);
12766
12767                assert_eq!(
12768                    rejection_reason(&outcome),
12769                    format!(
12770                        "cannot open {} for {EXCHANGE}: quantity {quantity} is not positive",
12771                        instrument_name()
12772                    )
12773                );
12774                assert!(outcome.events.is_empty(), "a refused order moves nothing");
12775                assert!(
12776                    venue.account.orders().is_empty(),
12777                    "and is neither held nor rested"
12778                );
12779                assert_eq!(free_quote(&venue), d("10000"));
12780                assert!(
12781                    observe(&mut venue, 20, traded("102")).is_empty(),
12782                    "so a fresh observation has nothing to release"
12783                );
12784            }
12785        }
12786    }
12787
12788    #[test]
12789    fn a_held_limit_order_the_account_can_no_longer_afford_is_cancelled_and_hands_back_its_draw() {
12790        let config = MockExecutionConfig {
12791            slippage: uniform(5),
12792            ..MockExecutionConfig {
12793                stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
12794                ..spot_config("10", "100", FeeModelConfig::default())
12795            }
12796        };
12797        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
12798        advance(&mut venue, time(1));
12799        assert!(observe(&mut venue, 1, traded("100")).is_empty());
12800        advance(&mut venue, time(12));
12801        // Held at the stale 100: the account's whole balance.
12802        let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
12803        assert_eq!(balance_of(&outcome.events[0]).balance.free, Decimal::ZERO);
12804
12805        let events = observe(&mut venue, 20, traded("104"));
12806
12807        assert_eq!(
12808            events.len(),
12809            2,
12810            "the released hold, then the order: {events:?}"
12811        );
12812        assert_eq!(balance_of(&events[0]).balance.free, d("100"));
12813        assert!(matches!(
12814            &order_of(&events[1]).state,
12815            OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
12816                if cancelled.filled_quantity == Some(Decimal::ZERO)
12817        ));
12818        assert!(venue.account.orders().is_empty());
12819        assert_eq!(venue.slippage, Slippage::new(uniform(5)));
12820    }
12821
12822    #[test]
12823    fn a_held_sell_limit_order_fills_at_the_fresh_price_or_rests_at_its_limit() {
12824        let mut venue = venue_waiting();
12825        advance(&mut venue, time(12));
12826        // Both cross the stale 100; the fresh 97 crosses only the first.
12827        let _ = venue.open_order(gtc_limit("crossed", Side::Sell, "1", "95"));
12828        let _ = venue.open_order(gtc_limit("passive", Side::Sell, "1", "99"));
12829
12830        let events = observe(&mut venue, 20, traded("97"));
12831
12832        let prices = trades_in(&events)
12833            .iter()
12834            .map(|trade| (trade.client_order_id.clone(), trade.price))
12835            .collect::<Vec<_>>();
12836        assert_eq!(
12837            prices,
12838            vec![(Some(ClientOrderId::new("crossed")), d("97"))],
12839            "above its limit, at the fresh price"
12840        );
12841        assert_eq!(
12842            resting_on(&venue, Side::Sell),
12843            [ClientOrderId::new("passive")]
12844        );
12845        assert_eq!(total_base(&venue), d("9"));
12846    }
12847
12848    #[test]
12849    fn a_held_limit_order_paying_its_fee_in_a_third_asset_takes_the_fee_apart_at_its_release() {
12850        let config = MockExecutionConfig {
12851            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
12852            ..fee_asset_config("10")
12853        };
12854        let mut venue = fee_asset_venue_from(&config, true);
12855        advance(&mut venue, time(12));
12856        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "51000"));
12857
12858        let events = observe(&mut venue, 20, traded("50000"));
12859
12860        assert_eq!(
12861            restated(&events)
12862                .iter()
12863                .map(|(asset, _)| asset.clone())
12864                .collect::<Vec<_>>(),
12865            [quote(), base(), bnb()],
12866            "paid, received, then the fee: {events:?}"
12867        );
12868        let trade = trades_in(&events)[0];
12869        assert_eq!(trade.price, d("50000"));
12870        // 10bp of 50,000 is 50 USDT, a quarter off in BNB at 600.
12871        assert_eq!(
12872            restated(&events)[2].1.free,
12873            d("10") - d("50") * d("0.75") / d("600")
12874        );
12875        assert_eq!(restated(&events)[0].1.total, d("50000"), "no fee in USDT");
12876    }
12877
12878    fn cfd_venue_with(usd: &str, stale_market: StaleMarketPolicy) -> SimulatedVenue {
12879        let config = MockExecutionConfig {
12880            stale_market,
12881            ..config_from_balances(vec![funded(self::usd(), d(usd))], FeeModelConfig::default())
12882        };
12883        let mut venue =
12884            SimulatedVenue::new_market_driven(&config, instruments_of(cfd_instrument()));
12885        advance(&mut venue, time(1));
12886        venue
12887    }
12888
12889    /// A released CFD order settles exactly as the same order arriving to the market that released
12890    /// it: against the position as it stands then, closing the long and opening a short, with the
12891    /// remainder the size there cannot fill held against the position the fill leaves.
12892    #[test]
12893    fn a_held_cfd_limit_order_settles_against_the_position_at_its_release_as_on_arrival() {
12894        let fresh = MarketObservation::new(MarketSnapshot {
12895            best_bid: Some(d("4950")),
12896            best_ask: Some(d("4960")),
12897            last_price: Some(d("4950")),
12898        });
12899        for depth in [MarketDepth::UNKNOWN, MarketDepth::new(Some(d("1.5")), None)] {
12900            let observation = fresh.with_depth(depth);
12901
12902            let mut held = cfd_venue_with(
12903                "400000",
12904                StaleMarketPolicy::Wait {
12905                    max_age: Duration::from_secs(10),
12906                },
12907            );
12908            assert!(cfd_market(&mut held, "5000", "5000", "5000").is_empty());
12909            cfd_market_fill(&mut held, Side::Buy, "1");
12910            advance(&mut held, time(12));
12911            let outcome = held.open_order(cfd_limit("flip", Side::Sell, "2", "4900"));
12912            assert!(outcome.response.state.is_accepted());
12913            assert!(held.account.orders().holds(&cfd_instrument_name()));
12914            let released = held.apply_market(&cfd_instrument_name(), observation, time(12));
12915            assert!(!trades_in(&released).is_empty(), "{released:?}");
12916
12917            let mut arriving = cfd_venue_with("400000", StaleMarketPolicy::Off);
12918            assert!(cfd_market(&mut arriving, "5000", "5000", "5000").is_empty());
12919            cfd_market_fill(&mut arriving, Side::Buy, "1");
12920            advance(&mut arriving, time(12));
12921            assert!(
12922                arriving
12923                    .apply_market(&cfd_instrument_name(), observation, time(12))
12924                    .is_empty()
12925            );
12926            let _ = arriving.open_order(cfd_limit("flip", Side::Sell, "2", "4900"));
12927
12928            assert_eq!(
12929                (free_usd(&held), total_usd(&held), cfd_position(&held)),
12930                (
12931                    free_usd(&arriving),
12932                    total_usd(&arriving),
12933                    cfd_position(&arriving)
12934                ),
12935                "depth {depth:?}"
12936            );
12937            assert_eq!(
12938                resting_count(&held),
12939                resting_count(&arriving),
12940                "depth {depth:?}"
12941            );
12942        }
12943    }
12944
12945    fn resting_count(venue: &SimulatedVenue) -> usize {
12946        venue
12947            .account
12948            .orders()
12949            .resting(&cfd_instrument_name(), Side::Sell)
12950            .count()
12951    }
12952
12953    #[test]
12954    fn under_wait_an_observation_that_is_not_fresh_crosses_no_resting_order() {
12955        let mut venue = venue_waiting();
12956        let _ = venue.open_order(gtc_limit("resting", Side::Buy, "1", "99"));
12957        let _ = venue.open_order(gtc_limit("selling", Side::Sell, "1", "101"));
12958
12959        assert!(observe_idle(&mut venue, 5, traded("95")).is_empty());
12960        assert!(observe_idle(&mut venue, 6, traded("105")).is_empty());
12961        assert_eq!(
12962            resting_on(&venue, Side::Buy),
12963            [ClientOrderId::new("resting")]
12964        );
12965        assert_eq!(
12966            resting_on(&venue, Side::Sell),
12967            [ClientOrderId::new("selling")]
12968        );
12969
12970        let events = observe(&mut venue, 7, traded("98"));
12971        assert_eq!(
12972            trade_of(&events[2]).price,
12973            d("99"),
12974            "a fresh observation crosses it"
12975        );
12976    }
12977
12978    #[test]
12979    fn without_the_policy_an_observation_that_is_not_fresh_still_crosses_a_resting_order() {
12980        let mut venue = SimulatedVenue::new_market_driven(
12981            &spot_config("10", "10000", FeeModelConfig::default()),
12982            spot_instruments(),
12983        );
12984        advance(&mut venue, time(1));
12985        assert!(observe(&mut venue, 1, traded("100")).is_empty());
12986        let _ = venue.open_order(gtc_limit("resting", Side::Buy, "1", "99"));
12987
12988        let events = observe_idle(&mut venue, 5, traded("95"));
12989
12990        assert_eq!(trade_of(&events[2]).price, d("99"));
12991    }
12992
12993    #[test]
12994    fn a_held_limit_order_is_not_released_by_an_observation_that_is_not_fresh() {
12995        let mut venue = venue_waiting();
12996        advance(&mut venue, time(12));
12997        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
12998
12999        assert!(observe_idle(&mut venue, 13, traded("90")).is_empty());
13000        assert!(venue.account.orders().holds(&instrument_name()));
13001
13002        let events = observe(&mut venue, 14, traded("103"));
13003        assert_eq!(trade_of(&events[2]).price, d("103"));
13004    }
13005
13006    #[test]
13007    fn under_next_bar_a_limit_order_on_a_stale_market_is_decided_against_the_next_fresh_bar() {
13008        let mut venue = venue_holding_fresh_bars();
13009        advance(&mut venue, time(12));
13010        let _ = venue.open_order(gtc_limit("crossed", Side::Buy, "1", "104"));
13011        let _ = venue.open_order(gtc_limit("passive", Side::Buy, "1", "101"));
13012        let bar = ohlc("102", "106", "101", "105");
13013        assert!(venue.fills_ahead_of(
13014            &instrument_name(),
13015            &MarketObservation::new(traded("105")).with_bar(Some(bar))
13016        ));
13017
13018        let events = feed_bar(&mut venue, 13, bar);
13019
13020        let prices = trades_in(&events)
13021            .iter()
13022            .map(|trade| (trade.client_order_id.clone(), trade.price))
13023            .collect::<Vec<_>>();
13024        assert_eq!(
13025            prices,
13026            vec![(Some(ClientOrderId::new("crossed")), d("102"))],
13027            "the open crosses 104 and fills it there; it does not cross 101, which rests"
13028        );
13029        assert_eq!(
13030            resting_on(&venue, Side::Buy),
13031            [ClientOrderId::new("passive")]
13032        );
13033    }
13034
13035    #[test]
13036    fn under_next_bar_a_held_limit_order_its_open_does_not_cross_can_be_crossed_by_its_close() {
13037        let mut venue = venue_holding_fresh_bars();
13038        advance(&mut venue, time(12));
13039        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "101"));
13040
13041        let events = feed_bar(&mut venue, 13, ohlc("102", "103", "99", "100"));
13042
13043        let trades = trades_in(&events);
13044        assert_eq!(trades.len(), 1, "{events:?}");
13045        assert_eq!(
13046            trades[0].price,
13047            d("101"),
13048            "rested at its limit when the open did not reach it, then crossed by the close"
13049        );
13050        assert!(venue.account.orders().is_empty());
13051    }
13052
13053    #[test]
13054    fn under_next_bar_a_limit_order_on_a_fresh_market_is_decided_on_arrival() {
13055        let mut venue = venue_holding_fresh_bars();
13056
13057        let outcome = venue.open_order(gtc_limit("crossing", Side::Buy, "1", "104"));
13058
13059        assert_eq!(filled_trade(&outcome).price, d("100"));
13060    }
13061
13062    #[test]
13063    fn a_held_limit_buy_that_rests_a_remainder_holds_the_loan_left_against_it() {
13064        let config = MockExecutionConfig {
13065            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
13066            ..margin_config("0", "100", "2")
13067        };
13068        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
13069        advance(&mut venue, time(1));
13070        assert!(observe(&mut venue, 1, traded("100")).is_empty());
13071        advance(&mut venue, time(12));
13072        // 1.5 at the stale 100 needs 150 of the 100 held: 50 is lent.
13073        let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
13074        assert_eq!(
13075            balance_of(sans_debt(&outcome.events)[0]).balance,
13076            Balance::new_margin(d("150"), Decimal::ZERO, d("50"), Decimal::ZERO)
13077        );
13078
13079        // 0.5 at 80 and 1 held at 100 need 140: 40 of the loan is still used, 10 is repaid.
13080        let events = venue.apply_market(
13081            &instrument_name(),
13082            MarketObservation::new(book("79", "80"))
13083                .with_depth(MarketDepth::new(Some(d("1")), Some(d("0.5")))),
13084            time(20),
13085        );
13086        assert_eq!(
13087            balance_of(sans_debt(&events)[0]).balance,
13088            Balance::new_margin(d("100"), Decimal::ZERO, d("40"), Decimal::ZERO)
13089        );
13090        assert_eq!(trade_of(sans_debt(&events)[2]).quantity, d("0.5"));
13091
13092        let cancelled = venue.cancel_order(OrderRequestCancel {
13093            key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
13094            state: RequestCancel { id: None },
13095        });
13096        assert!(cancelled.response.state.is_ok());
13097        assert_eq!(
13098            venue.balances(&[quote()])[0].balance,
13099            Balance::new_margin(d("60"), d("60"), Decimal::ZERO, Decimal::ZERO),
13100            "cancelling the remainder repays the 40 it held"
13101        );
13102    }
13103
13104    #[test]
13105    fn cancelling_a_released_remainder_repays_only_its_own_loan_and_not_one_already_owed() {
13106        // Owing 20 USDT already, the account has 100 of its own and 120 to spend.
13107        let config = MockExecutionConfig {
13108            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
13109            spot_margin: Some(
13110                SpotMarginConfig::new(quote(), d("2"), MarginEffect::Borrow).unwrap(),
13111            ),
13112            ..config_from_balances(
13113                vec![
13114                    funded(base(), Decimal::ZERO),
13115                    AssetBalance::new(
13116                        quote(),
13117                        Balance::new_margin(d("120"), d("120"), d("20"), Decimal::ZERO),
13118                        Utc::now(),
13119                    ),
13120                ],
13121                FeeModelConfig::default(),
13122            )
13123        };
13124        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
13125        advance(&mut venue, time(1));
13126        assert!(observe(&mut venue, 1, traded("100")).is_empty());
13127        advance(&mut venue, time(12));
13128        // 150 at the stale 100 lends 30 more.
13129        let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
13130
13131        // 0.5 at 80 and 1 held at 100 need 140: 10 of the 30 is repaid, 20 still used.
13132        let events = venue.apply_market(
13133            &instrument_name(),
13134            MarketObservation::new(book("79", "80"))
13135                .with_depth(MarketDepth::new(Some(d("1")), Some(d("0.5")))),
13136            time(20),
13137        );
13138        assert_eq!(
13139            balance_of(&events[0]).balance,
13140            Balance::new_margin(d("100"), Decimal::ZERO, d("40"), Decimal::ZERO)
13141        );
13142
13143        let cancelled = venue.cancel_order(OrderRequestCancel {
13144            key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
13145            state: RequestCancel { id: None },
13146        });
13147        assert!(cancelled.response.state.is_ok());
13148        assert_eq!(
13149            venue.balances(&[quote()])[0].balance,
13150            Balance::new_margin(d("80"), d("80"), d("20"), Decimal::ZERO),
13151            "the remainder repays the 20 it borrowed, and the 20 owed before stays owed"
13152        );
13153    }
13154
13155    // ---------------------------------------------------------------------------------------
13156    // Spot margin: what the venue lends, holds and repays.
13157    // ---------------------------------------------------------------------------------------
13158
13159    fn margin_config(btc: &str, usdt: &str, max_leverage: &str) -> MockExecutionConfig {
13160        MockExecutionConfig {
13161            spot_margin: Some(
13162                SpotMarginConfig::new(quote(), d(max_leverage), MarginEffect::Borrow).unwrap(),
13163            ),
13164            ..spot_config(btc, usdt, FeeModelConfig::default())
13165        }
13166    }
13167
13168    /// A venue with no feed of its own, pricing each order from its request.
13169    fn margin_venue(btc: &str, usdt: &str, max_leverage: &str) -> SimulatedVenue {
13170        SimulatedVenue::new(&margin_config(btc, usdt, max_leverage), spot_instruments())
13171    }
13172
13173    /// A fed venue, its book at 49,000 / 49,100, so a sell limit above the bid rests.
13174    fn margin_market_venue(btc: &str, usdt: &str) -> SimulatedVenue {
13175        let mut venue =
13176            SimulatedVenue::new_market_driven(&margin_config(btc, usdt, "3"), spot_instruments());
13177        advance(&mut venue, time(1));
13178        assert!(
13179            venue
13180                .apply_market(
13181                    &instrument_name(),
13182                    MarketObservation::new(book("49000", "49100")),
13183                    time(1)
13184                )
13185                .is_empty()
13186        );
13187        venue
13188    }
13189
13190    /// `asset`'s balance as the venue reports it now.
13191    fn balance_now(venue: &SimulatedVenue, asset: AssetNameExchange) -> Balance {
13192        venue.balances(&[asset])[0].balance
13193    }
13194
13195    /// The reason a rejection for an unaffordable order gives.
13196    fn insufficient_reason(outcome: &OpenOutcome) -> &str {
13197        match &outcome.response.state {
13198            OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
13199                ApiError::BalanceInsufficient(_, reason),
13200            ))) => reason,
13201            other => panic!("expected BalanceInsufficient, got: {other:?}"),
13202        }
13203    }
13204
13205    #[test]
13206    fn a_cash_venue_reports_no_loans_and_refuses_a_sell_of_base_it_lacks() {
13207        let mut venue = make_venue("0.25", "100000");
13208
13209        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
13210
13211        assert!(
13212            !insufficient_reason(&outcome).contains("Borrowable"),
13213            "{}",
13214            insufficient_reason(&outcome)
13215        );
13216        assert_eq!(
13217            balance_now(&venue, base()),
13218            Balance::new(d("0.25"), d("0.25"))
13219        );
13220    }
13221
13222    #[test]
13223    fn a_margin_sell_borrows_only_the_shortfall_of_base() {
13224        let mut venue = margin_venue("0.25", "100000", "3");
13225
13226        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
13227
13228        assert_eq!(
13229            filled_balance(&outcome).balance,
13230            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), Decimal::ZERO),
13231            "the sell delivers all the base it borrowed and what the account held"
13232        );
13233        assert_eq!(
13234            filled_received(&outcome).balance,
13235            Balance::new_margin(d("150000"), d("150000"), Decimal::ZERO, Decimal::ZERO)
13236        );
13237        assert_eq!(filled_trade(&outcome).quantity, d("1"));
13238    }
13239
13240    #[test]
13241    fn a_margin_sell_of_base_the_account_never_held_borrows_it() {
13242        let config = MockExecutionConfig {
13243            spot_margin: Some(
13244                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
13245            ),
13246            ..config_from_balances(vec![funded(quote(), d("10000"))], FeeModelConfig::default())
13247        };
13248        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13249
13250        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
13251
13252        assert_eq!(
13253            filled_balance(&outcome).balance,
13254            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.1"), Decimal::ZERO),
13255            "the sell opens a base balance with the loan, and delivers what it borrowed"
13256        );
13257    }
13258
13259    #[test]
13260    fn a_margin_buy_borrows_quote_valued_through_the_inverse_of_its_pair() {
13261        // Valued in BTC, so a USDT loan is valued at the inverse of BTCUSDT's price.
13262        let config = MockExecutionConfig {
13263            spot_margin: Some(SpotMarginConfig::new(base(), d("2"), MarginEffect::Borrow).unwrap()),
13264            ..spot_config("1", "0", FeeModelConfig::default())
13265        };
13266        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13267
13268        // Equity of 1 BTC at leverage 2 lends up to 1 BTC's worth: 50,000 USDT.
13269        let outcome = venue.open_order(buy_request("1.5", market_prices("50000")));
13270        assert!(
13271            insufficient_reason(&outcome).contains("Borrowable: 50000"),
13272            "{}",
13273            insufficient_reason(&outcome)
13274        );
13275
13276        let outcome = venue.open_order(buy_request("0.5", market_prices("50000")));
13277        assert_eq!(
13278            filled_balance(&outcome).balance,
13279            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("25000"), Decimal::ZERO)
13280        );
13281        assert_eq!(balance_now(&venue, base()).total, d("1.5"));
13282    }
13283
13284    #[test]
13285    fn a_request_snapshot_values_only_the_pair_it_names() {
13286        // Request-priced, so the venue holds no market and values from the request's snapshot.
13287        let venue = SimulatedVenue::new(
13288            &spot_config("1", "0", FeeModelConfig::default()),
13289            spot_instruments(),
13290        );
13291        let snapshot = market_prices("50000").unwrap();
13292        let eth = || AssetNameExchange::new("ETH");
13293        let price_in = |asset: AssetNameExchange,
13294                        valuation: AssetNameExchange,
13295                        priced: Underlying<AssetNameExchange>| {
13296            venue.price_in(&asset, &valuation, Some((&priced, &snapshot)))
13297        };
13298
13299        assert_eq!(
13300            price_in(base(), quote(), Underlying::new(base(), quote())),
13301            Some(d("50000"))
13302        );
13303        assert_eq!(
13304            price_in(quote(), base(), Underlying::new(base(), quote())),
13305            Some(d("0.00002")),
13306            "valued through the inverse of the pair the snapshot names"
13307        );
13308        assert_eq!(
13309            price_in(base(), quote(), Underlying::new(base(), eth())),
13310            None,
13311            "a snapshot of another pair sharing the base prices nothing"
13312        );
13313        assert_eq!(
13314            price_in(base(), quote(), Underlying::new(eth(), quote())),
13315            None,
13316            "a snapshot of another pair sharing the quote prices nothing"
13317        );
13318    }
13319
13320    #[test]
13321    fn a_venue_reads_back_the_instruments_it_was_constructed_with() {
13322        let instruments = fee_asset_instruments();
13323        let venue = SimulatedVenue::new(
13324            &spot_config("1", "0", FeeModelConfig::default()),
13325            instruments.clone(),
13326        );
13327
13328        assert_eq!(venue.instruments(), &instruments);
13329        assert_eq!(
13330            venue.instrument(&instrument_name()),
13331            instruments.get(&instrument_name())
13332        );
13333        assert!(venue.instrument(&instrument_name()).is_some());
13334        assert_eq!(
13335            venue.instrument(&InstrumentNameExchange::new("ETHUSDT")),
13336            None
13337        );
13338    }
13339
13340    #[test]
13341    fn a_margin_buy_repays_interest_then_principal_and_frees_the_rest() {
13342        let config = MockExecutionConfig {
13343            spot_margin: Some(
13344                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
13345            ),
13346            ..config_from_balances(
13347                vec![
13348                    AssetBalance::new(
13349                        base(),
13350                        Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.01")),
13351                        Utc::now(),
13352                    ),
13353                    funded(quote(), d("100000")),
13354                ],
13355                FeeModelConfig::default(),
13356            )
13357        };
13358        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13359
13360        let outcome = venue.open_order(with_effect(
13361            buy_request("1", market_prices("50000")),
13362            MarginEffect::Repay,
13363        ));
13364
13365        assert_eq!(
13366            filled_received(&outcome).balance,
13367            Balance::new_margin(d("0.24"), d("0.24"), Decimal::ZERO, Decimal::ZERO),
13368            "1 BTC repays 0.01 of interest and 0.75 of principal, leaving 0.24 free"
13369        );
13370    }
13371
13372    #[test]
13373    fn a_buy_smaller_than_the_debt_repays_interest_before_principal() {
13374        let config = MockExecutionConfig {
13375            spot_margin: Some(
13376                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
13377            ),
13378            ..config_from_balances(
13379                vec![
13380                    AssetBalance::new(
13381                        base(),
13382                        Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.01")),
13383                        Utc::now(),
13384                    ),
13385                    funded(quote(), d("100000")),
13386                ],
13387                FeeModelConfig::default(),
13388            )
13389        };
13390        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13391
13392        let outcome = venue.open_order(with_effect(
13393            buy_request("0.11", market_prices("50000")),
13394            MarginEffect::Repay,
13395        ));
13396
13397        assert_eq!(
13398            filled_received(&outcome).balance,
13399            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.65"), Decimal::ZERO)
13400        );
13401    }
13402
13403    #[test]
13404    fn a_borrow_beyond_the_leverage_limit_is_refused_naming_what_would_be_lent() {
13405        // Equity of 10,000 USDT at leverage 2 lends up to 10,000 USDT's worth: 0.2 BTC.
13406        let mut venue = margin_venue("0", "10000", "2");
13407
13408        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
13409        assert!(
13410            insufficient_reason(&outcome).contains("Borrowable: 0.2"),
13411            "{}",
13412            insufficient_reason(&outcome)
13413        );
13414        assert_eq!(
13415            balance_now(&venue, base()),
13416            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
13417            "a refused order borrows nothing"
13418        );
13419
13420        let outcome = venue.open_order(sell_request("0.2", market_prices("50000")));
13421        assert_eq!(filled_trade(&outcome).quantity, d("0.2"));
13422    }
13423
13424    #[test]
13425    fn debt_already_owed_counts_against_what_the_venue_lends() {
13426        // Equity 10,000 at leverage 2 lends 10,000 USDT's worth in all.
13427        let mut venue = margin_venue("0", "10000", "2");
13428        let outcome = venue.open_order(sell_request("0.15", market_prices("50000")));
13429        assert_eq!(filled_trade(&outcome).quantity, d("0.15"));
13430
13431        // 7,500 of the 10,000 is owed, so 2,500 is left: 0.05 BTC.
13432        let outcome = venue.open_order(sell_request("0.06", market_prices("50000")));
13433        assert!(
13434            insufficient_reason(&outcome).contains("Borrowable: 0.05"),
13435            "{}",
13436            insufficient_reason(&outcome)
13437        );
13438    }
13439
13440    #[test]
13441    fn a_leverage_of_one_lends_nothing_but_every_balance_reports_its_loan() {
13442        let mut venue = margin_venue("0", "10000", "1");
13443
13444        for balance in venue.balances(&[base(), quote()]) {
13445            assert_eq!(
13446                balance.balance.margin,
13447                Some(MarginDetails::default()),
13448                "{balance:?}"
13449            );
13450        }
13451
13452        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
13453        assert!(
13454            insufficient_reason(&outcome).contains("Borrowable: 0"),
13455            "{}",
13456            insufficient_reason(&outcome)
13457        );
13458    }
13459
13460    #[test]
13461    fn no_side_effect_lends_nothing_and_repays_nothing() {
13462        let config = MockExecutionConfig {
13463            spot_margin: Some(
13464                SpotMarginConfig::new(quote(), d("3"), MarginEffect::NoSideEffect).unwrap(),
13465            ),
13466            ..config_from_balances(
13467                vec![
13468                    AssetBalance::new(
13469                        base(),
13470                        Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.5"), Decimal::ZERO),
13471                        Utc::now(),
13472                    ),
13473                    funded(quote(), d("100000")),
13474                ],
13475                FeeModelConfig::default(),
13476            )
13477        };
13478        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13479
13480        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
13481        assert!(
13482            !insufficient_reason(&outcome).contains("Borrowable"),
13483            "{}",
13484            insufficient_reason(&outcome)
13485        );
13486
13487        let outcome = venue.open_order(buy_request("1", market_prices("50000")));
13488        assert_eq!(
13489            filled_received(&outcome).balance,
13490            Balance::new_margin(d("1"), d("1"), d("0.5"), Decimal::ZERO),
13491            "the bought base is free; the loan is left as it was"
13492        );
13493    }
13494
13495    #[test]
13496    fn an_asset_the_venue_cannot_value_is_not_lent() {
13497        // No instrument pairs BTC with EUR, so a BTC loan cannot be valued.
13498        let config = MockExecutionConfig {
13499            spot_margin: Some(
13500                SpotMarginConfig::new(AssetNameExchange::new("EUR"), d("3"), MarginEffect::Borrow)
13501                    .unwrap(),
13502            ),
13503            ..spot_config("0", "100000", FeeModelConfig::default())
13504        };
13505        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13506
13507        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
13508        assert!(
13509            insufficient_reason(&outcome).contains("Borrowable: 0"),
13510            "{}",
13511            insufficient_reason(&outcome)
13512        );
13513    }
13514
13515    #[test]
13516    fn a_cfd_order_is_not_lent_its_margin() {
13517        let config = MockExecutionConfig {
13518            spot_margin: Some(SpotMarginConfig::new(usd(), d("3"), MarginEffect::Borrow).unwrap()),
13519            ..config_from_balances(vec![funded(usd(), d("1000"))], FeeModelConfig::default())
13520        };
13521        let mut venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
13522
13523        let outcome = venue.open_order(cfd_request(Side::Sell, "1", market_prices("5000")));
13524        assert!(
13525            insufficient_reason(&outcome).contains("Borrowable: 0"),
13526            "{}",
13527            insufficient_reason(&outcome)
13528        );
13529        assert_eq!(
13530            balance_now(&venue, usd()),
13531            Balance::new_margin(d("1000"), d("1000"), Decimal::ZERO, Decimal::ZERO)
13532        );
13533    }
13534
13535    /// The error an order was refused with.
13536    fn open_error(outcome: &OpenOutcome) -> &UnindexedOrderError {
13537        match &outcome.response.state {
13538            OrderState::Inactive(InactiveOrderState::OpenFailed(error)) => error,
13539            other => panic!("expected the order to be refused, got: {other:?}"),
13540        }
13541    }
13542
13543    /// A margin account owing 0.75 BTC and 0.01 of interest on it, with `effect` as its default.
13544    fn config_owing_base(effect: MarginEffect) -> MockExecutionConfig {
13545        MockExecutionConfig {
13546            spot_margin: Some(SpotMarginConfig::new(quote(), d("3"), effect).unwrap()),
13547            ..config_from_balances(
13548                vec![
13549                    AssetBalance::new(
13550                        base(),
13551                        Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.01")),
13552                        Utc::now(),
13553                    ),
13554                    funded(quote(), d("100000")),
13555                ],
13556                FeeModelConfig::default(),
13557            )
13558        }
13559    }
13560
13561    /// A fed venue for `config`, its book at 49,000 / 49,100.
13562    fn market_venue_for(config: &MockExecutionConfig) -> SimulatedVenue {
13563        let mut venue = SimulatedVenue::new_market_driven(config, spot_instruments());
13564        advance(&mut venue, time(1));
13565        assert!(
13566            venue
13567                .apply_market(
13568                    &instrument_name(),
13569                    MarketObservation::new(book("49000", "49100")),
13570                    time(1),
13571                )
13572                .is_empty()
13573        );
13574        venue
13575    }
13576
13577    #[test]
13578    fn a_borrow_order_repays_nothing_from_what_it_receives() {
13579        let mut venue =
13580            SimulatedVenue::new(&config_owing_base(MarginEffect::Borrow), spot_instruments());
13581
13582        let outcome = venue.open_order(buy_request("1", market_prices("50000")));
13583
13584        assert_eq!(
13585            filled_received(&outcome).balance,
13586            Balance::new_margin(d("1"), d("1"), d("0.75"), d("0.01")),
13587            "the bought base is free and the loan, interest included, is left owed"
13588        );
13589    }
13590
13591    #[test]
13592    fn a_request_effect_overrides_the_configured_default() {
13593        // A venue that borrows by default, told not to: the sell is refused as on a cash account.
13594        let mut venue = margin_venue("0", "100000", "3");
13595        let outcome = venue.open_order(with_effect(
13596            sell_request("1", market_prices("50000")),
13597            MarginEffect::NoSideEffect,
13598        ));
13599        assert!(
13600            !insufficient_reason(&outcome).contains("Borrowable"),
13601            "{}",
13602            insufficient_reason(&outcome)
13603        );
13604
13605        // A venue that borrows nothing by default, told to: the same sell borrows its base.
13606        let config = MockExecutionConfig {
13607            spot_margin: Some(
13608                SpotMarginConfig::new(quote(), d("3"), MarginEffect::NoSideEffect).unwrap(),
13609            ),
13610            ..spot_config("0", "100000", FeeModelConfig::default())
13611        };
13612        let mut venue = SimulatedVenue::new(&config, spot_instruments());
13613        let outcome = venue.open_order(with_effect(
13614            sell_request("1", market_prices("50000")),
13615            MarginEffect::Borrow,
13616        ));
13617        assert_eq!(
13618            filled_balance(&outcome).balance,
13619            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("1"), Decimal::ZERO)
13620        );
13621    }
13622
13623    #[test]
13624    fn a_repay_order_borrows_nothing() {
13625        let mut venue = margin_venue("0.25", "100000", "3");
13626
13627        let outcome = venue.open_order(with_effect(
13628            sell_request("1", market_prices("50000")),
13629            MarginEffect::Repay,
13630        ));
13631
13632        assert!(
13633            !insufficient_reason(&outcome).contains("Borrowable"),
13634            "{}",
13635            insufficient_reason(&outcome)
13636        );
13637        assert_eq!(
13638            balance_now(&venue, base()),
13639            Balance::new_margin(d("0.25"), d("0.25"), Decimal::ZERO, Decimal::ZERO),
13640            "a refused order borrows nothing"
13641        );
13642    }
13643
13644    #[test]
13645    fn a_cfd_order_naming_a_borrow_or_repay_is_refused_unsent() {
13646        let config = MockExecutionConfig {
13647            spot_margin: Some(SpotMarginConfig::new(usd(), d("3"), MarginEffect::Borrow).unwrap()),
13648            ..config_from_balances(vec![funded(usd(), d("1000"))], FeeModelConfig::default())
13649        };
13650        let mut venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
13651
13652        for effect in [MarginEffect::Borrow, MarginEffect::Repay] {
13653            let outcome = venue.open_order(with_effect(
13654                cfd_request(Side::Buy, "1", market_prices("5000")),
13655                effect,
13656            ));
13657            assert_eq!(
13658                open_error(&outcome),
13659                &UnindexedOrderError::UnsupportedMarginEffect(effect)
13660            );
13661        }
13662        assert_eq!(
13663            balance_now(&venue, usd()),
13664            Balance::new_margin(d("1000"), d("1000"), Decimal::ZERO, Decimal::ZERO),
13665            "a refused order touches nothing"
13666        );
13667
13668        // `NoSideEffect` is honoured: a CFD never borrows, so it is not refused for its effect
13669        // whatever else the venue makes of it.
13670        let outcome = venue.open_order(with_effect(
13671            cfd_request(Side::Buy, "0.01", market_prices("5000")),
13672            MarginEffect::NoSideEffect,
13673        ));
13674        assert!(
13675            !matches!(
13676                &outcome.response.state,
13677                OrderState::Inactive(InactiveOrderState::OpenFailed(
13678                    UnindexedOrderError::UnsupportedMarginEffect(_)
13679                ))
13680            ),
13681            "{:?}",
13682            outcome.response.state
13683        );
13684    }
13685
13686    #[test]
13687    fn a_cash_venue_takes_no_effect_but_no_side_effect() {
13688        let mut venue = make_venue("1", "100000");
13689
13690        for effect in [MarginEffect::Borrow, MarginEffect::Repay] {
13691            let outcome = venue.open_order(with_effect(
13692                sell_request("0.1", market_prices("50000")),
13693                effect,
13694            ));
13695            assert_eq!(
13696                open_error(&outcome),
13697                &UnindexedOrderError::UnsupportedMarginEffect(effect)
13698            );
13699        }
13700
13701        let outcome = venue.open_order(with_effect(
13702            sell_request("0.1", market_prices("50000")),
13703            MarginEffect::NoSideEffect,
13704        ));
13705        assert_eq!(filled_trade(&outcome).quantity, d("0.1"));
13706    }
13707
13708    #[test]
13709    fn a_resting_order_repays_from_its_fill_as_it_was_placed_to() {
13710        // The venue borrows by default, but this buy was placed to repay: its effect rides on the
13711        // order, not on the configuration, so the fill that comes later repays the 0.76 owed.
13712        let mut venue = market_venue_for(&config_owing_base(MarginEffect::Borrow));
13713        let rested = venue.open_order(with_effect(
13714            limit_request("rest", Side::Buy, "1", "49050", gtc()),
13715            MarginEffect::Repay,
13716        ));
13717        assert_eq!(rested_open(&rested).filled_quantity, Decimal::ZERO);
13718
13719        let events = venue.apply_market(
13720            &instrument_name(),
13721            MarketObservation::new(book("48900", "49000")),
13722            time(2),
13723        );
13724
13725        assert_eq!(
13726            balance_of(&events[1]).balance,
13727            Balance::new_margin(d("0.24"), d("0.24"), Decimal::ZERO, Decimal::ZERO),
13728            "1 BTC repays 0.01 of interest and 0.75 of principal, leaving 0.24 free"
13729        );
13730    }
13731
13732    #[test]
13733    fn a_resting_order_placed_to_borrow_repays_nothing_from_its_fill() {
13734        let mut venue = market_venue_for(&config_owing_base(MarginEffect::Repay));
13735        venue.open_order(with_effect(
13736            limit_request("rest", Side::Buy, "1", "49050", gtc()),
13737            MarginEffect::Borrow,
13738        ));
13739
13740        let events = venue.apply_market(
13741            &instrument_name(),
13742            MarketObservation::new(book("48900", "49000")),
13743            time(2),
13744        );
13745
13746        assert_eq!(
13747            balance_of(&events[1]).balance,
13748            Balance::new_margin(d("1"), d("1"), d("0.75"), d("0.01")),
13749            "the loan is left owed although the venue's default would have repaid it"
13750        );
13751    }
13752
13753    #[test]
13754    fn without_auto_repay_at_cancel_a_cancelled_order_leaves_its_loan_owed() {
13755        let config = MockExecutionConfig {
13756            spot_margin: Some(
13757                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow)
13758                    .unwrap()
13759                    .with_auto_repay_at_cancel(false),
13760            ),
13761            ..spot_config("0", "100000", FeeModelConfig::default())
13762        };
13763        let mut venue = market_venue_for(&config);
13764        venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
13765
13766        let cancelled = venue.cancel_order(OrderRequestCancel {
13767            key: limit_request("rest", Side::Sell, "1", "50000", gtc()).key,
13768            state: RequestCancel { id: None },
13769        });
13770
13771        assert!(cancelled.response.state.is_ok());
13772        assert_eq!(
13773            balance_of(&cancelled.events[0]).balance,
13774            Balance::new_margin(d("1"), d("1"), d("1"), Decimal::ZERO),
13775            "the released base is free again and the loan that paid for it stays owed"
13776        );
13777    }
13778
13779    #[test]
13780    fn a_resting_sell_borrows_when_placed_and_a_cancel_repays_it() {
13781        let mut venue = margin_market_venue("0", "100000");
13782
13783        let outcome = venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
13784        assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
13785        assert_eq!(
13786            balance_of(&outcome.events[0]).balance,
13787            Balance::new_margin(d("1"), Decimal::ZERO, d("1"), Decimal::ZERO),
13788            "the borrowed base is held against the order"
13789        );
13790
13791        let cancelled = venue.cancel_order(OrderRequestCancel {
13792            key: limit_request("rest", Side::Sell, "1", "50000", gtc()).key,
13793            state: RequestCancel { id: None },
13794        });
13795        assert!(cancelled.response.state.is_ok());
13796        assert_eq!(
13797            balance_of(&cancelled.events[0]).balance,
13798            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
13799            "the cancel repays what the order borrowed"
13800        );
13801    }
13802
13803    #[test]
13804    fn a_resting_sell_that_fills_keeps_its_loan() {
13805        let mut venue = margin_market_venue("0", "100000");
13806        venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
13807
13808        let events = venue.apply_market(
13809            &instrument_name(),
13810            MarketObservation::new(book("50100", "50200")),
13811            time(2),
13812        );
13813
13814        assert_eq!(
13815            balance_of(&events[0]).balance,
13816            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("1"), Decimal::ZERO)
13817        );
13818        assert_eq!(trade_of(&events[2]).quantity, d("1"));
13819    }
13820
13821    #[test]
13822    fn a_partly_filled_sell_holds_its_loan_against_the_remainder() {
13823        let mut venue = margin_market_venue("0.4", "100000");
13824        // Only 0.5 bid at 49,000, so a marketable sell of 1 fills half and rests half.
13825        assert!(
13826            venue
13827                .apply_market(
13828                    &instrument_name(),
13829                    MarketObservation::new(book("49000", "49100"))
13830                        .with_depth(MarketDepth::new(Some(d("0.5")), None)),
13831                    time(2),
13832                )
13833                .is_empty()
13834        );
13835
13836        let outcome = venue.open_order(limit_request("part", Side::Sell, "1", "49000", gtc()));
13837        assert_eq!(rested_open(&outcome).filled_quantity, d("0.5"));
13838        assert_eq!(
13839            filled_balance(&outcome).balance,
13840            Balance::new_margin(d("0.5"), Decimal::ZERO, d("0.6"), Decimal::ZERO),
13841            "0.4 held plus 0.6 borrowed: 0.5 sold and 0.5 held against the rest"
13842        );
13843
13844        let cancelled = venue.cancel_order(OrderRequestCancel {
13845            key: limit_request("part", Side::Sell, "1", "49000", gtc()).key,
13846            state: RequestCancel { id: None },
13847        });
13848        assert_eq!(
13849            balance_of(&cancelled.events[0]).balance,
13850            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.1"), Decimal::ZERO),
13851            "the fill spent the account's own 0.4 first, so the cancel repays 0.5 of the loan"
13852        );
13853    }
13854
13855    #[test]
13856    fn a_held_market_sell_borrows_when_accepted_and_keeps_the_loan_when_its_bar_fills_it() {
13857        let config = MockExecutionConfig {
13858            market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
13859            ..margin_config("0", "1000", "3")
13860        };
13861        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
13862        advance(&mut venue, time(1));
13863        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
13864
13865        let outcome = venue.open_order(market_order("held", Side::Sell, "2", gtc()));
13866        assert_eq!(
13867            balance_of(&outcome.events[0]).balance,
13868            Balance::new_margin(d("2"), Decimal::ZERO, d("2"), Decimal::ZERO)
13869        );
13870
13871        let events = feed_bar(&mut venue, 2, ohlc("98", "99", "97", "98"));
13872        assert_eq!(
13873            balance_of(&events[0]).balance,
13874            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("2"), Decimal::ZERO)
13875        );
13876        assert_eq!(trade_of(&events[2]).price, d("98"));
13877    }
13878
13879    #[test]
13880    fn a_held_market_sell_that_expires_repays_its_loan() {
13881        let config = MockExecutionConfig {
13882            market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
13883            ..margin_config("0", "1000", "3")
13884        };
13885        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
13886        advance(&mut venue, time(1));
13887        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
13888        venue.open_order(market_order("held", Side::Sell, "2", gtc()));
13889
13890        let events = venue.expire_held(time(2));
13891
13892        assert_eq!(
13893            balance_of(&events[0]).balance,
13894            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO)
13895        );
13896    }
13897
13898    /// A venue holding market orders for the next bar, on margin, with 100 USDT and no BTC, fed
13899    /// one bar that closed at 100.
13900    fn margin_venue_holding() -> SimulatedVenue {
13901        let config = MockExecutionConfig {
13902            market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
13903            ..margin_config("0", "100", "2")
13904        };
13905        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
13906        advance(&mut venue, time(1));
13907        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
13908        venue
13909    }
13910
13911    #[test]
13912    fn a_held_buy_costed_higher_at_its_bar_borrows_the_difference() {
13913        let mut venue = margin_venue_holding();
13914        // 150 at the close of 100: 50 borrowed of the 100 equity allows.
13915        let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
13916        assert_eq!(
13917            balance_of(sans_debt(&outcome.events)[0]).balance,
13918            Balance::new_margin(d("150"), Decimal::ZERO, d("50"), Decimal::ZERO)
13919        );
13920
13921        // 180 at an open of 120: 30 more borrowed, within the 50 left.
13922        let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
13923        assert_eq!(
13924            balance_of(sans_debt(&events)[0]).balance,
13925            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("80"), Decimal::ZERO)
13926        );
13927        assert_eq!(trade_of(sans_debt(&events)[2]).price, d("120"));
13928    }
13929
13930    #[test]
13931    fn a_held_buy_costed_lower_at_its_bar_repays_the_loan_it_no_longer_needs() {
13932        let mut venue = margin_venue_holding();
13933        // 150 at the close of 100: 50 borrowed.
13934        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
13935
13936        // 120 at an open of 80: the 100 the account had pays first, so 20 of the loan is used
13937        // and the other 30 is repaid.
13938        let events = feed_bar(&mut venue, 2, ohlc("80", "81", "79", "80"));
13939        assert_eq!(
13940            balance_of(sans_debt(&events)[0]).balance,
13941            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("20"), Decimal::ZERO)
13942        );
13943        assert_eq!(
13944            balance_of(sans_debt(&events)[1]).balance,
13945            Balance::new_margin(d("1.5"), d("1.5"), Decimal::ZERO, Decimal::ZERO)
13946        );
13947        assert_eq!(trade_of(sans_debt(&events)[2]).price, d("80"));
13948    }
13949
13950    #[test]
13951    fn a_held_buy_unaffordable_at_its_bar_is_cancelled_and_repays_its_loan() {
13952        let mut venue = margin_venue_holding();
13953        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
13954
13955        // 375 at an open of 250 needs 225 more than the 150 held, and only 50 more may be lent.
13956        let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
13957
13958        assert_eq!(
13959            balance_of(sans_debt(&events)[0]).balance,
13960            Balance::new_margin(d("100"), d("100"), Decimal::ZERO, Decimal::ZERO),
13961            "the cancel repays the 50 the order borrowed when it was accepted"
13962        );
13963        assert!(matches!(
13964            order_of(sans_debt(&events)[1]).state,
13965            OrderState::Inactive(InactiveOrderState::Cancelled(_))
13966        ));
13967    }
13968
13969    /// [`margin_venue_holding`], with `config` adjusted by `adjust`.
13970    fn margin_venue_holding_with(
13971        adjust: impl FnOnce(SpotMarginConfig) -> SpotMarginConfig,
13972        effect: MarginEffect,
13973    ) -> SimulatedVenue {
13974        let mut config = MockExecutionConfig {
13975            market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
13976            ..margin_config("0", "100", "2")
13977        };
13978        config.spot_margin = Some(adjust(
13979            SpotMarginConfig::new(quote(), d("2"), effect).unwrap(),
13980        ));
13981        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
13982        advance(&mut venue, time(1));
13983        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
13984        venue
13985    }
13986
13987    #[test]
13988    fn without_auto_repay_at_cancel_a_held_buy_costed_lower_keeps_the_loan_it_no_longer_needs() {
13989        let mut venue = margin_venue_holding_with(
13990            |config| config.with_auto_repay_at_cancel(false),
13991            MarginEffect::Borrow,
13992        );
13993        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
13994
13995        // 120 at an open of 80: 20 of the 50 loan is used, and the other 30 stays owed.
13996        let events = feed_bar(&mut venue, 2, ohlc("80", "81", "79", "80"));
13997
13998        assert_eq!(
13999            balance_of(&events[0]).balance,
14000            Balance::new_margin(d("30"), d("30"), d("50"), Decimal::ZERO)
14001        );
14002    }
14003
14004    #[test]
14005    fn without_auto_repay_at_cancel_a_held_buy_unaffordable_at_its_bar_keeps_its_loan() {
14006        let mut venue = margin_venue_holding_with(
14007            |config| config.with_auto_repay_at_cancel(false),
14008            MarginEffect::Borrow,
14009        );
14010        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
14011
14012        let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
14013
14014        assert_eq!(
14015            balance_of(&events[0]).balance,
14016            Balance::new_margin(d("150"), d("150"), d("50"), Decimal::ZERO),
14017            "the cancel gives the hold back and leaves the 50 the order borrowed owed"
14018        );
14019        assert!(matches!(
14020            order_of(&events[1]).state,
14021            OrderState::Inactive(InactiveOrderState::Cancelled(_))
14022        ));
14023    }
14024
14025    #[test]
14026    fn without_auto_repay_at_cancel_a_held_sell_that_expires_keeps_its_loan() {
14027        let mut venue = margin_venue_holding_with(
14028            |config| config.with_auto_repay_at_cancel(false),
14029            MarginEffect::Borrow,
14030        );
14031        venue.open_order(market_order("held", Side::Sell, "1", gtc()));
14032
14033        let events = venue.expire_held(time(2));
14034
14035        assert_eq!(
14036            balance_of(&events[0]).balance,
14037            Balance::new_margin(d("1"), d("1"), d("1"), Decimal::ZERO)
14038        );
14039    }
14040
14041    #[test]
14042    fn a_held_buy_costed_afresh_at_its_bar_borrows_as_it_was_placed_to() {
14043        // The venue borrows nothing by default, but this buy was placed to borrow: at its bar
14044        // the fill is costed afresh, and the 30 more it needs is lent because of the effect it
14045        // was placed with, not the configuration's.
14046        let mut venue = margin_venue_holding_with(|config| config, MarginEffect::NoSideEffect);
14047        let mut request = market_order("held", Side::Buy, "1.5", gtc());
14048        request.state = request.state.with_margin_effect(MarginEffect::Borrow);
14049        venue.open_order(request);
14050
14051        let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
14052
14053        assert_eq!(
14054            balance_of(sans_debt(&events)[0]).balance,
14055            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("80"), Decimal::ZERO)
14056        );
14057        assert_eq!(trade_of(sans_debt(&events)[2]).price, d("120"));
14058    }
14059
14060    // ---------------------------------------------------------------------------------------
14061    // Spot margin: an asset's minimum borrow.
14062    // ---------------------------------------------------------------------------------------
14063
14064    /// `config`, its spot margin lending no less than `min_borrow` of `asset`.
14065    fn with_min_borrow(
14066        mut config: MockExecutionConfig,
14067        asset: AssetNameExchange,
14068        min_borrow: &str,
14069    ) -> MockExecutionConfig {
14070        config.spot_margin = config
14071            .spot_margin
14072            .map(|margin| margin.with_min_borrow(asset, d(min_borrow)).unwrap());
14073        config
14074    }
14075
14076    #[test]
14077    fn a_shortfall_below_the_minimum_borrows_the_minimum_and_the_rest_stays_free() {
14078        let config = with_min_borrow(margin_config("0.25", "100000", "3"), base(), "0.5");
14079        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14080
14081        // 0.3 of the 0.25 held lacks 0.05, so the minimum 0.5 is lent.
14082        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14083
14084        assert_eq!(
14085            filled_balance(&outcome).balance,
14086            Balance::new_margin(d("0.45"), d("0.45"), d("0.5"), Decimal::ZERO),
14087            "0.25 + 0.5 lent − 0.3 sold, and what the sell did not spend is free"
14088        );
14089        assert_eq!(filled_trade(&outcome).quantity, d("0.3"));
14090    }
14091
14092    #[test]
14093    fn interest_is_charged_on_the_whole_minimum_lent() {
14094        let config = with_min_borrow(margin_config("0.25", "100000", "3"), base(), "0.5");
14095        let mut venue = SimulatedVenue::new(&config, spot_instruments())
14096            .with_margin_interest(Arc::new(hourly_interest(base())));
14097        advance(&mut venue, time(1));
14098
14099        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14100
14101        assert_eq!(
14102            filled_balance(&outcome).balance,
14103            Balance::new_margin(d("0.45"), d("0.45"), d("0.5"), d("0.00005"))
14104        );
14105        assert_eq!(
14106            *cash_flow_of(sans_debt(&outcome.events)[3]),
14107            interest_flow(
14108                base(),
14109                "0.5",
14110                "0.00005",
14111                time(1),
14112                InterestTiming::OnBorrow,
14113                "0"
14114            ),
14115        );
14116    }
14117
14118    #[test]
14119    fn a_shortfall_above_the_minimum_borrows_only_the_shortfall() {
14120        let config = with_min_borrow(margin_config("0.25", "100000", "3"), base(), "0.5");
14121        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14122
14123        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
14124
14125        assert_eq!(
14126            filled_balance(&outcome).balance,
14127            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), Decimal::ZERO)
14128        );
14129    }
14130
14131    #[test]
14132    fn a_minimum_on_another_asset_lends_only_the_shortfall() {
14133        let config = with_min_borrow(margin_config("0.25", "100000", "3"), quote(), "1000");
14134        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14135
14136        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14137
14138        assert_eq!(
14139            filled_balance(&outcome).balance,
14140            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.05"), Decimal::ZERO)
14141        );
14142    }
14143
14144    #[test]
14145    fn a_minimum_borrow_beyond_what_the_venue_lends_refuses_the_order() {
14146        // Equity of 10,000 USDT at leverage 2 lends up to 0.2 BTC.
14147        let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.21");
14148        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14149
14150        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
14151
14152        assert!(
14153            insufficient_reason(&outcome).contains("Borrowable: 0.2")
14154                && insufficient_reason(&outcome).ends_with("Minimum Borrow: 0.21"),
14155            "the reason names what the venue would lend and the minimum it would not: {}",
14156            insufficient_reason(&outcome)
14157        );
14158        assert_eq!(
14159            balance_now(&venue, base()),
14160            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
14161            "a refused order borrows nothing"
14162        );
14163    }
14164
14165    #[test]
14166    fn a_shortfall_above_the_minimum_refused_by_the_leverage_limit_names_no_minimum() {
14167        let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.05");
14168        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14169
14170        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14171
14172        assert!(
14173            insufficient_reason(&outcome).contains("Borrowable: 0.2")
14174                && !insufficient_reason(&outcome).contains("Minimum Borrow"),
14175            "{}",
14176            insufficient_reason(&outcome)
14177        );
14178    }
14179
14180    #[test]
14181    fn a_shortfall_beyond_what_the_venue_lends_names_no_minimum() {
14182        // 0.3 lacks more than the 0.2 the venue lends, so the 0.5 minimum is not why it is refused.
14183        let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.5");
14184        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14185
14186        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14187
14188        assert!(
14189            insufficient_reason(&outcome).contains("Borrowable: 0.2")
14190                && !insufficient_reason(&outcome).contains("Minimum Borrow"),
14191            "{}",
14192            insufficient_reason(&outcome)
14193        );
14194    }
14195
14196    #[test]
14197    fn a_shortfall_of_exactly_what_the_venue_lends_names_the_minimum_that_refused_it() {
14198        // 0.2 is exactly what the venue lends, so the shortfall alone would have been lent.
14199        let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.5");
14200        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14201
14202        let outcome = venue.open_order(sell_request("0.2", market_prices("50000")));
14203
14204        assert!(
14205            insufficient_reason(&outcome).ends_with("Minimum Borrow: 0.5"),
14206            "{}",
14207            insufficient_reason(&outcome)
14208        );
14209    }
14210
14211    #[test]
14212    fn a_minimum_borrow_of_exactly_what_the_venue_lends_is_lent() {
14213        let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.2");
14214        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14215
14216        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
14217
14218        assert_eq!(
14219            filled_balance(&outcome).balance,
14220            Balance::new_margin(d("0.1"), d("0.1"), d("0.2"), Decimal::ZERO)
14221        );
14222    }
14223
14224    #[test]
14225    fn a_resting_sell_lent_the_minimum_holds_its_shortfall_and_a_cancel_repays_the_whole_loan() {
14226        let config = with_min_borrow(margin_config("0", "100000", "3"), base(), "0.5");
14227        let mut venue = market_venue_for(&config);
14228
14229        let outcome = venue.open_order(limit_request("rest", Side::Sell, "0.1", "50000", gtc()));
14230        assert_eq!(
14231            balance_of(&outcome.events[0]).balance,
14232            Balance::new_margin(d("0.5"), d("0.4"), d("0.5"), Decimal::ZERO),
14233            "0.1 is held against the order, and the 0.4 lent beyond it is free"
14234        );
14235
14236        let cancelled = venue.cancel_order(OrderRequestCancel {
14237            key: limit_request("rest", Side::Sell, "0.1", "50000", gtc()).key,
14238            state: RequestCancel { id: None },
14239        });
14240        assert!(cancelled.response.state.is_ok());
14241        assert_eq!(
14242            balance_of(&cancelled.events[0]).balance,
14243            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
14244            "the cancel repays all 0.5 the order borrowed"
14245        );
14246    }
14247
14248    #[test]
14249    fn without_auto_repay_at_cancel_a_cancel_leaves_the_whole_minimum_owed() {
14250        let mut config = with_min_borrow(margin_config("0", "100000", "3"), base(), "0.5");
14251        config.spot_margin = config
14252            .spot_margin
14253            .map(|margin| margin.with_auto_repay_at_cancel(false));
14254        let mut venue = market_venue_for(&config);
14255        venue.open_order(limit_request("rest", Side::Sell, "0.1", "50000", gtc()));
14256
14257        let cancelled = venue.cancel_order(OrderRequestCancel {
14258            key: limit_request("rest", Side::Sell, "0.1", "50000", gtc()).key,
14259            state: RequestCancel { id: None },
14260        });
14261
14262        assert_eq!(
14263            balance_of(&cancelled.events[0]).balance,
14264            Balance::new_margin(d("0.5"), d("0.5"), d("0.5"), Decimal::ZERO)
14265        );
14266    }
14267
14268    #[test]
14269    fn a_resting_sell_lent_the_minimum_that_fills_keeps_the_whole_loan() {
14270        let config = with_min_borrow(margin_config("0", "100000", "3"), base(), "0.5");
14271        let mut venue = market_venue_for(&config);
14272        venue.open_order(limit_request("rest", Side::Sell, "0.1", "50000", gtc()));
14273
14274        let events = venue.apply_market(
14275            &instrument_name(),
14276            MarketObservation::new(book("50100", "50200")),
14277            time(2),
14278        );
14279
14280        assert_eq!(
14281            balance_of(&events[0]).balance,
14282            Balance::new_margin(d("0.4"), d("0.4"), d("0.5"), Decimal::ZERO),
14283            "the filled order's loan stays owed, the part it did not spend included"
14284        );
14285        assert_eq!(trade_of(&events[2]).quantity, d("0.1"));
14286    }
14287
14288    #[test]
14289    fn a_partly_filled_sell_lent_the_minimum_repays_the_unspent_loan_at_cancel() {
14290        let config = with_min_borrow(margin_config("0.4", "100000", "3"), base(), "0.8");
14291        let mut venue = market_venue_for(&config);
14292        // Only 0.5 bid at 49,000, so a marketable sell of 1 fills half and rests half.
14293        assert!(
14294            venue
14295                .apply_market(
14296                    &instrument_name(),
14297                    MarketObservation::new(book("49000", "49100"))
14298                        .with_depth(MarketDepth::new(Some(d("0.5")), None)),
14299                    time(2),
14300                )
14301                .is_empty()
14302        );
14303
14304        // 1 of the 0.4 held lacks 0.6, so the minimum 0.8 is lent.
14305        let outcome = venue.open_order(limit_request("part", Side::Sell, "1", "49000", gtc()));
14306        assert_eq!(rested_open(&outcome).filled_quantity, d("0.5"));
14307        assert_eq!(
14308            filled_balance(&outcome).balance,
14309            Balance::new_margin(d("0.7"), d("0.2"), d("0.8"), Decimal::ZERO),
14310            "0.4 + 0.8 lent − 0.5 sold; 0.5 held against the rest and 0.2 free"
14311        );
14312
14313        let cancelled = venue.cancel_order(OrderRequestCancel {
14314            key: limit_request("part", Side::Sell, "1", "49000", gtc()).key,
14315            state: RequestCancel { id: None },
14316        });
14317        assert_eq!(
14318            balance_of(&cancelled.events[0]).balance,
14319            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.1"), Decimal::ZERO),
14320            "the 0.5 held against the rest and the 0.2 lent beyond the shortfall are repaid"
14321        );
14322    }
14323
14324    #[test]
14325    fn a_held_market_sell_lent_the_minimum_that_expires_repays_the_whole_loan() {
14326        let config = with_min_borrow(
14327            MockExecutionConfig {
14328                market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
14329                ..margin_config("0", "1000", "3")
14330            },
14331            base(),
14332            "3",
14333        );
14334        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
14335        advance(&mut venue, time(1));
14336        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
14337        let outcome = venue.open_order(market_order("held", Side::Sell, "2", gtc()));
14338        assert_eq!(
14339            balance_of(&outcome.events[0]).balance,
14340            Balance::new_margin(d("3"), d("1"), d("3"), Decimal::ZERO)
14341        );
14342
14343        let events = venue.expire_held(time(2));
14344
14345        assert_eq!(
14346            balance_of(&events[0]).balance,
14347            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO)
14348        );
14349    }
14350
14351    #[test]
14352    fn a_held_buy_lent_the_minimum_and_costed_lower_repays_only_the_held_loan_it_does_not_need() {
14353        let mut venue = margin_venue_holding_with(
14354            |config| config.with_min_borrow(quote(), d("80")).unwrap(),
14355            MarginEffect::Borrow,
14356        );
14357        // 150 at the close of 100 lacks 50, so the minimum 80 is lent: 30 of it free.
14358        let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
14359        assert_eq!(
14360            balance_of(&outcome.events[0]).balance,
14361            Balance::new_margin(d("180"), d("30"), d("80"), Decimal::ZERO)
14362        );
14363
14364        // 120 at an open of 80: of the 50 held on loan, 30 is not needed and is repaid; the 30
14365        // lent beyond the shortfall stays owed, since the order filled.
14366        let events = feed_bar(&mut venue, 2, ohlc("80", "81", "79", "80"));
14367        assert_eq!(
14368            balance_of(&events[0]).balance,
14369            Balance::new_margin(d("30"), d("30"), d("50"), Decimal::ZERO)
14370        );
14371    }
14372
14373    #[test]
14374    fn a_held_buy_lent_the_minimum_that_needs_none_of_its_loan_keeps_only_the_excess_owed() {
14375        let mut venue = margin_venue_holding_with(
14376            |config| config.with_min_borrow(quote(), d("80")).unwrap(),
14377            MarginEffect::Borrow,
14378        );
14379        // 150 at the close of 100 lacks 50, so the minimum 80 is lent: 30 of it free.
14380        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
14381
14382        // 30 at an open of 20 is paid from the account's own 100, so all 50 held on loan is
14383        // repaid, and only the 30 lent beyond the shortfall stays owed.
14384        let events = feed_bar(&mut venue, 2, ohlc("20", "21", "19", "20"));
14385        assert_eq!(
14386            balance_of(&events[0]).balance,
14387            Balance::new_margin(d("100"), d("100"), d("30"), Decimal::ZERO)
14388        );
14389    }
14390
14391    #[test]
14392    fn a_held_buy_costed_higher_at_its_bar_borrows_at_least_the_minimum_more() {
14393        let mut venue = margin_venue_holding_with(
14394            |config| config.with_min_borrow(quote(), d("40")).unwrap(),
14395            MarginEffect::Borrow,
14396        );
14397        // 150 at the close of 100 lacks 50, above the minimum, so 50 is lent.
14398        let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
14399        assert_eq!(
14400            balance_of(&outcome.events[0]).balance,
14401            Balance::new_margin(d("150"), Decimal::ZERO, d("50"), Decimal::ZERO)
14402        );
14403
14404        // 180 at an open of 120 lacks 30 more, below the minimum, so 40 is lent.
14405        let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
14406        assert_eq!(
14407            balance_of(&events[0]).balance,
14408            Balance::new_margin(d("10"), d("10"), d("90"), Decimal::ZERO)
14409        );
14410    }
14411
14412    #[test]
14413    fn a_held_buy_lent_the_minimum_and_unaffordable_at_its_bar_repays_the_whole_loan() {
14414        let mut venue = margin_venue_holding_with(
14415            |config| config.with_min_borrow(quote(), d("80")).unwrap(),
14416            MarginEffect::Borrow,
14417        );
14418        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
14419
14420        // 375 at an open of 250 is more than the venue lends.
14421        let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
14422
14423        assert_eq!(
14424            balance_of(sans_debt(&events)[0]).balance,
14425            Balance::new_margin(d("100"), d("100"), Decimal::ZERO, Decimal::ZERO),
14426            "the cancel repays all 80 the order borrowed"
14427        );
14428        assert!(matches!(
14429            order_of(sans_debt(&events)[1]).state,
14430            OrderState::Inactive(InactiveOrderState::Cancelled(_))
14431        ));
14432    }
14433
14434    #[test]
14435    fn a_held_limit_buy_lent_the_minimum_carries_it_to_the_remainder_it_rests() {
14436        let config = with_min_borrow(
14437            MockExecutionConfig {
14438                stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
14439                ..margin_config("0", "100", "2")
14440            },
14441            quote(),
14442            "80",
14443        );
14444        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
14445        advance(&mut venue, time(1));
14446        assert!(observe(&mut venue, 1, traded("100")).is_empty());
14447        advance(&mut venue, time(12));
14448        // 1.5 at the stale 100 lacks 50 of the 150, so the minimum 80 is lent: 30 of it free.
14449        let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
14450        assert_eq!(
14451            balance_of(&outcome.events[0]).balance,
14452            Balance::new_margin(d("180"), d("30"), d("80"), Decimal::ZERO)
14453        );
14454
14455        // 0.5 at 80 and 1 held at 100 need 140: 10 of the held loan is repaid.
14456        let events = venue.apply_market(
14457            &instrument_name(),
14458            MarketObservation::new(book("79", "80"))
14459                .with_depth(MarketDepth::new(Some(d("1")), Some(d("0.5")))),
14460            time(20),
14461        );
14462        assert_eq!(
14463            balance_of(&events[0]).balance,
14464            Balance::new_margin(d("130"), d("30"), d("70"), Decimal::ZERO)
14465        );
14466
14467        let cancelled = venue.cancel_order(OrderRequestCancel {
14468            key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
14469            state: RequestCancel { id: None },
14470        });
14471        assert!(cancelled.response.state.is_ok());
14472        assert_eq!(
14473            venue.balances(&[quote()])[0].balance,
14474            Balance::new_margin(d("60"), d("60"), Decimal::ZERO, Decimal::ZERO),
14475            "cancelling the remainder repays the 40 it held and the 30 lent beyond the shortfall"
14476        );
14477    }
14478
14479    #[test]
14480    fn a_held_limit_that_rests_untraded_at_its_release_keeps_its_interest_until_its_end() {
14481        let config = MockExecutionConfig {
14482            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
14483            ..margin_config("0", "100", "2")
14484        };
14485        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments())
14486            .with_margin_interest(Arc::new(hourly_interest(quote())));
14487        advance(&mut venue, time(1));
14488        assert!(observe(&mut venue, 1, traded("100")).is_empty());
14489        advance(&mut venue, time(12));
14490        // 1.5 at the stale 100 lacks 50 of the 150, which is lent and charged 0.005.
14491        let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
14492        assert_eq!(sans_debt(&outcome.events).len(), 1, "{:?}", outcome.events);
14493
14494        // A fresh market it does not cross: it joins the book whole, having traded nothing.
14495        let events = venue.apply_market(
14496            &instrument_name(),
14497            MarketObservation::new(book("101", "102")),
14498            time(20),
14499        );
14500        assert!(
14501            !events
14502                .iter()
14503                .any(|event| matches!(event.kind, AccountEventKind::CashFlow(_))),
14504            "still no position for the charge: {events:?}"
14505        );
14506
14507        advance(&mut venue, time(30));
14508        let cancelled = venue.cancel_order(OrderRequestCancel {
14509            key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
14510            state: RequestCancel { id: None },
14511        });
14512        assert_eq!(
14513            *cash_flow_of(sans_debt(&cancelled.events)[1]),
14514            interest_flow(
14515                quote(),
14516                "50",
14517                "0.005",
14518                time(30),
14519                InterestTiming::OnBorrow,
14520                "0"
14521            ),
14522            "carried to the book, and reported at the order's end"
14523        );
14524    }
14525
14526    #[test]
14527    fn a_held_limit_that_trades_in_part_at_its_release_reports_its_interest_once_after_the_trade() {
14528        let config = MockExecutionConfig {
14529            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
14530            ..margin_config("0", "100", "2")
14531        };
14532        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments())
14533            .with_margin_interest(Arc::new(hourly_interest(quote())));
14534        advance(&mut venue, time(1));
14535        assert!(observe(&mut venue, 1, traded("100")).is_empty());
14536        advance(&mut venue, time(12));
14537        // 1.5 at the stale 100 lacks 50 of the 150, which is lent and charged 0.005.
14538        let outcome = venue.open_order(gtc_limit("partial", Side::Buy, "1.5", "100"));
14539        assert_eq!(sans_debt(&outcome.events).len(), 1, "{:?}", outcome.events);
14540
14541        // A fresh market offering 0.5 under the limit: 0.5 trades and the other 1 joins the book.
14542        let events = venue.apply_market(
14543            &instrument_name(),
14544            MarketObservation::new(book("98", "99")).with_depth(sized("0.5")),
14545            time(20),
14546        );
14547        let events = sans_debt(&events);
14548        let traded_at = events
14549            .iter()
14550            .position(|event| matches!(event.kind, AccountEventKind::Trade(_)))
14551            .unwrap_or_else(|| panic!("a trade: {events:?}"));
14552        assert_eq!(trade_of(events[traded_at]).quantity, d("0.5"));
14553        assert_eq!(
14554            *cash_flow_of(events[traded_at + 1]),
14555            interest_flow(
14556                quote(),
14557                "50",
14558                "0.005",
14559                time(20),
14560                InterestTiming::OnBorrow,
14561                "0"
14562            ),
14563            "the hold's charge after the trade, stamped at it: {events:?}"
14564        );
14565        assert_eq!(
14566            events
14567                .iter()
14568                .filter(|event| matches!(event.kind, AccountEventKind::CashFlow(_)))
14569                .count(),
14570            1,
14571            "reported once: {events:?}"
14572        );
14573
14574        advance(&mut venue, time(30));
14575        let cancelled = venue.cancel_order(OrderRequestCancel {
14576            key: gtc_limit("partial", Side::Buy, "1.5", "100").key,
14577            state: RequestCancel { id: None },
14578        });
14579        assert!(
14580            !cancelled
14581                .events
14582                .iter()
14583                .any(|event| matches!(event.kind, AccountEventKind::CashFlow(_))),
14584            "the remainder carries no charge already reported: {:?}",
14585            cancelled.events
14586        );
14587    }
14588
14589    #[test]
14590    fn a_held_immediate_order_an_idle_bar_cancels_reports_its_interest_with_its_cancel() {
14591        let config = MockExecutionConfig {
14592            market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
14593            stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
14594            ..margin_config("0", "100", "2")
14595        };
14596        let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments())
14597            .with_margin_interest(Arc::new(hourly_interest(quote())));
14598        advance(&mut venue, time(1));
14599        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
14600        // 150 at the close of 100: 50 borrowed, charged 0.005, held for the next bar.
14601        let outcome = venue.open_order(market_order(
14602            "immediate",
14603            Side::Buy,
14604            "1.5",
14605            TimeInForce::ImmediateOrCancel,
14606        ));
14607        assert_eq!(sans_debt(&outcome.events).len(), 1, "{:?}", outcome.events);
14608
14609        let bar = ohlc("100", "100", "100", "100");
14610        let events = venue.apply_market(
14611            &instrument_name(),
14612            MarketObservation::new(MarketSnapshot::from_last_price(Some(bar.close)))
14613                .with_bar(Some(bar))
14614                .with_fresh(false),
14615            time(2),
14616        );
14617        let events = sans_debt(&events);
14618
14619        assert_eq!(events.len(), 3, "{events:?}");
14620        assert_eq!(
14621            *cash_flow_of(events[1]),
14622            interest_flow(
14623                quote(),
14624                "50",
14625                "0.005",
14626                time(2),
14627                InterestTiming::OnBorrow,
14628                "0"
14629            ),
14630            "the order never traded, so its cancel reports the charge, stamped then"
14631        );
14632        assert!(matches!(
14633            order_of(events[2]).state,
14634            OrderState::Inactive(InactiveOrderState::Cancelled(_))
14635        ));
14636    }
14637
14638    /// A margin venue also holding `eth`, which no instrument values, owing `eth_owed` of it.
14639    fn margin_venue_with_eth(eth: &str, eth_owed: &str) -> SimulatedVenue {
14640        let config = MockExecutionConfig {
14641            spot_margin: Some(
14642                SpotMarginConfig::new(quote(), d("2"), MarginEffect::Borrow).unwrap(),
14643            ),
14644            ..config_from_balances(
14645                vec![
14646                    funded(base(), Decimal::ZERO),
14647                    funded(quote(), d("10000")),
14648                    AssetBalance::new(
14649                        AssetNameExchange::new("ETH"),
14650                        Balance::new_margin(d(eth), d(eth), d(eth_owed), Decimal::ZERO),
14651                        Utc::now(),
14652                    ),
14653                ],
14654                FeeModelConfig::default(),
14655            )
14656        };
14657        SimulatedVenue::new(&config, spot_instruments())
14658    }
14659
14660    #[test]
14661    fn an_asset_the_venue_cannot_value_counts_as_no_equity() {
14662        // 5 ETH the venue cannot value adds nothing: 10,000 USDT at leverage 2 lends 0.2 BTC.
14663        let mut venue = margin_venue_with_eth("5", "0");
14664
14665        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14666        assert!(
14667            insufficient_reason(&outcome).contains("Borrowable: 0.2"),
14668            "{}",
14669            insufficient_reason(&outcome)
14670        );
14671    }
14672
14673    #[test]
14674    fn a_debt_the_venue_cannot_value_stops_it_lending() {
14675        let mut venue = margin_venue_with_eth("5", "1");
14676
14677        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
14678        assert!(
14679            insufficient_reason(&outcome).contains("Borrowable: 0"),
14680            "{}",
14681            insufficient_reason(&outcome)
14682        );
14683    }
14684
14685    #[test]
14686    fn interest_owed_counts_as_debt_against_what_the_venue_lends() {
14687        let config = MockExecutionConfig {
14688            spot_margin: Some(
14689                SpotMarginConfig::new(quote(), d("2"), MarginEffect::Borrow).unwrap(),
14690            ),
14691            ..config_from_balances(
14692                vec![
14693                    AssetBalance::new(
14694                        base(),
14695                        Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.05"), d("0.01")),
14696                        Utc::now(),
14697                    ),
14698                    funded(quote(), d("20000")),
14699                ],
14700                FeeModelConfig::default(),
14701            )
14702        };
14703        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14704
14705        // 0.06 BTC owed is 3,000: equity 17,000 at leverage 2 lends 17,000, less the 3,000
14706        // already owed, leaving 14,000: 0.28 BTC.
14707        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
14708        assert!(
14709            insufficient_reason(&outcome).contains("Borrowable: 0.28"),
14710            "{}",
14711            insufficient_reason(&outcome)
14712        );
14713    }
14714
14715    #[test]
14716    fn an_asset_a_margin_account_first_receives_reports_its_loan() {
14717        let config = MockExecutionConfig {
14718            spot_margin: Some(
14719                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
14720            ),
14721            ..config_from_balances(
14722                vec![funded(quote(), d("100000"))],
14723                FeeModelConfig::default(),
14724            )
14725        };
14726        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14727
14728        let outcome = venue.open_order(buy_request("1", market_prices("50000")));
14729
14730        assert_eq!(
14731            filled_received(&outcome).balance,
14732            Balance::new_margin(d("1"), d("1"), Decimal::ZERO, Decimal::ZERO)
14733        );
14734    }
14735
14736    #[test]
14737    fn an_asset_priced_at_zero_is_not_lent() {
14738        let mut venue = margin_venue("0", "10000", "2");
14739
14740        let outcome = venue.open_order(sell_request("0.1", market_prices("0")));
14741        assert!(
14742            insufficient_reason(&outcome).contains("Borrowable: 0"),
14743            "{}",
14744            insufficient_reason(&outcome)
14745        );
14746    }
14747
14748    #[test]
14749    fn a_pair_quoting_the_asset_in_another_does_not_value_it() {
14750        // AAVEBTC prices BTC in AAVE, not in the valuation asset, though it sorts first by name.
14751        let mut instruments = spot_instruments();
14752        let aave_btc = Instrument {
14753            exchange: EXCHANGE,
14754            name_internal: InstrumentNameInternal::new("aavebtc"),
14755            name_exchange: InstrumentNameExchange::new("AAVEBTC"),
14756            underlying: Underlying {
14757                base: AssetNameExchange::new("AAVE"),
14758                quote: base(),
14759            },
14760            quote: InstrumentQuoteAsset::UnderlyingQuote,
14761            kind: InstrumentKind::Spot,
14762            spec: None,
14763            data_venue: None,
14764        };
14765        instruments.insert(aave_btc.name_exchange.clone(), aave_btc);
14766        let mut venue =
14767            SimulatedVenue::new_market_driven(&margin_config("0", "10000", "2"), instruments);
14768        advance(&mut venue, time(1));
14769        for (name, snapshot) in [
14770            (instrument_name(), book("50000", "50000")),
14771            (InstrumentNameExchange::new("AAVEBTC"), book("0.05", "0.05")),
14772        ] {
14773            assert!(
14774                venue
14775                    .apply_market(&name, MarketObservation::new(snapshot), time(1))
14776                    .is_empty()
14777            );
14778        }
14779
14780        // Valued at 50,000 through BTCUSDT: equity 10,000 at leverage 2 lends 0.2 BTC.
14781        let outcome = venue.open_order(market_order(
14782            "sell",
14783            Side::Sell,
14784            "0.3",
14785            TimeInForce::ImmediateOrCancel,
14786        ));
14787        assert!(
14788            insufficient_reason(&outcome).contains("Borrowable: 0.2"),
14789            "{}",
14790            insufficient_reason(&outcome)
14791        );
14792    }
14793
14794    // ---------------------------------------------------------------------------------------
14795    // Spot margin: an asset's borrow cap.
14796    // ---------------------------------------------------------------------------------------
14797
14798    /// `config`, its spot margin letting the account owe no more than `max_borrow` of `asset`.
14799    fn with_max_borrow(
14800        mut config: MockExecutionConfig,
14801        asset: AssetNameExchange,
14802        max_borrow: &str,
14803    ) -> MockExecutionConfig {
14804        config.spot_margin = config
14805            .spot_margin
14806            .map(|margin| margin.with_max_borrow(asset, d(max_borrow)).unwrap());
14807        config
14808    }
14809
14810    /// A venue holding 10,000 USDT and no BTC, at leverage 3: the leverage limit lends up to 0.4
14811    /// BTC at 50,000, and `cap` caps BTC below that.
14812    fn capped_venue(cap: &str) -> SimulatedVenue {
14813        let config = with_max_borrow(margin_config("0", "10000", "3"), base(), cap);
14814        SimulatedVenue::new(&config, spot_instruments())
14815    }
14816
14817    #[test]
14818    fn a_borrow_of_exactly_what_the_cap_leaves_is_lent() {
14819        let mut venue = capped_venue("0.3");
14820
14821        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14822
14823        assert_eq!(
14824            filled_balance(&outcome).balance,
14825            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.3"), Decimal::ZERO)
14826        );
14827    }
14828
14829    #[test]
14830    fn a_borrow_past_a_cap_tighter_than_the_leverage_limit_is_rejected_as_over_the_limit() {
14831        let mut venue = capped_venue("0.3");
14832
14833        let outcome = venue.open_order(sell_request("0.31", market_prices("50000")));
14834
14835        let reject = borrow_rejection(&outcome);
14836        assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
14837        assert_eq!(reject.venue_code, None);
14838        assert_eq!(
14839            reject.available,
14840            Some(d("0.3")),
14841            "a smaller sell could still borrow what the cap leaves"
14842        );
14843        assert!(
14844            reject
14845                .message
14846                .starts_with("BTC borrow exceeds Max Borrow: 0.3;")
14847                && reject.message.ends_with("Borrowable: 0.3"),
14848            "the reason names the cap and what it leaves: {}",
14849            reject.message
14850        );
14851        assert_eq!(
14852            balance_now(&venue, base()),
14853            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
14854            "a refused order borrows nothing"
14855        );
14856    }
14857
14858    #[test]
14859    fn a_leverage_limit_tighter_than_the_cap_rejects_as_unaffordable_naming_no_cap() {
14860        let mut venue = capped_venue("1");
14861
14862        let outcome = venue.open_order(sell_request("0.5", market_prices("50000")));
14863
14864        assert!(
14865            insufficient_reason(&outcome).contains("Borrowable: 0.4")
14866                && !insufficient_reason(&outcome).contains("Max Borrow"),
14867            "{}",
14868            insufficient_reason(&outcome)
14869        );
14870    }
14871
14872    #[test]
14873    fn a_cap_leaving_exactly_what_the_leverage_limit_does_leaves_the_leverage_limit_deciding() {
14874        let mut venue = capped_venue("0.4");
14875
14876        let outcome = venue.open_order(sell_request("0.5", market_prices("50000")));
14877
14878        assert!(
14879            insufficient_reason(&outcome).contains("Borrowable: 0.4")
14880                && !insufficient_reason(&outcome).contains("Max Borrow"),
14881            "{}",
14882            insufficient_reason(&outcome)
14883        );
14884    }
14885
14886    #[test]
14887    fn a_cap_on_another_asset_does_not_limit_this_one() {
14888        let config = with_max_borrow(margin_config("0", "10000", "3"), quote(), "0");
14889        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14890
14891        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14892
14893        assert_eq!(
14894            filled_balance(&outcome).balance,
14895            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.3"), Decimal::ZERO)
14896        );
14897    }
14898
14899    #[test]
14900    fn a_cap_of_zero_lends_none_of_the_asset() {
14901        let mut venue = capped_venue("0");
14902
14903        let outcome = venue.open_order(sell_request("0.01", market_prices("50000")));
14904
14905        let reject = borrow_rejection(&outcome);
14906        assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
14907        assert_eq!(reject.available, Some(Decimal::ZERO));
14908    }
14909
14910    #[test]
14911    fn the_cap_counts_what_is_already_owed_interest_included() {
14912        let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3");
14913        let mut venue = SimulatedVenue::new(&config, spot_instruments())
14914            .with_margin_interest(Arc::new(hourly_interest(base())));
14915        advance(&mut venue, time(1));
14916        // Owes 0.2 lent plus 0.00002 interest charged on it, leaving 0.09998 under the cap.
14917        venue.open_order(sell_request("0.2", market_prices("50000")));
14918
14919        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
14920        assert_eq!(borrow_rejection(&outcome).available, Some(d("0.09998")));
14921
14922        let outcome = venue.open_order(sell_request("0.09998", market_prices("50000")));
14923        assert_eq!(
14924            filled_balance(&outcome).balance.margin,
14925            Some(MarginDetails::new(d("0.29998"), d("0.00003"))),
14926            "0.00002 on the first loan, and 0.000009998 on the second, rounded up to 0.00001"
14927        );
14928    }
14929
14930    #[test]
14931    fn the_interest_a_new_loan_is_charged_when_made_is_not_counted_against_the_cap() {
14932        let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3");
14933        let mut venue = SimulatedVenue::new(&config, spot_instruments())
14934            .with_margin_interest(Arc::new(hourly_interest(base())));
14935        advance(&mut venue, time(1));
14936
14937        let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
14938
14939        assert_eq!(
14940            filled_balance(&outcome).balance.margin,
14941            Some(MarginDetails::new(d("0.3"), d("0.00003"))),
14942            "lent the whole 0.3 the cap leaves, its first charge taking the debt past the cap"
14943        );
14944
14945        let outcome = venue.open_order(sell_request("0.01", market_prices("50000")));
14946        let reject = borrow_rejection(&outcome);
14947        assert_eq!(
14948            reject.available,
14949            Some(Decimal::ZERO),
14950            "a debt past the cap leaves nothing to lend"
14951        );
14952        assert!(
14953            reject.message.ends_with("Borrowable: 0"),
14954            "{}",
14955            reject.message
14956        );
14957    }
14958
14959    #[test]
14960    fn a_minimum_borrow_past_what_the_cap_leaves_is_refused_with_nothing_available() {
14961        let config = with_min_borrow(
14962            with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3"),
14963            base(),
14964            "0.5",
14965        );
14966        let mut venue = SimulatedVenue::new(&config, spot_instruments());
14967
14968        // The 0.1 shortfall fits under the cap, but the loan is raised to the 0.5 minimum.
14969        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
14970        let reject = borrow_rejection(&outcome);
14971        assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
14972        assert_eq!(
14973            reject.available,
14974            Some(Decimal::ZERO),
14975            "no loan can both reach the minimum and stay under the cap"
14976        );
14977        assert!(
14978            reject
14979                .message
14980                .ends_with("Borrowable: 0.3, Minimum Borrow: 0.5"),
14981            "{}",
14982            reject.message
14983        );
14984
14985        // A 0.35 shortfall is past the cap by itself, so the minimum is not named, but still
14986        // leaves nothing a smaller sell could borrow.
14987        let outcome = venue.open_order(sell_request("0.35", market_prices("50000")));
14988        let reject = borrow_rejection(&outcome);
14989        assert_eq!(reject.available, Some(Decimal::ZERO));
14990        assert!(
14991            !reject.message.contains("Minimum Borrow"),
14992            "{}",
14993            reject.message
14994        );
14995    }
14996
14997    #[test]
14998    fn a_minimum_borrow_within_what_the_cap_leaves_keeps_what_it_leaves_available() {
14999        let config = with_min_borrow(
15000            with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3"),
15001            base(),
15002            "0.3",
15003        );
15004        let mut venue = SimulatedVenue::new(&config, spot_instruments());
15005
15006        let outcome = venue.open_order(sell_request("0.35", market_prices("50000")));
15007
15008        assert_eq!(borrow_rejection(&outcome).available, Some(d("0.3")));
15009    }
15010
15011    #[test]
15012    fn a_buy_past_the_cap_on_its_quote_is_rejected_with_no_available_in_base() {
15013        // 1 BTC at 50,000 at leverage 3 lends up to 100,000 USDT; the cap allows 5,000.
15014        let config = with_max_borrow(margin_config("1", "0", "3"), quote(), "5000");
15015        let mut venue = SimulatedVenue::new(&config, spot_instruments());
15016
15017        let outcome = venue.open_order(buy_request("0.2", market_prices("50000")));
15018
15019        let reject = borrow_rejection(&outcome);
15020        assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
15021        assert_eq!(
15022            reject.available, None,
15023            "the loan is in USDT, and the order is sized in BTC"
15024        );
15025        assert!(
15026            reject
15027                .message
15028                .starts_with("USDT borrow exceeds Max Borrow: 5000;"),
15029            "{}",
15030            reject.message
15031        );
15032    }
15033
15034    #[test]
15035    fn a_resting_sell_past_the_cap_is_rejected_as_over_the_limit() {
15036        let config = with_max_borrow(margin_config("0", "100000", "3"), base(), "0.3");
15037        let mut venue = market_venue_for(&config);
15038
15039        let outcome = venue.open_order(limit_request("rest", Side::Sell, "0.4", "50000", gtc()));
15040
15041        let reject = borrow_rejection(&outcome);
15042        assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
15043        assert_eq!(reject.available, Some(d("0.3")));
15044    }
15045
15046    #[test]
15047    fn a_held_sell_past_the_cap_is_rejected_on_arrival_as_over_the_limit() {
15048        // 100 USDT at leverage 2 lends up to 1 BTC at the close of 100; the cap allows 0.5.
15049        let mut venue = margin_venue_holding_with(
15050            |config| config.with_max_borrow(base(), d("0.5")).unwrap(),
15051            MarginEffect::Borrow,
15052        );
15053
15054        let outcome = venue.open_order(market_order("held", Side::Sell, "0.6", gtc()));
15055
15056        let reject = borrow_rejection(&outcome);
15057        assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
15058        assert_eq!(reject.available, Some(d("0.5")));
15059    }
15060
15061    #[test]
15062    fn a_held_sell_lent_the_whole_cap_fills_at_its_bar_though_interest_took_the_debt_past_it() {
15063        let mut venue = margin_venue_holding_with(
15064            |config| config.with_max_borrow(base(), d("0.5")).unwrap(),
15065            MarginEffect::Borrow,
15066        )
15067        .with_margin_interest(Arc::new(hourly_interest(base())));
15068        let outcome = venue.open_order(market_order("held", Side::Sell, "0.5", gtc()));
15069        assert_eq!(
15070            balance_of(sans_debt(&outcome.events)[0]).balance.margin,
15071            Some(MarginDetails::new(d("0.5"), d("0.00005"))),
15072            "the loan's first charge takes what is owed past the 0.5 cap"
15073        );
15074
15075        // The loan is already held against the order, so its fill borrows nothing more.
15076        let events = feed_bar(&mut venue, 2, ohlc("98", "99", "97", "98"));
15077
15078        let trade = events
15079            .iter()
15080            .find_map(|event| match &event.kind {
15081                AccountEventKind::Trade(trade) => Some(trade),
15082                _ => None,
15083            })
15084            .expect("the held sell fills at its bar");
15085        assert_eq!(trade.quantity, d("0.5"));
15086    }
15087
15088    #[test]
15089    fn a_held_buy_whose_bar_needs_a_loan_past_the_cap_is_cancelled() {
15090        // 150 at the close of 100 borrows 50 USDT, all the cap allows.
15091        let mut venue = margin_venue_holding_with(
15092            |config| config.with_max_borrow(quote(), d("50")).unwrap(),
15093            MarginEffect::Borrow,
15094        );
15095        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
15096
15097        // 180 at an open of 120 would borrow 30 more, which the leverage limit allows and the cap
15098        // does not.
15099        let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
15100
15101        assert_eq!(
15102            balance_of(sans_debt(&events)[0]).balance,
15103            Balance::new_margin(d("100"), d("100"), Decimal::ZERO, Decimal::ZERO),
15104            "the cancel repays the 50 the order borrowed when it was accepted"
15105        );
15106        assert!(matches!(
15107            order_of(sans_debt(&events)[1]).state,
15108            OrderState::Inactive(InactiveOrderState::Cancelled(_))
15109        ));
15110    }
15111
15112    // ---------------------------------------------------------------------------------------
15113    // Margin interest: charged when a loan is taken, then at each boundary while it is open.
15114    // ---------------------------------------------------------------------------------------
15115
15116    /// 87.6% a year on the Act365 basis is exactly 0.01% an hour.
15117    const HOURLY_BASIS_POINT: &str = "0.876";
15118
15119    /// Hourly interest on loans of `asset` at [`HOURLY_BASIS_POINT`].
15120    fn hourly_interest(asset: AssetNameExchange) -> PeriodicMarginInterest {
15121        PeriodicMarginInterest::new(TimeDelta::hours(1), DayCount::Act365)
15122            .with_rates(asset, RateSeries::constant(d(HOURLY_BASIS_POINT)))
15123    }
15124
15125    /// The cash flow reporting `interest` charged on `principal` of `asset` at `time`.
15126    fn interest_flow(
15127        asset: AssetNameExchange,
15128        principal: &str,
15129        interest: &str,
15130        time: DateTime<Utc>,
15131        timing: InterestTiming,
15132        id: &str,
15133    ) -> CashFlow<AssetNameExchange, InstrumentNameExchange> {
15134        CashFlow::new(
15135            CashFlowKind::MarginInterest {
15136                rate: Some(d(HOURLY_BASIS_POINT)),
15137                principal: Some(d(principal)),
15138                timing: Some(timing),
15139            },
15140            asset,
15141            -d(interest),
15142            None,
15143            time,
15144            Some(CashFlowId::new(id)),
15145        )
15146    }
15147
15148    fn cash_flow_of(
15149        event: &UnindexedAccountEvent,
15150    ) -> &CashFlow<AssetNameExchange, InstrumentNameExchange> {
15151        match &event.kind {
15152            AccountEventKind::CashFlow(flow) => flow,
15153            other => panic!("expected a cash flow, got: {other:?}"),
15154        }
15155    }
15156
15157    /// A request-priced margin venue holding 0.25 BTC, charged hourly on BTC loans, which has
15158    /// just sold 1 BTC at 50,000, borrowing 0.75, at `time(1)`.
15159    fn margin_venue_short_btc() -> SimulatedVenue {
15160        let mut venue = margin_venue("0.25", "100000", "3")
15161            .with_margin_interest(Arc::new(hourly_interest(base())));
15162        advance(&mut venue, time(1));
15163        venue.open_order(sell_request("1", market_prices("50000")));
15164        venue
15165    }
15166
15167    #[test]
15168    fn a_sell_that_borrows_is_charged_a_period_of_interest_reported_after_its_trade() {
15169        let mut venue = margin_venue("0.25", "100000", "3")
15170            .with_margin_interest(Arc::new(hourly_interest(base())));
15171        advance(&mut venue, time(1));
15172
15173        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
15174
15175        assert_eq!(
15176            filled_balance(&outcome).balance,
15177            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.000075")),
15178            "the interest is added to the loan, leaving total and free as they were"
15179        );
15180        assert_eq!(trade_of(sans_debt(&outcome.events)[2]).quantity, d("1"));
15181        assert_eq!(
15182            *cash_flow_of(sans_debt(&outcome.events)[3]),
15183            interest_flow(
15184                base(),
15185                "0.75",
15186                "0.000075",
15187                time(1),
15188                InterestTiming::OnBorrow,
15189                "0"
15190            ),
15191            "charged on the amount lent, stamped at the borrow, after the trade that opened the \
15192             short, so the position it belongs to exists when it is read"
15193        );
15194    }
15195
15196    #[test]
15197    fn a_loan_is_charged_on_its_principal_at_each_boundary_and_never_on_its_interest() {
15198        let mut venue = margin_venue_short_btc();
15199        assert_eq!(venue.next_deadline(), Some(boundary(0)));
15200
15201        let events = venue.advance_time(boundary(1) + TimeDelta::seconds(1));
15202        let charged = charges(&events);
15203
15204        assert_eq!(charged.len(), 2);
15205        assert_eq!(
15206            charged[0].0.balance,
15207            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.00015"))
15208        );
15209        assert_eq!(charged[0].0.time_exchange, boundary(0));
15210        assert_eq!(
15211            *charged[0].1,
15212            interest_flow(
15213                base(),
15214                "0.75",
15215                "0.000075",
15216                boundary(0),
15217                InterestTiming::Periodic,
15218                "1"
15219            )
15220        );
15221        assert_eq!(
15222            charged[1].0.balance,
15223            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.000225"))
15224        );
15225        assert_eq!(
15226            *charged[1].1,
15227            interest_flow(
15228                base(),
15229                "0.75",
15230                "0.000075",
15231                boundary(1),
15232                InterestTiming::Periodic,
15233                "2"
15234            )
15235        );
15236        assert_eq!(venue.next_deadline(), Some(boundary(2)));
15237    }
15238
15239    /// Each margin debt update among `events`, checked to follow the restatement of its asset and
15240    /// to state that restatement's debt at its time, as `(asset, kind, debt)`.
15241    fn debt_updates(
15242        events: &[UnindexedAccountEvent],
15243    ) -> Vec<(AssetNameExchange, MarginDebtKind, MarginDetails)> {
15244        events
15245            .iter()
15246            .enumerate()
15247            .filter_map(|(at, event)| {
15248                let AccountEventKind::MarginDebt(update) = &event.kind else {
15249                    return None;
15250                };
15251                let restated = at
15252                    .checked_sub(1)
15253                    .map(|before| balance_of(&events[before]))
15254                    .expect("a debt update follows a restatement");
15255                assert_eq!(update.asset, restated.asset);
15256                assert_eq!(Some(update.debt), restated.balance.margin);
15257                assert_eq!(update.time_exchange, restated.time_exchange);
15258                assert_eq!(update.instrument, None);
15259                assert_eq!(update.venue_kind, update.kind.to_smolstr());
15260                Some((update.asset.clone(), update.kind, update.debt))
15261            })
15262            .collect()
15263    }
15264
15265    /// A spot-margin venue states the debt after each change, following the restatement that
15266    /// moved it: a sell that borrows, the interest charged at a boundary, and the buy that repays.
15267    #[test]
15268    fn a_margin_venue_reports_each_debt_change_after_its_restatement() {
15269        let mut venue = margin_venue("0.25", "100000", "3")
15270            .with_margin_interest(Arc::new(hourly_interest(base())));
15271        advance(&mut venue, time(1));
15272
15273        let sold = venue.open_order(sell_request("1", market_prices("50000")));
15274        assert_eq!(
15275            debt_updates(&sold.events),
15276            [(
15277                base(),
15278                MarginDebtKind::Borrow,
15279                MarginDetails::new(d("0.75"), d("0.000075"))
15280            )]
15281        );
15282
15283        let charged = venue.advance_time(boundary(0));
15284        assert_eq!(
15285            debt_updates(&charged),
15286            [(
15287                base(),
15288                MarginDebtKind::Interest,
15289                MarginDetails::new(d("0.75"), d("0.00015"))
15290            )]
15291        );
15292
15293        let repaid = venue.open_order(with_effect(
15294            buy_request("1", market_prices("50000")),
15295            MarginEffect::Repay,
15296        ));
15297        assert_eq!(
15298            debt_updates(&repaid.events),
15299            [(base(), MarginDebtKind::Repay, MarginDetails::default())]
15300        );
15301
15302        // Nothing moved the loan since: a restatement with the same debt reports none.
15303        let traded = venue.open_order(sell_request("0.1", market_prices("50000")));
15304        assert!(debt_updates(&traded.events).is_empty(), "{traded:?}");
15305    }
15306
15307    /// A repay is told apart from an interest charge by either part of the debt falling: interest
15308    /// alone, with the principal left as it was, or principal alone, with no interest owed.
15309    #[test]
15310    fn a_repay_of_either_part_of_the_debt_reports_a_repay() {
15311        // 0.00005 BTC repays part of the 0.000075 interest, and none of the 0.75 principal.
15312        let mut venue = margin_venue_short_btc();
15313        let repaid = venue.open_order(with_effect(
15314            buy_request("0.00005", market_prices("50000")),
15315            MarginEffect::Repay,
15316        ));
15317        assert_eq!(
15318            debt_updates(&repaid.events),
15319            [(
15320                base(),
15321                MarginDebtKind::Repay,
15322                MarginDetails::new(d("0.75"), d("0.000025"))
15323            )]
15324        );
15325
15326        // With no interest model, the loan owes no interest, so a repay lowers the principal alone.
15327        let mut venue = margin_venue("0.25", "100000", "3");
15328        advance(&mut venue, time(1));
15329        venue.open_order(sell_request("1", market_prices("50000")));
15330        let repaid = venue.open_order(with_effect(
15331            buy_request("0.5", market_prices("50000")),
15332            MarginEffect::Repay,
15333        ));
15334        assert_eq!(
15335            debt_updates(&repaid.events),
15336            [(
15337                base(),
15338                MarginDebtKind::Repay,
15339                MarginDetails::new(d("0.25"), Decimal::ZERO)
15340            )]
15341        );
15342    }
15343
15344    /// A seeded loan is the debt the venue starts from, so it is not reported as a change, and a
15345    /// venue that is not a margin one reports no debt at all.
15346    #[test]
15347    fn only_a_change_on_a_margin_venue_reports_debt() {
15348        let config = config_owing_base(MarginEffect::Borrow);
15349        let mut venue = market_venue_for(&config);
15350        let bought = venue.open_order(market_order("buy", Side::Buy, "0.1", gtc()));
15351        assert!(debt_updates(&bought.events).is_empty(), "{bought:?}");
15352
15353        let mut venue = make_venue("10", "1000000");
15354        let sold = venue.open_order(sell_request("1", market_prices("50000")));
15355        assert!(
15356            !sold
15357                .events
15358                .iter()
15359                .any(|event| matches!(event.kind, AccountEventKind::MarginDebt(_))),
15360            "{sold:?}"
15361        );
15362    }
15363
15364    #[test]
15365    fn a_loan_repaid_before_a_boundary_is_not_charged_there() {
15366        let mut venue = margin_venue_short_btc();
15367
15368        // 0.76 BTC repays the 0.000075 interest, then the 0.75 principal, leaving 0.009925 free.
15369        let outcome = venue.open_order(with_effect(
15370            buy_request("0.76", market_prices("50000")),
15371            MarginEffect::Repay,
15372        ));
15373        assert_eq!(
15374            filled_received(&outcome).balance,
15375            Balance::new_margin(d("0.009925"), d("0.009925"), Decimal::ZERO, Decimal::ZERO)
15376        );
15377
15378        assert_eq!(venue.next_deadline(), None);
15379        assert!(venue.advance_time(boundary(1)).is_empty());
15380    }
15381
15382    #[test]
15383    fn a_boundary_charges_the_principal_left_after_a_partial_repay() {
15384        let mut venue = margin_venue_short_btc();
15385
15386        // 0.5 BTC repays the 0.000075 interest, then 0.499925 of the 0.75 principal.
15387        venue.open_order(with_effect(
15388            buy_request("0.5", market_prices("50000")),
15389            MarginEffect::Repay,
15390        ));
15391
15392        let events = venue.advance_time(boundary(0));
15393        assert_eq!(
15394            *charges(&events)[0].1,
15395            interest_flow(
15396                base(),
15397                "0.250075",
15398                "0.00002501",
15399                boundary(0),
15400                InterestTiming::Periodic,
15401                "1"
15402            ),
15403            "0.250075 at 0.01% is 0.0000250075, rounded up to the satoshi"
15404        );
15405    }
15406
15407    #[test]
15408    fn a_loan_taken_at_a_boundary_s_instant_is_charged_there_only_at_borrow() {
15409        let mut venue = margin_venue("0.25", "100000", "3")
15410            .with_margin_interest(Arc::new(hourly_interest(base())));
15411        advance(&mut venue, time(1));
15412        advance(&mut venue, boundary(0));
15413
15414        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
15415        assert_eq!(
15416            *cash_flow_of(sans_debt(&outcome.events)[3]),
15417            interest_flow(
15418                base(),
15419                "0.75",
15420                "0.000075",
15421                boundary(0),
15422                InterestTiming::OnBorrow,
15423                "0"
15424            )
15425        );
15426
15427        assert!(
15428            venue
15429                .advance_time(boundary(0) + TimeDelta::seconds(1))
15430                .is_empty()
15431        );
15432        assert_eq!(venue.next_deadline(), Some(boundary(1)));
15433    }
15434
15435    #[test]
15436    fn a_resting_sell_is_charged_when_placed_reported_at_its_cancel_which_repays_interest_first() {
15437        let mut venue = margin_market_venue("0", "100000")
15438            .with_margin_interest(Arc::new(hourly_interest(base())));
15439
15440        let outcome = venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
15441        assert_eq!(
15442            sans_debt(&outcome.events).len(),
15443            1,
15444            "the hold's balance alone: nothing traded, so the interest has no position to belong \
15445             to yet: {:?}",
15446            outcome.events
15447        );
15448        assert_eq!(
15449            balance_of(sans_debt(&outcome.events)[0]).balance,
15450            Balance::new_margin(d("1"), Decimal::ZERO, d("1"), d("0.0001")),
15451            "the balance already carries the charge"
15452        );
15453
15454        advance(&mut venue, time(5));
15455        let cancelled = venue.cancel_order(OrderRequestCancel {
15456            key: limit_request("rest", Side::Sell, "1", "50000", gtc()).key,
15457            state: RequestCancel { id: None },
15458        });
15459        assert_eq!(
15460            balance_of(sans_debt(&cancelled.events)[0]).balance,
15461            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.0001"), Decimal::ZERO),
15462            "the released base repays the interest first, so as much principal stays owed"
15463        );
15464        assert_eq!(
15465            *cash_flow_of(sans_debt(&cancelled.events)[1]),
15466            interest_flow(
15467                base(),
15468                "1",
15469                "0.0001",
15470                time(5),
15471                InterestTiming::OnBorrow,
15472                "0"
15473            ),
15474            "reported with the cancel, the order's end, stamped then"
15475        );
15476        assert_eq!(sans_debt(&cancelled.events).len(), 2);
15477        assert_eq!(venue.next_deadline(), Some(boundary(0)));
15478    }
15479
15480    #[test]
15481    fn a_held_buy_reports_the_interest_of_its_hold_and_of_its_bar_after_its_fill() {
15482        let mut venue =
15483            margin_venue_holding().with_margin_interest(Arc::new(hourly_interest(quote())));
15484
15485        // 150 at the close of 100: 50 borrowed, charged 0.005.
15486        let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
15487        assert_eq!(
15488            sans_debt(&outcome.events).len(),
15489            1,
15490            "the hold's balance alone: the interest waits for the fill: {:?}",
15491            outcome.events
15492        );
15493        assert_eq!(
15494            balance_of(sans_debt(&outcome.events)[0]).balance,
15495            Balance::new_margin(d("150"), Decimal::ZERO, d("50"), d("0.005"))
15496        );
15497
15498        // 180 at an open of 120: 30 more borrowed, charged 0.003.
15499        let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
15500        let events = sans_debt(&events);
15501        assert_eq!(
15502            balance_of(events[0]).balance,
15503            Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("80"), d("0.008"))
15504        );
15505        assert_eq!(trade_of(events[2]).price, d("120"));
15506        assert_eq!(
15507            *cash_flow_of(events[3]),
15508            interest_flow(
15509                quote(),
15510                "50",
15511                "0.005",
15512                time(2),
15513                InterestTiming::OnBorrow,
15514                "0"
15515            ),
15516            "the hold's charge after the fill, stamped at the fill, so a position entered then \
15517             takes it"
15518        );
15519        assert_eq!(
15520            cash_flow_of(events[4]).amount,
15521            d("-0.003"),
15522            "then what the bar borrowed"
15523        );
15524        assert_eq!(cash_flow_of(events[4]).time_exchange, time(2));
15525        assert_eq!(events.len(), 6, "and the filled order last: {events:?}");
15526    }
15527
15528    #[test]
15529    fn a_resting_sell_that_borrowed_reports_its_interest_after_its_fill() {
15530        let mut venue = margin_market_venue("0", "100000")
15531            .with_margin_interest(Arc::new(hourly_interest(base())));
15532        venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
15533
15534        advance(&mut venue, time(7));
15535        let events = venue.apply_market(
15536            &instrument_name(),
15537            MarketObservation::new(book("50100", "50200")),
15538            time(7),
15539        );
15540        let events = sans_debt(&events);
15541
15542        assert_eq!(trade_of(events[2]).quantity, d("1"));
15543        assert_eq!(
15544            *cash_flow_of(events[3]),
15545            interest_flow(
15546                base(),
15547                "1",
15548                "0.0001",
15549                time(7),
15550                InterestTiming::OnBorrow,
15551                "0"
15552            ),
15553            "after the fill that opened the short, stamped at it"
15554        );
15555        assert_eq!(events.len(), 5, "and the filled order last: {events:?}");
15556    }
15557
15558    #[test]
15559    fn a_held_buy_unaffordable_at_its_bar_reports_its_interest_with_its_cancel() {
15560        let mut venue =
15561            margin_venue_holding().with_margin_interest(Arc::new(hourly_interest(quote())));
15562        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
15563
15564        // 375 at an open of 250 is more than the venue lends.
15565        let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
15566        let events = sans_debt(&events);
15567
15568        assert_eq!(
15569            *cash_flow_of(events[1]),
15570            interest_flow(
15571                quote(),
15572                "50",
15573                "0.005",
15574                time(2),
15575                InterestTiming::OnBorrow,
15576                "0"
15577            ),
15578            "the order never traded, so its end reports the charge, stamped then"
15579        );
15580        assert!(matches!(
15581            order_of(events[2]).state,
15582            OrderState::Inactive(InactiveOrderState::Cancelled(_))
15583        ));
15584    }
15585
15586    #[test]
15587    fn a_held_sell_that_expires_reports_its_interest_before_its_end() {
15588        let mut venue = margin_venue_holding_with(|config| config, MarginEffect::Borrow)
15589            .with_margin_interest(Arc::new(hourly_interest(base())));
15590        venue.open_order(market_order("held", Side::Sell, "1", gtc()));
15591
15592        let events = venue.expire_held(time(2));
15593        let events = sans_debt(&events);
15594
15595        assert_eq!(
15596            *cash_flow_of(events[1]),
15597            interest_flow(
15598                base(),
15599                "1",
15600                "0.0001",
15601                time(2),
15602                InterestTiming::OnBorrow,
15603                "0"
15604            )
15605        );
15606        assert!(matches!(
15607            order_of(events[2]).state,
15608            OrderState::Inactive(InactiveOrderState::Expired(_))
15609        ));
15610    }
15611
15612    #[test]
15613    fn a_held_buy_whose_bar_repays_its_whole_loan_owes_no_more_boundaries() {
15614        // A zero rate charges nothing at the borrow, so the loan is exactly what was lent.
15615        let free_loans = PeriodicMarginInterest::new(TimeDelta::hours(1), DayCount::Act365)
15616            .with_rates(quote(), RateSeries::constant(Decimal::ZERO));
15617        let mut venue = margin_venue_holding().with_margin_interest(Arc::new(free_loans));
15618
15619        // 150 at the close of 100: 50 borrowed.
15620        venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
15621        assert_eq!(venue.next_deadline(), Some(boundary(0)));
15622
15623        // 90 at an open of 60: the 100 the account had pays for all of it, so all 50 is repaid.
15624        let events = feed_bar(&mut venue, 2, ohlc("60", "61", "59", "60"));
15625        assert_eq!(
15626            balance_of(&events[0]).balance,
15627            Balance::new_margin(d("10"), d("10"), Decimal::ZERO, Decimal::ZERO)
15628        );
15629        assert_eq!(venue.next_deadline(), None);
15630    }
15631
15632    #[test]
15633    fn a_seeded_loan_is_charged_at_boundaries_from_the_clock_s_first_move_only() {
15634        let config = MockExecutionConfig {
15635            spot_margin: Some(
15636                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
15637            ),
15638            ..config_from_balances(
15639                vec![
15640                    AssetBalance::new(
15641                        base(),
15642                        Balance::new_margin(d("0.5"), d("0.5"), d("0.5"), Decimal::ZERO),
15643                        Utc::now(),
15644                    ),
15645                    funded(quote(), d("100000")),
15646                ],
15647                FeeModelConfig::default(),
15648            )
15649        };
15650        let mut venue = SimulatedVenue::new(&config, spot_instruments())
15651            .with_margin_interest(Arc::new(hourly_interest(base())));
15652        assert_eq!(
15653            venue.next_deadline(),
15654            None,
15655            "charging starts when the clock moves"
15656        );
15657
15658        advance(&mut venue, time(1));
15659        assert_eq!(venue.next_deadline(), Some(boundary(0)));
15660
15661        let events = venue.advance_time(boundary(0));
15662        assert_eq!(
15663            *charges(&events)[0].1,
15664            interest_flow(
15665                base(),
15666                "0.5",
15667                "0.00005",
15668                boundary(0),
15669                InterestTiming::Periodic,
15670                "0"
15671            )
15672        );
15673    }
15674
15675    #[test]
15676    fn at_borrow_only_charges_the_borrow_and_no_boundary() {
15677        let mut venue = margin_venue("0.25", "100000", "3").with_margin_interest(Arc::new(
15678            hourly_interest(base()).with_schedule(InterestSchedule::AtBorrowOnly),
15679        ));
15680        advance(&mut venue, time(1));
15681
15682        let outcome = venue.open_order(sell_request("1", market_prices("50000")));
15683
15684        assert_eq!(
15685            filled_balance(&outcome).balance.margin.unwrap().interest,
15686            d("0.000075")
15687        );
15688        assert_eq!(venue.next_deadline(), None);
15689        assert!(venue.advance_time(boundary(3)).is_empty());
15690    }
15691
15692    #[test]
15693    #[should_panic(expected = "without MockExecutionConfig::spot_margin")]
15694    fn a_cash_venue_given_an_interest_model_panics() {
15695        let _ = make_venue("1", "100000").with_margin_interest(Arc::new(hourly_interest(base())));
15696    }
15697
15698    /// A model that breaks its contract: `boundary` returns the instant it is asked from, and
15699    /// `charge` refunds a satoshi.
15700    #[derive(Debug)]
15701    struct Misbehaving;
15702
15703    impl MarginInterestModel for Misbehaving {
15704        fn next_boundary(
15705            &self,
15706            _: &AssetNameExchange,
15707            after: DateTime<Utc>,
15708        ) -> Option<DateTime<Utc>> {
15709            Some(after)
15710        }
15711
15712        fn charge(
15713            &self,
15714            _: &AssetNameExchange,
15715            _: Decimal,
15716            _: InterestDue,
15717        ) -> Option<InterestCharge> {
15718            Some(InterestCharge::new(d("-0.00000001"), None))
15719        }
15720    }
15721
15722    /// A model charging a satoshi at every borrow, whatever was lent, and at no boundary.
15723    #[derive(Debug)]
15724    struct FlatAtBorrow;
15725
15726    impl MarginInterestModel for FlatAtBorrow {
15727        fn next_boundary(&self, _: &AssetNameExchange, _: DateTime<Utc>) -> Option<DateTime<Utc>> {
15728            None
15729        }
15730
15731        fn charge(
15732            &self,
15733            _: &AssetNameExchange,
15734            _: Decimal,
15735            due: InterestDue,
15736        ) -> Option<InterestCharge> {
15737            matches!(due, InterestDue::Borrow(_))
15738                .then(|| InterestCharge::new(d("0.00000001"), None))
15739        }
15740    }
15741
15742    #[test]
15743    fn an_order_that_borrows_nothing_is_not_asked_about_interest() {
15744        let mut venue =
15745            margin_venue("0.25", "100000", "3").with_margin_interest(Arc::new(FlatAtBorrow));
15746        advance(&mut venue, time(1));
15747
15748        let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
15749
15750        assert_eq!(filled_trade(&outcome).quantity, d("0.1"));
15751        assert!(
15752            !outcome
15753                .events
15754                .iter()
15755                .any(|event| matches!(event.kind, AccountEventKind::CashFlow(_))),
15756            "0.1 of the 0.25 held lends nothing, so nothing is charged"
15757        );
15758    }
15759
15760    #[test]
15761    #[should_panic(expected = "charged negative interest")]
15762    fn a_model_charging_negative_interest_panics() {
15763        let mut venue =
15764            margin_venue("0.25", "100000", "3").with_margin_interest(Arc::new(Misbehaving));
15765        advance(&mut venue, time(1));
15766
15767        let _ = venue.open_order(sell_request("1", market_prices("50000")));
15768    }
15769
15770    #[test]
15771    #[should_panic(expected = "which is not after")]
15772    fn a_model_whose_boundary_is_not_after_the_instant_asked_panics() {
15773        let config = MockExecutionConfig {
15774            spot_margin: Some(
15775                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
15776            ),
15777            ..config_from_balances(
15778                vec![
15779                    AssetBalance::new(
15780                        base(),
15781                        Balance::new_margin(d("0.5"), d("0.5"), d("0.5"), Decimal::ZERO),
15782                        Utc::now(),
15783                    ),
15784                    funded(quote(), d("100000")),
15785                ],
15786                FeeModelConfig::default(),
15787            )
15788        };
15789        let mut venue = SimulatedVenue::new(&config, spot_instruments())
15790            .with_margin_interest(Arc::new(Misbehaving));
15791
15792        let _ = venue.advance_time(time(1));
15793    }
15794
15795    // ---------------------------------------------------------------------------------------
15796    // Fees charged in a third asset (`FeeAssetConfig`).
15797    // ---------------------------------------------------------------------------------------
15798
15799    fn bnb() -> AssetNameExchange {
15800        AssetNameExchange::new("BNB")
15801    }
15802
15803    fn spot_instrument(
15804        name: &str,
15805        base: &str,
15806        quote: &str,
15807    ) -> Instrument<ExchangeId, AssetNameExchange> {
15808        Instrument {
15809            exchange: EXCHANGE,
15810            name_internal: InstrumentNameInternal::new(name.to_lowercase()),
15811            name_exchange: InstrumentNameExchange::new(name),
15812            underlying: Underlying {
15813                base: AssetNameExchange::new(base),
15814                quote: AssetNameExchange::new(quote),
15815            },
15816            quote: InstrumentQuoteAsset::UnderlyingQuote,
15817            kind: InstrumentKind::Spot,
15818            spec: None,
15819            data_venue: None,
15820        }
15821    }
15822
15823    /// `BTCUSDT` as `spot_instruments` gives it, and `BNBUSDT`, which prices the fee asset.
15824    fn fee_asset_instruments()
15825    -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
15826        let mut instruments = spot_instruments();
15827        let bnb_usdt = spot_instrument("BNBUSDT", "BNB", "USDT");
15828        instruments.insert(bnb_usdt.name_exchange.clone(), bnb_usdt);
15829        instruments
15830    }
15831
15832    /// 10 BTC, 100,000 USDT and `bnb` BNB, maker 2bp against taker 10bp, and fees in BNB at a
15833    /// quarter off.
15834    fn fee_asset_config(bnb: &str) -> MockExecutionConfig {
15835        MockExecutionConfig {
15836            fee_asset: Some(FeeAssetConfig::new(self::bnb(), d("0.25")).unwrap()),
15837            ..config_from_balances(
15838                vec![
15839                    funded(base(), d("10")),
15840                    funded(quote(), d("100000")),
15841                    funded(self::bnb(), d(bnb)),
15842                ],
15843                maker_taker_fee(),
15844            )
15845        }
15846    }
15847
15848    /// A fed venue for `config`, `BTCUSDT` last traded at 50,000, and `BNBUSDT` at a 599 / 601
15849    /// book, a mid of 600, when `priced`.
15850    fn fee_asset_venue_from(config: &MockExecutionConfig, priced: bool) -> SimulatedVenue {
15851        let mut venue = SimulatedVenue::new_market_driven(config, fee_asset_instruments());
15852        advance(&mut venue, time(1));
15853        if priced {
15854            assert!(
15855                venue
15856                    .apply_market(
15857                        &InstrumentNameExchange::new("BNBUSDT"),
15858                        MarketObservation::new(book("599", "601")),
15859                        time(1)
15860                    )
15861                    .is_empty()
15862            );
15863        }
15864        assert!(
15865            venue
15866                .apply_market(
15867                    &instrument_name(),
15868                    MarketObservation::new(traded("50000")),
15869                    time(1)
15870                )
15871                .is_empty()
15872        );
15873        venue
15874    }
15875
15876    fn fee_asset_venue(bnb: &str) -> SimulatedVenue {
15877        fee_asset_venue_from(&fee_asset_config(bnb), true)
15878    }
15879
15880    /// The balance restatements an outcome's events carry, by asset.
15881    fn restated(events: &[UnindexedAccountEvent]) -> Vec<(AssetNameExchange, Balance)> {
15882        events
15883            .iter()
15884            .filter_map(|event| match &event.kind {
15885                AccountEventKind::BalanceSnapshot(balance) => {
15886                    Some((balance.0.asset.clone(), balance.0.balance))
15887                }
15888                _ => None,
15889            })
15890            .collect()
15891    }
15892
15893    /// A taker buy of 1 BTC at 50,000 pays the 50 USDT fee as 37.50 at a quarter off, which is
15894    /// 0.0625 BNB at 600, out of the BNB balance, and exchanges its two assets free of fee.
15895    #[test]
15896    fn a_fill_pays_its_fee_in_the_fee_asset_at_a_discount() {
15897        let mut venue = fee_asset_venue("1");
15898
15899        let outcome = venue.open_order(buy_request("1", None));
15900
15901        assert_eq!(
15902            filled_trade(&outcome).fees,
15903            AssetFees::new(bnb(), d("0.0625"), Some(d("37.5")))
15904        );
15905        assert_eq!(
15906            restated(&outcome.events),
15907            vec![
15908                (quote(), Balance::new(d("50000"), d("50000"))),
15909                (base(), Balance::new(d("11"), d("11"))),
15910                (bnb(), Balance::new(d("0.9375"), d("0.9375"))),
15911            ]
15912        );
15913    }
15914
15915    #[test]
15916    fn a_sell_receives_its_whole_proceeds_and_pays_its_fee_in_the_fee_asset() {
15917        let mut venue = fee_asset_venue("1");
15918
15919        let outcome = venue.open_order(sell_request("1", None));
15920
15921        assert_eq!(filled_trade(&outcome).fees.asset, bnb());
15922        assert_eq!(
15923            restated(&outcome.events),
15924            vec![
15925                (base(), Balance::new(d("9"), d("9"))),
15926                (quote(), Balance::new(d("150000"), d("150000"))),
15927                (bnb(), Balance::new(d("0.9375"), d("0.9375"))),
15928            ]
15929        );
15930    }
15931
15932    /// With less of the fee asset than the fee, the whole fee is charged undiscounted in quote,
15933    /// none of it split, and the fee asset is untouched.
15934    #[test]
15935    fn a_fee_asset_balance_short_of_the_fee_falls_back_whole() {
15936        for bnb in ["0.0624", "0"] {
15937            let mut venue = fee_asset_venue(bnb);
15938
15939            let outcome = venue.open_order(buy_request("1", None));
15940
15941            assert_eq!(
15942                filled_trade(&outcome).fees,
15943                AssetFees::new(quote(), d("50"), Some(d("50"))),
15944                "{bnb} BNB"
15945            );
15946            assert_eq!(
15947                restated(&outcome.events),
15948                vec![
15949                    (quote(), Balance::new(d("49950"), d("49950"))),
15950                    (base(), Balance::new(d("11"), d("11"))),
15951                ],
15952                "{bnb} BNB"
15953            );
15954        }
15955    }
15956
15957    /// Exactly the fee is enough.
15958    #[test]
15959    fn a_fee_asset_balance_of_exactly_the_fee_pays_it() {
15960        let mut venue = fee_asset_venue("0.0625");
15961
15962        let outcome = venue.open_order(buy_request("1", None));
15963
15964        assert_eq!(filled_trade(&outcome).fees.asset, bnb());
15965        assert_eq!(balance_now(&venue, bnb()), Balance::new(d("0"), d("0")));
15966    }
15967
15968    #[test]
15969    fn a_fee_asset_the_venue_cannot_price_falls_back() {
15970        let mut venue = fee_asset_venue_from(&fee_asset_config("1"), false);
15971
15972        let outcome = venue.open_order(buy_request("1", None));
15973
15974        assert_eq!(
15975            filled_trade(&outcome).fees,
15976            AssetFees::new(quote(), d("50"), Some(d("50")))
15977        );
15978        assert_eq!(balance_now(&venue, bnb()), Balance::new(d("1"), d("1")));
15979    }
15980
15981    /// On the fee asset's own instrument the venue charges as without the setting.
15982    #[test]
15983    fn a_fill_on_the_fee_assets_own_instrument_falls_back() {
15984        let mut venue = fee_asset_venue("1");
15985        assert!(
15986            venue
15987                .apply_market(
15988                    &InstrumentNameExchange::new("BNBUSDT"),
15989                    MarketObservation::new(traded("600")),
15990                    time(1)
15991                )
15992                .is_empty()
15993        );
15994        let mut request = buy_request("1", None);
15995        request.key.instrument = InstrumentNameExchange::new("BNBUSDT");
15996
15997        let outcome = venue.open_order(request);
15998
15999        // 600 × 10bp in quote, undiscounted.
16000        assert_eq!(
16001            filled_trade(&outcome).fees,
16002            AssetFees::new(quote(), d("0.6"), Some(d("0.6")))
16003        );
16004        assert_eq!(balance_now(&venue, bnb()), Balance::new(d("2"), d("2")));
16005    }
16006
16007    /// A resting buy holds its fee as the fallback would charge it, in quote. When it fills with
16008    /// the fee paid in BNB, that part of the hold comes back.
16009    #[test]
16010    fn a_resting_fill_paid_in_the_fee_asset_releases_the_fee_it_held() {
16011        let mut venue = fee_asset_venue("1");
16012        assert!(
16013            venue
16014                .apply_market(
16015                    &instrument_name(),
16016                    MarketObservation::new(book("49000", "49100")),
16017                    time(1)
16018                )
16019                .is_empty()
16020        );
16021        let outcome = venue.open_order(limit_request("resting", Side::Buy, "1", "49000", gtc()));
16022        // 49,000 plus the maker fee of 9.80 is held.
16023        assert_eq!(
16024            filled_balance(&outcome).balance,
16025            Balance::new(d("100000"), d("50990.2"))
16026        );
16027
16028        let events = venue.apply_market(
16029            &instrument_name(),
16030            MarketObservation::new(book("48900", "48950")),
16031            time(2),
16032        );
16033
16034        // The maker fee of 9.80, a quarter off, at 600: 0.01225 BNB.
16035        let trade = events
16036            .iter()
16037            .find_map(|event| match &event.kind {
16038                AccountEventKind::Trade(trade) => Some(trade),
16039                _ => None,
16040            })
16041            .unwrap();
16042        assert_eq!(
16043            trade.fees,
16044            AssetFees::new(bnb(), d("0.01225"), Some(d("7.35")))
16045        );
16046        assert_eq!(
16047            restated(&events),
16048            vec![
16049                (quote(), Balance::new(d("51000"), d("51000"))),
16050                (base(), Balance::new(d("11"), d("11"))),
16051                (bnb(), Balance::new(d("0.98775"), d("0.98775"))),
16052            ]
16053        );
16054    }
16055
16056    /// Under `Received`, a buy paying its fee in BNB receives its whole quantity.
16057    #[test]
16058    fn under_received_a_buy_paying_in_the_fee_asset_receives_its_whole_quantity() {
16059        let config = MockExecutionConfig {
16060            spot_fee_asset: SpotFeeAsset::Received,
16061            ..fee_asset_config("1")
16062        };
16063        let mut venue = fee_asset_venue_from(&config, true);
16064
16065        let outcome = venue.open_order(buy_request("1", None));
16066
16067        assert_eq!(filled_trade(&outcome).fees.asset, bnb());
16068        assert_eq!(balance_now(&venue, base()).total, d("11"));
16069        assert_eq!(balance_now(&venue, quote()).total, d("50000"));
16070    }
16071
16072    #[test]
16073    fn a_zero_fee_is_charged_without_the_fee_asset() {
16074        let config = MockExecutionConfig {
16075            fee_model: FeeModelConfig::default(),
16076            ..fee_asset_config("1")
16077        };
16078        let mut venue = fee_asset_venue_from(&config, true);
16079
16080        let outcome = venue.open_order(buy_request("1", None));
16081
16082        assert_eq!(
16083            filled_trade(&outcome).fees,
16084            AssetFees::new(quote(), Decimal::ZERO, Some(Decimal::ZERO))
16085        );
16086        assert_eq!(balance_now(&venue, bnb()), Balance::new(d("1"), d("1")));
16087    }
16088
16089    /// A discount of one charges nothing, even to an account that has never held the fee asset.
16090    #[test]
16091    fn a_full_discount_charges_nothing_and_needs_no_fee_asset_balance() {
16092        let config = MockExecutionConfig {
16093            fee_asset: Some(FeeAssetConfig::new(bnb(), Decimal::ONE).unwrap()),
16094            ..config_from_balances(
16095                vec![funded(base(), d("10")), funded(quote(), d("100000"))],
16096                maker_taker_fee(),
16097            )
16098        };
16099        let mut venue = fee_asset_venue_from(&config, true);
16100
16101        let outcome = venue.open_order(buy_request("1", None));
16102
16103        assert_eq!(
16104            filled_trade(&outcome).fees,
16105            AssetFees::new(bnb(), Decimal::ZERO, Some(Decimal::ZERO))
16106        );
16107        assert_eq!(
16108            restated(&outcome.events),
16109            vec![
16110                (quote(), Balance::new(d("50000"), d("50000"))),
16111                (base(), Balance::new(d("11"), d("11"))),
16112            ]
16113        );
16114    }
16115
16116    /// A market order held for its next bar pays its fee in BNB when the bar fills it.
16117    #[test]
16118    fn a_held_market_order_pays_its_fee_in_the_fee_asset() {
16119        let config = MockExecutionConfig {
16120            market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
16121            ..fee_asset_config("1")
16122        };
16123        let mut venue = SimulatedVenue::new_market_driven(&config, fee_asset_instruments());
16124        advance(&mut venue, time(1));
16125        assert!(
16126            venue
16127                .apply_market(
16128                    &InstrumentNameExchange::new("BNBUSDT"),
16129                    MarketObservation::new(book("599", "601")),
16130                    time(1)
16131                )
16132                .is_empty()
16133        );
16134        assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
16135        let held = venue.open_order(ioc_buy("held", "1"));
16136        assert!(held.response.state.is_accepted());
16137
16138        advance(&mut venue, time(2));
16139        let events = feed_bar(&mut venue, 2, ohlc("200", "210", "190", "205"));
16140
16141        // 200 × 10bp, a quarter off, at 600: 0.00025 BNB.
16142        assert_eq!(
16143            restated(&events),
16144            vec![
16145                (quote(), Balance::new(d("99800"), d("99800"))),
16146                (base(), Balance::new(d("11"), d("11"))),
16147                (bnb(), Balance::new(d("0.99975"), d("0.99975"))),
16148            ]
16149        );
16150    }
16151
16152    /// On an instrument quoting in the fee asset the venue charges as without the setting too:
16153    /// the fee is undiscounted, in quote, which is the fee asset, under the `SpotFeeAsset` rule.
16154    #[test]
16155    fn a_fill_on_an_instrument_quoted_in_the_fee_asset_falls_back() {
16156        let mut instruments = fee_asset_instruments();
16157        let eth_bnb = spot_instrument("ETHBNB", "ETH", "BNB");
16158        instruments.insert(eth_bnb.name_exchange.clone(), eth_bnb);
16159        let eth = || AssetNameExchange::new("ETH");
16160        let config = MockExecutionConfig {
16161            fee_asset: Some(FeeAssetConfig::new(bnb(), d("0.25")).unwrap()),
16162            ..config_from_balances(
16163                vec![
16164                    funded(base(), d("10")),
16165                    funded(quote(), d("100000")),
16166                    funded(bnb(), d("10")),
16167                    funded(eth(), d("1")),
16168                ],
16169                maker_taker_fee(),
16170            )
16171        };
16172
16173        for (side, eth_after, bnb_after) in [
16174            // 0.5 BNB plus 0.0005 on top.
16175            (Side::Buy, "2", "9.4995"),
16176            // 0.5 BNB less 0.0005 out of it.
16177            (Side::Sell, "0", "10.4995"),
16178        ] {
16179            let mut venue = SimulatedVenue::new_market_driven(&config, instruments.clone());
16180            advance(&mut venue, time(1));
16181            assert!(
16182                venue
16183                    .apply_market(
16184                        &InstrumentNameExchange::new("ETHBNB"),
16185                        MarketObservation::new(traded("0.5")),
16186                        time(1)
16187                    )
16188                    .is_empty()
16189            );
16190            let mut request = match side {
16191                Side::Buy => buy_request("1", None),
16192                Side::Sell => sell_request("1", None),
16193            };
16194            request.key.instrument = InstrumentNameExchange::new("ETHBNB");
16195
16196            let outcome = venue.open_order(request);
16197
16198            // 0.5 × 10bp, undiscounted.
16199            assert_eq!(
16200                filled_trade(&outcome).fees,
16201                AssetFees::new(bnb(), d("0.0005"), Some(d("0.0005"))),
16202                "{side:?}"
16203            );
16204            assert_eq!(balance_now(&venue, eth()).total, d(eth_after), "{side:?}");
16205            assert_eq!(balance_now(&venue, bnb()).total, d(bnb_after), "{side:?}");
16206        }
16207    }
16208
16209    /// A maker rebate is a negative fee, which stays in quote: the venue pays no rebate in the
16210    /// fee asset.
16211    #[test]
16212    fn a_negative_fee_is_paid_without_the_fee_asset() {
16213        let rebate =
16214            FeeModelConfig::Percentage(PercentageFeeModel::maker_taker(d("-0.0001"), d("-0.0001")));
16215        let config = MockExecutionConfig {
16216            fee_model: rebate,
16217            ..fee_asset_config("1")
16218        };
16219        let mut venue = fee_asset_venue_from(&config, true);
16220
16221        let outcome = venue.open_order(buy_request("1", None));
16222
16223        assert_eq!(
16224            filled_trade(&outcome).fees,
16225            AssetFees::new(quote(), d("-5"), Some(d("-5")))
16226        );
16227        assert_eq!(balance_now(&venue, quote()).total, d("50005"));
16228        assert_eq!(balance_now(&venue, bnb()), Balance::new(d("1"), d("1")));
16229    }
16230
16231    /// `USDTBNB` at a last price of 0.0016 prices BNB at its reciprocal, 625, and of two pairs
16232    /// pricing it, the one first by name is used.
16233    #[test]
16234    fn the_fee_asset_is_priced_by_a_reciprocal_pair_or_the_pair_first_by_name() {
16235        let usdt_bnb = spot_instrument("USDTBNB", "USDT", "BNB");
16236        let feed = |venue: &mut SimulatedVenue, name: &str, price: &str| {
16237            assert!(
16238                venue
16239                    .apply_market(
16240                        &InstrumentNameExchange::new(name),
16241                        MarketObservation::new(traded(price)),
16242                        time(1)
16243                    )
16244                    .is_empty()
16245            );
16246        };
16247
16248        // Only `USDTBNB`: 37.50 at 625 is 0.06 BNB.
16249        let mut instruments = spot_instruments();
16250        instruments.insert(usdt_bnb.name_exchange.clone(), usdt_bnb.clone());
16251        let mut venue = SimulatedVenue::new_market_driven(&fee_asset_config("1"), instruments);
16252        advance(&mut venue, time(1));
16253        feed(&mut venue, "USDTBNB", "0.0016");
16254        feed(&mut venue, "BTCUSDT", "50000");
16255        let outcome = venue.open_order(buy_request("1", None));
16256        assert_eq!(
16257            filled_trade(&outcome).fees,
16258            AssetFees::new(bnb(), d("0.06"), Some(d("37.5")))
16259        );
16260
16261        // Both: `BNBUSDT`, at a last price of 600, is first by name though fed first. 0.0625 BNB.
16262        let mut instruments = fee_asset_instruments();
16263        instruments.insert(usdt_bnb.name_exchange.clone(), usdt_bnb);
16264        let mut venue = SimulatedVenue::new_market_driven(&fee_asset_config("1"), instruments);
16265        advance(&mut venue, time(1));
16266        feed(&mut venue, "BNBUSDT", "600");
16267        feed(&mut venue, "USDTBNB", "0.0016");
16268        feed(&mut venue, "BTCUSDT", "50000");
16269        let outcome = venue.open_order(buy_request("1", None));
16270        assert_eq!(
16271            filled_trade(&outcome).fees,
16272            AssetFees::new(bnb(), d("0.0625"), Some(d("37.5")))
16273        );
16274    }
16275
16276    /// A marketable buy that fills half against the offer and rests the rest pays the filled
16277    /// half's fee in BNB, and holds the resting half's fee as the fallback would charge it.
16278    #[test]
16279    fn a_partly_filled_order_pays_its_fill_in_the_fee_asset_and_holds_the_rest_in_quote() {
16280        let mut venue = fee_asset_venue("1");
16281        assert!(
16282            venue
16283                .apply_market(
16284                    &instrument_name(),
16285                    MarketObservation::new(book("49000", "49100"))
16286                        .with_depth(MarketDepth::new(None, Some(d("0.5")))),
16287                    time(1),
16288                )
16289                .is_empty()
16290        );
16291
16292        let outcome = venue.open_order(limit_request("part", Side::Buy, "1", "49100", gtc()));
16293
16294        assert_eq!(rested_open(&outcome).filled_quantity, d("0.5"));
16295        // 24,550 × 10bp, a quarter off, at 600.
16296        assert_eq!(
16297            filled_trade(&outcome).fees,
16298            AssetFees::new(bnb(), d("0.0306875"), Some(d("18.4125")))
16299        );
16300        // 24,550 paid, and 24,550 plus the maker fee of 4.91 held for the resting half.
16301        assert_eq!(
16302            balance_now(&venue, quote()),
16303            Balance::new(d("75450"), d("50895.09"))
16304        );
16305        assert_eq!(
16306            balance_now(&venue, bnb()),
16307            Balance::new(d("0.9693125"), d("0.9693125"))
16308        );
16309    }
16310
16311    /// 10,000 USDT on a 3× margin account, BTC flat, paying fees in BNB and charged hourly on
16312    /// USDT loans, `BTCUSDT` at 50,000 and BNB at 600.
16313    fn fee_asset_margin_venue(bnb: &str) -> SimulatedVenue {
16314        let config = MockExecutionConfig {
16315            spot_margin: Some(
16316                SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
16317            ),
16318            fee_asset: Some(FeeAssetConfig::new(self::bnb(), d("0.25")).unwrap()),
16319            ..config_from_balances(
16320                vec![
16321                    funded(base(), Decimal::ZERO),
16322                    funded(quote(), d("10000")),
16323                    funded(self::bnb(), d(bnb)),
16324                ],
16325                maker_taker_fee(),
16326            )
16327        };
16328        fee_asset_venue_from(&config, true).with_margin_interest(Arc::new(hourly_interest(quote())))
16329    }
16330
16331    /// A buy of 0.3 BTC borrows the 5,000 USDT it pays beyond the account's own, and no more,
16332    /// since its fee is paid in BNB. The BNB restates after the fill's own balances, and the
16333    /// interest on the loan is reported after all three.
16334    #[test]
16335    fn a_margin_buy_paying_in_the_fee_asset_borrows_only_its_notional() {
16336        let mut venue = fee_asset_margin_venue("1");
16337
16338        let outcome = venue.open_order(buy_request("0.3", None));
16339
16340        // 15,000 × 10bp, a quarter off, at 600.
16341        assert_eq!(
16342            filled_trade(&outcome).fees,
16343            AssetFees::new(bnb(), d("0.01875"), Some(d("11.25")))
16344        );
16345        let paid = balance_of(sans_debt(&outcome.events)[0]);
16346        assert_eq!(paid.asset, quote());
16347        assert_eq!(
16348            paid.balance.margin.map(|margin| margin.borrowed),
16349            Some(d("5000"))
16350        );
16351        assert_eq!(balance_of(sans_debt(&outcome.events)[1]).asset, base());
16352        assert_eq!(
16353            *balance_of(sans_debt(&outcome.events)[2]),
16354            AssetBalance::new(
16355                bnb(),
16356                Balance::new_margin(d("0.98125"), d("0.98125"), d("0"), d("0")),
16357                time(1)
16358            )
16359        );
16360        assert_eq!(trade_of(sans_debt(&outcome.events)[3]).quantity, d("0.3"));
16361        // 5,000 at 0.01% for the hour, after the trade the loan paid for.
16362        assert_eq!(
16363            *cash_flow_of(sans_debt(&outcome.events)[4]),
16364            interest_flow(
16365                quote(),
16366                "5000",
16367                "0.5",
16368                time(1),
16369                InterestTiming::OnBorrow,
16370                "0"
16371            )
16372        );
16373    }
16374
16375    /// With no BNB, the fee falls back to quote, which the venue lends with the notional; it
16376    /// never lends BNB for a fee.
16377    #[test]
16378    fn a_margin_account_short_of_the_fee_asset_borrows_the_fallback_fee_not_the_fee_asset() {
16379        let mut venue = fee_asset_margin_venue("0");
16380
16381        let outcome = venue.open_order(buy_request("0.3", None));
16382
16383        assert_eq!(
16384            filled_trade(&outcome).fees,
16385            AssetFees::new(quote(), d("15"), Some(d("15")))
16386        );
16387        assert_eq!(
16388            balance_now(&venue, quote())
16389                .margin
16390                .map(|margin| margin.borrowed),
16391            Some(d("5015"))
16392        );
16393        assert_eq!(
16394            balance_now(&venue, bnb()),
16395            Balance::new_margin(d("0"), d("0"), d("0"), d("0"))
16396        );
16397    }
16398
16399    /// A resting buy borrows for its notional and its fallback fee. Filled with the fee paid in
16400    /// BNB, it repays the part of the loan the fee no longer needs, interest first.
16401    #[test]
16402    fn a_resting_margin_fill_paying_in_the_fee_asset_repays_the_fee_it_borrowed() {
16403        let mut venue = fee_asset_margin_venue("1");
16404        assert!(
16405            venue
16406                .apply_market(
16407                    &instrument_name(),
16408                    MarketObservation::new(book("49000", "49100")),
16409                    time(1)
16410                )
16411                .is_empty()
16412        );
16413        let outcome = venue.open_order(limit_request("resting", Side::Buy, "0.3", "49000", gtc()));
16414        // 14,700 plus the maker fee of 2.94, beyond the 10,000 held, and an hour's interest on it.
16415        assert_eq!(
16416            filled_balance(&outcome).balance,
16417            Balance::new_margin(d("14702.94"), d("0"), d("4702.94"), d("0.470294"))
16418        );
16419
16420        let events = venue.apply_market(
16421            &instrument_name(),
16422            MarketObservation::new(book("48900", "48950")),
16423            time(2),
16424        );
16425
16426        // 2.94, a quarter off, at 600.
16427        let trade = events
16428            .iter()
16429            .find_map(|event| match &event.kind {
16430                AccountEventKind::Trade(trade) => Some(trade),
16431                _ => None,
16432            })
16433            .unwrap();
16434        assert_eq!(
16435            trade.fees,
16436            AssetFees::new(bnb(), d("0.003675"), Some(d("2.205")))
16437        );
16438        // The 2.94 no longer needed repays the 0.470294 of interest, then 2.469706 of the loan.
16439        assert_eq!(
16440            balance_now(&venue, quote()),
16441            Balance::new_margin(d("0"), d("0"), d("4700.470294"), d("0"))
16442        );
16443        assert_eq!(balance_now(&venue, bnb()).total, d("0.996325"));
16444    }
16445
16446    /// A CFD fill is cash-settled, so its fee stays in quote even with the fee asset priced.
16447    #[test]
16448    fn a_cfd_fill_is_charged_without_the_fee_asset() {
16449        let mut instruments = instruments_of(cfd_instrument());
16450        let bnb_usd = spot_instrument("BNBUSD", "BNB", "usd");
16451        instruments.insert(bnb_usd.name_exchange.clone(), bnb_usd);
16452        let config = MockExecutionConfig {
16453            fee_asset: Some(FeeAssetConfig::new(bnb(), d("0.25")).unwrap()),
16454            ..config_from_balances(
16455                vec![funded(usd(), d("1000000")), funded(bnb(), d("10"))],
16456                maker_taker_fee(),
16457            )
16458        };
16459        let mut venue = SimulatedVenue::new_market_driven(&config, instruments);
16460        advance(&mut venue, time(1));
16461        assert!(
16462            venue
16463                .apply_market(
16464                    &InstrumentNameExchange::new("BNBUSD"),
16465                    MarketObservation::new(traded("600")),
16466                    time(1)
16467                )
16468                .is_empty()
16469        );
16470        assert!(cfd_market(&mut venue, "4999", "5001", "5000").is_empty());
16471
16472        let outcome = venue.open_order(cfd_request(Side::Buy, "1", None));
16473
16474        assert_eq!(filled_trade(&outcome).fees.asset, usd());
16475        assert_eq!(balance_now(&venue, bnb()).total, d("10"));
16476    }
16477
16478    fn eth() -> AssetNameExchange {
16479        AssetNameExchange::new("ETH")
16480    }
16481
16482    fn eth_instrument_name() -> InstrumentNameExchange {
16483        InstrumentNameExchange::new("ETHUSDT")
16484    }
16485
16486    /// BTCUSDT and ETHUSDT.
16487    fn btc_and_eth_instruments()
16488    -> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
16489        let mut instruments = spot_instruments();
16490        instruments.insert(
16491            eth_instrument_name(),
16492            Instrument {
16493                exchange: EXCHANGE,
16494                name_internal: InstrumentNameInternal::new("ethusdt"),
16495                name_exchange: eth_instrument_name(),
16496                underlying: Underlying {
16497                    base: eth(),
16498                    quote: quote(),
16499                },
16500                quote: InstrumentQuoteAsset::UnderlyingQuote,
16501                kind: InstrumentKind::Spot,
16502                spec: None,
16503                data_venue: None,
16504            },
16505        );
16506        instruments
16507    }
16508
16509    mod borrow_capacity;
16510    mod collateral_ratio;
16511    mod liquidation;
16512}