Skip to content
Merged

c #38

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/profitability/ProfitabilityEngineWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
15 changes: 15 additions & 0 deletions src/profitability/interfaces/IProfitabilityEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,19 @@ export interface IGovLstProfitabilityEngine {
analyzeAndGroupDeposits(
deposits: GovLstDeposit[],
): Promise<GovLstBatchAnalysis>;

/**
* 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;
}
38 changes: 38 additions & 0 deletions src/profitability/strategies/GovLstProfitabilityEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
217 changes: 217 additions & 0 deletions src/utils/RewardAccumulationTracker.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
}
Loading