We use cookies to improve your experience. By using this site, you agree to our use of cookies. Privacy Policy (opens in new tab)
Build a fully functional algorithmic trading bot using the eToro API with position management, risk controls, and automated strategy execution.
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.
Our bot consists of four main components:
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
A simple moving average (SMA) crossover strategy generates signals when a fast-period SMA crosses above or below a slow-period SMA:
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";
}
The order manager handles trade execution through the eToro API:
import { randomUUID } from "node:crypto";
class OrderManager {
constructor(apiKey, userKey, environment) {
this.apiBase = "https://public-api.etoro.com/api/v1";
this.executionPrefix =
environment === "demo"
? "trading/execution/demo"
: "trading/execution";
this.apiKey = apiKey;
this.userKey = userKey;
this.positions = new Map();
}
headers() {
return {
"x-api-key": this.apiKey,
"x-user-key": this.userKey,
"x-request-id": randomUUID(),
"Content-Type": "application/json",
};
}
async openPosition(instrument, direction, amount) {
const response = await fetch(
`${this.apiBase}/${this.executionPrefix}/market-open-orders/by-amount`,
{
method: "POST",
headers: this.headers(),
body: JSON.stringify({
InstrumentID: instrument,
IsBuy: direction === "BUY",
Amount: amount,
}),
}
);
const order = await response.json();
this.positions.set(instrument, {
id: order.positionId,
direction,
amount,
entryPrice: order.executionPrice,
});
console.log(
`Opened ${direction} position on ${instrument} at ${order.executionPrice}`
);
return order;
}
async closePosition(instrument) {
const position = this.positions.get(instrument);
if (!position) return null;
const response = await fetch(
`${this.apiBase}/${this.executionPrefix}/market-close-orders`,
{
method: "POST",
headers: this.headers(),
body: JSON.stringify({ positionId: position.id }),
}
);
this.positions.delete(instrument);
const result = await response.json();
console.log(`Closed position on ${instrument}`);
return result;
}
}
Never trade without risk controls. Our risk controller enforces:
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;
}
}
async function runBot() {
const orderManager = new OrderManager(
process.env.ETORO_API_KEY,
process.env.ETORO_USER_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();