Examples

  1#!/bin/env python
  2"""
  3Examples illustrating the usage of FixMessage and associated objects
  4"""
  5
  6from __future__ import print_function
  7
  8import os
  9import sys
 10import decimal
 11import argparse
 12from copy import copy
 13from random import randint
 14
 15sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
 16
 17from pyfixmsg import RepeatingGroup
 18from pyfixmsg.fixmessage import FixMessage, FixFragment
 19from pyfixmsg.reference import FixSpec, FixTag
 20from pyfixmsg.codecs.stringfix import Codec
 21
 22if sys.version_info.major >= 3:
 23    unicode = str  # pylint: disable=C0103,W0622
 24
 25
 26def main(spec_filename):
 27    """
 28    Illustration of the usage of FixMessage.
 29
 30    :param spec_filename: a specification file from Quickfix.org
 31    :type spec_filename: ``str``
 32    """
 33
 34    # For this example you need FIX 4.2 specification
 35    # refer to: path_to_quickfix/FIX42.xml (MS: fsf/quickfix/1.14.3.1ms)
 36    spec = FixSpec(spec_filename)
 37    codec = Codec(spec=spec,  # The codec will use the given spec to find repeating groups
 38                  fragment_class=FixFragment)  # The codec will produce FixFragment objects inside repeating groups
 39
 40    # (default is dict). This is required for the find_all() and anywhere()
 41    # methods to work. It would fail with AttributeError otherwise.
 42
 43    def fixmsg(*args, **kwargs):
 44        """
 45        Factory function. This allows us to keep the dictionary __init__
 46        arguments unchanged and force the codec to our given spec and avoid
 47        passing codec to serialisation and parsing methods.
 48
 49        The codec defaults to a reasonable parser but without repeating groups.
 50
 51        An alternative method is to use the ``to_wire`` and ``from_wire`` methods
 52        to serialise and parse messages and pass the codec explicitly.
 53        """
 54        returned = FixMessage(*args, **kwargs)
 55        returned.codec = codec
 56        return returned
 57
 58    ########################
 59    #    Vanilla tag/value
 60    #########################
 61    data = (b'8=FIX.4.2|9=97|35=6|49=ABC|56=CAB|34=14|52=20100204-09:18:42|'
 62            b'23=115685|28=N|55=BLAH|54=2|44=2200.75|27=S|25=H|10=248|')
 63    msg = fixmsg().load_fix(data, separator='|')
 64
 65    # The message object is a dictionary, with integer keys
 66    # and string values. No validation is performed.
 67    # The spec contains the information, however so it can be retrieved.
 68    print("Message Type {} ({})".format(msg[35],
 69                                        spec.msg_types[msg[35]].name))
 70    print("Price {} (note type: {}, spec defined type {})".format(
 71        msg[44], type(msg[44]), spec.tags.by_tag(44).type
 72    ))
 73
 74    check_tags = (55, 44, 27)
 75    for element, required in spec.msg_types[msg[35]].composition:
 76        if isinstance(element, FixTag) and element.tag in check_tags:
 77            if required:
 78                print("{} is required on this message type".format(element.name))
 79            else:
 80                print("{} is not required on this message type".format(element.name))
 81    print("Spec also allows looking up enums: {}  is {}".format(msg[54],
 82                                                                spec.tags.by_tag(54).enum_by_value(msg[54])))
 83
 84    # Although the values are stored as string there are rich operators provided that
 85    # allow to somewhat abstract the types
 86    print("exact comparison with decimal:", msg.tag_exact(44, decimal.Decimal("2200.75")))
 87    print("exact comparing with int:", msg.tag_exact(54, 2))
 88    print("lower than with float:", msg.tag_lt(44, 2500.0))
 89    print("greater than with float:", msg.tag_gt(23, 110000.1))
 90    print("contains, case sensitive and insensitive:", msg.tag_contains(55, "MI"), msg.tag_icontains(55, "blah"))
 91
 92    # Tags manipulation is as for a dictionary
 93    msg[56] = "ABC.1"  # There is no enforcement of what tags are used for, so changing 56 is no worry for the lib
 94    msg.update({55: 'ABC123.1', 28: 'M'})
 95
 96    # note regex matching
 97    print("tag 56 changed", msg.tag_match_regex(56, r"..M\.N"))
 98    print("Tag 55 and 28 changed to {} and {}".format(msg[55], msg[28]))
 99
100    # There are additional tools for updating the messages' content though
101    none_or_one = randint(0, 1) or None
102    msg.set_or_delete(27, none_or_one)
103    msg.apply({25: None, 26: 2})
104
105    if none_or_one is None:
106        print("Got None, the tag is deleted")
107        assert 27 not in msg
108    else:
109        print("Got None, the tag is maintained")
110        assert msg[27] == 1
111
112    assert 25 not in msg
113    assert msg.tag_exact(26, '2')
114
115    ########################
116    #    copying messages
117    #########################
118
119    # Because messages maintain a reference to the codec and the spec
120    # a deepcopy() of messages is extremely inefficient. It is a lot faster
121    # to serialise-deserialise to copy a message, which is what copy() does.
122    # Alternatively do a shallow copy through dict constructor.
123    new_msg = msg.copy()
124    assert new_msg is not msg
125    msg.set_len_and_chksum()
126    # Note that this reverts the type of values that were set manually
127    assert new_msg[26] != msg[26]
128    print("tag 26 before {}, after {}".format(type(msg[26]), type(new_msg[26])))
129    assert all(new_msg.tag_exact(t, msg[t]) for t in msg)
130
131    msg = fixmsg().load_fix(data, separator='|')
132
133    # if no types have changed, the copy() method returns a message that is identical
134    # to the original one
135    new_msg = copy(msg)
136    assert new_msg == msg
137
138    # note you can also use the method copy()
139    new_msg = msg.copy()
140    assert new_msg == msg
141    assert new_msg is not msg
142
143    # and codec is not copied
144    assert new_msg.codec is msg.codec
145
146    # note that msg equality is not true when using dict constructor
147    new_msg = FixMessage(msg)
148    assert new_msg != msg
149    # That's because codec, time, recipient and direction are not preserved
150    # when using this technique, only tags are
151    assert dict(new_msg) == dict(msg)
152
153    ########################
154    #    Repeating Groups
155    #########################
156
157    # Repeating groups are indexed by count tag (268 below)
158    # and stored as a list of FixMessages (technically FixFragments, but close enough)
159    data = (b'8=FIX.4.2|9=196|35=X|49=A|56=B|34=12|52=20100318-03:21:11.364'
160            b'|262=A|268=2|279=0|269=0|278=BID|55=EUR/USD|270=1.37215'
161            b'|15=EUR|271=2500000|346=1|279=0|269=1|278=OFFER|55=EUR/USD'
162            b'|270=1.37224|15=EUR|271=2503200|346=1|10=171|')
163    msg = fixmsg().load_fix(data, separator='|')
164    print("Message Type {} ({})".format(msg[35],
165                                        spec.msg_types[msg[35]].name))
166    print("Repeating group {} looks like {}".format(spec.tags.by_tag(268).name,
167                                                    msg[268]))
168    print("Accessing repeating groups at depth: tag 278 "
169          "in the second member of the group is '{}'".format(msg[268][1][278]))
170
171    # finding out if a tag is present can be done at depth
172    print("Utility functions like anywhere() allow you to find out "
173          "if a tag is present at depth, or find the path to it: {} is present : {}; "
174          "it can be found at the following paths {}".format(278, msg.anywhere(278),
175                                                             list(msg.find_all(278))))
176
177    ########################
178    # Customise the spec
179    #########################
180    # Sometimes it may be desirable to tweak the spec a bit, especially add a custom tag
181    # or a custom repeating group.
182
183    # Create tag
184    spec.tags.add_tag(10001, "MyTagName")
185    assert spec.tags.by_tag(10001).name == "MyTagName"
186    assert spec.tags.by_name("MyTagName").tag == 10001
187    # Change enum values
188    spec.tags.by_tag(54).add_enum_value(name="SELLFAST", value="SF")
189    assert spec.tags.by_tag(54).enum_by_value("SF") == "SELLFAST"
190
191    # Add repeating Group
192    # let's add repeating group 268 to msg type 'D'
193    # for illustration purposes, we'll create the group from scratch
194    data = (b'8=FIX.4.2|9=196|35=D|49=A|56=B|34=12|52=20100318-03:21:11.364'
195            b'|262=A|268=2|279=0|269=0|278=BID|55=EUR/USD|270=1.37215'
196            b'|15=EUR|271=2500000|346=1|279=0|269=1|278=OFFER|55=EUR/USD'
197            b'|270=1.37224|15=EUR|271=2503200|346=1|10=171|')
198    before = FixMessage()
199    before.codec = Codec(spec=spec)
200    before.load_fix(data, separator='|')
201
202    # The composition is a iterable of pairs (FixTag, bool), with the bool indicating whether
203    # the tag is required or not (although it's not enforced in the codec at this time
204    composition = [(spec.tags.by_tag(i), False) for i in (279, 269, 278, 55, 270, 15, 271, 346)]
205
206    # add_group takes a FixTag and the composition
207    spec.msg_types['D'].add_group(spec.tags.by_tag(268), composition)
208
209    after = FixMessage()
210    after.codec = Codec(spec=spec, fragment_class=FixFragment)
211    after.load_fix(data, separator='|')
212
213    assert isinstance(before[268], (str, unicode))  # 268 is not parsed as a repeating group
214    assert before[270] == '1.37224'  # 268 is not parsed as a repeating group, so 271 takes the second value
215
216    assert isinstance(after[268], RepeatingGroup)  # After the change, 268 becomes a repeating group
217    assert list(after.find_all(270)) == [[268, 0, 270], [268, 1, 270]]  # and both 270 can be found
218
219
220if __name__ == "__main__":
221    PARSER = argparse.ArgumentParser()
222    PARSER.add_argument("spec_xml")
223    main(PARSER.parse_args().spec_xml)