Phase 3 — Adaptive Strategy
Make the earning zone width respond to market conditions. In calm markets, a narrow zone captures more fees per trade. In choppy markets, a wider zone avoids costly repositions.
Time: ~5 minutes | Requires: Phase 2 complete
The idea
In Phase 2, the earning zone is always 200 price units wide. But the right width depends on how volatile the market is:
- Calm market (price barely moves): narrow zone = more fee income, rare repositions
- Choppy market (price swings a lot): wide zone = fewer repositions, lower gas costs
The agent tracks how much the price has been moving and adjusts automatically.
New configuration
Add to .env:
# Volatility adaptation
VOLATILITY_WINDOW=60 # Number of cycles to look back (60 cycles = 5 hours at 5min intervals)
MIN_RANGE_TICKS=100 # Narrowest earning zone
MAX_RANGE_TICKS=400 # Widest earning zone
VOLATILITY_MULTIPLIER=3.0 # How aggressively to widen for volatilityNew code: volatility calculation
Add these to agent.js:
const VOLATILITY_WINDOW = parseInt(process.env.VOLATILITY_WINDOW || '60');
const MIN_RANGE_TICKS = parseInt(process.env.MIN_RANGE_TICKS || '100');
const MAX_RANGE_TICKS = parseInt(process.env.MAX_RANGE_TICKS || '400');
const VOLATILITY_MULTIPLIER = parseFloat(process.env.VOLATILITY_MULTIPLIER || '3.0');
const BASE_RANGE = parseInt(process.env.POSITION_RANGE_TICKS || '200');
const tickHistory = []; // rolling window of price samples
function calculateVolatility() {
if (tickHistory.length < 2) return { volatility: 0, sampleSize: tickHistory.length };
const changes = [];
for (let i = 1; i < tickHistory.length; i++) {
changes.push(Math.abs(tickHistory[i].tick - tickHistory[i - 1].tick));
}
const mean = changes.reduce((a, b) => a + b, 0) / changes.length;
const variance = changes.reduce((a, b) => a + (b - mean) ** 2, 0) / changes.length;
return { volatility: Math.sqrt(variance), sampleSize: tickHistory.length };
}
function getAdaptiveRange(tickSpacing) {
const { volatility, sampleSize } = calculateVolatility();
if (sampleSize < 10) return BASE_RANGE; // not enough data yet
const adaptiveRange = Math.round(volatility * VOLATILITY_MULTIPLIER * 2);
const clamped = Math.max(MIN_RANGE_TICKS, Math.min(MAX_RANGE_TICKS, adaptiveRange));
return Math.ceil(clamped / tickSpacing) * tickSpacing || BASE_RANGE;
}Update the main cycle
At the start of each cycle, record the current price:
// Inside runCycle(), after getting the pool state:
tickHistory.push({ ts: Date.now(), tick: currentTick });
if (tickHistory.length > VOLATILITY_WINDOW) tickHistory.shift();Update position opening
Replace the fixed RANGE with the adaptive range everywhere you open a position:
// Instead of:
const tickLower = Math.floor((currentTick - RANGE) / tickSpacing) * tickSpacing;
// Use:
const range = getAdaptiveRange(tickSpacing);
const tickLower = Math.floor((currentTick - range) / tickSpacing) * tickSpacing;
const tickUpper = Math.ceil((currentTick + range) / tickSpacing) * tickSpacing;Do this in both openPosition() and the rebalance flow.
How it works
The agent keeps a rolling window of the last 60 price readings. It calculates the standard deviation of price changes (how much the price typically moves between checks).
- Standard deviation near 0 = calm market = use minimum range (100)
- Standard deviation around 30 = moderate = use ~180 range
- Standard deviation above 60 = choppy = use maximum range (400)
The VOLATILITY_MULTIPLIER controls how aggressively the range responds. Higher = wider zones sooner. The default (3.0) is conservative.
Next step
You’re now optimizing within one pool. Phase 4 — Compare Pools scans all Cetus pools to find higher-yielding opportunities.