MLink Websocket API
The MLink Websocket API is implemented as a standard HTTP WebSocket service accessible at:
- Real-time data - wss://mlink-live.nms.saturn.spiderrockconnect.com/mlink/
- Delayed data - wss://mlink-delay.nms.saturn.spiderrockconnect.com/mlink/
Authentication to MLink
You can authenticate to MLink through 2 different methods: Login or API Key. The Login method is intended for humans to access MLink via only a WebSocket connection. The API key method is intended for machines to access MLink through either a WebSocket connection or a REST API connection.
-
Login (WebSocket): Use this method to authenticate your application by challenging humans to login with their SpiderRock Connect ID credentials along with multi-factor authentication (MFA).
-
The SpiderRock Client Support Desk will issue you a SpiderRock ID, by which you will receive an invitation to your registered email. From there, you will complete your setup and select which MFA method you want: Authenticator App or SMS. Upon completion of sign up you will have access to MLink as determined by your configuration.
-
Session Keys: a Session Key looks similar to an API key but is issued for each valid session after login. Therefore, once a user authenticates with their SpiderRock ID credentials their Session Key will be automatically issued and used for the remainder of the session.
-
-
API Key (WebSocket ): Use this method to authenticate your application directly to MLink if there is no ability to use MFA or you only require a single data feed.
-
The SpiderRock Client Support Desk will issue an
M2MUser
user as a datafeed profile, by which we will generate an API Key scoped to the permissions requested by the customer. -
Using an API Key via WebSocket: Send an MLinkLogon to MLink servers, OR include
apikey
as a header in the initial HTTP handshake request:
-
{
"header": {"mTyp": "MLinkLogon"},
"message": {
"apiKey": "1234-5678-9012-3141" # Replace with actual API key or session key
}
}
async with websockets.connect(uriJson,
extra_headers={"Authorization": "Bearer {apikey}"}, ping_timeout=None) as websocket:
Websocket Parameters - MLinkStream:
Message | Description |
---|---|
MLinkStream | MLinkStream is a message type used to set or update an active subscription for a session by specifying a particular message type (msgName). This message allows clients to specify message types, filter conditions, and update frequency. It is essential for managing general subscriptions to specific streams. |
{
"header": {
"mTyp": "MLinkStream"
},
"message": {
"queryLabel": "ExampleStockBookQuote",
"activeLatency": 1,
"msgName": "stockbookquote",
"where": "ticker.tk:eq:AMZN",
"view": "bidprice|askprice|bidsize|asksize"
}
}
Websocket Parameters - MLinkSubscribe:
Message | Description |
---|---|
MLinkSubscribe | MLinkSubscribe is a message type used to set or update an active subscription for a session by specifying a particular message primary key (msgPKey) for a specific message and view. This message type is essential for managing subscriptions to specific primary keys within streams. |
{
"header": {
"mTyp": "MLinkSubscribe"
},
"message": {
"activeLatency": 1,
"doReset":"No",
"View":[{"msgName": "LiveImpliedQuote",
"view":"de|ga|th|ve"}],
"Subscribe": [{"msgName": "LiveImpliedQuote",
"msgPKey":"MSFT-NMS-EQT-2024-09-20-580-C"}],
}
}
Upon opening up a WebSocket connection to MLink, Authenticating, and requesting to stream a message (msgName) with user specified conditions (view, where, etc), the MLinkServer will initially clear cache. This means it will forward the latest updates to all of the primary keys in your message before it starts streaming.
Clients have the possibility to determine when cache has been cleared using the MLinkAdmin messages: The MLink Servers follow a specific sequence for clearing cache when starting a stream. The process involves sending initial administrative and acknowledgement messages, followed by cache clearance checkpoints, and finally, the streaming of messages. If there is an issue with the message construction or permissions, the MLinkStreamAck message will indicate an error.
- Initially, upon authentication MLinkServers will send an MLinkAdmin with the authentication state
- MLinkServers will then send an Acknowledgement message of type MLinkStreamAck or MLinkSubscribeAck to acknowledge reception of the message request.
- A first checkpoint MLinkStreamCheckPt is then sent with a message body state of begin (begin to clear cache)
- MLinkServers send the cached records
- A secondary checkpoint MLinkStreamCheckPt is then sent with a message body state of Active (request is active)
- The state.Active MLinkStreamCheckPt is followed by another MLinkStreamCheckPt with a message body state of Complete to alert the client side that cache has cleared
- MLink is now streaming.
Below is an example of streaming the following request, AMZN stock quote:
msg = {
"header": {
"mTyp":"MLinkStream"
},
"message": {
"queryLabel":"ExampleStockBookQuote",
"activeLatency": 1,
"msgName":"stockbookquote",
"where":"ticker.tk:eq:AMZN"
}
}
Response Order:
{'header': {'mTyp': 'MLinkAdmin', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 0, 'kLen': 0, 'mLen': 0, 'seqN': 0, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:23.464116', 'encT': '2023-11-09 15:26:23.464120', 'git': 'b981c14047'}, 'message': {'state': 'LoggedOn'}}
{'header': {'mTyp': 'MLinkStreamAck', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 0, 'kLen': 0, 'mLen': 0, 'seqN': 0, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:23.573021', 'encT': '2023-11-09 15:26:23.573023', 'git': 'b981c14047'}, 'message': {'msgName': 'stockbookquote', 'result': 'OK'}}
{'header': {'mTyp': 'MLinkStreamCheckPt', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 0, 'kLen': 0, 'mLen': 0, 'seqN': 0, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:23.573034', 'encT': '2023-11-09 15:26:23.573035', 'git': 'b981c14047'}, 'message': {'state': 'Begin', 'timestamp': '2023-11-09 15:26:23.566996'}}
{'header': {'mTyp': 'StockBookQuote', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 18409, 'kLen': 0, 'mLen': 0, 'seqN': 206, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:29.699590', 'encT': '2023-11-09 15:26:29.707677', 'git': 'b981c14047'}, 'message': {'pkey': {'ticker': {'at': 'EQT', 'ts': 'NMS', 'tk': 'AMZN'}}, 'updateType': 'PrcChange', 'marketStatus': 'Open', 'bidPrice1': 141.69, 'bidSize1': 9, 'bidExch1': 'EDGX', 'bidMask1': 18240, 'askPrice1': 141.7, 'askSize1': 1, 'askExch1': 'MPRL', 'bidPrice2': 141.68, 'bidSize2': 1, 'bidExch2': 'IEX', 'askPrice2': 141.71, 'askSize2': 8, 'askExch2': 'EDGX', 'askMask2': 18240, 'srcTimestamp': 1699543589698404599, 'netTimestamp': 1699543589698443200}}
{'header': {'mTyp': 'MLinkStreamCheckPt', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 0, 'kLen': 0, 'mLen': 0, 'seqN': 0, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:23.573034', 'encT': '2023-11-09 15:26:45.596555', 'git': 'b981c14047'}, 'message': {'state': 'Active', 'numMessagesSent': 1, 'timestamp': '2023-11-09 15:26:45.592286'}}
{'header': {'mTyp': 'MLinkStreamCheckPt', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 0, 'kLen': 0, 'mLen': 0, 'seqN': 0, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:23.573034', 'encT': '2023-11-09 15:26:45.596582', 'git': 'b981c14047'}, 'message': {'state': 'Complete', 'waitElapsed': -22.0229465, 'queryElapsed': 22.0229815, 'tryFwdElapsed': -1.5e-05, 'flushElapsed': 1.5e-05, 'timestamp': '2023-11-09 15:26:45.592286'}}
{'header': {'mTyp': 'StockBookQuote', 'sEnv0': 'Saturn', 'sRlm0': 'NMS', 'sID': 18409, 'kLen': 0, 'mLen': 0, 'seqN': 93, 'appN': 'MLinkServer.Prod.B.Extern', 'mn': 'SROCKNY5-130', 'sTim': '2023-11-09 15:26:47.543689', 'encT': '2023-11-09 15:26:47.611402', 'git': 'b981c14047'}, 'message': {'pkey': {'ticker': {'at': 'EQT', 'ts': 'NMS', 'tk': 'AMZN'}}, 'updateType': 'PrcChange', 'marketStatus': 'Open', 'bidPrice1': 141.64, 'bidSize1': 22, 'bidExch1': 'EDGX', 'bidMask1': 18240, 'askPrice1': 141.66, 'askSize1': 18, 'askExch1': 'EDGX', 'askMask1': 18240, 'bidPrice2': 141.62, 'bidSize2': 1, 'bidExch2': 'IEX', 'askPrice2': 141.71, 'askSize2': 1, 'askExch2': 'PSX', 'askMask2': 4096, 'srcTimestamp': 1699543607542434826, 'netTimestamp': 1699543607542516100}}
Websocket Active Latency
MLinkStream contains an ActiveLatency field that governs subsequent updates after the initial (cache) query. If this field is set to 0 then the user is required to send a SignalReady message which will trigger the MLinkServer to send an update (if any) to all response messages.
MLinkSignalReady
Field Name | Field Type | Description |
---|---|---|
sessionID | short | (Optional) Actions below apply only to the sessionID virtual session; should be zero for non-multiplexed web-socket connections. |
signalID | GroupingCode | (Optional) Will be reflected back in xCheckPt.signalID fields that indicates that a specified signal ready triggered active send is complete. |
readyScan | enum | (Optional; default is Incremental) Incremental = messages w/changes (all fields; cumulative changes) since previous MLinkSignalReady; FullScan = all messages. |
Note:
- It is possible for the MLinkServer to have received multiple updates to a single Primary Key between successive SignalReady messages. If this occurs, only the most recent record update will be forwarded to the client.
- If activelyLatency is set to any integer N greater than or equal to zero it will have the effect of automatically triggering the transmission of any pending updates every N milliseconds with zero being interpreted as no delay.
- If the client system is unable to process messages at the speed with which they are being sent, MLinkServer will fall back to sending messages at the rate the client is able to receive, which will also result is some messages being skipped in favor of more recent updates.
MLinkSignalReady and LiveImpliedQuoteAdj stream
The underlier-adjusted version of LiveImpliedQuote records used in parallel with MLinkSignalReady allow users to recalculate surfaces on demand. All records returned by the MLinkServer will be adjusted for the latest underlying price for each specific ticker.
After opening up a connection and setting the activeLatency to 0, users can send a series of MLinkSignalReady messages with a readyScan of FullScan to generate an underlier-adjusted response. With reasonable interval time between each, users can actively sample the LiveImpliedQuoteAdj message.
The following 2 message requests allow for this:
SignalReady (Should be sent in intervals through the same connection)
signal = {
"header": {
"mTyp": "MLinkSignalReady"
},
"message": {
"readyScan": "FullScan" #[readyScan:enum:ReadyScan:None=0,Incremental=2,FullScan=3]
}
}
MLinkStream
msg = {
"header": {
"mTyp": "MLinkStream"
},
"message": {
"queryLabel": "ExampleImpliedQuoteAdj",
"activeLatency": 0, #wait for Signal ready
"msgName": "LiveImpliedQuoteAdj",
"where":"ticker:eq:AAPL-NMS-EQT" #can also do ticker.tk:eq:AAPL & ticker.at:eq:EQT & ticker.ts:eq:NMS
}
}
Please refer to client-python\JSONFramed\examples\clientSignalReady.py for further examples.
Establishing a connection
After authenticating a websocket connection:
- The first message sent from the client to MLink must be an "MLinkLogon" message containing either an API Key or SessionKey.
- The first message sent from MLink to the client will be an "MLinkAdmin" message containing an application state [LoggedOn, WaitingForLogon, AuthError, OtherError] depending on the authentication state.
If at any time during a session, a user sends an MLinkLogon message, the server will attempt to (re-)authenticate the user and will return an MLinkAdmin message.
Protocol Usage
-
JSON - Standard JSON, each websocket frame can only contain one JSON message at a time.
Import Classes:
import asyncio
import json
import time
import websockets
import nest_asyncio
import threading
import datetime
nest_asyncio.apply()Authentication:
uriJson = "wss://mlink-live.nms.saturn.spiderrockconnect.com/mlink/json"
apiKey = 'your api key'
password = 'your password'
api_key_token = f"{apiKey}.{password}"Asynchronously query AAPL:
async def recv_msg(websocket):
buffer = await websocket.recv()
parts = list(filter(None, buffer.split(b'\r\n')))
for msg in parts:
result = json.loads(msg)
print(result, '\n')
return True
async def query_mlink(api_key_token):
retry = True
while retry:
try:
async with websockets.connect(
uriJson,
extra_headers={"Authorization": f"Bearer {api_key_token}"},
ping_timeout=None
) as websocket:
msg = {
"header": {
"mTyp": "MLinkStream"
},
"message": {
"queryLabel": "ExampleStockNbbo",
"activeLatency": 1, #1 ms
"msgName": "StockBookQuote",
"where":"ticker.tk:eq:AAPL | ticker.at:eq:EQT | ticker.ts:eq:NMS"
}
}
t = time.time_ns()
tstr = '.'.join([time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(t / 1000000000)), "%06d" % ((t / 1000) % 1000000)])
msg['header']['sTim'] = tstr
msg['header']['encT'] = tstr
smsg = json.dumps(msg)
await websocket.send(smsg)
notDone = True
while notDone:
notDone = await recv_msg(websocket)
retry = False
except asyncio.exceptions.TimeoutError:
print("timeout occurred, retrying...") -
Framed JSON - SpiderRock JSON with protobuf-like header.
Same as JSON above, except for the parser:
async def query_mlink(api_key_token):
retry = True
while retry:
try:
async with websockets.connect(
uriJson,
extra_headers={"Authorization": f"Bearer {api_key_token}"},
ping_timeout=None
) as websocket:
msg = {
"header": {
"mTyp": "MLinkStream"
},
"message": {
"queryLabel": "ExampleStockNbbo",
"activeLatency": 1, #stream
"msgName": "StockBookQuote",
"where":"ticker.tk:eq:AAPL & ticker.at:eq:EQT & ticker.ts:eq:NMS"
}
}
t = time.time_ns()
tstr = '.'.join([time.strftime("%Y-%m-%d %H:%M:%S",time.gmtime(t/1000000000)),"%06d"%((t/1000)%1000000)])
msg['header']['sTim'] = tstr
msg['header']['encT'] = tstr
smsg = json.dumps(msg)
jmsg = ''.join(['\r\nJ', '%011d'%len(smsg), smsg]) #header
await websocket.send(jmsg)
notDone = True
while notDone:
buffer = await websocket.recv()
parts = list(filter(None,buffer.split(b'\r\n')))
for msg in parts:
result = json.loads(msg[12:])
print(result, '\n')
except asyncio.exceptions.TimeoutError:
print("timeout occurred, retrying...")