first commit

This commit is contained in:
Louis-Sinan
2025-12-21 12:22:16 +01:00
commit c9daaddb77
34 changed files with 1735 additions and 0 deletions

191
src/bot.ts Normal file
View File

@@ -0,0 +1,191 @@
import { MeteoraWrapper } from "./meteora";
import logger from "./utils/logger";
import { REBALANCE_THRESHOLD_PERCENT, COMPOUND_THRESHOLD_USD, JLP_USD_ESTIMATE, SOL_USD_ESTIMATE } from "./utils/config";
import { getSwapQuote, executeSwap } from "./swap";
import { PublicKey } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
export class DLMMBot {
private meteora: MeteoraWrapper;
constructor() {
this.meteora = new MeteoraWrapper();
}
async init(): Promise<void> {
await this.meteora.init();
}
async rebalance(): Promise<void> {
const startTime = new Date().toISOString();
try {
logger.info(`--- Portfolio Status Report [${startTime}] ---`);
// Force refresh of pool data in each cycle
await this.meteora.init();
const activeBin = await this.meteora.getActiveBin();
const positions = await this.meteora.getPositions();
logger.info(`Active Bin ID: ${activeBin.binId}`);
logger.info(`Number of active positions: ${positions.length}`);
if (positions.length === 0) {
logger.info(
"No active positions found. Checking for tokens to deposit..."
);
await this.handleInitialDeposit(activeBin.binId);
return;
}
let priceRangeRebalance = false;
for (const position of positions) {
const lowerBinId = position.positionData.lowerBinId;
const upperBinId = position.positionData.upperBinId;
const range = upperBinId - lowerBinId;
const threshold = Math.floor(range * REBALANCE_THRESHOLD_PERCENT);
const lowerBoundary = lowerBinId + threshold;
const upperBoundary = upperBinId - threshold;
const distanceToLower = activeBin.binId - lowerBoundary;
const distanceToUpper = upperBoundary - activeBin.binId;
logger.info(`Position ${position.publicKey.toBase58()}: Range [${lowerBinId}, ${upperBinId}], Threshold Boundary [${lowerBoundary}, ${upperBoundary}]`);
logger.info(`Distance to boundaries: Lower=+${distanceToLower}, Upper=+${distanceToUpper}`);
if (activeBin.binId <= lowerBoundary || activeBin.binId >= upperBoundary) {
logger.info("Active bin reached boundary. Price range rebalance required.");
priceRangeRebalance = true;
break;
}
}
let rebalanceRequired = priceRangeRebalance;
let totalValueUSD = 0;
let hasSubstantialBalance = false;
if (!rebalanceRequired) {
// Value-based compound check
const { balanceX, balanceY } = await this.meteora.getBalances();
const tokenX = this.meteora.dlmmPool?.tokenX;
const tokenY = this.meteora.dlmmPool?.tokenY;
if (!tokenX || !tokenY) {
logger.warn("Pool tokens not available for value calculation.");
return;
}
const totalX = balanceX.toNumber() / 10 ** (tokenX.mint as any).decimals;
const totalY = balanceY.toNumber() / 10 ** (tokenY.mint as any).decimals;
const isXSol = tokenX.mint.address.toBase58() === "So11111111111111111111111111111111111111112";
const isYSol = tokenY.mint.address.toBase58() === "So11111111111111111111111111111111111111112";
let valueX = 0;
let valueY = 0;
if (isXSol) {
valueX = totalX * SOL_USD_ESTIMATE;
valueY = totalY * JLP_USD_ESTIMATE;
} else if (isYSol) {
valueY = totalY * SOL_USD_ESTIMATE;
valueX = totalX * JLP_USD_ESTIMATE;
} else {
valueX = totalX * JLP_USD_ESTIMATE;
valueY = totalY * JLP_USD_ESTIMATE;
}
totalValueUSD = valueX + valueY;
hasSubstantialBalance = totalValueUSD >= COMPOUND_THRESHOLD_USD;
if (hasSubstantialBalance) {
logger.info(`Substantial wallet balance detected ($${totalValueUSD.toFixed(2)}). Triggering value-based compounding...`);
rebalanceRequired = true;
} else {
logger.info(`Portfolio healthy. Uninvested balance ($${totalValueUSD.toFixed(2)}) is below threshold ($${COMPOUND_THRESHOLD_USD}).`);
logger.info(`--- Cycle Complete [${new Date().toISOString()}] ---`);
return;
}
}
await this.meteora.withdrawAll();
// Claim fees after withdrawal to consolidate balances
await this.meteora.claimFees();
const { balanceX, balanceY } = await this.meteora.getBalances();
if (balanceX.isZero() && balanceY.isZero()) {
logger.warn("No balances available to re-deposit after withdrawal.");
return;
}
// Financial Safety: Only skip if NOT a price-range rebalance and value is too low
const estimatedFeeUSD = 0.005 * SOL_USD_ESTIMATE;
if (!priceRangeRebalance && totalValueUSD < estimatedFeeUSD) {
logger.warn(`Skipping compounding: Total uninvested value ($${totalValueUSD.toFixed(2)}) is less than estimated transaction fees ($${estimatedFeeUSD.toFixed(2)}).`);
return;
}
await this.meteora.deposit(balanceX, balanceY, activeBin.binId);
logger.info("Rebalance/Compounding completed successfully.");
logger.info(`--- Cycle Complete [${new Date().toISOString()}] ---`);
} catch (error: any) {
logger.error(`CRITICAL: Error during rebalance cycle: ${error.message}`);
}
}
private async handleInitialDeposit(activeBinId: number): Promise<void> {
let { balanceX, balanceY } = await this.meteora.getBalances();
if (balanceX.isZero() && balanceY.isZero()) {
logger.info("No token balances found for initial deposit.");
return;
}
// Balanced investment logic: if one side is zero, swap 50%
const SOL_MINT = "So11111111111111111111111111111111111111112";
const pool = this.meteora.dlmmPool;
if (!pool) {
logger.error("Pool not initialized during handleInitialDeposit.");
return;
}
const isXSol = pool.tokenX.mint.address.toBase58() === SOL_MINT;
const isYSol = pool.tokenY.mint.address.toBase58() === SOL_MINT;
if (balanceX.isZero() || balanceY.isZero()) {
logger.info("Single asset detected. Performing 50/50 swap to balance the position...");
try {
if (isXSol && !balanceX.isZero()) {
const swapAmount = balanceX.div(new BN(2)).toNumber();
logger.info(`Swapping ${swapAmount / 1e9} SOL for TokenY...`);
const quote = await getSwapQuote(SOL_MINT, pool.tokenY.mint.address.toBase58(), swapAmount);
await executeSwap(quote);
} else if (isYSol && !balanceY.isZero()) {
const swapAmount = balanceY.div(new BN(2)).toNumber();
logger.info(`Swapping ${swapAmount / 1e9} SOL for TokenX...`);
const quote = await getSwapQuote(SOL_MINT, pool.tokenX.mint.address.toBase58(), swapAmount);
await executeSwap(quote);
} else {
logger.warn("Only non-SOL asset detected. Proceeding with single-sided deposit (no auto-swap for non-SOL yet).");
}
// Refetch balances after swap
({ balanceX, balanceY } = await this.meteora.getBalances());
} catch (error: any) {
logger.error(`Swap failed during initial deposit: ${error.message}. Proceeding with current balances.`);
}
}
logger.info(
`Initial deposit: X=${balanceX.toString()}, Y=${balanceY.toString()}`
);
await this.meteora.deposit(balanceX, balanceY, activeBinId);
}
async compound(): Promise<void> {
logger.info("Starting compounding...");
await this.meteora.claimFees();
logger.info("Compounding finished.");
}
}