Tardis Machine Server
Locally runnable server with built-in data caching, providing both tick-level historical and consolidated real-time cryptocurrency market data via HTTP and WebSocket APIs

Introduction

Tardis-machine is a locally runnable server with built-in data caching that uses Tardis.dev HTTP API under the hood. It provides both tick-level historical and consolidated real-time cryptocurrency market data via it's HTTP and WebSocket APIs and is available via npm and Docker.

Features

GitHub - tardis-dev/tardis-machine: Locally runnable server with built-in data caching, providing both tick-level historical and consolidated real-time cryptocurrency market data via HTTP and WebSocket APIs
GitHub
Tardis-machine GitHub repository

Installation

Docker

Pull and run latest version of tardisdev/tardis-machine image:
1
### running without persitent local cache
2
docker run -p 8000:8000 -p 8001:8001 -e "TM_API_KEY=YOUR_API_KEY" -d tardisdev/tardis-machine
Copied!
Tardis-machine server's HTTP endpoints will be available on port 8000 and WebSocket API endpoints on port 8001. Your API key will be passed via ENV variable (TM_API_KEY) — simply replace YOUR_API_KEY with API key you've received via email.
Command above does not use persistent volumes for local caching (each docker restart will result in loosing local data cache). In order to use for example./host-cache-dir as persistent volume (bind mount) cache directory, run:
1
docker run -v ./host-cache-dir:/.cache -p 8000:8000 -p 8001:8001 -e "TM_API_KEY=YOUR_API_KEY" -d tardisdev/tardis-machine
Copied!
Since using volumes can cause issues especially on Windows, it's fine to run Docker image without them with the caveat of potentially poor local cache ratio after each container's restart.

Config environment variables

You can set following environment config variables to configure tardis-machine server:
name
default
description
TM_API_KEY
API key for Tardis.dev HTTP API - if not provided only first day of each month of historical data is accessible
TM_PORT
8000
HTTP port on which server will be running, WebSocket port is always this value + 1 (8001 with port set to 8000)
TM_CACHE_DIR
/.cache
path to local dir that will be used as cache location
TM_CLUSTER_MODE
false
will launch cluster of Node.js processes to handle the incoming requests if set to true, by default server runs in single process mode
TM_DEBUG
false
server will print verbose debug logs to stdout if set to true
TM_CLEAR_CACHE
false
server will clear local cache dir on startup if set to true

npm

Requires Node.js v12+ and git installed.
Install and runtardis-machine server via npx command:
1
npx tardis-machine --api-key=YOUR_API_KEY
Copied!
or install globally via npm:
1
npm install -g tardis-machine
Copied!
and then run:
1
tardis-machine --api-key=YOUR_API_KEY
Copied!
Tardis-machine server's HTTP endpoints will be available on port 8000 and WebSocket API endpoints on port 8001. Your API key will be passed via --api-key config flag — simply replace YOUR_API_KEY with API key you've received via email.

CLI config flags

You can configure tardis-machine server via environment variables as described in Docker section as well.
You can set following CLI config flags when starting tardis-machine server installed via npm:
name
default
description
--api-key
API key for Tardis.dev HTTP API - if not provided only first day of each month of historical data is accessible
--port
8000
HTTP port on which server will be running, WebSocket port is always this value + 1 (8001 with port set to 8000)
--cache-dir
<os.tmpdir>/.tardis-cache
path to local dir that will be used as cache location - if not provided default temp dir for given OS will be used
--cluster-mode
false
will launch cluster of Node.js processes to handle the incoming requests if set to true, by default server runs in single process mode
--debug
false
server will print verbose debug logs to stdout if set to true
--clear-cache
false
server will clear local cache dir on startup is set to true
--help
shows CLI help
--version
shows tardis-machine version number

Exchange-native market data APIs

Exchange-native market data API endpoints provide historical data in exchange-native format. The main difference between HTTP and WebSocket endpoints is the logic of requesting data:
    HTTP API accepts request options payload via query string param
    WebSocket API accepts exchanges' specific 'subscribe' messages that define what data will be then "replayed" and send to WebSocket client

HTTP GET /replay?options={options}

Returns historical market data messages in exchange-native format for given replay options query string param. Single streaming HTTP response returns data for the whole requested time period as NDJSON.
In our preliminary benchmarks on AMD Ryzen 7 3700X, 64GB RAM, HTTP /replay API endpoint was returning ~700 000 messages/s (already locally cached data).
Python
Node.js
cURL
Your preferred language
1
import asyncio
2
import aiohttp
3
import json
4
import urllib.parse
5
6
7
async def replay_via_tardis_machine_machine(replay_options):
8
timeout = aiohttp.ClientTimeout(total=0)
9
10
async with aiohttp.ClientSession(timeout=timeout) as session:
11
# url encode as json object options
12
encoded_options = urllib.parse.quote_plus(json.dumps(replay_options))
13
14
# assumes tardis-machine HTTP API running on localhost:8000
15
url = f"http://localhost:8000/replay?options={encoded_options}"
16
17
async with session.get(url) as response:
18
# otherwise we may get line to long errors
19
response.content._high_water = 100_000_000
20
21
# returned data is in NDJSON format http://ndjson.org/
22
# each line is separate message JSON encoded
23
async for line in response.content:
24
yield line
25
26
27
async def run():
28
lines = replay_via_tardis_machine_machine(
29
{
30
"exchange": "bitmex",
31
"from": "2019-10-01",
32
"to": "2019-10-02",
33
"filters": [
34
{"channel": "trade", "symbols": ["XBTUSD", "ETHUSD"]},
35
{"channel": "orderBookL2", "symbols": ["XBTUSD", "ETHUSD"]},
36
],
37
}
38
)
39
40
async for line in lines:
41
message = json.loads(line)
42
# localTimestamp string marks timestamp when message was received
43
# message is a message dict as provided by exchange real-time stream
44
print(message["localTimestamp"], message["message"])
45
46
47
asyncio.run(run())
Copied!
See also official Tardis.dev Python client library.
1
const fetch = require('node-fetch')
2
const split2 = require('split2')
3
4
5
const serialize = options => {
6
return encodeURIComponent(JSON.stringify(options))
7
}
8
9
async function* replayViaTardisMachine(options) {
10
// assumes tardis-machine HTTP API running on localhost:8000
11
const url = `http://localhost:8000/replay?options=${serialize(options)}`
12
const response = await fetch(url)
13
14
// returned data is in NDJSON format http://ndjson.org/
15
// each line is separate message JSON encoded
16
17
// split response body stream by new lines
18
const lines = response.body.pipe(split2())
19
20
for await (const line of lines) {
21
yield line
22
}
23
}
24
25
async function run() {
26
const options = {
27
exchange: 'bitmex',
28
from: '2019-10-01',
29
to: '2019-10-02',
30
filters: [
31
{
32
channel: 'trade',
33
symbols: ['XBTUSD', 'ETHUSD']
34
},
35
{
36
channel: 'orderBookL2',
37
symbols: ['XBTUSD', 'ETHUSD']
38
}
39
]
40
}
41
42
const lines = replayViaTardisMachine(options)
43
44
for await (const line of lines) {
45
// localTimestamp string marks timestamp when message was received
46
// message is a message object as provided by exchange real-time stream
47
const { message, localTimestamp } = JSON.parse(line)
48
49
console.log(message, localTimestamp)
50
}
51
}
52
53
run()
Copied!
See also official Tardis.dev Node.js client library.
1
curl -g 'localhost:8000/replay?options={"exchange":"bitmex","filters":[{"channel":"orderBookL2","symbols":["XBTUSD","ETHUSD"]}],"from":"2019-07-01","to":"2019-07-02"}'
Copied!
http://localhost:8000/replay?options={%22exchange%22:%22bitmex%22,%22filters%22:[{%22channel%22:%22orderBookL2%22,%22symbols%22:[%22XBTUSD%22,%22ETHUSD%22]}],%22from%22:%222019-07-01%22,%22to%22:%222019-07-02%22}
localhost
Click to see API response in the browser as long as tardis-machine is running on localhost:8000
We're working on providing more samples and dedicated client libraries in different languages, but in the meanwhile to consume HTTP /replay API responses in your language of choice, you should:
    1.
    Provide url encoded JSON options object via options query string param when sending HTTP request
    2.
    Parse HTTP response stream line by line as it's returned - buffering in memory whole response may result in slow performance and memory overflows
    3.
    Parse each response line as JSON containing messages in exchange-native format

Replay options

HTTP /replay endpoint accepts required options query string param in url encoded JSON format.
name
type
default
exchange
string
-
requested exchange id - use /exchanges HTTP API to get list of valid exchanges ids
filters
{channel:string, symbols?: string[]}[]
[]
optional filters of requested historical data feed - check historical data details for each exchange and /exchanges/:exchange HTTP API to get allowed channels and symbols for requested exchange
from
string
-
replay period start date (UTC) in a ISO 8601 format, e.g., 2019-04-01
to
string
-
replay period end date (UTC) in a ISO 8601 format, e.g., 2019-04-02
withDisconnects
boolean | undefined
undefined
when set to true, response includes empty lines (\n) that mark events when real-time WebSocket connection that was used to collect the historical data got disconnected

Response format

Streamed HTTP response provides data in NDJSON format (new line delimited JSON) - each response line is a JSON with market data message in exchange-native format plus local timestamp:
    localTimestamp - date when message has been received in ISO 8601 format
    message - JSON with exactly the same format as provided by requested exchange real-time feeds
Sample response
1
{"localTimestamp":"2019-05-01T00:09:42.2760012Z","message":{"table":"orderBookL2","action":"update","data":[{"symbol":"XBTUSD","id":8799473750,"side":"Buy","size":2333935}]}}
2
{"localTimestamp":"2019-05-01T00:09:42.2932826Z","message":{"table":"orderBookL2","action":"update","data":[{"symbol":"XBTUSD","id":8799474250,"side":"Buy","size":227485}]}}
3
{"localTimestamp":"2019-05-01T00:09:42.4249304Z","message":{"table":"trade","action":"insert","data":[{"timestamp":"2019-05-01T00:09:42.407Z","symbol":"XBTUSD","side":"Buy","size":1500,"price":5263,"tickDirection":"ZeroPlusTick","trdMatchID":"29d7de7f-27b6-9574-48d1-3ee9874831cc","grossValue":28501500,"homeNotional":0.285015,"foreignNotional":1500}]}}
4
{"localTimestamp":"2019-05-01T00:09:42.4249403Z","message":{"table":"orderBookL2","action":"update","data":[{"symbol":"XBTUSD","id":8799473700,"side":"Sell","size":454261}]}}
5
{"localTimestamp":"2019-05-01T00:09:42.4583155Z","message":{"table":"orderBookL2","action":"update","data":[{"symbol":"XBTUSD","id":8799473750,"side":"Buy","size":2333838},{"symbol":"XBTUSD","id":8799473800,"side":"Buy","size":547746}]}}
Copied!

WebSocket /ws-replay?exchange={exchange}&from={fromDate}&to={toDate}

Exchanges' WebSocket APIs are designed to publish real-time market data feeds, not historical ones. Tardis-machine WebSocket /ws-replay API fills that gap and allows "replaying" historical market data from any given past point in time with the same data format and 'subscribe' logic as real-time exchanges' APIs. In many cases existing exchanges' WebSocket clients can be used to connect to this endpoint just by changing URL, and receive historical market data in exchange-native format for date ranges specified in URL query string params.
After connection is established, client has 2 seconds to send subscriptions payloads and then market data replay starts.
If two clients connect at the same time requesting data for different exchanges and provide the same session key via query string param, then data being send to those clients will be synchronized (by local timestamp).
In our preliminary benchmarks on AMD Ryzen 7 3700X, 64GB RAM, WebSocket /ws-replay API endpoint was sending ~500 000 messages/s (already locally cached data).
Python
Node.js
Your preferred language
1
import asyncio
2
import aiohttp
3
import json
4
5
6
async def run():
7
WS_REPLAY_URL = "ws://localhost:8001/ws-replay"
8
URL = f"{WS_REPLAY_URL}?exchange=bitmex&from=2019-10-01&to=2019-10-02"
9
10
async with aiohttp.ClientSession() as session:
11
async with session.ws_connect(URL) as websocket:
12
13
await websocket.send_str(
14
json.dumps(
15
{
16
"op": "subscribe",
17
"args": [
18
"trade:XBTUSD",
19
"trade:ETHUSD",
20
"orderBookL2:XBTUSD",
21
"orderBookL2:ETHUSD",
22
],
23
}
24
)
25
)
26
27
async for msg in websocket:
28
print(msg.data)
29
30
31
asyncio.run(run())
Copied!
You can also try using existing WebSocket client by changing URL endpoint to the one shown in the example above.
1
const WebSocket = require('ws')
2
3
4
const WS_REPLAY_URL = 'ws://localhost:8001/ws-replay'
5
6
const ws = new WebSocket(
7
`${WS_REPLAY_URL}?exchange=bitmex&from=2019-10-01&to=2019-10-02`
8
)
9
10
ws.onmessage = message => {
11
console.log(message.data)
12
}
13
14
ws.onopen = () => {
15
ws.send(
16
JSON.stringify({
17
op: 'subscribe',
18
args: [
19
'trade:XBTUSD',
20
'trade:ETHUSD',
21
'orderBookL2:XBTUSD',
22
'orderBookL2:ETHUSD'
23
]
24
})
25
)
26
}
Copied!
You can also use existing WebSocket client, just by changing URL endpoint as shown in the example below that uses ccxws.
1
const ccxws = require('ccxws')
2
const BASE_URL = 'ws://localhost:8001/ws-replay'
3
const WS_REPLAY_URL = `${BASE_URL}?exchange=bitmex&from=2019-10-01&to=2019-10-02`
4
5
const bitMEXClient = new ccxws.bitmex()
6
// only change required for ccxws client is to point it to /ws-replay URL
7
bitMEXClient._wssPath = WS_REPLAY_URL
8
9
const market = {
10
id: 'XBTUSD',
11
base: 'BTC',
12
quote: 'USD'
13
}
14
15
bitMEXClient.on('l2snapshot', snapshot =>
16
console.log('snapshot', snapshot.asks.length, snapshot.bids.length)
17
)
18
19
bitMEXClient.on('l2update', update => console.log(update))
20
21
bitMEXClient.on('trade', trade => console.log(trade))
22
23
bitMEXClient.subscribeTrades(market)
24
bitMEXClient.subscribeLevel2Updates(market)
Copied!
As long as you already use existing WebSocket client that connects to and consumes real-time exchange market data feed, in most cases you can use it to connect to /ws-replay API as well just by changing URL endpoint.

Query string params

name
type
default
description
exchange
string
-
requested exchange id - use /exchanges HTTP API to get list of valid exchanges ids
from
string
-
replay period start date (UTC) in a ISO 8601 format, e.g., 2019-04-01
to
string
-
replay period end date (UTC) in a ISO 8601 format, e.g., 2019-04-02
session
string | undefined
undefined
optional replay session key. When specified and multiple clients use it when connecting at the same time then data being send to those clients is synchronized (by local timestamp).

Normalized market data APIs

Normalized market data API endpoints provide data in unified format across all supported exchanges. Both HTTP /replay-normalized and WebSocket /ws-replay-normalized APIs accept the same replay options payload via query string param. It's mostly matter of preference when choosing which protocol to use, but WebSocket /ws-replay-normalized API has also it's real-time counterpart /ws-stream-normalized, which connects directly to exchanges' real-time WebSocket APIs. This opens the possibility of seamless switching between real-time streaming and historical normalized market data replay.

HTTP GET /replay-normalized?options={options}

Returns historical market data for data types specified via query string. Single streaming HTTP response returns data for the whole requested time period as NDJSON. See supported data types which include normalized trade, order book change, customizable order book snapshots etc.
In our preliminary benchmarks on AMD Ryzen 7 3700X, 64GB RAM, HTTP /replay-normalized API endpoint was returning ~100 000 messages/s and ~50 000 messages/s when order book snapshots were also requested.
Python
Node.js
cURL
Your preferred language
1
import asyncio
2
import aiohttp
3
import json
4
import urllib.parse
5
6
7
async def replay_normalized_via_tardis_machine_machine(replay_options):
8
timeout = aiohttp.ClientTimeout(total=0)
9
10
async with aiohttp.ClientSession(timeout=timeout) as session:
11
# url encode as json object options
12
encoded_options = urllib.parse.quote_plus(json.dumps(replay_options))
13
14
# assumes tardis-machine HTTP API running on localhost:8000
15
url = f"http://localhost:8000/replay-normalized?options={encoded_options}"
16
17
async with session.get(url) as response:
18
# otherwise we may get line to long errors
19
response.content._high_water = 100_000_000
20
21
# returned data is in NDJSON format http://ndjson.org/ streamed
22
# each line is separate message JSON encoded
23
async for line in response.content:
24
yield line
25
26
27
async def run():
28
lines = replay_normalized_via_tardis_machine_machine(
29
{
30
"exchange": "bitmex",
31
"from": "2019-10-01",
32
"to": "2019-10-02",
33
"symbols": ["XBTUSD", "ETHUSD"],
34
"withDisconnectMessages": True,
35
# other available data types examples:
36
# 'book_snapshot_10_100ms', 'derivative_ticker', 'quote',
37
# 'trade_bar_10ms', 'trade_bar_10s'
38
"dataTypes": ["trade", "book_change", "book_snapshot_10_100ms"],
39
}
40
)
41
42
async for line in lines:
43
normalized_message = json.loads(line)
44
print(normalized_message)
45
46
47
asyncio.run(run())
Copied!
1
const fetch = require('node-fetch')
2
const split2 = require('split2')
3
4
5
const serialize = options => {
6
return encodeURIComponent(JSON.stringify(options))
7
}
8
9
async function* replayNormalizedViaTardisMachine(options) {
10
// assumes tardis-machine HTTP API running on localhost:8000
11
const url = `http://localhost:8000/replay-normalized?options=${serialize(
12
options
13
)}`
14
const response = await fetch(url)
15
16
// returned data is in NDJSON format http://ndjson.org/
17
// each line is separate message JSON encoded
18
19
// split response body stream by new lines
20
const lines = response.body.pipe(split2())
21
22
for await (const line of lines) {
23
yield line
24
}
25
}
26
27
async function run() {
28
const options = {
29
exchange: 'bitmex',
30
from: '2019-10-01',
31
to: '2019-10-02',
32
symbols: ['XBTUSD', 'ETHUSD'],
33
withDisconnectMessages: true,
34
// other available data types examples:
35
// 'book_snapshot_10_100ms', 'derivative_ticker', 'quote',
36
// 'trade_bar_10ms', 'trade_bar_10s'
37
dataTypes: ['trade', 'book_change', 'book_snapshot_10_100ms']
38
}
39
40
const lines = replayNormalizedViaTardisMachine(options)
41
42
for await (const line of lines) {
43
const normalizedMessage = JSON.parse(line)
44
45
console.log(normalizedMessage)
46
}
47
}
48
49
run()
Copied!
See also official Tardis.dev Node.js client library.
1
curl -g 'http://localhost:8000/replay-normalized?options={"exchange":"bitmex","from":"2019-10-01","to":"2019-10-02","symbols":["XBTUSD","ETHUSD"],"withDisconnectMessages":true,"dataTypes":["trade","book_change","book_snapshot_10_100ms"]}'
Copied!
http://localhost:8000/replay-normalized?options={%22exchange%22:%22bitmex%22,%22from%22:%222019-10-01%22,%22to%22:%222019-10-02%22,%22symbols%22:[%22XBTUSD%22,%22ETHUSD%22],%22withDisconnectMessages%22:true,%22dataTypes%22:[%22trade%22,%22book_change%22,%22book_snapshot_10_100ms%22]}
localhost
Click to see API response in the browser as long as tardis-machine is running on localhost:8000
We're working on providing more samples and dedicated client libraries in different languages, but in the meanwhile to consume HTTP /replay-normalized API responses in your language of choice, you should:
    1.
    Provide url encoded JSON options via options query string param when sending HTTP request
    2.
    Parse HTTP response stream line by line as it's returned - buffering in memory whole response may result in slow performance and memory overflows
    3.
    Parse each response line as JSON containing normalized data messages.

Replay normalized options

HTTP /replay-normalized endpoint accepts required options query string param in url encoded JSON format.
Options JSON needs to be an object or an array of objects with fields as specified below. If array is provided, then data requested for multiple exchanges is returned synchronized (by local timestamp).
name
type
default
exchange
string
-
requested exchange id - use /exchanges HTTP API to get list of valid exchanges ids
symbols
string[] | undefined
undefined
optional symbols of requested historical data feed - use /exchanges/:exchange HTTP API to get allowed symbols for requested exchange
from
string
-
replay period start date (UTC) in a ISO 8601 format, e.g., 2019-04-01
to
string
-
replay period end date (UTC) in a ISO 8601 format, e.g., 2019-04-02
dataTypes
string[]
-
array of normalized data types for which historical data will be returned
withDisconnectMessages
boolean | undefined
undefined
when set to true, response includes disconnect messages that mark events when real-time WebSocket connection that was used to collect the historical data got disconnected

Response format & sample messages

WebSocket /ws-replay-normalized?options={options}

Sends normalized historical market data for data types specified via query string. See supported data types which include normalized trade, order book change, customizable order book snapshots etc.
WebSocket /ws-stream-normalized is the real-time counterpart of this API endpoint, providing real-time market data in the same format, but not requiring API key as connects directly to exchanges' real-time WebSocket APIs.
Python
Node.js
Your preferred language
1
import asyncio
2
import aiohttp
3
import json
4
import urllib.parse
5
6
7
async def run():
8
replay_options = {
9
"exchange": "bitmex",
10
"from": "2019-10-01",
11
"to": "2019-10-02",
12
"symbols": ["XBTUSD", "ETHUSD"],