TickAtlas
Intermediate ~20 min

Real-Time Streaming with WebSocket

Build a Python script that connects to the TickAtlas WebSocket endpoint and streams live EURUSD bid/ask prices in real-time. By the end of this guide, you will have a production-ready client with authentication, subscription management, and automatic reconnection.

Prerequisites

  • TickAtlas API key on Starter plan or higher (get one here)
  • Python 3.8+ installed
  • websocket-client package (pip install websocket-client)
  • Basic familiarity with JSON and WebSocket concepts

What You Will Build

A Python script that opens a persistent WebSocket connection to wss://tickatlas.com/ws/v1/quotes, authenticates with your API key, subscribes to forex symbols, and prints live M1 price data as it arrives. The script handles disconnections with exponential backoff and respects your daily credit quota. M1 means 1-minute candles. Each subscribed symbol sends a price update roughly every 60 seconds with bid, ask, spread, and full OHLCV data for the completed candle.

Step 1: Connect and Authenticate

Authentication happens over the WebSocket connection itself, not via URL parameters or HTTP headers. After the connection opens, you send a JSON message with your API key. The server must receive this within 10 seconds or it closes the connection.

python
import json
import websocket  # pip install websocket-client

WS_URL = "wss://tickatlas.com/ws/v1/quotes"
API_KEY = "YOUR_API_KEY"

def on_open(ws):
    """Called when the connection is established."""
    print("Connected. Sending auth...")
    ws.send(json.dumps({
        "action": "auth",
        "key": API_KEY
    }))

def on_message(ws, message):
    msg = json.loads(message)
    if msg["type"] == "authenticated":
        print(f"Authenticated! Plan: {msg['plan']}")
        print(f"  Max symbols: {msg['max_symbols']}")
        print(f"  Credits per push: {msg['credits_per_push']}")
        print(f"  Daily quota remaining: {msg['daily_quota_remaining']}")
    else:
        print(f"Received: {msg}")

ws = websocket.WebSocketApp(WS_URL, on_open=on_open, on_message=on_message)
ws.run_forever()

Step 2: Subscribe to Symbols

Once authenticated, tell the server which symbols you want. The subscribe message takes an array of symbol names. The server responds with a subscribed confirmation listing all active symbols.

python
def on_message(ws, message):
    msg = json.loads(message)

    if msg["type"] == "authenticated":
        print(f"Authenticated on {msg['plan']} plan")
        # Subscribe to symbols after auth succeeds
        ws.send(json.dumps({
            "action": "subscribe",
            "symbols": ["EURUSD", "XAUUSD"]
        }))

    elif msg["type"] == "subscribed":
        print(f"Now watching {msg['total_subscribed']} symbols: "
              f"{msg['symbols']}")

    elif msg["type"] == "error":
        print(f"Error [{msg['code']}]: {msg['message']}")

Step 3: Handle Incoming Messages

The server sends several message types. The most important is quote which carries the actual price data.

python
def on_message(ws, message):
    msg = json.loads(message)

    if msg["type"] == "authenticated":
        ws.send(json.dumps({
            "action": "subscribe",
            "symbols": ["EURUSD", "XAUUSD"]
        }))

    elif msg["type"] == "subscribed":
        print(f"Watching {msg['total_subscribed']} symbols")

    elif msg["type"] == "quote":
        q = msg["data"]
        print(f"[{q['time']}] {q['symbol']} "
              f"O={q['open']} H={q['high']} L={q['low']} C={q['close']} "
              f"bid={q['bid']} ask={q['ask']} spread={q['spread']} "
              f"vol={q['volume']}")

    elif msg["type"] == "heartbeat":
        # Server sends this every 30 seconds — connection is alive
        pass

    elif msg["type"] == "pong":
        # Response to our ping — useful for latency measurement
        pass

    elif msg["type"] == "error":
        code = msg["code"]
        print(f"Error [{code}]: {msg['message']}")

        if code == "quota_exceeded":
            print("Daily quota exhausted. Quotes paused until midnight UTC.")
        elif code in ("auth_timeout", "invalid_key", "connection_limit"):
            print("Fatal error — connection will close.")

Step 4: Handle Reconnection

WebSocket connections can drop due to network issues, server restarts, or maintenance windows. A production client must reconnect automatically. Use exponential backoff to avoid hammering the server:

python
import time

def connect_with_backoff():
    """
    Reconnect with exponential backoff.
    Starts at 1s, doubles each attempt, caps at 60s.
    Resets to 1s after a successful connection.
    """
    delay = 1

    while True:
        try:
            ws = websocket.WebSocketApp(
                WS_URL,
                on_open=on_open,
                on_message=on_message,
                on_error=on_error,
                on_close=on_close,
            )
            # ping_interval keeps the TCP connection alive
            ws.run_forever(ping_interval=30, ping_timeout=10)
        except KeyboardInterrupt:
            print("Shutting down.")
            break
        except Exception as e:
            print(f"Connection error: {e}")

        print(f"Reconnecting in {delay}s...")
        time.sleep(delay)
        delay = min(delay * 2, 60)

def on_error(ws, error):
    print(f"WebSocket error: {error}")

def on_close(ws, close_status_code, close_msg):
    print(f"Connection closed ({close_status_code}): {close_msg}")

Complete Example

Here is the full script combining all four steps into a production-ready client. Save this as ws_stream.py and run it:

bash
pip install websocket-client
export TICKATLAS_API_KEY="your_key_here"
python ws_stream.py
python
#!/usr/bin/env python3
"""
TickAtlas WebSocket Streaming Client
Streams real-time M1 bid/ask data for subscribed symbols.
"""

import json
import time
import os
import websocket  # pip install websocket-client

WS_URL = "wss://tickatlas.com/ws/v1/quotes"
API_KEY = os.environ.get("TICKATLAS_API_KEY", "YOUR_API_KEY")
SYMBOLS = ["EURUSD", "XAUUSD", "GBPUSD"]

reconnect_delay = 1


def on_open(ws):
    global reconnect_delay
    reconnect_delay = 1  # Reset backoff on successful connect
    print("Connected. Authenticating...")
    ws.send(json.dumps({
        "action": "auth",
        "key": API_KEY
    }))


def on_message(ws, message):
    msg = json.loads(message)
    msg_type = msg.get("type")

    if msg_type == "authenticated":
        print(f"Authenticated | plan={msg['plan']} "
              f"quota={msg['daily_quota_remaining']}")
        ws.send(json.dumps({
            "action": "subscribe",
            "symbols": SYMBOLS
        }))

    elif msg_type == "subscribed":
        print(f"Subscribed to {msg['total_subscribed']} symbols: "
              f"{msg['symbols']}")

    elif msg_type == "quote":
        q = msg["data"]
        print(f"  {q['symbol']:8s} bid={q['bid']:<10} ask={q['ask']:<10} "
              f"spread={q['spread']:<6} vol={q['volume']}")

    elif msg_type == "heartbeat":
        pass  # Connection alive

    elif msg_type == "error":
        print(f"ERROR [{msg['code']}]: {msg['message']}")
        if msg["code"] == "quota_exceeded":
            print("Quota exhausted — quotes paused until midnight UTC.")


def on_error(ws, error):
    print(f"WebSocket error: {error}")


def on_close(ws, close_status_code, close_msg):
    print(f"Disconnected ({close_status_code}): {close_msg}")


def main():
    global reconnect_delay

    print(f"Connecting to {WS_URL}")
    print(f"Symbols: {SYMBOLS}")
    print("-" * 60)

    while True:
        try:
            ws = websocket.WebSocketApp(
                WS_URL,
                on_open=on_open,
                on_message=on_message,
                on_error=on_error,
                on_close=on_close,
            )
            ws.run_forever(ping_interval=30, ping_timeout=10)
        except KeyboardInterrupt:
            print("\nShutting down.")
            break
        except Exception as e:
            print(f"Connection failed: {e}")

        print(f"Reconnecting in {reconnect_delay}s...")
        time.sleep(reconnect_delay)
        reconnect_delay = min(reconnect_delay * 2, 60)


if __name__ == "__main__":
    main()

JavaScript / Browser Example

If you are building a web dashboard or using Node.js, here is the equivalent client in JavaScript:

javascript
// Browser / Node.js WebSocket streaming client
// Node.js: npm install ws, then const WebSocket = require("ws");

const WS_URL = "wss://tickatlas.com/ws/v1/quotes";
const API_KEY = "YOUR_API_KEY";
const SYMBOLS = ["EURUSD", "XAUUSD"];

let reconnectDelay = 1000;

function connect() {
  const ws = new WebSocket(WS_URL);

  ws.onopen = () => {
    reconnectDelay = 1000;
    console.log("Connected. Authenticating...");
    ws.send(JSON.stringify({ action: "auth", api_key: API_KEY }));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);

    switch (msg.type) {
      case "authenticated":
        console.log("Auth OK. Plan:", msg.plan,
                     "Quota:", msg.daily_quota_remaining);
        ws.send(JSON.stringify({
          action: "subscribe",
          symbols: SYMBOLS
        }));
        break;

      case "subscribed":
        console.log("Subscribed to", msg.symbols);
        break;

      case "quote":
        const q = msg.data;
        console.log(
          q.symbol, "bid=" + q.bid, "ask=" + q.ask,
          "spread=" + q.spread
        );
        // Update your UI or trading logic here
        break;

      case "heartbeat":
        break;

      case "error":
        console.error("[" + msg.code + "]", msg.message);
        break;
    }
  };

  ws.onclose = (event) => {
    console.log("Disconnected (" + event.code + "). "
                + "Reconnecting in " + reconnectDelay + "ms...");
    setTimeout(connect, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 60000);
  };

  ws.onerror = (error) => {
    console.error("WS error:", error.message || error);
  };
}

connect();

Understanding Credits

PlanCredits per Push5 Symbols / Day20 Symbols / Day
Starter ($29/mo)5~36,000 creditsN/A (5 symbol limit)
Pro ($79/mo)4~28,800 credits~115,200 credits
Enterprise ($349/mo)3~21,600 credits~86,400 credits

Troubleshooting Common Errors

auth_timeout

Cause: You did not send the auth message within 10 seconds of connecting. Fix: Send the auth message immediately in your on_open handler. Check that your connection is not blocked by a proxy or firewall that delays the initial send.

quota_exceeded

Cause: Your daily credit quota is exhausted. Fix: Reduce the number of subscribed symbols, upgrade to a higher plan for a larger quota, or wait for midnight UTC reset. The connection stays open — do not disconnect and reconnect.

connection_limit

Cause: You already have 2 active WebSocket connections for this API key. Fix: Close one of the existing connections before opening a new one. If you believe a stale connection is still counted, wait a few minutes for the server to detect the stale connection and release the slot.

forbidden

Cause: Your API key is on the Free or Pay-As-You-Go plan, which does not include WebSocket access. Fix: Upgrade to the Starter plan ($29/mo) or higher.

invalid_key

Cause: The API key does not exist or has been revoked. Fix: Check your key in the dashboard. If it was rotated, update your script with the new key.

Related