use super::*;
use crate::{
margin::{SpotMarginConfigError, SpotMarginLiquidation},
notice::NoticeKind,
};
fn ratio_config(
balances: Vec<AssetBalance<AssetNameExchange>>,
max_leverage: &str,
configure: impl FnOnce(SpotMarginConfig) -> Result<SpotMarginConfig, SpotMarginConfigError>,
) -> MockExecutionConfig {
let spot_margin =
SpotMarginConfig::new(quote(), d(max_leverage), MarginEffect::Borrow).unwrap();
MockExecutionConfig {
spot_margin: Some(configure(spot_margin).unwrap()),
..config_from_balances(balances, FeeModelConfig::default())
}
}
fn fed_venue(config: &MockExecutionConfig, btc: &str, eth: &str) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(config, btc_and_eth_instruments());
advance(&mut venue, time(1));
for (instrument, price) in [(instrument_name(), btc), (eth_instrument_name(), eth)] {
assert!(
venue
.apply_market(&instrument, MarketObservation::new(traded(price)), time(1))
.is_empty()
);
}
venue
}
fn borrowable_now(venue: &SimulatedVenue, side: Side) -> Decimal {
venue
.borrow_capacity(&instrument_name(), side)
.unwrap()
.borrowable_now
.unwrap()
}
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(),
)
}
#[test]
fn binances_worked_max_borrow_example_is_reproduced() {
let config = ratio_config(
vec![
funded(base(), d("0.1")),
funded(eth(), d("1")),
funded(quote(), Decimal::ZERO),
],
"5",
|config| {
config
.with_borrow_collateral_ratio(base(), d("0.999"))?
.with_borrow_collateral_ratio(eth(), d("0.95"))
},
);
let venue = fed_venue(&config, "80500", "2180");
let lent = borrowable_now(&venue, 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 only_what_an_asset_holds_beyond_its_debt_is_weighted() {
let config = ratio_config(
vec![holding(base(), "2", "1"), funded(quote(), Decimal::ZERO)],
"3",
|config| config.with_borrow_collateral_ratio(base(), d("0.8")),
);
let venue = fed_venue(&config, "10000", "1000");
assert_eq!(borrowable_now(&venue, Side::Buy), d("6000"));
}
#[test]
fn an_asset_owed_beyond_its_holdings_is_not_weighted() {
let config = ratio_config(
vec![holding(base(), "0.5", "1"), funded(quote(), d("20000"))],
"3",
|config| config.with_borrow_collateral_ratio(base(), d("0.1")),
);
let venue = fed_venue(&config, "10000", "1000");
assert_eq!(borrowable_now(&venue, Side::Buy), d("20000"));
}
#[test]
fn an_order_needing_more_than_the_weighted_equity_allows_is_refused() {
let config = ratio_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"2",
|config| config.with_borrow_collateral_ratio(base(), d("0.5")),
);
let mut venue = fed_venue(&config, "10000", "1000");
let refused = venue.open_order(buy_request("0.6", None));
assert!(
insufficient_reason(&refused).contains("Borrowable: 5000"),
"{}",
insufficient_reason(&refused)
);
let bought = venue.open_order(buy_request("0.5", None));
assert_eq!(filled_trade(&bought).quantity, d("0.5"));
}
#[test]
fn a_ratio_of_zero_lends_nothing_against_the_asset() {
let config = ratio_config(
vec![funded(base(), d("1")), funded(quote(), Decimal::ZERO)],
"3",
|config| config.with_borrow_collateral_ratio(base(), Decimal::ZERO),
);
let venue = fed_venue(&config, "10000", "1000");
assert_eq!(borrowable_now(&venue, Side::Buy), Decimal::ZERO);
}
fn owing_8000_usdt(liquidation_ratio: &str) -> MockExecutionConfig {
ratio_config(
vec![funded(base(), d("1")), holding(quote(), "0", "8000")],
"3",
|config| {
config
.with_borrow_collateral_ratio(base(), d("0.5"))?
.with_liquidation_collateral_ratio(base(), d(liquidation_ratio))?
.with_liquidation(SpotMarginLiquidation::new(d("1.1"), Decimal::ZERO).unwrap())
},
)
}
fn liquidated(events: &[UnindexedAccountEvent]) -> bool {
events.iter().any(|event| {
matches!(&event.kind, AccountEventKind::Notice(notice) if notice.kind == NoticeKind::Liquidation)
})
}
#[test]
fn liquidation_weighs_collateral_by_the_liquidation_ratio_not_the_borrow_ratio() {
let config = owing_8000_usdt("1");
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("10000")),
time(1),
);
assert!(!liquidated(&events), "{events:?}");
let config = owing_8000_usdt("0.85");
let mut venue = SimulatedVenue::new_market_driven(&config, btc_and_eth_instruments());
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("10000")),
time(1),
);
assert!(liquidated(&events), "{events:?}");
}