use super::*;
use crate::{margin::SpotMarginLiquidation, notice::NoticeKind};
fn window_config(
balances: Vec<AssetBalance<AssetNameExchange>>,
max_leverage: &str,
window: Option<TimeDelta>,
configure: impl FnOnce(SpotMarginConfig) -> SpotMarginConfig,
) -> MockExecutionConfig {
let mut spot_margin =
SpotMarginConfig::new(quote(), d(max_leverage), MarginEffect::Borrow).unwrap();
if let Some(window) = window {
spot_margin = spot_margin.with_borrow_price_window(window).unwrap();
}
MockExecutionConfig {
spot_margin: Some(configure(spot_margin)),
..config_from_balances(balances, FeeModelConfig::default())
}
}
fn hour() -> Option<TimeDelta> {
Some(TimeDelta::hours(1))
}
fn holding(
asset: AssetNameExchange,
amount: &str,
borrowed: &str,
) -> AssetBalance<AssetNameExchange> {
AssetBalance::new(
asset,
Balance::new_margin(d(amount), d(amount), d(borrowed), Decimal::ZERO),
Utc::now(),
)
}
fn feed(venue: &mut SimulatedVenue, instrument: &InstrumentNameExchange, prices: &[(i64, &str)]) {
for (at, price) in prices {
let events =
venue.apply_market(instrument, MarketObservation::new(traded(price)), time(*at));
assert!(events.is_empty(), "{events:?}");
}
}
fn borrowable_now(venue: &SimulatedVenue, side: Side) -> Decimal {
venue
.borrow_capacity(&instrument_name(), side)
.unwrap()
.borrowable_now
.unwrap()
}
fn binances_example(window: Option<TimeDelta>) -> SimulatedVenue {
let config = window_config(
vec![
funded(base(), d("0.1")),
funded(eth(), d("1")),
funded(quote(), Decimal::ZERO),
],
"5",
window,
|config| {
config
.with_borrow_collateral_ratio(base(), d("0.999"))
.unwrap()
.with_borrow_collateral_ratio(eth(), d("0.95"))
.unwrap()
},
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
feed(
&mut venue,
&instrument_name(),
&[(1, "80200"), (2, "80500"), (3, "80300")],
);
feed(
&mut venue,
ð_instrument_name(),
&[(1, "2200"), (2, "2180"), (3, "2190")],
);
venue
}
#[test]
fn binances_example_values_the_borrowed_token_at_its_highest_and_the_others_at_their_lowest() {
let lent = borrowable_now(&binances_example(hour()), Side::Sell);
assert_eq!(lent, d("4") * (d("8041.95") + d("2071")) / d("80500"));
assert_eq!(lent.round_dp(4), d("0.5025"));
}
#[test]
fn without_a_window_the_current_prices_value_the_account() {
let lent = borrowable_now(&binances_example(None), Side::Sell);
assert_eq!(lent, d("4") * (d("8021.97") + d("2080.5")) / d("80300"));
}
#[test]
fn an_asset_held_and_not_owed_counts_at_its_lowest() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
feed(
&mut venue,
&instrument_name(),
&[(1, "10000"), (2, "8000"), (3, "9000")],
);
assert_eq!(borrowable_now(&venue, Side::Buy), d("16000"));
}
#[test]
fn a_price_older_than_the_window_no_longer_counts() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
Some(TimeDelta::minutes(1)),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
feed(&mut venue, &instrument_name(), &[(1, "8000"), (30, "9000")]);
assert_eq!(borrowable_now(&venue, Side::Buy), d("16000"));
advance(&mut venue, time(61));
assert_eq!(
borrowable_now(&venue, Side::Buy),
d("16000"),
"61 s: 1 s is in"
);
advance(&mut venue, time(62));
assert_eq!(
borrowable_now(&venue, Side::Buy),
d("18000"),
"62 s: 1 s is out"
);
advance(&mut venue, time(100));
assert_eq!(
borrowable_now(&venue, Side::Buy),
d("18000"),
"100 s: only the current"
);
}
#[test]
fn the_window_never_lends_more_than_the_current_prices() {
let config = window_config(
vec![holding(base(), "10", "1"), funded(quote(), Decimal::ZERO)],
"5",
hour(),
|config| {
config
.with_borrow_collateral_ratio(base(), d("0.95"))
.unwrap()
},
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
feed(
&mut venue,
&instrument_name(),
&[(1, "20000"), (2, "10000")],
);
assert_eq!(borrowable_now(&venue, Side::Buy), d("332000"));
}
#[test]
fn a_debt_is_valued_at_its_highest() {
let config = window_config(
vec![holding(base(), "0", "1"), funded(quote(), d("30000"))],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
feed(
&mut venue,
&instrument_name(),
&[(1, "12000"), (2, "10000")],
);
assert_eq!(borrowable_now(&venue, Side::Buy), d("24000"));
}
#[test]
fn a_bars_high_and_low_count() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), d("10000"))],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
let bar = MarketBar::new(d("9000"), d("11000"), d("7000"), d("9000"));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("9000")).with_bar(Some(bar)),
time(1),
);
assert!(events.is_empty(), "{events:?}");
assert_eq!(borrowable_now(&venue, Side::Buy), d("34000"));
assert_eq!(borrowable_now(&venue, Side::Sell), d("42000") / d("11000"));
}
fn usdt_btc_instruments()
-> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
let name = InstrumentNameExchange::new("USDTBTC");
let mut instruments = FnvHashMap::default();
instruments.insert(
name.clone(),
Instrument {
exchange: EXCHANGE,
name_internal: InstrumentNameInternal::new("usdtbtc"),
name_exchange: name,
underlying: Underlying {
base: quote(),
quote: base(),
},
quote: InstrumentQuoteAsset::UnderlyingQuote,
kind: InstrumentKind::Spot,
spec: None,
data_venue: None,
},
);
instruments
}
#[test]
fn on_an_inverse_pair_an_assets_lowest_is_the_inverse_of_the_instruments_highest() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, usdt_btc_instruments());
let name = InstrumentNameExchange::new("USDTBTC");
feed(
&mut venue,
&name,
&[(1, "0.0001"), (2, "0.000125"), (3, "0.0001")],
);
let capacity = venue.borrow_capacity(&name, Side::Sell).unwrap();
assert_eq!(capacity.asset, quote());
assert_eq!(capacity.borrowable_now, Some(d("16000")));
}
#[test]
fn an_order_is_lent_no_more_than_the_window_allows() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
feed(&mut venue, &instrument_name(), &[(1, "8000"), (2, "9000")]);
let refused = venue.open_order(buy_request("1.9", None));
assert!(
insufficient_reason(&refused).contains("Borrowable: 16000"),
"{}",
insufficient_reason(&refused)
);
let bought = venue.open_order(buy_request("1.7", None));
assert_eq!(filled_trade(&bought).quantity, d("1.7"));
}
#[test]
fn the_liquidation_check_values_the_account_at_current_prices() {
let config = window_config(
vec![funded(base(), d("1")), holding(quote(), "0", "8000")],
"3",
hour(),
|config| {
config
.with_liquidation(SpotMarginLiquidation::new(d("1.1"), Decimal::ZERO).unwrap())
.unwrap()
},
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
let bar = MarketBar::new(d("20000"), d("20000"), d("8000"), d("20000"));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("20000")).with_bar(Some(bar)),
time(1),
);
assert!(
!events.iter().any(|event| matches!(
&event.kind,
AccountEventKind::Notice(notice) if notice.kind == NoticeKind::Liquidation
)),
"{events:?}"
);
assert_eq!(borrowable_now(&venue, Side::Buy), Decimal::ZERO);
}
#[test]
fn a_request_priced_venue_lends_at_the_requests_own_price() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new(&config, spot_instruments());
advance(&mut venue, time(1));
let refused = venue.open_order(buy_request("2.1", Some(traded("9000"))));
assert!(
insufficient_reason(&refused).contains("Borrowable: 18000"),
"{}",
insufficient_reason(&refused)
);
}
#[test]
fn a_price_of_zero_is_not_recorded() {
let config = window_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
let bar = MarketBar::new(d("9000"), d("9000"), Decimal::ZERO, d("9000"));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("9000")).with_bar(Some(bar)),
time(1),
);
assert!(events.is_empty(), "{events:?}");
assert_eq!(borrowable_now(&venue, Side::Buy), d("18000"));
}
#[test]
fn on_an_inverse_pair_an_owed_assets_highest_is_the_inverse_of_the_instruments_lowest() {
let config = window_config(
vec![holding(base(), "0", "1"), funded(quote(), d("30000"))],
"3",
hour(),
|config| config,
);
let mut venue = SimulatedVenue::new_market_driven(&config, usdt_btc_instruments());
let name = InstrumentNameExchange::new("USDTBTC");
feed(&mut venue, &name, &[(1, "0.0001"), (2, "0.000125")]);
let capacity = venue.borrow_capacity(&name, Side::Sell).unwrap();
assert_eq!(capacity.borrowable_now, Some(d("30000")));
}