Как отправить сообщение о входе FIX с помощью Python в GDAX/Coinbase

Я пытаюсь установить сеанс FIX 4.2 для fix.gdax.com (документы: https://docs.gdax.com/#fix-api или https://docs.prime.coinbase.com/?python#logon-a) с использованием Python 3.5 и Stunnel. Все работает, за исключением моего сообщения о входе в систему, которое было отклонено, и сеанс был закрыт сервером без ответа, что затрудняет отладку того, что происходит не так. Мой код Python выглядит следующим образом:

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

Результаты этих двух операторов печати:

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

Здесь есть много переменных, которые могут быть неправильными, и процесс проб и ошибок становится немного утомительным. Может ли кто-нибудь увидеть, что не так с сообщением о входе в систему?


person jp94    schedule 04.11.2017    source источник
comment
Похоже, вы используете | в качестве разделителя полей. Разделителем полей FIX является символ ASCII 1 (я полагаю, что это \x01 в Python), поэтому ваши сообщения не соответствуют протоколу FIX, что может объяснить отсутствие ответа. Я настоятельно рекомендую использовать существующую библиотеку FIX, а не пытаться реализовать свою собственную.   -  person Iridium    schedule 04.11.2017
comment
@Iridium Я тоже пробовал это, но безрезультатно :( Вместо этого я попробую библиотеку, спасибо!   -  person jp94    schedule 04.11.2017
comment
У меня почти такой же код, как у вас, и я получаю результат со значением msgSeqNum (34) › 1. Однако он все еще не входит в систему.   -  person bill0ute    schedule 08.11.2017


Ответы (2)


Я внес некоторые изменения в ваш код и добавил комментарии там, где он отличается от вашего (исправляет ваши ошибки):

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

Для меня этот исправленный код теперь возвращает сообщение о входе в систему с сервера, а не просто 0 байт, как это было в вашем случае. Можете ли вы подтвердить, что это также работает для вас и что вы можете успешно отправлять другие транзакции после входа в систему?

person some    schedule 15.12.2017
comment
В этом скрипте все еще есть ошибка — python интерпретирует «u\0001» как строку из 6 символов. Вам нужно u перед строкой (u''), чтобы получить разделитель из одного символа, который требуется для FIX. Еще проще было бы преобразовать в шестнадцатеричное обозначение '\ x01', которое работает в контекстах, отличных от Unicode, - в этот момент я думаю, что ни один из вызовов кодирования ascii/utf-8 не потребуется. - person JHumphrey; 16.01.2018
comment
Спасибо, что заметили это. Я забыл упомянуть, что тестировал свой код на Python 3, где строки по умолчанию имеют юникод. И поскольку они по умолчанию юникодные, я думаю, всегда нужно кодировать/декодировать при отправке/получении строк через сокеты. В Python 2, возможно, это и не нужно, но я не проверял. - person some; 17.01.2018
comment
Ах, спасибо за комментарий. Я на Python 2, так что в этом и есть разница. - person JHumphrey; 17.01.2018
comment
Как это вообще связано с GDAX? Сокет подключен только к локальному хосту. - person Arda Arslan; 08.02.2018

Ничего нового, чтобы добавить, просто хотел перефразировать вышеприведенное решение более функционально, без туннелирования:

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))
person Jonathan    schedule 08.05.2018
comment
Вам не хватает этой строки для создания контекста: context = ssl.create_default_context() - person John Shahbazian; 05.12.2020