use std::collections::HashMap;
use alloy::primitives::{I256, TxHash, U256};
use fastnum::{D256, dec64, dec256, udec64, udec128};
use crate::{
Chain,
abi::dex::Exchange::{
AccountCreated, ExchangeEvents, MaintenanceMarginFractionUpdated, PositionDecreased,
PositionOpened,
},
num::Converter,
state::{
ContractFeatures, Exchange, FeeSchedule, FeeScheduleKey, FeeScheduleRegistry, Perpetual,
},
stream::{RawBlockEvents, RawEvent},
types::StateInstant,
};
const PERP: u32 = 123456789;
const PERP_B: u32 = 987654321;
const LONG: u8 = 0;
const SHORT: u8 = 1;
fn si(block: u64) -> StateInstant { StateInstant::new(block, block) }
fn ev(event: ExchangeEvents, log_index: u64) -> RawEvent {
RawEvent::new(TxHash::ZERO, 0, log_index, event)
}
fn perp(id: u32) -> Perpetual {
let mut p = Perpetual::for_testing(id);
p.update_maintenance_margin(si(0), udec64!(20));
p
}
fn perp_funding(id: u32, payment: D256, event_block: u64) -> Perpetual {
let mut p = perp(id);
p.update_funding(si(0), dec64!(0.01), payment, event_block);
p
}
fn exchange(perps: HashMap<u32, Perpetual>) -> Exchange {
Exchange::new(
Chain::testnet(),
si(0),
ContractFeatures::current(),
Converter::new(4), 100,
udec128!(0.001),
udec128!(0.001),
udec128!(0.001),
FeeScheduleRegistry::new(
FeeSchedule::flat(FeeScheduleKey::Default, udec64!(0), udec64!(0)),
FeeSchedule::flat(FeeScheduleKey::RwaDefault, udec64!(0), udec64!(0)),
HashMap::new(),
),
perps,
HashMap::new(),
false, true, )
}
fn account_created(id: u32) -> ExchangeEvents {
ExchangeEvents::AccountCreated(AccountCreated {
account: Default::default(),
id: U256::from(id),
})
}
fn position_opened(
perp_id: u32,
account: u32,
kind: u8,
price: u64,
lot: u64,
deposit: u64,
) -> ExchangeEvents {
ExchangeEvents::PositionOpened(PositionOpened {
perpId: U256::from(perp_id),
accountId: U256::from(account),
positionType: kind,
leverageHdths: U256::ZERO,
depositCNS: U256::from(deposit),
pnlCollateralizedCNS: I256::ZERO,
pricePNS: U256::from(price),
lotLNS: U256::from(lot),
insFeeCNS: U256::ZERO,
protFeeCNS: U256::ZERO,
})
}
fn position_decreased(
perp_id: u32,
account: u32,
kind: u8,
end_lot: u64,
funding_cns: i64,
) -> ExchangeEvents {
ExchangeEvents::PositionDecreased(PositionDecreased {
perpId: U256::from(perp_id),
accountId: U256::from(account),
positionType: kind,
startDepositCNS: U256::ZERO,
endDepositCNS: U256::from(deposit_for(end_lot)),
startLotLNS: U256::from(10u64),
endLotLNS: U256::from(end_lot),
deltaPnlCNS: I256::ZERO,
fundingCNS: I256::try_from(funding_cns).unwrap(),
})
}
fn maintenance_margin(perp_id: u32, hdths: u64) -> ExchangeEvents {
ExchangeEvents::MaintenanceMarginFractionUpdated(MaintenanceMarginFractionUpdated {
perpId: U256::from(perp_id),
maintMarginFracHdths: U256::from(hdths),
})
}
fn deposit_for(lot: u64) -> u64 { lot * 100_000 }
fn premium(exchange: &Exchange, account: u32, perp_id: u32) -> D256 {
exchange
.accounts()
.get(&account)
.expect("account tracked")
.positions()
.get(&perp_id)
.expect("position exists")
.premium_pnl()
}
#[test]
fn exchange_funding_before_same_block_decrease_both_sides() {
let perps = HashMap::from([(PERP, perp_funding(PERP, dec256!(1), 2))]);
let mut exchange = exchange(perps);
exchange
.apply_events(&RawBlockEvents::new(
si(1),
vec![
ev(account_created(1), 0),
ev(account_created(2), 1),
ev(account_created(3), 2),
ev(position_opened(PERP, 1, LONG, 100, 10, deposit_for(10)), 3),
ev(position_opened(PERP, 2, SHORT, 100, 10, deposit_for(10)), 4),
ev(position_opened(PERP, 3, LONG, 100, 10, deposit_for(10)), 5),
],
))
.expect("block 1");
exchange
.apply_events(&RawBlockEvents::new(
si(2),
vec![
ev(position_decreased(PERP, 1, LONG, 4, 0), 0),
ev(position_decreased(PERP, 2, SHORT, 4, 0), 1),
],
))
.expect("block 2");
assert_eq!(premium(&exchange, 1, PERP), dec256!(-10), "long: funding on pre-decrease size");
assert_eq!(premium(&exchange, 2, PERP), dec256!(10), "short: funding on pre-decrease size");
assert_eq!(premium(&exchange, 3, PERP), dec256!(-10), "untouched long still funded");
}
#[test]
fn exchange_position_opened_in_funding_block_gets_no_funding() {
let perps = HashMap::from([(PERP, perp_funding(PERP, dec256!(1), 2))]);
let mut exchange = exchange(perps);
exchange
.apply_events(&RawBlockEvents::new(
si(1),
vec![
ev(account_created(1), 0),
ev(position_opened(PERP, 1, LONG, 100, 10, deposit_for(10)), 1),
],
))
.expect("block 1");
exchange
.apply_events(&RawBlockEvents::new(
si(2),
vec![
ev(account_created(2), 0),
ev(position_opened(PERP, 2, LONG, 100, 10, deposit_for(10)), 1),
],
))
.expect("block 2");
assert_eq!(premium(&exchange, 1, PERP), dec256!(-10), "pre-existing long funded");
assert_eq!(premium(&exchange, 2, PERP), dec256!(0), "opened-in-block long unfunded");
}
#[test]
fn exchange_multi_perp_funding_fanout_isolation() {
let perps = HashMap::from([(PERP, perp_funding(PERP, dec256!(1), 2)), (PERP_B, perp(PERP_B))]);
let mut exchange = exchange(perps);
exchange
.apply_events(&RawBlockEvents::new(
si(1),
vec![
ev(account_created(1), 0),
ev(position_opened(PERP, 1, LONG, 100, 10, deposit_for(10)), 1),
ev(position_opened(PERP_B, 1, LONG, 100, 10, deposit_for(10)), 2),
],
))
.expect("block 1");
exchange
.apply_events(&RawBlockEvents::new(si(2), vec![]))
.expect("block 2");
assert_eq!(premium(&exchange, 1, PERP), dec256!(-10), "PERP position funded");
assert_eq!(premium(&exchange, 1, PERP_B), dec256!(0), "PERP_B position untouched");
}
#[test]
fn exchange_pass1_funding_vs_pass3_mmf_ordering() {
let perps = HashMap::from([(PERP, perp_funding(PERP, dec256!(1), 2))]);
let mut exchange = exchange(perps);
exchange
.apply_events(&RawBlockEvents::new(
si(1),
vec![
ev(account_created(1), 0),
ev(position_opened(PERP, 1, LONG, 100, 10, deposit_for(10)), 1),
],
))
.expect("block 1");
assert_eq!(
exchange
.accounts()
.get(&1)
.unwrap()
.positions()
.get(&PERP)
.unwrap()
.liquidation_price(),
udec64!(95), );
exchange
.apply_events(&RawBlockEvents::new(si(2), vec![ev(maintenance_margin(PERP, 1000), 0)]))
.expect("block 2");
let pos = exchange
.accounts()
.get(&1)
.unwrap()
.positions()
.get(&PERP)
.unwrap()
.clone();
assert_eq!(pos.premium_pnl(), dec256!(-10), "funding applied in Pass 1");
assert_eq!(pos.maintenance_margin_requirement(), udec128!(100), "MMF fanned out in Pass 3");
assert_eq!(pos.liquidation_price(), udec64!(101), "liq composes funding + MMF");
}