ソースを参照

feat: copy parent positions instead of target's positions

- Add getPositionDetail() API method to fetch position details
- Check if target's position is a copied position (has parentPositionAddress)
- If copied, fetch and copy the PARENT position instead
- Maintain mapping between target's position and parent position
- When target closes their position, close the corresponding parent position
- Update cache and closed logic to track parent positions
zhangchunrui 1 ヶ月 前
コミット
a5faba7a7c
2 ファイル変更101 行追加37 行削除
  1. 81 37
      src/core/sniper.js
  2. 20 0
      src/services/byreal.js

+ 81 - 37
src/core/sniper.js

@@ -8,19 +8,20 @@ export class SniperEngine {
     this.closedCache = new PositionCache('.closed-positions.json', CONFIG.DATA_DIR);
     this.initialPositions = new Set(); // Store positions at startup
     this.hasInitialized = false; // Track if initialization is complete
+    this.targetToParentMap = new Map(); // Map target's position -> parent position we copied
     this.swapper = new JupiterSwapper();
     this.isRunning = false;
     this.myWallet = CONFIG.MY_WALLET || this.swapper.getWalletAddress();
   }
 
-  async analyzePosition(position, poolInfo) {
+  async analyzePosition(positionDetail, poolInfo) {
     const decimalsA = poolInfo.mintA?.decimals || 6;
     const decimalsB = poolInfo.mintB?.decimals || 6;
-    const currentPrice = parseFloat(poolInfo.mintB?.price || 0);
+    const currentPrice = parseFloat(poolInfo.mintB?.price || poolInfo.baseMint?.price || 0);
 
     try {
-      const lowerPrice = await ByrealAPI.getPriceFromTick(position.lowerTick, decimalsA, decimalsB);
-      const upperPrice = await ByrealAPI.getPriceFromTick(position.upperTick, decimalsA, decimalsB);
+      const lowerPrice = await ByrealAPI.getPriceFromTick(positionDetail.lowerTick, decimalsA, decimalsB);
+      const upperPrice = await ByrealAPI.getPriceFromTick(positionDetail.upperTick, decimalsA, decimalsB);
       
       const min = Math.min(lowerPrice, upperPrice);
       const max = Math.max(lowerPrice, upperPrice);
@@ -38,11 +39,11 @@ export class SniperEngine {
     }
   }
 
-  async executeCopy(position, poolInfo) {
-    const targetUsdValue = parseFloat(position.totalUsdValue || position.liquidityUsd || 0);
+  async executeCopy(parentDetail, poolInfo, parentPositionAddress, targetPositionAddress) {
+    const targetUsdValue = parseFloat(parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
     
     if (targetUsdValue <= 0) {
-      logger.warn('Position has no USD value, skipping');
+      logger.warn('Parent position has no USD value, skipping');
       return false;
     }
 
@@ -56,8 +57,8 @@ export class SniperEngine {
       return false;
     }
 
-    logger.info(`Copying position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
-    logger.info(`Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
+    logger.info(`Copying PARENT position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
+    logger.info(`Parent Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
 
     const mintA = poolInfo.mintA?.address;
     const mintB = poolInfo.mintB?.address;
@@ -107,21 +108,24 @@ export class SniperEngine {
       }
     }
 
-    // Execute copy
+    // Execute copy of PARENT position
     const success = await ByrealAPI.copyPosition(
-      position.nftMintAddress,
-      position.positionAddress,
+      parentDetail.nftMint,
+      parentPositionAddress,
       copyUsdValue
     );
     
     if (success) {
-      this.copiedCache.add(position.positionAddress, {
-        poolAddress: position.poolAddress,
+      this.copiedCache.add(parentPositionAddress, {
+        poolAddress: parentDetail.poolAddress,
         targetUsdValue,
         copiedAt: new Date().toISOString(),
+        targetPositionAddress: targetPositionAddress, // Store which target position triggered this copy
       });
+      // Map target's position to the parent position we copied
+      this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
     }
-    
+
     return success;
   }
 
@@ -185,36 +189,72 @@ export class SniperEngine {
         continue;
       }
 
-      const poolInfo = poolMap[position.poolAddress];
+      logger.info(`\n🆕 New position detected in target wallet: ${positionAddress}`);
+      
+      // Fetch position detail to check if it's a copied position
+      logger.info('Fetching position detail to check parent...');
+      const positionDetail = await ByrealAPI.getPositionDetail(positionAddress);
+      
+      if (!positionDetail) {
+        logger.error(`Failed to fetch detail for position ${positionAddress}`);
+        continue;
+      }
+
+      // Check if this is a copied position (has parentPositionAddress)
+      const parentPositionAddress = positionDetail.parentPositionAddress;
+      
+      if (!parentPositionAddress) {
+        logger.info('Position is not a copy (no parent), skipping...');
+        // Mark as processed so we don't check again
+        this.initialPositions.add(positionAddress);
+        continue;
+      }
+
+      logger.success(`Found copied position! Parent: ${parentPositionAddress}`);
+      
+      // Now we copy the PARENT position, not the target's position
+      const parentDetail = await ByrealAPI.getPositionDetail(parentPositionAddress);
+      
+      if (!parentDetail) {
+        logger.error(`Failed to fetch parent position detail: ${parentPositionAddress}`);
+        continue;
+      }
+
+      const poolInfo = parentDetail.pool;
       if (!poolInfo) {
-        logger.warn(`Pool info not found for ${position.poolAddress}`);
+        logger.warn(`Pool info not found for parent position ${parentPositionAddress}`);
         continue;
       }
 
-      logger.info(`\n🆕 New position detected: ${positionAddress}`);
-      logger.info(`Pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
+      logger.info(`Parent pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
+      logger.info(`Parent position NFT: ${parentDetail.nftMint}`);
 
-      const analysis = await this.analyzePosition(position, poolInfo);
+      const analysis = await this.analyzePosition(parentDetail, poolInfo);
 
       if (analysis.inRange) {
-        logger.success('Position is IN RANGE - ready to copy!');
+        logger.success('Parent position is IN RANGE - ready to copy!');
 
         const isCopied = await ByrealAPI.checkIfCopied(
-          position.poolAddress,
-          positionAddress,
+          parentDetail.poolAddress,
+          parentPositionAddress,
           this.myWallet
         );
         
         if (isCopied) {
-          logger.info('Already copied (confirmed via API)');
-          this.copiedCache.add(positionAddress, { poolAddress: position.poolAddress });
+          logger.info('Parent position already copied (confirmed via API)');
+          this.copiedCache.add(parentPositionAddress, { poolAddress: parentDetail.poolAddress });
+          // Still map target's position to parent even if already copied
+          this.targetToParentMap.set(positionAddress, parentPositionAddress);
         } else {
-          await this.executeCopy(position, poolInfo);
+          await this.executeCopy(parentDetail, poolInfo, parentPositionAddress, positionAddress);
         }
       } else {
-        logger.info('Position is OUT OF RANGE');
+        logger.info('Parent position is OUT OF RANGE');
       }
 
+      // Mark target's position as processed
+      this.initialPositions.add(positionAddress);
+
       await sleep(500);
     }
   }
@@ -225,21 +265,25 @@ export class SniperEngine {
     const { positions } = await ByrealAPI.fetchTargetPositions(CONFIG.TARGET_WALLET);
     const targetPositionAddresses = new Set(positions.map(p => p.positionAddress));
     
-    const myCopiedPositions = this.copiedCache.getAll();
-
-    for (const positionAddress of myCopiedPositions.positions) {
-      if (!targetPositionAddresses.has(positionAddress) && !this.closedCache.has(positionAddress)) {
-        logger.warn(`Target closed position: ${positionAddress}`);
-        logger.info('Closing my copy...');
+    // Check if any target positions that triggered our copies are now closed
+    for (const [targetPositionAddress, parentPositionAddress] of this.targetToParentMap.entries()) {
+      // If target's position is no longer active and we haven't closed our copy yet
+      if (!targetPositionAddresses.has(targetPositionAddress) && 
+          !this.closedCache.has(parentPositionAddress) &&
+          this.copiedCache.has(parentPositionAddress)) {
+        
+        logger.warn(`Target closed their copied position: ${targetPositionAddress}`);
+        logger.info(`Closing our copy of parent position: ${parentPositionAddress}`);
         
-        const success = await ByrealAPI.closePosition(positionAddress);
+        const success = await ByrealAPI.closePosition(parentPositionAddress);
         
         if (success) {
-          this.closedCache.add(positionAddress, {
+          this.closedCache.add(parentPositionAddress, {
             closedAt: new Date().toISOString(),
-            reason: 'Target closed position',
+            reason: `Target closed their position ${targetPositionAddress}`,
           });
-          this.copiedCache.remove(positionAddress);
+          this.copiedCache.remove(parentPositionAddress);
+          this.targetToParentMap.delete(targetPositionAddress);
         }
       }
     }

+ 20 - 0
src/services/byreal.js

@@ -26,6 +26,26 @@ export class ByrealAPI {
     }
   }
 
+  static async getPositionDetail(positionAddress) {
+    try {
+      const url = `${CONFIG.BYREAL_API_BASE}/position/detail`;
+      const params = {
+        address: positionAddress,
+      };
+
+      const response = await axios.get(url, { params });
+      
+      if (response.data.retCode !== 0) {
+        throw new Error(response.data.retMsg || 'Failed to fetch position detail');
+      }
+
+      return response.data.result?.data || null;
+    } catch (error) {
+      logger.error('Error fetching position detail:', error.message);
+      return null;
+    }
+  }
+
   static async getPriceFromTick(tick, decimalsA, decimalsB) {
     try {
       const url = `${CONFIG.TICK_API}?tick=${tick}&decimalsA=${decimalsA}&decimalsB=${decimalsB}&baseIn=false`;