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.
Before we begin, make sure you have Node.js installed. Our project will have a few key files:
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"
}
}
.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"
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.
We will now issue a series of prompts to the Gemini CLI to build our trade.js
file piece by piece.
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 thedotenv
,ws
, andcrypto-js
libraries. Define constants for the API key and secret from environment variables, the API endpointhttps://api.india.delta.exchange/v2
, and the WebSocket endpointwss://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 forBTCUSD
.
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 usesaxios
to get the last 100 30-minute candles forBTCUSD
from the/history/candles
endpoint. It should call this function after the WebSocket connection is authenticated.
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.
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 thetechnicalindicators
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.
When our strategy generates a signal, we need to find a suitable options contract to sell.
User Prompt: Write a function
findAndSellOption
that takes anoptionType
('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.
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
.
Finally, to make our bot robust, we need it to remember its open positions.
User Prompt: Add two functions,
readState
andwriteState
, to manage the bot's position using a localstate.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.
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);
}
}
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!