From 56199ba31187cc9d745a48b92df0d847b184d617 Mon Sep 17 00:00:00 2001 From: seroxdesign Date: Tue, 5 Aug 2025 07:32:31 -0400 Subject: [PATCH] c --- src/configuration/index.ts | 15 ++ .../ProfitabilityEngineWrapper.ts | 21 ++ .../interfaces/IProfitabilityEngine.ts | 15 ++ .../strategies/GovLstProfitabilityEngine.ts | 38 +++ src/utils/RewardAccumulationTracker.ts | 217 ++++++++++++++++++ 5 files changed, 306 insertions(+) create mode 100644 src/utils/RewardAccumulationTracker.ts diff --git a/src/configuration/index.ts b/src/configuration/index.ts index ec1f686..039d4f7 100644 --- a/src/configuration/index.ts +++ b/src/configuration/index.ts @@ -125,6 +125,21 @@ export const CONFIG = { maxBatchSize: 10, defaultTipReceiver: process.env.TIP_RECEIVER_ADDRESS || '', rewardTokenAddress: process.env.REWARD_TOKEN_ADDRESS || '', + rewardAccumulation: { + expectedRewardsPerPeriod: process.env.EXPECTED_REWARDS_PER_PERIOD + ? BigInt(process.env.EXPECTED_REWARDS_PER_PERIOD) + : ethers.parseEther('5000'), // 5k ETH per period + accumulationPeriodHours: parseInt(process.env.ACCUMULATION_PERIOD_HOURS || '12'), + minimumProfitableRatio: parseFloat(process.env.MIN_PROFITABLE_RATIO || '0.6'), // 60% + sampleIntervalMinutes: parseInt(process.env.REWARD_SAMPLE_INTERVAL_MINUTES || '30'), + maxSamples: parseInt(process.env.MAX_REWARD_SAMPLES || '48'), // 24 hours worth + defaultPauseDurationHours: parseInt(process.env.DEFAULT_PAUSE_HOURS || '6'), + pauseBufferFactor: parseFloat(process.env.PAUSE_BUFFER_FACTOR || '0.8'), // 80% of calculated + maxPauseDurationHours: parseInt(process.env.MAX_PAUSE_HOURS || '12'), + minPauseDurationHours: parseInt(process.env.MIN_PAUSE_HOURS || '1'), + highProfitThreshold: parseFloat(process.env.HIGH_PROFIT_THRESHOLD || '1.5'), // 150% of payout + cooldownHoursAfterSuccess: parseInt(process.env.COOLDOWN_HOURS_AFTER_SUCCESS || '2'), + }, priceFeed: { tokenAddress: process.env.PRICE_FEED_TOKEN_ADDRESS || '', cacheDuration: 10 * 60 * 1000, // 10 minutes diff --git a/src/profitability/ProfitabilityEngineWrapper.ts b/src/profitability/ProfitabilityEngineWrapper.ts index d862423..7a80adb 100644 --- a/src/profitability/ProfitabilityEngineWrapper.ts +++ b/src/profitability/ProfitabilityEngineWrapper.ts @@ -937,4 +937,25 @@ export class GovLstProfitabilityEngineWrapper get config(): ProfitabilityEngineConfig { return this.engine.config; } + + /** + * Get reward accumulation tracker status + */ + getRewardTrackerStatus() { + return this.engine.getRewardTrackerStatus(); + } + + /** + * Manually resume operations (override pause) + */ + resumeOperations(): void { + this.engine.resumeOperations(); + } + + /** + * Force pause for a specific duration + */ + forcePause(durationHours: number, reason: string): void { + this.engine.forcePause(durationHours, reason); + } } diff --git a/src/profitability/interfaces/IProfitabilityEngine.ts b/src/profitability/interfaces/IProfitabilityEngine.ts index b9d469f..db27021 100644 --- a/src/profitability/interfaces/IProfitabilityEngine.ts +++ b/src/profitability/interfaces/IProfitabilityEngine.ts @@ -49,4 +49,19 @@ export interface IGovLstProfitabilityEngine { analyzeAndGroupDeposits( deposits: GovLstDeposit[], ): Promise; + + /** + * Get reward accumulation tracker status + */ + getRewardTrackerStatus(): any; + + /** + * Manually resume operations (override pause) + */ + resumeOperations(): void; + + /** + * Force pause for a specific duration + */ + forcePause(durationHours: number, reason: string): void; } diff --git a/src/profitability/strategies/GovLstProfitabilityEngine.ts b/src/profitability/strategies/GovLstProfitabilityEngine.ts index 0679efb..4726a0b 100644 --- a/src/profitability/strategies/GovLstProfitabilityEngine.ts +++ b/src/profitability/strategies/GovLstProfitabilityEngine.ts @@ -22,6 +22,7 @@ import { ContractDataCache } from '@/utils/ContractDataCache'; import { CUMonitor } from '@/utils/CUMonitor'; import { SimplifiedLogger } from '@/utils/SimplifiedLogger'; import { DepositCountPredictor } from '@/utils/DepositCountPredictor'; +import { RewardAccumulationTracker } from '@/utils/RewardAccumulationTracker'; /** * Updated ProfitabilityConfig to include errorLogger @@ -45,6 +46,7 @@ export class GovLstProfitabilityEngine implements IGovLstProfitabilityEngine { private readonly cuMonitor: CUMonitor; private readonly simpleLogger: SimplifiedLogger; private readonly depositPredictor: DepositCountPredictor; + private readonly rewardTracker: RewardAccumulationTracker; private isRunning: boolean; private lastGasPrice: bigint; private lastUpdateTimestamp: number; @@ -103,6 +105,7 @@ export class GovLstProfitabilityEngine implements IGovLstProfitabilityEngine { this.cuMonitor = new CUMonitor(this.logger); this.simpleLogger = new SimplifiedLogger(this.logger, 'Profitability'); this.depositPredictor = new DepositCountPredictor(this.stakerContract, this.logger); + this.rewardTracker = new RewardAccumulationTracker(this.logger, CONFIG.profitability.rewardAccumulation); // Initialize multicall batcher with CU monitor this.multicallBatcher = new MulticallBatcher( @@ -635,6 +638,17 @@ export class GovLstProfitabilityEngine implements IGovLstProfitabilityEngine { depositCount: depositIds.length, }); + // Track reward levels and check if operations should be paused + this.rewardTracker.trackRewardLevels(totalAvailableRewards, payoutAmount); + + // Check if we should pause operations to save CUs + if (this.rewardTracker.shouldPauseOperations()) { + const status = this.rewardTracker.getStatus(); + this.simpleLogger.critical(`鈴革笍 Operations paused - ${status.pauseRemainingMinutes} minutes remaining`); + + return this.createEmptyBatchAnalysis(); + } + // Sort deposits by rewards in descending order for optimal filling // Add secondary sort by deposit_id for deterministic ordering when rewards are equal const sortedDeposits = [...normalizedDeposits].sort((a, b) => { @@ -801,6 +815,9 @@ export class GovLstProfitabilityEngine implements IGovLstProfitabilityEngine { this.simpleLogger.critical(`馃幆 PROFITABLE: ${this.activeBin.deposit_ids.length} deposits, ${ethers.formatEther(expectedProfit)} ETH profit`); + // Record this as a successful transaction for reward accumulation tracking + this.rewardTracker.recordSuccessfulTransaction(this.activeBin.total_rewards, payoutAmount); + readyBins.push(this.activeBin); } else { // Silent rejection - no logging needed @@ -1811,4 +1828,25 @@ export class GovLstProfitabilityEngine implements IGovLstProfitabilityEngine { throw new Error(errorMessage); } + + /** + * Get reward accumulation tracker status + */ + getRewardTrackerStatus() { + return this.rewardTracker.getStatus(); + } + + /** + * Manually resume operations (override pause) + */ + resumeOperations(): void { + this.rewardTracker.resumeOperations(); + } + + /** + * Force pause for a specific duration + */ + forcePause(durationHours: number, reason: string): void { + this.rewardTracker.forcePause(durationHours, reason); + } } diff --git a/src/utils/RewardAccumulationTracker.ts b/src/utils/RewardAccumulationTracker.ts new file mode 100644 index 0000000..df0df9b --- /dev/null +++ b/src/utils/RewardAccumulationTracker.ts @@ -0,0 +1,217 @@ +import { ethers } from 'ethers'; +import { Logger } from '@/monitor/logging'; +import { SimplifiedLogger } from './SimplifiedLogger'; + +export interface RewardTrackerConfig { + /** Expected rewards per accumulation period (in wei) */ + expectedRewardsPerPeriod: bigint; + /** Accumulation period in hours */ + accumulationPeriodHours: number; + /** Minimum ratio of rewards to payout amount to proceed (0.0 - 1.0) */ + minimumProfitableRatio: number; + /** How often to sample reward levels (in minutes) */ + sampleIntervalMinutes: number; + /** Maximum number of samples to keep for rate calculation */ + maxSamples: number; + /** Default pause duration when no data available (in hours) */ + defaultPauseDurationHours: number; + /** Buffer factor for calculated pause time (0.5 = 50% of calculated time) */ + pauseBufferFactor: number; + /** Maximum pause duration in hours */ + maxPauseDurationHours: number; + /** Minimum pause duration in hours */ + minPauseDurationHours: number; + /** High profit threshold ratio to skip pausing */ + highProfitThreshold: number; + /** Hours after successful transaction before auto-pause is allowed */ + cooldownHoursAfterSuccess: number; +} + +/** + * Tracks reward accumulation patterns and determines when to pause operations + * to avoid wasting CUs when rewards are insufficient + */ +export class RewardAccumulationTracker { + private readonly logger: Logger; + private readonly simpleLogger: SimplifiedLogger; + private readonly config: RewardTrackerConfig; + + // Reward accumulation tracking + private lastSuccessfulTransaction: number = 0; + private rewardAccumulationRate: number = 0; // Rewards per hour + private totalRewardsSample: Array<{ timestamp: number; totalRewards: bigint }> = []; + private pauseUntil: number = 0; + + constructor(logger: Logger, config: RewardTrackerConfig) { + this.logger = logger; + this.simpleLogger = new SimplifiedLogger(logger, 'RewardTracker'); + this.config = config; + } + + /** + * Check if operations should be paused based on reward accumulation + */ + shouldPauseOperations(): boolean { + const now = Date.now(); + + if (now < this.pauseUntil) { + const minutesRemaining = Math.ceil((this.pauseUntil - now) / 1000 / 60); + this.simpleLogger.debug(`Operations paused for ${minutesRemaining} more minutes`); + return true; + } + + return false; + } + + /** + * Record a successful transaction and reset accumulation tracking + */ + recordSuccessfulTransaction(totalRewards: bigint, payoutAmount: bigint): void { + const now = Date.now(); + this.lastSuccessfulTransaction = now; + + this.simpleLogger.critical(`馃挵 Transaction executed: ${ethers.formatEther(totalRewards)} ETH rewards, ${ethers.formatEther(payoutAmount)} ETH payout`); + + // Don't pause if we just started or if this was a large transaction + const rewardRatio = Number(totalRewards) / Number(payoutAmount); + if (rewardRatio > this.config.highProfitThreshold) { + this.simpleLogger.critical(`馃殌 High-profit transaction completed, continuing operations`); + return; + } + + // Calculate pause duration based on reward accumulation rate + this.calculateOptimalPauseDuration(payoutAmount); + } + + /** + * Track reward levels to understand accumulation patterns + */ + trackRewardLevels(totalRewards: bigint, payoutAmount: bigint): void { + const now = Date.now(); + + // Add sample for tracking accumulation + this.totalRewardsSample.push({ timestamp: now, totalRewards }); + + // Keep only recent samples + if (this.totalRewardsSample.length > this.config.maxSamples) { + this.totalRewardsSample = this.totalRewardsSample.slice(-this.config.maxSamples); + } + + // Update accumulation rate if we have enough samples + this.updateAccumulationRate(); + + // Check if we should pause due to insufficient rewards + const rewardRatio = Number(totalRewards) / Number(payoutAmount); + + if (rewardRatio < this.config.minimumProfitableRatio) { + this.simpleLogger.critical(`鈴革笍 Insufficient rewards: ${ethers.formatEther(totalRewards)} ETH (${(rewardRatio * 100).toFixed(1)}% of payout)`); + + // Always pause when rewards are insufficient, regardless of recent transaction history + // This saves CUs immediately when we know there aren't enough rewards + this.calculateOptimalPauseDuration(payoutAmount); + } + } + + /** + * Force a pause for a specific duration (when user manually triggers) + */ + forcePause(durationHours: number, reason: string): void { + const now = Date.now(); + this.pauseUntil = now + (durationHours * 60 * 60 * 1000); + + this.simpleLogger.critical(`鈴革笍 Operations paused for ${durationHours}h: ${reason}`); + } + + /** + * Calculate optimal pause duration based on reward accumulation patterns + */ + private calculateOptimalPauseDuration(payoutAmount: bigint): void { + const now = Date.now(); + + // If we don't have enough data, use conservative estimate + if (this.rewardAccumulationRate === 0) { + this.pauseUntil = now + (this.config.defaultPauseDurationHours * 60 * 60 * 1000); + + this.simpleLogger.critical(`鈴革笍 Pausing for ${this.config.defaultPauseDurationHours}h (default - insufficient data)`); + return; + } + + // Calculate how long until we have enough rewards + const neededRewards = Number(payoutAmount) * this.config.minimumProfitableRatio; + const currentRewards = this.totalRewardsSample.length > 0 + ? Number(this.totalRewardsSample[this.totalRewardsSample.length - 1]?.totalRewards) + : 0; + + const rewardDeficit = Math.max(0, neededRewards - currentRewards); + const hoursToAccumulate = rewardDeficit / this.rewardAccumulationRate; + + // Apply buffer factor and clamp to min/max bounds + const rawPauseHours = hoursToAccumulate * this.config.pauseBufferFactor; + const pauseHours = Math.min( + this.config.maxPauseDurationHours, + Math.max(this.config.minPauseDurationHours, rawPauseHours) + ); + + this.pauseUntil = now + (pauseHours * 60 * 60 * 1000); + + this.simpleLogger.critical(`鈴革笍 Pausing for ${pauseHours.toFixed(1)}h (calculated: ${rawPauseHours.toFixed(1)}h, rate: ${this.rewardAccumulationRate.toFixed(0)}/hour)`); + } + + /** + * Update reward accumulation rate based on recent samples + */ + private updateAccumulationRate(): void { + if (this.totalRewardsSample.length < 2) return; + + // Use samples from the configured accumulation period to calculate rate + const now = Date.now(); + const periodAgo = now - (this.config.accumulationPeriodHours * 60 * 60 * 1000); + + const recentSamples = this.totalRewardsSample.filter(s => s.timestamp > periodAgo); + if (recentSamples.length < 2) return; + + const oldestSample = recentSamples[0]; + const newestSample = recentSamples[recentSamples.length - 1]; + + const rewardIncrease = Number((newestSample?.totalRewards || 0n) - (oldestSample?.totalRewards || 0n)); + const timeSpanHours = ((newestSample?.timestamp || 0) - (oldestSample?.timestamp || 0)) / (1000 * 60 * 60); + + if (timeSpanHours > 0) { + this.rewardAccumulationRate = rewardIncrease / timeSpanHours; + } + } + + /** + * Get current status and statistics + */ + getStatus(): { + isPaused: boolean; + pauseRemainingMinutes: number; + rewardAccumulationRate: number; + lastTransactionMinutesAgo: number; + samples: number; + } { + const now = Date.now(); + const isPaused = now < this.pauseUntil; + const pauseRemainingMinutes = isPaused ? Math.ceil((this.pauseUntil - now) / 1000 / 60) : 0; + const lastTransactionMinutesAgo = this.lastSuccessfulTransaction > 0 + ? (now - this.lastSuccessfulTransaction) / 1000 / 60 + : Infinity; + + return { + isPaused, + pauseRemainingMinutes, + rewardAccumulationRate: this.rewardAccumulationRate, + lastTransactionMinutesAgo, + samples: this.totalRewardsSample.length + }; + } + + /** + * Override pause (for manual resume) + */ + resumeOperations(): void { + this.pauseUntil = 0; + this.simpleLogger.critical(`鈻讹笍 Operations resumed manually`); + } +} \ No newline at end of file