Search code examples
pythonfix-protocolcoinbase-apigdax-api

How to send FIX logon message with Python to GDAX/Coinbase


I'm trying to establish a FIX 4.2 session to fix.gdax.com (docs: https://docs.gdax.com/#fix-api or https://docs.prime.coinbase.com/?python#logon-a) using Python 3.5 and stunnel. Everything is working apart from my logon message which is rejected and the session is closed by the server with no response making it difficult to debug what's going wrong. My Python code is as follows:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 4197)) # address and port specified in stunnel config file

# generate a signature according to the gdax protocol for signing a message:
timestamp = str(time.time())
message   = [timestamp, "A", "0", "f3e85389ffb809650c367d42b37e0a80", "Coinbase", "password-goes-here"] # these are the components of the pre-hash string as specified in the docs for a logon message
message   = bytes("|".join(message), 'utf-8') # add the field separator

hmac_key  = base64.b64decode(r"api-secret-goes-here")
signature = hmac.new(hmac_key, message, hashlib.sha256)
sign_b64  = base64.b64encode(signature.digest()).decode()
# in the above line the .decode() is not included when used to authenticate messages to the REST API and those are working successfully.
#The reason I've included it here is to allow a string to be passed into the variable 'body' below:

msgType    = "A"
t          = str(datetime.utcnow()).replace("-","").replace(" ", "-")[:-3] # format the timestamp into YYYYMMDD-HH:MM:SS.sss as per the FIX standard

body       = '34=1|52=%s|49=f3e85389ffb809650c367d42b37e0a80|56=Coinbase|98=0|108=30|554=password-goes-here|96=%s|8013=Y|' % (t, sign_b64)
bodyLength = len(body.encode('utf-8')) # length of the message in bytes
header     = '8=FIX.4.2|9=%s|35=%s|' % (bodyLength, msgType)
msg        = header + body

# generate the checksum:
def check_sum(s):
    sum = 0
    for char in msg:
        sum += ord(char)
    sum = str(sum % 256)
    while len(sum) < 3:
        sum = '0' + sum
    return sum

c_sum = check_sum(msg)
logon = msg + "10=%s" % c_sum # append the check sum onto the message
logon = logon.encode('ascii') # create a bytes object to send over the socket
print(logon)

s.sendall(logon)
print(s.recv(4096))

The results of those two print statements are:

b'8=FIX.4.2|9=159|35=A|34=1|52=20171104-11:13:53.331|49=f3e85389ffb809650c367d42b37e0a80|56=Coinbase|98=0|108=30|554=password-goes-here|96=G7yeX8uQqsCEhAjWDWHoBiQz9lZuoE0Q8+bLJp4XnPY=|8013=Y|10=212'
b''

There are a lot of variables here that could be wrong and the process of trial and error is getting a bit tedious. Can anyone see what is wrong with the logon message?


Solution

  • I made some modifications to your code and put comments where it differs from yours (corrects your errors):

    import socket
    import base64
    import time, datetime
    import hmac
    import hashlib
    
    PASSPHRASE = "your passphrase"
    API_KEY = "your api key"
    API_SECRET = "your secret"
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("127.0.0.1", 4197))
    
    seq_num = "1" # Correction: using the same MsgSeqNum for signed text and for the field 34
    
    
    # Correction: t is the same in both signed RawData and in SendingTime (52)
    timestamp = str(time.time())
    t = str(datetime.datetime.utcnow()).replace("-","").replace(" ", "-")[:-3]
    
    # Correction: '|' is not a valid separator for FIX, it must be '\u0001'
    message   = "\u0001".join([t, "A", seq_num, API_KEY, "Coinbase", PASSPHRASE]).encode("utf-8")
    
    hmac_key  = base64.b64decode(API_SECRET)
    signature = hmac.new(hmac_key, message, hashlib.sha256)
    sign_b64  = base64.b64encode(signature.digest()).decode()
    
    msgType = "A"
    
    body = "34={}|52={}|49={}|56=Coinbase|98=0|108=30|554={}|96={}|8013=Y|".format(seq_num, t, API_KEY, PASSPHRASE, sign_b64) # using the same time (t) and seq_num as in signed text
    
    # Correction: bodyLength is the number of characters, not bytes, also it must include everything after "8=FIX.4.2|9={}|" i.e. the "35=A|" part of the header
    bodyLength = len("35={}|".format(msgType)) + len(body)
    header     = "8=FIX.4.2|9={}|35={}|".format(bodyLength, msgType)
    msg        = header + body
    
    msg = msg.replace('|', '\u0001') # Correction: '|' is not a valid separator for FIX, it must be '\u0001'
    
    # generate the checksum:
    def check_sum(s):
        sum = 0
        for char in msg:
            sum += ord(char)
        sum = str(sum % 256)
        while len(sum) < 3:
            sum = '0' + sum
        return sum
    
    c_sum = check_sum(msg)
    
    logon = msg + "10={}\u0001".format(c_sum)
    logon = logon.encode('ascii')
    print(logon)
    
    s.sendall(logon)
    print(s.recv(4096))
    

    For me this corrected code now returns the Logon message from the server instead of just 0 bytes as it was in your case. Can you confirm that it also works for you and that you can successfully send other transactions after logon is done?