Skip to main content

C++ Trading - Building a DEX Market Maker: Simulating Uniswap V3 Liquidity Provision

Decentralized exchanges (DEXs) like Uniswap V3 revolutionized liquidity provision by allowing market makers to provide liquidity in specific price ranges, rather than across the entire price curve. This article demonstrates how to build a DEX market maker that simulates liquidity provision in price bands, showing how liquidity providers earn fees and how liquidity distribution changes as prices move.

Overview

This implementation demonstrates:

  • Concentrated liquidity in price ranges (like Uniswap V3)
  • Fee distribution to active liquidity positions
  • Dynamic liquidity allocation based on current price
  • Trade execution with price impact
  • Position tracking and analytics

Project Structure

Required Headers

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <iomanip>
#include <cmath>
#include <algorithm>
  • cmath: Mathematical operations for liquidity calculations
  • algorithm: Standard algorithms like remove_if for position management

Data Structures

LiquidityPosition Struct

struct LiquidityPosition {
int id;
double price_lower;
double price_upper;
double amount_token0; // e.g., USDC
double amount_token1; // e.g., BTC
double fees_earned;

LiquidityPosition(int i, double lower, double upper,
double token0, double token1)
: id(i), price_lower(lower), price_upper(upper),
amount_token0(token0), amount_token1(token1), fees_earned(0.0) {}
};

Represents a single liquidity position in a specific price range.

Trade Struct

struct Trade {
double price;
double amount;
bool is_buy;
double fee_paid;

Trade(double p, double amt, bool buy, double fee)
: price(p), amount(amt), is_buy(buy), fee_paid(fee) {}
};

Tracks individual trades for analysis.

DEXMarketMaker Class

Private Members

class DEXMarketMaker {
private:
double current_price = 50000.0; // Current pool price
std::vector<LiquidityPosition> positions;
double fee_rate = 0.003; // 0.3% fee (Uniswap V3 style)
double total_fees_earned = 0.0;
std::vector<Trade> trade_history;

Key parameters:

  • current_price: Current market price (e.g., BTC/USDC)
  • fee_rate: Trading fee percentage (0.3% = Uniswap V3 standard)

Helper Methods

Checking Price Range

bool isPriceInRange(double price, const LiquidityPosition& pos) const {
return price >= pos.price_lower && price <= pos.price_upper;
}

Determines if a position is active at the current price.

Calculating Liquidity Distribution

void calculateLiquidityDistribution(LiquidityPosition& pos, double price) {
if (!isPriceInRange(price, pos)) {
// Price outside range - all liquidity in one token
if (price < pos.price_lower) {
// All in token1 (BTC)
pos.amount_token0 = 0.0;
pos.amount_token1 = (pos.amount_token0 * pos.price_lower +
pos.amount_token1 * price) / price;
} else {
// All in token0 (USDC)
pos.amount_token1 = 0.0;
pos.amount_token0 = pos.amount_token0 +
(pos.amount_token1 * pos.price_upper);
}
} else {
// Price in range - distribute proportionally
double range_size = pos.price_upper - pos.price_lower;
double price_from_lower = price - pos.price_lower;
double ratio = price_from_lower / range_size;

double total_value = pos.amount_token0 +
(pos.amount_token1 * price);

pos.amount_token0 = total_value * (1.0 - ratio);
pos.amount_token1 = (total_value - pos.amount_token0) / price;
}
}

Key concept: In Uniswap V3, liquidity distribution depends on where the current price is relative to the position's range:

  • Below range: All liquidity in token1 (e.g., BTC)
  • Above range: All liquidity in token0 (e.g., USDC)
  • In range: Distributed proportionally between both tokens

Distributing Fees

void distributeFees(double fee_amount, double price) {
// Calculate total active liquidity
double total_active_liquidity = 0.0;
for (auto& pos : positions) {
if (isPriceInRange(price, pos)) {
total_active_liquidity += pos.amount_token0 +
(pos.amount_token1 * price);
}
}

// Distribute fees proportionally
for (auto& pos : positions) {
if (isPriceInRange(price, pos)) {
double pos_liquidity = pos.amount_token0 +
(pos.amount_token1 * price);
double share = pos_liquidity / total_active_liquidity;
pos.fees_earned += fee_amount * share;
total_fees_earned += fee_amount * share;
}
}
}

Fees are distributed proportionally to active positions based on their liquidity share.

Public Methods

Adding Liquidity

void addLiquidity(double price_lower, double price_upper,
double amount_token0, double amount_token1) {
int new_id = positions.size() + 1;
LiquidityPosition new_pos(new_id, price_lower, price_upper,
amount_token0, amount_token1);

calculateLiquidityDistribution(new_pos, current_price);
positions.push_back(new_pos);

std::cout << "Added position #" << new_id
<< " in range [$" << std::fixed << std::setprecision(2)
<< price_lower << ", $" << price_upper << "]\n";
}

Removing Liquidity

void removeLiquidity(int position_id) {
positions.erase(
std::remove_if(positions.begin(), positions.end(),
[position_id](const LiquidityPosition& pos) {
return pos.id == position_id;
}),
positions.end()
);
std::cout << "Removed position #" << position_id << "\n";
}

Executing Trades

void executeTrade(double amount, bool is_buy) {
// Calculate price impact (simplified model)
double price_impact = amount * 0.0001; // 0.01% per unit

double new_price;
if (is_buy) {
new_price = current_price * (1.0 + price_impact);
} else {
new_price = current_price * (1.0 - price_impact);
}

// Calculate fee
double trade_value = amount * current_price;
double fee = trade_value * fee_rate;

// Record trade
trade_history.push_back(Trade(current_price, amount, is_buy, fee));

// Distribute fees to active positions
distributeFees(fee, current_price);

// Update price
current_price = new_price;

// Recalculate liquidity distribution for all positions
for (auto& pos : positions) {
calculateLiquidityDistribution(pos, current_price);
}

std::cout << (is_buy ? "BUY" : "SELL") << " " << amount
<< " @ $" << std::fixed << std::setprecision(2)
<< current_price << " (Fee: $" << fee << ")\n";
}

Note: This uses a simplified price impact model. Real Uniswap V3 uses the constant product formula (x * y = k), which creates non-linear price impact.

Updating Price (External Market Move)

void updatePrice(double new_price) {
current_price = new_price;

// Recalculate all positions
for (auto& pos : positions) {
calculateLiquidityDistribution(pos, current_price);
}

std::cout << "Price updated to $" << std::fixed
<< std::setprecision(2) << current_price << "\n";
}

Simulates external price movements (e.g., from another exchange).

Complete Example

int main() {
DEXMarketMaker mm;

std::cout << "=== DEX Market Maker Simulation ===\n";
std::cout << "Simulating liquidity provision in price bands (Uniswap V3 style)\n\n";

// Add liquidity positions
mm.addLiquidity(49000.0, 51000.0, 10000.0, 0.0); // Wide range, mostly USDC
mm.addLiquidity(49500.0, 50000.0, 5000.0, 0.0); // Narrow range
mm.addLiquidity(50000.0, 50500.0, 0.0, 0.1); // Narrow range, mostly BTC
mm.addLiquidity(45000.0, 55000.0, 20000.0, 0.0); // Very wide range

mm.printPositions();

// Execute trades
mm.executeTrade(0.05, true); // Small buy
mm.printPositions();

mm.executeTrade(0.1, false); // Medium sell
mm.printPositions();

mm.executeTrade(0.2, true); // Large buy
mm.printPositions();

// External price movement
mm.updatePrice(51500.0);
mm.printPositions();

mm.executeTrade(0.15, false); // Sell at new price
mm.printPositions();

mm.printSummary();

return 0;
}

Key Concepts

Concentrated Liquidity

Unlike Uniswap V2 (which spreads liquidity across all prices), V3 allows:

  • Capital efficiency: More liquidity in narrower ranges
  • Targeted strategies: Focus on specific price ranges
  • Higher fees: Narrower ranges earn more fees per dollar

Active vs. Inactive Positions

  • Active: Current price is within the position's range → earning fees
  • Inactive: Current price is outside the range → not earning fees

Fee Distribution

Fees are distributed proportionally based on:

  1. Liquidity share: How much liquidity each position provides
  2. Active status: Only active positions earn fees
  3. Time-weighted: Positions active longer earn more (in real systems)

Price Impact

Large trades move the price:

  • Buy orders: Push price up
  • Sell orders: Push price down
  • Impact increases with trade size

Real-World Considerations

Uniswap V3 Formula

Real Uniswap V3 uses the constant product formula with concentrated liquidity:

L = sqrt(x * y)

Where L is liquidity, x is token0, and y is token1. Our simplified version uses linear distribution for clarity.

Gas Costs

In production, consider:

  • Gas costs for adding/removing liquidity
  • Gas costs for trades
  • Optimization strategies (e.g., batching)

Impermanent Loss

When price moves outside your range:

  • You're no longer earning fees
  • Your capital is in the "wrong" token
  • You may experience impermanent loss

Extending the Implementation

Multiple Fee Tiers

enum class FeeTier {
LOW = 500, // 0.05%
MEDIUM = 3000, // 0.3%
HIGH = 10000 // 1.0%
};

Position Analytics

struct PositionStats {
double total_value;
double fees_per_dollar;
double utilization_rate;
};

Rebalancing Strategies

void rebalancePosition(int position_id, double new_lower, double new_upper) {
// Remove old position, add new one
}

Performance Considerations

  • Vector operations: Consider using std::deque for frequent insertions/deletions
  • Price calculations: Cache liquidity calculations when possible
  • Fee distribution: Batch fee distributions to reduce computation

Common Pitfalls

  1. Price range validation: Ensure price_lower < price_upper
  2. Division by zero: Check for zero liquidity before distributing fees
  3. Precision: Use appropriate precision for financial calculations (see our float precision article)

Conclusion

This DEX market maker demonstrates:

  • Concentrated liquidity in price bands
  • Dynamic fee distribution to active positions
  • Price impact modeling
  • Position management and analytics

Key takeaways:

  • Narrower ranges = higher capital efficiency but require more rebalancing
  • Only active positions earn fees
  • Price movements activate/deactivate positions
  • Fee distribution is proportional to liquidity share

From here, you can extend it with:

  • Real Uniswap V3 math (constant product formula)
  • Gas cost modeling
  • Impermanent loss calculations
  • Advanced rebalancing strategies
  • Multi-pool support

The foundation is solid for understanding how modern DEX liquidity provision works and building your own market-making strategies.


Read more from Cryptogrammar

Tools & Utilities