use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use crate::error::{CoreError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TickSpacing {
VeryFine = 1,
Fine = 5,
Medium = 10,
Coarse = 30,
VeryCoarse = 100,
}
impl TickSpacing {
pub fn value(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FeeTier {
VeryLow = 1,
Low = 5,
Medium = 30,
High = 100,
}
impl FeeTier {
pub fn as_decimal(&self) -> Decimal {
match self {
FeeTier::VeryLow => dec!(0.0001),
FeeTier::Low => dec!(0.0005),
FeeTier::Medium => dec!(0.003),
FeeTier::High => dec!(0.01),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConcentratedPosition {
pub id: String,
pub owner: String,
pub pool_id: String,
pub tick_lower: i32,
pub tick_upper: i32,
pub liquidity: Decimal,
pub amount0: Decimal,
pub amount1: Decimal,
pub fees_earned0: Decimal,
pub fees_earned1: Decimal,
pub fee_growth_inside0_last: Decimal,
pub fee_growth_inside1_last: Decimal,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ConcentratedPosition {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
owner: String,
pool_id: String,
tick_lower: i32,
tick_upper: i32,
liquidity: Decimal,
amount0: Decimal,
amount1: Decimal,
) -> Result<Self> {
if tick_lower >= tick_upper {
return Err(CoreError::Validation(
"Lower tick must be less than upper tick".to_string(),
));
}
if liquidity <= dec!(0) {
return Err(CoreError::Validation(
"Liquidity must be positive".to_string(),
));
}
let now = Utc::now();
Ok(Self {
id,
owner,
pool_id,
tick_lower,
tick_upper,
liquidity,
amount0,
amount1,
fees_earned0: dec!(0),
fees_earned1: dec!(0),
fee_growth_inside0_last: dec!(0),
fee_growth_inside1_last: dec!(0),
created_at: now,
updated_at: now,
})
}
pub fn is_in_range(&self, current_tick: i32) -> bool {
current_tick >= self.tick_lower && current_tick < self.tick_upper
}
pub fn update_fees(&mut self, fee_growth_inside0: Decimal, fee_growth_inside1: Decimal) {
let fee_growth_delta0 = fee_growth_inside0 - self.fee_growth_inside0_last;
let fee_growth_delta1 = fee_growth_inside1 - self.fee_growth_inside1_last;
self.fees_earned0 += self.liquidity * fee_growth_delta0;
self.fees_earned1 += self.liquidity * fee_growth_delta1;
self.fee_growth_inside0_last = fee_growth_inside0;
self.fee_growth_inside1_last = fee_growth_inside1;
self.updated_at = Utc::now();
}
pub fn collect_fees(&mut self) -> (Decimal, Decimal) {
let fees0 = self.fees_earned0;
let fees1 = self.fees_earned1;
self.fees_earned0 = dec!(0);
self.fees_earned1 = dec!(0);
self.updated_at = Utc::now();
(fees0, fees1)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tick {
pub index: i32,
pub liquidity_gross: Decimal,
pub liquidity_net: Decimal,
pub fee_growth_outside0: Decimal,
pub fee_growth_outside1: Decimal,
pub initialized: bool,
}
impl Tick {
pub fn new(index: i32) -> Self {
Self {
index,
liquidity_gross: dec!(0),
liquidity_net: dec!(0),
fee_growth_outside0: dec!(0),
fee_growth_outside1: dec!(0),
initialized: false,
}
}
pub fn update(&mut self, liquidity_delta: Decimal, upper: bool) {
self.liquidity_gross += liquidity_delta.abs();
if upper {
self.liquidity_net -= liquidity_delta;
} else {
self.liquidity_net += liquidity_delta;
}
self.initialized = self.liquidity_gross != dec!(0);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConcentratedLiquidityPool {
pub id: String,
pub token0: String,
pub token1: String,
pub fee_tier: FeeTier,
pub tick_spacing: TickSpacing,
pub current_tick: i32,
pub sqrt_price: Decimal,
pub liquidity: Decimal,
pub reserve0: Decimal,
pub reserve1: Decimal,
pub fee_growth_global0: Decimal,
pub fee_growth_global1: Decimal,
pub ticks: BTreeMap<i32, Tick>,
pub tvl: Decimal,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ConcentratedLiquidityPool {
pub fn new(
id: String,
token0: String,
token1: String,
fee_tier: FeeTier,
tick_spacing: TickSpacing,
initial_sqrt_price: Decimal,
) -> Self {
let current_tick = Self::sqrt_price_to_tick(initial_sqrt_price);
let now = Utc::now();
Self {
id,
token0,
token1,
fee_tier,
tick_spacing,
current_tick,
sqrt_price: initial_sqrt_price,
liquidity: dec!(0),
reserve0: dec!(0),
reserve1: dec!(0),
fee_growth_global0: dec!(0),
fee_growth_global1: dec!(0),
ticks: BTreeMap::new(),
tvl: dec!(0),
created_at: now,
updated_at: now,
}
}
fn sqrt_price_to_tick(sqrt_price: Decimal) -> i32 {
let price = sqrt_price * sqrt_price;
if price <= dec!(0) {
return 0;
}
let log_price = (price.to_string().parse::<f64>().unwrap_or(1.0)).ln();
let log_base = 1.0001_f64.ln();
(log_price / log_base) as i32
}
fn tick_to_sqrt_price(tick: i32) -> Decimal {
let price = 1.0001_f64.powi(tick);
Decimal::from_f64_retain(price.sqrt()).unwrap_or(dec!(1))
}
pub fn add_liquidity(
&mut self,
tick_lower: i32,
tick_upper: i32,
amount0_desired: Decimal,
amount1_desired: Decimal,
) -> Result<(Decimal, Decimal, Decimal)> {
if tick_lower % self.tick_spacing.value() != 0
|| tick_upper % self.tick_spacing.value() != 0
{
return Err(CoreError::Validation(
"Ticks must be aligned to tick spacing".to_string(),
));
}
let liquidity =
self.calculate_liquidity(tick_lower, tick_upper, amount0_desired, amount1_desired)?;
self.update_tick(tick_lower, liquidity, false)?;
self.update_tick(tick_upper, liquidity, true)?;
if self.current_tick >= tick_lower && self.current_tick < tick_upper {
self.liquidity += liquidity;
}
let (amount0, amount1) = self.calculate_amounts(tick_lower, tick_upper, liquidity)?;
self.reserve0 += amount0;
self.reserve1 += amount1;
self.tvl = self.reserve0 + self.reserve1;
self.updated_at = Utc::now();
Ok((liquidity, amount0, amount1))
}
fn calculate_liquidity(
&self,
tick_lower: i32,
tick_upper: i32,
amount0: Decimal,
amount1: Decimal,
) -> Result<Decimal> {
let sqrt_price_lower = Self::tick_to_sqrt_price(tick_lower);
let sqrt_price_upper = Self::tick_to_sqrt_price(tick_upper);
let liquidity_from_amount1 = if sqrt_price_upper > sqrt_price_lower {
amount1 / (sqrt_price_upper - sqrt_price_lower)
} else {
dec!(0)
};
let liquidity_from_amount0 = if sqrt_price_upper > dec!(0) && sqrt_price_lower > dec!(0) {
amount0 * sqrt_price_upper * sqrt_price_lower / (sqrt_price_upper - sqrt_price_lower)
} else {
dec!(0)
};
Ok(liquidity_from_amount0
.min(liquidity_from_amount1)
.max(dec!(1)))
}
fn calculate_amounts(
&self,
tick_lower: i32,
tick_upper: i32,
liquidity: Decimal,
) -> Result<(Decimal, Decimal)> {
let sqrt_price_lower = Self::tick_to_sqrt_price(tick_lower);
let sqrt_price_upper = Self::tick_to_sqrt_price(tick_upper);
let sqrt_price_current = self.sqrt_price;
let amount0;
let amount1;
if self.current_tick < tick_lower {
amount0 = liquidity * (sqrt_price_upper - sqrt_price_lower)
/ (sqrt_price_upper * sqrt_price_lower);
amount1 = dec!(0);
} else if self.current_tick >= tick_upper {
amount0 = dec!(0);
amount1 = liquidity * (sqrt_price_upper - sqrt_price_lower);
} else {
amount0 = liquidity * (sqrt_price_upper - sqrt_price_current)
/ (sqrt_price_upper * sqrt_price_current);
amount1 = liquidity * (sqrt_price_current - sqrt_price_lower);
}
Ok((amount0.max(dec!(0)), amount1.max(dec!(0))))
}
fn update_tick(&mut self, tick: i32, liquidity_delta: Decimal, upper: bool) -> Result<()> {
let tick_data = self.ticks.entry(tick).or_insert_with(|| Tick::new(tick));
tick_data.update(liquidity_delta, upper);
Ok(())
}
pub fn swap(&mut self, zero_for_one: bool, amount_in: Decimal) -> Result<Decimal> {
if amount_in <= dec!(0) {
return Err(CoreError::Validation(
"Amount in must be positive".to_string(),
));
}
let fee_amount = amount_in * self.fee_tier.as_decimal();
let amount_in_less_fee = amount_in - fee_amount;
let amount_out = if zero_for_one {
if self.reserve0 == dec!(0) || self.liquidity == dec!(0) {
return Err(CoreError::Validation("Insufficient liquidity".to_string()));
}
let amount_out =
(self.reserve1 * amount_in_less_fee) / (self.reserve0 + amount_in_less_fee);
self.reserve0 += amount_in;
self.reserve1 -= amount_out;
if self.liquidity > dec!(0) {
self.fee_growth_global0 += fee_amount / self.liquidity;
}
amount_out
} else {
if self.reserve1 == dec!(0) || self.liquidity == dec!(0) {
return Err(CoreError::Validation("Insufficient liquidity".to_string()));
}
let amount_out =
(self.reserve0 * amount_in_less_fee) / (self.reserve1 + amount_in_less_fee);
self.reserve1 += amount_in;
self.reserve0 -= amount_out;
if self.liquidity > dec!(0) {
self.fee_growth_global1 += fee_amount / self.liquidity;
}
amount_out
};
if self.reserve0 > dec!(0) && self.reserve1 > dec!(0) {
let new_price = self.reserve1 / self.reserve0;
self.sqrt_price = Decimal::from_f64_retain(
new_price.to_string().parse::<f64>().unwrap_or(1.0).sqrt(),
)
.unwrap_or(dec!(1));
self.current_tick = Self::sqrt_price_to_tick(self.sqrt_price);
}
self.updated_at = Utc::now();
Ok(amount_out)
}
pub fn current_price(&self) -> Decimal {
self.sqrt_price * self.sqrt_price
}
pub fn calculate_fee_growth_inside(
&self,
tick_lower: i32,
tick_upper: i32,
) -> (Decimal, Decimal) {
let lower_tick = self.ticks.get(&tick_lower);
let upper_tick = self.ticks.get(&tick_upper);
let fee_growth_below0 = lower_tick.map(|t| t.fee_growth_outside0).unwrap_or(dec!(0));
let fee_growth_below1 = lower_tick.map(|t| t.fee_growth_outside1).unwrap_or(dec!(0));
let fee_growth_above0 = upper_tick.map(|t| t.fee_growth_outside0).unwrap_or(dec!(0));
let fee_growth_above1 = upper_tick.map(|t| t.fee_growth_outside1).unwrap_or(dec!(0));
let fee_growth_inside0 = self.fee_growth_global0 - fee_growth_below0 - fee_growth_above0;
let fee_growth_inside1 = self.fee_growth_global1 - fee_growth_below1 - fee_growth_above1;
(fee_growth_inside0, fee_growth_inside1)
}
}
#[derive(Debug)]
pub struct ConcentratedLiquidityManager {
pools: HashMap<String, ConcentratedLiquidityPool>,
positions: HashMap<String, ConcentratedPosition>,
positions_by_owner: HashMap<String, Vec<String>>,
next_position_id: u64,
}
impl ConcentratedLiquidityManager {
pub fn new() -> Self {
Self {
pools: HashMap::new(),
positions: HashMap::new(),
positions_by_owner: HashMap::new(),
next_position_id: 1,
}
}
pub fn create_pool(
&mut self,
id: String,
token0: String,
token1: String,
fee_tier: FeeTier,
tick_spacing: TickSpacing,
initial_sqrt_price: Decimal,
) -> Result<()> {
if self.pools.contains_key(&id) {
return Err(CoreError::Validation("Pool already exists".to_string()));
}
let pool = ConcentratedLiquidityPool::new(
id.clone(),
token0,
token1,
fee_tier,
tick_spacing,
initial_sqrt_price,
);
self.pools.insert(id, pool);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn mint_position(
&mut self,
owner: String,
pool_id: String,
tick_lower: i32,
tick_upper: i32,
amount0_desired: Decimal,
amount1_desired: Decimal,
) -> Result<String> {
let pool = self
.pools
.get_mut(&pool_id)
.ok_or_else(|| CoreError::Validation("Pool not found".to_string()))?;
let (liquidity, amount0, amount1) =
pool.add_liquidity(tick_lower, tick_upper, amount0_desired, amount1_desired)?;
let position_id = format!("pos-{}", self.next_position_id);
self.next_position_id += 1;
let position = ConcentratedPosition::new(
position_id.clone(),
owner.clone(),
pool_id,
tick_lower,
tick_upper,
liquidity,
amount0,
amount1,
)?;
self.positions.insert(position_id.clone(), position);
self.positions_by_owner
.entry(owner)
.or_default()
.push(position_id.clone());
Ok(position_id)
}
pub fn get_positions_by_owner(&self, owner: &str) -> Vec<&ConcentratedPosition> {
self.positions_by_owner
.get(owner)
.map(|ids| ids.iter().filter_map(|id| self.positions.get(id)).collect())
.unwrap_or_default()
}
pub fn get_pool(&self, pool_id: &str) -> Option<&ConcentratedLiquidityPool> {
self.pools.get(pool_id)
}
pub fn swap(
&mut self,
pool_id: &str,
zero_for_one: bool,
amount_in: Decimal,
) -> Result<Decimal> {
let pool = self
.pools
.get_mut(pool_id)
.ok_or_else(|| CoreError::Validation("Pool not found".to_string()))?;
pool.swap(zero_for_one, amount_in)
}
}
impl Default for ConcentratedLiquidityManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_concentrated_position_creation() {
let position = ConcentratedPosition::new(
"pos1".to_string(),
"user1".to_string(),
"pool1".to_string(),
-100,
100,
dec!(1000),
dec!(100),
dec!(100),
)
.unwrap();
assert_eq!(position.liquidity, dec!(1000));
assert_eq!(position.tick_lower, -100);
assert_eq!(position.tick_upper, 100);
}
#[test]
fn test_position_in_range() {
let position = ConcentratedPosition::new(
"pos1".to_string(),
"user1".to_string(),
"pool1".to_string(),
-100,
100,
dec!(1000),
dec!(100),
dec!(100),
)
.unwrap();
assert!(position.is_in_range(0));
assert!(position.is_in_range(-50));
assert!(position.is_in_range(50));
assert!(!position.is_in_range(-101));
assert!(!position.is_in_range(100));
}
#[test]
fn test_pool_creation() {
let pool = ConcentratedLiquidityPool::new(
"pool1".to_string(),
"USDC".to_string(),
"USDT".to_string(),
FeeTier::Low,
TickSpacing::Fine,
dec!(1),
);
assert_eq!(pool.token0, "USDC");
assert_eq!(pool.token1, "USDT");
assert_eq!(pool.liquidity, dec!(0));
}
#[test]
fn test_manager_create_pool() {
let mut manager = ConcentratedLiquidityManager::new();
manager
.create_pool(
"pool1".to_string(),
"USDC".to_string(),
"USDT".to_string(),
FeeTier::Low,
TickSpacing::Fine,
dec!(1),
)
.unwrap();
assert!(manager.get_pool("pool1").is_some());
}
#[test]
fn test_mint_position() {
let mut manager = ConcentratedLiquidityManager::new();
manager
.create_pool(
"pool1".to_string(),
"USDC".to_string(),
"USDT".to_string(),
FeeTier::Low,
TickSpacing::Medium,
dec!(1),
)
.unwrap();
let position_id = manager
.mint_position(
"user1".to_string(),
"pool1".to_string(),
-100,
100,
dec!(1000),
dec!(1000),
)
.unwrap();
assert!(position_id.starts_with("pos-"));
let positions = manager.get_positions_by_owner("user1");
assert_eq!(positions.len(), 1);
}
}