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)