use std::num::NonZeroU16;
use const_decimal::Decimal;
use crate::{
DECIMALS,
EXPECT_CONFIG,
EXPECT_CONTRACT_SPEC,
EXPECT_DECIMAL,
EXPECT_NON_ZERO,
EXPECT_QUANTITY_FILTER,
mock_exchange_linear,
prelude::*,
test_fee_maker,
test_fee_taker,
utils::NoUserOrderId,
};
fn setup_long_with_resting_ask()
-> Exchange<i64, DECIMALS, BaseCurrency<i64, DECIMALS>, NoUserOrderId> {
let mut exchange = mock_exchange_linear();
assert!(
exchange
.update_state(&Bba {
bid: QuoteCurrency::new(100, 0),
ask: QuoteCurrency::new(101, 0),
timestamp_exchange_ns: 0.into()
})
.unwrap()
.is_empty()
);
exchange
.submit_market_order(MarketOrder::new(Side::Buy, BaseCurrency::new(97, 1)).unwrap())
.unwrap();
exchange
.submit_limit_order(
LimitOrder::new(
Side::Sell,
QuoteCurrency::new(103, 0),
BaseCurrency::new(97, 1),
)
.unwrap(),
)
.unwrap();
exchange
}
#[test]
fn reducing_market_order_triggers_margin_call_cancel() {
let mut exchange = setup_long_with_resting_ask();
let settlement = exchange
.submit_market_order(MarketOrder::new(Side::Sell, BaseCurrency::new(4, 0)).unwrap())
.unwrap();
assert_eq!(settlement.solvency, Solvency::Solvent);
assert_eq!(settlement.forced_cancels.len(), 1);
assert_eq!(
settlement.forced_cancels[0].limit_price(),
QuoteCurrency::new(103, 0)
);
assert!(exchange.account().active_limit_orders().is_empty());
assert_eq!(
exchange.account().balances().equity(),
QuoteCurrency::new(99_517_218, 5)
);
assert_eq!(
exchange.account().available_balance(),
QuoteCurrency::new(41_947_218, 5)
);
exchange
.submit_limit_order(
LimitOrder::new(
Side::Sell,
QuoteCurrency::new(104, 0),
BaseCurrency::new(57, 1),
)
.unwrap(),
)
.unwrap();
}
#[test]
fn liquidation_force_cancels_resting_orders() {
let mut exchange = setup_long_with_resting_ask();
let result = exchange.update_state(&Bba {
bid: QuoteCurrency::new(50, 0),
ask: QuoteCurrency::new(51, 0),
timestamp_exchange_ns: 1.into(),
});
assert!(matches!(result, Err(RiskError::Liquidate)));
assert_eq!(exchange.limit_order_events().len(), 1);
assert!(matches!(
exchange.limit_order_events()[0],
LimitOrderEvent::ForcedCancel(_)
));
assert!(exchange.account().position().quantity().is_zero());
assert!(exchange.account().active_limit_orders().is_empty());
assert_eq!(
exchange.account().balances().equity(),
QuoteCurrency::new(50_442_118, 5)
);
assert!(exchange.account().balances().bad_debt().is_zero());
assert_eq!(
exchange.account().available_balance(),
QuoteCurrency::new(50_442_118, 5)
);
}
#[test]
fn reducing_short_market_order_triggers_margin_call_cancel() {
let mut exchange = mock_exchange_linear();
assert!(
exchange
.update_state(&Bba {
bid: QuoteCurrency::new(100, 0),
ask: QuoteCurrency::new(101, 0),
timestamp_exchange_ns: 0.into()
})
.unwrap()
.is_empty()
);
exchange
.submit_market_order(MarketOrder::new(Side::Sell, BaseCurrency::new(97, 1)).unwrap())
.unwrap();
exchange
.submit_limit_order(
LimitOrder::new(
Side::Buy,
QuoteCurrency::new(100, 0),
BaseCurrency::new(97, 1),
)
.unwrap(),
)
.unwrap();
assert!(
exchange
.update_state(&Bba {
bid: QuoteCurrency::new(110, 0),
ask: QuoteCurrency::new(111, 0),
timestamp_exchange_ns: 1.into()
})
.unwrap()
.is_empty()
);
let settlement = exchange
.submit_market_order(MarketOrder::new(Side::Buy, BaseCurrency::new(4, 0)).unwrap())
.unwrap();
assert_eq!(settlement.solvency, Solvency::Solvent);
assert_eq!(settlement.forced_cancels.len(), 1);
assert!(exchange.account().active_limit_orders().is_empty());
assert_eq!(
exchange.account().balances().equity(),
QuoteCurrency::new(95_515_160, 5)
);
assert_eq!(
exchange.account().available_balance(),
QuoteCurrency::new(38_515_160, 5)
);
}
#[test]
fn margin_call_cancels_largest_order_first() {
let mut exchange = mock_exchange_linear();
assert!(
exchange
.update_state(&Bba {
bid: QuoteCurrency::new(100, 0),
ask: QuoteCurrency::new(101, 0),
timestamp_exchange_ns: 0.into()
})
.unwrap()
.is_empty()
);
exchange
.submit_market_order(MarketOrder::new(Side::Buy, BaseCurrency::new(97, 1)).unwrap())
.unwrap();
exchange
.submit_limit_order(
LimitOrder::new(
Side::Sell,
QuoteCurrency::new(103, 0),
BaseCurrency::new(5, 0),
)
.unwrap(),
)
.unwrap();
exchange
.submit_limit_order(
LimitOrder::new(
Side::Sell,
QuoteCurrency::new(103, 0),
BaseCurrency::new(47, 1),
)
.unwrap(),
)
.unwrap();
let settlement = exchange
.submit_market_order(MarketOrder::new(Side::Sell, BaseCurrency::new(4, 0)).unwrap())
.unwrap();
assert_eq!(settlement.solvency, Solvency::Solvent);
assert_eq!(settlement.forced_cancels.len(), 1);
assert_eq!(
settlement.forced_cancels[0].remaining_quantity(),
BaseCurrency::new(5, 0)
);
let surviving: Vec<_> = exchange.account().active_limit_orders().iter().collect();
assert_eq!(surviving.len(), 1);
assert_eq!(surviving[0].remaining_quantity(), BaseCurrency::new(47, 1));
assert_eq!(
exchange.account().available_balance(),
QuoteCurrency::new(41_937_536, 5)
);
}
#[test]
fn gapped_liquidation_records_bad_debt_without_panicking() {
let contract_spec = ContractSpecification::new(
leverage!(5),
Decimal::try_from_scaled(5, 1).expect(EXPECT_DECIMAL),
PriceFilter::default(),
QuantityFilter::new(None, None, BaseCurrency::new(1, 2)).expect(EXPECT_QUANTITY_FILTER),
test_fee_maker(),
test_fee_taker(),
)
.expect(EXPECT_CONTRACT_SPEC);
let config = Config::new(
QuoteCurrency::new(1000, 0),
NonZeroU16::new(10).expect(EXPECT_NON_ZERO),
contract_spec,
OrderRateLimits::default(),
)
.expect(EXPECT_CONFIG);
let mut exchange =
Exchange::<i64, DECIMALS, BaseCurrency<i64, DECIMALS>, NoUserOrderId>::new(config);
assert!(
exchange
.update_state(&Bba {
bid: QuoteCurrency::new(100, 0),
ask: QuoteCurrency::new(101, 0),
timestamp_exchange_ns: 0.into()
})
.unwrap()
.is_empty()
);
exchange
.submit_market_order(MarketOrder::new(Side::Buy, BaseCurrency::new(40, 0)).unwrap())
.unwrap();
let result = exchange.update_state(&Bba {
bid: QuoteCurrency::new(70, 0),
ask: QuoteCurrency::new(71, 0),
timestamp_exchange_ns: 1.into(),
});
assert!(matches!(result, Err(RiskError::Liquidate)));
assert!(exchange.account().position().quantity().is_zero());
assert!(exchange.account().balances().equity().is_zero());
assert_eq!(
exchange.account().balances().bad_debt(),
QuoteCurrency::new(24_410_400, 5)
);
assert!(exchange.account().available_balance().is_zero());
}