Calculating Impermanent Loss in Real-Time on Solana using the GoldRush Streaming API - Part 2

Zeeshan Jawed
Content Writer
Build a Node.js engine to compute real-time Impermanent Loss and fees for standard and CLMM pools. Get the complete TypeScript code here

Introduction: Building the Calculation Engine

In Part 1, we built the most critical and most frequently overlooked piece of any profitability tracker: Memory. Our ingestor.ts script successfully scanned our wallet's Solana transaction history using the Covalent GoldRush API and created a "Ledger of Truth" in an SQLite database.

We now have our "Anchor"—a cryptographically verified record of every LP deposit, containing:

  • token_a_initial_amount

  • token_a_initial_price

  • token_b_initial_amount

  • token_b_initial_price

  • timestamp

This database of "what we did" is our anchor in the past. Now, we need to build the engine that compares this anchor to the chaotic reality of the present.

In Part 2, we will build the "Brain." We will write the code to:

  1. Fetch live, real-time pool data (reserves and prices) from Solana.

  2. Implement the core Impermanent Loss (IL) formula in TypeScript, comparing our "Anchor" state to the "Current" state.

  3. Go Beyond IL to calculate true profitability by fetching and including accrued trading fees.

  4. Tackle the advanced case of Concentrated Liquidity (Orca Whirlpools), which uses different math.

  5. Combine everything into a single, runnable script that produces a complete PnL report for every position in our database.

Step 1: Fetching Live On-Chain Data (The "Current Reality")

To calculate IL, we need to know the current state of two things for every position in our database:

  1. Current Token Prices: What are SOL and RAY worth right now?

  2. Current Pool State: What is the total reserve of SOL and RAY in the pool, and what is the total supply of LP tokens?

We will use the Covalent GoldRush API for both, ensuring a single, consistent data source.

Let's create a new file, src/monitor.ts. This will be our main application file for this part.

//create src/monitor.ts import { GoldRushClient } from "@covalenthq/client-sdk"; import * as dotenv from "dotenv"; import { initDB } from "./db"; // Import from Part 1 import { Database } from "sqlite"; dotenv.config(); const API_KEY = process.env.GOLDRUSH_API_KEY!; const WALLET = process.env.WALLET_ADDRESS!; const client = new GoldRushClient(API_KEY); // --- 1. LIVE PRICE FETCHER --- /** * Fetches the current USD price for a list of token addresses on Solana. */ async function fetchCurrentPrices(tokenAddresses: string[]): Promise<Map<string, number>> {     console.log(`Fetching live prices for ${tokenAddresses.length} tokens...`);     const priceMap = new Map<string, number>();     const resp = await client.PricingService.getTokenPrices(         "solana-mainnet",         "USD",         tokenAddresses     );     if (resp.error) {         console.error("Failed to fetch prices:", resp.error_message);         return priceMap;     }     resp.data.items.forEach(item => {         priceMap.set(item.contract_address, item.price);     });         return priceMap; } // --- 2. LIVE POOL STATE FETCHER --- /** * Fetches the current reserves and LP supply for a specific pool. */ async function fetchCurrentPoolState(poolAddress: string): Promise<any> {     // We use the GoldRush DEXService, which is optimized for AMMs     const resp = await client.DEXService.getPoolByAddress(         "solana-mainnet",         poolAddress     );         if (resp.error) {         console.error(`Failed to fetch pool state for ${poolAddress}:`, resp.error_message);         return null;     }         // We only care about the first pool found     const pool = resp.data.items[0];         return {         current_total_lp_supply: parseFloat(pool.total_liquidity) / Math.pow(10, pool.lp_token_decimals),         current_reserve_a: parseFloat(pool.token_0.reserve) / Math.pow(10, pool.token_0.contract_decimals),         current_reserve_b: parseFloat(pool.token_1.reserve) / Math.pow(10, pool.token_1.contract_decimals),     }; }

This code gives us our two crucial data streams. The fetchCurrentPrices function gives us the data for the "HODL" scenario, and fetchCurrentPoolState gives us the data for the "Current LP" scenario.

Step 2: The Mathematics of IL for Developers (with Code)

This is the core of the guide. We will now translate the abstract, academic IL formula into clean, reusable TypeScript functions.

The standard formula for IL is:

𝐼𝐿 = (2 * 𝑠𝑞𝑟𝑡(𝑘)) / (1 + 𝑘) - 1

...where k is the price ratio change.

This formula is elegant but not practical for coding. A developer-friendly approach is to compare two distinct portfolio values:

Impermanent Loss = (Value if HODL) - (Value in LP)

Let's build functions for each part.

Implementing the "HODL" Value

This is the "Alternate Reality" calculation. We take the initial quantities of tokens we deposited (from our database) and multiply them by the current prices (from the API).

// Add this to src/monitor.ts /** * Calculates the current USD value of our *initial* bundle of tokens. * This is the "What if I just held?" scenario. */ function calculateHodlValue(position: any, currentPrices: Map<string, number>): number {     const priceA = currentPrices.get(position.token_a_address) || 0;     const priceB = currentPrices.get(position.token_b_address) || 0;         const hodlValueA = position.token_a_initial_amount * priceA;     const hodlValueB = position.token_b_initial_amount * priceB;         return hodlValueA + hodlValueB; }

Implementing the "Current LP" Value

This is the "Current Reality" calculation. When you provide liquidity, the Automated Market Maker (AMM) constantly changes the ratio of your tokens. If SOL pumps, you will end up with less SOL and more USDC.

To find out what you own right now, you must determine your percentage share of the pool and apply it to the pool's current reserves.

// Add this to src/monitor.ts /** * Calculates the current USD value of our holdings *inside* the LP. * This is the "What's my LP token worth?" scenario. */ function calculateCurrentLpValue(     position: any,     poolState: any,     currentPrices: Map<string, number> ): { value: number, currentAmountA: number, currentAmountB: number } {         // 1. Calculate our share of the pool     const userShare = position.lp_initial_amount / poolState.current_total_lp_supply;         // 2. Determine our current token amounts     // Note: We check symbol to match our 'A' and 'B' from the DB     let currentAmountA, currentAmountB;     if (position.token_a_symbol === poolState.token_0_symbol) {         currentAmountA = poolState.current_reserve_a * userShare;         currentAmountB = poolState.current_reserve_b * userShare;     } else {         currentAmountA = poolState.current_reserve_b * userShare;         currentAmountB = poolState.current_reserve_a * userShare;     }     // 3. Calculate the USD value of those amounts     const priceA = currentPrices.get(position.token_a_address) || 0;     const priceB = currentPrices.get(position.token_b_address) || 0;         const value = (currentAmountA * priceA) + (currentAmountB * priceB);         return { value, currentAmountA, currentAmountB }; }

Implementing the Final IL Formula

Now we just combine them.

// Add this to src/monitor.ts interface ILReport {     hodlValue: number;     currentLpValue: number;     impermanentLossUSD: number;     impermanentLossPct: number; } function calculateImpermanentLoss(     position: any,     poolState: any,     currentPrices: Map<string, number> ): ILReport {         const hodlValue = calculateHodlValue(position, currentPrices);     const { value: currentLpValue } = calculateCurrentLpValue(position, poolState, currentPrices);         if (hodlValue === 0) {         // Avoid divide-by-zero, e.g., if API prices are missing         return { hodlValue: 0, currentLpValue: 0, impermanentLossUSD: 0, impermanentLossPct: 0 };     }     const impermanentLossUSD = hodlValue - currentLpValue;     const impermanentLossPct = (impermanentLossUSD / hodlValue) * 100;         return {         hodlValue,         currentLpValue,         impermanentLossUSD,         impermanentLossPct     }; }

You now have a production-ready function that can calculate the exact IL for any standard AMM position, as long as you have the "Anchor" data.

Step 3: Beyond Impermanent Loss: Calculating True Profitability

Here is the secret: Impermanent Loss doesn't matter. Only Total Profitability does.

IL is an opportunity cost (HODL vs. LP), not a real PnL. It famously ignores the entire reason we provide liquidity: to earn trading fees.

If your IL is -5% but you earned +8% in fees, you are +3% profitable. Your strategy is working. A simple IL calculator would tell you you're losing.

True Profitability = (Current LP Value + Accrued Fees) - (Initial Deposit Value)

How do we find Accrued Fees?

This is notoriously difficult on standard AMMs, as fees are auto-compounded back into the pool, growing the pool's $k$ constant.

The Solution: We don't need to find the fees. The calculateCurrentLpValue function we already wrote implicitly includes the fees. Because the pool's reserves (current_reserve_a, current_reserve_b) have grown from fees, our userShare of that pool is automatically worth more.

Therefore, our True PnL (vs. HODL) is:

Net PnL = (Current LP Value) - (HODL Value)

A negative value means IL is larger than your fee rewards. A positive value means your fees have overcome your IL. This is the only number that matters.

Step 4: Special Case: Concentrated Liquidity (Orca Whirlpools)

This is the advanced topic that separates a basic tracker from a pro tool. On Raydium, you provide liquidity across the entire price range ($0 to $\infty$). On Orca Whirlpools (a fork of Uniswap V3), you provide liquidity in a specific price range (e.g., SOL between $150 and $160).

Why this breaks our math:

  1. IL is NOT gradual: If the price moves outside your range, your position becomes 100% one asset (e.g., all USDC or all SOL). Your IL is no longer "impermanent"—it's fully realized until the price moves back.

  2. Fees are NOT implicit: Fees are collected separately and must be claimed. They do not auto-compound into the pool.

We cannot use the (x*y=k) math from Step 2. We must query the Covalent DEXService for specialized Uniswap V3-style pool data.

// Add this to src/monitor.ts /** * Fetches the state of a Concentrated Liquidity position. * This is different from a standard pool! */ async function fetchConcentratedPosition(walletAddress: string, lpMintAddress: string): Promise<any> {         // We use a different GoldRush endpoint for V3-style positions     const resp = await client.DEXService.getLpPosition(         "solana-mainnet",         walletAddress,         lpMintAddress     );     if (resp.error || resp.data.items.length === 0) {         console.error(`Failed to get CLMM position: ${lpMintAddress}`);         return null;     }     const pos = resp.data.items[0];         // This endpoint provides the exact data we need, decoded.     return {         currentAmountA: parseFloat(pos.token_0.amount),         currentAmountB: parseFloat(pos.token_1.amount),         unclaimedFeesA: parseFloat(pos.fee_unclaimed_0),         unclaimedFeesB: parseFloat(pos.fee_unclaimed_1)     }; }

Now, we can write a different PnL function for these positions.

// Add this to src/monitor.ts async function calculateConcentratedPnL(     position: any,     currentPrices: Map<string, number> ): Promise<any> {         // 1. Get the "HODL" value (same as before)     const hodlValue = calculateHodlValue(position, currentPrices);         // 2. Get the "Current" value from the specialized endpoint     const clmmState = await fetchConcentratedPosition(WALLET, position.lp_mint_address);     if (!clmmState) return { error: "Failed to fetch CLMM state" };     const priceA = currentPrices.get(position.token_a_address) || 0;     const priceB = currentPrices.get(position.token_b_address) || 0;     // 3. Calculate Current Value (Assets + Fees)     const currentAssetValue = (clmmState.currentAmountA * priceA) + (clmmState.currentAmountB * priceB);     const feesValue = (clmmState.unclaimedFeesA * priceA) + (clmmState.unclaimedFeesB * priceB);     const totalCurrentValue = currentAssetValue + feesValue;         // 4. Calculate PnL vs HODL     const netPnlVsHodl = totalCurrentValue - hodlValue;     return {         hodlValue,         currentLpValue: currentAssetValue,         feesValue,         totalCurrentValue,         netPnlVsHodl,         ilAmount: hodlValue - currentAssetValue // IL is just (HODL - Current Assets), *excluding* fees     }; }

By using Covalent's specialized endpoints, we have completely bypassed the need to implement the complex sqrtPriceX96 and tick math ourselves.

Step 5: Building the Monitoring Service Script

Let's put it all together. We will now write the main function for src/monitor.ts that reads from our DB and runs all the calculations.

// Add this to the bottom of src/monitor.ts /** * MAIN EXECUTION FUNCTION */ async function runMonitor() {     const db = await initDB();     const positions = await db.all("SELECT * FROM positions WHERE is_active = 1");     if (positions.length === 0) {         console.log("No active positions found in database. Run 'npm run ingest' first.");         return;     }     console.log(`Monitoring ${positions.length} active LP positions...`);         // 1. Collect all unique tokens we need prices for     const allTokens = new Set<string>();     positions.forEach(p => {         allTokens.add(p.token_a_address);         allTokens.add(p.token_b_address);     });         // 2. Fetch all prices in one batch     const currentPrices = await fetchCurrentPrices(Array.from(allTokens));     // 3. Process each position     const reports = [];     for (const pos of positions) {         console.log(`\nProcessing: ${pos.token_a_symbol}/${pos.token_b_symbol} (Deposited ${new Date(pos.timestamp * 1000).toLocaleDateString()})`);                 let report: any;                 if (pos.protocol === "Raydium") { // Standard AMM             const poolState = await fetchCurrentPoolState(pos.pool_address);             if (!poolState) continue;                         const { hodlValue, currentLpValue, impermanentLossUSD, impermanentLossPct } =                 calculateImpermanentLoss(pos, poolState, currentPrices);             report = {                 Pair: `${pos.token_a_symbol}/${pos.token_b_symbol}`,                 "HODL Value": hodlValue.toFixed(2),                 "Current LP Value": currentLpValue.toFixed(2),                 "IL ($)": impermanentLossUSD.toFixed(2),                 "IL (%)": impermanentLossPct.toFixed(2),                 "Total PnL ($)": (currentLpValue - ((pos.token_a_initial_amount * pos.token_a_initial_price) + (pos.token_b_initial_amount * pos.token_b_initial_price))).toFixed(2)             };                     } else if (pos.protocol === "Orca") { // Concentrated Liquidity (CLMM)             // (Assuming 'Orca' was stored during ingestion for a CLMM pool)             const pnl = await calculateConcentratedPnL(pos, currentPrices);                         report = {                 Pair: `${pos.token_a_symbol}/${pos.token_b_symbol}`,                 "HODL Value": pnl.hodlValue.toFixed(2),                 "Current LP Value": pnl.currentLpValue.toFixed(2),                 "Fees ($)": pnl.feesValue.toFixed(2),                 "Total PnL ($)": pnl.netPnlVsHodl.toFixed(2),                 "IL ($)": pnl.ilAmount.toFixed(2)             };         }                 if (report) reports.push(report);     }         // 4. Display Final Report     console.log("\n--- REAL-TIME LP PROFITABILITY REPORT ---");     console.table(reports); } runMonitor();

Expected Output

When you run your new monitor.ts script, it will read your database, fetch live data, and print a complete PnL report to your console.

Report Analysis:

  • RAY/USDC: We have $5.10 of IL (1.02%), but our total PnL is +$65.99. This is a winning position! The fees we earned ($71.09) crushed our impermanent loss.

  • SOL/USDC: We have negative $30.20 of IL (which is a gain vs. HODL, or -3.18%). This happens if the price returned closer to our entry. Our total PnL is +$23.86.

Conclusion: We Have Our "Number"

We have successfully built the "Brain" of our monitor. We now have a single script that can, for any position in our database, instantly calculate its true PnL against the "HODL" alternative.

What we have built:

  1. Live Data Engine: Functions to fetch real-time prices and pool reserves from GoldRush.

  2. IL Calculator: A set of TypeScript functions that implement the AMM math.

  3. True PnL: Logic that goes beyond IL to include fees (implicitly for AMMs, explicitly for CLMMs).

  4. CLMM Solver: A solution for handling concentrated liquidity pools by using Covalent's specialized endpoints, saving us from implementing the complex math ourselves.

What is missing?

Right now, this is a manual tool. You have to run the script to see your PnL. This is not automation.

In Part 3, we will build the "Hands and Voice." We will:

  • Wrap this script in a loop (e.g., run every 5 minutes).

  • Add a Threshold Alert: If IL (%) > 5% or Total PnL ($) < $0, send an alert.

  • Implement Discord/Email notifications to "scream at you" when your strategy is failing.

  • Discuss advanced triggers for automated rebalancing or withdrawal.

Get Started

Get started with GoldRush API in minutes. Sign up for a free API key and start building.

Support

Explore multiple support options! From FAQs for self-help to real-time interactions on Discord.

Contact Sales

Interested in our professional or enterprise plans? Contact our sales team to learn more.