Here is an answer in Python. Depending on the user's need there are three solutions presented here.
- Marked-up XML
- List of fields (groups embedded).
- JSON-like output.
In the quickfix Python library, the FieldMap field and group key iterators are not exposed. So the approach is to first generate the XML and iterate over the tree. There is also no access to the getFieldType method of the DataDictionary, so the dictionary must be pre-processed to store the field types for conversion and handling of groups.
import quickfix as fix
import xml.etree.ElementTree as ET
from collections import OrderedDict
import json
string = "8=FIX.4.4\0019=247\00135=s\00134=5\00149=sender\00152=20060319-09:08:20.881\00156=target\00122=8\00140=2\00144=9\00148=ABC\00155=ABC\00160=20060319-09:08:19\001548=184214\001549=2\001550=0\001552=2\00154=1\001453=2\001448=8\001447=D\001452=4\001448=AAA35777\001447=D\001452=3\00138=9\00154=2\001453=2\001448=8\001447=D\001452=4\001448=aaa\001447=D\001452=3\00138=9\00110=056\001"
# Load data dictionary
data_dictionary_xml = "FIX44.xml"
data_dictionary = fix.DataDictionary(data_dictionary_xml)
fix.Message().InitializeXML(data_dictionary_xml)
# String as fix message according to dictionary
message = fix.Message(string, data_dictionary, True)
# Marked-up XML
xml = message.toXML()
print(xml)
def get_field_type_map(data_dictionary_xml):
"""Preprocess DataDictionary to get field types."""
field_type_map = {}
with open(data_dictionary_xml, "r") as f:
xml = f.read()
tree = ET.fromstring(xml)
fields = tree.find("fields")
for field in fields.iter("field"):
field_type_map[field.attrib["number"]] = field.attrib["type"]
return field_type_map
field_type_map = get_field_type_map(data_dictionary_xml)
INT_TYPES = ["INT", "LENGTH", "NUMINGROUP", "QTY", "SEQNUM"]
FLOAT_TYPES = ["FLOAT", "PERCENTAGE", "PRICE", "PRICEOFFSET"]
BOOL_TYPES = ["BOOLEAN"]
DATETIME_TYPES = ["LOCALMKTDATE", "MONTHYEAR", "UTCDATEONLY", "UTCTIMEONLY", "UTCTIMESTAMP"]
STRING_TYPES = ["AMT", "CHAR", "COUNTRY", "CURRENCY", "DATA", "EXCHANGE", "MULTIPLEVALUESTRING", "STRING"]
def field_map_to_list(field_map, field_type_map):
fields = []
field_iter = iter([el for el in field_map if el.tag == "field"])
group_iter = iter([el for el in field_map if el.tag == "group"])
for field in field_iter:
# Extract raw value
raw = field.text
# Type the raw value
field_type = field_type_map.get(field.attrib["number"])
if field_type in INT_TYPES:
value = int(raw)
elif field_type in FLOAT_TYPES:
value = float(raw)
elif field_type in BOOL_TYPES:
value = bool(int(raw))
elif field_type in DATETIME_TYPES:
value = str(raw)
elif field_type in STRING_TYPES:
value = str(raw)
else:
value = str(raw)
# field.attrib should contain "name", "number", "enum"
_field = {
**field.attrib,
"type": field_type,
"raw": raw,
"value": value,
}
# If NUMINGROUP type then iterate groups the number indicated
# This assumes groups are in the same order as their field keys
if field_type == "NUMINGROUP":
groups = []
for _ in range(value):
group = next(group_iter)
# Parse group as field map
group_fields = field_map_to_list(group, field_type_map)
groups.append(group_fields)
_field["groups"] = groups
fields.append(_field)
return fields
def field_map_to_dict(field_map, field_type_map):
fields = OrderedDict()
field_iter = iter([el for el in field_map if el.tag == "field"])
group_iter = iter([el for el in field_map if el.tag == "group"])
for field in field_iter:
# Define key
# field.attrib should contain "name", "number", "enum"
key = field.attrib.get("name") or field.attrib.get("number")
# Extract raw value
raw = field.text
# Type the raw value
field_type = field_type_map.get(field.attrib["number"])
if field_type in INT_TYPES:
value = int(raw)
elif field_type in FLOAT_TYPES:
value = float(raw)
elif field_type in BOOL_TYPES:
value = bool(int(raw))
elif field_type in DATETIME_TYPES:
value = str(raw)
elif field_type in STRING_TYPES:
value = str(raw)
else:
value = str(raw)
# If NUMINGROUP type then iterate groups the number indicated
# This assumes groups are in the same order as their field keys
if field_type == "NUMINGROUP":
groups = []
for _ in range(value):
group = next(group_iter)
# Parse group as field map
group_fields = field_map_to_dict(group, field_type_map)
groups.append(group_fields)
fields[key] = groups
else:
# Preference enum above value
fields[key] = field.attrib.get("enum") or value
return fields
def parse_message_xml(xml, field_type_map, as_dict=False):
parsed = OrderedDict()
tree = ET.fromstring(xml)
for field_map in tree:
if not as_dict:
parsed[field_map.tag] = field_map_to_list(field_map, field_type_map)
else:
parsed[field_map.tag] = field_map_to_dict(field_map, field_type_map)
return parsed
# List of fields (groups embedded)
parsed = parse_message_xml(xml, field_type_map, as_dict=False)
print(json.dumps(parsed, indent=True))
# JSON-like output
parsed = parse_message_xml(xml, field_type_map, as_dict=True)
print(json.dumps(parsed, indent=True))
Outputs:
<message>
<header>
<field name="BeginString" number="8"><![CDATA[FIX.4.4]]></field>
<field name="BodyLength" number="9"><![CDATA[247]]></field>
<field name="MsgType" number="35" enum="NewOrderCross"><![CDATA[s]]></field>
<field name="MsgSeqNum" number="34"><![CDATA[5]]></field>
<field name="SenderCompID" number="49"><![CDATA[sender]]></field>
<field name="SendingTime" number="52"><![CDATA[20060319-09:08:20.881]]></field>
<field name="TargetCompID" number="56"><![CDATA[target]]></field>
</header>
<body>
<field name="SecurityIDSource" number="22" enum="EXCHANGE_SYMBOL"><![CDATA[8]]></field>
<field name="OrdType" number="40" enum="LIMIT"><![CDATA[2]]></field>
<field name="Price" number="44"><![CDATA[9]]></field>
<field name="SecurityID" number="48"><![CDATA[ABC]]></field>
<field name="Symbol" number="55"><![CDATA[ABC]]></field>
<field name="TransactTime" number="60"><![CDATA[20060319-09:08:19]]></field>
<field name="CrossID" number="548"><![CDATA[184214]]></field>
<field name="CrossType" number="549" enum="CROSS_TRADE_WHICH_IS_EXECUTED_PARTIALLY_AND_THE_REST_IS_CANCELLED_ONE_SIDE_IS_FULLY_EXECUTED_THE_OTHER_SIDE_IS_PARTIALLY_EXECUTED_WITH_THE_REMAINDER_BEING_CANCELLED_THIS_IS_EQUIVALENT_TO_AN_IMMEDIATE_OR_CANCEL_ON_THE_OTHER_SIDE_NOTE_THE_CROSSPRIORITZATION"><![CDATA[2]]></field>
<field name="CrossPrioritization" number="550" enum="NONE"><![CDATA[0]]></field>
<field name="NoSides" number="552" enum="BOTH_SIDES"><![CDATA[2]]></field>
<group>
<field name="Side" number="54" enum="BUY"><![CDATA[1]]></field>
<field name="NoPartyIDs" number="453"><![CDATA[2]]></field>
<field name="OrderQty" number="38"><![CDATA[9]]></field>
<group>
<field name="PartyID" number="448"><![CDATA[8]]></field>
<field name="PartyIDSource" number="447" enum="PROPRIETARY_CUSTOM_CODE"><![CDATA[D]]></field>
<field name="PartyRole" number="452" enum="CLEARING_FIRM"><![CDATA[4]]></field>
</group>
<group>
<field name="PartyID" number="448"><![CDATA[AAA35777]]></field>
<field name="PartyIDSource" number="447" enum="PROPRIETARY_CUSTOM_CODE"><![CDATA[D]]></field>
<field name="PartyRole" number="452" enum="CLIENT_ID"><![CDATA[3]]></field>
</group>
</group>
<group>
<field name="Side" number="54" enum="SELL"><![CDATA[2]]></field>
<field name="NoPartyIDs" number="453"><![CDATA[2]]></field>
<field name="OrderQty" number="38"><![CDATA[9]]></field>
<group>
<field name="PartyID" number="448"><![CDATA[8]]></field>
<field name="PartyIDSource" number="447" enum="PROPRIETARY_CUSTOM_CODE"><![CDATA[D]]></field>
<field name="PartyRole" number="452" enum="CLEARING_FIRM"><![CDATA[4]]></field>
</group>
<group>
<field name="PartyID" number="448"><![CDATA[aaa]]></field>
<field name="PartyIDSource" number="447" enum="PROPRIETARY_CUSTOM_CODE"><![CDATA[D]]></field>
<field name="PartyRole" number="452" enum="CLIENT_ID"><![CDATA[3]]></field>
</group>
</group>
</body>
<trailer>
<field name="CheckSum" number="10"><![CDATA[056]]></field>
</trailer>
</message>
{
"header": [
{
"number": "8",
"name": "BeginString",
"value": "FIX.4.4",
"raw": "FIX.4.4",
"type": "STRING"
},
{
"number": "9",
"name": "BodyLength",
"value": 247,
"raw": "247",
"type": "LENGTH"
},
{
"number": "35",
"name": "MsgType",
"enum": "NewOrderCross",
"value": "s",
"raw": "s",
"type": "STRING"
},
{
"number": "34",
"name": "MsgSeqNum",
"value": 5,
"raw": "5",
"type": "SEQNUM"
},
{
"number": "49",
"name": "SenderCompID",
"value": "sender",
"raw": "sender",
"type": "STRING"
},
{
"number": "52",
"name": "SendingTime",
"value": "20060319-09:08:20.881",
"raw": "20060319-09:08:20.881",
"type": "UTCTIMESTAMP"
},
{
"number": "56",
"name": "TargetCompID",
"value": "target",
"raw": "target",
"type": "STRING"
}
],
"body": [
{
"number": "22",
"name": "SecurityIDSource",
"enum": "EXCHANGE_SYMBOL",
"value": "8",
"raw": "8",
"type": "STRING"
},
{
"number": "40",
"name": "OrdType",
"enum": "LIMIT",
"value": "2",
"raw": "2",
"type": "CHAR"
},
{
"number": "44",
"name": "Price",
"value": 9.0,
"raw": "9",
"type": "PRICE"
},
{
"number": "48",
"name": "SecurityID",
"value": "ABC",
"raw": "ABC",
"type": "STRING"
},
{
"number": "55",
"name": "Symbol",
"value": "ABC",
"raw": "ABC",
"type": "STRING"
},
{
"number": "60",
"name": "TransactTime",
"value": "20060319-09:08:19",
"raw": "20060319-09:08:19",
"type": "UTCTIMESTAMP"
},
{
"number": "548",
"name": "CrossID",
"value": "184214",
"raw": "184214",
"type": "STRING"
},
{
"number": "549",
"name": "CrossType",
"enum": "CROSS_TRADE_WHICH_IS_EXECUTED_PARTIALLY_AND_THE_REST_IS_CANCELLED_ONE_SIDE_IS_FULLY_EXECUTED_THE_OTHER_SIDE_IS_PARTIALLY_EXECUTED_WITH_THE_REMAINDER_BEING_CANCELLED_THIS_IS_EQUIVALENT_TO_AN_IMMEDIATE_OR_CANCEL_ON_THE_OTHER_SIDE_NOTE_THE_CROSSPRIORITZATION",
"value": 2,
"raw": "2",
"type": "INT"
},
{
"number": "550",
"name": "CrossPrioritization",
"enum": "NONE",
"value": 0,
"raw": "0",
"type": "INT"
},
{
"number": "552",
"name": "NoSides",
"enum": "BOTH_SIDES",
"value": 2,
"raw": "2",
"type": "NUMINGROUP",
"groups": [
[
{
"number": "54",
"name": "Side",
"enum": "BUY",
"value": "1",
"raw": "1",
"type": "CHAR"
},
{
"number": "453",
"name": "NoPartyIDs",
"groups": [
[
{
"number": "448",
"name": "PartyID",
"value": "8",
"raw": "8",
"type": "STRING"
},
{
"number": "447",
"name": "PartyIDSource",
"enum": "PROPRIETARY_CUSTOM_CODE",
"value": "D",
"raw": "D",
"type": "CHAR"
},
{
"number": "452",
"name": "PartyRole",
"enum": "CLEARING_FIRM",
"value": 4,
"raw": "4",
"type": "INT"
}
],
[
{
"number": "448",
"name": "PartyID",
"value": "AAA35777",
"raw": "AAA35777",
"type": "STRING"
},
{
"number": "447",
"name": "PartyIDSource",
"enum": "PROPRIETARY_CUSTOM_CODE",
"value": "D",
"raw": "D",
"type": "CHAR"
},
{
"number": "452",
"name": "PartyRole",
"enum": "CLIENT_ID",
"value": 3,
"raw": "3",
"type": "INT"
}
]
],
"value": 2,
"raw": "2",
"type": "NUMINGROUP"
},
{
"number": "38",
"name": "OrderQty",
"value": 9,
"raw": "9",
"type": "QTY"
}
],
[
{
"number": "54",
"name": "Side",
"enum": "SELL",
"value": "2",
"raw": "2",
"type": "CHAR"
},
{
"number": "453",
"name": "NoPartyIDs",
"groups": [
[
{
"number": "448",
"name": "PartyID",
"value": "8",
"raw": "8",
"type": "STRING"
},
{
"number": "447",
"name": "PartyIDSource",
"enum": "PROPRIETARY_CUSTOM_CODE",
"value": "D",
"raw": "D",
"type": "CHAR"
},
{
"number": "452",
"name": "PartyRole",
"enum": "CLEARING_FIRM",
"value": 4,
"raw": "4",
"type": "INT"
}
],
[
{
"number": "448",
"name": "PartyID",
"value": "aaa",
"raw": "aaa",
"type": "STRING"
},
{
"number": "447",
"name": "PartyIDSource",
"enum": "PROPRIETARY_CUSTOM_CODE",
"value": "D",
"raw": "D",
"type": "CHAR"
},
{
"number": "452",
"name": "PartyRole",
"enum": "CLIENT_ID",
"value": 3,
"raw": "3",
"type": "INT"
}
]
],
"value": 2,
"raw": "2",
"type": "NUMINGROUP"
},
{
"number": "38",
"name": "OrderQty",
"value": 9,
"raw": "9",
"type": "QTY"
}
]
]
}
],
"trailer": [
{
"number": "10",
"name": "CheckSum",
"value": "056",
"raw": "056",
"type": "STRING"
}
]
}
{
"header": {
"BeginString": "FIX.4.4",
"BodyLength": 247,
"MsgType": "NewOrderCross",
"MsgSeqNum": 5,
"SenderCompID": "sender",
"SendingTime": "20060319-09:08:20.881",
"TargetCompID": "target"
},
"body": {
"SecurityIDSource": "EXCHANGE_SYMBOL",
"OrdType": "LIMIT",
"Price": 9.0,
"SecurityID": "ABC",
"Symbol": "ABC",
"TransactTime": "20060319-09:08:19",
"CrossID": "184214",
"CrossType": "CROSS_TRADE_WHICH_IS_EXECUTED_PARTIALLY_AND_THE_REST_IS_CANCELLED_ONE_SIDE_IS_FULLY_EXECUTED_THE_OTHER_SIDE_IS_PARTIALLY_EXECUTED_WITH_THE_REMAINDER_BEING_CANCELLED_THIS_IS_EQUIVALENT_TO_AN_IMMEDIATE_OR_CANCEL_ON_THE_OTHER_SIDE_NOTE_THE_CROSSPRIORITZATION",
"CrossPrioritization": "NONE",
"NoSides": [
{
"Side": "BUY",
"NoPartyIDs": [
{
"PartyID": "8",
"PartyIDSource": "PROPRIETARY_CUSTOM_CODE",
"PartyRole": "CLEARING_FIRM"
},
{
"PartyID": "AAA35777",
"PartyIDSource": "PROPRIETARY_CUSTOM_CODE",
"PartyRole": "CLIENT_ID"
}
],
"OrderQty": 9
},
{
"Side": "SELL",
"NoPartyIDs": [
{
"PartyID": "8",
"PartyIDSource": "PROPRIETARY_CUSTOM_CODE",
"PartyRole": "CLEARING_FIRM"
},
{
"PartyID": "aaa",
"PartyIDSource": "PROPRIETARY_CUSTOM_CODE",
"PartyRole": "CLIENT_ID"
}
],
"OrderQty": 9
}
]
},
"trailer": {
"CheckSum": "056"
}
}