Skip to main content
Calculating accurate position metrics is crucial for LP analytics dashboards. This guide shows how to compute comprehensive position performance data including P&L, yield, fees earned, and risk metrics using the DLMM SDK.

Core Position Metrics

Calculate the essential metrics that LPs need to track their performance:
// src/services/PositionMetricsCalculator.ts
import { Connection, PublicKey } from '@solana/web3.js';
import { DLMM } from '@saros-finance/dlmm-sdk';
import { ComposedPoolData } from '../types/pool';

export interface PositionMetrics {
  position: {
    address: string;
    lowerBinId: number;
    upperBinId: number;
    createdAt: Date;
    lastUpdated: Date;
  };
  liquidity: {
    currentX: number;
    currentY: number;
    originalX: number;
    originalY: number;
    liquiditySupply: number;
  };
  value: {
    current: number;
    original: number;
    change: number;
    changePercent: number;
  };
  fees: {
    totalEarned: number;
    earnedX: number;
    earnedY: number;
    apy: number;
    dailyRate: number;
  };
  performance: {
    impermanentLoss: number;
    impermanentLossPercent: number;
    totalReturn: number;
    totalReturnPercent: number;
    holdVsLP: number; // Hold vs LP comparison
  };
  risk: {
    isInRange: boolean;
    distanceFromActive: number;
    utilizationRate: number;
    concentrationRisk: number;
  };
  timestamp: number;
}

export class PositionMetricsCalculator {
  private connection: Connection;
  private tokenPrices: Map<string, number>;

  constructor(rpcUrl: string) {
    this.connection = new Connection(rpcUrl, 'confirmed');
    this.tokenPrices = new Map();
  }

  /**
   * Calculate comprehensive metrics for a single position
   */
  async calculatePositionMetrics(
    poolAddress: string,
    positionAddress: string,
    poolData: ComposedPoolData
  ): Promise<PositionMetrics> {
    console.log(`📊 Calculating metrics for position ${positionAddress}`);

    try {
      const pool = new DLMM(this.connection, new PublicKey(poolAddress));
      const position = await this.getPositionData(pool, positionAddress);
      
      // Get current token prices
      await this.updateTokenPrices(poolData.pool.tokenX.mint, poolData.pool.tokenY.mint);

      // Calculate all metrics
      const liquidityMetrics = await this.calculateLiquidityMetrics(position, poolData);
      const valueMetrics = await this.calculateValueMetrics(liquidityMetrics, poolData);
      const feeMetrics = await this.calculateFeeMetrics(position, poolData);
      const performanceMetrics = await this.calculatePerformanceMetrics(
        valueMetrics, 
        feeMetrics, 
        poolData
      );
      const riskMetrics = this.calculateRiskMetrics(position, poolData);

      const metrics: PositionMetrics = {
        position: {
          address: positionAddress,
          lowerBinId: position.lowerBinId,
          upperBinId: position.upperBinId,
          createdAt: new Date(position.createdAt * 1000),
          lastUpdated: new Date(position.lastUpdatedAt * 1000)
        },
        liquidity: liquidityMetrics,
        value: valueMetrics,
        fees: feeMetrics,
        performance: performanceMetrics,
        risk: riskMetrics,
        timestamp: Date.now()
      };

      console.log(`✅ Calculated metrics for position ${positionAddress}`);
      console.log(`   Current Value: $${valueMetrics.current.toLocaleString()}`);
      console.log(`   Total Return: ${performanceMetrics.totalReturnPercent.toFixed(2)}%`);
      console.log(`   APY: ${feeMetrics.apy.toFixed(2)}%`);

      return metrics;

    } catch (error) {
      console.error(`❌ Failed to calculate position metrics:`, error);
      throw new Error(`Position metrics calculation failed: ${error.message}`);
    }
  }

  /**
   * Calculate metrics for multiple positions
   */
  async calculatePortfolioMetrics(
    userAddress: string,
    poolAddresses: string[]
  ): Promise<{
    positions: PositionMetrics[];
    portfolio: PortfolioSummary;
  }> {
    console.log(`📊 Calculating portfolio metrics for ${poolAddresses.length} pools`);

    const allPositions: PositionMetrics[] = [];

    // Process each pool
    for (const poolAddress of poolAddresses) {
      try {
        const pool = new DLMM(this.connection, new PublicKey(poolAddress));
        const userPositions = await pool.getPositionsByUser(new PublicKey(userAddress));
        const poolData = await this.getPoolData(poolAddress); // You'd implement this

        // Calculate metrics for each position
        for (const position of userPositions) {
          const metrics = await this.calculatePositionMetrics(
            poolAddress,
            position.address.toString(),
            poolData
          );
          allPositions.push(metrics);
        }

      } catch (error) {
        console.warn(`Failed to process pool ${poolAddress}:`, error.message);
      }
    }

    // Calculate portfolio summary
    const portfolioSummary = this.calculatePortfolioSummary(allPositions);

    return {
      positions: allPositions,
      portfolio: portfolioSummary
    };
  }

  /**
   * Calculate liquidity metrics
   */
  private async calculateLiquidityMetrics(position: any, poolData: ComposedPoolData) {
    // Get current liquidity amounts
    const currentX = this.lamportsToNumber(position.liquidityX, poolData.pool.tokenX.decimals);
    const currentY = this.lamportsToNumber(position.liquidityY, poolData.pool.tokenY.decimals);

    // Calculate original liquidity (from creation transaction)
    const originalX = this.lamportsToNumber(position.originalLiquidityX, poolData.pool.tokenX.decimals);
    const originalY = this.lamportsToNumber(position.originalLiquidityY, poolData.pool.tokenY.decimals);

    return {
      currentX,
      currentY,
      originalX,
      originalY,
      liquiditySupply: position.liquiditySupply.toNumber()
    };
  }

  /**
   * Calculate value metrics
   */
  private async calculateValueMetrics(liquidityMetrics: any, poolData: ComposedPoolData) {
    const tokenXPrice = this.getTokenPrice(poolData.pool.tokenX.mint);
    const tokenYPrice = this.getTokenPrice(poolData.pool.tokenY.mint);

    const currentValue = 
      (liquidityMetrics.currentX * tokenXPrice) + 
      (liquidityMetrics.currentY * tokenYPrice);

    const originalValue = 
      (liquidityMetrics.originalX * tokenXPrice) + 
      (liquidityMetrics.originalY * tokenYPrice);

    const change = currentValue - originalValue;
    const changePercent = originalValue > 0 ? (change / originalValue) * 100 : 0;

    return {
      current: currentValue,
      original: originalValue,
      change,
      changePercent
    };
  }

  /**
   * Calculate fee metrics and APY
   */
  private async calculateFeeMetrics(position: any, poolData: ComposedPoolData) {
    // Get accumulated fees
    const earnedX = this.lamportsToNumber(position.feeEarnedX, poolData.pool.tokenX.decimals);
    const earnedY = this.lamportsToNumber(position.feeEarnedY, poolData.pool.tokenY.decimals);

    const tokenXPrice = this.getTokenPrice(poolData.pool.tokenX.mint);
    const tokenYPrice = this.getTokenPrice(poolData.pool.tokenY.mint);

    const totalEarned = (earnedX * tokenXPrice) + (earnedY * tokenYPrice);

    // Calculate time-based metrics
    const positionAgeMs = Date.now() - (position.createdAt * 1000);
    const positionAgeDays = positionAgeMs / (1000 * 60 * 60 * 24);

    // Calculate APY based on fees earned vs original investment
    const originalValue = this.calculateOriginalPositionValue(position, tokenXPrice, tokenYPrice);
    const dailyRate = positionAgeDays > 0 ? (totalEarned / originalValue) / positionAgeDays : 0;
    const apy = dailyRate * 365 * 100;

    return {
      totalEarned,
      earnedX,
      earnedY,
      apy,
      dailyRate: dailyRate * 100
    };
  }

  /**
   * Calculate performance metrics including impermanent loss
   */
  private async calculatePerformanceMetrics(
    valueMetrics: any, 
    feeMetrics: any, 
    poolData: ComposedPoolData
  ) {
    // Calculate impermanent loss
    const impermanentLoss = this.calculateImpermanentLoss(valueMetrics, poolData);
    const impermanentLossPercent = valueMetrics.original > 0 ? 
      (impermanentLoss / valueMetrics.original) * 100 : 0;

    // Total return = value change + fees earned
    const totalReturn = valueMetrics.change + feeMetrics.totalEarned;
    const totalReturnPercent = valueMetrics.original > 0 ? 
      (totalReturn / valueMetrics.original) * 100 : 0;

    // Hold vs LP comparison
    const holdVsLP = this.calculateHoldVsLP(valueMetrics, feeMetrics, poolData);

    return {
      impermanentLoss,
      impermanentLossPercent,
      totalReturn,
      totalReturnPercent,
      holdVsLP
    };
  }

  /**
   * Calculate risk metrics
   */
  private calculateRiskMetrics(position: any, poolData: ComposedPoolData) {
    const activeBinId = poolData.state.activeId;
    const isInRange = position.lowerBinId <= activeBinId && position.upperBinId >= activeBinId;
    
    const distanceFromActive = isInRange ? 0 : Math.min(
      Math.abs(position.lowerBinId - activeBinId),
      Math.abs(position.upperBinId - activeBinId)
    );

    // Calculate position width as a proxy for concentration risk
    const positionWidth = position.upperBinId - position.lowerBinId;
    const concentrationRisk = Math.max(0, (50 - positionWidth) / 50); // Higher risk for narrower ranges

    // Calculate utilization rate based on in-range status and liquidity
    const utilizationRate = isInRange ? 1.0 : 0.0;

    return {
      isInRange,
      distanceFromActive,
      utilizationRate,
      concentrationRisk
    };
  }

  /**
   * Calculate impermanent loss compared to holding
   */
  private calculateImpermanentLoss(valueMetrics: any, poolData: ComposedPoolData): number {
    // This is a simplified calculation
    // In production, you'd need to track the original price ratio and current ratio
    const priceRatioChange = 0; // Implement based on price history
    
    // IL formula: IL = 2 * sqrt(ratio) / (1 + ratio) - 1
    // For now, return estimated IL based on value change
    return Math.max(0, -valueMetrics.change * 0.5); // Rough approximation
  }

  /**
   * Compare LP returns vs simply holding tokens
   */
  private calculateHoldVsLP(valueMetrics: any, feeMetrics: any, poolData: ComposedPoolData): number {
    // Calculate what the tokens would be worth if just held
    const holdValue = valueMetrics.original; // Simplified - would need price appreciation calculation
    
    // LP value = current position value + fees earned
    const lpValue = valueMetrics.current + feeMetrics.totalEarned;
    
    return lpValue - holdValue;
  }

  /**
   * Calculate portfolio summary from all positions
   */
  private calculatePortfolioSummary(positions: PositionMetrics[]) {
    const totalCurrentValue = positions.reduce((sum, pos) => sum + pos.value.current, 0);
    const totalOriginalValue = positions.reduce((sum, pos) => sum + pos.value.original, 0);
    const totalFeesEarned = positions.reduce((sum, pos) => sum + pos.fees.totalEarned, 0);
    const totalReturn = positions.reduce((sum, pos) => sum + pos.performance.totalReturn, 0);

    const inRangePositions = positions.filter(pos => pos.risk.isInRange).length;
    const averageAPY = positions.reduce((sum, pos) => sum + pos.fees.apy, 0) / positions.length;

    return {
      totalPositions: positions.length,
      totalCurrentValue,
      totalOriginalValue,
      totalFeesEarned,
      totalReturn,
      totalReturnPercent: totalOriginalValue > 0 ? (totalReturn / totalOriginalValue) * 100 : 0,
      inRangePositions,
      inRangePercent: positions.length > 0 ? (inRangePositions / positions.length) * 100 : 0,
      averageAPY,
      bestPerformingPosition: positions.reduce((best, pos) => 
        pos.performance.totalReturnPercent > best.performance.totalReturnPercent ? pos : best
      ),
      worstPerformingPosition: positions.reduce((worst, pos) => 
        pos.performance.totalReturnPercent < worst.performance.totalReturnPercent ? pos : worst
      )
    };
  }

  // Utility methods
  private async getPositionData(pool: DLMM, positionAddress: string): Promise<any> {
    // Implementation would fetch position data from the pool
    // This is a placeholder - actual implementation depends on SDK methods
    return {
      address: positionAddress,
      lowerBinId: 8388600,
      upperBinId: 8388620,
      liquidityX: { toNumber: () => 1000 * Math.pow(10, 6) },
      liquidityY: { toNumber: () => 1000 * Math.pow(10, 9) },
      originalLiquidityX: { toNumber: () => 1000 * Math.pow(10, 6) },
      originalLiquidityY: { toNumber: () => 1000 * Math.pow(10, 9) },
      feeEarnedX: { toNumber: () => 10 * Math.pow(10, 6) },
      feeEarnedY: { toNumber: () => 10 * Math.pow(10, 9) },
      liquiditySupply: { toNumber: () => 1000000 },
      createdAt: Math.floor(Date.now() / 1000) - 86400, // 1 day ago
      lastUpdatedAt: Math.floor(Date.now() / 1000)
    };
  }

  private async getPoolData(poolAddress: string): Promise<ComposedPoolData> {
    // This would use your existing pool data composition service
    // Placeholder implementation
    return {
      pool: {
        address: poolAddress,
        tokenX: { mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', symbol: 'USDC', decimals: 6 },
        tokenY: { mint: 'So11111111111111111111111111111111111111112', symbol: 'SOL', decimals: 9 },
        binStep: 25,
        baseFactor: 5000
      },
      state: {
        activeId: 8388608,
        currentPrice: 0.01,
        reserveX: 1000000,
        reserveY: 10000,
        totalLiquidity: 1000000
      },
      liquidity: {
        distribution: [],
        activeBins: 20,
        concentrationRatio: 0.8,
        totalValueLocked: 2000000
      },
      pricing: {
        currentRate: 0.01,
        priceImpact: 0.1,
        tradingFees: 0.25
      },
      timestamp: Date.now()
    };
  }

  private async updateTokenPrices(tokenXMint: string, tokenYMint: string): Promise<void> {
    // Mock prices - in production, fetch from price APIs
    this.tokenPrices.set(tokenXMint, 1.0); // USDC = $1
    this.tokenPrices.set(tokenYMint, 100.0); // SOL = $100
  }

  private getTokenPrice(mint: string): number {
    return this.tokenPrices.get(mint) || 1.0;
  }

  private calculateOriginalPositionValue(position: any, tokenXPrice: number, tokenYPrice: number): number {
    const originalX = this.lamportsToNumber(position.originalLiquidityX, 6);
    const originalY = this.lamportsToNumber(position.originalLiquidityY, 9);
    return (originalX * tokenXPrice) + (originalY * tokenYPrice);
  }

  private lamportsToNumber(lamports: any, decimals: number): number {
    return Number(lamports.toString()) / Math.pow(10, decimals);
  }
}

Advanced Analytics

Calculate sophisticated metrics for professional LP management:
// src/services/AdvancedPositionAnalytics.ts

export class AdvancedPositionAnalytics {
  /**
   * Calculate position efficiency metrics
   */
  static calculateEfficiencyMetrics(position: PositionMetrics, poolData: ComposedPoolData) {
    const positionWidth = position.position.upperBinId - position.position.lowerBinId;
    const activeRange = this.getActiveRange(poolData);
    
    return {
      capitalEfficiency: this.calculateCapitalEfficiency(position, poolData),
      rangeUtilization: this.calculateRangeUtilization(position, activeRange),
      feeCapture: this.calculateFeeCapture(position, poolData),
      rebalanceFrequency: this.calculateRebalanceFrequency(position),
      optimalRange: this.suggestOptimalRange(position, poolData)
    };
  }

  /**
   * Calculate risk-adjusted returns
   */
  static calculateRiskAdjustedMetrics(position: PositionMetrics, historicalData: any[]) {
    const volatility = this.calculatePositionVolatility(historicalData);
    const sharpeRatio = this.calculateSharpeRatio(position, volatility);
    const maxDrawdown = this.calculateMaxDrawdown(historicalData);
    
    return {
      volatility,
      sharpeRatio,
      maxDrawdown,
      informationRatio: this.calculateInformationRatio(position, historicalData),
      valueatRisk: this.calculateVaR(position, volatility)
    };
  }

  /**
   * Generate position optimization suggestions
   */
  static generateOptimizationSuggestions(position: PositionMetrics, poolData: ComposedPoolData) {
    const suggestions: string[] = [];
    
    // Check if position is out of range
    if (!position.risk.isInRange) {
      const distance = position.risk.distanceFromActive;
      if (distance > 10) {
        suggestions.push(`Position is ${distance} bins away from active price. Consider rebalancing.`);
      }
    }
    
    // Check fee earning efficiency
    if (position.fees.apy < 10) {
      suggestions.push('Low APY detected. Consider concentrating liquidity in a narrower range.');
    }
    
    // Check impermanent loss
    if (position.performance.impermanentLossPercent > 5) {
      suggestions.push('High impermanent loss. Consider widening range or exiting position.');
    }
    
    // Check concentration risk
    if (position.risk.concentrationRisk > 0.8) {
      suggestions.push('High concentration risk. Consider diversifying across multiple ranges.');
    }
    
    return suggestions;
  }

  private static calculateCapitalEfficiency(position: PositionMetrics, poolData: ComposedPoolData): number {
    // Compare active capital vs total capital
    const activeCapital = position.risk.isInRange ? position.value.current : 0;
    return activeCapital / position.value.current;
  }

  private static calculateRangeUtilization(position: PositionMetrics, activeRange: any): number {
    const positionRange = position.position.upperBinId - position.position.lowerBinId;
    const overlap = Math.max(0, Math.min(position.position.upperBinId, activeRange.upper) - 
                    Math.max(position.position.lowerBinId, activeRange.lower));
    return overlap / positionRange;
  }

  private static calculateFeeCapture(position: PositionMetrics, poolData: ComposedPoolData): number {
    // Estimate fee capture efficiency based on position placement
    return position.risk.isInRange ? 1.0 : 0.0;
  }

  private static calculateRebalanceFrequency(position: PositionMetrics): number {
    // Estimate optimal rebalancing frequency based on position characteristics
    const width = position.position.upperBinId - position.position.lowerBinId;
    return width > 20 ? 7 : 1; // Days between rebalances
  }

  private static suggestOptimalRange(position: PositionMetrics, poolData: ComposedPoolData) {
    const currentPrice = poolData.state.currentPrice;
    const volatility = 0.05; // Would calculate from historical data
    
    // Suggest range based on volatility
    const optimalWidth = Math.floor(volatility * 100); // Convert to bin range
    
    return {
      lower: poolData.state.activeId - Math.floor(optimalWidth / 2),
      upper: poolData.state.activeId + Math.floor(optimalWidth / 2),
      reasoning: `Based on current volatility of ${(volatility * 100).toFixed(1)}%`
    };
  }

  private static getActiveRange(poolData: ComposedPoolData) {
    // Calculate active trading range
    const activeBins = poolData.liquidity.distribution.filter(bin => bin.liquidityAmount > 0);
    return {
      lower: Math.min(...activeBins.map(bin => bin.binId)),
      upper: Math.max(...activeBins.map(bin => bin.binId))
    };
  }

  private static calculatePositionVolatility(historicalData: any[]): number {
    // Calculate volatility from historical position values
    if (historicalData.length < 2) return 0;
    
    const returns = [];
    for (let i = 1; i < historicalData.length; i++) {
      const prevValue = historicalData[i - 1].value;
      const currentValue = historicalData[i].value;
      returns.push((currentValue - prevValue) / prevValue);
    }
    
    const meanReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length;
    const variance = returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / returns.length;
    
    return Math.sqrt(variance);
  }

  private static calculateSharpeRatio(position: PositionMetrics, volatility: number): number {
    const riskFreeRate = 0.02; // 2% annual
    const excessReturn = (position.fees.apy / 100) - riskFreeRate;
    return volatility > 0 ? excessReturn / volatility : 0;
  }

  private static calculateMaxDrawdown(historicalData: any[]): number {
    let maxValue = 0;
    let maxDrawdown = 0;
    
    for (const dataPoint of historicalData) {
      maxValue = Math.max(maxValue, dataPoint.value);
      const drawdown = (maxValue - dataPoint.value) / maxValue;
      maxDrawdown = Math.max(maxDrawdown, drawdown);
    }
    
    return maxDrawdown;
  }

  private static calculateInformationRatio(position: PositionMetrics, historicalData: any[]): number {
    // Information ratio vs benchmark (holding strategy)
    const benchmarkReturn = 0.05; // 5% annual for holding
    const excessReturn = (position.performance.totalReturnPercent / 100) - benchmarkReturn;
    const trackingError = this.calculateTrackingError(historicalData, benchmarkReturn);
    
    return trackingError > 0 ? excessReturn / trackingError : 0;
  }

  private static calculateTrackingError(historicalData: any[], benchmark: number): number {
    // Standard deviation of excess returns vs benchmark
    const excessReturns = historicalData.map(d => (d.returnPercent / 100) - benchmark);
    const meanExcess = excessReturns.reduce((sum, r) => sum + r, 0) / excessReturns.length;
    const variance = excessReturns.reduce((sum, r) => sum + Math.pow(r - meanExcess, 2), 0) / excessReturns.length;
    
    return Math.sqrt(variance);
  }

  private static calculateVaR(position: PositionMetrics, volatility: number): number {
    // 95% Value at Risk (simplified)
    const confidence = 0.95;
    const zScore = 1.645; // 95% confidence z-score
    
    return position.value.current * volatility * zScore;
  }
}

Usage Examples

Individual Position Analysis

async function analyzePosition(poolAddress: string, positionAddress: string) {
  const calculator = new PositionMetricsCalculator('https://api.mainnet-beta.solana.com');
  
  // Get pool data first
  const poolData = await getPoolData(poolAddress); // Your existing function
  
  // Calculate comprehensive metrics
  const metrics = await calculator.calculatePositionMetrics(
    poolAddress,
    positionAddress,
    poolData
  );
  
  console.log('📊 Position Analysis Results:');
  console.log('================================');
  console.log(`Position: ${metrics.position.address}`);
  console.log(`Range: Bin ${metrics.position.lowerBinId} - ${metrics.position.upperBinId}`);
  console.log(`In Range: ${metrics.risk.isInRange ? '✅' : '❌'}`);
  console.log(`Current Value: $${metrics.value.current.toLocaleString()}`);
  console.log(`Total Return: ${metrics.performance.totalReturnPercent.toFixed(2)}%`);
  console.log(`APY: ${metrics.fees.apy.toFixed(2)}%`);
  console.log(`Impermanent Loss: ${metrics.performance.impermanentLossPercent.toFixed(2)}%`);
  
  // Get optimization suggestions
  const suggestions = AdvancedPositionAnalytics.generateOptimizationSuggestions(metrics, poolData);
  if (suggestions.length > 0) {
    console.log('\n💡 Optimization Suggestions:');
    suggestions.forEach((suggestion, i) => {
      console.log(`   ${i + 1}. ${suggestion}`);
    });
  }
  
  return metrics;
}

Portfolio Dashboard

async function createPortfolioDashboard(userAddress: string) {
  const calculator = new PositionMetricsCalculator('https://api.mainnet-beta.solana.com');
  
  const poolAddresses = [
    '2wT4jHyF6o5fFzJJM9cHH8WKrZt1kqEKnEwqYZgQy8wq',
    '3nExkXBEPtjXrTD8ctwxjZyB1G8vLRoHPpg9SLrE2EMY'
    // Add more pool addresses
  ];
  
  const { positions, portfolio } = await calculator.calculatePortfolioMetrics(
    userAddress,
    poolAddresses
  );
  
  console.log('📊 Portfolio Dashboard');
  console.log('======================');
  console.log(`Total Positions: ${portfolio.totalPositions}`);
  console.log(`Total Value: $${portfolio.totalCurrentValue.toLocaleString()}`);
  console.log(`Total Return: ${portfolio.totalReturnPercent.toFixed(2)}%`);
  console.log(`Average APY: ${portfolio.averageAPY.toFixed(2)}%`);
  console.log(`In-Range Positions: ${portfolio.inRangePercent.toFixed(1)}%`);
  
  // Show top and bottom performers
  console.log('\n🏆 Best Performer:');
  console.log(`   Position: ${portfolio.bestPerformingPosition.position.address}`);
  console.log(`   Return: ${portfolio.bestPerformingPosition.performance.totalReturnPercent.toFixed(2)}%`);
  
  console.log('\n⚠️ Worst Performer:');
  console.log(`   Position: ${portfolio.worstPerformingPosition.position.address}`);
  console.log(`   Return: ${portfolio.worstPerformingPosition.performance.totalReturnPercent.toFixed(2)}%`);
  
  // Alert for positions needing attention
  const alertPositions = positions.filter(pos => !pos.risk.isInRange || pos.fees.apy < 5);
  if (alertPositions.length > 0) {
    console.log(`\n🚨 ${alertPositions.length} positions need attention`);
  }
  
  return { positions, portfolio };
}

Performance Monitoring

async function monitorPositionPerformance(positionAddress: string, poolAddress: string) {
  const calculator = new PositionMetricsCalculator('https://api.mainnet-beta.solana.com');
  
  const monitor = setInterval(async () => {
    try {
      const poolData = await getPoolData(poolAddress);
      const metrics = await calculator.calculatePositionMetrics(
        poolAddress,
        positionAddress,
        poolData
      );
      
      // Check for alerts
      const alerts = [];
      
      if (!metrics.risk.isInRange) {
        alerts.push(`Position out of range (${metrics.risk.distanceFromActive} bins away)`);
      }
      
      if (metrics.performance.impermanentLossPercent > 10) {
        alerts.push(`High impermanent loss: ${metrics.performance.impermanentLossPercent.toFixed(2)}%`);
      }
      
      if (metrics.fees.apy < 5) {
        alerts.push(`Low APY: ${metrics.fees.apy.toFixed(2)}%`);
      }
      
      if (alerts.length > 0) {
        console.log('🚨 Position Alerts:');
        alerts.forEach(alert => console.log(`   - ${alert}`));
      } else {
        console.log(`✅ Position healthy - APY: ${metrics.fees.apy.toFixed(2)}%, Return: ${metrics.performance.totalReturnPercent.toFixed(2)}%`);
      }
      
    } catch (error) {
      console.error('Monitoring error:', error.message);
    }
  }, 60000); // Check every minute
  
  // Stop monitoring after 1 hour
  setTimeout(() => clearInterval(monitor), 3600000);
  
  return () => clearInterval(monitor);
}

Key Formulas

APY Calculation:
APY = (fees_earned / original_investment) * (365 / days_active) * 100
Impermanent Loss:
IL = 2 * sqrt(price_ratio) / (1 + price_ratio) - 1
Sharpe Ratio:
Sharpe = (return - risk_free_rate) / volatility
Your position metrics calculator now provides comprehensive LP analytics suitable for professional portfolio management and optimization.