Forex WebSocket API in Python: Is EURUSD Still Updating?

TickDB Research ·

For Python developers building a forex quote monitor, this guide uses TickDB to subscribe to EURUSD and save each quote with its data timestamp and local receipt time. A bounded script writes raw messages, a quote CSV and a run summary. Quote counts, receipt gaps and changes in data timestamps help distinguish an open connection from continuing quote updates. The recorded 30-second observation received 38 EURUSD messages. The same code lets you inspect a new sample in your own environment.

Give a frozen price something you can trace

Your EURUSD monitor displays a price, the terminal is quiet, and the connection appears open. Then the price stops changing. Has the quote stayed the same, or has your application stopped reading updates? A single number on the screen leaves little to investigate later.

Keep the price, the timestamp carried by the message, and the time your script read it. Then inspect whether target quotes continue arriving and whether their data timestamps advance. TickDB supplies the continuing forex quote stream; the Python application filters EURUSD, adds the receipt time and writes the files. Your displayed price can now be traced to a specific received message.

Where TickDB fits

TickDB provides a unified market data interface for developers. Here we use its WebSocket ticker subscription to receive continuing EURUSD quote messages. A REST or MCP query can return an on-demand snapshot; this task needs a subscription that keeps receiving subsequent updates.

The official ticker documentation lists last_price, a Unix-millisecond timestamp, and bid/ask fields when available. The data timestamp comes from the message. The local receipt time, file storage and diagnostic summaries are added by this script.

The recorded observation

On October 28, 2026, the connection and observation ran between 02:10:30 and 02:11:02 UTC. A 30-second receive window started after connection. It captured 38 EURUSD quote messages, with 37 changes between adjacent data timestamps and no observed backward timestamps. The first three rows were:

Data time UTCReceipt time UTClast_price
2026-10-28T02:10:33.305+00:002026-10-28T02:10:33.405+00:001.11242
2026-10-28T02:10:33.706+00:002026-10-28T02:10:33.816+00:001.11245
2026-10-28T02:10:33.803+00:002026-10-28T02:10:33.905+00:001.11240

This count describes one short observation. It is not a fixed update rate. Prices and message counts will change on another run.

What to save, and why

FieldOriginUse
last_priceThe quote messagePreserve the price string without an early float conversion
data_time_ms, data_time_utcMessage timestampCheck whether data timestamps change or move backward
received_time_ms, received_time_utcLocal wall clock after reading a messageTrace when this application read the message
receive_gap_secondsLocal monotonic clockInspect gaps between target quote receipts without directly depending on wall-clock adjustments

Subtracting the two UTC timestamps mixes source timestamp semantics, transport, buffering, scheduling and clock differences. Do not label that difference network latency. This script records both times without producing a latency metric.

Run a bounded subscription

Use Python 3.11 or later:

python3 -m pip install websockets certifi

Save the code below as eurusd_monitor.py. It reads TICKDB_API_KEY if set, otherwise prompts for your key without echoing it. Keep keys out of source code and screenshots. Access to the requested symbol depends on your account permissions.

python3 eurusd_monitor.py --seconds 30 --out eurusd_run_01

Choose an output directory that does not yet exist. The script preserves earlier observations by refusing to overwrite one.

"""Python 3.11+; pip install websockets certifi. Bounded observation, not trading."""
import argparse
import asyncio
import csv
import getpass
import json
import os
import ssl
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlencode
import certifi
import websockets


def utc(ms):
    return datetime.fromtimestamp(ms / 1000, timezone.utc).isoformat(timespec='milliseconds')


async def observe(seconds, out):
    key = os.environ.get('TICKDB_API_KEY') or getpass.getpass('TickDB API key (hidden): ')
    if not key.strip():
        raise ValueError('API key required')
    out.mkdir(parents=True, exist_ok=False)
    fields = ['symbol', 'last_price', 'bid_price', 'ask_price', 'data_time_ms',
              'data_time_utc', 'received_time_ms', 'received_time_utc', 'receive_gap_seconds']
    summary = {'started_utc': utc(time.time_ns() // 1000000), 'requested_seconds': seconds,
               'symbol': 'EURUSD', 'status': 'starting', 'quote_count': 0,
               'data_time_changes': 0, 'backward_data_times': 0, 'last_receive_age_seconds': None,
               'max_receive_gap_seconds': None, 'handshake_http_status': None}
    previous_mono = previous_data = last_receive_mono = None
    gaps = []
    try:
        uri = 'wss://api.tickdb.ai/v1/realtime?' + urlencode({'api_key': key})
        context = ssl.create_default_context(cafile=certifi.where())
        async with websockets.connect(uri, ssl=context, open_timeout=10, close_timeout=3,
                                      ping_interval=20, ping_timeout=20) as ws:
            summary['status'] = 'connected'
            summary['handshake_http_status'] = 101
            await ws.send(json.dumps({'cmd': 'subscribe', 'data': {
                'channel': 'ticker', 'symbols': ['EURUSD'], 'type': 'forex'}}))
            deadline = time.monotonic() + seconds
            with (out / 'messages.jsonl').open('w') as raw, (out / 'quotes.csv').open('w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fields)
                writer.writeheader()
                while time.monotonic() < deadline:
                    try:
                        message = await asyncio.wait_for(ws.recv(), timeout=min(1, max(.001, deadline-time.monotonic())))
                    except asyncio.TimeoutError:
                        continue
                    received_ms = time.time_ns() // 1000000
                    received_mono = time.monotonic()
                    try:
                        decoded = json.loads(message)
                    except (ValueError, UnicodeDecodeError):
                        summary['invalid_json_count'] = summary.get('invalid_json_count', 0) + 1
                        continue
                    raw.write(json.dumps({'received_time_ms': received_ms, 'message': decoded}).replace(key, '[REDACTED]')+'\n')
                    raw.flush()
                    for item in decoded if isinstance(decoded, list) else [decoded]:
                        if not isinstance(item, dict):
                            continue
                        if item.get('cmd') == 'error':
                            summary['api_error_code'] = item.get('code')
                            raise RuntimeError('API subscription error')
                        data = item.get('data')
                        if item.get('cmd') != 'ticker' or not isinstance(data, dict) or data.get('symbol') != 'EURUSD':
                            continue
                        stamp = data.get('timestamp')
                        if isinstance(stamp, bool) or not isinstance(stamp, int) or not isinstance(data.get('last_price'), str):
                            summary['invalid_quote_count'] = summary.get('invalid_quote_count', 0) + 1
                            continue
                        gap = None if previous_mono is None else round(received_mono-previous_mono, 6)
                        if gap is not None:
                            gaps.append(gap)
                        if previous_data is not None:
                            summary['data_time_changes'] += int(stamp != previous_data)
                            summary['backward_data_times'] += int(stamp < previous_data)
                        row = dict(symbol='EURUSD', last_price=data['last_price'],
                                   bid_price=data.get('bid_price'), ask_price=data.get('ask_price'),
                                   data_time_ms=stamp, data_time_utc=utc(stamp),
                                   received_time_ms=received_ms, received_time_utc=utc(received_ms),
                                   receive_gap_seconds=gap)
                        writer.writerow(row)
                        f.flush()
                        summary['quote_count'] += 1
                        previous_mono = last_receive_mono = received_mono
                        previous_data = stamp
            summary['status'] = 'observed_quotes' if summary['quote_count'] else 'connected_without_target_quotes'
    except Exception as exc:
        summary['status'] = 'failed'
        summary['error_type'] = type(exc).__name__
        response = getattr(exc, 'response', None)
        summary['handshake_http_status'] = getattr(response, 'status_code', None)
        # Never print exception text: it may contain the authenticated URL.
    finally:
        summary['ended_utc'] = utc(time.time_ns() // 1000000)
        summary['last_receive_age_seconds'] = None if last_receive_mono is None else round(time.monotonic()-last_receive_mono, 6)
        summary['max_receive_gap_seconds'] = max(gaps) if gaps else None
        (out / 'summary.json').write_text(json.dumps(summary, indent=2)+'\n')
        print(json.dumps(summary, indent=2))
    return summary['status'] == 'observed_quotes'


if __name__ == '__main__':
    p = argparse.ArgumentParser()
    p.add_argument('--seconds', type=int, default=30)
    p.add_argument('--out', type=Path, default=Path('eurusd_observation'))
    a = p.parse_args()
    if not 1 <= a.seconds <= 300:
        p.error('--seconds must be 1..300')
    raise SystemExit(0 if asyncio.run(observe(a.seconds, a.out)) else 1)

The script closes after the requested receive window. It uses WebSocket protocol Ping/Pong checks. There is no need to copy an older example that sends an application ping every second; the current connection guidance distinguishes protocol heartbeats from optional application checks.

The run writes messages.jsonl for received messages, quotes.csv for valid EURUSD quote rows, and summary.json for status, counts and times. A handshake failure may produce only the summary. Read its status instead of treating a missing CSV as a successful run with zero quotes. Exceptions are reported without printing an authenticated connection URL.

Decide what to investigate next

observed_quotes means target quotes were read during the observation. Inspect the count, data_time_changes and the CSV to see the pattern over the window. A changing timestamp with an unchanged price can still be an update; a monitor should not require every message to carry a different price.

connected_without_target_quotes means the connection opened but no valid EURUSD quotes were read within the window. Check the requested symbol, permissions and raw control messages, then consider whether quotes were active during that period. Connection heartbeats do not establish that target quotes are updating.

failed identifies an error during connection, subscription or receipt. Preserve that status even if some quotes were saved before the interruption. Address authentication or permission failures before retrying.

A short stream of received messages does not establish that no messages were missed. An operational alert needs a silence threshold appropriate to the application, timestamp checks, reconnection and resubscription handling. This example is an investigation starting point; it does not implement recovery or trading.

Start with your own credentials from TickDB, record one EURUSD run, and inspect whether the final data timestamps advance and the final receipt is close to the observation end. Connect that record to your monitor so its displayed price has a traceable source.

Related articles

Integrating a Forex Real-Time Market Data API: How to Avoid Displaying Stale Quotes After a WebSocket Reconnection

After integrating real-time market data into a forex dashboard, exchange-rate monitor, or price alert, when should the page resume showing that it is “updating normally” once the connection is restored? This article is aimed at developers responsible for these features. Using TickDB’s actual EUR/USD subscription as an example, it walks through the full process of connecting, subscribing, deliberately disconnecting, and reconnecting, and explains how quote timestamps, heartbeats, and page status work together to keep teams from being misled by stale data. Product managers and technical colleagues can use the recovery sequence and acceptance checklist in the article to jointly review the page behavior.

TickDB Research ·

Forex Tick Data API: Live Quotes, Historical Downloads, and Access Costs

For quant teams choosing an FX data supplier, a working live feed answers only part of the purchasing question. Research may need historical bid and ask updates, while a dashboard may need current quotes and charts. This guide explains how to distinguish quotes, trades, and bars, check historical access, and compare costs against a specific job. It includes a dated EUR/USD quote sample, three actual TickDB historical queries, and a supplier questionnaire you can copy into an enquiry.

TickDB Research ·

How to Choose a Real-Time Market Data API: Verify Coverage First, Then Test Latency and Cost

When integrating stock, foreign exchange, futures, index, or cryptocurrency quotes, how do you determine whether a real-time market data API is suitable for your project? First, turn your asset classes, fields, and business use cases into a requirements matrix. Then validate REST queries and WebSocket streams separately, and finally decide whether to purchase based on sampling, reconnection tests, and license verification. Receiving quotes does not mean that tick-by-tick data, a complete order book, or backfillable history is available. The difference between receipt time and source time is meaningful only after the time definitions are clear and clocks are synchronized.

TickDB Research ·

Stock API: Pricing, Permissions & Access Limits

Stock API: Pricing, Permissions & Access Limits How are stock API interfaces charged, what data can you access after paying, and what restrictions apply during integration? Pricing typically depend…

TickDB Research ·