Skip to main content

rustrade_execution/exchange/mock/venue/
snapshot.rs

1//! A [`SimulatedVenue`]'s state as plain data, and the venue it restores.
2
3use super::price_window::PriceWindow;
4use super::{NetPosition, SimulatedVenue, VenueRegime, liquidation::LiquidationState};
5use crate::{
6    balance::MarginDetails,
7    client::mock::MockExecutionConfig,
8    exchange::mock::account::{AccountState, AccountStateSnapshot, DuplicateSeededOrder},
9    slippage::Slippage,
10    stored::FormatVersion,
11};
12use chrono::{DateTime, Utc};
13use fnv::FnvHashMap;
14use itertools::Itertools;
15use rust_decimal::Decimal;
16use rustrade_instrument::{
17    asset::name::AssetNameExchange,
18    exchange::ExchangeId,
19    instrument::{Instrument, name::InstrumentNameExchange},
20};
21use serde::{Deserialize, Serialize};
22use std::{
23    collections::{BTreeSet, HashSet},
24    hash::Hash,
25};
26use thiserror::Error;
27
28/// Everything a [`SimulatedVenue`] has accumulated since it was built, as a value serde can write,
29/// from which [`SimulatedVenue::from_snapshot`] builds a venue that carries on exactly as this one
30/// would have.
31///
32/// Taken by [`SimulatedVenue::snapshot`]. When and where to keep one is the caller's choice.
33///
34/// # What it holds
35/// - The account: every balance with its loan and unpaid interest, every open and held order with
36///   what is held against it and its place in the queue, the orders that ended, and the trades.
37/// - The net position of each cash-settled instrument, with its entry price.
38/// - The order, trade and cash-flow id sequences, so no id the venue mints after a restore repeats
39///   one minted before it.
40/// - The venue's clock: the latest instant it reached and the instant up to which it has charged
41///   interest and holding costs.
42/// - The debt it last reported, whether a liquidation is under way or a margin call outstanding,
43///   the prices over the borrow price window, and how far the slippage stream has drawn.
44///
45/// # What it does not hold
46/// - **The configuration**, and the models set by builder methods (holding costs, margin interest,
47///   a fill model, shortability): code, not data. Restore with the configuration and models the
48///   venue ran with. A changed fee model or interest model applies from the restore on.
49/// - **The market**: what the venue last observed is stale by the time it is restored. A restored
50///   venue holds no market until its driver feeds one, so it values and matches nothing before
51///   then, as a fresh venue does not.
52/// - **Warnings it gave once**, which a restored venue gives again.
53///
54/// # The time between the snapshot and the restore
55/// A restored venue resumes from the instant recorded:
56/// - **Interest and holding costs** are charged for every boundary after it, once its clock first
57///   moves, as for a host that slept through them.
58/// - **A resting order** is matched against the first market observed after the restore, as on
59///   any observation. A price that crossed it and turned back while the venue was not running was
60///   never observed, so the order did not fill.
61///
62/// # Versions
63/// Carries a format [`version`](Self::version). In a self-describing format such as JSON, fields a
64/// later release adds are read with defaults from an older snapshot. A change an older snapshot
65/// cannot be read under raises the version, and [`SimulatedVenue::from_snapshot`] refuses a
66/// version other than [`VERSION`](Self::VERSION).
67///
68/// A snapshot whose shape this release cannot parse fails in the deserialiser, before it reaches
69/// the venue. Read one as a [`Stored`](crate::stored::Stored) snapshot to be told the version it
70/// was written in instead.
71///
72/// `Ord` and `Hash` exist so it can sit in an engine output, which requires them, and are not a
73/// meaningful ranking.
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
75pub struct SimulatedVenueSnapshot {
76    version: u32,
77    exchange: ExchangeId,
78    regime: VenueRegime,
79    /// Sorted.
80    instruments: Vec<InstrumentNameExchange>,
81    account: AccountStateSnapshot,
82    /// Sorted by instrument.
83    positions: Vec<(InstrumentNameExchange, NetPosition)>,
84    order_sequence: u64,
85    /// Written as a string: a number this wide does not survive every reader, such as a
86    /// [`Stored`](crate::stored::Stored) snapshot read from JSON.
87    #[serde(with = "u128_as_string")]
88    order_id_start: u128,
89    trade_sequence: u64,
90    cash_flow_sequence: u64,
91    time_exchange: DateTime<Utc>,
92    accrued_through: Option<DateTime<Utc>>,
93    /// Sorted by asset.
94    reported_debt: Vec<(AssetNameExchange, MarginDetails)>,
95    liquidation_under_way: bool,
96    margin_called: bool,
97    /// Sorted by instrument.
98    borrow_prices: Vec<(InstrumentNameExchange, PriceWindow)>,
99    slippage_draws: u64,
100    checkpoint_sequence: u64,
101}
102
103impl SimulatedVenueSnapshot {
104    /// The format version this release writes, and the only one it restores.
105    pub const VERSION: u32 = 1;
106
107    /// The format version this snapshot was written in.
108    pub fn version(&self) -> u32 {
109        self.version
110    }
111
112    /// The exchange the venue simulated.
113    pub fn exchange(&self) -> ExchangeId {
114        self.exchange
115    }
116
117    /// The venue's price source, which the restored venue keeps.
118    pub fn regime(&self) -> VenueRegime {
119        self.regime
120    }
121
122    /// The latest instant the venue's clock had reached.
123    pub fn time_exchange(&self) -> DateTime<Utc> {
124        self.time_exchange
125    }
126
127    /// The net position of each cash-settled instrument the venue held one in, in contracts:
128    /// positive long, negative short. A flat instrument is not listed, nor is a spot holding, which
129    /// the venue holds as an asset balance.
130    pub fn positions(&self) -> impl Iterator<Item = (&InstrumentNameExchange, Decimal)> + '_ {
131        self.positions
132            .iter()
133            .map(|(instrument, position)| (instrument, position.quantity))
134    }
135
136    /// The instant up to which the venue had charged every interest and holding-cost boundary, or
137    /// `None` if its clock had never moved.
138    pub fn accrued_through(&self) -> Option<DateTime<Utc>> {
139        self.accrued_through
140    }
141}
142
143/// A [`SimulatedVenueSnapshot`] taken at one point in the venue's account stream, and sent on it as
144/// an [`AccountEventKind::VenueCheckpoint`](crate::AccountEventKind::VenueCheckpoint): every event
145/// the venue sent before it is in the snapshot, and none sent after it.
146///
147/// Made by [`SimulatedVenue::checkpoint`].
148#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
149pub struct VenueCheckpoint {
150    /// Numbers the venue's checkpoints from one, counting across restores, so a caller can tell
151    /// the checkpoint it asked for from an earlier one.
152    pub id: u64,
153    /// The venue's state at this point in its stream.
154    pub snapshot: SimulatedVenueSnapshot,
155}
156
157/// Why [`SimulatedVenue::from_snapshot`] refused a [`SimulatedVenueSnapshot`].
158#[derive(Debug, Clone, PartialEq, Eq, Error)]
159#[non_exhaustive]
160pub enum VenueRestoreError {
161    /// The snapshot was written in a format version this release does not read.
162    #[error("venue snapshot is format version {found}; this release reads version {supported}")]
163    UnsupportedVersion { found: u32, supported: u32 },
164
165    /// The snapshot is of another exchange than the configuration.
166    #[error("venue snapshot is of {snapshot}, but the configuration simulates {config}")]
167    ExchangeMismatch {
168        snapshot: ExchangeId,
169        config: ExchangeId,
170    },
171
172    /// The snapshot's venue traded other instruments than the venue being restored.
173    #[error(
174        "venue snapshot traded other instruments than the venue being restored: only in the \
175         snapshot {only_in_snapshot:?}, only in the venue {only_in_venue:?}"
176    )]
177    InstrumentsMismatch {
178        only_in_snapshot: Vec<InstrumentNameExchange>,
179        only_in_venue: Vec<InstrumentNameExchange>,
180    },
181
182    /// The snapshot names an instrument it does not list among those it traded.
183    #[error("venue snapshot names {0}, which it does not list among its instruments")]
184    UnknownInstrument(InstrumentNameExchange),
185
186    /// The snapshot was of a margin account and the configuration lends nothing, or the other way
187    /// round.
188    #[error(
189        "venue snapshot is of a {} account, but the configuration {}",
190        if *snapshot_margin { "margin" } else { "cash" },
191        if *snapshot_margin { "sets no spot_margin" } else { "sets spot_margin" }
192    )]
193    MarginMismatch { snapshot_margin: bool },
194
195    /// The snapshot lists one order id twice.
196    #[error(transparent)]
197    DuplicateOrder(#[from] DuplicateSeededOrder),
198
199    /// The snapshot lists one instrument twice among its positions, or among its borrow price
200    /// windows.
201    #[error("venue snapshot lists {0} twice")]
202    DuplicateInstrument(InstrumentNameExchange),
203
204    /// The snapshot lists one asset twice among its balances, or among the debt it last reported.
205    #[error("venue snapshot lists {0} twice")]
206    DuplicateAsset(AssetNameExchange),
207}
208
209impl FormatVersion for SimulatedVenueSnapshot {
210    // The inherent constant: an inherent item takes priority over the trait's, so this does not
211    // refer to itself.
212    const VERSION: u32 = Self::VERSION;
213
214    fn format_version(&self) -> u32 {
215        self.version
216    }
217}
218
219/// A `u128` written as a decimal string.
220mod u128_as_string {
221    use serde::{Deserialize, Deserializer, Serializer, de::Error};
222
223    pub(super) fn serialize<S: Serializer>(value: &u128, serializer: S) -> Result<S::Ok, S::Error> {
224        serializer.collect_str(value)
225    }
226
227    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
228        deserializer: D,
229    ) -> Result<u128, D::Error> {
230        let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
231        text.parse().map_err(D::Error::custom)
232    }
233}
234
235/// The first item `items` yields that an earlier one equals.
236fn first_repeat<'a, T: Eq + Hash + 'a>(items: impl IntoIterator<Item = &'a T>) -> Option<&'a T> {
237    let mut seen = HashSet::new();
238    items.into_iter().find(|item| !seen.insert(*item))
239}
240
241impl SimulatedVenue {
242    /// This venue's state as plain data: see [`SimulatedVenueSnapshot`].
243    ///
244    /// Taken between inputs, so it reflects every input applied so far and nothing of the next.
245    pub fn snapshot(&self) -> SimulatedVenueSnapshot {
246        SimulatedVenueSnapshot {
247            version: SimulatedVenueSnapshot::VERSION,
248            exchange: self.exchange,
249            regime: self.regime,
250            instruments: self.instruments.keys().cloned().sorted_unstable().collect(),
251            account: self.account.snapshot(),
252            positions: self
253                .positions
254                .iter()
255                .map(|(instrument, position)| (instrument.clone(), *position))
256                .sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
257                .collect(),
258            order_sequence: self.order_sequence,
259            order_id_start: self.order_id_start,
260            trade_sequence: self.trade_sequence,
261            cash_flow_sequence: self.cash_flow_sequence,
262            time_exchange: self.time_exchange_latest,
263            accrued_through: self.accrued_through,
264            reported_debt: self
265                .reported_debt
266                .iter()
267                .map(|(asset, debt)| (asset.clone(), *debt))
268                .sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
269                .collect(),
270            liquidation_under_way: self.liquidation.under_way(),
271            margin_called: self.liquidation.margin_called(),
272            borrow_prices: self
273                .borrow_prices
274                .iter()
275                .map(|(instrument, window)| (instrument.clone(), window.clone()))
276                .sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
277                .collect(),
278            slippage_draws: self.slippage.draws(),
279            checkpoint_sequence: self.checkpoint_sequence,
280        }
281    }
282
283    /// This venue's state, numbered as its next checkpoint: see [`VenueCheckpoint`].
284    ///
285    /// A driver sends it as an
286    /// [`AccountEventKind::VenueCheckpoint`](crate::AccountEventKind::VenueCheckpoint) in order
287    /// with the venue's other output, so whoever applies the stream up to it holds what the
288    /// snapshot holds.
289    pub fn checkpoint(&mut self) -> VenueCheckpoint {
290        self.checkpoint_sequence += 1;
291        VenueCheckpoint {
292            id: self.checkpoint_sequence,
293            snapshot: self.snapshot(),
294        }
295    }
296
297    /// The venue `snapshot` was taken of, configured by `config` and trading `instruments`, which
298    /// carries on exactly as that venue would have: see [`SimulatedVenueSnapshot`] for what it
299    /// restores and what happens to the time in between.
300    ///
301    /// Takes the regime the snapshot's venue had; a driver checks [`regime`](Self::regime) is the
302    /// one it drives. `config.initial_state` is not read: the snapshot's account replaces it. Add
303    /// the models the venue ran with through the same builder methods as a fresh venue, such as
304    /// [`with_margin_interest`](Self::with_margin_interest), which then charge from the recorded
305    /// instant.
306    ///
307    /// # Errors
308    /// - [`VenueRestoreError::UnsupportedVersion`] for a snapshot in another format version.
309    /// - [`VenueRestoreError::ExchangeMismatch`] if `config` simulates another exchange.
310    /// - [`VenueRestoreError::InstrumentsMismatch`] unless `instruments` are exactly the ones the
311    ///   snapshot's venue traded.
312    /// - [`VenueRestoreError::UnknownInstrument`] if the snapshot names an instrument it does not
313    ///   list.
314    /// - [`VenueRestoreError::MarginMismatch`] if the snapshot is of a margin account and `config`
315    ///   sets no `spot_margin`, or the other way round.
316    /// - [`VenueRestoreError::DuplicateOrder`] if the snapshot lists one order id twice among its
317    ///   open orders, or among its cancelled, filled or expired ones.
318    /// - [`VenueRestoreError::DuplicateInstrument`] or [`VenueRestoreError::DuplicateAsset`] if it
319    ///   lists one instrument or asset twice in a list that holds each once.
320    ///
321    /// # Cost
322    /// The slippage stream is resumed by drawing again every value the snapshot's venue drew, one
323    /// per fill it slipped, so a restore takes time in proportion to the snapshot's fills. A
324    /// snapshot is trusted input: one edited to claim a vast number of draws restores as slowly.
325    pub fn from_snapshot(
326        config: &MockExecutionConfig,
327        instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
328        snapshot: SimulatedVenueSnapshot,
329    ) -> Result<Self, VenueRestoreError> {
330        if snapshot.version != SimulatedVenueSnapshot::VERSION {
331            return Err(VenueRestoreError::UnsupportedVersion {
332                found: snapshot.version,
333                supported: SimulatedVenueSnapshot::VERSION,
334            });
335        }
336        if snapshot.exchange != config.mocked_exchange {
337            return Err(VenueRestoreError::ExchangeMismatch {
338                snapshot: snapshot.exchange,
339                config: config.mocked_exchange,
340            });
341        }
342
343        let listed = snapshot.instruments.iter().collect::<BTreeSet<_>>();
344        let trading = instruments.keys().collect::<BTreeSet<_>>();
345        if listed != trading {
346            return Err(VenueRestoreError::InstrumentsMismatch {
347                only_in_snapshot: listed
348                    .difference(&trading)
349                    .map(|&name| name.clone())
350                    .collect(),
351                only_in_venue: trading
352                    .difference(&listed)
353                    .map(|&name| name.clone())
354                    .collect(),
355            });
356        }
357        if let Some(unknown) = snapshot
358            .account
359            .instruments()
360            .chain(snapshot.positions.iter().map(|(instrument, _)| instrument))
361            .chain(
362                snapshot
363                    .borrow_prices
364                    .iter()
365                    .map(|(instrument, _)| instrument),
366            )
367            .find(|instrument| !instruments.contains_key(*instrument))
368        {
369            return Err(VenueRestoreError::UnknownInstrument(unknown.clone()));
370        }
371        // Each list below becomes a map, which would keep one of two entries and drop the other.
372        if let Some(repeat) = first_repeat(snapshot.positions.iter().map(|(name, _)| name))
373            .or_else(|| first_repeat(snapshot.borrow_prices.iter().map(|(name, _)| name)))
374        {
375            return Err(VenueRestoreError::DuplicateInstrument(repeat.clone()));
376        }
377        if let Some(repeat) = first_repeat(snapshot.account.balance_assets())
378            .or_else(|| first_repeat(snapshot.reported_debt.iter().map(|(asset, _)| asset)))
379        {
380            return Err(VenueRestoreError::DuplicateAsset(repeat.clone()));
381        }
382        if snapshot.account.margin() != config.spot_margin.is_some() {
383            return Err(VenueRestoreError::MarginMismatch {
384                snapshot_margin: snapshot.account.margin(),
385            });
386        }
387
388        let SimulatedVenueSnapshot {
389            version: _,
390            exchange: _,
391            regime,
392            instruments: _,
393            account,
394            positions,
395            order_sequence,
396            order_id_start,
397            trade_sequence,
398            cash_flow_sequence,
399            time_exchange,
400            accrued_through,
401            reported_debt,
402            liquidation_under_way,
403            margin_called,
404            borrow_prices,
405            slippage_draws,
406            checkpoint_sequence,
407        } = snapshot;
408
409        let mut venue = Self::assemble(
410            config,
411            instruments,
412            regime,
413            AccountState::from_snapshot(account)?,
414            positions.into_iter().collect(),
415            order_id_start,
416            reported_debt.into_iter().collect(),
417        );
418        venue.order_sequence = order_sequence;
419        venue.trade_sequence = trade_sequence;
420        venue.cash_flow_sequence = cash_flow_sequence;
421        venue.time_exchange_latest = time_exchange;
422        venue.accrued_through = accrued_through;
423        venue.liquidation = LiquidationState::restored(liquidation_under_way, margin_called);
424        // A window the configuration no longer sets would never be read or trimmed.
425        if config
426            .spot_margin
427            .as_ref()
428            .is_some_and(|margin| margin.borrow_price_window().is_some())
429        {
430            venue.borrow_prices = borrow_prices.into_iter().collect();
431        } else if !borrow_prices.is_empty() {
432            tracing::warn!(
433                exchange = %config.mocked_exchange,
434                windows = borrow_prices.len(),
435                "restored venue drops the borrow price windows of its snapshot: the configuration \
436                 sets no borrow price window"
437            );
438        }
439        venue.slippage = Slippage::resumed(config.slippage, slippage_draws);
440        venue.checkpoint_sequence = checkpoint_sequence;
441        venue.refresh_next_charge();
442        venue.refresh_next_interest();
443        Ok(venue)
444    }
445}