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:
WebSocket Client
WebSocket Server
// 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:
Price Ingestion
Type Definition
// 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:
Asset Symbol Binance Stream Bitcoin BTC_USDC_PERP trade.BTC_USDC_PERP Ethereum ETH_USDC_PERP trade.ETH_USDC_PERP Solana SOL_USDC_PERP trade.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"
{
"symbol" : "BTCUSDT" ,
"dbSymbol" : "BTC_USDC_PERP" ,
"interval" : "1h" ,
"from" : "2024-01-01T00:00:00.000Z" ,
"to" : "2024-03-15T14:30:00.000Z" ,
"count" : 1825 ,
"data" : [
{
"time" : "2024-03-15T14:00:00.000Z" ,
"open" : 43000 ,
"high" : 43500 ,
"low" : 42900 ,
"close" : 43200 ,
"volume" : 125.5 ,
"trade_count" : 3542
}
]
}
Available Intervals
Interval Description Default Time Range 1m1 minute Last 1 day 5m5 minutes Last 7 days 15m15 minutes Last 14 days 30m30 minutes Last 1 month 1h1 hour Last 3 months 4h4 hours Last 1 year 1d1 day Last 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:
Candles Route
Time Range Helper
// 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"
{
"totalTrades" : 1500000 ,
"symbols" : [
{
"symbol" : "BTC_USDC_PERP" ,
"count" : 500000 ,
"earliest" : "2024-01-01T00:00:00.000Z" ,
"latest" : "2024-03-15T14:30:00.000Z"
},
{
"symbol" : "ETH_USDC_PERP" ,
"count" : 750000 ,
"earliest" : "2024-01-01T00:00:00.000Z" ,
"latest" : "2024-03-15T14:30:00.000Z"
},
{
"symbol" : "SOL_USDC_PERP" ,
"count" : 250000 ,
"earliest" : "2024-01-01T00:00:00.000Z" ,
"latest" : "2024-03-15T14:30:00.000Z"
}
],
"continuousAggregates" : [
{ "view_name" : "candles_1m" , "materialized_only" : false },
{ "view_name" : "candles_5m" , "materialized_only" : false },
{ "view_name" : "candles_15m" , "materialized_only" : false },
{ "view_name" : "candles_30m" , "materialized_only" : false },
{ "view_name" : "candles_1h" , "materialized_only" : false },
{ "view_name" : "candles_4h" , "materialized_only" : false },
{ "view_name" : "candles_1d" , "materialized_only" : false }
]
}
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:
Live Prices
Historical Chart
// 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