use super::*;
fn fed_venue(config: &MockExecutionConfig, price: &str) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(config, spot_instruments());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(traded(price)),
time(1),
)
.is_empty()
);
venue
}
fn capacity(venue: &SimulatedVenue, side: Side) -> BorrowCapacity {
venue
.borrow_capacity(&instrument_name(), side)
.expect("a margin venue lends against BTCUSDT")
}
fn invalid_reason(venue: &SimulatedVenue, instrument: &InstrumentNameExchange) -> String {
match venue.borrow_capacity(instrument, Side::Sell) {
Err(ApiError::InstrumentInvalid(named, reason)) => {
assert_eq!(named, *instrument);
reason
}
other => panic!("expected InstrumentInvalid, got: {other:?}"),
}
}
fn owing_btc_config(btc_owed: &str, btc_interest: &str) -> MockExecutionConfig {
MockExecutionConfig {
spot_margin: Some(SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap()),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d(btc_owed), d(btc_interest)),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
}
}
#[test]
fn a_sell_borrows_base_and_a_buy_quote_up_to_the_leverage_limit() {
let venue = fed_venue(&margin_config("0", "10000", "3"), "50000");
assert_eq!(
capacity(&venue, Side::Sell),
BorrowCapacity::new(base()).with_borrowable_now(d("0.4"))
);
assert_eq!(
capacity(&venue, Side::Buy),
BorrowCapacity::new(quote()).with_borrowable_now(d("20000"))
);
}
#[test]
fn the_capacity_is_what_an_order_arriving_now_is_lent() {
let mut venue = fed_venue(&margin_config("0", "10000", "3"), "50000");
let borrowable_now = capacity(&venue, Side::Sell).borrowable_now.unwrap();
let outcome = venue.open_order(sell_request("1", None));
assert!(
insufficient_reason(&outcome).ends_with(&format!("Borrowable: {borrowable_now}")),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn the_free_balance_is_not_added_to_the_capacity() {
let venue = fed_venue(&margin_config("0.25", "10000", "3"), "50000");
assert_eq!(capacity(&venue, Side::Sell).borrowable_now, Some(d("0.9")));
}
#[test]
fn a_cap_tighter_than_the_leverage_limit_bounds_the_capacity_and_is_its_limit() {
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3");
let venue = fed_venue(&config, "50000");
assert_eq!(
capacity(&venue, Side::Sell),
BorrowCapacity::new(base())
.with_borrowable_now(d("0.3"))
.with_account_limit(d("0.3"))
);
assert_eq!(
capacity(&venue, Side::Buy),
BorrowCapacity::new(quote()).with_borrowable_now(d("20000")),
"the cap is on BTC alone"
);
}
#[test]
fn a_cap_looser_than_the_leverage_limit_is_still_reported_as_the_limit() {
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "1");
let venue = fed_venue(&config, "50000");
assert_eq!(
capacity(&venue, Side::Sell),
BorrowCapacity::new(base())
.with_borrowable_now(d("0.4"))
.with_account_limit(d("1"))
);
}
#[test]
fn what_the_account_owes_principal_and_interest_counts_against_the_cap() {
let config = with_max_borrow(owing_btc_config("0.1", "0.01"), base(), "0.3");
let venue = fed_venue(&config, "50000");
assert_eq!(capacity(&venue, Side::Sell).borrowable_now, Some(d("0.19")));
}
#[test]
fn a_debt_past_the_cap_leaves_nothing_to_borrow() {
let config = with_max_borrow(owing_btc_config("0.1", "0.01"), base(), "0.05");
let venue = fed_venue(&config, "50000");
assert_eq!(
capacity(&venue, Side::Sell),
BorrowCapacity::new(base())
.with_borrowable_now(Decimal::ZERO)
.with_account_limit(d("0.05"))
);
}
#[test]
fn room_below_the_minimum_borrow_is_reported_as_none_to_borrow() {
let config = with_min_borrow(margin_config("0", "10000", "3"), base(), "0.5");
assert_eq!(
capacity(&fed_venue(&config, "50000"), Side::Sell).borrowable_now,
Some(Decimal::ZERO)
);
let config = with_min_borrow(margin_config("0", "10000", "3"), base(), "0.4");
assert_eq!(
capacity(&fed_venue(&config, "50000"), Side::Sell).borrowable_now,
Some(d("0.4"))
);
let config = with_min_borrow(
with_max_borrow(margin_config("0", "100000", "3"), base(), "0.3"),
base(),
"0.35",
);
assert_eq!(
capacity(&fed_venue(&config, "50000"), Side::Sell).borrowable_now,
Some(Decimal::ZERO)
);
let config = with_min_borrow(
with_max_borrow(margin_config("0", "100000", "3"), base(), "0.3"),
base(),
"0.3",
);
assert_eq!(
capacity(&fed_venue(&config, "50000"), Side::Sell).borrowable_now,
Some(d("0.3"))
);
}
#[test]
fn a_venue_without_markets_answers_only_what_it_can_value() {
let venue = margin_venue("0", "10000", "3");
assert_eq!(
capacity(&venue, Side::Sell),
BorrowCapacity::new(base()),
"BTC cannot be valued"
);
assert_eq!(capacity(&venue, Side::Buy).borrowable_now, Some(d("20000")));
let venue = margin_venue("1", "10000", "3");
assert_eq!(capacity(&venue, Side::Buy), BorrowCapacity::new(quote()));
let venue = SimulatedVenue::new(&owing_btc_config("1", "0"), spot_instruments());
assert_eq!(capacity(&venue, Side::Buy), BorrowCapacity::new(quote()));
}
#[test]
fn a_cap_that_rules_a_loan_out_is_known_without_a_market() {
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0");
let venue = SimulatedVenue::new(&config, spot_instruments());
assert_eq!(
capacity(&venue, Side::Sell),
BorrowCapacity::new(base())
.with_borrowable_now(Decimal::ZERO)
.with_account_limit(Decimal::ZERO)
);
let config = with_min_borrow(
with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3"),
base(),
"0.35",
);
let venue = SimulatedVenue::new(&config, spot_instruments());
assert_eq!(
capacity(&venue, Side::Sell).borrowable_now,
Some(Decimal::ZERO),
"what the cap leaves is below the minimum borrow"
);
let config = with_max_borrow(margin_config("1", "10000", "3"), base(), "0");
let venue = SimulatedVenue::new(&config, spot_instruments());
assert_eq!(
capacity(&venue, Side::Sell).borrowable_now,
Some(Decimal::ZERO)
);
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3");
let venue = SimulatedVenue::new(&config, spot_instruments());
assert_eq!(capacity(&venue, Side::Sell).borrowable_now, None);
}
#[test]
fn a_cash_venue_lends_against_no_instrument() {
let venue = make_venue("1", "10000");
let reason = invalid_reason(&venue, &instrument_name());
assert!(reason.contains("no spot margin configuration"), "{reason}");
}
#[test]
fn an_instrument_the_venue_does_not_trade_is_invalid() {
let venue = margin_venue("0", "10000", "3");
invalid_reason(&venue, &InstrumentNameExchange::new("ETHUSDT"));
}
#[test]
fn a_cash_settled_instrument_is_invalid_on_a_margin_venue() {
let config = MockExecutionConfig {
spot_margin: Some(SpotMarginConfig::new(usd(), d("3"), MarginEffect::Borrow).unwrap()),
..config_from_balances(vec![funded(usd(), d("10000"))], FeeModelConfig::default())
};
let venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
let reason = invalid_reason(&venue, &cfd_instrument_name());
assert!(reason.contains("cash-settled"), "{reason}");
}