Back to Blog
July 23, 2025 at 06:27 AM

How to Build a Crypto Options Trading Bot with the Gemini CLI

Hitesh Agja
Gemini-CLIGeminicryptocurrencyoptions tradingtrading botalgorithmic trading
How to Build a Crypto Options Trading Bot with the Gemini CLI

In this tutorial, I'll walk through how to build a fully functional automated trading bot from scratch using the Gemini CLI. This bot will trade BTC options on the Delta Exchange based on an EMA (Exponential Moving Average) crossover strategy. I will construct the entire application by giving a series of natural language prompts to the Gemini CLI, which will generate the code for us.

This guide is for anyone interested in automated trading, Node.js, or seeing how AI-powered development tools can accelerate the coding process.

image

Prerequisites & Setup

Before we begin, make sure you have Node.js installed. Our project will have a few key files:

  1. package.json: This file defines our project's dependencies.I'll need axios for making HTTP requests, ws for WebSocket connections, crypto-js for generating API signatures, technicalindicators for our trading strategy, and dotenv to manage our API keys.

    {
      "name": "tradebot",
      "version": "1.0.0",
      "description": "",
      "main": "trade.js",
      "scripts": {
        "start": "node trade.js"
      },
      "dependencies": {
        "axios": "^1.7.2",
        "crypto-js": "^4.2.0",
        "dotenv": "^16.4.5",
        "technicalindicators": "^3.0.2",
        "ws": "^8.17.0"
      }
    }
    
  2. .env: This file will store your secret API credentials. Never commit this file to version control.

    API_KEY="YOUR_DELTA_EXCHANGE_API_KEY"
    API_SECRET="YOUR_DELTA_EXCHANGE_API_SECRET"
    
  3. state.json: A simple file to keep track of our bot's open positions. This ensures that if the bot restarts, it knows its current state. It can be an empty object {} to start.

With the setup complete, let's start building our bot with the Gemini CLI.

Step-by-Step: Building the Bot with Prompts

We will now issue a series of prompts to the Gemini CLI to build our trade.js file piece by piece.

Step 1: Initial Setup and WebSocket Connection

First, we need to create the main file and set up the basic structure, including constants and a WebSocket client that connects to the exchange and authenticates.

User Prompt: Create a Node.js script named trade.js. It should use the dotenv, ws, and crypto-js libraries. Define constants for the API key and secret from environment variables, the API endpoint https://api.india.delta.exchange/v2, and the WebSocket endpoint wss://socket.delta.exchange. Then, create a WebSocket client that connects, and on 'open', it should authenticate using the API key and secret. Also, subscribe to the ticker for BTCUSD.

image

Step 2: Fetching Historical Data

Our trading strategy depends on historical candle data to calculate EMAs. Let's ask Gemini to create a function to fetch the last 100 30-minute candles.

User Prompt: Now, add a function called fetchInitialCandles that uses axios to get the last 100 30-minute candles for BTCUSD from the /history/candles endpoint. It should call this function after the WebSocket connection is authenticated.

Step 3: Processing Real-Time Data

With historical data in place, we need to process live price updates from the WebSocket and update our candles accordingly.

User Prompt: Add a function updateCandles that takes a new price. It should update the close price of the last candle. If a new 30-minute interval has started, it should create a new candle and remove the oldest one. This function should be called when a new ticker message arrives from the WebSocket.

Step 4: Implementing the Trading Strategy

This is the core logic of our bot. We will implement the 9/21 EMA crossover strategy.

User Prompt: Create a function named checkCrossover. It should use the technicalindicators library to calculate the 9-period and 21-period EMAs from the candle closing prices. If the EMA9 crosses above the EMA21 by a gap of at least 50, it's a buy signal. If the EMA21 crosses above the EMA9 by the same gap, it's a sell signal. On a buy signal, the bot should look to sell a PUT option, and on a sell signal, it should look to sell a CALL option.

image

Step 5: Finding an Option to Trade

When our strategy generates a signal, we need to find a suitable options contract to sell.

User Prompt: Write a function findAndSellOption that takes an optionType ('put' or 'call'). It should query the /tickers endpoint to find options expiring two days from now with a premium greater than 1000. It should then select the first suitable option to trade.

Step 6: Executing Orders and Setting Leverage

This is the most critical part where we interact with the trading API. It's essential that the API signature is generated correctly.

User Prompt: I need two functions to execute trades. The first, setLeverage, should send a POST request to /products/:productId/leverage to set the leverage to 25x. The second, placeOrder, should send a POST request to the /orders endpoint to place a market order. For both functions, ensure the API signature is generated correctly using the format: METHOD + TIMESTAMP + '/v2' + PATH + BODY.

Step 7: Adding State Management

Finally, to make our bot robust, we need it to remember its open positions.

User Prompt: Add two functions, readState and writeState, to manage the bot's position using a local state.json file. The bot should read this file on startup. After selling an option, it should write the position details to the file. When it closes a position, it should clear the file.

The Complete Code

After following all the steps, the Gemini CLI has generated our complete trade.js file. Here is the final result:

require('dotenv').config();
const WebSocket = require('ws');
const crypto = require('crypto-js');
const EMA = require('technicalindicators').EMA;
const axios = require('axios');
const fs = require('fs');

const apiKey = process.env.API_KEY;
const apiSecret = process.env.API_SECRET;
const underlyingSymbol = 'BTC'; // Replace with the underlying asset for the options
const leverage = 25;
const tradeSize = 1; // Trading a single contract
const stateFilePath = './state.json';

const apiEndpoint = 'https://api.india.delta.exchange/v2';
const wsEndpoint = 'wss://socket.delta.exchange';

const ema9Period = 9;
const ema21Period = 21;
const emaGap = 50;
const timeframe = '30m';
const candleIntervalSeconds = 1800; // 30 * 60

let candles = [];

const ws = new WebSocket(wsEndpoint);

ws.on('open', async function open() {
  console.log('Connected to Delta Exchange WebSocket API');

  const timestamp = Date.now();
  const signature = crypto.HmacSHA256(timestamp.toString(), apiSecret).toString();

  const authPayload = {
    type: 'authenticate',
    api_key: apiKey,
    timestamp: timestamp,
    signature: signature
  };

  ws.send(JSON.stringify(authPayload));

  await fetchInitialCandles();

  const subscribePayload = {
    type: 'subscribe',
    payload: {
      channels: [
        {
          name: 'v2/ticker',
          symbols: [`${underlyingSymbol}USD`]
        }
      ]
    }
  };
  ws.send(JSON.stringify(subscribePayload));
});

ws.on('message', function incoming(data) {
  const message = JSON.parse(data);

  if (message.type === 'v2/ticker') {
    const newPrice = parseFloat(message.close);
    updateCandles(newPrice);
  } else if (message.type !== 'authentication_success'){
    console.log('Received:', message);
  }
});

ws.on('close', function close() {
  console.log('Disconnected from Delta Exchange WebSocket API');
});

ws.on('error', function error(err) {
  console.error('WebSocket error:', err);
});

async function fetchInitialCandles() {
    try {
        const now = Math.floor(Date.now() / 1000);
        const startTime = now - (100 * candleIntervalSeconds);
        const response = await axios.get(`${apiEndpoint}/history/candles`, {
            params: {
                symbol: `${underlyingSymbol}USD`,
                resolution: timeframe,
                start: startTime,
                end: now
            }
        });
        candles = response.data.result;
        console.log('Fetched initial candles.');
        checkCrossover(); // Initial check
    } catch (error) {
        console.error('Error fetching initial candles:', error.response ? error.response.data : error.message);
    }
}

function updateCandles(price) {
    if (candles.length === 0) return;

    let lastCandle = candles[candles.length - 1];
    const now = Math.floor(Date.now() / 1000);

    if (now >= lastCandle.time + candleIntervalSeconds) { // New 30-minute candle
        candles.push({ time: lastCandle.time + candleIntervalSeconds, open: price, high: price, low: price, close: price });
        if (candles.length > 100) {
            candles.shift();
        }
        checkCrossover();
    } else { // Update current candle
        lastCandle.close = price;
        if (price > lastCandle.high) lastCandle.high = price;
        if (price < lastCandle.low) lastCandle.low = price;
    }
}

async function checkCrossover() {
    if (candles.length < ema21Period) return;

    const closingPrices = candles.map(c => c.close);
    const ema9 = EMA.calculate({ period: ema9Period, values: closingPrices });
    const ema21 = EMA.calculate({ period: ema21Period, values: closingPrices });

    const latestEma9 = ema9[ema9.length - 1];
    const latestEma21 = ema21[ema21.length - 1];

    console.log(`EMA9: ${latestEma9}, EMA21: ${latestEma21}`);

    const state = readState();

    if (latestEma9 > latestEma21 && (latestEma9 - latestEma21) >= emaGap) {
        console.log('Buy signal on index, selling PUT option...');
        if (state.openPosition) await placeOrder('buy', state.openPosition.productId, state.openPosition.size);
        findAndSellOption('put');
    } else if (latestEma21 > latestEma9 && (latestEma21 - latestEma9) >= emaGap) {
        console.log('Sell signal on index, selling CALL option...');
        if (state.openPosition) await placeOrder('buy', state.openPosition.productId, state.openPosition.size);
        findAndSellOption('call');
    }
}

async function setLeverage(productId) {
    const path = `/products/${productId}/leverage`;
    const method = 'POST';
    const timestamp = Math.floor(Date.now() / 1000);
    const body = JSON.stringify({ leverage: leverage });
    const stringToSign = method + timestamp + '/v2' + path + body;
    const signature = crypto.HmacSHA256(stringToSign, apiSecret).toString(crypto.enc.Hex);

    const config = {
        headers: {
            'api-key': apiKey,
            'signature': signature,
            'timestamp': timestamp,
            'Content-Type': 'application/json'
        }
    };

    try {
        await axios.post(apiEndpoint + path, body, config);
        console.log(`Leverage set to ${leverage}x for product ${productId}`);
    } catch (error) {
        console.error(`Error setting leverage for product ${productId}:`, error.response ? error.response.data : error.message);
    }
}

async function findAndSellOption(optionType) {
    try {
        const today = new Date();
        const expiryDate = new Date(today.setDate(today.getDate() + 2));
        const expiryString = `${expiryDate.getDate().toString().padStart(2, '0')}-${(expiryDate.getMonth() + 1).toString().padStart(2, '0')}-${expiryDate.getFullYear()}`;

        const response = await axios.get(`${apiEndpoint}/tickers?contract_types=${optionType}_options&underlying_asset_symbols=${underlyingSymbol}&expiry_date=${expiryString}`);
        const tickers = response.data.result;

        const filteredTickers = tickers.filter(t => t.close > 1000);

        if (filteredTickers.length > 0) {
            const tickerToSell = filteredTickers[0];
            console.log(`Found option to sell: ${tickerToSell.symbol}`);
            await setLeverage(tickerToSell.product_id);
            await placeOrder('sell', tickerToSell.product_id, tradeSize);
            writeState({ openPosition: { productId: tickerToSell.product_id, size: tradeSize } });
        } else {
            console.log(`No suitable ${optionType} option found with premium > 1000 and expiry on ${expiryString}`);
        }
    } catch (error) {
        console.error('Error fetching tickers:', error.response ? error.response.data : error.message);
    }
}

async function placeOrder(side, productId, size) {
  const path = '/orders';
  const method = 'POST';
  const timestamp = Math.floor(Date.now() / 1000);
  const orderPayload = {
    product_id: productId,
    order_type: 'market_order',
    side: side,
    size: size,
  };
  const body = JSON.stringify(orderPayload);
  const stringToSign = method + timestamp + '/v2' + path + body;
  const signature = crypto.HmacSHA256(stringToSign, apiSecret).toString(crypto.enc.Hex);

  const config = {
    headers: {
      'api-key': apiKey,
      'signature': signature,
      'timestamp': timestamp,
      'Content-Type': 'application/json'
    }
  };

  try {
    console.log(`Placing ${side.toUpperCase()} order for product ${productId}...`);
    const response = await axios.post(apiEndpoint + path, body, config);
    console.log(`${side.toUpperCase()} order placed successfully:`, response.data);
    if (side === 'buy') {
        writeState({}); // Clear the state after closing a position
    }
  } catch (error) {
    console.error(`Error placing ${side.toUpperCase()} order:`, error.response ? error.response.data : error.message);
  }
}

function readState() {
    try {
        if (fs.existsSync(stateFilePath)) {
            const data = fs.readFileSync(stateFilePath);
            return JSON.parse(data);
        }
    } catch (error) {
        console.error('Error reading state file:', error);
    }
    return {};
}

function writeState(state) {
    try {
        fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
    } catch (error) {
        console.error('Error writing to state file:', error);
    }
}

Conclusion

Using the Gemini CLI, we've successfully built a crypto trading bot by breaking down the problem into logical steps and using natural language prompts to generate the necessary code. This approach allows for rapid development and lets you focus on the strategy and logic rather than boilerplate code. Happy trading!