Core Position Metrics
Calculate the essential metrics that LPs need to track their performance:Copy
// 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:Copy
// 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
Copy
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
Copy
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
Copy
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:Copy
APY = (fees_earned / original_investment) * (365 / days_active) * 100
Copy
IL = 2 * sqrt(price_ratio) / (1 + price_ratio) - 1
Copy
Sharpe = (return - risk_free_rate) / volatility