Execution Strategies
Advanced strategies for profitable DCA order execution
Understanding the Dutch Auction Mechanism
DCA.fun implements a Dutch auction system that creates predictable arbitrage opportunities for fillers while ensuring users get fair execution prices over time.
How the Auction Works
The protocol adjusts the effective price of the input token based on time elapsed since the order became executable:
adjustedTokenInPrice = tokenInPrice × (10000 + slippage - scalingFactor) / 10000
where:
- scalingFactor = (timeElapsed / scalingInterval) × slippage × 2
- timeElapsed = current time - order execution time
- slippage and scalingFactor are in basis pointsTimeline Example
For an order with 100 bp (1%) slippage and 120-second scaling interval:
Time | Scaling | User Price | Filler Opportunity
--------|---------|---------------|-------------------
T+0s | 0% | Market +1% | No profit
T+30s | 25% | Market +0.5% | 0.5% profit
T+60s | 50% | Market | 1% profit
T+90s | 75% | Market -0.5% | 1.5% profit
T+120s | 100% | Market -1% | 2% profit (max)Key Parameters
- Slippage: User-defined max price deviation (typically 50-200 bp)
- Scaling Interval: Time to reach max discount (max 300 seconds)
- Frequency Interval: Time between order executions
- Protocol Fee: Small fee taken from output tokens
Profit Calculation
Basic Formula
function calculateFillerProfit(orderParams, currentPrices, timeElapsed) {
// Calculate scaling factor
const scalingFactor = Math.min(
(timeElapsed / orderParams.scalingInterval) * orderParams.slippage * 2,
orderParams.slippage * 2
);
// Adjusted price user pays
const adjustedPrice = currentPrices.tokenIn *
(10000 + orderParams.slippage - scalingFactor) / 10000;
// Value calculations
const inputValue = orderParams.amount * adjustedPrice;
const outputValue = orderParams.amount * currentPrices.tokenIn;
// Profit calculation
const grossProfit = inputValue - outputValue;
const gasEstimate = estimateGasInUSD();
const netProfit = grossProfit - gasEstimate;
return {
grossProfit,
netProfit,
profitPercentage: (grossProfit / outputValue) * 100,
optimalWaitTime: orderParams.scalingInterval // Maximum profit point
};
}Advanced Considerations
- Gas Costs: Must be factored into profitability
- Price Volatility: Market movements during execution
- Competition: Other fillers may execute first
- Slippage on DEX: Your swap execution costs
Optimal Timing Strategy
The Timing Dilemma
Fillers face a trade-off:
- Wait longer: Higher profit per fill
- Execute early: Guarantee execution before competitors
Optimal Strategy Framework
class OptimalFillingStrategy {
constructor(config) {
this.minProfitUSD = config.minProfitUSD || 1;
this.gasPrice = config.gasPrice;
this.competitionFactor = config.competitionFactor || 0.7; // 0-1
}
calculateOptimalExecutionTime(order, currentTime) {
const timeSinceExecutable = currentTime - order.nextExecutionTime;
// Base optimal time (maximum profit)
let optimalTime = order.scalingInterval;
// Adjust for competition
optimalTime *= this.competitionFactor;
// Adjust for order value
const orderValueUSD = this.calculateOrderValue(order);
if (orderValueUSD > 1000) {
// High-value orders: execute earlier
optimalTime *= 0.5;
} else if (orderValueUSD < 100) {
// Low-value orders: wait for max profit
optimalTime *= 0.9;
}
// Check if we've reached optimal time
return timeSinceExecutable >= optimalTime;
}
shouldExecute(order, quote) {
const profit = this.calculateProfit(quote);
const optimalTimeReached = this.calculateOptimalExecutionTime(
order,
Date.now() / 1000
);
// Execute if:
// 1. Profit exceeds minimum AND optimal time reached
// 2. OR profit is exceptionally high (>2x minimum)
return (
(profit.netProfit > this.minProfitUSD && optimalTimeReached) ||
(profit.netProfit > this.minProfitUSD * 2)
);
}
}MEV Protection Strategies
Understanding MEV Risks
Fillers face several MEV-related risks:
- Front-running: Competitors seeing and copying your transaction
- Sandwich attacks: Being trapped between buy and sell orders
- Block stuffing: Validators filling blocks to exclude your tx
Protection Mechanisms
1. Private Mempools
Use services like Flashbots to keep transactions private:
const { FlashbotsBundleProvider } = require("@flashbots/ethers-provider-bundle");
async function submitPrivateTransaction(tx) {
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
authSigner,
"https://relay.flashbots.net"
);
const bundle = [
{
signedTransaction: tx
}
];
const bundleSubmission = await flashbotsProvider.sendBundle(
bundle,
targetBlockNumber
);
return bundleSubmission;
}2. Commit-Reveal Schemes
For high-value orders, use a two-phase approach:
class CommitRevealFiller {
async commitToFill(orderId, secretHash) {
// Phase 1: Commit to filling with a hash
const commitment = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["uint256", "address", "bytes32"],
[orderId, this.address, secretHash]
)
);
// Store commitment on-chain or off-chain
await this.storeCommitment(commitment);
// Wait for commitment period
await this.wait(COMMITMENT_PERIOD);
// Phase 2: Reveal and execute
return this.revealAndFill(orderId, secretHash);
}
}3. Dynamic Gas Pricing
Adjust gas dynamically to avoid being priced out:
async function calculateOptimalGasPrice() {
const baseGasPrice = await provider.getGasPrice();
const profitMargin = calculateExpectedProfit();
// Scale gas price with profit opportunity
const maxGasPrice = baseGasPrice.mul(150).div(100); // 1.5x base
const optimalGasPrice = baseGasPrice.add(
profitMargin.mul(10).div(100) // Use 10% of profit for gas
);
return optimalGasPrice.lt(maxGasPrice) ? optimalGasPrice : maxGasPrice;
}Competition Analysis
Monitoring Competitors
Track other fillers to optimize your strategy:
class CompetitorAnalyzer {
constructor(dcaContract) {
this.competitors = new Map();
this.dcaContract = dcaContract;
// Monitor fill events
dcaContract.on("FillOrder", this.handleFillEvent.bind(this));
}
handleFillEvent(orderId, filler, recipient, fillableAmount) {
if (!this.competitors.has(filler)) {
this.competitors.set(filler, {
fills: 0,
volume: BigNumber.from(0),
avgResponseTime: 0
});
}
const competitor = this.competitors.get(filler);
competitor.fills++;
competitor.volume = competitor.volume.add(fillableAmount);
// Calculate average response time
this.updateResponseTime(filler, orderId);
}
getCompetitionLevel(orderId) {
// Analyze how many competitors typically fill similar orders
const similarOrders = this.findSimilarOrders(orderId);
const uniqueFillers = new Set(
similarOrders.map(o => o.filler)
).size;
return {
level: uniqueFillers > 10 ? "high" : uniqueFillers > 5 ? "medium" : "low",
count: uniqueFillers,
recommendation: this.getStrategyRecommendation(uniqueFillers)
};
}
}Portfolio Management
Diversification Strategy
Don't put all eggs in one basket:
class FillerPortfolio {
constructor(config) {
this.maxExposurePerToken = config.maxExposurePerToken || 0.2; // 20%
this.maxExposurePerOrder = config.maxExposurePerOrder || 0.05; // 5%
this.reserveRatio = config.reserveRatio || 0.3; // 30% reserve
}
async evaluateOrder(order, quote) {
const portfolio = await this.getCurrentPortfolio();
const orderValue = this.calculateOrderValue(quote);
// Check token exposure
const tokenExposure = portfolio.tokenExposure[order.tokenOut] || 0;
const newExposure = tokenExposure + orderValue;
if (newExposure / portfolio.totalValue > this.maxExposurePerToken) {
return { shouldFill: false, reason: "Token exposure limit" };
}
// Check order size
if (orderValue / portfolio.totalValue > this.maxExposurePerOrder) {
return { shouldFill: false, reason: "Order size limit" };
}
// Check reserves
const availableCapital = portfolio.totalValue * (1 - this.reserveRatio);
if (orderValue > availableCapital) {
return { shouldFill: false, reason: "Insufficient reserves" };
}
return { shouldFill: true };
}
}Automated Execution Framework
Complete Bot Implementation
class DCAFillerBot {
constructor(config) {
this.dcaContract = config.dcaContract;
this.strategy = new OptimalFillingStrategy(config.strategy);
this.portfolio = new FillerPortfolio(config.portfolio);
this.competitors = new CompetitorAnalyzer(config.dcaContract);
this.activeOrders = new Map();
}
async start() {
// Monitor new orders
this.dcaContract.on("CreateOrder", this.handleNewOrder.bind(this));
// Periodic scanning
setInterval(() => this.scanOrders(), 30000); // Every 30 seconds
console.log("Filler bot started");
}
async handleNewOrder(orderId, creator, params) {
const order = {
id: orderId,
creator,
nextExecution: Date.now() / 1000 + params.freqInterval,
...params
};
this.activeOrders.set(orderId, order);
this.scheduleExecution(order);
}
async scheduleExecution(order) {
const optimalDelay = this.strategy.calculateOptimalExecutionTime(
order,
Date.now() / 1000
);
setTimeout(
() => this.attemptFill(order.id),
optimalDelay * 1000
);
}
async attemptFill(orderId) {
try {
// 1. Get quote
const quote = await this.getQuote(orderId);
// 2. Evaluate profitability
if (!this.strategy.shouldExecute(this.activeOrders.get(orderId), quote)) {
console.log(`Order ${orderId} not profitable yet`);
return;
}
// 3. Check portfolio limits
const portfolioCheck = await this.portfolio.evaluateOrder(
this.activeOrders.get(orderId),
quote
);
if (!portfolioCheck.shouldFill) {
console.log(`Order ${orderId} rejected: ${portfolioCheck.reason}`);
return;
}
// 4. Check competition
const competition = this.competitors.getCompetitionLevel(orderId);
if (competition.level === "high") {
// Use MEV protection for high competition
await this.fillWithMEVProtection(orderId, quote);
} else {
// Standard fill
await this.standardFill(orderId, quote);
}
} catch (error) {
console.error(`Failed to fill order ${orderId}:`, error);
}
}
async standardFill(orderId, quote) {
const gasPrice = await calculateOptimalGasPrice();
const tx = await this.dcaContract.fillOrder(
quote.encodedData,
this.wallet.address,
{ gasPrice }
);
console.log(`Filled order ${orderId}: ${tx.hash}`);
return tx.wait();
}
async fillWithMEVProtection(orderId, quote) {
// Use Flashbots or similar
return submitPrivateTransaction(
await this.prepareFillTransaction(orderId, quote)
);
}
}
// Start the bot
const bot = new DCAFillerBot({
dcaContract: dcaDotFun,
strategy: {
minProfitUSD: 1,
competitionFactor: 0.7
},
portfolio: {
maxExposurePerToken: 0.2,
maxExposurePerOrder: 0.05,
reserveRatio: 0.3
}
});
bot.start();Performance Optimization
1. Batch Processing
Process multiple orders in single transaction:
async function batchFillOrders(orderIds) {
const quotes = await Promise.all(
orderIds.map(id => getQuote(id))
);
// Filter profitable orders
const profitableOrders = quotes.filter(
q => calculateProfit(q).netProfit > MIN_PROFIT
);
// Use multicall for batch execution
const multicall = new Multicall(provider);
const calls = profitableOrders.map(q => ({
target: DCA_CONTRACT,
callData: dcaInterface.encodeFunctionData("fillOrder", [
q.encodedData,
fillerAddress
])
}));
return multicall.aggregate(calls);
}2. Caching Strategy
Cache frequently accessed data:
class CachedDataProvider {
constructor(ttl = 30000) { // 30 second TTL
this.cache = new Map();
this.ttl = ttl;
}
async getTokenPrice(token) {
const cacheKey = `price_${token}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.value;
}
const price = await fetchTokenPrice(token);
this.cache.set(cacheKey, {
value: price,
timestamp: Date.now()
});
return price;
}
}Risk Management
Stop-Loss Mechanisms
Implement safeguards to limit losses:
class RiskManager {
constructor(config) {
this.maxDailyLoss = config.maxDailyLoss || 100; // USD
this.maxConsecutiveLosses = config.maxConsecutiveLosses || 5;
this.dailyStats = { profit: 0, losses: 0, consecutiveLosses: 0 };
}
async evaluateFill(order, expectedProfit) {
// Check daily loss limit
if (this.dailyStats.losses > this.maxDailyLoss) {
throw new Error("Daily loss limit reached");
}
// Check consecutive losses
if (this.dailyStats.consecutiveLosses >= this.maxConsecutiveLosses) {
throw new Error("Too many consecutive losses");
}
return true;
}
recordResult(profit) {
if (profit < 0) {
this.dailyStats.losses += Math.abs(profit);
this.dailyStats.consecutiveLosses++;
} else {
this.dailyStats.profit += profit;
this.dailyStats.consecutiveLosses = 0;
}
}
}Best Practices Summary
- Start Conservative: Begin with small fills to understand dynamics
- Monitor Constantly: Track performance metrics and adjust strategies
- Diversify: Don't focus on single token pairs or order sizes
- Protect Against MEV: Use private mempools for valuable fills
- Manage Risk: Implement stop-losses and exposure limits
- Optimize Gas: Balance speed with cost efficiency
- Stay Updated: Monitor protocol changes and market conditions
- Test Thoroughly: Use testnets before mainnet deployment
- Document Everything: Keep detailed logs for analysis
- Collaborate: Share strategies with the community (keeping alpha private)