// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; /** * @title PerformanceTracking * @dev Tracks strategy performance metrics including returns, volatility, and win rates * * This contract maintains historical performance data and calculates key metrics: * - Total return percentage * - Annualized return * - Win rate (profitable harvests) * - Sharpe ratio (return per unit of risk) * - Maximum drawdown * * Example usage: * - Deploy with initial capital: PerformanceTracking(1000000) * - Record harvests: recordHarvest(50000, 0) * - Calculate metrics: getPerformanceMetrics() */ contract PerformanceTracking is ReentrancyGuard, Ownable { struct HarvestRecord { uint256 timestamp; uint256 profit; uint256 loss; uint256 portfolioValue; } struct PerformanceMetrics { uint256 totalReturn; // Percentage (e.g., 1500 = 15%) uint256 annualizedReturn; // Percentage (e.g., 800 = 8%) uint256 winRate; // Percentage (e.g., 9500 = 95%) uint256 sharpeRatio; // Scaled by 1e18 uint256 maxDrawdown; // Percentage (e.g., 500 = 5%) uint256 volatility; // Percentage (e.g., 1200 = 12%) } // Initial capital when strategy started uint256 public initialCapital; // Current portfolio value uint256 public currentValue; // Total gains and losses uint256 public totalGain; uint256 public totalLoss; // Harvest history HarvestRecord[] public harvestHistory; // Performance tracking uint256 public peakValue; uint256 public startTime; uint256 public profitableHarvests; uint256 public totalHarvests; // Risk-free rate for Sharpe ratio (annual, scaled by 1e18) uint256 public riskFreeRate = 2e16; // 2% annual event HarvestRecorded(uint256 indexed harvestId, uint256 profit, uint256 loss); event PerformanceCalculated( uint256 totalReturn, uint256 annualizedReturn, uint256 winRate ); event RiskFreeRateUpdated(uint256 newRate); /** * @dev Initialize performance tracking * @param _initialCapital Initial capital amount */ constructor(uint256 _initialCapital) { require(_initialCapital > 0, "Initial capital must be greater than 0"); initialCapital = _initialCapital; currentValue = _initialCapital; peakValue = _initialCapital; startTime = block.timestamp; totalGain = 0; totalLoss = 0; profitableHarvests = 0; totalHarvests = 0; } /** * @dev Record a harvest event * @param profit Profit from harvest * @param loss Loss from harvest */ function recordHarvest(uint256 profit, uint256 loss) external onlyOwner nonReentrant { require(profit == 0 || loss == 0, "Cannot have both profit and loss"); // Update totals if (profit > 0) { totalGain += profit; currentValue += profit; profitableHarvests += 1; } if (loss > 0) { totalLoss += loss; currentValue -= loss; } // Update peak value for drawdown calculation if (currentValue > peakValue) { peakValue = currentValue; } // Record harvest harvestHistory.push(HarvestRecord({ timestamp: block.timestamp, profit: profit, loss: loss, portfolioValue: currentValue })); totalHarvests += 1; emit HarvestRecorded(harvestHistory.length - 1, profit, loss); } /** * @dev Get total return percentage * @return Total return as percentage (e.g., 1500 = 15%) */ function getTotalReturn() public view returns (uint256) { if (initialCapital == 0) { return 0; } if (currentValue >= initialCapital) { return ((currentValue - initialCapital) * 10000) / initialCapital; } else { return 0; // Don't show negative returns as percentage } } /** * @dev Get annualized return percentage * @return Annualized return as percentage (e.g., 800 = 8%) */ function getAnnualizedReturn() public view returns (uint256) { uint256 totalReturn = getTotalReturn(); uint256 daysSinceStart = (block.timestamp - startTime) / 1 days; if (daysSinceStart == 0) { return 0; } // Annualized = (totalReturn / days) * 365 return (totalReturn * 365) / daysSinceStart; } /** * @dev Get win rate percentage * @return Win rate as percentage (e.g., 9500 = 95%) */ function getWinRate() public view returns (uint256) { if (totalHarvests == 0) { return 0; } return (profitableHarvests * 10000) / totalHarvests; } /** * @dev Get maximum drawdown percentage * @return Maximum drawdown as percentage (e.g., 500 = 5%) */ function getMaxDrawdown() public view returns (uint256) { if (peakValue == 0 || currentValue == 0) { return 0; } if (currentValue >= peakValue) { return 0; } uint256 drawdown = peakValue - currentValue; return (drawdown * 10000) / peakValue; } /** * @dev Get volatility (standard deviation of returns) * @return Volatility as percentage (e.g., 1200 = 12%) */ function getVolatility() public view returns (uint256) { if (harvestHistory.length < 2) { return 0; } // Calculate returns for each harvest uint256 sumSquaredDeviation = 0; uint256 avgReturn = getTotalReturn() / harvestHistory.length; for (uint256 i = 0; i < harvestHistory.length; i++) { HarvestRecord memory record = harvestHistory[i]; // Calculate return for this harvest uint256 harvestReturn; if (i == 0) { if (record.profit > 0) { harvestReturn = (record.profit * 10000) / initialCapital; } else { harvestReturn = 0; } } else { uint256 prevValue = harvestHistory[i - 1].portfolioValue; if (record.profit > 0) { harvestReturn = (record.profit * 10000) / prevValue; } else { harvestReturn = 0; } } // Calculate squared deviation from average uint256 deviation; if (harvestReturn >= avgReturn) { deviation = harvestReturn - avgReturn; } else { deviation = avgReturn - harvestReturn; } sumSquaredDeviation += (deviation * deviation); } // Calculate variance and standard deviation uint256 variance = sumSquaredDeviation / harvestHistory.length; uint256 volatility = sqrt(variance); return volatility; } /** * @dev Get Sharpe ratio (return per unit of risk) * @return Sharpe ratio scaled by 1e18 */ function getSharpeRatio() public view returns (uint256) { uint256 annualizedReturn = getAnnualizedReturn(); uint256 volatility = getVolatility(); if (volatility == 0) { return 0; } // Sharpe = (annualizedReturn - riskFreeRate) / volatility // Scaled by 1e18 for precision uint256 excessReturn = annualizedReturn > (riskFreeRate / 100) ? annualizedReturn - (riskFreeRate / 100) : 0; return (excessReturn * 1e18) / volatility; } /** * @dev Get all performance metrics * @return metrics PerformanceMetrics struct with all calculated metrics */ function getPerformanceMetrics() external view returns (PerformanceMetrics memory metrics) { metrics.totalReturn = getTotalReturn(); metrics.annualizedReturn = getAnnualizedReturn(); metrics.winRate = getWinRate(); metrics.sharpeRatio = getSharpeRatio(); metrics.maxDrawdown = getMaxDrawdown(); metrics.volatility = getVolatility(); return metrics; } /** * @dev Get harvest history length * @return Number of harvests recorded */ function getHarvestCount() external view returns (uint256) { return harvestHistory.length; } /** * @dev Get specific harvest record * @param index Harvest index * @return HarvestRecord at index */ function getHarvestRecord(uint256 index) external view returns (HarvestRecord memory) { require(index < harvestHistory.length, "Index out of bounds"); return harvestHistory[index]; } /** * @dev Get recent harvest records * @param count Number of recent records to return * @return Array of recent HarvestRecords */ function getRecentHarvests(uint256 count) external view returns (HarvestRecord[] memory) { uint256 start = harvestHistory.length > count ? harvestHistory.length - count : 0; HarvestRecord[] memory recent = new HarvestRecord[]( harvestHistory.length - start ); for (uint256 i = start; i < harvestHistory.length; i++) { recent[i - start] = harvestHistory[i]; } return recent; } /** * @dev Get performance metrics for a specific time period * @param startTimestamp Start of period * @param endTimestamp End of period * @return metrics PerformanceMetrics for the period */ function getPerformanceMetricsForPeriod( uint256 startTimestamp, uint256 endTimestamp ) external view returns (PerformanceMetrics memory metrics) { require(startTimestamp < endTimestamp, "Invalid time range"); uint256 periodProfit = 0; uint256 periodLoss = 0; uint256 periodWins = 0; uint256 periodTotal = 0; uint256 startValue = initialCapital; uint256 endValue = initialCapital; // Find harvest records in period for (uint256 i = 0; i < harvestHistory.length; i++) { HarvestRecord memory record = harvestHistory[i]; if (record.timestamp >= startTimestamp && record.timestamp <= endTimestamp) { periodProfit += record.profit; periodLoss += record.loss; if (record.profit > 0) { periodWins += 1; } periodTotal += 1; endValue = record.portfolioValue; } } // Calculate metrics for period if (startValue > 0) { metrics.totalReturn = ((endValue - startValue) * 10000) / startValue; } uint256 periodDays = (endTimestamp - startTimestamp) / 1 days; if (periodDays > 0) { metrics.annualizedReturn = (metrics.totalReturn * 365) / periodDays; } if (periodTotal > 0) { metrics.winRate = (periodWins * 10000) / periodTotal; } return metrics; } /** * @dev Set risk-free rate for Sharpe ratio calculation * @param _riskFreeRate Risk-free rate (scaled by 1e18, e.g., 2e16 = 2%) */ function setRiskFreeRate(uint256 _riskFreeRate) external onlyOwner { riskFreeRate = _riskFreeRate; emit RiskFreeRateUpdated(_riskFreeRate); } /** * @dev Update current portfolio value * @param _value New portfolio value */ function updatePortfolioValue(uint256 _value) external onlyOwner { currentValue = _value; if (currentValue > peakValue) { peakValue = currentValue; } } /** * @dev Calculate integer square root * @param x Value to calculate square root of * @return Square root of x */ function sqrt(uint256 x) internal pure returns (uint256) { if (x == 0) return 0; uint256 z = (x + 1) / 2; uint256 y = x; while (z < y) { y = z; z = (x / z + z) / 2; } return y; } /** * @dev Reset performance tracking * Useful for starting a new performance period */ function reset() external onlyOwner { initialCapital = currentValue; peakValue = currentValue; startTime = block.timestamp; totalGain = 0; totalLoss = 0; profitableHarvests = 0; totalHarvests = 0; // Clear harvest history delete harvestHistory; } }