fix-protocol 0.0.3

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b7794c49ae6633b51af2c60befb621831ecff826
4
+ data.tar.gz: 95ebd5ecd54b776d0f799c4aaf87e6e091ae4d88
5
+ SHA512:
6
+ metadata.gz: 373a1c89bda40efec6577efeb641870ef02f4e5e5444734ec745717821947aac54b659b508ae844a99f97fcafe510be229f69368f1e9bc0e4fbf3553112cb639
7
+ data.tar.gz: fb095eff99fac138baaa9c50f5d8c007b0ae6bba593d854de5ab996608bb83f93737fc4e985a090b801e4c9169c4e05c9e7a54b1f876156143670d85382f69da
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2
+ of this software and associated documentation files (the "Software"), to deal
3
+ in the Software without restriction, including without limitation the rights
4
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5
+ copies of the Software, and to permit persons to whom the Software is
6
+ furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in
9
+ all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
17
+ THE SOFTWARE.
@@ -0,0 +1,4 @@
1
+ FIX Protocol [![Build Status](https://secure.travis-ci.org/Paymium/fix-protocol.png?branch=master)](http://travis-ci.org/Paymium/fix-protocol)
2
+ =
3
+
4
+ This library aims to provide a set of useful tools to generate, parse and process messages FIX standard.
@@ -0,0 +1,75 @@
1
+ #require 'treetop'
2
+
3
+ require 'fix/protocol/version'
4
+ require 'fix/protocol/message'
5
+ require 'fix/protocol/message_class_mapping'
6
+ require 'fix/protocol/parse_failure'
7
+
8
+ #
9
+ # Main Fix namespace
10
+ #
11
+ module Fix
12
+
13
+ #
14
+ # Main protocol namespace
15
+ #
16
+ module Protocol
17
+
18
+ #
19
+ # Parses a string into a Fix::Protocol::Message instance
20
+ #
21
+ # @param str [String] A FIX message string
22
+ # @return [Fix::Protocol::Message] A +Fix::Protocol::Message+ instance, or a +Fix::Protocol::ParseFailure+ in case of failure
23
+ #
24
+ def self.parse(str)
25
+ errors = []
26
+ msg_type = str.match(/^8\=[^\x01]+\x019\=[^\x01]+\x0135\=([^\x01]+)\x01/)
27
+
28
+ unless str.match(/^8\=[^\x01]+\x019\=[^\x01]+\x0135\=[^\x01]+\x01.+10\=[^\x01]+\x01/)
29
+ FP::ParseFailure.new("Malformed message <#{str}>")
30
+ else
31
+
32
+ klass = MessageClassMapping.get(msg_type[1])
33
+
34
+ unless klass
35
+ errors << "Unknown message type <#{msg_type[1]}>"
36
+ end
37
+
38
+ # Check message length
39
+ length = str.gsub(/10\=[^\x01]+\x01$/, '').gsub(/^8\=[^\x01]+\x019\=([^\x01]+)\x01/, '').length
40
+ if length != $1.to_i
41
+ errors << "Incorrect body length"
42
+ end
43
+
44
+ # Check checksum
45
+ checksum = str.match(/10\=([^\x01]+)\x01/)[1]
46
+ if checksum != ('%03d' % (str.gsub(/10\=[^\x01]+\x01/, '').bytes.inject(&:+) % 256))
47
+ errors << "Incorrect checksum"
48
+ end
49
+
50
+ if errors.empty?
51
+ msg = klass.parse(str)
52
+
53
+ if msg.valid?
54
+ msg
55
+ else
56
+ FP::ParseFailure.new(msg.errors)
57
+ end
58
+ else
59
+ FP::ParseFailure.new(errors)
60
+ end
61
+ end
62
+ end
63
+
64
+ #
65
+ # Alias the +Fix::Protocol+ namespace to +FP+ if possible
66
+ #
67
+ def self.alias_namespace!
68
+ Object.const_set(:FP, Protocol) unless Object.const_defined?(:FP)
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ Fix::Protocol.alias_namespace!
75
+
@@ -0,0 +1,129 @@
1
+ require 'fix/protocol/type_conversions'
2
+
3
+ module Fix
4
+ module Protocol
5
+
6
+ #
7
+ # A FIX message field
8
+ #
9
+ class Field
10
+
11
+ include TypeConversions
12
+
13
+ @@attrs = [:tag, :name, :default, :type, :required, :parse_failure, :mapping]
14
+ @@attrs.each { |attr| attr_accessor(attr) }
15
+
16
+ def initialize(node)
17
+ @@attrs.each { |attr| node[attr] && send("#{attr}=", node[attr]) }
18
+ self.value ||= (default.is_a?(Proc) ? default.call(self) : default)
19
+ end
20
+
21
+ #
22
+ # Returns the field as a message fragment
23
+ #
24
+ # @return [String] A message fragment terminated by the separator byte
25
+ #
26
+ def dump
27
+ "#{tag}=#{@value}\x01"
28
+ end
29
+
30
+ #
31
+ # Parses the value for this field from the beginning of the string passed as parameter
32
+ # and return the rest of the string. The field value is assigned to the +@value+ instance variable
33
+ #
34
+ # @param str [String] A string starting with the field to parse
35
+ # @return [String] The same string with the field stripped off
36
+ #
37
+ def parse(str)
38
+ if str.match(/^#{tag}\=([^\x01]+)\x01/)
39
+ @value = $1
40
+ str.gsub(/^[^\x01]+\x01/, '')
41
+ elsif required
42
+ self.parse_failure = "Expected <#{str}> to start with a <#{tag}=...|> required field"
43
+ end
44
+ end
45
+
46
+ #
47
+ # Returns the type-casted value of the field, according to its type or mapping definition
48
+ #
49
+ # @return [Object] The type-casted value
50
+ #
51
+ def value
52
+ to_type(@value)
53
+ end
54
+
55
+ #
56
+ # Assigns a typed value to the field, it is cast according to its type or mapping definition
57
+ #
58
+ # @param v [Object] An object of the defined type for this field
59
+ #
60
+ def value=(v)
61
+ @value = from_type(v)
62
+ end
63
+
64
+ #
65
+ # Returns the string representing this value as it would appear in a FIX message without
66
+ # any kind of type conversion or mapping
67
+ #
68
+ # @return [String] The raw field value
69
+ #
70
+ def raw_value
71
+ @value
72
+ end
73
+
74
+ #
75
+ # Assigns a string directly to the field value without type casting or mapping it
76
+ #
77
+ # @param v [String] The string value to assign
78
+ #
79
+ def raw_value=(v)
80
+ @value = v
81
+ end
82
+
83
+ #
84
+ # Returns the errors for this field, if any
85
+ #
86
+ # @return [Array] The errors for this field
87
+ #
88
+ def errors
89
+ if required && !@value
90
+ "Missing value for <#{name}> field"
91
+ end
92
+ end
93
+
94
+ #
95
+ # Performs the actual mapping or type casting by converting an object to a string
96
+ # or a symbol to a mapped string
97
+ #
98
+ # @param obj [Object] The mapping key or object to convert
99
+ # @return [String] The FIX field value
100
+ #
101
+ def from_type(obj)
102
+ if obj && type && !mapping
103
+ send("dump_#{type}", obj)
104
+ elsif obj && mapping && mapping.has_key?(obj)
105
+ mapping[obj]
106
+ else
107
+ obj
108
+ end
109
+ end
110
+
111
+ #
112
+ # Maps a string to an object or a mapped symbol
113
+ #
114
+ # @param str [String] The string to cast or map
115
+ # @return [Object] An object of the defined type or a mapped symbol
116
+ #
117
+ def to_type(str)
118
+ if str && type && !mapping
119
+ send("parse_#{type}", str)
120
+ elsif str && mapping && mapping.values.map(&:to_s).include?(str)
121
+ mapping.find { |k,v| v.to_s == str.to_s }[0]
122
+ else
123
+ str
124
+ end
125
+ end
126
+
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,79 @@
1
+ require 'fix/protocol/grammar_extensions/message'
2
+
3
+ module Fix
4
+ grammar Fix
5
+
6
+ rule main
7
+ message <GrammarExtensions::Message>
8
+ end
9
+
10
+ rule message
11
+ header body trailer
12
+ end
13
+
14
+ rule header
15
+ begin_string body_length msg_type field 4..4
16
+ end
17
+
18
+ rule body
19
+ field*
20
+ end
21
+
22
+ rule trailer
23
+ checksum_tag equal value soh
24
+ end
25
+
26
+ rule begin_string
27
+ "8" equal value soh
28
+ end
29
+
30
+ rule body_length
31
+ "9" equal value soh
32
+ end
33
+
34
+ rule msg_type
35
+ "35" equal value soh
36
+ end
37
+
38
+ rule sender_comp_id
39
+ "49" equal value soh
40
+ end
41
+
42
+ rule target_comp_id
43
+ "56" equal value soh
44
+ end
45
+
46
+ rule msg_seq_num
47
+ "34" equal value soh
48
+ end
49
+
50
+ rule sending_time
51
+ "52" equal value soh
52
+ end
53
+
54
+ rule field
55
+ tag equal value soh
56
+ end
57
+
58
+ rule tag
59
+ !"10=" [1-9] [0-9]*
60
+ end
61
+
62
+ rule checksum_tag
63
+ "10"
64
+ end
65
+
66
+ rule equal
67
+ "="
68
+ end
69
+
70
+ rule value
71
+ [^\x01]+
72
+ end
73
+
74
+ rule soh
75
+ [\x01]
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ module Fix
2
+
3
+ #
4
+ # Namespace for Treetop grammar extensions
5
+ #
6
+ module GrammarExtensions
7
+
8
+ #
9
+ # Extends the message component to return the header, body,
10
+ # message type, and checksum from an AST
11
+ #
12
+ module Message
13
+
14
+ #
15
+ # Returns the FIX message header as a fields array
16
+ #
17
+ # @return <Array> The message header fields
18
+ #
19
+ def header
20
+ unless @header
21
+ hdr = elements[0].elements
22
+ last_fields = hdr.pop
23
+
24
+ @header = hdr.map do |e|
25
+ [ e.elements[0].text_value.to_i, e.elements[2].text_value ]
26
+ end
27
+
28
+ last_fields.elements.inject(@header) do |h, e|
29
+ h << [ e.elements[0].text_value.to_i, e.elements[2].text_value ]
30
+ end
31
+ end
32
+
33
+ @header
34
+ end
35
+
36
+ #
37
+ # Returns the FIX message body as a fields array
38
+ #
39
+ # @return <Array> The message body fields
40
+ #
41
+ def body
42
+ @fields ||= elements[1].elements.map do |e|
43
+ [ e.elements[0].text_value.to_i, e.elements[2].text_value ]
44
+ end
45
+ end
46
+
47
+ #
48
+ # Returns the FIX message type code
49
+ #
50
+ # @return <String> The message type code
51
+ #
52
+ def msg_type
53
+ header.find { |f| f[0] == 35 }[1]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ require 'polyglot'
2
+ require 'treetop'
3
+
4
+ require 'fix/protocol/message_part'
5
+ require 'fix/protocol/repeating_message_part'
6
+ require 'fix/protocol/message_header'
7
+
8
+ module Fix
9
+ module Protocol
10
+
11
+ #
12
+ # Represents an instance of a FIX message
13
+ #
14
+ class Message < MessagePart
15
+
16
+ part :header, klass: MessageHeader
17
+
18
+ def initialize
19
+ super
20
+ header.msg_type = MessageClassMapping.reverse_get(self.class)
21
+ end
22
+
23
+ #
24
+ # Dumps this message as a FIX protocol message, it will automatically
25
+ # calculate the body length and and checksum
26
+ #
27
+ # @return [String] The FIX message
28
+ #
29
+ def dump
30
+ if valid?
31
+ dumped = super
32
+ header.body_length = dumped.gsub(/^8=[^\x01]+\x019=[^\x01]+\x01/, '').length
33
+ dumped = super
34
+ "#{dumped}10=#{'%03d' % (dumped.bytes.inject(&:+) % 256)}\x01"
35
+ end
36
+ end
37
+
38
+ #
39
+ # Whether this instance is ready to be dumped as a valid FIX message
40
+ #
41
+ # @return [Boolean] Whether there are errors present for this instance
42
+ #
43
+ def valid?
44
+ (errors.nil? || errors.empty?) && parse_failure.nil?
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,62 @@
1
+ require 'fix/protocol/messages'
2
+
3
+ module Fix
4
+ module Protocol
5
+
6
+ #
7
+ # Maps the FIX message type codes to message classes
8
+ #
9
+ module MessageClassMapping
10
+
11
+ # The actual code <-> class mapping
12
+ MAPPING = {
13
+ '0' => :heartbeat,
14
+ 'A' => :logon,
15
+ '1' => :test_request,
16
+ '2' => :resend_request,
17
+ '3' => :reject,
18
+ '5' => :logout,
19
+ 'V' => :market_data_request
20
+ }
21
+
22
+ #
23
+ # Returns the message class associated to a message code
24
+ #
25
+ # @param msg_type [Integer] The FIX message type code
26
+ # @return [Class] The FIX message class
27
+ #
28
+ def self.get(msg_type)
29
+ Messages.const_get(camelcase(MAPPING[msg_type])) if MAPPING.has_key?(msg_type)
30
+ end
31
+
32
+ #
33
+ # Returns the message code associated to a message class
34
+ #
35
+ # @param klass [Class] The FIX message class
36
+ # @return [Integer] The FIX message type code
37
+ #
38
+ def self.reverse_get(klass)
39
+ key = klass.name.split('::').last.gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase.to_sym
40
+ MAPPING.find { |p| p[1] == key }[0]
41
+ end
42
+
43
+ #
44
+ # Formats a symbol as a proper class name
45
+ #
46
+ # @param s [Symbol] A name to camelcase
47
+ # @return [Symbol] A camelcased class name
48
+ #
49
+ def self.camelcase(s)
50
+ s.to_s.split(' ').map { |str| str.split('_') }.flatten.map(&:capitalize).join.to_sym
51
+ end
52
+
53
+ #
54
+ # Mark all the message classes for autoloading
55
+ #
56
+ MAPPING.values.each do |klass|
57
+ Messages.autoload(camelcase(klass), "fix/protocol/messages/#{klass}")
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ require 'fix/protocol/message_part'
2
+
3
+ module Fix
4
+ module Protocol
5
+
6
+ #
7
+ # The standard FIX message header
8
+ #
9
+ class MessageHeader < MessagePart
10
+
11
+ field :version, tag: 8, required: true, default: 'FIX.4.4'
12
+ field :body_length, tag: 9
13
+ field :msg_type, tag: 35, required: true
14
+ field :sender_comp_id, tag: 49, required: true
15
+ field :target_comp_id, tag: 56, required: true
16
+ field :msg_seq_num, tag: 34, required: true, type: :integer
17
+ field :sending_time, tag: 52, required: true, type: :timestamp, default: proc { Time.now }
18
+
19
+ #
20
+ # Returns the errors relevant to the message header
21
+ #
22
+ # @return [Array<String>] The errors on the message header
23
+ #
24
+ def errors
25
+ if version == 'FIX.4.4'
26
+ super
27
+ else
28
+ [super, "Unsupported version: <#{version}>"].flatten
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,175 @@
1
+ require 'fix/protocol/field'
2
+
3
+ module Fix
4
+ module Protocol
5
+
6
+ #
7
+ # Basic building block for messages. Message parts can define fields, sub-parts and collections
8
+ #
9
+ class MessagePart
10
+
11
+ attr_accessor :parse_failure, :name
12
+
13
+ def initialize(opts = {})
14
+ self.name = opts[:name]
15
+ self.class.structure.each { |node| initialize_node(node) }
16
+ end
17
+
18
+ #
19
+ # Inherits the +@structure+ class instance variable but allows
20
+ # the child to modify it without impacting the parent, this allows
21
+ # for message structure refinement
22
+ #
23
+ def self.inherited(klass)
24
+ klass.send(:instance_variable_set, :@structure, structure.dup)
25
+ end
26
+
27
+ #
28
+ # Dumps this message part as a FIX message fragment
29
+ #
30
+ # @return [String] A FIX message fragment
31
+ #
32
+ def dump
33
+ nodes.map(&:dump).join
34
+ end
35
+
36
+ #
37
+ # Parses a full or partial FIX message string into the message part nodes
38
+ #
39
+ # @return [String] The string part that wasn't consumed during the parsing
40
+ #
41
+ def parse(str)
42
+ left_to_parse = str
43
+
44
+ nodes.each do |node|
45
+ unless parse_failure
46
+ left_to_parse = node.parse(left_to_parse)
47
+ self.parse_failure = node.parse_failure
48
+ end
49
+ end
50
+
51
+ left_to_parse
52
+ end
53
+
54
+ #
55
+ # Initializes a node depending on its type, this is called when initializing a message
56
+ # part instance by the constructor. Usually one node is initialized for each structure element
57
+ #
58
+ def initialize_node(node)
59
+ if node[:node_type] == :part
60
+ nodes << node[:klass].new(node)
61
+ elsif node[:node_type] == :field
62
+ nodes << FP::Field.new(node)
63
+ elsif node[:node_type] == :collection
64
+ nodes << FP::RepeatingMessagePart.new(node)
65
+ end
66
+ end
67
+
68
+ #
69
+ # The message part nodes, they'll be either a +FP::Field+, an +FP::RepeatingMessagePart+ or a +FP::MessagePart+
70
+ #
71
+ # @return [Array] The nodes for this message part
72
+ #
73
+ def nodes
74
+ @nodes ||= []
75
+ end
76
+
77
+ #
78
+ # Searches the immediate hierarchy by node name to return the requested node
79
+ #
80
+ # @param n [String] The node name to look for
81
+ # @return [Object] The found node, if any
82
+ #
83
+ def node_for_name(n)
84
+ nodes.find { |node| node.name.to_s == n.to_s }
85
+ end
86
+
87
+ #
88
+ # Class-level shortcut to directly parse an instance of a specific message part subclass
89
+ #
90
+ def self.parse(str)
91
+ instce = new
92
+ instce.parse(str)
93
+ instce
94
+ end
95
+
96
+ #
97
+ # Defines a collection as a part of this message part, collections typically have a counter and a repeating element
98
+ #
99
+ # @param name [String] The collection name, this will be the name of a dynamically created accessor on the message part
100
+ # @param opts [Hash] The required options are +:counter_tag+ and +:klass+
101
+ #
102
+ def self.collection(name, opts = {})
103
+ structure << { node_type: :collection, name: name, counter_tag: opts[:counter_tag], klass: opts[:klass] }
104
+
105
+ define_method(name) do
106
+ node_for_name(name)
107
+ end
108
+ end
109
+
110
+ #
111
+ # Defines a reusable message part as element of this particular class
112
+ #
113
+ # @param name [String] The part name, this will be the name of a dynamically created accessor on the message part
114
+ # @param opts [Hash] Options hash
115
+ #
116
+ def self.part(name, opts = {})
117
+ structure << { node_type: :part, name: name }.merge(opts)
118
+
119
+ define_method(name) do
120
+ node_for_name(name)
121
+ end
122
+ end
123
+
124
+ #
125
+ # Defines a field as part of the structure for this class
126
+ #
127
+ # @param name [String] The field name, this will be the base name of a set of methods created on the class instances
128
+ # @param opts [Hash] Options hash
129
+ #
130
+ def self.field(name, opts)
131
+ structure << { node_type: :field, name: name }.merge(opts)
132
+
133
+ # Getter
134
+ define_method(name) do
135
+ node_for_name(name).value
136
+ end
137
+
138
+ # Setter
139
+ define_method("#{name}=") do |val|
140
+ node_for_name(name).value = val
141
+ end
142
+
143
+ if opts[:mapping]
144
+ define_method("raw_#{name}") do
145
+ node_for_name(name).raw_value
146
+ end
147
+
148
+ define_method("raw_#{name}=") do |val|
149
+ node_for_name(name).raw_value = val
150
+ end
151
+ end
152
+ end
153
+
154
+ #
155
+ # Returns this message part class' structure
156
+ #
157
+ # @return [Array] The message structure
158
+ #
159
+ def self.structure
160
+ @structure ||= []
161
+ end
162
+
163
+ #
164
+ # Returns the errors for this instance
165
+ #
166
+ # @return [Array] The errors for this instance
167
+ #
168
+ def errors
169
+ [nodes.map(&:errors), nodes.map(&:parse_failure)].flatten.compact
170
+ end
171
+
172
+ end
173
+ end
174
+ end
175
+
@@ -0,0 +1,11 @@
1
+ module Fix
2
+ module Protocol
3
+
4
+ #
5
+ # The message classes container module
6
+ #
7
+ module Messages
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A FIX heartbeat message
7
+ #
8
+ class Heartbeat < ::Fix::Protocol::Message
9
+
10
+ field :test_req_id, tag: 112
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # An Instrument component, see http://www.onixs.biz/fix-dictionary/4.4/compBlock_Instrument.html
7
+ #
8
+ class Instrument < MessagePart
9
+
10
+ field :symbol, tag: 55, required: true
11
+
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,31 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A FIX logon message
7
+ #
8
+ class Logon < Message
9
+
10
+ field :encrypt_method, tag: 98, required: true, type: :integer, default: 0
11
+ field :heart_bt_int, tag: 108, required: true, type: :integer, default: 30
12
+ field :username, tag: 553, required: true
13
+ field :reset_seq_num_flag, tag: 141
14
+
15
+ #
16
+ # Returns the logon-specific errors
17
+ #
18
+ # @return [Array] The error messages
19
+ #
20
+ def errors
21
+ e = []
22
+ e << "Encryption is not supported, the transport level should handle it" unless (encrypt_method == 0)
23
+ e << "Heartbeat interval should be between 10 and 60 seconds" unless heart_bt_int && heart_bt_int <= 60 && heart_bt_int >= 10
24
+ [super, e].flatten
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,17 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A FIX session reject message
7
+ #
8
+ class Logout < Message
9
+
10
+ has_field :text, tag: 58
11
+
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+
@@ -0,0 +1,42 @@
1
+ require 'fix/protocol/messages/md_entry_type'
2
+ require 'fix/protocol/messages/instrument'
3
+
4
+ module Fix
5
+ module Protocol
6
+ module Messages
7
+
8
+ #
9
+ # A FIX market data request message
10
+ #
11
+ class MarketDataRequest < Message
12
+
13
+ # SUBSCRIPTION_TYPES = {
14
+ # snapshot: 1,
15
+ # updates: 2,
16
+ # unsubscribe: 3
17
+ # }
18
+ #
19
+ # MKT_DPTH_TYPES = {
20
+ # full: 0,
21
+ # top: 1
22
+ # }
23
+ #
24
+ # UPDATE_TYPES = {
25
+ # full: 0,
26
+ # incremental: 1
27
+ # }
28
+
29
+ field :md_req_id, tag: 262, required: true
30
+ field :subscription_request_type, tag: 263, required: true, type: :integer #, mapping: SUBSCRIPTION_TYPES
31
+ field :market_depth, tag: 264, required: true, type: :integer #, mapping: MKT_DPTH_TYPES
32
+ field :md_update_type, tag: 265, required: true, type: :integer #, mapping: UPDATE_TYPES
33
+
34
+ collection :md_entry_types, counter_tag: 267, klass: FP::Messages::MdEntryType
35
+ collection :instruments, counter_tag: 146, klass: FP::Messages::Instrument
36
+
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+
@@ -0,0 +1,34 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A market data entry type component, see http://www.onixs.biz/fix-dictionary/4.4/tagNum_269.html
7
+ #
8
+ class MdEntryType < MessagePart
9
+
10
+ #
11
+ # The MD entry type mapping
12
+ #
13
+ MD_ENTRY_TYPES = {
14
+ bid: 0,
15
+ ask: 1,
16
+ offer: 1,
17
+ trade: 2,
18
+ index: 3,
19
+ open: 4,
20
+ close: 5,
21
+ settlement: 6,
22
+ high: 7,
23
+ low: 8,
24
+ vwap: 9,
25
+ volume: 'B'
26
+ }
27
+
28
+ field :md_entry_type, tag: 269, required: true, mapping: MD_ENTRY_TYPES
29
+
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A FIX session reject message
7
+ #
8
+ class Reject < Message
9
+
10
+ has_field :ref_seq_num, tag: 45, position: 0, required: true, type: :integer
11
+ has_field :text, tag: 58, position: 1, required: true
12
+
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,27 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A FIX resend request message, see http://www.onixs.biz/fix-dictionary/4.4/msgType_2_2.html
7
+ #
8
+ class ResendRequest < Message
9
+
10
+ field :begin_seq_no, tag: 7, required: true, type: :integer
11
+ field :end_seq_no, tag: 16, required: true, type: :integer, default: 0
12
+
13
+ #
14
+ # Returns the logon-specific errors
15
+ #
16
+ # @return [Array] The error messages
17
+ #
18
+ def errors
19
+ e = []
20
+ e << "EndSeqNo must either be 0 (inifinity) or be >= BeginSeqNo" unless (end_seq_no.zero? || (end_seq_no >= begin_seq_no))
21
+ [super, e].flatten
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module Fix
2
+ module Protocol
3
+ module Messages
4
+
5
+ #
6
+ # A FIX test request message
7
+ #
8
+ class TestRequest < Message
9
+
10
+ field :test_req_id, tag: 112, required: true
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Fix
2
+ module Protocol
3
+
4
+ #
5
+ # Represents a failure to parse a message, the +errors+ collection
6
+ # should contain the specific error messages
7
+ #
8
+ class ParseFailure
9
+
10
+ attr_accessor :errors, :message
11
+
12
+ def initialize(errs, msg = nil)
13
+ @errors = [errs].flatten.compact
14
+ @message = msg
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,63 @@
1
+ require 'fix/protocol/message_part'
2
+
3
+ module Fix
4
+ module Protocol
5
+
6
+ #
7
+ # Represents a portion of a FIX message consisting of a similar repeating structure
8
+ # preceded by a counter element
9
+ #
10
+ class RepeatingMessagePart < MessagePart
11
+
12
+ extend Forwardable
13
+ def_delegators :nodes, :[], :first, :last, :length, :size, :each
14
+
15
+ include Enumerable
16
+
17
+ attr_accessor :counter_tag, :element_klass
18
+
19
+ def initialize(opts = {})
20
+ @counter_tag = opts[:counter_tag]
21
+ @element_klass = opts[:klass]
22
+ super
23
+ end
24
+
25
+ #
26
+ # Appends a new blank node as the last element of the collection
27
+ #
28
+ def build
29
+ nodes << element_klass.new
30
+
31
+ if block_given?
32
+ yield(nodes.last)
33
+ end
34
+
35
+ nodes.last
36
+ end
37
+
38
+ #
39
+ # Dumps the message fragment as the set of dumped elements preceded by the relevant counter tag
40
+ #
41
+ # @return [String] The part of the initial string that didn't get consumed during the parsing
42
+ #
43
+ def dump
44
+ "#{counter_tag}=#{length}\x01#{super}"
45
+ end
46
+
47
+ #
48
+ # Parses an arbitrary number of nodes according to the found counter tag
49
+ #
50
+ def parse(str)
51
+ if str.match(/^#{counter_tag}\=([^\x01]+)\x01/)
52
+ len = $1.to_i
53
+ @nodes = []
54
+ len.times { @nodes << element_klass.new }
55
+ super(str.gsub(/^#{counter_tag}\=[^\x01]+\x01/, ''))
56
+ else
57
+ self.parse_failure = "Expected <#{str}> to begin with <#{counter_tag}=...|>"
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,54 @@
1
+ module Fix
2
+ module Protocol
3
+
4
+ #
5
+ # Defines helper methods to convert to and from FIX data types
6
+ #
7
+ module TypeConversions
8
+
9
+ #
10
+ # Parses a FIX-formatted timestamp into a Time instance, milliseconds are discarded
11
+ #
12
+ # @param str [String] A FIX-formatted timestamp
13
+ # @return [Time] An UTC date and time
14
+ #
15
+ def parse_timestamp(str)
16
+ if m = str.match(/\A([0-9]{4})([0-9]{2})([0-9]{2})-([0-9]{2}):([0-9]{2}):([0-9]{2})(.[0-9]{3})?\Z/)
17
+ elts = m.to_a.map(&:to_i)
18
+ Time.new(elts[1], elts[2], elts[3], elts[4], elts[5], elts[6], 0)
19
+ end
20
+ end
21
+
22
+ #
23
+ # Outputs a DateTime object as a FIX-formatted timestamp
24
+ #
25
+ # @param dt [DateTime] An UTC date and time
26
+ # @return [String] A FIX-formatted timestamp
27
+ #
28
+ def dump_timestamp(dt)
29
+ dt.utc.strftime('%Y%m%d-%H:%M:%S')
30
+ end
31
+
32
+ #
33
+ # Parses an integer
34
+ #
35
+ # @param str [String] An integer as a string
36
+ # @return [Fixnum] The parsed integer
37
+ #
38
+ def parse_integer(str)
39
+ str && str.to_i
40
+ end
41
+
42
+ #
43
+ # Dumps an integer to a string
44
+ #
45
+ # @param i [Fixnum] An integer
46
+ # @return [String] It's string representation
47
+ #
48
+ def dump_integer(i)
49
+ i.to_s
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ module Fix
2
+ module Protocol
3
+
4
+ #
5
+ # The fix-protocol gem version string
6
+ #
7
+ VERSION = '0.0.3'
8
+
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fix-protocol
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - David François
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redcarpet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: treetop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: polyglot
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: FIX message classes, parsing and generation
126
+ email:
127
+ - david.francois@paymium.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - lib/fix/protocol/field.rb
133
+ - lib/fix/protocol/grammar.treetop
134
+ - lib/fix/protocol/grammar_extensions/message.rb
135
+ - lib/fix/protocol/message.rb
136
+ - lib/fix/protocol/message_class_mapping.rb
137
+ - lib/fix/protocol/message_header.rb
138
+ - lib/fix/protocol/message_part.rb
139
+ - lib/fix/protocol/messages/heartbeat.rb
140
+ - lib/fix/protocol/messages/instrument.rb
141
+ - lib/fix/protocol/messages/logon.rb
142
+ - lib/fix/protocol/messages/logout.rb
143
+ - lib/fix/protocol/messages/market_data_request.rb
144
+ - lib/fix/protocol/messages/md_entry_type.rb
145
+ - lib/fix/protocol/messages/reject.rb
146
+ - lib/fix/protocol/messages/resend_request.rb
147
+ - lib/fix/protocol/messages/test_request.rb
148
+ - lib/fix/protocol/messages.rb
149
+ - lib/fix/protocol/parse_failure.rb
150
+ - lib/fix/protocol/repeating_message_part.rb
151
+ - lib/fix/protocol/type_conversions.rb
152
+ - lib/fix/protocol/version.rb
153
+ - lib/fix/protocol.rb
154
+ - LICENSE
155
+ - README.md
156
+ homepage: https://github.com/paymium/fix-protocol
157
+ licenses: []
158
+ metadata: {}
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - '>='
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - '>='
171
+ - !ruby/object:Gem::Version
172
+ version: 1.3.6
173
+ requirements: []
174
+ rubyforge_project:
175
+ rubygems_version: 2.0.3
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: FIX message classes, parsing and generation
179
+ test_files: []
180
+ has_rdoc: