# Building Your First Trading Bot with the eToro API

A step-by-step guide to building a simple trading bot using the eToro Demo Trading API.

---


Automated trading on a demo account is the safest way to learn how orders, risk, and execution behave before you put real capital at risk. This tutorial walks you through a minimal **Node.js** bot that authenticates against the eToro Demo Trading API, polls instrument rates, applies a simple **moving average crossover** rule, places market orders when the signal flips, and periodically reconciles open positions.

You will need an API key with demo trading enabled, and a user key that identifies the demo portfolio. Store both in environment variables and never commit them to source control.

## Project setup

Create a new folder, run `npm init -y`, and add a single dependency if you want structured logging (`pino`) or use `console.log` for simplicity. Your `.env` file should define `ETORO_API_KEY`, `ETORO_USER_KEY`, and optionally `ETORO_INSTRUMENT_ID` for the instrument you want to trade in demo mode.

Every HTTP call in this tutorial uses the public API base URL `https://public-api.etoro.com/api/v1/`. Each request should send a unique `x-request-id` for traceability (UUIDs work well), your `x-api-key`, and when the endpoint acts on behalf of a user, the `x-user-key` header.

## Authenticating and health-checking the session

Before placing trades, confirm that your credentials are accepted by calling a lightweight **session** or **account** endpoint. The pattern below is reusable for any authenticated GET.

<!-- skip-test -->
```javascript
import { randomUUID } from "node:crypto";

const BASE = "https://public-api.etoro.com/api/v1";

function headers() {
  return {
    "x-api-key": process.env.ETORO_API_KEY,
    "x-user-key": process.env.ETORO_USER_KEY,
    "x-request-id": randomUUID(),
    "content-type": "application/json",
  };
}

async function verifyDemoSession() {
  const res = await fetch(`${BASE}/trading/info/demo/portfolio`, { headers: headers() });
  if (!res.ok) throw new Error(`Session check failed: ${res.status}`);
  return res.json();
}
```

If this call returns account metadata (currency, buying power, open P/L), you are ready to pull market data and send orders.

## Fetching instrument rates for your strategy

For a moving average crossover you need a rolling window of **mid prices** or **last trade** prices. Poll an instrument rates endpoint at a sensible interval (for example every 15–60 seconds on demo), append each sample to an in-memory array, and trim the array to the longest period you need (for instance 50 closes for a 50-period slow average).

<!-- skip-test -->
```javascript
const closes = [];
const FAST = 10;
const SLOW = 30;

function sma(period) {
  if (closes.length < period) return null;
  const slice = closes.slice(-period);
  return slice.reduce((a, b) => a + b, 0) / period;
}

async function fetchLastClose(instrumentId) {
  const res = await fetch(
    `${BASE}/market-data/rates?instrumentIds=${instrumentId}`,
    { headers: headers() }
  );
  if (!res.ok) throw new Error(`Rates failed: ${res.status}`);
  const body = await res.json();
  const last = body.data?.candles?.at(-1);
  return last?.close ?? last?.mid;
}
```

In production you might replace polling with WebSocket candles; for a first bot, polling keeps the control flow linear and easier to debug.

## Crossover logic and order placement

When the fast SMA crosses **above** the slow SMA, treat it as a **buy** signal; when it crosses **below**, treat it as a **sell** or **close long** signal depending on your demo rules. Keep position sizing conservative: pass a fixed notional or a fraction of buying power, and always send an idempotency-friendly client reference in the body if the API supports it.

<!-- skip-test -->
```javascript
import { randomUUID } from "node:crypto";

let lastSignal = "flat";

function crossoverSignal() {
  const fast = sma(FAST);
  const slow = sma(SLOW);
  if (fast == null || slow == null) return "hold";
  if (lastSignal !== "long" && fast > slow) return "buy";
  if (lastSignal === "long" && fast < slow) return "sell";
  return "hold";
}

async function placeMarketOrder({ instrumentId, side, amount }) {
  const res = await fetch(
    `${BASE}/trading/execution/demo/market-open-orders/by-amount`,
    {
      method: "POST",
      headers: headers(),
      body: JSON.stringify({
        InstrumentID: instrumentId,
        IsBuy: side === "BUY",
        Amount: amount,
      }),
    }
  );
  if (!res.ok) {
    const errText = await res.text();
    throw new Error(`Order failed ${res.status}: ${errText}`);
  }
  return res.json();
}
```

## Monitoring positions

After each order, poll **open positions** to confirm fills, average price, and unrealized P/L. Use the same header helper so support can correlate logs with `x-request-id`.

<!-- skip-test -->
```javascript
async function listOpenPositions() {
  const res = await fetch(`${BASE}/trading/info/demo/portfolio`, {
    headers: headers(),
  });
  if (!res.ok) throw new Error(`Positions failed: ${res.status}`);
  return res.json();
}

async function tick(instrumentId) {
  const price = await fetchLastClose(instrumentId);
  if (typeof price === "number") closes.push(price);

  const signal = crossoverSignal();
  if (signal === "buy") {
    await placeMarketOrder({ instrumentId, side: "BUY", amount: 100 });
    lastSignal = "long";
  } else if (signal === "sell" && lastSignal === "long") {
    await placeMarketOrder({ instrumentId, side: "SELL", amount: 100 });
    lastSignal = "flat";
  }

  const book = await listOpenPositions();
  console.log("signal=%s positions=%j", signal, book);
}
```

## Operational tips

Rate-limit your polling loop, handle HTTP 429 with exponential backoff, and log the **request id** whenever you escalate a support ticket. Demo trading mirrors many production constraints but not slippage or liquidity perfectly—treat results as educational, not as a guarantee of live performance.

From here you can add stop-loss and take-profit orders, multi-instrument portfolios, or swap polling for streaming data. The same authentication and header discipline applies across those upgrades.
