Skip to main content
Version: Upcoming

MLink WebSocket API

The MLink WebSocket API is implemented as a standard HTTP WebSocket service accessible at:

NMS / USA:

  • Real-time data - wss://mlink-live.nms.saturn.spiderrockconnect.com/mlink/
  • Delayed data - wss://mlink-delay.nms.saturn.spiderrockconnect.com/mlink/

Europe:

  • Real-time data - wss://mlink-live.fr2.saturn.spiderrockconnect.com/mlink/
  • Delayed data - wss://mlink-delay.fr2.saturn.spiderrockconnect.com/mlink/

You can authenticate to MLink through two 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.

  1. Login (WebSocket): Use this method to authenticate your application by having users log in with their SpiderRock Connect ID credentials along with multi-factor authentication (MFA).

    • The SpiderRock Client Support Desk will issue you a SpiderRock Connect ID, and 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. Once a user authenticates with their SpiderRock Connect ID credentials, their Session Key will be automatically issued and used for the remainder of the session.

  2. 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, and 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, 
additional_headers={"Authorization": "Bearer {apikey}"}, ping_timeout=None) as websocket:

WebSocket Parameters - MLinkStream

MessageDescription
MLinkStreamMLinkStream 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

MessageDescription
MLinkSubscribeMLinkSubscribe 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 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 primary keys in your message before it starts streaming.

Clients can determine when cache has been cleared using 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 that cache has cleared.
  • MLink is now streaming.

Below is an example of streaming the following request for an 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 NameField TypeDescription
sessionIDshort(Optional) Actions below apply only to the sessionID virtual session; should be zero for non-multiplexed WebSocket connections.
signalIDGroupingCode(Optional) Will be reflected back in xCheckPt.signalID fields that indicate a specified signal ready triggered active send is complete.
readyScanenum(Optional; default is Incremental) Incremental = messages with 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 activeLatency is set to any integer N greater than or equal to zero, it will automatically trigger 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 in 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, allows 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 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 two message requests enable this:

SignalReady (should be sent at 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,
    additional_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,
    additional_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...")