Skip to main content
Back to Learn
advancedtrading-botalgo-tradingautomationadvanced

Building an Algo Trading Bot

Build a fully functional algorithmic trading bot using the eToro API with position management, risk controls, and automated strategy execution.

4 min readBy eToro Developer Relations

Overview

This guide walks through building an algorithmic trading bot that connects to the eToro API, implements a simple moving average crossover strategy, and manages positions with proper risk controls. We'll start in the demo environment before going live.

Important: Always test thoroughly with the Demo Trading API before using real funds. Algorithmic trading carries significant risk.

Architecture

Our bot consists of four main components:

  1. Data collector — Fetches historical and real-time price data
  2. Strategy engine — Implements trading logic (SMA crossover)
  3. Order manager — Executes trades and tracks positions
  4. Risk controller — Enforces position limits and stop-losses

Setting Up the Project

mkdir etoro-trading-bot && cd etoro-trading-bot
npm init -y
npm install ws node-fetch dotenv

Create a .env file for your credentials:

ETORO_API_KEY=your_api_key_here
ETORO_API_SECRET=your_api_secret_here
ETORO_ENVIRONMENT=demo

The Strategy: SMA Crossover

A simple moving average (SMA) crossover strategy generates signals when a fast-period SMA crosses above or below a slow-period SMA:

  • Buy signal: Fast SMA crosses above slow SMA (bullish)
  • Sell signal: Fast SMA crosses below slow SMA (bearish)
function calculateSMA(prices, period) {
  if (prices.length < period) return null;
  const slice = prices.slice(-period);
  return slice.reduce((sum, p) => sum + p, 0) / period;
}

function getSignal(prices, fastPeriod = 10, slowPeriod = 30) {
  const fastSMA = calculateSMA(prices, fastPeriod);
  const slowSMA = calculateSMA(prices, slowPeriod);

  if (!fastSMA || !slowSMA) return "HOLD";

  const prevFast = calculateSMA(prices.slice(0, -1), fastPeriod);
  const prevSlow = calculateSMA(prices.slice(0, -1), slowPeriod);

  if (!prevFast || !prevSlow) return "HOLD";

  if (prevFast <= prevSlow && fastSMA > slowSMA) return "BUY";
  if (prevFast >= prevSlow && fastSMA < slowSMA) return "SELL";

  return "HOLD";
}

Order Manager

The order manager handles trade execution through the eToro API:

class OrderManager {
  constructor(apiKey, environment) {
    this.apiBase =
      environment === "demo"
        ? "https://demo-api.etoro.com/api/sor/v3"
        : "https://api.etoro.com/api/sor/v3";
    this.apiKey = apiKey;
    this.positions = new Map();
  }

  async openPosition(instrument, direction, amount) {
    const response = await fetch(`${this.apiBase}/orders`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        instrument,
        direction,
        amount,
        type: "market",
      }),
    });

    const order = await response.json();
    this.positions.set(instrument, {
      id: order.data.positionId,
      direction,
      amount,
      entryPrice: order.data.executionPrice,
    });

    console.log(
      `Opened ${direction} position on ${instrument} at ${order.data.executionPrice}`
    );
    return order;
  }

  async closePosition(instrument) {
    const position = this.positions.get(instrument);
    if (!position) return null;

    const response = await fetch(
      `${this.apiBase}/positions/${position.id}/close`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          "Content-Type": "application/json",
        },
      }
    );

    this.positions.delete(instrument);
    const result = await response.json();
    console.log(`Closed position on ${instrument}`);
    return result;
  }
}

Risk Controller

Never trade without risk controls. Our risk controller enforces:

  • Maximum position size
  • Stop-loss percentage
  • Maximum number of concurrent positions
class RiskController {
  constructor(config) {
    this.maxPositionSize = config.maxPositionSize || 1000;
    this.stopLossPercent = config.stopLossPercent || 0.02;
    this.maxPositions = config.maxPositions || 5;
    this.currentPositions = 0;
  }

  canOpenPosition(amount) {
    if (amount > this.maxPositionSize) {
      console.warn(`Position size $${amount} exceeds max $${this.maxPositionSize}`);
      return false;
    }
    if (this.currentPositions >= this.maxPositions) {
      console.warn(`Max concurrent positions (${this.maxPositions}) reached`);
      return false;
    }
    return true;
  }

  shouldStopLoss(entryPrice, currentPrice, direction) {
    const change =
      direction === "BUY"
        ? (currentPrice - entryPrice) / entryPrice
        : (entryPrice - currentPrice) / entryPrice;

    return change <= -this.stopLossPercent;
  }
}

Putting It All Together

async function runBot() {
  const orderManager = new OrderManager(
    process.env.ETORO_API_KEY,
    process.env.ETORO_ENVIRONMENT
  );
  const riskController = new RiskController({
    maxPositionSize: 500,
    stopLossPercent: 0.02,
    maxPositions: 3,
  });

  const instrument = "AAPL";
  const priceHistory = [];

  // Simulated price feed loop
  setInterval(async () => {
    // In production, fetch from WebSocket or REST API
    const price = await getCurrentPrice(instrument);
    priceHistory.push(price);

    // Keep last 100 prices
    if (priceHistory.length > 100) priceHistory.shift();

    const signal = getSignal(priceHistory);
    console.log(`${instrument}: $${price} | Signal: ${signal}`);

    if (signal === "BUY" && !orderManager.positions.has(instrument)) {
      if (riskController.canOpenPosition(500)) {
        await orderManager.openPosition(instrument, "BUY", 500);
        riskController.currentPositions++;
      }
    }

    if (signal === "SELL" && orderManager.positions.has(instrument)) {
      await orderManager.closePosition(instrument);
      riskController.currentPositions--;
    }

    // Check stop-loss
    const position = orderManager.positions.get(instrument);
    if (
      position &&
      riskController.shouldStopLoss(position.entryPrice, price, position.direction)
    ) {
      console.log(`Stop-loss triggered for ${instrument}`);
      await orderManager.closePosition(instrument);
      riskController.currentPositions--;
    }
  }, 60000);
}

runBot();

Best Practices

  1. Start with demo — Always validate your strategy in the sandbox first
  2. Log everything — Record all trades, signals, and errors for analysis
  3. Set hard limits — Use the risk controller to prevent catastrophic losses
  4. Monitor actively — Don't leave a bot running unattended for extended periods
  5. Handle errors gracefully — Network failures, API errors, and edge cases will happen
  6. Backtest first — Test your strategy against historical data before live trading

Next Steps

We use cookies to improve your experience. By using this site, you agree to our use of cookies. Privacy Policy (opens in new tab)