Intermediate ~20 min PythonShellJavaScript Build guide

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.

6 sections 7 copy-paste code samples Python · Shell · JavaScript
6Guide sections
7Code samples
IntermediateLevel
20 minTime

What you need before you start.

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
Scope: TickAtlas supplies market data, indicators and analytics. It does not place orders or hold funds — any broker or execution layer stays inside your own application.

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 quotes as they arrive. The script handles disconnections with exponential backoff and prints quota and connection-limit errors from the server, then reconnects with backoff. Each subscribed symbol sends a quote roughly every 60 seconds carrying bid, ask, volume and a timestamp; compute the spread client-side as ask minus bid.

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 Connect and Authenticate
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"  Max connections: {msg['max_connections']}")
    else:
        print(f"Received: {msg}")

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

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 Subscribe to Symbols
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 {len(msg['symbols'])} symbols: "
              f"{msg['symbols']}")

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

Handle Incoming Messages

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

Python Handle Incoming Messages
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 {len(msg['symbols'])} symbols")

    elif msg["type"] == "quote":
        q = msg  # quote frames are flat: symbol, bid, ask, volume, time
        spread = q['ask'] - q['bid']
        print(f"[{q['time']}] {q['symbol']} "
              f"bid={q['bid']} ask={q['ask']} spread={spread:.5f} "
              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 exceeded. The server closes the stream (4029) until midnight UTC.")
        elif code in ("auth_timeout", "invalid_key", "connection_limit"):
            print("Fatal error — connection will close.")

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 Handle Reconnection
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:

Shell Complete Example
pip install websocket-client
export TICKATLAS_API_KEY="your_key_here"
python ws_stream.py
Python Complete Example
#!/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"max_symbols={msg['max_symbols']}")
        ws.send(json.dumps({
            "action": "subscribe",
            "symbols": SYMBOLS
        }))

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

    elif msg_type == "quote":
        q = msg  # flat quote frame
        spread = round(q['ask'] - q['bid'], 5)
        print(f"  {q['symbol']:8s} bid={q['bid']:<10} ask={q['ask']:<10} "
              f"spread={spread:<8} 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 exceeded — the server closes the stream (4029) 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 JavaScript / Browser Example
// 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", key: API_KEY }));
  };

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

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

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

      case "quote":
        const q = msg; // flat quote frame: symbol, bid, ask, volume, time
        console.log(
          q.symbol, "bid=" + q.bid, "ask=" + q.ask,
          "spread=" + (q.ask - q.bid).toFixed(5)
        );
        // 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();

Streaming Is Unmetered

PlanCredits per PushMax symbols
Starter ($29/mo)05
Pro ($79/mo)020
Enterprise ($349/mo)0Unlimited

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 account's REST usage for the day is over your plan's daily quota (pushes themselves never count), so the server closes the stream with code 4029. Fix: Wait for the midnight UTC reset, enable overage, or upgrade for a larger quota. Reconnecting before then closes again with 4029.
connection_limit Cause: You already have 2 active WebSocket connections on this account. 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: WebSocket streaming requires a Starter plan or higher; pay-as-you-go keys do 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.

Production hardening

The code above is the happy path. These are the concerns that decide whether it survives contact with a real deployment.

Keep the key server-side. The API key authenticates with the X-API-Key header and must never reach a browser bundle. Proxy it, or use a public widget key, which is domain-scoped and revocable. Authentication
Handle 429 before you need to. Rate limits are per key and per minute. Back off on 429 rather than retrying immediately, and read the X-RateLimit-* headers on every response. Rate limits
Branch on the error code, not the message. Errors carry a stable machine-readable code; the human-readable text can change. Codes were unified in v3.15. Error handling
Expect gaps, and do not invent values. Markets close, feeds stall, and a retention window can reject a request outright. Surface an explicit unavailable state rather than substituting a zero or the last known price. Troubleshooting
Cache what you poll. Responses are already cached briefly upstream, so polling faster than the data changes spends credits without improving freshness. Cache on your side and poll on the cadence your timeframe actually updates. Pricing and credits
Watch retention per timeframe. History depth is set per timeframe, never per plan, so a request that works on D1 can fall outside the window on M1. Check the published windows before backfilling. Timeframes
Rotate keys and scope them. Issue a separate key per deployment so one can be revoked without taking the others down, and rotate on a schedule rather than after an incident. Authentication
Log the request, not the key. Record endpoint, parameters, status and latency so a failure is reproducible. Never log the key itself, and scrub it from error reports.

What's Next

Everything this guide touches, linked directly — so it never dead-ends.

Build against live market data

Start with the data layer already solved.

Create an API key, run the first request, then extend one layer at a time. Every account starts pay-as-you-go with $2.50 of credit.