After reviewing the available Python FIX implementations I decided to create my own, both as a learning exercise and to fill some functional gaps. In particular I wanted Python-friendly code generation from any FIX specification from 4.0 to 5.0SP2 with a rich metadata model so later on I can build tools like a FIX schema validator if I want to. Pelican FIX 0.0.3, released today, covers all those requirements -- with more to come.
Parsing QuickFIX XML
The QuickFIX project contains XML-format specifications for FIX 4.0 through 5.0SP2. These XML files contain metadata describing FIX fields and (where applicable) their enumerated values; FIX messages; components (reusable collections of fields); repeating groups; and FIX headers & trailers. The format allows for forward references and is fully recursive, so you can define a message as a collection of components which contain other components and repeating groups which reference other groups.
For this reason I chose to write a parser that runs two passes over the metadata: once to get the fundamental definitions and a second pass to "link"references to fields, repeating groups, etc.. You can use the very nice backport of importlib_resources from Python 3.9 to load directly from the Pelican module path:
from importlib_resources import files, as_file
source = files('pelicanfix.dictionary').joinpath('FIX44.xml')
with as_file(source) as dict_file:
parser = FIXDictionaryParser(dict_file)
fix_dict = parser.parse()
and from here you can get all fields; get all messages; or get individual ones by name:
msg = fix_dict.get_message('NewOrderSingle')
msg_type = msg.get_msg_type()
elements = msg.get_elements()
Elements are of type FIXElement
which includes as concrete implementations:
- FIXFieldRef
- FIXComponentRef
- FIXRepeatingGroup
You are handed back references after the resolve() phase, which links up actual values, so you can do things like this (basic) pretty printer:
from importlib_resources import files, as_file
from pelicanfix.dictionary.metadata import FIXFieldRef, FIXComponentRef, FIXRepeatingGroup
from pelicanfix.dictionary.parser import FIXDictionaryParser
def print_fields(elements, prefix=''):
for element in elements:
if isinstance(element, FIXFieldRef):
print(prefix + element.get_field().get_name())
elif isinstance(element, FIXComponentRef):
print_fields(element.get_component().get_elements(), prefix)
elif isinstance(element, FIXRepeatingGroup):
print(f'+ {element.get_name()}')
print_fields(element.get_elements(), prefix=' ')
def main():
source = files('pelicanfix.dictionary').joinpath('FIX44.xml')
with as_file(source) as dict_file:
parser = FIXDictionaryParser(dict_file)
fix_dict = parser.parse()
header = fix_dict.get_header()
msg = fix_dict.get_message('NewOrderSingle')
trailer = fix_dict.get_trailer()
print_fields(header.get_elements())
print('------------')
print_fields(msg.get_elements())
print('------------')
print_fields(trailer.get_elements())
to unpack all the fields relevant to a particular messsage type, including header & trailer. Note for simplicity I haven't handled multiple levels of indentation to show the relationship of groups to parents, it's flat, but the recursive structure is all there.
Generating code
Now that we have a dictionary describing fields and messages, the next thing we'll want to do is generate some proper Python enums for all the known values, for all specifications. This is done with pelicanfix.codegen
which takes the following arguments:
python3 pelicanfix.codegen --to-dir=src/ \
--specification FIX44.xml \
--base_module=pelicanfix.protocol.fix44 \
--protocol=FIX44
The generated code can then be used directly when constructing a FIX message with simplefix -- both the field numbers and values of enumerations like Side.BUY
:
import random
import uuid
import simplefix
from pelicanfix.protocol.fix44 import Field, MsgType, Side, HandlInst, SecurityIDSource
def main():
msg = simplefix.FixMessage()
msg.append_pair(Field.BEGIN_STRING.value.get_number(), 'FIX.4.4')
msg.append_pair(Field.MSG_TYPE.value.get_number(), MsgType.ORDER_SINGLE.value)
msg.append_pair(Field.PRICE.value.get_number(), "%0.2f" % (random.random() * 2 + 10))
msg.append_pair(Field.ORDER_QTY.value.get_number(), int(random.random() * 100))
msg.append_pair(Field.SYMBOL.value.get_number(), "VOD.L")
msg.append_pair(Field.SECURITY_ID.value.get_number(), "GB00BH4HKS39")
msg.append_pair(Field.SECURITY_ID_SOURCE.value.get_number(), SecurityIDSource.ISIN_NUMBER.value)
msg.append_pair(Field.ACCOUNT.value.get_number(), "TEST")
msg.append_pair(Field.HANDL_INST.value.get_number(),
HandlInst.AUTOMATED_EXECUTION_ORDER_PRIVATE_NO_BROKER_INTERVENTION.value)
msg.append_pair(Field.EX_DESTINATION.value.get_number(), "XLON")
msg.append_pair(Field.SIDE.value.get_number(), Side.BUY.value)
msg.append_pair(Field.CL_ORD_ID.value.get_number(), str(uuid.uuid1()))
msg.append_pair(Field.CURRENCY.value.get_number(), "GBP")
print(str(msg.encode()).replace('\\x01', '|'))
This yields something like this -- note the BodyLength(9) and CheckSum(10) fields in header and trailer have been added automatically by simplefix's encoder:
b'8=FIX.4.4|9=123|35=D|44=12.00|38=67|55=VOD.L|48=GB00BH4HKS39|22=4|1=TEST|21=1|100=XLON|54=1|11=d57a795c-9df0-11ea-92c1-acde48001122|15=GBP|10=051|'
Next steps
The next step on Pelican will be to start building an asyncio-based TCP connection layer, so given the above plus simplefix we will have a complete FIX engine in Python!