logstash-filter-fix_protocol 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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