use alloy::primitives::{B256, I256, U256};
use fastnum::{D64, D256, UD64, UD128};
use super::*;
use crate::{abi::dex::Exchange::PerpetualInfoV2, types};
const FUNDING_RATE_SCALE: u8 = 5;
const LEVERAGE_SCALE: u8 = 2;
fn next_funding_event_block(block: u64, funding_interval: u64) -> u64 {
if funding_interval == 0 {
return block;
}
block - (block % funding_interval) + funding_interval
}
#[derive(Clone, derive_more::Debug)]
pub struct Perpetual {
instant: types::StateInstant,
state_instant: types::StateInstant,
id: types::PerpetualId,
name: String,
symbol: String,
is_paused: bool,
price_converter: num::Converter,
size_converter: num::Converter,
leverage_converter: num::Converter,
fee_converter: num::Converter,
funding_rate_converter: num::Converter,
funding_sum_converter: num::Converter,
#[debug("{base_price}")]
base_price: UD64,
fee_schedule: FeeSchedule,
#[debug("{initial_margin}")]
initial_margin: UD64, #[debug("{maintenance_margin}")]
maintenance_margin: UD64,
#[debug("{last_price}")]
last_price: UD64, last_price_block: Option<u64>,
last_price_timestamp: u64,
#[debug("{mark_price}")]
mark_price: UD64, mark_price_block: Option<u64>,
mark_price_timestamp: u64,
#[debug("{oracle_price}")]
oracle_price: UD64, oracle_price_block: Option<u64>,
oracle_price_timestamp: u64,
#[debug("{prev_funding_rate}")]
prev_funding_rate: D64, #[debug("{:?}", next_funding_rate.map(|v| format!("{v}")))]
next_funding_rate: Option<D64>, #[debug("{:?}", next_funding_payment.map(|v| format!("{v}")))]
next_funding_payment: Option<D256>, next_funding_event_block: Option<u64>,
funding_start_block: u64,
oracle_feed_id: B256,
is_oracle_used: bool,
price_max_age_sec: u64,
l3_book: OrderBook,
#[debug("{open_interest}")]
open_interest: UD128,
}
impl Perpetual {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
instant: types::StateInstant,
id: types::PerpetualId,
info: &PerpetualInfoV2,
fee_schedule: FeeSchedule,
initial_margin: U256,
maintenance_margin: U256,
) -> Self {
let price_converter = num::Converter::new(info.priceDecimals.to());
let size_converter = num::Converter::new(info.lotDecimals.to());
let leverage_converter = num::Converter::new(LEVERAGE_SCALE);
let fee_converter = num::fee_converter();
let funding_rate_converter = num::Converter::new(FUNDING_RATE_SCALE);
let funding_sum_converter = num::Converter::new(
info.fundingSumScalingExp.to::<u8>() + info.priceDecimals.to::<u8>(),
);
Self {
instant,
state_instant: instant,
id,
name: info.name.clone(),
symbol: info.symbol.clone(),
is_paused: info.status == 0,
price_converter,
size_converter,
leverage_converter,
fee_converter,
funding_rate_converter,
funding_sum_converter,
base_price: price_converter.from_unsigned(info.basePricePNS),
fee_schedule,
initial_margin: leverage_converter.from_unsigned(initial_margin),
maintenance_margin: leverage_converter.from_unsigned(maintenance_margin),
last_price: price_converter.from_unsigned(info.lastPNS),
last_price_block: None,
last_price_timestamp: info.lastTimestamp.to(),
mark_price: price_converter.from_unsigned(info.markPNS),
mark_price_block: None,
mark_price_timestamp: info.markTimestamp.to(),
oracle_price: price_converter.from_unsigned(info.oraclePNS),
oracle_price_block: None,
oracle_price_timestamp: info.oracleTimestampSec.to(),
prev_funding_rate: funding_rate_converter
.from_signed(I256::try_from(info.fundingRatePct100k).unwrap()),
next_funding_rate: None,
next_funding_payment: None,
next_funding_event_block: None,
funding_start_block: info.fundingStartBlock.to(),
oracle_feed_id: info.linkFeedId,
is_oracle_used: !info.ignOracle,
price_max_age_sec: info.refPriceMaxAgeSec.to(),
l3_book: OrderBook::new(),
open_interest: size_converter.from_unsigned(info.longOpenInterestLNS),
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn added(
instant: types::StateInstant,
id: types::PerpetualId,
name: String,
symbol: String,
is_paused: bool,
price_decimals: u8,
size_decimals: u8,
base_price: U256,
fee_schedule: FeeSchedule,
initial_margin: U256,
maintenance_margin: U256,
) -> Self {
let price_converter = num::Converter::new(price_decimals);
let size_converter = num::Converter::new(size_decimals);
let leverage_converter = num::Converter::new(LEVERAGE_SCALE);
let fee_converter = num::fee_converter();
let funding_rate_converter = num::Converter::new(FUNDING_RATE_SCALE);
let funding_sum_converter = num::Converter::new(price_decimals);
Self {
instant,
state_instant: instant,
id,
name,
symbol,
is_paused,
price_converter,
size_converter,
leverage_converter,
fee_converter,
funding_rate_converter,
funding_sum_converter,
base_price: price_converter.from_unsigned(base_price),
fee_schedule,
initial_margin: leverage_converter.from_unsigned(initial_margin),
maintenance_margin: leverage_converter.from_unsigned(maintenance_margin),
last_price: UD64::ZERO,
last_price_block: None,
last_price_timestamp: 0,
mark_price: UD64::ZERO,
mark_price_block: None,
mark_price_timestamp: 0,
oracle_price: UD64::ZERO,
oracle_price_block: None,
oracle_price_timestamp: 0,
prev_funding_rate: D64::ZERO,
next_funding_rate: None,
next_funding_payment: None,
next_funding_event_block: None,
funding_start_block: 0,
oracle_feed_id: B256::ZERO,
is_oracle_used: true,
price_max_age_sec: 60,
l3_book: OrderBook::new(),
open_interest: UD128::ZERO,
}
}
pub fn instant(&self) -> types::StateInstant { self.instant }
pub fn id(&self) -> types::PerpetualId { self.id }
pub fn name(&self) -> String { self.name.clone() }
pub fn symbol(&self) -> String { self.symbol.clone() }
pub fn is_paused(&self) -> bool { self.is_paused }
pub fn price_converter(&self) -> num::Converter { self.price_converter }
pub fn size_converter(&self) -> num::Converter { self.size_converter }
pub fn leverage_converter(&self) -> num::Converter { self.leverage_converter }
pub fn fee_converter(&self) -> num::Converter { self.fee_converter }
pub fn funding_rate_converter(&self) -> num::Converter { self.funding_rate_converter }
pub fn funding_sum_converter(&self) -> num::Converter { self.funding_sum_converter }
pub fn fee_schedule(&self) -> FeeSchedule { self.fee_schedule }
pub fn maker_fee(&self) -> UD64 { self.fee_schedule.base_maker_fee() }
pub fn taker_fee(&self) -> UD64 { self.fee_schedule.base_taker_fee() }
pub fn maker_fee_for_tier(&self, tier: types::FeeTier) -> UD64 {
self.fee_schedule.maker_fee(tier)
}
pub fn taker_fee_for_tier(&self, tier: types::FeeTier) -> UD64 {
self.fee_schedule.taker_fee(tier)
}
pub fn initial_margin(&self) -> UD64 { self.initial_margin }
pub fn maintenance_margin(&self) -> UD64 { self.maintenance_margin }
pub fn last_price(&self) -> UD64 { self.last_price }
pub fn last_price_instant(&self) -> types::StateInstant {
types::StateInstant::new(
self.last_price_block.unwrap_or_default(),
self.last_price_timestamp,
)
}
pub fn last_price_timestamp(&self) -> u64 { self.last_price_timestamp }
pub fn mark_price(&self) -> UD64 { self.mark_price }
pub fn mark_price_instant(&self) -> types::StateInstant {
types::StateInstant::new(
self.mark_price_block.unwrap_or_default(),
self.mark_price_timestamp,
)
}
pub fn mark_price_timestamp(&self) -> u64 { self.mark_price_timestamp }
pub fn is_mark_price_obsolete(&self) -> bool {
self.mark_price_timestamp + self.price_max_age_sec <= self.instant.block_timestamp()
}
pub fn oracle_price(&self) -> UD64 { self.oracle_price }
pub fn oracle_price_instant(&self) -> types::StateInstant {
types::StateInstant::new(
self.oracle_price_block.unwrap_or_default(),
self.oracle_price_timestamp,
)
}
pub fn oracle_price_timestamp(&self) -> u64 { self.oracle_price_timestamp }
pub fn is_oracle_price_obsolete(&self) -> bool {
self.oracle_price_timestamp + self.price_max_age_sec <= self.instant.block_timestamp()
}
pub fn funding_rate(&self) -> D64 {
if let Some((next, bl)) = self.next_funding_rate.zip(self.next_funding_event_block)
&& bl <= self.state_instant.block_number()
{
next
} else {
self.prev_funding_rate
}
}
pub fn has_next_funding_rate(&self) -> bool {
self.next_funding_rate.is_some()
&& self
.next_funding_event_block
.is_some_and(|bl| bl > self.state_instant.block_number())
}
pub fn next_funding_rate(&self) -> Option<D64> {
if self.has_next_funding_rate() { self.next_funding_rate } else { None }
}
pub fn funding_start_block(&self) -> u64 { self.funding_start_block }
pub fn next_funding_event_block(&self) -> Option<u64> { self.next_funding_event_block }
pub fn oracle_feed_id(&self) -> B256 { self.oracle_feed_id }
pub fn is_oracle_used(&self) -> bool { self.is_oracle_used }
pub fn price_max_age_sec(&self) -> u64 { self.price_max_age_sec }
pub fn get_order(&self, order_id: types::OrderId) -> Option<&Order> {
self.l3_book.get_order(order_id).map(|o| &*(*o))
}
pub fn total_orders(&self) -> usize { self.l3_book.total_orders() }
pub fn l3_book(&self) -> &OrderBook { &self.l3_book }
pub fn open_interest(&self) -> UD128 { self.open_interest }
pub fn open_interest_amount(&self) -> UD128 { self.open_interest * self.last_price.resize() }
pub(crate) fn base_price(&self) -> UD64 { self.base_price }
pub(crate) fn update_state_instant(&mut self, instant: types::StateInstant) {
self.state_instant = instant;
self.l3_book.check_expired(instant);
}
pub(crate) fn take_funding_payment(
&mut self,
instant: types::StateInstant,
) -> Option<(D64, D256)> {
if self
.next_funding_event_block
.is_some_and(|fe| fe == instant.block_number())
{
let rate = self.next_funding_rate.unwrap_or(self.prev_funding_rate);
self.next_funding_payment
.take()
.map(|payment| (rate, payment))
} else {
None
}
}
pub(crate) fn add_order(&mut self, order: Order) -> Result<(), DexError> {
self.l3_book
.add_order(&order)
.map_err(|err| DexError::OrderBook(self.id, err))?;
Ok(())
}
pub(crate) fn add_orders_from_snapshot(&mut self, orders: Vec<Order>) -> Result<(), DexError> {
self.l3_book
.add_orders_from_snapshot(&orders)
.map_err(|err| DexError::OrderBook(self.id, err))?;
Ok(())
}
pub(crate) fn update_order(&mut self, order: Order) -> Result<(), DexError> {
let prev = self
.l3_book
.get_order(order.order_id())
.cloned()
.ok_or(DexError::OrderNotFound(self.id, order.order_id()))?;
if prev.price() != order.price() {
self.l3_book
.remove_order(&prev)
.map_err(|err| DexError::OrderBook(self.id, err))?;
self.l3_book
.add_order(&order)
.map_err(|err| DexError::OrderBook(self.id, err))?;
} else if order.size() > prev.size() {
self.l3_book
.move_to_back(&order, &prev)
.map_err(|err| DexError::OrderBook(self.id, err))?;
} else if prev.expiry_block() > 0
&& prev.expiry_block() < order.instant().block_number()
&& prev.expiry_block() != order.expiry_block()
{
self.l3_book
.move_to_back(&order, &prev)
.map_err(|err| DexError::OrderBook(self.id, err))?;
} else {
self.l3_book
.update_order(&order, &prev)
.map_err(|err| DexError::OrderBook(self.id, err))?;
}
Ok(())
}
pub(crate) fn remove_order(&mut self, order_id: types::OrderId) -> Result<Order, DexError> {
let order = self
.l3_book
.get_order(order_id)
.cloned()
.ok_or(DexError::OrderNotFound(self.id, order_id))?;
self.l3_book
.remove_order(&order)
.map_err(|err| DexError::OrderBook(self.id, err))
}
pub(crate) fn update_paused(
&mut self,
instant: types::StateInstant,
paused: bool,
funding_interval: u64,
) {
self.is_paused = paused;
self.instant = instant;
if !paused && self.funding_start_block == 0 {
self.funding_start_block =
next_funding_event_block(instant.block_number(), funding_interval)
}
}
pub(crate) fn update_fee_schedule(
&mut self,
instant: types::StateInstant,
fee_schedule: FeeSchedule,
) {
self.fee_schedule = fee_schedule;
self.instant = instant;
}
pub(crate) fn update_fee_schedule_key(
&mut self,
instant: types::StateInstant,
key: FeeScheduleKey,
) {
self.fee_schedule = self.fee_schedule.with_key(key);
self.instant = instant;
}
pub(crate) fn update_base_maker_fee(&mut self, instant: types::StateInstant, maker_fee: UD64) {
self.fee_schedule = self.fee_schedule.with_base_maker_fee(maker_fee);
self.instant = instant;
}
pub(crate) fn update_base_taker_fee(&mut self, instant: types::StateInstant, taker_fee: UD64) {
self.fee_schedule = self.fee_schedule.with_base_taker_fee(taker_fee);
self.instant = instant;
}
pub(crate) fn update_initial_margin(
&mut self,
instant: types::StateInstant,
initial_margin: UD64,
) {
self.initial_margin = initial_margin;
self.instant = instant;
}
pub(crate) fn update_maintenance_margin(
&mut self,
instant: types::StateInstant,
maintenance_margin: UD64,
) {
self.maintenance_margin = maintenance_margin;
self.instant = instant;
}
pub(crate) fn update_last_price(&mut self, instant: types::StateInstant, last_price: UD64) {
self.last_price = last_price;
self.last_price_block = Some(instant.block_number());
self.last_price_timestamp = instant.block_timestamp();
self.instant = instant;
}
pub(crate) fn update_mark_price(&mut self, instant: types::StateInstant, mark_price: UD64) {
self.mark_price = mark_price;
self.mark_price_block = Some(instant.block_number());
self.mark_price_timestamp = instant.block_timestamp();
self.instant = instant;
}
pub(crate) fn update_oracle_price(&mut self, instant: types::StateInstant, oracle_price: UD64) {
self.oracle_price = oracle_price;
self.oracle_price_block = Some(instant.block_number());
self.oracle_price_timestamp = instant.block_timestamp();
self.instant = instant;
}
pub(crate) fn update_funding(
&mut self,
instant: types::StateInstant,
funding_rate: D64,
funding_payment: D256,
block_num: u64,
) {
if let Some(next) = self.next_funding_rate
&& self
.next_funding_event_block
.expect("next_funding_event_block set")
< block_num
{
self.prev_funding_rate = next;
}
self.next_funding_rate = Some(funding_rate);
self.next_funding_payment = Some(funding_payment);
self.next_funding_event_block = Some(block_num);
self.instant = instant;
}
pub(crate) fn update_funding_sum_scaling_exp(&mut self, instant: types::StateInstant, exp: u8) {
self.funding_sum_converter = num::Converter::new(exp + self.price_converter.decimals());
self.instant = instant;
}
pub(crate) fn update_oracle_feed_id(
&mut self,
instant: types::StateInstant,
oracle_feed_id: B256,
) {
self.oracle_feed_id = oracle_feed_id;
self.instant = instant;
}
pub(crate) fn update_is_oracle_used(
&mut self,
instant: types::StateInstant,
is_oracle_used: bool,
) {
self.is_oracle_used = is_oracle_used;
self.instant = instant;
}
pub(crate) fn update_price_max_age_sec(
&mut self,
instant: types::StateInstant,
price_max_age_sec: u64,
) {
self.price_max_age_sec = price_max_age_sec;
self.instant = instant;
}
pub(crate) fn update_open_interest(
&mut self,
instant: types::StateInstant,
prev_size: UD64,
new_size: UD64,
) {
self.open_interest -= prev_size.resize();
self.open_interest += new_size.resize();
self.instant = instant;
}
#[cfg(test)]
pub(crate) fn for_testing(id: types::PerpetualId) -> Self { Self::testing(id) }
#[cfg(any(test, feature = "test-utils"))]
fn testing(id: types::PerpetualId) -> Self {
Self {
instant: types::StateInstant::new(0, 0),
state_instant: types::StateInstant::new(0, 0),
id,
name: "TEST".to_string(),
symbol: "TEST".to_string(),
is_paused: false,
price_converter: num::Converter::new(0),
size_converter: num::Converter::new(0),
leverage_converter: num::Converter::new(2),
fee_converter: num::fee_converter(),
funding_rate_converter: num::Converter::new(5),
funding_sum_converter: num::Converter::new(0),
base_price: UD64::ZERO,
fee_schedule: FeeSchedule::flat(FeeScheduleKey::Default, UD64::ZERO, UD64::ZERO),
initial_margin: UD64::ZERO,
maintenance_margin: UD64::ZERO,
last_price: UD64::ZERO,
last_price_block: None,
last_price_timestamp: 0,
mark_price: UD64::ZERO,
mark_price_block: None,
mark_price_timestamp: 0,
oracle_price: UD64::ZERO,
oracle_price_block: None,
oracle_price_timestamp: 0,
prev_funding_rate: D64::ZERO,
next_funding_rate: None,
next_funding_payment: None,
next_funding_event_block: None,
funding_start_block: 0,
oracle_feed_id: B256::ZERO,
is_oracle_used: false,
price_max_age_sec: 0,
l3_book: OrderBook::new(),
open_interest: UD128::ZERO,
}
}
}
#[cfg(any(test, feature = "test-utils"))]
impl Perpetual {
pub fn for_test(id: types::PerpetualId) -> Self { Self::testing(id) }
pub fn with_last_price(mut self, price: UD64) -> Self {
self.last_price = price;
self
}
pub fn with_last_price_timestamp(mut self, timestamp: u64) -> Self {
self.last_price_timestamp = timestamp;
self
}
pub fn with_bid(mut self, price: UD64, size: UD64) -> Self {
use std::num::NonZeroU16;
let order_id =
NonZeroU16::new((self.l3_book.total_orders() + 1) as u16).expect("order id overflow");
let order = Order::for_l3_testing(types::OrderType::OpenLong, price, size, 0, order_id, 0);
self.l3_book
.add_order(&order)
.expect("failed to add bid order");
self
}
pub fn with_ask(mut self, price: UD64, size: UD64) -> Self {
use std::num::NonZeroU16;
let order_id =
NonZeroU16::new((self.l3_book.total_orders() + 1) as u16).expect("order id overflow");
let order = Order::for_l3_testing(types::OrderType::OpenShort, price, size, 0, order_id, 0);
self.l3_book
.add_order(&order)
.expect("failed to add ask order");
self
}
}
#[cfg(feature = "display")]
impl std::fmt::Display for Perpetual {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use colored::Colorize;
use tabled::{
Table,
settings::{Alignment, Style, object::Cell},
};
let mut table = Table::from_iter(vec![
vec![
format!(
"Last: {}\n{}",
self.last_price.to_string().green(),
self.last_price_instant()
),
format!(
"Mark: {}\n{}",
if self.is_mark_price_obsolete() {
self.mark_price.to_string().red()
} else {
self.mark_price.to_string().green()
},
self.mark_price_instant(),
),
format!(
"Oracle: {}\n{}",
if self.is_oracle_used {
if self.is_oracle_price_obsolete() {
self.oracle_price.to_string().red()
} else {
self.oracle_price.to_string().green()
}
} else {
"N/A".red()
},
if self.is_oracle_used {
self.oracle_price_instant().to_string()
} else {
"".to_string()
},
),
format!(
"Funding Rate: {}\nnext: {}",
if self.funding_rate().is_negative() {
self.funding_rate().to_string().red()
} else {
self.funding_rate().to_string().green()
},
if let Some((nfr, nfb)) = self
.next_funding_rate()
.zip(self.next_funding_event_block())
{
if nfr.is_negative() {
format!("{} @ #{}", nfr, nfb).red()
} else {
format!("{} @ #{}", nfr, nfb).green()
}
} else {
"TBA".to_string().dimmed()
},
),
format!(
"Open Interest: {}\namount: ${}",
self.open_interest.to_string().cyan(),
self.open_interest_amount()
.trunc_with_scale(2)
.to_string()
.cyan(),
),
],
vec![
format!(
"Fees: {} / {} (tkr/mkr, {})",
self.fee_schedule.base_taker_fee(),
self.fee_schedule.base_maker_fee(),
self.fee_schedule.key(),
),
format!("Margin: {} / {} (ini/mnt)", self.initial_margin, self.maintenance_margin),
format!("Price Max Age: {}", self.price_max_age_sec),
format!("Funding Start: {}", self.funding_start_block),
],
]);
table.with(Style::modern());
table.modify(Cell::new(0, 4), Alignment::right());
writeln!(
f,
"{}\n{}",
format_args!(
"{} {}",
format!("* Perp #{} {}", self.id, self.symbol).bold().cyan(),
if self.is_paused { "PAUSED ".bright_red() } else { Default::default() },
),
table,
)?;
if f.alternate() && self.l3_book().total_orders() > 0 {
writeln!(f, "{:}", self.l3_book)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU16;
use fastnum::{dec64, dec256, udec64};
use super::*;
fn oid(n: u16) -> types::OrderId { NonZeroU16::new(n).expect("test order id must be non-zero") }
const INTERVAL: u64 = 8571;
fn at(block: u64) -> types::StateInstant { types::StateInstant::new(block, 1_700_000_000) }
#[test]
fn next_funding_event_block_rounds_up_to_the_grid() {
assert_eq!(next_funding_event_block(1, INTERVAL), INTERVAL);
assert_eq!(next_funding_event_block(INTERVAL - 1, INTERVAL), INTERVAL);
assert_eq!(next_funding_event_block(INTERVAL, INTERVAL), 2 * INTERVAL);
assert_eq!(next_funding_event_block(0, INTERVAL), INTERVAL);
for block in [1u64, 5_000, 8_570, 8_571, 97_627_404, 99_360_126] {
let next = next_funding_event_block(block, INTERVAL);
assert_eq!(next % INTERVAL, 0, "block {block} produced off-grid {next}");
assert!(next > block, "block {block} produced non-advancing {next}");
}
}
#[test]
fn next_funding_event_block_tolerates_a_zero_interval() {
assert_eq!(next_funding_event_block(1_234, 0), 1_234);
}
#[test]
fn first_unpause_anchors_funding_to_the_grid_not_the_observed_block() {
let mut perp = Perpetual::for_testing(1);
assert_eq!(perp.funding_start_block(), 0);
perp.update_paused(at(97_627_404), false, INTERVAL);
assert_eq!(perp.funding_start_block(), 97_632_261);
assert_eq!(perp.funding_start_block() % INTERVAL, 0);
assert!(!perp.is_paused());
}
#[test]
fn unpause_on_a_grid_block_anchors_to_the_following_interval() {
let mut perp = Perpetual::for_testing(1);
perp.update_paused(at(INTERVAL), false, INTERVAL);
assert_eq!(perp.funding_start_block(), 2 * INTERVAL);
}
#[test]
fn funding_anchor_is_written_once_and_survives_a_repause() {
let mut perp = Perpetual::for_testing(1);
perp.update_paused(at(97_627_404), false, INTERVAL);
let anchor = perp.funding_start_block();
assert_eq!(anchor % INTERVAL, 0, "the preserved anchor must be a grid block");
perp.update_paused(at(97_627_404), true, INTERVAL);
assert_eq!(perp.funding_start_block(), anchor);
assert!(perp.is_paused());
perp.update_paused(at(98_000_000), false, INTERVAL);
assert_eq!(perp.funding_start_block(), anchor);
}
#[test]
fn pause_never_sets_the_funding_anchor() {
let mut perp = Perpetual::for_testing(1);
perp.update_paused(at(97_627_404), true, INTERVAL);
assert_eq!(perp.funding_start_block(), 0);
}
#[test]
fn update_order_expired_order_renewal_moves_to_back() {
let mut perp = Perpetual::for_testing(1);
let order1 = Order::for_l3_testing(
types::OrderType::OpenShort, udec64!(100), udec64!(1.0), 50, oid(1), 101, )
.with_expiry_block(100);
let order2 = Order::for_l3_testing(
types::OrderType::OpenShort,
udec64!(100),
udec64!(2.0),
50,
oid(2),
102,
);
perp.add_order(order1).unwrap();
perp.add_order(order2).unwrap();
let orders: Vec<_> = perp.l3_book.ask_orders().map(|o| o.order_id()).collect();
assert_eq!(orders, vec![oid(1), oid(2)], "Initial FIFO should be [1, 2]");
let order1_renewed = Order::for_l3_testing(
types::OrderType::OpenShort,
udec64!(100),
udec64!(1.0),
150, oid(1),
101,
)
.with_expiry_block(200);
perp.update_order(order1_renewed).unwrap();
let orders: Vec<_> = perp.l3_book.ask_orders().map(|o| o.order_id()).collect();
assert_eq!(orders, vec![oid(2), oid(1)], "After expiry renewal, FIFO should be [2, 1]");
}
#[test]
fn update_order_non_expired_order_keeps_position() {
let mut perp = Perpetual::for_testing(1);
let order1 = Order::for_l3_testing(
types::OrderType::OpenShort,
udec64!(100),
udec64!(1.0),
50,
oid(1),
101,
)
.with_expiry_block(100);
let order2 = Order::for_l3_testing(
types::OrderType::OpenShort,
udec64!(100),
udec64!(2.0),
50,
oid(2),
102,
);
perp.add_order(order1).unwrap();
perp.add_order(order2).unwrap();
let order1_updated = Order::for_l3_testing(
types::OrderType::OpenShort,
udec64!(100),
udec64!(1.0),
80, oid(1),
101,
)
.with_expiry_block(200);
perp.update_order(order1_updated).unwrap();
let orders: Vec<_> = perp.l3_book.ask_orders().map(|o| o.order_id()).collect();
assert_eq!(orders, vec![oid(1), oid(2)], "Non-expired order should keep position");
}
#[test]
fn perpetual_funding_effective_dated_consumed_once() {
let mut perp = Perpetual::for_testing(1);
perp.update_funding(types::StateInstant::new(1, 1), dec64!(0.02), dec256!(1.25), 3);
assert_eq!(perp.take_funding_payment(types::StateInstant::new(2, 2)), None);
assert_eq!(
perp.take_funding_payment(types::StateInstant::new(3, 3)),
Some((dec64!(0.02), dec256!(1.25))),
);
assert_eq!(perp.take_funding_payment(types::StateInstant::new(3, 3)), None);
assert_eq!(perp.take_funding_payment(types::StateInstant::new(4, 4)), None);
}
#[test]
fn perpetual_funding_rate_prev_next_boundary() {
let mut perp = Perpetual::for_testing(2);
perp.update_funding(types::StateInstant::new(1, 1), dec64!(0.02), dec256!(1.25), 3);
assert_eq!(perp.funding_rate(), D64::ZERO); assert!(perp.has_next_funding_rate()); perp.update_state_instant(types::StateInstant::new(2, 2));
assert_eq!(perp.funding_rate(), D64::ZERO); perp.update_state_instant(types::StateInstant::new(3, 3));
assert_eq!(perp.funding_rate(), dec64!(0.02)); assert!(!perp.has_next_funding_rate()); perp.update_state_instant(types::StateInstant::new(4, 4));
assert_eq!(perp.funding_rate(), dec64!(0.02)); }
#[test]
fn perpetual_update_funding_prev_rollover() {
let mut perp = Perpetual::for_testing(3);
perp.update_funding(types::StateInstant::new(1, 1), dec64!(0.02), dec256!(1.0), 3);
perp.update_funding(types::StateInstant::new(2, 2), dec64!(0.03), dec256!(2.0), 5);
perp.update_state_instant(types::StateInstant::new(4, 4));
assert_eq!(perp.funding_rate(), dec64!(0.02)); perp.update_state_instant(types::StateInstant::new(5, 5));
assert_eq!(perp.funding_rate(), dec64!(0.03)); }
}