What you need before you start.
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.
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.
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.
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:
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:
pip install websocket-client
export TICKATLAS_API_KEY="your_key_here"
python ws_stream.py #!/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:
// 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
| Plan | Credits per Push | Max symbols |
|---|---|---|
| Starter ($29/mo) | 0 | 5 |
| Pro ($79/mo) | 0 | 20 |
| Enterprise ($349/mo) | 0 | Unlimited |
Troubleshooting Common Errors
Production hardening
The code above is the happy path. These are the concerns that decide whether it survives contact with a real deployment.
What's Next
Everything this guide touches, linked directly — so it never dead-ends.