[NeoX 튜토리얼] 시세차익 봇 개발하기 3편 - 자동 swap 개발하기

네오 블록체인·2025년 7월 10일

NeoX

목록 보기
11/12

시세차익(Arbitrage)을 노리는 봇에서 가장 중요한 건 정확한 타이밍입니다.
단순히 가격 차이가 있다고 무작정 스왑을 실행하면 손해를 볼 수도 있습니다.
슬리피지나 수수료뿐 아니라, 트랜잭션 가스비까지 감안했을 때 실제 수익이 남는지를 판단해야 하기 때문입니다.

이번 포스트에서는 바이낸스와 Carrot Swap의 GAS/USDT 가격을 비교한 뒤, Swap 이벤트를 통해 가격을 실시간으로 감지하고, 가스비까지 계산해 실제 수익이 날 경우에만 스왑을 실행하는 구조를 만들어보겠습니다.

1. Swap 이벤트로 Carrot Swap 가격 변화를 감지하기

Carrot Swap은 Uniswap V2 스타일의 AMM(Automated Market Maker)을 기반으로 동작합니다. 이 구조에서는 누군가 토큰을 스왑할 때마다 Swap 이벤트가 발생하며, 이 이벤트에는 실제로 교환된 토큰의 수량이 그대로 담깁니다.

기존에는 풀의 getReserves() 값을 직접 조회해 가격을 계산했지만, 이 방식은 매번 RPC 호출이 필요해 느리고 비효율적입니다.

대신 우리는 Swap 이벤트를 실시간으로 구독해서 가격을 캐싱합니다.

const router = new ethers.Contract(ROUTER_ADDRESS, ROUTER_ABI, provider);

router.on("Swap", (sender, amount0In, amount1In, amount0Out, amount1Out, to) => {
  const gasIn = amount0In.gt(0) ? amount0In : amount1In;
  const usdtOut = amount0Out.gt(0) ? amount0Out : amount1Out;

  const price = parseFloat(usdtOut.toString()) / parseFloat(gasIn.toString());
  cachedCarrotPrice = price;

  console.log(`[캐싱됨] Carrot 가격: 1 GAS ≈ ${price} USDT`);
});

위 코드에서는 Swap 이벤트를 구독한 뒤, 실제로 입력된 GAS와 출력된 USDT를 기준으로 가격을 계산해 cachedCarrotPrice에 저장합니다.

참고: token0과 token1의 순서에 따라 GAS가 amount0In일 수도, amount1In일 수도 있습니다.
이 예제에서는 단순화를 위해 둘 중 양수인 값을 GAS 입력으로, 대응되는 출력을 USDT로 간주합니다.
실제 배포 환경에서는 풀 구성에 맞게 정확히 매핑해야 합니다.

이제 Carrot Swap 쪽 GAS 가격은 스왑이 발생할 때마다 자동으로 갱신되며,
별도의 RPC 호출 없이도 가장 최신의 실거래 기준 가격을 사용할 수 있습니다.


2. 주기적으로 바이낸스 가격 조회하기

Carrot Swap의 GAS 가격은 이벤트로 실시간 감지할 수 있게 되었으니,
이제 남은 건 바이낸스에서 GAS를 살 때의 가격을 가져오는 일입니다.

우리가 참고할 가격은 매수호가(ask price)입니다.
즉, 바이낸스에서 GAS를 시장가로 매수할 때 얼마를 지불하게 될지를 의미하죠.

바이낸스는 이를 REST API로 제공하며, ticker/bookTicker 엔드포인트에서 간단히 조회할 수 있습니다:

async function refreshBinancePrice() {
  const res = await axios.get(
    "https://api.binance.com/api/v3/ticker/bookTicker?symbol=GASUSDT"
  );
  binanceAskPrice = parseFloat(res.data.askPrice);
}

위 함수는 바이낸스에서 현재 GAS를 시장가로 매수할 때의 가격을 binanceAskPrice 변수에 저장합니다.

보통 3~5초 간격으로만 호출해도 충분하며,
너무 자주 호출할 경우 바이낸스 측 rate limit에 걸릴 수 있으니 주의가 필요합니다.


3. 예상 수익률 계산하기

이제 Carrot Swap과 바이낸스의 GAS 가격을 모두 확보했으니, 이 두 가격을 비교해 실제 시세차익이 있는지를 계산해볼 수 있습니다.

하지만 단순히 가격 차이만 비교하면 안 됩니다.
각 거래소에는 다음과 같은 비용이 따르기 때문입니다:

  • 바이낸스 매수 시 수수료: 보통 0.1%
  • Carrot Swap에서 GAS 매도 시 수수료 및 슬리피지: 약 0.3%

이 모든 비용을 고려한 순수익률을 계산해야 실제로 수익이 나는지 알 수 있습니다.

아래는 이를 계산하는 함수입니다:

function calculateProfitRate(binanceAsk: number, carrotPrice: number) {
  const buyFee = 0.001;  // 바이낸스 수수료 0.1%
  const sellFee = 0.003; // Carrot Swap LP 수수료 + 슬리피지 추정

  const adjustedBuy = binanceAsk * (1 + buyFee);
  const adjustedSell = carrotPrice * (1 - sellFee);

  const profit = adjustedSell - adjustedBuy;
  const profitRate = profit / adjustedBuy;

  return { profit, profitRate };
}

이 함수는 다음을 계산합니다:

  • adjustedBuy: 바이낸스에서 GAS를 매수할 때 실제로 드는 비용
  • adjustedSell: Carrot Swap에서 GAS를 팔 때 실제로 얻는 수익
  • profitRate: 순수익률 ((수익 - 비용) / 비용)

4. 가스비 예측 및 수익성 판단

단순히 가격 차익과 수익률만 보고 스왑을 실행하면, 실제론 손해를 볼 수 있습니다.
스왑 트랜잭션에 들어가는 가스비가 예상보다 클 경우, 수익이 모두 사라지거나 오히려 손실로 이어질 수 있기 때문입니다.

따라서 우리는 반드시 스왑 실행 전 예상 가스비를 계산하고, 그 비용을 수익에서 차감한 결과가 양수인지 확인해야 합니다.

아래 함수는 swapExactTokensForTokens 트랜잭션에 대해 예상 가스비를 계산하고, 이를 기준으로 실제 수익이 남는지 판단합니다:

async function isProfitable(amountIn: BigNumber): Promise<boolean> {
  const { profit } = calculateProfitRate(binanceAskPrice, cachedCarrotPrice);

  const tx = await router.populateTransaction.swapExactTokensForTokens(
    amountIn,
    0, // slippage 고려한 최소 수령량은 실행 시 별도 설정
    [GAS_ADDRESS, USDT_ADDRESS],
    wallet.address,
    Math.floor(Date.now() / 1000) + 60
  );

  const estimatedGas = await provider.estimateGas({
    ...tx,
    from: wallet.address,
  });

  const gasPrice = await provider.getGasPrice();
  const gasCost = estimatedGas.mul(gasPrice); // 단위: wei

  const gasCostInUSDT = parseFloat(ethers.utils.formatUnits(gasCost, 18)) * cachedCarrotPrice;

  console.log(`예상 가스비: $${gasCostInUSDT.toFixed(4)} / 예측 수익: $${profit.toFixed(4)}`);

  return profit > gasCostInUSDT;
}

이 함수가 수행하는 핵심은 다음과 같습니다:
1. router.populateTransaction으로 실제 스왑 트랜잭션을 미리 구성합니다.
2. estimateGas()를 호출해 해당 트랜잭션에 소요될 가스량을 예측합니다.
3. 현재 gasPrice와 곱해 가스비 총합(wei)을 계산합니다.
4. 이 가스비를 USDT 기준으로 환산합니다 (단가: GAS → USDT 가격 사용)
5. 예측된 수익에서 가스비를 뺀 값이 양수일 경우에만 true를 반환합니다.

💡 중요한 점: 이 예시는 GAS를 입력으로 사용하는 스왑만 다룹니다.
만약 다른 토큰을 사용할 경우 가스비 환산에 사용할 환율 기준이 달라질 수 있습니다.

5. 스왑 실행 조건 및 실행 함수 호출

이제 모든 준비는 끝났습니다.
Carrot Swap에서 실시간으로 GAS 가격을 감지하고, 바이낸스에서 가격을 가져와 수익률을 계산한 뒤, 가스비까지 고려해 진짜 수익이 남는 상황인지까지 판단할 수 있게 되었습니다.

이제는 조건이 만족되었을 때 실제로 스왑을 실행하는 코드만 남았습니다.
아래 함수는 수익률이 기준 이상이고, 가스비를 뺀 후에도 수익이 남을 경우에만
swapGasToUsdt()를 호출해 스왑을 실행합니다:

async function tryArbitrage() {
  if (!cachedCarrotPrice || !binanceAskPrice) return;

  const { profitRate } = calculateProfitRate(binanceAskPrice, cachedCarrotPrice);
  console.log(`[${new Date().toISOString()}] 수익률: ${(profitRate * 100).toFixed(2)}%`);

  if (profitRate > 0.01) {
    const amountIn = ethers.utils.parseUnits("10", 18); // 예: 10 GAS
    const ok = await isProfitable(amountIn);

    if (ok) {
      console.log("🟢 시세차익 확정 → 스왑 실행");
      await swapGasToUsdt(amountIn);
    } else {
      console.log("⚠️ 수익보다 가스비가 큼 → 스킵");
    }
  }
}

이 구조는 다음과 같은 과정을 거칩니다:
1. calculateProfitRate로 현재 시세차익 계산
2. 수익률이 1% 이상일 경우에만 다음 단계로 진행
3. isProfitable()로 가스비 포함 실제 수익 여부 확인
4. 조건을 모두 만족하면 스왑 실행
5. 그렇지 않으면 스킵하고 대기

swapGasToUsdt() 함수는 이전 편에서 구현한 실제 스왑 실행 로직을 사용합니다.
여기에서는 단순히 조건 판단만을 다루고, 실행 자체는 기존 로직을 재활용합니다.

마무리

이번 튜토리얼에서는 단순한 가격 차익 계산을 넘어서,
Carrot Swap의 실시간 가격을 이벤트로 감지하고,
바이낸스 가격과 비교한 시세차익을 계산한 뒤,
가스비까지 고려해 실제 수익이 남는 경우에만 스왑을 실행하는 로직을 구축했습니다.

이제 단순한 시뮬레이션이 아니라, 실제 수익을 낼 수 있는 조건을 만족할 때만 움직이는 아비트라지 실행 구조를 갖춘 셈입니다.


다음편 예고

여기까지가 아비트라지 봇의 핵심 실행 로직이라면,
다음 편부터는 이 로직을 지속적으로 모니터링하고 안정적으로 운영하기 위한 기능들을 추가해 나갈 예정입니다.

예정된 기능은 다음과 같습니다:

  • 트랜잭션 실패 시 자동 재시도
  • 지갑 잔액 부족 감지 및 스왑 제한
  • 슬랙/디스코드 알림 연동
  • 스왑 이력 기록 및 디버깅 로그
  • 전체 매수-스왑 루프 자동화

실제 운영 가능한 수준의 아비트라지 봇을 완성해가는 여정을 계속 이어가겠습니다.
다음 편도 기대해주세요.

profile
스마트 이코노미를 위한 퍼블릭 블록체인, 네오에 대한 모든것

0개의 댓글