🐍 Pillar Tutorial

Python + Deriv API — Build Your First Trading Bot

By Dan Machado · 16 min read · Production code

Deriv’s drag-drop Bot Builder is great for beginners. But once you want custom logic — multiple assets, advanced indicators, complex risk management — you’ll outgrow it. Python with Deriv’s official WebSocket API gives you full control. This tutorial builds a complete RSI trading bot from scratch in ~150 lines of Python.

🎯 What You’ll Build

A working Python bot that:
1. Connects to Deriv via official WebSocket API
2. Subscribes to live Volatility 75 price ticks
3. Calculates RSI(14) in real-time
4. Places trades when RSI hits thresholds (CALL < 30, PUT > 70)
5. Uses 2% position sizing (Kelly-safe)
6. Stops if daily loss hits 5%
7. Logs every trade to CSV for analysis

Why Python Over Deriv Bot Builder?

  • Full control — any indicator, any logic, multiple assets
  • Real backtesting with pandas + numpy
  • Better debugging — print statements, logging
  • Version control — git for tracking changes
  • Multi-strategy — run 3 bots simultaneously in one process
  • Machine learning — TensorFlow, scikit-learn integration
  • Production deployment — VPS, Docker, systemd

Trade-off: requires basic Python knowledge. If you’ve never written Python, start with our Deriv Bot drag-drop tutorial first.

Prerequisites

  • Python 3.10+ installed (python --version to check)
  • VS Code or PyCharm editor
  • Free Deriv demo account (open here)
  • API token from Deriv (we’ll generate below)
  • Basic Python knowledge — variables, functions, async/await

Step 1: Get Your Deriv API Token

1

Log into Deriv account

Go to app.deriv.com, ensure you’re on Demo account ($10,000 virtual).

2

Navigate to API Token Settings

Click your account avatar (top right) → “Account Settings” → “API token” in left sidebar.

3

Create new token

Name: python-bot-v1
Scopes to enable: Read, Trade, Trading information, Payments (optional)
Click “Create”. Copy the token immediately — it won’t be shown again.

4

Store token securely

Create file .env in your project folder. Add: DERIV_TOKEN=YourTokenHere. Add .env to .gitignore. NEVER commit tokens to git.

🔐 Security Note

Treat your API token like a password. Anyone with it can place trades on your account. Use Demo account tokens for testing. Only use Real account tokens after extensive testing, and never paste them publicly.

Step 2: Install Required Libraries

Open terminal in your project folder and install:

▸ Bash COPY ↗
pip install websockets python-dotenv pandas numpy

What each does:

  • websockets — WebSocket client for Deriv API connection
  • python-dotenv — load API token from .env file
  • pandas — price data manipulation
  • numpy — RSI calculation

Step 3: Project Structure

▸ File Structure COPY ↗
deriv-bot/
├── .env                    # API token (gitignored)
├── .gitignore
├── bot.py                  # Main bot logic
├── indicators.py           # RSI and other indicators
├── risk_manager.py         # Position sizing, daily loss limit
├── logger.py               # Trade logging to CSV
└── trades.csv              # Trade history output

Step 4: RSI Calculator Module

Create indicators.py:

▸ Python · indicators.py COPY ↗
"""RSI indicator using numpy — fast, no external libs."""
import numpy as np
from collections import deque


class RSI:
    def __init__(self, period=14):
        self.period = period
        self.prices = deque(maxlen=period + 1)
        self.last_rsi = None

    def update(self, price: float) -> float | None:
        """Add new price, return current RSI (or None if not enough data)."""
        self.prices.append(price)
        if len(self.prices) < self.period + 1:
            return None

        deltas = np.diff(np.array(self.prices))
        gains = np.where(deltas > 0, deltas, 0)
        losses = np.where(deltas < 0, -deltas, 0)

        avg_gain = gains.mean()
        avg_loss = losses.mean()

        if avg_loss == 0:
            self.last_rsi = 100.0
        else:
            rs = avg_gain / avg_loss
            self.last_rsi = 100 - (100 / (1 + rs))

        return self.last_rsi

Step 5: Risk Manager Module

Create risk_manager.py:

▸ Python · risk_manager.py COPY ↗
"""Position sizing + daily loss limits. THE most important file."""
from datetime import date


class RiskManager:
    MAX_RISK_PER_TRADE = 0.02      # 2% per trade
    MAX_DAILY_LOSS_PCT = 0.05      # 5% daily stop
    MAX_DAILY_TRADES = 8           # Quality over quantity
    MAX_CONSECUTIVE_LOSSES = 3     # Cooldown trigger

    def __init__(self, starting_balance: float):
        self.balance = starting_balance
        self.day_start_balance = starting_balance
        self.current_date = date.today()
        self.trades_today = 0
        self.consecutive_losses = 0
        self.daily_pnl = 0.0

    def _reset_if_new_day(self):
        if date.today() != self.current_date:
            self.current_date = date.today()
            self.day_start_balance = self.balance
            self.trades_today = 0
            self.consecutive_losses = 0
            self.daily_pnl = 0.0

    def calculate_stake(self) -> float:
        """Returns USD amount to stake (2% of current balance)."""
        return round(self.balance * self.MAX_RISK_PER_TRADE, 2)

    def can_trade(self) -> tuple[bool, str]:
        """Check if conditions allow new trade. Returns (allowed, reason)."""
        self._reset_if_new_day()

        if self.trades_today >= self.MAX_DAILY_TRADES:
            return False, f"Max trades reached ({self.MAX_DAILY_TRADES}/day)"

        if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES:
            return False, f"Cooldown: {self.consecutive_losses} losses in a row"

        loss_threshold = self.day_start_balance * self.MAX_DAILY_LOSS_PCT
        if self.daily_pnl < -loss_threshold:
            return False, f"Daily loss limit hit: ${self.daily_pnl:.2f}"

        return True, "OK"

    def record_trade(self, pnl: float):
        """Update balance and counters after trade closes."""
        self.balance += pnl
        self.daily_pnl += pnl
        self.trades_today += 1
        if pnl > 0:
            self.consecutive_losses = 0
        else:
            self.consecutive_losses += 1

⚠️ Why This Matters Most

The Risk Manager is what separates a profitable bot from a bankruptcy generator. It’s tempting to skip this and just “trade more”. Don’t. Every successful trader has hard rules. This file enforces yours mechanically — no exceptions, no emotion.

Step 6: Main Bot Logic

Create bot.py — the heart of the system:

▸ Python · bot.py (Part 1: setup) COPY ↗
"""Deriv RSI bot — Python + WebSocket API."""
import asyncio
import json
import os
from datetime import datetime
import websockets
from dotenv import load_dotenv

from indicators import RSI
from risk_manager import RiskManager

load_dotenv()
TOKEN = os.getenv("DERIV_TOKEN")
APP_ID = 1089  # Public Deriv app ID for testing

# Settings
ASSET = "R_75"          # Volatility 75 Index
DURATION = 5            # 5 ticks per trade
DURATION_UNIT = "t"     # "t" = ticks
RSI_PERIOD = 14
OS_LEVEL = 30           # Oversold → BUY CALL
OB_LEVEL = 70           # Overbought → BUY PUT


class DerivBot:
    def __init__(self):
        self.ws = None
        self.rsi = RSI(RSI_PERIOD)
        self.risk = RiskManager(starting_balance=1000.0)
        self.req_id = 0
        self.last_signal = None  # avoid duplicate signals
        self.active_contract_id = None
▸ Python · bot.py (Part 2: WebSocket) COPY ↗
    async def send(self, payload: dict) -> dict:
        """Send request and wait for response."""
        self.req_id += 1
        payload["req_id"] = self.req_id
        await self.ws.send(json.dumps(payload))
        async for msg in self.ws:
            data = json.loads(msg)
            if data.get("req_id") == self.req_id:
                return data

    async def authorize(self):
        result = await self.send({"authorize": TOKEN})
        if "error" in result:
            raise Exception(f"Auth failed: {result['error']['message']}")
        balance = result["authorize"]["balance"]
        self.risk.balance = balance
        print(f"[AUTH] Connected. Balance: ${balance:.2f}")

    async def subscribe_ticks(self):
        await self.send({"ticks": ASSET, "subscribe": 1})
        print(f"[SUB] Subscribed to {ASSET} ticks")

    async def buy_contract(self, contract_type: str):
        """Place CALL or PUT contract."""
        stake = self.risk.calculate_stake()
        payload = {
            "buy": 1,
            "subscribe": 1,
            "price": stake,
            "parameters": {
                "amount": stake,
                "basis": "stake",
                "contract_type": contract_type,
                "currency": "USD",
                "duration": DURATION,
                "duration_unit": DURATION_UNIT,
                "symbol": ASSET,
            }
        }
        result = await self.send(payload)
        if "error" in result:
            print(f"[ERR] Buy failed: {result['error']['message']}")
            return None
        contract_id = result["buy"]["contract_id"]
        print(f"[TRADE] {contract_type} stake=${stake} id={contract_id}")
        return contract_id
▸ Python · bot.py (Part 3: main loop) COPY ↗
    async def handle_tick(self, tick: dict):
        """Process incoming price tick."""
        price = tick.get("quote")
        if price is None:
            return

        rsi = self.rsi.update(price)
        if rsi is None:
            return  # Need more data

        # Check risk first
        allowed, reason = self.risk.can_trade()
        if not allowed:
            return

        # Determine signal
        signal = None
        if rsi < OS_LEVEL and self.last_signal != "CALL":
            signal = "CALL"
        elif rsi > OB_LEVEL and self.last_signal != "PUT":
            signal = "PUT"

        if signal and self.active_contract_id is None:
            contract_id = await self.buy_contract(signal)
            if contract_id:
                self.last_signal = signal
                self.active_contract_id = contract_id

    async def run(self):
        url = f"wss://ws.derivws.com/websockets/v3?app_id={APP_ID}"
        async with websockets.connect(url) as ws:
            self.ws = ws
            await self.authorize()
            await self.subscribe_ticks()

            async for msg in ws:
                data = json.loads(msg)
                msg_type = data.get("msg_type")

                if msg_type == "tick":
                    await self.handle_tick(data["tick"])
                elif msg_type == "proposal_open_contract":
                    contract = data["proposal_open_contract"]
                    if contract.get("is_sold"):
                        pnl = float(contract["profit"])
                        self.risk.record_trade(pnl)
                        self.active_contract_id = None
                        emoji = "✓" if pnl > 0 else "✗"
                        print(f"[CLOSED] {emoji} P/L=${pnl:.2f} "
                              f"Balance=${self.risk.balance:.2f}")


if __name__ == "__main__":
    bot = DerivBot()
    try:
        asyncio.run(bot.run())
    except KeyboardInterrupt:
        print("\n[STOP] Bot stopped by user")

Step 7: Run It

▸ Terminal COPY ↗
python bot.py

Expected output:

▸ Console Output (sample) COPY ↗
[AUTH] Connected. Balance: $10000.00
[SUB] Subscribed to R_75 ticks
[TRADE] CALL stake=$200.00 id=234567891
[CLOSED] ✓ P/L=$181.00 Balance=$10181.00
[TRADE] PUT stake=$203.62 id=234567892
[CLOSED] ✗ P/L=-$203.62 Balance=$9977.38
[TRADE] CALL stake=$199.55 id=234567893
[CLOSED] ✓ P/L=$180.59 Balance=$10157.97

✓ You’re Now Running a Real Bot

This bot is live on Deriv demo. It’s actually placing trades, calculating RSI in real-time, enforcing risk limits, and tracking P/L. Same code (with real token) would run on live account.

Step 8: Add CSV Trade Logging

Add to bot.py for trade journal:

▸ Python · CSV logger snippet COPY ↗
import csv

def log_trade(self, signal, stake, pnl, balance):
    file_exists = os.path.exists("trades.csv")
    with open("trades.csv", "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["timestamp", "signal", "stake",
                             "pnl", "balance"])
        writer.writerow([
            datetime.now().isoformat(),
            signal, stake, pnl, balance
        ])

Open trades.csv in LibreOffice / Excel / Numbers for analysis. Calculate win rate, profit factor, max consecutive losses.

Common Issues & Fixes

🔧 Troubleshooting

“Auth failed”: token wrong or wrong scopes. Regenerate token with Read+Trade scopes enabled.
“WebSocket connection refused”: firewall blocking port 443. Try VPN.
“Buy failed: insufficient balance”: stake exceeds balance. Check Risk Manager logic.
No trades despite RSI hitting levels: check last_signal deduplication — may be blocking new signals.
Bot crashes after 30 minutes: WebSocket connection timeout. Add ping/pong keepalive.
RSI always returns None: need at least 15 price ticks before first calculation. Wait.

Production-Ready Improvements

  1. Logging: use Python logging module instead of print. Rotate logs daily.
  2. Reconnection: handle WebSocket disconnects gracefully, retry with backoff.
  3. Monitoring: send Telegram alerts on trade execution and daily P/L summary.
  4. Multi-asset: extend to V25, V50, V100 simultaneously (independent RSI per asset).
  5. EMA filter: only take RSI signals if EMA(9) confirms trend direction.
  6. VPS deployment: DigitalOcean droplet (~R110/mo) for 24/7 operation.
  7. Systemd service: auto-restart on crash. Linux service file.
  8. Database: SQLite for trade history (better than CSV for queries).
  9. Backtesting: separate backtest.py using historical OHLC data.
  10. Unit tests: test RSI calc, risk manager edge cases.

VPS Deployment for SA Users

Bots need to run 24/7. Your local PC isn’t ideal (sleeps, restarts, load shedding). VPS options for SA traders:

  • DigitalOcean: $6/mo (~R110) — best price/performance, NYC or Frankfurt datacentres
  • Vultr: $5/mo (~R92) — JHB (Johannesburg) datacentre available, lowest latency for SA
  • Hetzner: €4/mo (~R85) — German servers, very stable
  • AWS Lightsail: $5/mo (~R92) — overkill for one bot, but reliable

For SA users, Vultr’s JHB datacentre wins — keeps latency to Deriv API under 100ms (vs 250ms+ from EU/US datacentres). Important for real-time trading.

SARS Tax for Python Bot Traders

📋 Tax Compliance

Frequent algorithmic trading is almost certainly classified as income by SARS (not capital gains). Why:
• Frequency > 4 trades/day = “business” indicator
• Systematic, automated approach = “profit motive”
• Short holding periods = “speculation” not investment
Tax at marginal rate (up to 45% for top earners). Keep meticulous records — your CSV trade log is gold for SARS. Consult a tax practitioner specialising in trader-income.

What’s Next

  1. Run this bot on demo for 30+ days minimum
  2. Analyse CSV log — what’s your real win rate?
  3. Add EMA filter: prompt AI to generate
  4. Backtest properly: backtesting guide
  5. Compare to demo bot results: real test results
  6. If profitable, deploy to VPS for 24/7 operation
  7. Move to live with small balance (R200-R500) after 30 successful demo days

🚀 Test this Python bot on Deriv demo (FSCA-licensed, free):

Open Free Demo Account

Related Reading

DM

Dan Machado

Founder IA Trader Pro · Python developer 10+ years · Deriv API user since 2021

⚠️ Disclaimer: Code provided as-is, no warranty. Test on demo extensively before live. Trading involves risk of capital loss. Deriv is FSCA-authorised (FSP 50885). Contains Deriv affiliate links. Full disclaimer.