Cómo enviar un mensaje de inicio de sesión FIX con Python a GDAX / Coinbase

Estoy intentando establecer una sesión de FIX 4.2 para fix.gdax.com (docs: https://docs.gdax.com/#fix-api o https://docs.prime.coinbase.com/?python#logon -a ) usando Python 3.5 y stunnel. Todo funciona aparte del mensaje de inicio de sesión que se rechaza y el servidor cierra la sesión sin respuesta, lo que dificulta la depuración de los errores. Mi código de Python es el siguiente:

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)) 

Los resultados de esas dos declaraciones impresas son:

 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'' 

Aquí hay muchas variables que podrían estar equivocadas y el proceso de prueba y error se está volviendo un poco tedioso. ¿Alguien puede ver lo que está mal con el mensaje de inicio de sesión?

Hice algunas modificaciones a su código y puse comentarios donde difieren de los suyos (corrige sus errores):

 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={}|" ie 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)) 

Para mí, este código corregido ahora devuelve el mensaje de inicio de sesión del servidor en lugar de solo 0 bytes como en su caso. ¿Puede confirmar que también funciona para usted y que puede enviar con éxito otras transacciones después de que se inicie la sesión?

No hay nada nuevo que agregar, solo quería reformular la solución anterior de una manera más basada en funciones sin hacer túneles:

 import socket import base64 import time, datetime import hmac import hashlib import ssl host = 'fix.gdax.com' #sandbox_host = 'fix-public.sandbox.gdax.com' port = 4198 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssl_sock = context.wrap_socket(s, server_hostname=host) ssl_sock.connect((host, port)) def check_sum(s): sum = 0 for char in s: sum += ord(char) sum = str(sum % 256) while len(sum) < 3: sum = '0' + sum return sum def sign(t, msg_type, seq_num, api_key, password, secret): message = "\x01".join([t, msg_type, seq_num, api_key, "Coinbase", password]).encode("utf-8") hmac_key = base64.b64decode(secret) signature = hmac.new(hmac_key, message, hashlib.sha256) return base64.b64encode(signature.digest()).decode() def wrap_fix_string(msg_type, body): bodyLength = len("35={}|".format(msg_type)) + len(body) header = "8=FIX.4.2|9=00{}|35={}|".format(bodyLength, msg_type) msg = header + body return msg def generate_login_string(seq_num, t, api_key, password, secret): msgType = "A" sign_b64 = sign(t, msgType, seq_num, api_key, password, secret) body = f"49={api_key}|554={password}|96={sign_b64}|8013=S|52={t}|56=Coinbase|98=0|108=30|34={seq_num}|9406=N|" # using the same time (t) and seq_num as in signed text msg = wrap_fix_string(msgType, body) msg = msg.replace('|', '\x01') c_sum = check_sum(msg) return msg + "10={}\x01".format(c_sum) PASSPHRASE = "your passphrase" API_KEY = "your api key" API_SECRET = "your secret" seq_num = "1" t = str(datetime.datetime.utcnow()).replace("-","").replace(" ", "-")[:-3] logon = generate_login_string(seq_num, t, API_KEY, PASSPHRASE, API_SECRET) logon = logon.encode('ascii') print(f'logon: {logon}') ssl_sock.sendall(logon) print('GETTING') print(ssl_sock.recv(4096))