logstash-filter-fix_protocol 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,212 @@
1
+ require 'spec_helper'
2
+
3
+ describe LF::FixMessage do
4
+ let(:message_str) { "8=FIXT.1.1\x0135=8\x0149=ITG\x0156=SILO\x01315=8\x016=100.25\x01410=50.25\x01424=23.45\x01411=Y\x0143=N\x0140=1\x015=N\x01" }
5
+ let(:another_str) { "8=FIXT.1.1\x0135=B\x0149=ITG\x0156=SILO\x01148=Market Bulls Have Short Sellers on the Run\x0133=2\x0158=The bears have been cowed by the bulls.\x0158=Buy buy buy\x01354=0\x0143=N\x0140=1\x015=N\x01" }
6
+
7
+ let(:data_dictionary) { LF::DataDictionary.new(load_fixture("FIX50SP1.xml")) }
8
+ let(:session_dictionary) { LF::DataDictionary.new(load_fixture("FIXT11.xml")) }
9
+ let(:message) { LF::FixMessage.new(message_str, data_dictionary, session_dictionary) }
10
+ let(:message2) { LF::FixMessage.new(another_str, data_dictionary, session_dictionary) }
11
+
12
+ describe '#to_hash' do
13
+ it 'converts the FIX message string to a hash in human readable format' do
14
+ expect(message.to_hash).to eq({
15
+ "BeginString"=>"FIXT.1.1",
16
+ "MsgType"=>"ExecutionReport",
17
+ "PossDupFlag"=>false,
18
+ "SenderCompID"=>"ITG",
19
+ "TargetCompID"=>"SILO",
20
+ "AdvTransType"=>"NEW",
21
+ "AvgPx"=>100.25,
22
+ "OrdType"=>"MARKET",
23
+ "UnderlyingPutOrCall"=>8,
24
+ "WtAverageLiquidity"=>"50.25",
25
+ "ExchangeForPhysical"=>true,
26
+ "DayOrderQty"=>23.45
27
+ })
28
+
29
+ expect(message2.to_hash).to eq({
30
+ "BeginString"=>"FIXT.1.1",
31
+ "MsgType"=>"News",
32
+ "PossDupFlag"=>false,
33
+ "SenderCompID"=>"ITG",
34
+ "TargetCompID"=>"SILO",
35
+ "AdvTransType"=>"NEW",
36
+ "NoLinesOfText"=>[
37
+ {
38
+ "Text"=>"The bears have been cowed by the bulls."
39
+ }, {
40
+ "Text"=>"Buy buy buy", "EncodedTextLen"=>"0"
41
+ }
42
+ ],
43
+ "OrdType"=>"MARKET",
44
+ "Headline"=>"Market Bulls Have Short Sellers on the Run"
45
+ })
46
+ end
47
+ end
48
+
49
+ def should_parse_fix_messages(file_path, dictionary = "FIX42.xml", session_dictionary = nil)
50
+ data_dictionary = LF::DataDictionary.new(load_fixture(dictionary))
51
+ session_dictionary = session_dictionary.present? ? LF::DataDictionary.new(load_fixture(session_dictionary)) : data_dictionary
52
+
53
+ File.open(load_fixture(file_path), encoding: 'UTF-8') do |file|
54
+ file.each_entry do |line|
55
+ line.chomp! # remove new line character
56
+ message = LF::FixMessage.new(line, data_dictionary, session_dictionary)
57
+ yield(message.to_hash)
58
+ end
59
+ end
60
+ end
61
+
62
+ context 'message types' do
63
+ let(:fix_4) { {data_dictionary: "FIX42.xml", session_dictionary: nil} }
64
+ let(:fix_5) { {data_dictionary: "FIX50SP1.xml", session_dictionary: "FIXT11.xml"} }
65
+ # data is from: http://fixparser.targetcompid.com/
66
+ context 'heartbeats' do
67
+ it 'can parse these' do
68
+ [fix_4, fix_5].each do |version|
69
+ should_parse_fix_messages('message_types/heartbeat.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
70
+ expect(["Heartbeat", "HEARTBEAT"].include?(hash["MsgType"])).to be true
71
+
72
+ expect(hash["BeginString"]).to be_a String
73
+ expect(hash["SendingTime"]).to be_a String
74
+ expect(hash["CheckSum"]).to be_a String
75
+ expect([String, Fixnum].include?(hash["BodyLength"].class)).to be true
76
+ expect([String, Fixnum].include?(hash["MsgSeqNum"].class)).to be true
77
+ expect(["BANZAI", "EXEC"].include?(hash["TargetCompID"])).to be true
78
+ expect(["BANZAI", "EXEC"].include?(hash["SenderCompID"])).to be true
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'logon' do
85
+ it 'can parse these' do
86
+ [fix_4, fix_5].each do |version|
87
+ should_parse_fix_messages('message_types/logon.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
88
+ expect(["Logon", "LOGON"].include?(hash["MsgType"])).to be true
89
+
90
+ expect(hash["BeginString"]).to be_a String
91
+ # NOTE: This field was changed between FIX 4 / 5
92
+ # expect([String, Fixnum].include?(hash["HeartBtInt"].class)).to be true
93
+
94
+ expect(["BANZAI", "EXEC"].include?(hash["TargetCompID"])).to be true
95
+ expect(["BANZAI", "EXEC"].include?(hash["SenderCompID"])).to be true
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'execution_report' do
102
+ it 'can parse these' do
103
+ [fix_4, fix_5].each do |version|
104
+ should_parse_fix_messages('message_types/execution_report.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
105
+ expect(["ExecutionReport"].include?(hash["MsgType"])).to be true
106
+
107
+ expect(hash["ClOrdID"]).to be_a String
108
+ expect(hash["Symbol"]).to be_a String
109
+
110
+ expect(["NEW", "FILLED"].include?(hash["OrdStatus"])).to be true
111
+ expect(["BUY", "SELL"].include?(hash["Side"])).to be true
112
+
113
+ expect([String, Float].include?(hash["LastPx"].class)).to be true
114
+ # NOTE: This field was changed between FIX 4 / 5
115
+ # expect([String, Float].include?(hash["LastShares"].class)).to be true
116
+ expect([String, Float].include?(hash["OrderQty"].class)).to be true
117
+
118
+ expect(hash["TargetSubID"]).to be_a(String) if hash["TargetSubID"].present?
119
+
120
+ expect(["BANZAI", "EXEC", "ANOTHER_INC"].include?(hash["TargetCompID"])).to be true
121
+ expect(["BANZAI", "EXEC", "DUMMY_INC"].include?(hash["SenderCompID"])).to be true
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ context 'new order single' do
128
+ it 'can parse these' do
129
+ [fix_4, fix_5].each do |version|
130
+ should_parse_fix_messages('message_types/new_order_single.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
131
+ expect(hash["MsgType"]).to eq "NewOrderSingle"
132
+
133
+ expect(hash["ClOrdID"]).to be_a String
134
+ expect(hash["Symbol"]).to be_a String
135
+ expect(hash["TimeInForce"]).to be_a String
136
+ expect(hash["OrderQty"]).to be_a Float
137
+
138
+ expect(["MARKET", "LIMIT"].include?(hash["OrdType"])).to be true
139
+ expect(["BUY", "SELL"].include?(hash["Side"])).to be true
140
+
141
+ expect(["BANZAI", "EXEC", "DUMMY_INC"].include?(hash["TargetCompID"])).to be true
142
+ expect(["BANZAI", "EXEC", "ANOTHER_INC"].include?(hash["SenderCompID"])).to be true
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'order cancel request' do
149
+ it 'can parse these' do
150
+ [fix_4, fix_5].each do |version|
151
+ should_parse_fix_messages('message_types/order_cancel_request.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
152
+ expect(hash["MsgType"]).to eq "OrderCancelRequest"
153
+
154
+ expect(hash["ClOrdID"]).to be_a String
155
+ expect(hash["OrigClOrdID"]).to be_a String
156
+ expect(hash["Symbol"]).to be_a String
157
+
158
+ expect(hash["OrderQty"]).to be_a(Float) if hash["OrderQty"].present?
159
+ expect(["FUTURE"].include?(hash["SecurityType"])).to be(true) if hash["SecurityType"].present?
160
+
161
+ expect(["BUY", "SELL"].include?(hash["Side"])).to be true
162
+
163
+ expect(["BANZAI", "EXEC", "DUMMY_INC"].include?(hash["TargetCompID"])).to be true
164
+ expect(["BANZAI", "EXEC", "ANOTHER_INC"].include?(hash["SenderCompID"])).to be true
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ context 'rejects' do
171
+ it 'can parse these' do
172
+ [fix_4, fix_5].each do |version|
173
+ should_parse_fix_messages('message_types/reject.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
174
+ expect(["Reject", "REJECT"].include?(hash["MsgType"])).to be true
175
+ expect(hash["Text"]).to eq "Unsupported message type"
176
+
177
+ expect(["BANZAI", "EXEC"].include?(hash["TargetCompID"])).to be true
178
+ expect(["BANZAI", "EXEC"].include?(hash["SenderCompID"])).to be true
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ context 'market data snapshots' do
185
+ it 'can parse these' do
186
+ [fix_4, fix_5].each do |version|
187
+ should_parse_fix_messages('message_types/market_data_snapshot.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
188
+ expect(["MarketDataSnapshotFullRefresh", "REJECT"].include?(hash["MsgType"])).to be true
189
+
190
+ expect(hash["NoMDEntries"]).to be_a(Array)
191
+ expect(hash["NoMDEntries"].first).to be_a(Hash)
192
+ expect(hash["NoMDEntries"].first["MDEntryPx"]).to be_a(Float)
193
+ expect(hash["NoMDEntries"].first["MDEntrySize"]).to be_a(Float) if hash["NoMDEntries"].first["MDEntrySize"].present?
194
+
195
+ expect(["ANOTHER_INC"].include?(hash["TargetCompID"])).to be true
196
+ expect(["DUMMY_INC"].include?(hash["SenderCompID"])).to be true
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ context 'human-readable group enums' do
203
+ it 'can parse these' do
204
+ [fix_4, fix_5].each do |version|
205
+ should_parse_fix_messages('message_types/market_data_snapshot.txt', version[:data_dictionary], version[:session_dictionary]) do |hash|
206
+ expect(["BID", "OFFER", "INDEX_VALUE"].include?(hash["NoMDEntries"].first["MDEntryType"])).to be true
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe LF::FixProtocol do
4
+ let(:fix_5_config) do
5
+ {
6
+ "message" => "fix_message",
7
+ "session_dictionary_path" => load_fixture("FIXT11.xml"),
8
+ "data_dictionary_path" => load_fixture("FIX50SP1.xml")
9
+ }
10
+ end
11
+
12
+ let(:fix_4_config) do
13
+ {
14
+ "message" => "fix_message",
15
+ "data_dictionary_path" => load_fixture("FIX42.xml")
16
+ }
17
+ end
18
+
19
+ describe 'config' do
20
+ context 'fix 4 configuration' do
21
+ let(:filter) { LF::FixProtocol.new(fix_4_config) }
22
+
23
+ it 'reuses the data dictionary as the session dictionary' do
24
+ expect(filter.data_dictionary).to be_a(LF::DataDictionary)
25
+ expect(filter.session_dictionary == filter.data_dictionary).to be true
26
+ end
27
+ end
28
+
29
+ context 'fix 5 configuration' do
30
+ let(:filter) { LF::FixProtocol.new(fix_5_config) }
31
+
32
+ it 'instantiates a new data dictionary for a session dictionary' do
33
+ expect(filter.data_dictionary).to be_a(LF::DataDictionary)
34
+ expect(filter.session_dictionary == filter.data_dictionary).to be false
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'an incoming execution report' do
40
+ config fix_4_configuration
41
+
42
+ execution = "8=FIXT.1.1\x0135=8\x0149=ITG\x0156=SILO\x01315=8\x016=100.25\x01410=50.25\x01424=23.45\x01411=Y\x0143=N\x0140=1\x015=N\x01"
43
+
44
+ sample("fix_message" => execution) do
45
+ filtered_event = subject
46
+ insist { filtered_event["BeginString"] } == "FIXT.1.1"
47
+ insist { filtered_event["MsgType"] } == "ExecutionReport"
48
+ insist { filtered_event["SenderCompID"] } == "ITG"
49
+ insist { filtered_event["AvgPx"] } == 100.25
50
+ insist { filtered_event["OrdType"] } == "MARKET"
51
+ insist { filtered_event["UnderlyingPutOrCall"] } == 8
52
+ end
53
+ end
54
+
55
+ context 'it removes unparseable key-value pairs' do
56
+ config fix_5_configuration
57
+
58
+ execution = "8=FIX.4.2\x019=240\x0135=8\x0134=6\x0149=DUMMY_INC\x0152=20150826-23:10:17.744\x0156=ANOTHER_INC\x0157=Firm_B\x011=Inst_B\x016=0\x0111=151012569\x0117=ITRZ1201508261_24\x0120=0\x0122=8\x0131=1010\x0132=5\x0137=ITRZ1201508261_12\x0138=5\x0139=2\x0140=2\x0141=best_buy\x0144=1011\x0154=1\x0155=ITRZ1\x0160=20150826-23:10:15.547\x01150=2\x01151=0\x0110=227\x01"
59
+
60
+ sample("fix_message" => execution) do
61
+ expect { subject }.to output.to_stdout
62
+ filtered_event = subject
63
+ insist { filtered_event["BeginString"] } == "FIX.4.2"
64
+ insist { filtered_event["MsgType"] } == "ExecutionReport"
65
+ insist { filtered_event["SenderCompID"] } == "DUMMY_INC"
66
+ insist { filtered_event["AvgPx"] } == 0.0
67
+ insist { filtered_event["OrdType"] } == "LIMIT"
68
+ insist { filtered_event["LeavesQty"] } == 0.0 # this should fail if parsing gets rescued, but doesnt finish setting on the event object
69
+ end
70
+ end
71
+ end