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}