use ndarray::Array1;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use crate::error::{DigiFiError, ErrorTitle};
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LiquidityCurve {
pub x: Array1<f64>,
pub y: Array1<f64>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AMMToken {
pub id: String,
pub supply: f64,
pub fee_lower_bound: f64,
pub fee_upper_bound: f64,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AMMLiquidityPool {
token_1: AMMToken,
token_2: AMMToken,
char_number: f64,
tolerance: f64,
}
impl AMMLiquidityPool {
pub fn build(token_1: AMMToken, token_2: AMMToken, char_number: f64, tolerance: f64) -> Result<Self, DigiFiError> {
if char_number <= 0.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `char_number` must be positive.".to_owned(),
});
}
if tolerance < ((token_1.supply * token_2.supply) - char_number).abs() {
return Err(DigiFiError::ValidationError {
title: Self::error_title(),
details: "The argument `char_number` must be the product of supplies of the tokens.".to_owned(),
});
}
Ok(Self { token_1, token_2, char_number, tolerance })
}
pub fn token_1(&self) -> AMMToken {
self.token_1.clone()
}
pub fn token_2(&self) -> AMMToken {
self.token_2.clone()
}
pub fn char_number(&self) -> f64 {
self.char_number
}
pub fn update_token_supply(&mut self, token_1: AMMToken, token_2: AMMToken) -> Result<(), DigiFiError> {
if self.tolerance < ((token_1.supply * token_2.supply) - self.char_number).abs() {
return Err(DigiFiError::ValidationError {
title: Self::error_title(),
details: "The argument `char_number` must be the product of supplies of the tokens.".to_owned(),
});
}
if self.token_1.id != token_1.id {
return Err(DigiFiError::ParameterConstraint { title: Self::error_title(), constraint: "Wrong `token_1` id is provided.".to_owned(), });
}
if self.token_2.id != token_2.id {
return Err(DigiFiError::ParameterConstraint { title: Self::error_title(), constraint: "Wrong `token_2` id is provided.".to_owned(), });
}
self.token_1 = token_1;
self.token_2 = token_2;
Ok(())
}
}
impl ErrorTitle for AMMLiquidityPool {
fn error_title() -> String {
String::from("AMM Liquidity Pool")
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AMMTransactionData {
token_id: String,
quantity: f64,
percent_fee: f64,
}
impl AMMTransactionData {
pub fn build(token_id: String, quantity: f64, percent_fee: f64) -> Result<Self, DigiFiError> {
if quantity <= 0.0 {
return Err(DigiFiError::ParameterConstraint { title: Self::error_title(), constraint: "The argument `quantity` must be positive.".to_owned(), });
}
if percent_fee < 0.0 {
return Err(DigiFiError::ParameterConstraint { title: Self::error_title(), constraint: "The argument `percent_fee` must be non-negative.".to_owned(), });
}
Ok(Self { token_id, quantity, percent_fee })
}
pub fn token_id(&self) -> &str {
&self.token_id
}
pub fn quantity(&self) -> f64 {
self.quantity
}
pub fn percent_fee(&self) -> f64 {
self.percent_fee
}
}
impl ErrorTitle for AMMTransactionData {
fn error_title() -> String {
String::from("AMM Transaction Data")
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AMMTransactionResult {
pub quantity_to_sell: f64,
pub exchange_price: f64,
pub fee_in_purchased_token: f64,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SimpleAMM {
liquidity_pool: AMMLiquidityPool,
}
impl SimpleAMM {
pub fn new(liquidity_pool: AMMLiquidityPool) -> Self {
Self { liquidity_pool, }
}
pub fn make_transaction(&mut self, tx_data: AMMTransactionData) -> Result<AMMTransactionResult, DigiFiError> {
let mut token_1: AMMToken = self.liquidity_pool.token_1();
let mut token_2: AMMToken = self.liquidity_pool.token_2();
let (token_id, token_supply, token_fee_lower_bound, token_fee_upper_bound, counterparty_token_supply) = {
if tx_data.token_id() == &token_1.id {
(&token_1.id, token_1.supply, token_1.fee_lower_bound, token_1.fee_upper_bound, token_2.supply)
} else if tx_data.token_id() == &token_2.id {
(&token_2.id, token_2.supply, token_2.fee_lower_bound, token_2.fee_upper_bound, token_1.supply)
} else {
return Err(DigiFiError::NotFound { title: Self::error_title(), data: format!("token with identifier {}", tx_data.token_id()), });
}
};
let tx_buy_size: f64 = tx_data.quantity() * (1.0 + tx_data.percent_fee());
if token_supply < tx_buy_size {
return Err(DigiFiError::Other {
title: Self::error_title(),
details: format!("Not enough supply of token `{}` (`{}`) to fill in the buy order of `{}`.", token_id, token_supply, tx_buy_size),
});
}
if (tx_data.percent_fee() < token_fee_lower_bound) || (token_fee_upper_bound < tx_data.percent_fee()) {
return Err(DigiFiError::Other {
title: Self::error_title(),
details: format!("The argument `percent_fee` must be in the range [{}, {}].", token_fee_lower_bound, token_fee_upper_bound),
});
}
let updated_token_supply: f64 = token_supply - tx_buy_size;
let updated_counterparty_token_supply: f64 = self.liquidity_pool.char_number() / updated_token_supply;
let dx: f64 = updated_counterparty_token_supply - counterparty_token_supply;
let price: f64 = updated_counterparty_token_supply / updated_token_supply;
let fee: f64 = tx_data.quantity() * tx_data.percent_fee() * price;
if tx_data.token_id() == &token_1.id {
token_1.supply = updated_token_supply;
token_2.supply = updated_counterparty_token_supply;
} else {
token_1.supply = updated_counterparty_token_supply;
token_2.supply = updated_token_supply;
}
self.liquidity_pool.update_token_supply(token_1, token_2)?;
Ok(AMMTransactionResult { quantity_to_sell: dx, exchange_price: price, fee_in_purchased_token: fee })
}
pub fn get_liquidity_curve(&self, n_points: usize, token_1_start: f64, token_1_end: f64) -> LiquidityCurve {
let x: Array1<f64> = Array1::linspace(token_1_start, token_1_end, n_points);
let y: Array1<f64> = self.liquidity_pool.char_number() / &x;
LiquidityCurve { x, y, }
}
}
impl ErrorTitle for SimpleAMM {
fn error_title() -> String {
String::from("Simple AMM")
}
}
#[cfg(test)]
mod tests {
#[test]
fn unit_test_simple_amm_make_transaction() -> () {
use crate::market_making::amm::{AMMToken, AMMLiquidityPool, AMMTransactionData, AMMTransactionResult, SimpleAMM};
let token_1: AMMToken = AMMToken { id: String::from("BTC"), supply: 10.0, fee_lower_bound: 0.0, fee_upper_bound: 0.03 };
let token_2: AMMToken = AMMToken { id: String::from("ETH"), supply: 1_000.0, fee_lower_bound: 0.0, fee_upper_bound: 0.03 };
let liquidity_pool: AMMLiquidityPool = AMMLiquidityPool::build(token_1, token_2, 10_000.0, 0.00001).unwrap();
let tx_data: AMMTransactionData = AMMTransactionData::build(String::from("BTC"), 1.0, 0.01).unwrap();
let mut amm: SimpleAMM = SimpleAMM::new(liquidity_pool);
let receipt: AMMTransactionResult = amm.make_transaction(tx_data).unwrap();
assert_eq!(receipt.quantity_to_sell, 10_000.0/8.99 - 1_000.0);
assert_eq!(receipt.exchange_price, (10_000.0/8.99) / 8.99);
}
}