diff --git a/algebra-core/src/lib.rs b/algebra-core/src/lib.rs index 544030bd6..62f54080d 100644 --- a/algebra-core/src/lib.rs +++ b/algebra-core/src/lib.rs @@ -1 +1,2 @@ pub mod monoid; +pub mod semigroup; diff --git a/algebra-core/src/monoid.rs b/algebra-core/src/monoid.rs index 63d8addc6..c3b9c421c 100644 --- a/algebra-core/src/monoid.rs +++ b/algebra-core/src/monoid.rs @@ -1,13 +1,17 @@ -pub trait Monoid { +use crate::semigroup::Semigroup; + +pub trait Monoid: Semigroup { fn empty() -> Self; - fn combine(self, other: Self) -> Self; +} + +impl Semigroup for u64 { + fn combine(self, other: Self) -> Self { + self + other + } } impl Monoid for u64 { fn empty() -> Self { 0 } - fn combine(self, other: Self) -> Self { - self + other - } } diff --git a/algebra-core/src/semigroup.rs b/algebra-core/src/semigroup.rs new file mode 100644 index 000000000..4de0ceb5b --- /dev/null +++ b/algebra-core/src/semigroup.rs @@ -0,0 +1,3 @@ +pub trait Semigroup { + fn combine(self, other: Self) -> Self; +} diff --git a/bloom-derivation/src/lib.rs b/bloom-derivation/src/lib.rs index cb0c102a5..3e4441893 100644 --- a/bloom-derivation/src/lib.rs +++ b/bloom-derivation/src/lib.rs @@ -14,6 +14,7 @@ pub fn derive_fragment(input: TokenStream) -> TokenStream { type U; fn side(&self) -> bloom_offchain::execution_engine::liquidity_book::side::SideM; fn input(&self) -> bloom_offchain::execution_engine::liquidity_book::types::InputAsset; + fn output(&self) -> bloom_offchain::execution_engine::liquidity_book::types::OutputAsset; fn price(&self) -> bloom_offchain::execution_engine::liquidity_book::types::AbsolutePrice; fn linear_fee(&self, input_consumed: bloom_offchain::execution_engine::liquidity_book::types::InputAsset) -> bloom_offchain::execution_engine::liquidity_book::types::FeeAsset; fn fee(&self) -> bloom_offchain::execution_engine::liquidity_book::types::FeeAsset; diff --git a/bloom-offchain-cardano/src/orders/limit.rs b/bloom-offchain-cardano/src/orders/limit.rs index 3c383fb65..f46073ee6 100644 --- a/bloom-offchain-cardano/src/orders/limit.rs +++ b/bloom-offchain-cardano/src/orders/limit.rs @@ -7,7 +7,7 @@ use cml_crypto::{blake2b224, Ed25519KeyHash, RawBytesEncoding}; use cml_multi_era::babbage::BabbageTransactionOutput; use bloom_offchain::execution_engine::liquidity_book::fragment::{Fragment, OrderState, StateTrans}; -use bloom_offchain::execution_engine::liquidity_book::linear_output_rel; +use bloom_offchain::execution_engine::liquidity_book::linear_output_relative; use bloom_offchain::execution_engine::liquidity_book::side::SideM; use bloom_offchain::execution_engine::liquidity_book::time::TimeBounds; use bloom_offchain::execution_engine::liquidity_book::types::{ @@ -148,6 +148,10 @@ impl Fragment for LimitOrder { self.input_amount } + fn output(&self) -> OutputAsset { + self.output_amount + } + fn price(&self) -> AbsolutePrice { AbsolutePrice::from_price(self.side(), self.base_price) } @@ -311,7 +315,7 @@ where .checked_sub(reserved_lovelace) .and_then(|lov| lov.checked_sub(conf.fee)) .and_then(|lov| lov.checked_sub(tradable_lovelace))?; - if let Some(base_output) = linear_output_rel(conf.tradable_input, conf.base_price) { + if let Some(base_output) = linear_output_relative(conf.tradable_input, conf.base_price) { let min_marginal_output = conf.min_marginal_output; let max_execution_steps_possible = base_output.checked_div(min_marginal_output); let max_execution_steps_available = execution_budget.checked_div(conf.cost_per_ex_step); diff --git a/bloom-offchain/src/execution_engine/liquidity_book/core.rs b/bloom-offchain/src/execution_engine/liquidity_book/core.rs new file mode 100644 index 000000000..0cefdd030 --- /dev/null +++ b/bloom-offchain/src/execution_engine/liquidity_book/core.rs @@ -0,0 +1,198 @@ +use std::collections::HashMap; + +use either::Either; + +use algebra_core::monoid::Monoid; +use algebra_core::semigroup::Semigroup; +use spectrum_offchain::data::Stable; + +use crate::execution_engine::liquidity_book::fragment::Fragment; +use crate::execution_engine::liquidity_book::side::SideM; +use crate::execution_engine::liquidity_book::types::{FeeAsset, InputAsset, OutputAsset}; + +/// Usage of liquidity from market maker. +/// take(P, M_1 + M_2 + ... + M_n) = take(P, M_1) |> take(_, M_2) |> ... take(_, M_n) +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Make { + pub side: SideM, + pub input: InputAsset, + pub output: OutputAsset, +} + +impl Semigroup for Make { + fn combine(self, other: Self) -> Self { + todo!("Semigroup for Make") + } +} + +/// Taking liquidity from market. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TerminalTake { + /// Input asset removed as a result of this transaction. + pub removed_input: InputAsset, + /// Output asset added as a result of this transaction. + pub added_output: OutputAsset, + /// Overall execution budget used. + pub budget_used: FeeAsset, + /// Execution fee charged. + pub fee_used: FeeAsset, +} + +impl TerminalTake { + pub fn new() -> Self { + Self { + removed_input: 0, + added_output: 0, + budget_used: 0, + fee_used: 0, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum Next { + /// Successive state is available. + Succ(S), + /// Terminal state. + Term(T), +} + +/// State transition of a take. +#[derive(Debug, Copy, Clone)] +pub struct Trans { + pub target: Cont, + pub result: Next, +} + +impl Semigroup for Trans { + fn combine(self, other: Self) -> Self { + Self { + target: self.target, + result: other.result, + } + } +} + +pub type TakerTrans = Trans; + +impl TakerTrans { + pub fn added_output(&self) -> OutputAsset + where + T: Fragment, + { + let accumulated_output = match &self.result { + Next::Succ(next) => next.output(), + Next::Term(term) => term.added_output, + }; + accumulated_output - self.target.output() + } +} + +#[derive(Debug, Copy, Clone)] +pub struct TryApply { + pub action: Action, + pub target: Subject, + pub result: Next, +} + +impl Semigroup for TryApply +where + A: Semigroup, +{ + fn combine(self, other: Self) -> Self { + Self { + action: self.action.combine(other.action), + target: self.target, + result: other.result, + } + } +} + +#[derive(Debug, Clone)] +pub struct MatchmakingAttempt { + takes: HashMap>, + makes: HashMap>, + execution_units_consumed: U, +} + +impl MatchmakingAttempt { + pub fn empty() -> Self + where + U: Monoid, + { + Self { + takes: HashMap::new(), + makes: HashMap::new(), + execution_units_consumed: U::empty(), + } + } + + pub fn execution_units_consumed(&self) -> U + where + U: Copy, + { + self.execution_units_consumed + } + + pub fn is_complete(&self) -> bool { + self.takes.len() > 0 + } + + pub fn unsatisfied_fragments(&self) -> Vec + where + Taker: Fragment + Copy, + { + let not_ok_terminal_takes = self.takes.iter().filter_map(|(_, apply)| { + let target = apply.target; + if apply.added_output() < target.min_marginal_output() { + Some(target) + } else { + None + } + }); + not_ok_terminal_takes.collect() + } + + pub fn add_take(&mut self, take: TakerTrans) { + let sid = take.target.stable_id(); + let take_combined = match self.takes.remove(&sid) { + None => take, + Some(existing_transition) => existing_transition.combine(take), + }; + self.takes.insert(sid, take_combined); + } + + pub fn add_make(&mut self, make: TryApply) { + let sid = make.target.stable_id(); + let maker_combined = match self.makes.remove(&sid) { + None => make, + Some(existing_transition) => existing_transition.combine(make), + }; + self.makes.insert(sid, maker_combined); + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Applied { + pub action: Action, + pub target: Subject::StableId, + pub result: Next, +} + +#[derive(Debug, Clone)] +pub struct MatchmakingRecipe { + instructions: Vec, Applied>>, +} + +impl MatchmakingRecipe +where + Taker: Stable, + Maker: Stable, +{ + pub fn try_from(attempt: MatchmakingAttempt) -> Result>> + where + Taker: Fragment + Copy, + { + Err(None) + } +} diff --git a/bloom-offchain/src/execution_engine/liquidity_book/fragment.rs b/bloom-offchain/src/execution_engine/liquidity_book/fragment.rs index a1080437a..f033110c8 100644 --- a/bloom-offchain/src/execution_engine/liquidity_book/fragment.rs +++ b/bloom-offchain/src/execution_engine/liquidity_book/fragment.rs @@ -1,3 +1,4 @@ +use crate::execution_engine::liquidity_book::core::{TerminalTake, Trans}; use num_rational::Ratio; use std::fmt::{Display, Formatter}; @@ -17,6 +18,16 @@ pub trait OrderState: Sized { ) -> (StateTrans, ExBudgetUsed, ExFeeUsed); } +/// Order as a state machine. +pub trait TakerBehaviour: Sized { + fn with_updated_time(self, time: u64) -> Trans; + fn with_applied_trade( + self, + removed_input: InputAsset, + added_output: OutputAsset, + ) -> Trans; +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum StateTrans { /// Next state is available. @@ -53,8 +64,10 @@ pub trait Fragment { type U; /// Side of the fragment relative to pair it maps to. fn side(&self) -> SideM; - /// Input asset. + /// Amount of input asset remaining. fn input(&self) -> InputAsset; + /// Amount of output asset accumulated. + fn output(&self) -> OutputAsset; /// Price of base asset in quote asset. fn price(&self) -> AbsolutePrice; /// Batcher fee for whole swap. diff --git a/bloom-offchain/src/execution_engine/liquidity_book/pool.rs b/bloom-offchain/src/execution_engine/liquidity_book/market_maker.rs similarity index 67% rename from bloom-offchain/src/execution_engine/liquidity_book/pool.rs rename to bloom-offchain/src/execution_engine/liquidity_book/market_maker.rs index 042a06845..0c11a574a 100644 --- a/bloom-offchain/src/execution_engine/liquidity_book/pool.rs +++ b/bloom-offchain/src/execution_engine/liquidity_book/market_maker.rs @@ -1,25 +1,27 @@ +use crate::execution_engine::liquidity_book::core::{Make, TryApply}; use crate::execution_engine::liquidity_book::side::Side; use crate::execution_engine::liquidity_book::types::AbsolutePrice; use derive_more::{Display, Div, From, Into, Mul}; use num_rational::Ratio; use std::cmp::Ordering; +use bignumber::BigNumber; /// Price of a theoretical 0-swap in pool. #[repr(transparent)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Div, Mul, From, Into, Display)] -pub struct StaticPrice(AbsolutePrice); +pub struct SpotPrice(AbsolutePrice); -impl StaticPrice { +impl SpotPrice { pub fn unwrap(self) -> Ratio { self.0.unwrap() } } /// Pooled liquidity. -pub trait Pool { +pub trait MarketMaker { type U; /// Static price (regardless swap vol) in this pool. - fn static_price(&self) -> StaticPrice; + fn static_price(&self) -> SpotPrice; /// Real price of swap. fn real_price(&self, input: Side) -> AbsolutePrice; /// Output of a swap. @@ -28,9 +30,16 @@ pub trait Pool { fn quality(&self) -> PoolQuality; /// How much (approximately) execution of this fragment will cost. fn marginal_cost_hint(&self) -> Self::U; - // Determine is swaps allowed for current pool, based on lq_bound. - // Used for correct support of legacy v1/v2 and fee switch pools - fn swaps_allowed(&self) -> bool; + // Is this maker active at the moment or not. + fn is_active(&self) -> bool; + fn available_liquidity_by_user_impact(&self, max_user_price_impact: Side>) -> (u64, u64); + fn available_liquidity_by_spot_impact(&self, max_spot_price_impact: Side>) -> (u64, u64); +} + +/// Pooled liquidity. +pub trait MakerBehavior: Sized { + /// Output of a swap. + fn swap(self, input: Side) -> TryApply; } #[derive(Debug, Copy, Clone, Eq, PartialEq, Into, From, Display)] diff --git a/bloom-offchain/src/execution_engine/liquidity_book/mod.rs b/bloom-offchain/src/execution_engine/liquidity_book/mod.rs index 81bef940e..ece41f8c9 100644 --- a/bloom-offchain/src/execution_engine/liquidity_book/mod.rs +++ b/bloom-offchain/src/execution_engine/liquidity_book/mod.rs @@ -12,9 +12,9 @@ use spectrum_offchain::data::{Has, Stable}; use spectrum_offchain::maker::Maker; use crate::execution_engine::liquidity_book::fragment::{Fragment, OrderState, StateTrans}; -use crate::execution_engine::liquidity_book::pool::{Pool, StaticPrice}; +use crate::execution_engine::liquidity_book::market_maker::{MarketMaker, SpotPrice}; use crate::execution_engine::liquidity_book::recipe::{ - ExecutionRecipe, Fill, IntermediateRecipe, PartialFill, Swap, TerminalInstruction, + ExecutionRecipe, Fill, IntermediateRecipe, PartialFill, Take, TerminalInstruction, }; use crate::execution_engine::liquidity_book::side::Side::{Ask, Bid}; use crate::execution_engine::liquidity_book::side::{Side, SideM}; @@ -23,9 +23,11 @@ use crate::execution_engine::liquidity_book::state::{IdleState, TLBState}; use crate::execution_engine::liquidity_book::types::{AbsolutePrice, RelativePrice}; use crate::execution_engine::types::Time; +mod core; pub mod fragment; pub mod interpreter; -pub mod pool; +pub mod market_maker; +mod parallel; pub mod recipe; pub mod side; pub mod stashing_option; @@ -103,7 +105,7 @@ impl TLB { impl TLB where Fr: Fragment + OrderState + Ord + Copy + Debug, - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, U: PartialOrd, { fn on_transition(&mut self, tx: StateTrans) { @@ -116,7 +118,7 @@ where impl TemporalLiquidityBook for TLB where Fr: Fragment + OrderState + Copy + Ord + Display + Debug, - Pl: Pool + Stable + Copy + Debug + Display, + Pl: MarketMaker + Stable + Copy + Debug + Display, U: PartialOrd + SubAssign + Sub + Copy + Debug, { fn attempt(&mut self) -> Option> { @@ -264,7 +266,7 @@ where impl ExternalTLBEvents for TLB where Fr: Fragment + OrderState + Ord + Copy + Display, - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, { fn advance_clocks(&mut self, new_time: u64) { requiring_settled_state(self, |st| st.advance_clocks(new_time)) @@ -292,7 +294,7 @@ where impl TLBFeedback for TLB where Fr: Fragment + OrderState + Ord + Copy, - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, { fn on_recipe_succeeded(&mut self) { self.state.commit(); @@ -309,9 +311,9 @@ const MAX_BIAS_PERCENT: u128 = 3; // | // p: >.... P_x ......(.)...... P_index .... P_y.... > // | | | | -// ask bias<=3%....pivot bid +// ask |bias|<=3%...pivot bid /// Settle execution price for two interleaving fragments. -fn settle_price(ask: &Fr, bid: &Fr, index_price: Option) -> AbsolutePrice { +fn settle_price(ask: &Fr, bid: &Fr, index_price: Option) -> AbsolutePrice { let price_ask = ask.price(); let price_bid = bid.price(); let price_ask_rat = price_ask.unwrap(); @@ -441,7 +443,7 @@ where } } -pub fn linear_output_rel(input: u64, price: RelativePrice) -> Option { +pub fn linear_output_relative(input: u64, price: RelativePrice) -> Option { u64::try_from(U256::from(input) * U256::from(*price.numer()) / U256::from(*price.denom())).ok() } @@ -454,13 +456,13 @@ fn linear_output_unsafe(input: u64, price: Side) -> u64 { struct FillFromPool { term_fill: Fill, - swap: Swap, + swap: Take, } fn fill_from_pool(lhs: PartialFill, pool: Pl) -> FillFromPool where Fr: Fragment + OrderState + Copy, - Pl: Pool + Copy, + Pl: MarketMaker + Copy, { match lhs.target.side() { SideM::Bid => { @@ -469,7 +471,7 @@ where let quote_input = bid.remaining_input; let (execution_amount, next_pool) = pool.swap(Side::Bid(quote_input)); bid.accumulated_output += execution_amount; - let swap = Swap { + let swap = Take { target: pool, transition: next_pool, side: SideM::Bid, @@ -487,7 +489,7 @@ where let base_input = ask.remaining_input; let (execution_amount, next_pool) = pool.swap(Side::Ask(base_input)); ask.accumulated_output += execution_amount; - let swap = Swap { + let swap = Take { target: pool, transition: next_pool, side: SideM::Ask, @@ -507,9 +509,9 @@ mod tests { use either::Either; use crate::execution_engine::liquidity_book::fragment::StateTrans; - use crate::execution_engine::liquidity_book::pool::Pool; + use crate::execution_engine::liquidity_book::market_maker::MarketMaker; use crate::execution_engine::liquidity_book::recipe::{ - ExecutionRecipe, Fill, IntermediateRecipe, PartialFill, Swap, TerminalInstruction, + ExecutionRecipe, Fill, IntermediateRecipe, PartialFill, Take, TerminalInstruction, }; use crate::execution_engine::liquidity_book::side::SideM::Bid; use crate::execution_engine::liquidity_book::side::{Side, SideM}; @@ -595,7 +597,7 @@ mod tests { budget_used: 990000, fee_used: 990, }), - TerminalInstruction::Swap(Swap { + TerminalInstruction::Swap(Take { target: p1, transition: p2, side: SideM::Ask, diff --git a/bloom-offchain/src/execution_engine/liquidity_book/parallel.rs b/bloom-offchain/src/execution_engine/liquidity_book/parallel.rs new file mode 100644 index 000000000..9b81ebc0b --- /dev/null +++ b/bloom-offchain/src/execution_engine/liquidity_book/parallel.rs @@ -0,0 +1,181 @@ +use std::fmt::Debug; + +use algebra_core::monoid::Monoid; +use spectrum_offchain::data::Stable; + +use crate::execution_engine::liquidity_book::core::{Make, TryApply}; +use crate::execution_engine::liquidity_book::core::{ + MatchmakingAttempt, MatchmakingRecipe, Next, TakerTrans, +}; +use crate::execution_engine::liquidity_book::fragment::{Fragment, OrderState, TakerBehaviour}; +use crate::execution_engine::liquidity_book::market_maker::{MakerBehavior, MarketMaker, SpotPrice}; +use crate::execution_engine::liquidity_book::side::Side::{Ask, Bid}; +use crate::execution_engine::liquidity_book::side::{Side, SideM}; +use crate::execution_engine::liquidity_book::stashing_option::StashingOption; +use crate::execution_engine::liquidity_book::state::{max_by_distance_to_spot, max_by_volume, TLBState}; +use crate::execution_engine::liquidity_book::types::AbsolutePrice; +use crate::execution_engine::liquidity_book::{ + linear_output_unsafe, settle_price, ExecutionCap, TLBFeedback, +}; + +/// TLB is a Universal Liquidity Aggregator (ULA), it is able to aggregate every piece of composable +/// liquidity available in the market. +/// +/// Composable liquidity falls into two essential categories: +/// (1.) Discrete Fragments of liquidity; +/// (2.) Pooled (according to some AMM formula) liquidity; +pub trait TemporalLiquidityBook { + fn attempt(&mut self) -> Option>; +} + +#[derive(Debug, Clone)] +pub struct TLB { + state: TLBState, + execution_cap: ExecutionCap, + attempt_side: SideM, +} + +impl TLBFeedback for TLB +where + Taker: Fragment + OrderState + Ord + Copy, + Maker: MarketMaker + Stable + Copy, +{ + fn on_recipe_succeeded(&mut self) { + self.state.commit(); + } + + fn on_recipe_failed(&mut self, stashing_opt: StashingOption) { + self.state.rollback(stashing_opt); + } +} + +impl TLB +where + Maker: MarketMaker + Stable, +{ + fn spot_price(&self) -> Option { + None + } +} + +impl TLB +where + Taker: Fragment + Ord + Copy + Debug, + Maker: MarketMaker + Stable + Copy, + U: PartialOrd, +{ + fn on_take(&mut self, tx: Next) { + if let Next::Succ(next) = tx { + self.state.pre_add_fragment(next); + } + } + + fn on_make(&mut self, tx: Next) { + if let Next::Succ(next) = tx { + self.state.pre_add_pool(next); + } + } +} + +impl TemporalLiquidityBook for TLB +where + Taker: Stable + Fragment + TakerBehaviour + Ord + Copy + Debug, + Maker: Stable + MarketMaker + MakerBehavior + Copy + Debug, + U: Monoid + PartialOrd + Copy, +{ + fn attempt(&mut self) -> Option> { + let mut batch: MatchmakingAttempt = MatchmakingAttempt::empty(); + while batch.execution_units_consumed() < self.execution_cap.soft { + let spot_price = self.spot_price(); + if let Some(target_taker) = self.state.pick_taker(|fs| { + spot_price + .map(|sp| max_by_distance_to_spot(fs, sp)) + .unwrap_or(max_by_volume(fs)) + }) { + let target_side = target_taker.side(); + let target_price = target_side.wrap(target_taker.price()); + let maybe_price_counter_taker = self.state.best_fr_price(!target_side); + let chunk_offered = target_side.wrap(0); // todo + let maybe_price_maker = self.state.preselect_market_maker(chunk_offered); + match (maybe_price_counter_taker, maybe_price_maker) { + (Some(price_counter_taker), maybe_price_maker) + if maybe_price_maker + .map(|(_, p)| price_counter_taker.better_than(p)) + .unwrap_or(true) => + { + if let Some(counter_taker) = self.state.try_pick_fr(!target_side, ok) { + //fill target_taker <- counter_taker + let make_match = |ask: &Taker, bid: &Taker| settle_price(ask, bid, spot_price); + let (take_a, take_b) = + execute_with_taker(target_taker, counter_taker, make_match); + for take in vec![take_a, take_b] { + batch.add_take(take); + self.on_take(take.result); + } + } + } + (_, Some((maker_sid, price_maker))) if target_price.overlaps(price_maker) => { + if let Some(maker) = self.state.take_pool(&maker_sid) { + //fill target_taker <- maker + let (take, make) = execute_with_maker(target_taker, maker, chunk_offered); + batch.add_take(take); + batch.add_make(make); + self.on_take(take.result); + self.on_make(make.result); + } + } + _ => {} + } + } + } + None + } +} + +fn execute_with_maker( + target_taker: Taker, + maker: Maker, + chunk_size: Side, +) -> (TakerTrans, TryApply) +where + Taker: Fragment + TakerBehaviour + Copy, + Maker: MakerBehavior + Copy, +{ + let maker_applied = maker.swap(chunk_size); + let taker_applied = target_taker.with_applied_trade(chunk_size.unwrap(), maker_applied.action.output); + (taker_applied, maker_applied) +} + +fn execute_with_taker( + target_taker: Taker, + counter_taker: Taker, + matchmaker: F, +) -> (TakerTrans, TakerTrans) +where + Taker: Fragment + TakerBehaviour + Copy, + F: FnOnce(&Taker, &Taker) -> AbsolutePrice, +{ + let (ask, bid) = match target_taker.side() { + SideM::Bid => (counter_taker, target_taker), + SideM::Ask => (target_taker, counter_taker), + }; + let price = matchmaker(&ask, &bid); + let quote_input = bid.input(); + let demand_base = linear_output_unsafe(quote_input, Bid(price)); + let supply_base = ask.input(); + let (quote, base) = if supply_base > demand_base { + (quote_input, demand_base) + } else if supply_base < demand_base { + let quote_executed = linear_output_unsafe(supply_base, Ask(price)); + (quote_executed, supply_base) + } else { + (quote_input, demand_base) + }; + let next_bid = bid.with_applied_trade(quote, base); + let next_ask = ask.with_applied_trade(base, quote); + (next_bid, next_ask) +} + +fn ok<'a, T>(_: &'a T) -> bool { + true +} diff --git a/bloom-offchain/src/execution_engine/liquidity_book/recipe.rs b/bloom-offchain/src/execution_engine/liquidity_book/recipe.rs index 8ef41bb1a..f65858845 100644 --- a/bloom-offchain/src/execution_engine/liquidity_book/recipe.rs +++ b/bloom-offchain/src/execution_engine/liquidity_book/recipe.rs @@ -1,4 +1,5 @@ use std::cmp::max; +use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; use log::{info, trace}; @@ -169,7 +170,7 @@ impl LinkedTerminalInstruction { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TerminalInstruction { Fill(Fill), - Swap(Swap), + Swap(Take), } impl Display for TerminalInstruction { @@ -340,7 +341,7 @@ pub struct LinkedSwap { } impl LinkedSwap { - pub fn from_swap(swap: Swap, target_src: Src) -> Self { + pub fn from_swap(swap: Take, target_src: Src) -> Self { Self { target: Bundled(swap.target, target_src), transition: swap.transition, @@ -352,7 +353,7 @@ impl LinkedSwap { } #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct Swap { +pub struct Take { pub target: Pl, pub transition: Pl, pub side: SideM, @@ -360,7 +361,7 @@ pub struct Swap { pub output: u64, } -impl Display for Swap { +impl Display for Take { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&*format!( "Swap(target={}, transition={}, side={}, input={}, output={})", diff --git a/bloom-offchain/src/execution_engine/liquidity_book/state.rs b/bloom-offchain/src/execution_engine/liquidity_book/state.rs index 658089671..880debb9b 100644 --- a/bloom-offchain/src/execution_engine/liquidity_book/state.rs +++ b/bloom-offchain/src/execution_engine/liquidity_book/state.rs @@ -1,4 +1,3 @@ -use either::{Either, Left, Right}; use std::collections::hash_map::Entry; use std::collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; @@ -6,15 +5,16 @@ use std::hash::Hash; use std::mem; use std::ops::Add; +use either::{Either, Left, Right}; use log::trace; use spectrum_offchain::data::Stable; use crate::execution_engine::liquidity_book::fragment::{Fragment, OrderState, StateTrans}; -use crate::execution_engine::liquidity_book::pool::{Pool, PoolQuality, StaticPrice}; +use crate::execution_engine::liquidity_book::market_maker::{MarketMaker, PoolQuality, SpotPrice}; use crate::execution_engine::liquidity_book::side::{Side, SideM}; use crate::execution_engine::liquidity_book::stashing_option::StashingOption; -use crate::execution_engine::liquidity_book::types::AbsolutePrice; +use crate::execution_engine::liquidity_book::types::{AbsolutePrice, InputAsset}; use crate::execution_engine::liquidity_book::weight::Weighted; #[derive(Debug, Clone, Eq, PartialEq)] @@ -36,7 +36,7 @@ impl IdleState { impl IdleState where Fr: Fragment + OrderState + Ord + Copy, - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, { pub fn advance_clocks(&mut self, new_time: u64) { self.fragments.advance_clocks(new_time) @@ -417,12 +417,27 @@ where } } +pub fn max_by_distance_to_spot(fragments: &mut Fragments, spot_price: SpotPrice) -> Option { + None +} + +pub fn max_by_volume(fragments: &mut Fragments) -> Option { + None +} + impl TLBState where Fr: Fragment + Ord + Copy + Debug, - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, U: PartialOrd, { + pub fn pick_taker(&mut self, strategy: F) -> Option + where + F: FnOnce(&mut Fragments) -> Option, + { + None + } + pub fn show_state(&self) -> String where Pl::StableId: Display, @@ -551,17 +566,35 @@ where impl TLBState where Fr: Fragment + Ord + Copy, - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, { - pub fn try_select_pool( + pub fn best_market_maker(&self) -> Option<&Pl> { + self.pools().pools.values().max_by_key(|p| p.quality()) + } + + pub fn preselect_market_maker( &self, - trade_hint: Side, - ) -> Option<(AbsolutePrice, StaticPrice, Pl::StableId)> { + offered_amount: Side>, + ) -> Option<(Pl::StableId, AbsolutePrice)> { let pools = self .pools() .pools .values() - .filter(|pool| pool.swaps_allowed()) + .filter(|pool| pool.is_active()) + .map(|p| (p.stable_id(), p.real_price(offered_amount))) + .collect::>(); + match offered_amount { + Side::Bid(_) => pools.into_iter().min_by_key(|(_, rp)| *rp), + Side::Ask(_) => pools.into_iter().max_by_key(|(_, rp)| *rp), + } + } + + pub fn try_select_pool(&self, trade_hint: Side) -> Option<(AbsolutePrice, SpotPrice, Pl::StableId)> { + let pools = self + .pools() + .pools + .values() + .filter(|pool| pool.is_active()) .map(|p| { let real_p = p.real_price(trade_hint); let static_p = p.static_price(); @@ -910,7 +943,7 @@ impl Pools { impl Pools where - Pl: Pool + Stable + Copy, + Pl: MarketMaker + Stable + Copy, { pub fn update_pool(&mut self, pool: Pl) { if let Some(old_pool) = self.pools.insert(pool.stable_id(), pool) { @@ -928,18 +961,17 @@ where #[cfg(test)] pub mod tests { - use either::Left; use std::cmp::Ordering; use std::fmt::{Debug, Display, Formatter}; + use either::Left; + use spectrum_offchain::data::Stable; use crate::execution_engine::liquidity_book::fragment::{Fragment, OrderState, StateTrans}; - use crate::execution_engine::liquidity_book::pool::{Pool, StaticPrice}; + use crate::execution_engine::liquidity_book::market_maker::{MarketMaker, SpotPrice}; use crate::execution_engine::liquidity_book::side::{Side, SideM}; - use crate::execution_engine::liquidity_book::state::{ - Fragments, IdleState, PoolQuality, StashingOption, TLBState, - }; + use crate::execution_engine::liquidity_book::state::{IdleState, PoolQuality, StashingOption, TLBState}; use crate::execution_engine::liquidity_book::time::TimeBounds; use crate::execution_engine::liquidity_book::types::{ AbsolutePrice, ExBudgetUsed, ExCostUnits, ExFeeUsed, OutputAsset, @@ -1330,7 +1362,7 @@ pub mod tests { } } - fn with_applied_swap( + fn apply_swap( mut self, removed_input: u64, added_output: u64, @@ -1377,10 +1409,10 @@ pub mod tests { } } - impl Pool for SimpleCFMMPool { + impl MarketMaker for SimpleCFMMPool { type U = u64; - fn static_price(&self) -> StaticPrice { + fn static_price(&self) -> SpotPrice { AbsolutePrice::new(self.reserves_quote, self.reserves_base).into() } @@ -1430,7 +1462,7 @@ pub mod tests { 10 } - fn swaps_allowed(&self) -> bool { + fn is_active(&self) -> bool { // SimpleCFMMPool used only for tests true } diff --git a/spectrum-cardano-lib/src/ex_units.rs b/spectrum-cardano-lib/src/ex_units.rs index 2640aaba3..60e4d5c1f 100644 --- a/spectrum-cardano-lib/src/ex_units.rs +++ b/spectrum-cardano-lib/src/ex_units.rs @@ -1,4 +1,5 @@ use algebra_core::monoid::Monoid; +use algebra_core::semigroup::Semigroup; use derive_more::{Add, AddAssign, Sub, SubAssign}; use std::ops::Add; @@ -19,13 +20,16 @@ impl ExUnits { } } +impl Semigroup for ExUnits { + fn combine(self, other: Self) -> Self { + self.add(other) + } +} + impl Monoid for ExUnits { fn empty() -> Self { ExUnits { mem: 0, steps: 0 } } - fn combine(self, other: Self) -> Self { - self.add(other) - } } impl From for cml_chain::plutus::ExUnits { diff --git a/spectrum-offchain-cardano/Cargo.toml b/spectrum-offchain-cardano/Cargo.toml index b8db88501..cd2e8916a 100644 --- a/spectrum-offchain-cardano/Cargo.toml +++ b/spectrum-offchain-cardano/Cargo.toml @@ -26,6 +26,8 @@ futures = "0.3.25" tokio = { version = "1.22.0", features = ["full"] } log = "0.4.17" log4rs = "1.2.0" +dashu-base = "0.4.1" +dashu-float = "0.4.1" rand = { version = "0.8.5", features = ["small_rng"] } rand_chacha = "0.3.1" async-trait = "0.1.58" diff --git a/spectrum-offchain-cardano/src/data/balance_pool.rs b/spectrum-offchain-cardano/src/data/balance_pool.rs index 55a8d4a85..ed04ff301 100644 --- a/spectrum-offchain-cardano/src/data/balance_pool.rs +++ b/spectrum-offchain-cardano/src/data/balance_pool.rs @@ -1,32 +1,35 @@ use std::fmt::Debug; -use std::ops::Mul; +use std::ops::{Div, Mul, Neg}; use bignumber::BigNumber; use cml_chain::address::Address; use cml_chain::assets::MultiAsset; use cml_chain::certs::StakeCredential; -use cml_chain::plutus::utils::ConstrPlutusDataEncoding; use cml_chain::plutus::{ConstrPlutusData, PlutusData}; +use cml_chain::plutus::utils::ConstrPlutusDataEncoding; use cml_chain::transaction::{ConwayFormatTxOut, DatumOption, TransactionOutput}; use cml_chain::utils::BigInteger; use cml_chain::Value; use cml_core::serialization::LenEncoding::{Canonical, Indefinite}; use cml_multi_era::babbage::BabbageTransactionOutput; +use dashu_base::sign::Abs; +use dashu_float::DBig; use num_integer::Roots; use num_rational::Ratio; use num_traits::{CheckedAdd, CheckedSub}; +use num_traits::ToPrimitive; use primitive_types::U512; -use bloom_offchain::execution_engine::liquidity_book::pool::{Pool, PoolQuality, StaticPrice}; +use bloom_offchain::execution_engine::liquidity_book::market_maker::{MarketMaker, PoolQuality, SpotPrice}; use bloom_offchain::execution_engine::liquidity_book::side::{Side, SideM}; use bloom_offchain::execution_engine::liquidity_book::types::AbsolutePrice; +use spectrum_cardano_lib::{TaggedAmount, TaggedAssetClass}; use spectrum_cardano_lib::ex_units::ExUnits; use spectrum_cardano_lib::plutus_data::{ConstrPlutusDataExtension, DatumExtension}; use spectrum_cardano_lib::plutus_data::{IntoPlutusData, PlutusDataExtension}; use spectrum_cardano_lib::transaction::TransactionOutputExtension; use spectrum_cardano_lib::types::TryFromPData; use spectrum_cardano_lib::value::ValueExtension; -use spectrum_cardano_lib::{TaggedAmount, TaggedAssetClass}; use spectrum_offchain::data::{Has, Stable}; use spectrum_offchain::ledger::{IntoLedger, TryFromLedger}; @@ -39,10 +42,10 @@ use crate::data::pair::order_canonical; use crate::data::pool::{ ApplyOrder, ApplyOrderError, AssetDeltas, CFMMPoolAction, ImmutablePoolUtxo, Lq, PoolBounds, Rx, Ry, }; -use crate::data::redeem::ClassicalOnChainRedeem; use crate::data::PoolId; -use crate::deployment::ProtocolValidator::BalanceFnPoolV1; +use crate::data::redeem::ClassicalOnChainRedeem; use crate::deployment::{DeployedScriptInfo, DeployedValidator, DeployedValidatorErased, RequiresValidator}; +use crate::deployment::ProtocolValidator::BalanceFnPoolV1; use crate::pool_math::balance_math::balance_cfmm_output_amount; use crate::pool_math::cfmm_math::{classic_cfmm_reward_lp, classic_cfmm_shares_amount}; @@ -388,9 +391,9 @@ impl AMMOps for BalancePool { } } -impl Pool for BalancePool { +impl MarketMaker for BalancePool { type U = ExUnits; - fn static_price(&self) -> StaticPrice { + fn static_price(&self) -> SpotPrice { let x = self.asset_x.untag(); let y = self.asset_y.untag(); let [base, _] = order_canonical(x, y); @@ -484,13 +487,156 @@ impl Pool for BalancePool { self.marginal_cost } - fn swaps_allowed(&self) -> bool { + fn is_active(&self) -> bool { if self.asset_x.is_native() { self.reserves_x.untag() >= self.min_pool_lovelace } else { self.reserves_y.untag() >= self.min_pool_lovelace } } + + fn available_liquidity_by_user_impact(&self, max_user_price_impact: Side>) -> (u64, u64) { + // "max_user_price_impact" is calculated as + // "max_user_price_impact = 1 - avg_sell_price / market_price" to be always > 0. + // Outputs are ("quote_amount_available", "base_amount_required"). + // Outputs reflects how many quote asset the user will receive and how many base asset + // must be added to the pool in order for this operation to occur with a given + // "max_price_impact" relative to the current state of the pool. + // Note: all calculations are made taking fees into account, thus "max_user_price_impact" + // must also include fees in "market_price" calculation. + const BN_ONE: BigNumber = BigNumber { value: DBig::ONE }; + + const MAX_ERR: i32 = 1; + const MAX_ITERS: u32 = 25; + + let (tradable_reserves_base, w_base, tradable_reserves_quote, w_quote, total_fee_mult) = + match max_user_price_impact { + Side::Bid(_) => ( + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from(self.weight_y as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from(self.weight_x as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.lp_fee_y - self.treasury_fee).to_f64().unwrap()), + ), + Side::Ask(_) => ( + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from(self.weight_x as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from(self.weight_y as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.lp_fee_x - self.treasury_fee).to_f64().unwrap()), + ), + }; + let lq_balance = + tradable_reserves_base.pow(&w_base.clone()) * tradable_reserves_quote.pow(&w_quote.clone()); + + let market_price = tradable_reserves_quote + .clone() + .div(w_quote.clone()) + .div(tradable_reserves_base.clone().div(w_base.clone())) + * total_fee_mult.clone(); + + let avg_sell_price = market_price + * (BN_ONE + - BigNumber::from(*max_user_price_impact.unwrap().numer() as f64) + .div(BigNumber::from(*max_user_price_impact.unwrap().denom() as f64))); + + //# Constants for calculations: + let a = (w_base.clone() + w_quote.clone()) / w_quote.clone(); + let b = BN_ONE - a.clone(); + let c = lq_balance.pow(&BN_ONE.div(w_quote.clone())) * w_base.clone() / w_quote.clone(); + let k = c.clone() / b.clone(); + // + let x0 = tradable_reserves_base.clone(); + let mut x1 = x0.clone().mul(BigNumber::from(1.1)).to_precision(0); //int(1.1 * x0); + let mut err = tradable_reserves_base.clone(); + let mut counter = 0; + // // # Numerical calculation procedure (usual less than 5 iterations). + // // # You can increase 'maxErr' value to decrease number of iters. + while err.to_precision(10).value.ge(&DBig::from(MAX_ERR)) && counter < MAX_ITERS { + let f_x = (avg_sell_price.clone().div(total_fee_mult.clone())) + - k.clone() * (x1.clone().pow(&b.clone()) - x0.clone().pow(&b.clone())) + / (x1.clone() - x0.clone()); + let f_x_der = k.clone() + * ((b.clone() - BN_ONE) * x1.clone().pow(&(b.clone() + BN_ONE)) + + x1.clone() * x0.clone().pow(&b.clone()) + - b.clone() * x0.clone() * x1.clone().pow(&b.clone())) + / (x1.clone() * (x1.clone() - x0.clone()).powi(2)); + let add = f_x.clone().div(f_x_der.clone()); + + if (x1.clone() + add.clone()).value.to_f64().value() > 0_f64 { + x1 = x1.clone() + add.clone(); + err = BigNumber::from(add.clone().value.to_f32().value().abs()); + counter += 1; + } else { + break; + } + } + let base_delta = (x1.clone() - tradable_reserves_base.clone()) / total_fee_mult; + + let tradable_reserves_quote_final = + (lq_balance / x1.clone().pow(&w_base.clone())).pow(&BN_ONE.div(&w_quote)); + let quote_delta = tradable_reserves_quote - tradable_reserves_quote_final; + + return ( + ::try_from(quote_delta.value.to_int().value()).unwrap(), + ::try_from(base_delta.value.to_int().value()).unwrap(), + ); + } + fn available_liquidity_by_spot_impact(&self, max_spot_price_impact: Side>) -> (u64, u64) { + // "max_spot_price_impact" is calculated as + // "max_spot_price_impact = 1 - market_price_new / market_price" to be always > 0. + // Outputs are ("quote_amount_available", "base_amount_required"). + // Outputs reflects how many quote asset the user will receive and how many base asset + // must be added to the pool in order for this operation to occur with a given + // "max_spot_price_impact" relative to the current state of the pool. + // Note: all calculations are made taking fees into account, thus "max_spot_price_impact" + // must also include fees in "market_price" calculation. + const BN_ONE: BigNumber = BigNumber { value: DBig::ONE }; + + let (tradable_reserves_base, w_base, tradable_reserves_quote, w_quote, total_fee_mult) = + match max_spot_price_impact { + Side::Bid(_) => ( + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from(self.weight_y as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from(self.weight_x as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.lp_fee_y - self.treasury_fee).to_f64().unwrap()), + ), + Side::Ask(_) => ( + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from(self.weight_x as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from(self.weight_y as f64).div(BigNumber::from(WEIGHT_FEE_DEN as f64)), + BigNumber::from((self.lp_fee_x - self.treasury_fee).to_f64().unwrap()), + ), + }; + let lq_balance = + tradable_reserves_base.pow(&w_base.clone()) * tradable_reserves_quote.pow(&w_quote.clone()); + + let market_price = tradable_reserves_quote + .clone() + .div(w_quote.clone()) + .div(tradable_reserves_base.clone().div(w_base.clone())) + * total_fee_mult.clone(); + + let impact_price = market_price.clone() + * (BN_ONE + - BigNumber::from(*max_spot_price_impact.unwrap().numer() as f64) + .div(BigNumber::from(*max_spot_price_impact.unwrap().denom() as f64))); + //# Constants for calculations: + let x1 = (market_price.clone() / impact_price.clone()).pow(&w_quote.clone()) + * tradable_reserves_base.clone(); + let base_delta = (x1.clone() - tradable_reserves_base.clone()) / total_fee_mult; + + let tradable_reserves_quote_final = + (lq_balance / x1.clone().pow(&w_base.clone())).pow(&BN_ONE.div(&w_quote)); + let quote_delta = tradable_reserves_quote - tradable_reserves_quote_final; + + return ( + ::try_from(quote_delta.value.to_int().value()).unwrap(), + ::try_from(base_delta.value.to_int().value()).unwrap(), + ); + } } impl ApplyOrder for BalancePool { @@ -602,38 +748,39 @@ impl ApplyOrder for BalancePool { #[cfg(test)] mod tests { - use cml_chain::plutus::PlutusData; use cml_chain::Deserialize; + use cml_chain::plutus::PlutusData; use cml_core::serialization::Serialize; use cml_crypto::{Ed25519KeyHash, ScriptHash, TransactionHash}; use num_rational::Ratio; - use crate::constants::MAX_LQ_CAP; - use bloom_offchain::execution_engine::liquidity_book::pool::Pool; + use bloom_offchain::execution_engine::liquidity_book::market_maker::MarketMaker; use bloom_offchain::execution_engine::liquidity_book::side::Side; + use bloom_offchain::execution_engine::liquidity_book::side::Side::{Ask, Bid}; + use spectrum_cardano_lib::{AssetClass, AssetName, OutputRef, TaggedAmount, TaggedAssetClass}; use spectrum_cardano_lib::ex_units::ExUnits; use spectrum_cardano_lib::types::TryFromPData; - use spectrum_cardano_lib::{AssetClass, AssetName, OutputRef, TaggedAmount, TaggedAssetClass}; + use crate::data::{OnChainOrderId, PoolId}; use crate::data::balance_pool::{BalancePool, BalancePoolConfig, BalancePoolRedeemer, BalancePoolVer}; use crate::data::order::ClassicalOrder; use crate::data::order::OrderType::BalanceFn; use crate::data::pool::{ApplyOrder, CFMMPoolAction}; use crate::data::redeem::{ClassicalOnChainRedeem, Redeem}; - use crate::data::{OnChainOrderId, PoolId}; const DATUM_SAMPLE: &str = "d8799fd8799f581c5df8fe3f9f0e10855f930e0ea6c227e3bba0aba54d39f9d55b95e21c436e6674ffd8799f4040ff01d8799f581c4b3459fd18a1dbabe207cd19c9951a9fac9f5c0f9c384e3d97efba26457465737443ff04d8799f581c0df79145b95580c14ef4baf8d022d7f0cbb08f3bed43bf97a2ddd8cb426c71ff1a000186820a00009fd8799fd87a9f581cb046b660db0eaf9be4f4300180ccf277e4209dada77c48fbd37ba81dffffff581c8d4be10d934b60a22f267699ea3f7ebdade1f8e535d1bd0ef7ce18b61a0501bced08ff"; - #[test] - fn parse_balance_pool_datum() { - let pd = PlutusData::from_cbor_bytes(&*hex::decode(DATUM_SAMPLE).unwrap()).unwrap(); - let maybe_conf = BalancePoolConfig::try_from_pd(pd); - assert!(maybe_conf.is_some()) - } - - #[test] - fn swap() { - let pool = BalancePool { + fn gen_ada_token_pool( + reserves_x: u64, + reserves_y: u64, + liquidity: u64, + lp_fee_x: u64, + lp_fee_y: u64, + treasury_fee: u64, + treasury_x: u64, + treasury_y: u64, + ) -> BalancePool { + return BalancePool { id: PoolId::from(( ScriptHash::from([ 162, 206, 112, 95, 150, 240, 52, 167, 61, 102, 158, 92, 11, 47, 25, 41, 48, 224, 188, @@ -647,11 +794,11 @@ mod tests { ], )), )), - reserves_x: TaggedAmount::new(2115301811439), + reserves_x: TaggedAmount::new(reserves_x), weight_x: 1, - reserves_y: TaggedAmount::new(27887555508598), + reserves_y: TaggedAmount::new(reserves_y), weight_y: 4, - liquidity: TaggedAmount::new(0), + liquidity: TaggedAmount::new(liquidity), asset_x: TaggedAssetClass::new(AssetClass::Native), asset_y: TaggedAssetClass::new(AssetClass::Token(( ScriptHash::from([ @@ -679,11 +826,11 @@ mod tests { ], )), ))), - lp_fee_x: Ratio::new_raw(99000, 100000), - lp_fee_y: Ratio::new_raw(99000, 100000), - treasury_fee: Ratio::new_raw(100, 100000), - treasury_x: TaggedAmount::new(1143236614), - treasury_y: TaggedAmount::new(3057757049), + lp_fee_x: Ratio::new_raw(lp_fee_x, 100000), + lp_fee_y: Ratio::new_raw(lp_fee_y, 100000), + treasury_fee: Ratio::new_raw(treasury_fee, 100000), + treasury_x: TaggedAmount::new(treasury_x), + treasury_y: TaggedAmount::new(treasury_y), ver: BalancePoolVer::V1, marginal_cost: ExUnits { mem: 120000000, @@ -691,6 +838,28 @@ mod tests { }, min_pool_lovelace: 10000000000, }; + } + + #[test] + fn parse_balance_pool_datum() { + let pd = PlutusData::from_cbor_bytes(&*hex::decode(DATUM_SAMPLE).unwrap()).unwrap(); + let maybe_conf = BalancePoolConfig::try_from_pd(pd); + assert!(maybe_conf.is_some()) + } + + #[test] + fn swap() { + let pool = gen_ada_token_pool( + 2115301811439, + 27887555508598, + 0, + 99000, + 99000, + 100, + 1143236614, + 3057757049, + ); + let result = pool.swap(Side::Ask(200000000)); assert_eq!(result.0, 652178037) @@ -698,64 +867,7 @@ mod tests { #[test] fn swap_redeemer_test() { - let pool = BalancePool { - id: PoolId::from(( - ScriptHash::from([ - 162, 206, 112, 95, 150, 240, 52, 167, 61, 102, 158, 92, 11, 47, 25, 41, 48, 224, 188, - 211, 138, 203, 127, 107, 246, 89, 115, 157, - ]), - AssetName::from(( - 3, - [ - 110, 102, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ], - )), - )), - reserves_x: TaggedAmount::new(200000000), - weight_x: 1, - reserves_y: TaggedAmount::new(84093845), - weight_y: 4, - liquidity: TaggedAmount::new(0), - asset_x: TaggedAssetClass::new(AssetClass::Native), - asset_y: TaggedAssetClass::new(AssetClass::Token(( - ScriptHash::from([ - 75, 52, 89, 253, 24, 161, 219, 171, 226, 7, 205, 25, 201, 149, 26, 159, 172, 159, 92, 15, - 156, 56, 78, 61, 151, 239, 186, 38, - ]), - AssetName::from(( - 5, - [ - 116, 101, 115, 116, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, - ], - )), - ))), - asset_lq: TaggedAssetClass::new(AssetClass::Token(( - ScriptHash::from([ - 114, 191, 27, 172, 195, 20, 1, 41, 111, 158, 228, 210, 254, 123, 132, 165, 36, 56, 38, - 251, 3, 233, 206, 25, 51, 218, 254, 192, - ]), - AssetName::from(( - 2, - [ - 108, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - )), - ))), - lp_fee_x: Ratio::new_raw(99970, 100000), - lp_fee_y: Ratio::new_raw(99970, 100000), - treasury_fee: Ratio::new_raw(10, 100000), - treasury_x: TaggedAmount::new(10000), - treasury_y: TaggedAmount::new(0), - ver: BalancePoolVer::V1, - marginal_cost: ExUnits { - mem: 120000000, - steps: 100000000000, - }, - min_pool_lovelace: 1000000000, - }; + let pool = gen_ada_token_pool(200000000, 84093845, 0, 99970, 99970, 10, 10000, 0); let (_result, new_pool) = pool.clone().swap(Side::Ask(363613802862)); @@ -775,84 +887,29 @@ mod tests { #[test] fn deposit_redeemer_test() { - let pool_id = PoolId::from(( - ScriptHash::from([ - 162, 206, 112, 95, 150, 240, 52, 167, 61, 102, 158, 92, 11, 47, 25, 41, 48, 224, 188, 211, - 138, 203, 127, 107, 246, 89, 115, 157, - ]), - AssetName::from(( - 3, - [ - 110, 102, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ], - )), - )); - - let token_x = TaggedAssetClass::new(AssetClass::Native); - let token_y = TaggedAssetClass::new(AssetClass::Token(( - ScriptHash::from([ - 75, 52, 89, 253, 24, 161, 219, 171, 226, 7, 205, 25, 201, 149, 26, 159, 172, 159, 92, 15, - 156, 56, 78, 61, 151, 239, 186, 38, - ]), - AssetName::from(( - 5, - [ - 116, 101, 115, 116, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - )), - ))); - - let token_lq = TaggedAssetClass::new(AssetClass::Token(( - ScriptHash::from([ - 114, 191, 27, 172, 195, 20, 1, 41, 111, 158, 228, 210, 254, 123, 132, 165, 36, 56, 38, 251, - 3, 233, 206, 25, 51, 218, 254, 192, - ]), - AssetName::from(( - 2, - [ - 108, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, - ], - )), - ))); - - let pool = BalancePool { - id: pool_id, - reserves_x: TaggedAmount::new(152570837), - weight_x: 1, - reserves_y: TaggedAmount::new(42092), - weight_y: 4, - liquidity: TaggedAmount::new(MAX_LQ_CAP - 9223372036854560080), - asset_x: token_x, - asset_y: token_y, - asset_lq: token_lq, - lp_fee_x: Ratio::new_raw(99000, 100000), - lp_fee_y: Ratio::new_raw(99000, 100000), - treasury_fee: Ratio::new_raw(100, 100000), - treasury_x: TaggedAmount::new(354535), - treasury_y: TaggedAmount::new(121), - ver: BalancePoolVer::V1, - marginal_cost: ExUnits { - mem: 120000000, - steps: 100000000000, - }, - min_pool_lovelace: 100000000000, - }; + let pool = gen_ada_token_pool( + 1981759952, + 53144, + 9223372036854587823, + 99000, + 99000, + 100, + 13000, + 94, + ); const TX: &str = "6c038a69587061acd5611507e68b1fd3a7e7d189367b7853f3bb5079a118b880"; const IX: u64 = 1; let test_order: ClassicalOnChainRedeem = ClassicalOrder { id: OnChainOrderId(OutputRef::new(TransactionHash::from_hex(TX).unwrap(), IX)), - pool_id: pool_id, + pool_id: pool.id, order: Redeem { - pool_nft: pool_id, - token_x: token_x, - token_y: token_y, - token_lq: token_lq, - token_lq_amount: TaggedAmount::new(1581993), + pool_nft: pool.id, + token_x: pool.asset_x, + token_y: pool.asset_y, + token_lq: pool.asset_lq, + token_lq_amount: TaggedAmount::new(1900727), ex_fee: 1500000, reward_pkh: Ed25519KeyHash::from([0u8; 28]), reward_stake_pkh: None, @@ -867,4 +924,31 @@ mod tests { assert_eq!(1, 1) } + + #[test] + #[test] + fn available_liquidity_test() { + let pool = gen_ada_token_pool(2105999997, 1981759952, 9223372036854587823, 99000, 99000, 0, 0, 0); + + // Repair available volumes from average sell price impact. + let max_target_price_impact = Ratio::new_raw(45035996273705, 2251799813685248); + + let (available_liquidity_quote_ask, _) = + pool.available_liquidity_by_user_impact(Ask(max_target_price_impact)); + let (available_liquidity_quote_bid, _) = + pool.available_liquidity_by_user_impact(Bid(max_target_price_impact)); + + assert_eq!(available_liquidity_quote_ask, 15918267); + assert_eq!(available_liquidity_quote_bid, 67120253); + + // Repair available volumes from pool spot price impact. + let max_spot_price_impact = Ratio::new_raw(355981766792995, 9007199254740992); + + let (quote_qty_ask_spot, _) = pool.available_liquidity_by_spot_impact(Ask(max_spot_price_impact)); + + let (quote_qty_bid_spot, _) = pool.available_liquidity_by_spot_impact(Bid(max_spot_price_impact)); + + assert_eq!(quote_qty_ask_spot, 15918267); + assert_eq!(quote_qty_bid_spot, 66853939) + } } diff --git a/spectrum-offchain-cardano/src/data/cfmm_pool.rs b/spectrum-offchain-cardano/src/data/cfmm_pool.rs index c7d899f3b..877a0bdac 100644 --- a/spectrum-offchain-cardano/src/data/cfmm_pool.rs +++ b/spectrum-offchain-cardano/src/data/cfmm_pool.rs @@ -1,20 +1,23 @@ +use std::fmt::Debug; +use std::ops::Div; + +use bignumber::BigNumber; use cml_chain::address::Address; use cml_chain::assets::MultiAsset; use cml_chain::certs::StakeCredential; -use std::fmt::Debug; - use cml_chain::plutus::{ConstrPlutusData, PlutusData}; use cml_chain::transaction::{ConwayFormatTxOut, DatumOption, TransactionOutput}; use cml_chain::utils::BigInteger; use cml_chain::Value; - use cml_multi_era::babbage::BabbageTransactionOutput; +use dashu_float::DBig; use num_integer::Roots; use num_rational::Ratio; +use num_traits::ToPrimitive; use num_traits::{CheckedAdd, CheckedSub}; use type_equalities::IsEqual; -use bloom_offchain::execution_engine::liquidity_book::pool::{Pool, PoolQuality, StaticPrice}; +use bloom_offchain::execution_engine::liquidity_book::market_maker::{MarketMaker, PoolQuality, SpotPrice}; use bloom_offchain::execution_engine::liquidity_book::side::{Side, SideM}; use bloom_offchain::execution_engine::liquidity_book::types::AbsolutePrice; use spectrum_cardano_lib::ex_units::ExUnits; @@ -29,23 +32,17 @@ use spectrum_offchain::data::{Has, Stable}; use spectrum_offchain::ledger::{IntoLedger, TryFromLedger}; use crate::constants::{FEE_DEN, LEGACY_FEE_NUM_MULTIPLIER, MAX_LQ_CAP}; - use crate::data::deposit::ClassicalOnChainDeposit; - use crate::data::fee_switch_bidirectional_fee::FeeSwitchBidirectionalPoolConfig; - +use crate::data::fee_switch_pool::FeeSwitchPoolConfig; use crate::data::limit_swap::ClassicalOnChainLimitSwap; use crate::data::operation_output::{DepositOutput, RedeemOutput, SwapOutput}; use crate::data::order::{Base, ClassicalOrder, PoolNft, Quote}; use crate::data::pair::order_canonical; - use crate::data::pool::{ ApplyOrder, ApplyOrderError, AssetDeltas, ImmutablePoolUtxo, Lq, PoolBounds, Rx, Ry, }; use crate::data::redeem::ClassicalOnChainRedeem; - -use crate::data::fee_switch_pool::FeeSwitchPoolConfig; -use crate::data::pool::ApplyOrderError::Incompatible; use crate::data::PoolId; use crate::deployment::ProtocolValidator::{ ConstFnPoolFeeSwitch, ConstFnPoolFeeSwitchBiDirFee, ConstFnPoolV1, ConstFnPoolV2, @@ -282,10 +279,10 @@ where } } -impl Pool for ConstFnPool { +impl MarketMaker for ConstFnPool { type U = ExUnits; - fn static_price(&self) -> StaticPrice { + fn static_price(&self) -> SpotPrice { let x = self.asset_x.untag(); let y = self.asset_y.untag(); let [base, _] = order_canonical(x, y); @@ -370,7 +367,7 @@ impl Pool for ConstFnPool { self.marginal_cost } - fn swaps_allowed(&self) -> bool { + fn is_active(&self) -> bool { let lq_bound = (self.reserves_x.untag() * 2) >= self.lq_lower_bound.untag(); let bot_bound = if self.asset_x.is_native() { self.reserves_x.untag() >= self.min_pool_lovelace @@ -379,6 +376,109 @@ impl Pool for ConstFnPool { }; lq_bound && bot_bound } + + fn available_liquidity_by_user_impact(&self, max_user_price_impact: Side>) -> (u64, u64) { + // "max_user_price_impact" is calculated as + // "max_user_price_impact = 1 - avg_sell_price / market_price" to be always > 0. + // Outputs are ("quote_amount_available", "base_amount_required"). + // Outputs reflects how many quote asset the user will receive and how many base asset + // must be added to the pool in order for this operation to occur with a given + // "max_price_impact" relative to the current state of the pool. + // Note: all calculations are made taking fees into account, thus "max_user_price_impact" + // must also include fees in "market_price" calculation. + const BN_ONE: BigNumber = BigNumber { value: DBig::ONE }; + let sqrt_degree = BigNumber::from(0.5); + + let (tradable_reserves_base, tradable_reserves_quote, total_fee_mult) = match max_user_price_impact { + Side::Bid(_) => ( + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from((self.lp_fee_y - self.treasury_fee).to_f64().unwrap()), + ), + Side::Ask(_) => ( + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from((self.lp_fee_x - self.treasury_fee).to_f64().unwrap()), + ), + }; + + let lq_balance = (tradable_reserves_base.clone() * tradable_reserves_quote.clone()).pow(&sqrt_degree); + + let market_price = tradable_reserves_quote + .clone() + .div(tradable_reserves_base.clone()) + * total_fee_mult.clone(); + + let impact_price = market_price + * (BN_ONE + - BigNumber::from(*max_user_price_impact.unwrap().numer() as f64) + .div(BigNumber::from(*max_user_price_impact.unwrap().denom() as f64))) + .div(total_fee_mult.clone()); + + let p1 = + (impact_price * lq_balance.clone() / tradable_reserves_quote.clone()).pow(&(BigNumber::from(2))); + let p1_sqrt = p1.clone().pow(&sqrt_degree); + let x1 = lq_balance.clone() / p1_sqrt.clone(); + let y1 = lq_balance.clone() * p1_sqrt.clone(); + + let base = (x1.clone() - tradable_reserves_base.clone()) / total_fee_mult; + let quote = tradable_reserves_quote - y1.clone(); + + return ( + ::try_from(quote.to_precision(0).value.to_int().value()).unwrap(), + ::try_from(base.to_precision(0).value.to_int().value()).unwrap(), + ); + } + fn available_liquidity_by_spot_impact(&self, max_spot_price_impact: Side>) -> (u64, u64) { + // "max_spot_price_impact" is calculated as + // "max_spot_price_impact = 1 - market_price_new / market_price" to be always > 0. + // Outputs are ("quote_amount_available", "base_amount_required"). + // Outputs reflects how many quote asset the user will receive and how many base asset + // must be added to the pool in order for this operation to occur with a given + // "max_spot_price_impact" relative to the current state of the pool. + // Note: all calculations are made taking fees into account, thus "max_spot_price_impact" + // must also include fees in "market_price" calculation. + const BN_ONE: BigNumber = BigNumber { value: DBig::ONE }; + let sqrt_degree = BigNumber::from(0.5); + + let (tradable_reserves_base, tradable_reserves_quote, total_fee_mult) = match max_spot_price_impact { + Side::Bid(_) => ( + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from((self.lp_fee_y - self.treasury_fee).to_f64().unwrap()), + ), + Side::Ask(_) => ( + BigNumber::from((self.reserves_x - self.treasury_x).untag() as f64), + BigNumber::from((self.reserves_y - self.treasury_y).untag() as f64), + BigNumber::from((self.lp_fee_x - self.treasury_fee).to_f64().unwrap()), + ), + }; + + let lq_balance = (tradable_reserves_base.clone() * tradable_reserves_quote.clone()).pow(&sqrt_degree); + + let market_price = tradable_reserves_quote + .clone() + .div(tradable_reserves_base.clone()) + * total_fee_mult.clone(); + + let impact_price = market_price + * (BN_ONE + - BigNumber::from(*max_spot_price_impact.unwrap().numer() as f64) + .div(BigNumber::from(*max_spot_price_impact.unwrap().denom() as f64))) + .div(total_fee_mult.clone()); + + let p1_sqrt = impact_price.clone().pow(&sqrt_degree); + let x1 = lq_balance.clone() / p1_sqrt.clone(); + let y1 = lq_balance.clone() * p1_sqrt.clone(); + + let base = (x1.clone() - tradable_reserves_base.clone()) / total_fee_mult; + let quote = tradable_reserves_quote - y1.clone(); + + return ( + ::try_from(quote.to_precision(0).value.to_int().value()).unwrap(), + ::try_from(base.to_precision(0).value.to_int().value()).unwrap(), + ); + } } impl Has for ConstFnPool { @@ -718,20 +818,29 @@ impl ApplyOrder for ConstFnPool { } mod tests { - use crate::data::balance_pool::{BalancePool, BalancePoolRedeemer, BalancePoolVer}; - use crate::data::cfmm_pool::{ConstFnPool, ConstFnPoolVer}; - use crate::data::pool::CFMMPoolAction; - use crate::data::PoolId; - use bloom_offchain::execution_engine::liquidity_book::pool::Pool; - use bloom_offchain::execution_engine::liquidity_book::side::Side; use cml_crypto::ScriptHash; use num_rational::Ratio; + + use bloom_offchain::execution_engine::liquidity_book::market_maker::MarketMaker; + use bloom_offchain::execution_engine::liquidity_book::side::Side; + use bloom_offchain::execution_engine::liquidity_book::side::Side::{Ask, Bid}; use spectrum_cardano_lib::ex_units::ExUnits; use spectrum_cardano_lib::{AssetClass, AssetName, TaggedAmount, TaggedAssetClass}; - #[test] - fn treasury_x_test() { - let pool = ConstFnPool { + use crate::data::cfmm_pool::{ConstFnPool, ConstFnPoolVer}; + use crate::data::PoolId; + + fn gen_ada_token_pool( + reserves_x: u64, + reserves_y: u64, + liquidity: u64, + lp_fee_x: u64, + lp_fee_y: u64, + treasury_fee: u64, + treasury_x: u64, + treasury_y: u64, + ) -> ConstFnPool { + return ConstFnPool { id: PoolId::from(( ScriptHash::from([ 162, 206, 112, 95, 150, 240, 52, 167, 61, 102, 158, 92, 11, 47, 25, 41, 48, 224, 188, @@ -745,9 +854,9 @@ mod tests { ], )), )), - reserves_x: TaggedAmount::new(1632109645), - reserves_y: TaggedAmount::new(1472074052), - liquidity: TaggedAmount::new(0), + reserves_x: TaggedAmount::new(reserves_x), + reserves_y: TaggedAmount::new(reserves_y), + liquidity: TaggedAmount::new(liquidity), asset_x: TaggedAssetClass::new(AssetClass::Native), asset_y: TaggedAssetClass::new(AssetClass::Token(( ScriptHash::from([ @@ -775,16 +884,21 @@ mod tests { ], )), ))), - lp_fee_x: Ratio::new_raw(99970, 100000), - lp_fee_y: Ratio::new_raw(99970, 100000), - treasury_fee: Ratio::new_raw(10, 100000), - treasury_x: TaggedAmount::new(11500), - treasury_y: TaggedAmount::new(2909), + lp_fee_x: Ratio::new_raw(lp_fee_x, 100000), + lp_fee_y: Ratio::new_raw(lp_fee_y, 100000), + treasury_fee: Ratio::new_raw(treasury_fee, 100000), + treasury_x: TaggedAmount::new(treasury_x), + treasury_y: TaggedAmount::new(treasury_y), lq_lower_bound: TaggedAmount::new(0), ver: ConstFnPoolVer::FeeSwitch, marginal_cost: ExUnits { mem: 100, steps: 100 }, min_pool_lovelace: 10000000, }; + } + + #[test] + fn treasury_x_test() { + let pool = gen_ada_token_pool(1632109645, 1472074052, 0, 99970, 99970, 10, 11500, 2909); let (_, new_pool) = pool.clone().swap(Side::Ask(900000000)); @@ -792,4 +906,33 @@ mod tests { assert_eq!(new_pool.treasury_x.untag(), correct_x_treasury) } + + #[test] + fn available_liquidity_test() { + let fee_num = 98500; + let reserves_x = 1116854094529; + let reserves_y = 4602859113047; + + let pool = gen_ada_token_pool(reserves_x, reserves_y, 0, fee_num, fee_num, 0, 0, 0); + + // Repair available volumes from average sell price impact. + let max_target_price_impact = Ratio::new_raw(45035996273705, 4503599627370496); + + let (quote_qty_ask, _) = pool.available_liquidity_by_user_impact(Ask(max_target_price_impact)); + + let (quote_qty_bid, _) = pool.available_liquidity_by_user_impact(Bid(max_target_price_impact)); + + assert_eq!(quote_qty_ask, 46028591130); + assert_eq!(quote_qty_bid, 11168540945); + + // Repair available volumes from pool spot price impact. + let max_spot_price_impact = Ratio::new_raw(179243265169347, 9007199254740992); + + let (quote_qty_ask_spot, _) = pool.available_liquidity_by_spot_impact(Ask(max_spot_price_impact)); + + let (quote_qty_bid_spot, _) = pool.available_liquidity_by_spot_impact(Bid(max_spot_price_impact)); + + assert_eq!(quote_qty_ask_spot, 46028591130); + assert_eq!(quote_qty_bid_spot, 11168540945) + } } diff --git a/spectrum-offchain-cardano/src/data/pool.rs b/spectrum-offchain-cardano/src/data/pool.rs index 99f993ac6..e673b1eea 100644 --- a/spectrum-offchain-cardano/src/data/pool.rs +++ b/spectrum-offchain-cardano/src/data/pool.rs @@ -17,10 +17,11 @@ use cml_chain::{Coin, PolicyId}; use cml_multi_era::babbage::BabbageTransactionOutput; use log::info; +use num_rational::Ratio; use tracing_subscriber::filter::combinator::Or; use bloom_offchain::execution_engine::bundled::Bundled; -use bloom_offchain::execution_engine::liquidity_book::pool::{Pool, PoolQuality, StaticPrice}; +use bloom_offchain::execution_engine::liquidity_book::market_maker::{MarketMaker, PoolQuality, SpotPrice}; use bloom_offchain::execution_engine::liquidity_book::side::Side; use bloom_offchain::execution_engine::liquidity_book::types::AbsolutePrice; use spectrum_cardano_lib::collateral::Collateral; @@ -242,9 +243,9 @@ pub struct AssetDeltas { pub asset_to_add_to: AssetClass, } -impl Pool for AnyPool { +impl MarketMaker for AnyPool { type U = ExUnits; - fn static_price(&self) -> StaticPrice { + fn static_price(&self) -> SpotPrice { match self { PureCFMM(p) => p.static_price(), BalancedCFMM(p) => p.static_price(), @@ -285,10 +286,24 @@ impl Pool for AnyPool { } } - fn swaps_allowed(&self) -> bool { + fn available_liquidity_by_spot_impact(&self, max_spot_price_impact: Side>) -> (u64, u64) { match self { - PureCFMM(p) => p.swaps_allowed(), - BalancedCFMM(p) => p.swaps_allowed(), + PureCFMM(p) => p.available_liquidity_by_spot_impact(max_spot_price_impact), + BalancedCFMM(p) => p.available_liquidity_by_spot_impact(max_spot_price_impact), + } + } + + fn available_liquidity_by_user_impact(&self, max_user_price_impact: Side>) -> (u64, u64) { + match self { + PureCFMM(p) => p.available_liquidity_by_user_impact(max_user_price_impact), + BalancedCFMM(p) => p.available_liquidity_by_user_impact(max_user_price_impact), + } + } + + fn is_active(&self) -> bool { + match self { + PureCFMM(p) => p.is_active(), + BalancedCFMM(p) => p.is_active(), } } } @@ -521,7 +536,7 @@ pub mod tests { use cml_crypto::TransactionHash; use rand::Rng; - use bloom_offchain::execution_engine::liquidity_book::{pool::Pool, side::Side}; + use bloom_offchain::execution_engine::liquidity_book::{market_maker::MarketMaker, side::Side}; use spectrum_cardano_lib::OutputRef; use spectrum_offchain::ledger::TryFromLedger;