use super::price_window::PriceWindow;
use super::{NetPosition, SimulatedVenue, VenueRegime, liquidation::LiquidationState};
use crate::{
balance::MarginDetails,
client::mock::MockExecutionConfig,
exchange::mock::account::{AccountState, AccountStateSnapshot, DuplicateSeededOrder},
slippage::Slippage,
stored::FormatVersion,
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use itertools::Itertools;
use rust_decimal::Decimal;
use rustrade_instrument::{
asset::name::AssetNameExchange,
exchange::ExchangeId,
instrument::{Instrument, name::InstrumentNameExchange},
};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeSet, HashSet},
hash::Hash,
};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct SimulatedVenueSnapshot {
version: u32,
exchange: ExchangeId,
regime: VenueRegime,
instruments: Vec<InstrumentNameExchange>,
account: AccountStateSnapshot,
positions: Vec<(InstrumentNameExchange, NetPosition)>,
order_sequence: u64,
#[serde(with = "u128_as_string")]
order_id_start: u128,
trade_sequence: u64,
cash_flow_sequence: u64,
time_exchange: DateTime<Utc>,
accrued_through: Option<DateTime<Utc>>,
reported_debt: Vec<(AssetNameExchange, MarginDetails)>,
liquidation_under_way: bool,
margin_called: bool,
borrow_prices: Vec<(InstrumentNameExchange, PriceWindow)>,
slippage_draws: u64,
checkpoint_sequence: u64,
}
impl SimulatedVenueSnapshot {
pub const VERSION: u32 = 1;
pub fn version(&self) -> u32 {
self.version
}
pub fn exchange(&self) -> ExchangeId {
self.exchange
}
pub fn regime(&self) -> VenueRegime {
self.regime
}
pub fn time_exchange(&self) -> DateTime<Utc> {
self.time_exchange
}
pub fn positions(&self) -> impl Iterator<Item = (&InstrumentNameExchange, Decimal)> + '_ {
self.positions
.iter()
.map(|(instrument, position)| (instrument, position.quantity))
}
pub fn accrued_through(&self) -> Option<DateTime<Utc>> {
self.accrued_through
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct VenueCheckpoint {
pub id: u64,
pub snapshot: SimulatedVenueSnapshot,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum VenueRestoreError {
#[error("venue snapshot is format version {found}; this release reads version {supported}")]
UnsupportedVersion { found: u32, supported: u32 },
#[error("venue snapshot is of {snapshot}, but the configuration simulates {config}")]
ExchangeMismatch {
snapshot: ExchangeId,
config: ExchangeId,
},
#[error(
"venue snapshot traded other instruments than the venue being restored: only in the \
snapshot {only_in_snapshot:?}, only in the venue {only_in_venue:?}"
)]
InstrumentsMismatch {
only_in_snapshot: Vec<InstrumentNameExchange>,
only_in_venue: Vec<InstrumentNameExchange>,
},
#[error("venue snapshot names {0}, which it does not list among its instruments")]
UnknownInstrument(InstrumentNameExchange),
#[error(
"venue snapshot is of a {} account, but the configuration {}",
if *snapshot_margin { "margin" } else { "cash" },
if *snapshot_margin { "sets no spot_margin" } else { "sets spot_margin" }
)]
MarginMismatch { snapshot_margin: bool },
#[error(transparent)]
DuplicateOrder(#[from] DuplicateSeededOrder),
#[error("venue snapshot lists {0} twice")]
DuplicateInstrument(InstrumentNameExchange),
#[error("venue snapshot lists {0} twice")]
DuplicateAsset(AssetNameExchange),
}
impl FormatVersion for SimulatedVenueSnapshot {
const VERSION: u32 = Self::VERSION;
fn format_version(&self) -> u32 {
self.version
}
}
mod u128_as_string {
use serde::{Deserialize, Deserializer, Serializer, de::Error};
pub(super) fn serialize<S: Serializer>(value: &u128, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(value)
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<u128, D::Error> {
let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
text.parse().map_err(D::Error::custom)
}
}
fn first_repeat<'a, T: Eq + Hash + 'a>(items: impl IntoIterator<Item = &'a T>) -> Option<&'a T> {
let mut seen = HashSet::new();
items.into_iter().find(|item| !seen.insert(*item))
}
impl SimulatedVenue {
pub fn snapshot(&self) -> SimulatedVenueSnapshot {
SimulatedVenueSnapshot {
version: SimulatedVenueSnapshot::VERSION,
exchange: self.exchange,
regime: self.regime,
instruments: self.instruments.keys().cloned().sorted_unstable().collect(),
account: self.account.snapshot(),
positions: self
.positions
.iter()
.map(|(instrument, position)| (instrument.clone(), *position))
.sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
.collect(),
order_sequence: self.order_sequence,
order_id_start: self.order_id_start,
trade_sequence: self.trade_sequence,
cash_flow_sequence: self.cash_flow_sequence,
time_exchange: self.time_exchange_latest,
accrued_through: self.accrued_through,
reported_debt: self
.reported_debt
.iter()
.map(|(asset, debt)| (asset.clone(), *debt))
.sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
.collect(),
liquidation_under_way: self.liquidation.under_way(),
margin_called: self.liquidation.margin_called(),
borrow_prices: self
.borrow_prices
.iter()
.map(|(instrument, window)| (instrument.clone(), window.clone()))
.sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
.collect(),
slippage_draws: self.slippage.draws(),
checkpoint_sequence: self.checkpoint_sequence,
}
}
pub fn checkpoint(&mut self) -> VenueCheckpoint {
self.checkpoint_sequence += 1;
VenueCheckpoint {
id: self.checkpoint_sequence,
snapshot: self.snapshot(),
}
}
pub fn from_snapshot(
config: &MockExecutionConfig,
instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
snapshot: SimulatedVenueSnapshot,
) -> Result<Self, VenueRestoreError> {
if snapshot.version != SimulatedVenueSnapshot::VERSION {
return Err(VenueRestoreError::UnsupportedVersion {
found: snapshot.version,
supported: SimulatedVenueSnapshot::VERSION,
});
}
if snapshot.exchange != config.mocked_exchange {
return Err(VenueRestoreError::ExchangeMismatch {
snapshot: snapshot.exchange,
config: config.mocked_exchange,
});
}
let listed = snapshot.instruments.iter().collect::<BTreeSet<_>>();
let trading = instruments.keys().collect::<BTreeSet<_>>();
if listed != trading {
return Err(VenueRestoreError::InstrumentsMismatch {
only_in_snapshot: listed
.difference(&trading)
.map(|&name| name.clone())
.collect(),
only_in_venue: trading
.difference(&listed)
.map(|&name| name.clone())
.collect(),
});
}
if let Some(unknown) = snapshot
.account
.instruments()
.chain(snapshot.positions.iter().map(|(instrument, _)| instrument))
.chain(
snapshot
.borrow_prices
.iter()
.map(|(instrument, _)| instrument),
)
.find(|instrument| !instruments.contains_key(*instrument))
{
return Err(VenueRestoreError::UnknownInstrument(unknown.clone()));
}
if let Some(repeat) = first_repeat(snapshot.positions.iter().map(|(name, _)| name))
.or_else(|| first_repeat(snapshot.borrow_prices.iter().map(|(name, _)| name)))
{
return Err(VenueRestoreError::DuplicateInstrument(repeat.clone()));
}
if let Some(repeat) = first_repeat(snapshot.account.balance_assets())
.or_else(|| first_repeat(snapshot.reported_debt.iter().map(|(asset, _)| asset)))
{
return Err(VenueRestoreError::DuplicateAsset(repeat.clone()));
}
if snapshot.account.margin() != config.spot_margin.is_some() {
return Err(VenueRestoreError::MarginMismatch {
snapshot_margin: snapshot.account.margin(),
});
}
let SimulatedVenueSnapshot {
version: _,
exchange: _,
regime,
instruments: _,
account,
positions,
order_sequence,
order_id_start,
trade_sequence,
cash_flow_sequence,
time_exchange,
accrued_through,
reported_debt,
liquidation_under_way,
margin_called,
borrow_prices,
slippage_draws,
checkpoint_sequence,
} = snapshot;
let mut venue = Self::assemble(
config,
instruments,
regime,
AccountState::from_snapshot(account)?,
positions.into_iter().collect(),
order_id_start,
reported_debt.into_iter().collect(),
);
venue.order_sequence = order_sequence;
venue.trade_sequence = trade_sequence;
venue.cash_flow_sequence = cash_flow_sequence;
venue.time_exchange_latest = time_exchange;
venue.accrued_through = accrued_through;
venue.liquidation = LiquidationState::restored(liquidation_under_way, margin_called);
if config
.spot_margin
.as_ref()
.is_some_and(|margin| margin.borrow_price_window().is_some())
{
venue.borrow_prices = borrow_prices.into_iter().collect();
} else if !borrow_prices.is_empty() {
tracing::warn!(
exchange = %config.mocked_exchange,
windows = borrow_prices.len(),
"restored venue drops the borrow price windows of its snapshot: the configuration \
sets no borrow price window"
);
}
venue.slippage = Slippage::resumed(config.slippage, slippage_draws);
venue.checkpoint_sequence = checkpoint_sequence;
venue.refresh_next_charge();
venue.refresh_next_interest();
Ok(venue)
}
}