Skip to main content
The Exness Trading Platform provides comprehensive market data through multiple channels: real-time WebSocket streams for live prices, REST API for historical data, and TimescaleDB for efficient time-series storage.

Real-Time Price Streaming

Connect to live market data via WebSocket to receive instant price updates:
// Connect to WebSocket server
const ws = new WebSocket('wss://ws.exness.com');

ws.onopen = () => {
  console.log('Connected to price stream');
};

ws.onmessage = (event) => {
  const priceUpdate = JSON.parse(event.data);
  console.log('Price update:', priceUpdate);
  
  // Update UI with new prices
  updatePriceDisplay(priceUpdate);
};

// Price update format
{
  "asset": "BTC_USDC_PERP",
  "bid": 429700,      // Bid price (with 4 decimal precision)
  "ask": 430300       // Ask price (with 4 decimal precision)
}
The WebSocket server receives prices from Redis Pub/Sub and distributes them to all connected clients in real-time.

Price Data Flow

Market data flows through the platform in this sequence:

Price Poller Service

The Price Poller connects to Binance and processes raw market data:
// apps/Price_Poller/src/index.ts
import WebSocket from "ws";
import { pubsubClient, redisClient, redisStreams } from "@repo/config";

const ws = new WebSocket(config.BINANCE_WS_URL);
const PubsubClient = pubsubClient(config.REDIS_URL);
const RedisStreams = redisStreams(config.REDIS_URL);

const crypto_trades = ["ETH_USDC_PERP", "SOL_USDC_PERP", "BTC_USDC_PERP"];
const price_updates: PriceUpdate[] = [];

// Subscribe to Binance trade streams
ws.on("open", function open() {
  crypto_trades.forEach((asset) => {
    ws.send(`{"method":"SUBSCRIBE","params":["trade.${asset}"],"id":4}`);
  });
});

ws.on("message", async (data) => {
  const msg = JSON.parse(data.toString());
  
  // Calculate bid/ask with 0.5% spread
  const decimal = 4;
  const asset = msg.data.s.toString();
  const price = Math.floor(Number(msg.data.p) * 10 ** decimal);
  const bidValue = Math.floor((price - price * 0.005) * 10 ** decimal);
  const askValue = Math.floor((price + price * 0.005) * 10 ** decimal);
  
  // Publish to WebSocket clients via Redis Pub/Sub
  const bidAsk = { asset, bid: bidValue, ask: askValue };
  await PubsubClient.publish(
    constant.pubsubKey,
    JSON.stringify(bidAsk)
  );
  
  // Update price_updates array
  const idx = price_updates.findIndex((u) => u.asset === asset);
  if (idx !== -1) {
    price_updates[idx] = { asset, price, bidValue, askValue, decimal };
  } else {
    price_updates.push({ asset, price, bidValue, askValue, decimal });
  }
});

// Send to Engine every 3 seconds
setInterval(async () => {
  await RedisStreams.addToRedisStream(
    constant.redisStream,
    {
      function: "pricePoller",
      message: JSON.stringify(price_updates)
    }
  );
}, 3000);

Supported Assets

The platform currently supports three perpetual futures:
AssetSymbolBinance Stream
BitcoinBTC_USDC_PERPtrade.BTC_USDC_PERP
EthereumETH_USDC_PERPtrade.ETH_USDC_PERP
SolanaSOL_USDC_PERPtrade.SOL_USDC_PERP
Prices include a 0.5% spread between bid and ask to simulate real market conditions.

Historical Candlestick Data

Access OHLCV (Open, High, Low, Close, Volume) data for technical analysis:
curl "https://api.exness.com/api/v1/candles?symbol=BTCUSDT&interval=1h"

Available Intervals

IntervalDescriptionDefault Time Range
1m1 minuteLast 1 day
5m5 minutesLast 7 days
15m15 minutesLast 14 days
30m30 minutesLast 1 month
1h1 hourLast 3 months
4h4 hoursLast 1 year
1d1 dayLast 5 years
Use shorter intervals (1m, 5m) for scalping strategies and longer intervals (4h, 1d) for swing trading and position analysis.

Candle Data API

The candles endpoint retrieves data from TimescaleDB with automatic time-range selection:
// apps/Backend/src/routes/candles.routes.ts
import { Router } from "express";
import { timeScaleDB } from "@repo/timescaledb";

const candleRouter = Router();
const client = timeScaleDB();

const allowedIntervals = ["1m", "5m", "15m", "30m", "1h", "4h", "1d"];

// Symbol mapping from user-facing to database format
const symbolMapping: Record<string, string> = {
  'BTCUSDT': 'BTC_USDC_PERP',
  'ETHUSDT': 'ETH_USDC_PERP',
  'SOLUSDT': 'SOL_USDC_PERP',
};

export const getCandles = async (req: Request, res: Response) => {
  const { symbol, interval } = req.query;

  // Validate parameters
  if (!symbol || !interval) {
    return res.status(400).json({
      error: "Missing required query parameters: symbol and interval"
    });
  }

  if (!allowedIntervals.includes(interval as string)) {
    return res.status(400).json({
      error: "Invalid interval value",
      allowedIntervals
    });
  }

  // Map symbol to database format
  const inputSymbol = (symbol as string).toUpperCase();
  const dbSymbol = symbolMapping[inputSymbol] || inputSymbol;

  // Get default time range for interval
  const { from, to } = getTimeRange(interval as string);

  // Retrieve data from TimescaleDB
  const data = await retrieveData(dbSymbol, interval as string, from, to);

  return res.json({
    symbol: inputSymbol,
    dbSymbol,
    interval,
    from,
    to,
    count: data.length,
    data
  });
};

candleRouter.get("/", getCandles);

TimescaleDB Integration

The platform uses TimescaleDB for efficient time-series data storage:

Continuous Aggregates

TimescaleDB automatically computes candlesticks using continuous aggregates:
-- Create continuous aggregate for 1-hour candles
CREATE MATERIALIZED VIEW candles_1h
WITH (timescaledb.continuous) AS
SELECT 
  time_bucket('1 hour', time) AS bucket,
  symbol,
  FIRST(price, time) AS open,
  MAX(price) AS high,
  MIN(price) AS low,
  LAST(price, time) AS close,
  SUM(volume) AS volume,
  COUNT(*) AS trade_count
FROM trades
GROUP BY bucket, symbol;

Querying Candles

The API queries pre-aggregated candles for fast response:
async function retrieveData(
  symbol: string,
  interval: string,
  from: string,
  to: string
) {
  const table = `candles_${interval}`;
  
  // Refresh continuous aggregate
  await client.getClient().query(
    `CALL refresh_continuous_aggregate('${table}', $1::timestamptz, $2::timestamptz);`,
    [from, to]
  );

  // Query aggregated data
  const query = `
    SELECT bucket AS time,
           open, high, low, close, volume, trade_count
    FROM ${table}
    WHERE UPPER(symbol) = UPPER($1)
      AND bucket BETWEEN $2::timestamptz AND $3::timestamptz
    ORDER BY bucket ASC;
  `;

  const result = await client.getClient().query(query, [symbol, from, to]);
  return result.rows;
}
Continuous aggregates are automatically maintained by TimescaleDB, providing instant access to pre-computed candles without scanning raw trade data.

Data Storage Pipeline

Trade data flows from Binance to TimescaleDB:

Trade Storage

Raw trades are stored in the trades table:
CREATE TABLE trades (
  time TIMESTAMPTZ NOT NULL,
  symbol TEXT NOT NULL,
  price NUMERIC NOT NULL,
  volume NUMERIC NOT NULL,
  trade_id TEXT
);

-- Convert to hypertable for time-series optimization
SELECT create_hypertable('trades', 'time');

Diagnostics

Check database status and available data:
curl "https://api.exness.com/api/v1/candles/diagnostics"

Refresh Aggregates

Manually refresh continuous aggregates:
curl -X POST "https://api.exness.com/api/v1/candles/refresh"
Aggregates are automatically refreshed when querying candles, but you can trigger a manual refresh for all intervals.

Using Market Data in Your Application

Here’s a complete example of connecting to live prices and fetching historical data:
// Connect to WebSocket for real-time updates
const ws = new WebSocket('wss://ws.exness.com');
const prices = {};

ws.onmessage = (event) => {
  const update = JSON.parse(event.data);
  
  // Store latest prices
  prices[update.asset] = {
    bid: update.bid / 10000,  // Convert to actual price
    ask: update.ask / 10000,
    timestamp: Date.now()
  };
  
  // Update UI
  document.getElementById(`${update.asset}-bid`).textContent = 
    prices[update.asset].bid.toFixed(2);
  document.getElementById(`${update.asset}-ask`).textContent = 
    prices[update.asset].ask.toFixed(2);
};
Prices are stored with 4 decimal precision (multiplied by 10000). Remember to divide by 10000 when displaying prices to users.

Next Steps

Real-Time Trading

Use live prices to execute trades instantly

Order Management

Monitor your positions with real-time P/L

Build docs developers (and LLMs) love