What You’ll Create
- Pool performance tracking (TVL, volume, fees)
- User portfolio analytics with P&L tracking
- Trading signal generation for opportunities
- Real-time monitoring and alerts
Analytics Example
Copy
import { DLMM, LiquidityBookServices } from '@saros-finance/dlmm-sdk';
import { Connection, PublicKey } from '@solana/web3.js';
import { BN } from '@coral-xyz/anchor';
// DLMM Analytics and Data Query Service
export class DLMMAnalytics {
private connection: Connection;
private services: LiquidityBookServices;
private cache: Map<string, { data: any; timestamp: number }> = new Map();
private cacheTTL = 30000; // 30 seconds
constructor(connection: Connection) {
this.connection = connection;
this.services = new LiquidityBookServices(connection);
}
/**
* Get comprehensive pool analytics
*/
async getPoolAnalytics(poolAddress: PublicKey) {
const cacheKey = `pool-analytics-${poolAddress.toString()}`;
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
console.log('Fetching pool analytics for:', poolAddress.toString());
try {
const pool = new DLMM(this.connection, poolAddress);
// Get basic pool information
const poolState = await pool.getPoolState();
const poolInfo = await pool.getPoolInfo();
// Get liquidity distribution across bins
const liquidityDistribution = await this.getLiquidityDistribution(pool, poolState.activeId);
// Calculate volume and fees (24h)
const volumeData = await this.calculate24hVolume(pool);
const feeData = await this.calculate24hFees(pool);
// Get price history
const priceHistory = await this.getPriceHistory(pool, '24h');
// Calculate TVL
const tvl = await this.calculateTVL(pool);
// Calculate APY/APR
const yieldMetrics = await this.calculateYieldMetrics(pool, feeData, tvl);
const analytics = {
pool: {
address: poolAddress.toString(),
tokenX: poolInfo.tokenX.toString(),
tokenY: poolInfo.tokenY.toString(),
binStep: poolInfo.binStep,
baseFactor: poolInfo.baseFactor,
filterPeriod: poolInfo.filterPeriod
},
state: {
activeId: poolState.activeId,
currentPrice: this.binIdToPrice(poolState.activeId, poolInfo.binStep),
reserveX: poolState.reserveX.toNumber(),
reserveY: poolState.reserveY.toNumber(),
liquiditySupply: poolState.liquiditySupply.toNumber(),
feeGrowthGlobalX: poolState.feeGrowthGlobalX.toNumber(),
feeGrowthGlobalY: poolState.feeGrowthGlobalY.toNumber()
},
metrics: {
tvl: tvl.total,
tvlBreakdown: tvl.breakdown,
volume24h: volumeData.volume,
fees24h: feeData.total,
feesBreakdown: feeData.breakdown,
apy: yieldMetrics.apy,
apr: yieldMetrics.apr,
utilization: this.calculateUtilization(liquidityDistribution)
},
liquidity: {
distribution: liquidityDistribution,
concentrationRatio: this.calculateConcentrationRatio(liquidityDistribution),
activeBins: liquidityDistribution.filter(bin => bin.liquidity > 0).length
},
price: {
current: this.binIdToPrice(poolState.activeId, poolInfo.binStep),
change24h: this.calculatePriceChange(priceHistory),
high24h: Math.max(...priceHistory.map(p => p.price)),
low24h: Math.min(...priceHistory.map(p => p.price)),
history: priceHistory
},
timestamp: Date.now()
};
this.setCache(cacheKey, analytics);
return analytics;
} catch (error) {
console.error('Failed to get pool analytics:', error);
throw error;
}
}
/**
* Get historical data for a pool
*/
async getPoolHistory(poolAddress: PublicKey, timeframe: '1h' | '24h' | '7d' | '30d') {
console.log(`Getting ${timeframe} history for pool:`, poolAddress.toString());
try {
const pool = new DLMM(this.connection, poolAddress);
const poolInfo = await pool.getPoolInfo();
// Generate historical data points
const dataPoints = this.generateHistoricalDataPoints(timeframe);
const history = [];
for (const timestamp of dataPoints) {
// In production, this would fetch actual historical data
// For now, we'll generate realistic mock data
const historicalData = await this.getHistoricalSnapshot(pool, timestamp);
history.push(historicalData);
}
return {
pool: poolAddress.toString(),
timeframe,
dataPoints: history.length,
data: history,
summary: {
avgVolume: history.reduce((sum, d) => sum + d.volume, 0) / history.length,
totalVolume: history.reduce((sum, d) => sum + d.volume, 0),
avgTVL: history.reduce((sum, d) => sum + d.tvl, 0) / history.length,
maxTVL: Math.max(...history.map(d => d.tvl)),
minTVL: Math.min(...history.map(d => d.tvl)),
priceVolatility: this.calculateVolatility(history.map(d => d.price))
}
};
} catch (error) {
console.error('Failed to get pool history:', error);
throw error;
}
}
/**
* Get user-specific analytics
*/
async getUserAnalytics(userAddress: PublicKey, pools?: PublicKey[]) {
console.log('Getting user analytics for:', userAddress.toString());
try {
const userPools = pools || await this.getUserPools(userAddress);
const userPositions = [];
const userSwapHistory = [];
let totalValue = 0;
let totalFeesEarned = 0;
let totalVolume = 0;
for (const poolAddress of userPools) {
const pool = new DLMM(this.connection, poolAddress);
// Get user positions
const positions = await pool.getPositionsByUser(userAddress);
for (const position of positions) {
const positionValue = await this.calculatePositionValue(pool, position);
const positionFees = await this.calculatePositionFees(pool, position);
userPositions.push({
pool: poolAddress.toString(),
position: position.address.toString(),
lowerBinId: position.lowerBinId,
upperBinId: position.upperBinId,
liquidityX: position.liquidityX.toNumber(),
liquidityY: position.liquidityY.toNumber(),
value: positionValue,
feesEarned: positionFees,
isInRange: this.isPositionInRange(pool, position),
createdAt: new Date(position.createdAt * 1000)
});
totalValue += positionValue.total;
totalFeesEarned += positionFees.total;
}
// Get user swap history
const swapHistory = await this.getUserSwapHistory(pool, userAddress);
userSwapHistory.push(...swapHistory);
totalVolume += swapHistory.reduce((sum, swap) => sum + swap.amountIn, 0);
}
return {
user: userAddress.toString(),
summary: {
totalPositions: userPositions.length,
totalValue,
totalFeesEarned,
totalVolume,
activePools: userPools.length,
avgPositionSize: totalValue / Math.max(userPositions.length, 1),
totalROI: totalFeesEarned / Math.max(totalValue - totalFeesEarned, 1) * 100
},
positions: userPositions,
swapHistory: userSwapHistory.slice(0, 100), // Last 100 swaps
breakdown: {
valueByPool: this.groupBy(userPositions, 'pool', pos => pos.value.total),
feesByPool: this.groupBy(userPositions, 'pool', pos => pos.feesEarned.total),
volumeByPool: this.groupBy(userSwapHistory, 'pool', swap => swap.amountIn)
},
timestamp: Date.now()
};
} catch (error) {
console.error('Failed to get user analytics:', error);
throw error;
}
}
/**
* Get market-wide DLMM statistics
*/
async getMarketStatistics() {
const cacheKey = 'market-statistics';
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
console.log('Fetching market-wide DLMM statistics...');
try {
// Get all DLMM pools
const allPools = await this.getAllPools();
let totalTVL = 0;
let total24hVolume = 0;
let total24hFees = 0;
let totalActiveBins = 0;
let totalPositions = 0;
const poolAnalytics = [];
for (const poolAddress of allPools) {
const analytics = await this.getPoolAnalytics(poolAddress);
totalTVL += analytics.metrics.tvl;
total24hVolume += analytics.metrics.volume24h;
total24hFees += analytics.metrics.fees24h;
totalActiveBins += analytics.liquidity.activeBins;
totalPositions += await this.getPoolPositionCount(poolAddress);
poolAnalytics.push({
address: poolAddress.toString(),
tvl: analytics.metrics.tvl,
volume24h: analytics.metrics.volume24h,
apy: analytics.metrics.apy,
utilization: analytics.metrics.utilization
});
}
// Sort pools by TVL
poolAnalytics.sort((a, b) => b.tvl - a.tvl);
const statistics = {
overview: {
totalPools: allPools.length,
totalTVL,
total24hVolume,
total24hFees,
avgAPY: poolAnalytics.reduce((sum, p) => sum + p.apy, 0) / poolAnalytics.length,
totalActiveBins,
totalPositions
},
topPools: {
byTVL: poolAnalytics.slice(0, 10),
byVolume: poolAnalytics.sort((a, b) => b.volume24h - a.volume24h).slice(0, 10),
byAPY: poolAnalytics.sort((a, b) => b.apy - a.apy).slice(0, 10)
},
metrics: {
avgPoolTVL: totalTVL / allPools.length,
avgPoolVolume: total24hVolume / allPools.length,
totalUtilization: totalActiveBins / (allPools.length * 100), // Assuming 100 bins per pool avg
feeToVolumeRatio: total24hFees / total24hVolume * 100
},
timestamp: Date.now()
};
this.setCache(cacheKey, statistics, 60000); // 1 minute cache
return statistics;
} catch (error) {
console.error('Failed to get market statistics:', error);
throw error;
}
}
/**
* Generate trading signals based on analytics
*/
async generateTradingSignals(poolAddress: PublicKey) {
console.log('Generating trading signals for:', poolAddress.toString());
try {
const analytics = await this.getPoolAnalytics(poolAddress);
const history = await this.getPoolHistory(poolAddress, '24h');
const signals = [];
// Volume-based signals
if (analytics.metrics.volume24h > history.summary.avgVolume * 2) {
signals.push({
type: 'high_volume',
strength: 'strong',
message: 'Unusually high trading volume detected',
confidence: 0.8
});
}
// Liquidity concentration signals
if (analytics.liquidity.concentrationRatio > 0.8) {
signals.push({
type: 'concentrated_liquidity',
strength: 'medium',
message: 'High liquidity concentration - tight spreads expected',
confidence: 0.7
});
}
// Price movement signals
const priceChange = analytics.price.change24h;
if (Math.abs(priceChange) > 10) {
signals.push({
type: 'price_movement',
strength: priceChange > 0 ? 'bullish' : 'bearish',
message: `Significant price movement: ${priceChange.toFixed(2)}%`,
confidence: Math.min(Math.abs(priceChange) / 20, 1)
});
}
// Yield opportunity signals
if (analytics.metrics.apy > 20 && analytics.metrics.utilization < 0.5) {
signals.push({
type: 'yield_opportunity',
strength: 'strong',
message: `High APY (${analytics.metrics.apy.toFixed(1)}%) with low utilization`,
confidence: 0.75
});
}
// Market inefficiency signals
const volatility = history.summary.priceVolatility;
if (volatility > 0.05 && analytics.liquidity.activeBins < 10) {
signals.push({
type: 'market_inefficiency',
strength: 'medium',
message: 'High volatility with sparse liquidity - arbitrage opportunity',
confidence: 0.6
});
}
return {
pool: poolAddress.toString(),
signals,
overallSentiment: this.calculateOverallSentiment(signals),
recommendations: this.generateRecommendations(analytics, signals),
timestamp: Date.now()
};
} catch (error) {
console.error('Failed to generate trading signals:', error);
throw error;
}
}
/**
* Real-time pool monitoring with WebSocket-like updates
*/
async startPoolMonitoring(poolAddresses: PublicKey[], callback: (data: any) => void) {
console.log('Starting real-time monitoring for', poolAddresses.length, 'pools');
const monitorPool = async (poolAddress: PublicKey) => {
try {
const analytics = await this.getPoolAnalytics(poolAddress);
const signals = await this.generateTradingSignals(poolAddress);
callback({
type: 'pool_update',
pool: poolAddress.toString(),
analytics,
signals,
timestamp: Date.now()
});
} catch (error) {
callback({
type: 'error',
pool: poolAddress.toString(),
error: error.message,
timestamp: Date.now()
});
}
};
// Monitor each pool every 30 seconds
const intervals = poolAddresses.map(poolAddress => {
// Initial update
monitorPool(poolAddress);
// Set up recurring updates
return setInterval(() => monitorPool(poolAddress), 30000);
});
// Return cleanup function
return () => {
intervals.forEach(clearInterval);
console.log('Pool monitoring stopped');
};
}
// Helper methods
private async getLiquidityDistribution(pool: DLMM, activeBinId: number, range: number = 50) {
const distribution = [];
for (let i = -range; i <= range; i++) {
const binId = activeBinId + i;
try {
const bin = await pool.getBin(binId);
distribution.push({
binId,
price: this.binIdToPrice(binId, 25), // Assuming 25 bps bin step
liquidity: bin.liquiditySupply.toNumber(),
amountX: bin.amountX.toNumber(),
amountY: bin.amountY.toNumber(),
feeX: bin.feeAmountX.toNumber(),
feeY: bin.feeAmountY.toNumber()
});
} catch {
// Bin doesn't exist or has no liquidity
distribution.push({
binId,
price: this.binIdToPrice(binId, 25),
liquidity: 0,
amountX: 0,
amountY: 0,
feeX: 0,
feeY: 0
});
}
}
return distribution;
}
private async calculate24hVolume(pool: DLMM) {
// In production, this would query historical swap events
// Mock implementation
return {
volume: 1250000 + Math.random() * 500000,
swapCount: 342 + Math.floor(Math.random() * 100),
uniqueTraders: 156 + Math.floor(Math.random() * 50)
};
}
private async calculate24hFees(pool: DLMM) {
// Calculate fees from pool state
const poolState = await pool.getPoolState();
return {
total: 3125.50,
breakdown: {
tradingFees: 2875.25,
protocolFees: 250.25
}
};
}
private async calculateTVL(pool: DLMM) {
const poolState = await pool.getPoolState();
// Mock token prices (in production, fetch from price APIs)
const tokenXPrice = 1.0; // USDC
const tokenYPrice = 105.0; // SOL
const valueX = (poolState.reserveX.toNumber() / 1e6) * tokenXPrice;
const valueY = (poolState.reserveY.toNumber() / 1e9) * tokenYPrice;
return {
total: valueX + valueY,
breakdown: {
tokenX: valueX,
tokenY: valueY
}
};
}
private async calculateYieldMetrics(pool: DLMM, feeData: any, tvl: any) {
const dailyFees = feeData.total;
const yearlyFees = dailyFees * 365;
return {
apy: (yearlyFees / tvl.total) * 100,
apr: (yearlyFees / tvl.total) * 100, // Simplified
dailyYield: (dailyFees / tvl.total) * 100
};
}
private binIdToPrice(binId: number, binStep: number): number {
return Math.pow(1 + binStep / 10000, binId - 8388608);
}
private calculateConcentrationRatio(distribution: any[]): number {
const totalLiquidity = distribution.reduce((sum, bin) => sum + bin.liquidity, 0);
const activeBins = distribution.filter(bin => bin.liquidity > 0);
if (activeBins.length === 0) return 0;
// Calculate what percentage of liquidity is in the top 20% of active bins
const topBins = activeBins.sort((a, b) => b.liquidity - a.liquidity).slice(0, Math.ceil(activeBins.length * 0.2));
const topLiquidity = topBins.reduce((sum, bin) => sum + bin.liquidity, 0);
return topLiquidity / totalLiquidity;
}
private calculateUtilization(distribution: any[]): number {
const activeBins = distribution.filter(bin => bin.liquidity > 0).length;
return activeBins / distribution.length;
}
private calculatePriceChange(history: any[]): number {
if (history.length < 2) return 0;
const first = history[0].price;
const last = history[history.length - 1].price;
return ((last - first) / first) * 100;
}
private calculateVolatility(prices: number[]): number {
if (prices.length < 2) return 0;
const returns = [];
for (let i = 1; i < prices.length; i++) {
returns.push((prices[i] - prices[i-1]) / prices[i-1]);
}
const mean = returns.reduce((sum, r) => sum + r, 0) / returns.length;
const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length;
return Math.sqrt(variance);
}
private generateHistoricalDataPoints(timeframe: string): number[] {
const now = Date.now();
const points = [];
const intervals = {
'1h': { count: 60, step: 60000 }, // 1 minute intervals
'24h': { count: 24, step: 3600000 }, // 1 hour intervals
'7d': { count: 168, step: 3600000 }, // 1 hour intervals
'30d': { count: 30, step: 86400000 } // 1 day intervals
};
const config = intervals[timeframe] || intervals['24h'];
for (let i = config.count; i >= 0; i--) {
points.push(now - (i * config.step));
}
return points;
}
// Cache management
private getFromCache(key: string): any | null {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.data;
}
this.cache.delete(key);
return null;
}
private setCache(key: string, data: any, ttl?: number): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
if (ttl) {
setTimeout(() => this.cache.delete(key), ttl);
}
}
// Additional helper methods would be implemented here...
private async getAllPools(): Promise<PublicKey[]> { /* Implementation */ return []; }
private async getUserPools(user: PublicKey): Promise<PublicKey[]> { /* Implementation */ return []; }
private async getHistoricalSnapshot(pool: DLMM, timestamp: number): Promise<any> { /* Implementation */ return {}; }
private async calculatePositionValue(pool: DLMM, position: any): Promise<any> { /* Implementation */ return {}; }
private async calculatePositionFees(pool: DLMM, position: any): Promise<any> { /* Implementation */ return {}; }
private async isPositionInRange(pool: DLMM, position: any): Promise<boolean> { /* Implementation */ return true; }
private async getUserSwapHistory(pool: DLMM, user: PublicKey): Promise<any[]> { /* Implementation */ return []; }
private async getPoolPositionCount(pool: PublicKey): Promise<number> { /* Implementation */ return 0; }
private groupBy(array: any[], key: string, valueExtractor: (item: any) => number): any { /* Implementation */ return {}; }
private calculateOverallSentiment(signals: any[]): string { /* Implementation */ return 'neutral'; }
private generateRecommendations(analytics: any, signals: any[]): string[] { /* Implementation */ return []; }
}
// Usage example
export async function main() {
const connection = new Connection('https://api.devnet.solana.com');
const analytics = new DLMMAnalytics(connection);
const poolAddress = new PublicKey('YourPoolAddress');
const userAddress = new PublicKey('YourUserAddress');
try {
// Get comprehensive pool analytics
const poolData = await analytics.getPoolAnalytics(poolAddress);
console.log('Pool Analytics:', {
tvl: `$${poolData.metrics.tvl.toLocaleString()}`,
volume24h: `$${poolData.metrics.volume24h.toLocaleString()}`,
apy: `${poolData.metrics.apy.toFixed(2)}%`,
activeBins: poolData.liquidity.activeBins,
currentPrice: poolData.price.current.toFixed(6)
});
// Get user analytics
const userData = await analytics.getUserAnalytics(userAddress);
console.log('User Analytics:', {
totalPositions: userData.summary.totalPositions,
totalValue: `$${userData.summary.totalValue.toLocaleString()}`,
feesEarned: `$${userData.summary.totalFeesEarned.toLocaleString()}`,
roi: `${userData.summary.totalROI.toFixed(2)}%`
});
// Generate trading signals
const signals = await analytics.generateTradingSignals(poolAddress);
console.log('Trading Signals:', {
signalCount: signals.signals.length,
sentiment: signals.overallSentiment,
recommendations: signals.recommendations
});
// Start real-time monitoring
const stopMonitoring = await analytics.startPoolMonitoring(
[poolAddress],
(data) => {
if (data.type === 'pool_update') {
console.log(`Pool Update: TVL $${data.analytics.metrics.tvl.toLocaleString()}`);
}
}
);
// Stop monitoring after 5 minutes
setTimeout(stopMonitoring, 5 * 60 * 1000);
} catch (error) {
console.error('Analytics operation failed:', error);
}
}
Analytics Features
Pool Analytics
- TVL Tracking: Total value locked with token breakdown
- Volume Analysis: 24h trading volume and transaction counts
- Liquidity Distribution: Concentration analysis across price bins
- Yield Metrics: APY/APR calculations based on fee generation
- Price Analytics: Historical price data and volatility metrics
User Analytics
- Position Tracking: All user liquidity positions across pools
- Performance Metrics: ROI, fees earned, and yield analysis
- Trading History: Complete swap transaction history
- Portfolio Overview: Value distribution across different pools
Market Intelligence
- Trading Signals: Algorithmic signals based on market data
- Market Statistics: Protocol-wide metrics and comparisons
- Arbitrage Opportunities: Price discrepancy detection
- Trend Analysis: Market movement patterns and predictions
Real-Time Monitoring
- Live Updates: Real-time pool state changes
- Alert System: Notifications for significant market events
- Performance Tracking: Continuous metric monitoring
- Data Streaming: WebSocket-like real-time data feeds
Data Visualization Integration
Copy
// Format data for chart libraries
const formatForChart = (analytics: any) => {
return {
priceChart: analytics.price.history.map(p => ({ x: p.timestamp, y: p.price })),
volumeChart: analytics.volume.history.map(v => ({ x: v.timestamp, y: v.amount })),
liquidityHeatmap: analytics.liquidity.distribution.map(bin => ({
binId: bin.binId,
price: bin.price,
liquidity: bin.liquidity
})),
yieldChart: analytics.yield.history.map(y => ({ x: y.timestamp, y: y.apy }))
};
};
Performance Optimization
- Intelligent Caching: Multi-layer caching with TTL
- Batch Processing: Efficient bulk data operations
- Data Compression: Optimized data storage and transfer
- Parallel Queries: Concurrent data fetching for better performance