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 calculationsalgorithm: Standard algorithms likeremove_iffor 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:
- Liquidity share: How much liquidity each position provides
- Active status: Only active positions earn fees
- 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::dequefor frequent insertions/deletions - Price calculations: Cache liquidity calculations when possible
- Fee distribution: Batch fee distributions to reduce computation
Common Pitfalls
- Price range validation: Ensure
price_lower < price_upper - Division by zero: Check for zero liquidity before distributing fees
- 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
- Want to run two processes simultaneously? → go-script-run-processes