fix-engine 0.0.24 → 1.0.0
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.
- checksums.yaml +4 -4
- data/README.md +151 -1
- data/bin/fix-engine +7 -1
- data/lib/fix/engine.rb +3 -2
- data/lib/fix/engine/client.rb +41 -10
- data/lib/fix/engine/client_connection.rb +50 -0
- data/lib/fix/engine/connection.rb +123 -151
- data/lib/fix/engine/logger.rb +7 -1
- data/lib/fix/engine/message_buffer.rb +41 -23
- data/lib/fix/engine/server.rb +12 -3
- data/lib/fix/engine/server_connection.rb +103 -0
- data/lib/fix/engine/version.rb +1 -1
- metadata +41 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32ec2077ed212596bc692e4650be57c8a003a55c
|
4
|
+
data.tar.gz: e2ad9ef6e6824e8d06de895dd44747f01a10934f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3cced1625f0196a24a4006e1b9784fa5db83d9141167bca0286bab405fd0e6abefa5ebf0db81b6042ec66fd4824e48b5d5ad1d4e0dd0d561a2bd56e9e075b46c
|
7
|
+
data.tar.gz: 36a2e95f782491cd465c5bf81029a84956884d85180c2beb08dff94652f261a43490d2bdd86feff1ec4bc2c770c99b886fc133ded5fc68b126f59e09710e1642
|
data/README.md
CHANGED
@@ -1,4 +1,154 @@
|
|
1
1
|
FIX Engine [](http://travis-ci.org/Paymium/fix-engine)
|
2
2
|
=
|
3
3
|
|
4
|
-
This library provides an event-machine based FIX server.
|
4
|
+
This library provides an event-machine based FIX server and client connection implementation.
|
5
|
+
|
6
|
+
# Usage as a FIX client
|
7
|
+
|
8
|
+
## Implement a connection handler
|
9
|
+
|
10
|
+
Create an `EM::Connection` subclass and include the `FE::ClientConnection` module. You will then have to implement
|
11
|
+
the callbacks for the various message types you are interested in.
|
12
|
+
|
13
|
+
````ruby
|
14
|
+
require 'fix/engine'
|
15
|
+
|
16
|
+
module Referee
|
17
|
+
|
18
|
+
class FixConnection < EM::Connection
|
19
|
+
|
20
|
+
include FE::ClientConnection
|
21
|
+
|
22
|
+
attr_accessor :exchange
|
23
|
+
|
24
|
+
#
|
25
|
+
# When a logon message is received we request a market snapshot
|
26
|
+
# and subscribe for continuous updates
|
27
|
+
#
|
28
|
+
def on_logon(msg)
|
29
|
+
mdr = FP::Messages::MarketDataRequest.new
|
30
|
+
|
31
|
+
mdr.md_req_id = 'foo'
|
32
|
+
|
33
|
+
mdr.subscription_request_type = :updates
|
34
|
+
mdr.market_depth = :full
|
35
|
+
mdr.md_update_type = :incremental
|
36
|
+
|
37
|
+
mdr.instruments.build do |i|
|
38
|
+
i.symbol = 'EUR/XBT'
|
39
|
+
end
|
40
|
+
|
41
|
+
[:bid, :ask, :trade, :open, :vwap, :close].each do |mdet|
|
42
|
+
mdr.md_entry_types.build do |m|
|
43
|
+
m.md_entry_type = mdet
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
send_msg(mdr)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Called when a market data snapshot is received
|
51
|
+
def on_market_data_snapshot(msg)
|
52
|
+
update_book_with(msg)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Called upon each subsequent update
|
56
|
+
def on_market_data_incremental_refresh(msg)
|
57
|
+
update_book_with(msg)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Update the local order book copy with the new data
|
61
|
+
def update_book_with(msg)
|
62
|
+
msg.md_entries.each do |mde|
|
63
|
+
exchange.book[mde.md_entry_type].set_depth_at(BigDecimal(mde.md_entry_px), BigDecimal(mde.md_entry_size))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
````
|
70
|
+
|
71
|
+
## Use it to connect to a FIX acceptor
|
72
|
+
|
73
|
+
Once your connection class has been created you can establish a connection in a running EventMachine reactor.
|
74
|
+
See the [referee gem](https://github.com/davout/referee) for the full code example.
|
75
|
+
|
76
|
+
````ruby
|
77
|
+
require 'referee/exchange'
|
78
|
+
require 'referee/fix_connection'
|
79
|
+
|
80
|
+
module Referee
|
81
|
+
module Exchanges
|
82
|
+
class Paymium < Referee::Exchange
|
83
|
+
|
84
|
+
FIX_SERVER = 'fix.paymium.com'
|
85
|
+
FIX_PORT = 8359
|
86
|
+
|
87
|
+
def symbol
|
88
|
+
'PAYM'
|
89
|
+
end
|
90
|
+
|
91
|
+
def currency
|
92
|
+
'EUR'
|
93
|
+
end
|
94
|
+
|
95
|
+
def connect
|
96
|
+
FE::Logger.logger.level = Logger::WARN
|
97
|
+
|
98
|
+
EM.connect(FIX_SERVER, FIX_PORT, FixConnection) do |conn|
|
99
|
+
conn.target_comp_id = 'PAYMIUM'
|
100
|
+
conn.comp_id = 'BC-U458625'
|
101
|
+
conn.username = 'BC-U458625'
|
102
|
+
conn.exchange = self
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
````
|
109
|
+
|
110
|
+
|
111
|
+
# Usage as a FIX acceptor
|
112
|
+
|
113
|
+
You can start a simple FIX acceptor that will maintain a session by running the `fix-engine` executable.
|
114
|
+
The basic FIX acceptor requires a `COMP_ID` environment variable to be set.
|
115
|
+
|
116
|
+
````
|
117
|
+
$ COMP_ID=MY_COMP_ID fix-engine
|
118
|
+
|
119
|
+
> D, [2015-01-07T12:47:07.807867 #87486] DEBUG -- : Starting FIX engine v0.0.31, listening on <0.0.0.0:8359>, exit with <Ctrl-C>
|
120
|
+
> D, [2015-01-07T12:47:12.379787 #87486] DEBUG -- : Client connected <127.0.0.1:54204>, expecting logon message in the next 10s
|
121
|
+
> D, [2015-01-07T12:47:12.816626 #87486] DEBUG -- : Received a <Fix::Protocol::Messages::Logon> from <127.0.0.1:54204> with sequence number <1>
|
122
|
+
> D, [2015-01-07T12:47:12.820093 #87486] DEBUG -- : Peer authenticated as <JAVA_TESTS> with heartbeat interval of <30s> and message sequence number start <1>
|
123
|
+
> D, [2015-01-07T12:47:12.820259 #87486] DEBUG -- : Heartbeat interval for <127.0.0.1:54204> : <30s>
|
124
|
+
> D, [2015-01-07T12:47:12.820899 #87486] DEBUG -- : Sending <Fix::Protocol::Messages::Logon> to <127.0.0.1:54204> with sequence number <1>
|
125
|
+
````
|
126
|
+
|
127
|
+
In order to handle business messages appropriately you need to implement a connection handler
|
128
|
+
that includes the `FE::ServerConnection` module and use that as a connection handler.
|
129
|
+
|
130
|
+
````ruby
|
131
|
+
class MyHandler < EM::Connection
|
132
|
+
|
133
|
+
include FE::ServerConnection
|
134
|
+
|
135
|
+
def on_market_data_request
|
136
|
+
# Fetch market data and send the relevant response
|
137
|
+
# ...
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
server = FE::Server.new('127.0.0.1', 8095, MyHandler) do |conn|
|
143
|
+
conn.comp_id = 'MY_COMP_ID'
|
144
|
+
end
|
145
|
+
|
146
|
+
# This will also start an EventMachine reactor
|
147
|
+
server.run!
|
148
|
+
|
149
|
+
# This would be used inside an already-running reactor
|
150
|
+
EM.run do
|
151
|
+
server.start_server
|
152
|
+
end
|
153
|
+
````
|
154
|
+
|
data/bin/fix-engine
CHANGED
@@ -5,5 +5,11 @@ require 'fix/engine'
|
|
5
5
|
ip = ENV['FE_IP'] || FE::DEFAULT_IP
|
6
6
|
port = ENV['FE_PORT'] || FE::DEFAULT_PORT
|
7
7
|
|
8
|
-
|
8
|
+
if ENV['COMP_ID']
|
9
|
+
Fix::Engine.run!(ip, port) do |conn|
|
10
|
+
conn.comp_id = ENV['COMP_ID']
|
11
|
+
end
|
12
|
+
else
|
13
|
+
puts "The COMP_ID environment variable must be set to be able to start a basic acceptor."
|
14
|
+
end
|
9
15
|
|
data/lib/fix/engine.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'fix/engine/logger'
|
2
2
|
require 'fix/engine/server'
|
3
|
+
require 'fix/engine/client_connection'
|
3
4
|
|
4
5
|
#
|
5
6
|
# Main FIX namespace
|
@@ -20,8 +21,8 @@ module Fix
|
|
20
21
|
#
|
21
22
|
# Runs a FIX server engine
|
22
23
|
#
|
23
|
-
def self.run!(ip = DEFAULT_IP, port = DEFAULT_PORT, handler = FE::
|
24
|
-
Server.new(ip, port, handler).run!
|
24
|
+
def self.run!(ip = DEFAULT_IP, port = DEFAULT_PORT, handler = FE::ServerConnection, &block)
|
25
|
+
Server.new(ip, port, handler, &block).run!
|
25
26
|
end
|
26
27
|
|
27
28
|
#
|
data/lib/fix/engine/client.rb
CHANGED
@@ -8,9 +8,9 @@ module Fix
|
|
8
8
|
#
|
9
9
|
class Client
|
10
10
|
|
11
|
-
|
11
|
+
@clients = {}
|
12
12
|
|
13
|
-
attr_accessor :ip, :port, :connection, :
|
13
|
+
attr_accessor :ip, :port, :connection, :username
|
14
14
|
|
15
15
|
include Logger
|
16
16
|
|
@@ -18,34 +18,65 @@ module Fix
|
|
18
18
|
@ip = ip
|
19
19
|
@port = port
|
20
20
|
@connection = connection
|
21
|
+
|
22
|
+
self.class.instance_variable_get(:@clients)[key] = self
|
21
23
|
end
|
22
24
|
|
23
25
|
#
|
24
26
|
# Returns a client instance from its connection IP
|
25
27
|
#
|
26
28
|
# @param ip [String] The connection IP
|
29
|
+
# @param port [Fixnum] The connection port
|
30
|
+
# @param connection [FE::Connection] Optionnally the connection which will used to create an instance if none exists
|
27
31
|
# @return [Fix::Engine::Client] The client connected for this IP
|
28
32
|
#
|
29
|
-
def self.get(ip, port, connection)
|
30
|
-
|
33
|
+
def self.get(ip, port, connection = nil)
|
34
|
+
@clients[key(ip, port)] || Client.new(ip, port, connection)
|
31
35
|
end
|
32
36
|
|
37
|
+
#
|
38
|
+
# Returns the count of currently connected clients
|
39
|
+
#
|
40
|
+
# @return [Fixnum] The client count
|
41
|
+
#
|
33
42
|
def self.count
|
34
|
-
|
43
|
+
@clients.count
|
35
44
|
end
|
36
45
|
|
46
|
+
#
|
47
|
+
# Removes a client from the currently connected ones
|
48
|
+
#
|
49
|
+
# @param ip [String] The client's remote IP
|
50
|
+
# @param port [Fixnum] The client's port
|
51
|
+
#
|
37
52
|
def self.delete(ip, port)
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
def has_session?
|
42
|
-
!!client_id
|
53
|
+
@clients.delete(key(ip, port))
|
43
54
|
end
|
44
55
|
|
56
|
+
#
|
57
|
+
# Returns an identifier for the current client
|
58
|
+
#
|
59
|
+
# @return [String] An identifier
|
60
|
+
#
|
45
61
|
def key
|
46
62
|
self.class.key(ip, port)
|
47
63
|
end
|
48
64
|
|
65
|
+
#
|
66
|
+
# Removes the current client from the array of connected ones
|
67
|
+
#
|
68
|
+
def delete
|
69
|
+
self.class.delete(ip, port)
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Returns an identifier for the given IP and port
|
74
|
+
#
|
75
|
+
# @param ip [String] The client's remote IP
|
76
|
+
# @param port [Fixnum] The client's port
|
77
|
+
#
|
78
|
+
# @return [String] An identifier
|
79
|
+
#
|
49
80
|
def self.key(ip, port)
|
50
81
|
"#{ip}:#{port}"
|
51
82
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'fix/engine/connection'
|
2
|
+
|
3
|
+
module Fix
|
4
|
+
module Engine
|
5
|
+
|
6
|
+
#
|
7
|
+
# The client connection wrapper, used in order to connect a remote FIX server
|
8
|
+
#
|
9
|
+
module ClientConnection
|
10
|
+
|
11
|
+
include Connection
|
12
|
+
|
13
|
+
attr_accessor :username
|
14
|
+
|
15
|
+
#
|
16
|
+
# Run after we've connected to the server
|
17
|
+
#
|
18
|
+
def post_init
|
19
|
+
super
|
20
|
+
|
21
|
+
log("Connecting to server sending a logon message with our COMP_ID <#{@comp_id}>")
|
22
|
+
|
23
|
+
@logged_in = false
|
24
|
+
|
25
|
+
EM.next_tick { send_logon }
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Sends a logon message to the server we're connected to
|
30
|
+
#
|
31
|
+
def send_logon
|
32
|
+
logon = FP::Messages::Logon.new
|
33
|
+
logon.username = @username
|
34
|
+
logon.target_comp_id = @peer_comp_id
|
35
|
+
logon.sender_comp_id = @comp_id
|
36
|
+
logon.reset_seq_num_flag = true
|
37
|
+
send_msg(logon)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Consider ourselves logged-in if we receive on of these
|
42
|
+
#
|
43
|
+
def on_logon(msg)
|
44
|
+
@logged_in = true
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'eventmachine'
|
2
2
|
|
3
3
|
require 'fix/engine/message_buffer'
|
4
|
-
require 'fix/engine/client'
|
5
4
|
|
6
5
|
module Fix
|
7
6
|
module Engine
|
@@ -9,61 +8,52 @@ module Fix
|
|
9
8
|
#
|
10
9
|
# The client connection handling logic and method overrides
|
11
10
|
#
|
12
|
-
|
11
|
+
module Connection
|
13
12
|
|
14
13
|
include Logger
|
15
14
|
|
16
|
-
#
|
17
|
-
# Timespan during which a client must send a logon message after connecting
|
18
|
-
#
|
19
|
-
LOGON_TIMEOUT = 10
|
20
|
-
|
21
|
-
#
|
22
|
-
# Timespan during which we will wait for a heartbeat response from the client
|
23
|
-
#
|
24
|
-
HRTBT_TIMEOUT = 10
|
25
|
-
|
26
15
|
#
|
27
16
|
# Grace time before we disconnect a client that doesn't reply to a test request
|
28
17
|
#
|
29
18
|
TEST_REQ_GRACE_TIME = 15
|
30
19
|
|
31
|
-
attr_accessor :ip, :port, :
|
20
|
+
attr_accessor :ip, :port, :msg_buf, :hrtbt_int, :last_request_at, :comp_id, :target_comp_id
|
32
21
|
|
33
22
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
DEFAULT_COMP_ID = 'PYMBTC'
|
37
|
-
|
38
|
-
#
|
39
|
-
# Run after a client has connected
|
23
|
+
# Initialize the messages array, our comp_id, and the expected message sequence number
|
40
24
|
#
|
41
25
|
def post_init
|
42
|
-
@
|
43
|
-
@client = Client.get(ip, port, self)
|
44
|
-
@expected_clt_seq_num = 1
|
26
|
+
@expected_seq_num = 1
|
45
27
|
|
46
|
-
log("Client connected from <#{@client.key}>, expecting logon message in the next #{LOGON_TIMEOUT}s")
|
47
|
-
|
48
|
-
@comp_id ||= DEFAULT_COMP_ID
|
49
|
-
|
50
28
|
# The sent messages
|
51
29
|
@messages = []
|
30
|
+
end
|
52
31
|
|
53
|
-
|
54
|
-
|
55
|
-
|
32
|
+
#
|
33
|
+
# The way we refer to our connection peer in various logs and messages
|
34
|
+
#
|
35
|
+
def peer
|
36
|
+
"server"
|
56
37
|
end
|
57
38
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
39
|
+
#
|
40
|
+
# Sets the heartbeat interval and schedules the keep alive call
|
41
|
+
#
|
42
|
+
# @param interval [Fixnum] The frequency in seconds at which a heartbeat should be emitted
|
43
|
+
#
|
44
|
+
def set_heartbeat_interval(interval)
|
45
|
+
@hrtbt_int && raise("Can't set heartbeat interval twice")
|
46
|
+
@hrtbt_int = interval
|
47
|
+
|
48
|
+
log("Heartbeat interval for #{peer} : <#{hrtbt_int}s>")
|
49
|
+
@keep_alive_timer = EM.add_periodic_timer(1) { keep_alive }
|
64
50
|
end
|
65
51
|
|
66
|
-
|
52
|
+
#
|
53
|
+
# Keeps the connection alive by sending regular heartbeats, and test request
|
54
|
+
# messages whenever the connection has been idl'ing for too long
|
55
|
+
#
|
56
|
+
def keep_alive
|
67
57
|
@last_send_at ||= 0
|
68
58
|
@last_request_at ||= 0
|
69
59
|
@hrtbt_int ||= 0
|
@@ -75,25 +65,48 @@ module Fix
|
|
75
65
|
|
76
66
|
# Trigger a test req message when we haven't received anything for a while
|
77
67
|
if !@pending_test_req_id && (last_request_at < (Time.now.to_i - @hrtbt_int))
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
@pending_test_req_id = tr.test_req_id
|
68
|
+
send_test_request
|
69
|
+
end
|
70
|
+
end
|
82
71
|
|
83
|
-
|
84
|
-
|
85
|
-
|
72
|
+
#
|
73
|
+
# Sends a test request and expects an answer before +TEST_REQ_GRACE_TIME+
|
74
|
+
#
|
75
|
+
def send_test_request
|
76
|
+
tr = FP::Messages::TestRequest.new
|
77
|
+
tr.test_req_id = SecureRandom.hex(6)
|
78
|
+
send_msg(tr)
|
79
|
+
@pending_test_req_id = tr.test_req_id
|
80
|
+
|
81
|
+
EM.add_timer(TEST_REQ_GRACE_TIME) do
|
82
|
+
@pending_test_req_id && kill!
|
86
83
|
end
|
87
84
|
end
|
88
85
|
|
86
|
+
#
|
87
|
+
# Sends a heartbeat message with an optional +test_req_id+ parameter
|
88
|
+
#
|
89
|
+
# @param test_req_id [String] Sets the test request ID if sent in response to a test request
|
90
|
+
#
|
91
|
+
def send_heartbeat(test_req_id = nil)
|
92
|
+
msg = FP::Messages::Heartbeat.new
|
93
|
+
test_req_id && msg.test_req_id = test_req_id
|
94
|
+
send_msg(msg)
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Sends a +Fix::Protocol::Message+ to the connected peer
|
99
|
+
#
|
100
|
+
# @param msg [Fix::Protocol::Message] The message to send
|
101
|
+
#
|
89
102
|
def send_msg(msg)
|
90
103
|
@send_seq_num ||= 1
|
91
104
|
|
92
105
|
msg.msg_seq_num = @send_seq_num
|
93
106
|
msg.sender_comp_id = @comp_id
|
94
|
-
msg.target_comp_id
|
107
|
+
msg.target_comp_id = @target_comp_id
|
95
108
|
|
96
|
-
log("Sending <#{msg.class}> to
|
109
|
+
log("Sending <#{msg.class}> to #{peer} with sequence number <#{msg.msg_seq_num}>")
|
97
110
|
|
98
111
|
if msg.valid?
|
99
112
|
@messages[msg.msg_seq_num] = msg
|
@@ -102,85 +115,71 @@ module Fix
|
|
102
115
|
@last_send_at = Time.now.to_i
|
103
116
|
else
|
104
117
|
log(msg.errors.join(', '))
|
105
|
-
raise "Tried to send invalid message!"
|
118
|
+
raise "Tried to send invalid message! <#{msg.errors.join(', ')}>"
|
106
119
|
end
|
107
120
|
end
|
108
121
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
log("Heartbeat interval for <#{ip}:#{port}> : <#{hrtbt_int}s>")
|
114
|
-
@hrtbt_monitor = EM.add_periodic_timer(1) { manage_hrtbts }
|
115
|
-
end
|
116
|
-
|
122
|
+
#
|
123
|
+
# Kills the connection after sending a logout message, if applicable
|
124
|
+
#
|
117
125
|
def kill!
|
118
|
-
if @
|
119
|
-
log("Logging out
|
126
|
+
if @target_comp_id
|
127
|
+
log("Logging out #{peer}")
|
128
|
+
|
120
129
|
logout = FP::Messages::Logout.new
|
121
130
|
logout.text = 'Bye!'
|
131
|
+
|
122
132
|
send_msg(logout)
|
123
133
|
end
|
124
134
|
|
125
135
|
close_connection_after_writing
|
126
136
|
end
|
127
137
|
|
138
|
+
#
|
139
|
+
# Cleans up after we're done
|
140
|
+
#
|
128
141
|
def unbind
|
129
|
-
log("Terminating
|
130
|
-
|
131
|
-
@hrtbt_monitor && @hrtbt_monitor.cancel
|
142
|
+
log("Terminating connection to #{peer}")
|
143
|
+
@keep_alive_timer && @keep_alive_timer.cancel
|
132
144
|
end
|
133
145
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
146
|
+
#
|
147
|
+
# Notifies the connected peer it fucked up somehow and kill the connection
|
148
|
+
#
|
149
|
+
# @param error_msg [String] The reason to embed in the reject message
|
150
|
+
# @param msg_seq_num [Fixnum] The message sequence number this error pertains to
|
151
|
+
#
|
152
|
+
def peer_error(error_msg, msg_seq_num)
|
153
|
+
log("Notifying #{peer} of error: <#{error_msg}> and terminating")
|
154
|
+
|
155
|
+
rjct = FP::Messages::Reject.new
|
156
|
+
rjct.text = error_msg
|
157
|
+
rjct.ref_seq_num = msg_seq_num
|
158
|
+
|
140
159
|
send_msg(rjct)
|
141
160
|
kill!
|
142
161
|
end
|
143
162
|
|
144
|
-
|
163
|
+
#
|
164
|
+
# Maintains the message sequence consistency before handing off the message to +#handle_msg+
|
165
|
+
#
|
166
|
+
def process_msg(msg)
|
145
167
|
@recv_seq_num = msg.msg_seq_num
|
146
|
-
|
147
|
-
log("Received a <#{msg.class}> from <#{ip}:#{port}> with sequence number <#{msg.msg_seq_num}>")
|
148
168
|
|
149
|
-
|
150
|
-
if (@expected_clt_seq_num == @recv_seq_num)
|
169
|
+
log("Received a <#{msg.class}> from #{peer} with sequence number <#{msg.msg_seq_num}>")
|
151
170
|
|
171
|
+
# If sequence number == expected, then process it normally
|
172
|
+
if (@expected_seq_num == @recv_seq_num)
|
152
173
|
if @comp_id && msg.target_comp_id != @comp_id
|
153
|
-
@
|
174
|
+
@target_comp_id = msg.sender_comp_id
|
154
175
|
|
176
|
+
# Whoops, incorrect COMP_ID received, kill it with fire
|
155
177
|
if (msg.target_comp_id != @comp_id)
|
156
|
-
|
178
|
+
peer_error("Incorrect TARGET_COMP_ID in message, expected <#{@comp_id}>, got <#{msg.target_comp_id}>", msg.header.msg_seq_num)
|
157
179
|
end
|
158
180
|
|
159
181
|
else
|
160
|
-
if
|
161
|
-
log("Client authenticated as <#{msg.username}> with heartbeat interval of <#{msg.heart_bt_int}s> and message sequence number start <#{msg.msg_seq_num}>")
|
162
|
-
client.client_id = msg.username
|
163
|
-
@client_comp_id = msg.sender_comp_id
|
164
|
-
set_heartbeat_interval(msg.heart_bt_int)
|
165
|
-
|
166
|
-
logon = FP::Messages::Logon.new
|
167
|
-
logon.username = msg.username
|
168
|
-
logon.target_comp_id = msg.sender_comp_id
|
169
|
-
logon.sender_comp_id = msg.target_comp_id
|
170
|
-
logon.reset_seq_num_flag = true
|
171
|
-
send_msg(logon)
|
172
|
-
|
173
|
-
elsif @client_comp_id && msg.is_a?(FP::Messages::Logon)
|
174
|
-
log("Received second logon message, reset_seq_num_flag <#{msg.reset_seq_num_flag}>")
|
175
|
-
if msg.reset_seq_num_flag = 'Y'
|
176
|
-
@send_seq_num = 1
|
177
|
-
@messages = []
|
178
|
-
end
|
179
|
-
|
180
|
-
elsif !@client_comp_id
|
181
|
-
client_error("The session must be started with a logon message", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
182
|
-
|
183
|
-
elsif msg.is_a?(FP::Messages::Heartbeat)
|
182
|
+
if msg.is_a?(FP::Messages::Heartbeat)
|
184
183
|
# If we were expecting an answer to a test request we can sign it off and
|
185
184
|
# cancel the scheduled connection termination
|
186
185
|
if @pending_test_req_id && msg.test_req_id && (@pending_test_req_id == msg.test_req_id)
|
@@ -195,36 +194,40 @@ module Fix
|
|
195
194
|
|
196
195
|
elsif msg.is_a?(FP::Messages::ResendRequest)
|
197
196
|
# Re-send requested message range
|
198
|
-
@messages[msg.begin_seq_no, msg.end_seq_no.zero? ? @messages.length : msg.end_seq_no].each do |m|
|
197
|
+
@messages[msg.begin_seq_no - 1, (msg.end_seq_no.zero? ? @messages.length : (msg.end_seq_no - msg.begin_seq_no + 1))].each do |m|
|
199
198
|
log("Re-sending <#{m.class}> to <#{ip}:#{port}> with sequence number <#{m.msg_seq_num}>")
|
200
199
|
send_data(m.dump)
|
201
200
|
@last_send_at = Time.now.to_i
|
202
201
|
end
|
203
202
|
|
204
203
|
elsif msg.is_a?(FP::Message)
|
205
|
-
|
204
|
+
run_message_handler(msg)
|
206
205
|
end
|
207
206
|
end
|
208
207
|
|
209
|
-
@
|
210
|
-
|
211
|
-
elsif msg.is_a?(FP::Messages::Logon)
|
212
|
-
client_error("Expected logon message to have msg_seq_num = <1>, received <#{msg.msg_seq_num}>", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
208
|
+
@expected_seq_num += 1
|
213
209
|
|
214
|
-
elsif (@
|
215
|
-
log("Ignoring message <#{msg}> with stale sequence number <#{msg.msg_seq_num}>, expecting <#{@
|
210
|
+
elsif (@expected_seq_num > @recv_seq_num)
|
211
|
+
log("Ignoring message <#{msg}> with stale sequence number <#{msg.msg_seq_num}>, expecting <#{@expected_seq_num}>")
|
216
212
|
|
217
|
-
elsif (@
|
213
|
+
elsif (@expected_seq_num < @recv_seq_num) && @target_comp_id
|
218
214
|
# Request missing range when detect a gap
|
219
215
|
rr = FP::Messages::ResendRequest.new
|
220
|
-
rr.begin_seq_no = @
|
216
|
+
rr.begin_seq_no = @expected_seq_num
|
221
217
|
send_msg(rr)
|
222
218
|
end
|
223
219
|
|
224
220
|
self.last_request_at = Time.now.to_i
|
225
221
|
end
|
226
222
|
|
227
|
-
|
223
|
+
#
|
224
|
+
# Runs the defined message handler for the message's class
|
225
|
+
#
|
226
|
+
# @param msg [FP::Message] The message to handle
|
227
|
+
#
|
228
|
+
def run_message_handler(msg)
|
229
|
+
m = "on_#{msg.class.to_s.split('::').last.gsub(/(.)([A-Z])/, '\1_\2').downcase}".to_sym
|
230
|
+
send(m, msg) if respond_to?(m)
|
228
231
|
end
|
229
232
|
|
230
233
|
#
|
@@ -234,58 +237,27 @@ module Fix
|
|
234
237
|
# @param data [String] The received data chunk
|
235
238
|
#
|
236
239
|
def receive_data(data)
|
237
|
-
|
238
|
-
|
240
|
+
@buf ||= MessageBuffer.new do |parsed|
|
241
|
+
if (parsed.class == FP::ParseFailure) || !parsed.errors.count.zero?
|
242
|
+
peer_error("#{parsed.message} -- #{parsed.errors.join(", ")}", @expected_seq_num)
|
243
|
+
log("Failed to parse message <#{parsed.message}>")
|
244
|
+
parsed.errors.each { |err| log(" >>> #{err}") }
|
245
|
+
|
246
|
+
else
|
247
|
+
process_msg(parsed)
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|
239
251
|
|
240
252
|
begin
|
241
|
-
|
253
|
+
@buf.add_data(data)
|
242
254
|
rescue
|
243
|
-
log("
|
255
|
+
log("Raised exception by #{peer} when parsing data <#{@buf.msg_buf.gsub(/\x01/, '|')}>, terminating.")
|
244
256
|
log($!.message + $!.backtrace.join("\n"))
|
245
257
|
kill!
|
246
258
|
end
|
247
259
|
end
|
248
260
|
|
249
|
-
#
|
250
|
-
# Attempts to parse fields from the message buffer, if the fields that get parsed
|
251
|
-
# complete the temporary message, it is handled
|
252
|
-
#
|
253
|
-
def parse_messages_from_buffer
|
254
|
-
while idx = msg_buf.index("\x01")
|
255
|
-
field = msg_buf.slice!(0, idx + 1).gsub(/\x01\Z/, '')
|
256
|
-
msg.append(field)
|
257
|
-
|
258
|
-
if msg.complete?
|
259
|
-
parsed = msg.parse!
|
260
|
-
if parsed.is_a?(FP::Message)
|
261
|
-
handle_msg(parsed)
|
262
|
-
elsif parsed.is_a?(FP::ParseFailure)
|
263
|
-
client_error(parsed.errors.join(", "), @expected_clt_seq_num, target_comp_id: (@client_comp_id || 'UNKNOWN'))
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
#
|
270
|
-
# The data buffer string
|
271
|
-
#
|
272
|
-
def msg_buf
|
273
|
-
@msg_buf ||= ''
|
274
|
-
end
|
275
|
-
|
276
|
-
#
|
277
|
-
# The temporary message to which fields get appended
|
278
|
-
#
|
279
|
-
def msg
|
280
|
-
@msg ||= MessageBuffer.new(@client)
|
281
|
-
end
|
282
|
-
|
283
|
-
def send_heartbeat(test_req_id = nil)
|
284
|
-
msg = FP::Messages::Heartbeat.new
|
285
|
-
test_req_id && msg.test_req_id = test_req_id
|
286
|
-
send_msg(msg)
|
287
|
-
end
|
288
|
-
|
289
261
|
end
|
290
262
|
end
|
291
263
|
end
|
data/lib/fix/engine/logger.rb
CHANGED
@@ -13,9 +13,21 @@ module Fix
|
|
13
13
|
|
14
14
|
attr_accessor :fields, :client
|
15
15
|
|
16
|
-
def initialize(
|
16
|
+
def initialize(&block)
|
17
17
|
@fields = []
|
18
|
-
|
18
|
+
|
19
|
+
raise "A block accepting a FP::Message as single parameter must be provided" unless (block && (block.arity == 1))
|
20
|
+
@msg_handler = block
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Adds received bytes to the message buffer and attempt to process them
|
25
|
+
#
|
26
|
+
# @param data [String] The received FIX message bits, as they come
|
27
|
+
#
|
28
|
+
def add_data(data)
|
29
|
+
msg_buf << data.chomp
|
30
|
+
parse_messages
|
19
31
|
end
|
20
32
|
|
21
33
|
#
|
@@ -41,42 +53,48 @@ module Fix
|
|
41
53
|
end
|
42
54
|
|
43
55
|
#
|
44
|
-
#
|
56
|
+
# Attempts to parse fields from the message buffer, if the fields that get parsed
|
57
|
+
# complete the temporary message, it is processed
|
45
58
|
#
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
def parse_messages(&block)
|
60
|
+
while idx = msg_buf.index("\x01")
|
61
|
+
field = msg_buf.slice!(0, idx + 1).gsub(/\x01\Z/, '')
|
62
|
+
append(field)
|
63
|
+
|
64
|
+
if complete?
|
65
|
+
parsed = FP.parse(to_s)
|
66
|
+
@fields = []
|
67
|
+
@msg_handler.call(parsed)
|
68
|
+
end
|
51
69
|
end
|
52
|
-
|
53
|
-
msg
|
54
70
|
end
|
55
71
|
|
56
72
|
#
|
57
|
-
#
|
58
|
-
# can start to get buffered right away
|
73
|
+
# The data buffer string
|
59
74
|
#
|
60
|
-
def
|
61
|
-
|
62
|
-
@fields = []
|
63
|
-
parsed
|
75
|
+
def msg_buf
|
76
|
+
@msg_buf ||= ''
|
64
77
|
end
|
65
78
|
|
79
|
+
#
|
80
|
+
# Returns a human-friendly string of the currently handled data
|
81
|
+
#
|
82
|
+
# @return [String] The parsed fields and the temporary buffer
|
83
|
+
#
|
66
84
|
def debug
|
67
|
-
to_s('|')
|
85
|
+
"#{to_s('|')}#{@msg_buf}"
|
68
86
|
end
|
69
87
|
|
88
|
+
#
|
89
|
+
# Returns the current fields as a string joined by a given separator
|
90
|
+
#
|
91
|
+
# @param sep [String] The separator
|
92
|
+
# @return [String] The fields joined by the separator
|
93
|
+
#
|
70
94
|
def to_s(sep = "\x01")
|
71
95
|
fields.map { |f| f.join('=') }.join(sep) + sep
|
72
96
|
end
|
73
97
|
|
74
|
-
def log_errors(msg)
|
75
|
-
log("Invalid message received <#{debug}>")
|
76
|
-
msg.errors.each { |e| log(" >>> #{e}") }
|
77
|
-
end
|
78
|
-
|
79
|
-
|
80
98
|
end
|
81
99
|
end
|
82
100
|
end
|
data/lib/fix/engine/server.rb
CHANGED
@@ -2,7 +2,7 @@ require 'eventmachine'
|
|
2
2
|
|
3
3
|
require 'fix/protocol'
|
4
4
|
require 'fix/engine/version'
|
5
|
-
require 'fix/engine/
|
5
|
+
require 'fix/engine/server_connection'
|
6
6
|
|
7
7
|
module Fix
|
8
8
|
module Engine
|
@@ -14,14 +14,18 @@ module Fix
|
|
14
14
|
|
15
15
|
include Logger
|
16
16
|
|
17
|
+
#
|
18
|
+
# Periodicity in seconds of logged status reports
|
19
|
+
#
|
17
20
|
REPORT_INTERVAL = 10
|
18
21
|
|
19
22
|
attr_accessor :ip, :port
|
20
23
|
|
21
|
-
def initialize(ip, port, handler)
|
24
|
+
def initialize(ip, port, handler, &block)
|
22
25
|
@ip = ip
|
23
26
|
@port = port
|
24
27
|
@handler = handler
|
28
|
+
@block = block
|
25
29
|
end
|
26
30
|
|
27
31
|
#
|
@@ -38,10 +42,15 @@ module Fix
|
|
38
42
|
#
|
39
43
|
def start_server
|
40
44
|
raise "EventMachine must be running to start a server" unless EM.reactor_running?
|
41
|
-
|
45
|
+
|
46
|
+
EM.start_server(ip, port, @handler) { |conn| @block && @block.call(conn) }
|
47
|
+
|
42
48
|
REPORT_INTERVAL && EM.add_periodic_timer(REPORT_INTERVAL) { report_status }
|
43
49
|
end
|
44
50
|
|
51
|
+
#
|
52
|
+
# Logs a short summary of the current server status
|
53
|
+
#
|
45
54
|
def report_status
|
46
55
|
log("#{Client.count} client(s) currently connected")
|
47
56
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'fix/engine/connection'
|
2
|
+
require 'fix/engine/client'
|
3
|
+
|
4
|
+
module Fix
|
5
|
+
module Engine
|
6
|
+
|
7
|
+
#
|
8
|
+
# The server connection wrapper, used when accepting a connection
|
9
|
+
#
|
10
|
+
module ServerConnection
|
11
|
+
|
12
|
+
include Connection
|
13
|
+
|
14
|
+
#
|
15
|
+
# Timespan during which a client must send a logon message after connecting
|
16
|
+
#
|
17
|
+
LOGON_TIMEOUT = 10
|
18
|
+
|
19
|
+
#
|
20
|
+
# Run after a client has connected
|
21
|
+
#
|
22
|
+
def post_init
|
23
|
+
super
|
24
|
+
|
25
|
+
@port, @ip = Socket.unpack_sockaddr_in(get_peername)
|
26
|
+
@client = Client.get(ip, port, self)
|
27
|
+
|
28
|
+
log("Client connected #{peer}, expecting logon message in the next #{LOGON_TIMEOUT}s")
|
29
|
+
|
30
|
+
EM.add_timer(LOGON_TIMEOUT) { logon_timeout }
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Logs the client out should he fail to authenticate before +LOGON_TIMEOUT+ seconds
|
35
|
+
#
|
36
|
+
def logon_timeout
|
37
|
+
unless @target_comp_id
|
38
|
+
log("Client #{peer} failed to authenticate before timeout, closing connection")
|
39
|
+
close_connection_after_writing
|
40
|
+
client.delete
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Returns the currently connected client
|
46
|
+
#
|
47
|
+
def client
|
48
|
+
Client.get(ip, port, self)
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# The way we refer to our connection peer in various logs and messages
|
53
|
+
#
|
54
|
+
def peer
|
55
|
+
"<#{client.key}>"
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Deletes the +FE::Client+ instance after the connection is terminated
|
60
|
+
#
|
61
|
+
def unbind
|
62
|
+
super
|
63
|
+
client.delete
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# We override +FE::Connection#run_message_handlers+ to add some session-related logic
|
68
|
+
#
|
69
|
+
def run_message_handler(msg)
|
70
|
+
if !@target_comp_id && msg.is_a?(FP::Messages::Logon)
|
71
|
+
log("Peer authenticated as <#{msg.username}> with heartbeat interval of <#{msg.heart_bt_int}s> and message sequence number start <#{msg.msg_seq_num}>")
|
72
|
+
client.username = msg.username
|
73
|
+
@target_comp_id = msg.sender_comp_id
|
74
|
+
set_heartbeat_interval(msg.heart_bt_int)
|
75
|
+
|
76
|
+
logon = FP::Messages::Logon.new
|
77
|
+
logon.username = msg.username
|
78
|
+
logon.target_comp_id = msg.sender_comp_id
|
79
|
+
logon.sender_comp_id = msg.target_comp_id
|
80
|
+
logon.reset_seq_num_flag = true
|
81
|
+
|
82
|
+
send_msg(logon)
|
83
|
+
|
84
|
+
elsif @target_comp_id && msg.is_a?(FP::Messages::Logon)
|
85
|
+
log("Received second logon message, reset_seq_num_flag <#{msg.reset_seq_num_flag}>")
|
86
|
+
if msg.reset_seq_num_flag = 'Y'
|
87
|
+
@send_seq_num = 1
|
88
|
+
@messages = []
|
89
|
+
end
|
90
|
+
|
91
|
+
elsif !@target_comp_id
|
92
|
+
peer_error("The session must be started with a logon message", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
93
|
+
|
94
|
+
else
|
95
|
+
super(msg)
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
data/lib/fix/engine/version.rb
CHANGED
metadata
CHANGED
@@ -1,128 +1,120 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fix-engine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David François
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-01-
|
11
|
+
date: 2015-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.1'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ~>
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '3.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '10.3'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ~>
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '10.3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: yard
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ~>
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
47
|
+
version: '0.8'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ~>
|
53
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'
|
54
|
+
version: '0.8'
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: redcarpet
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
72
58
|
requirements:
|
73
|
-
- -
|
59
|
+
- - ~>
|
74
60
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
61
|
+
version: '3.1'
|
76
62
|
type: :development
|
77
63
|
prerelease: false
|
78
64
|
version_requirements: !ruby/object:Gem::Requirement
|
79
65
|
requirements:
|
80
|
-
- -
|
66
|
+
- - ~>
|
81
67
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
68
|
+
version: '3.1'
|
83
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: simplecov
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
86
72
|
requirements:
|
87
|
-
- -
|
73
|
+
- - ~>
|
88
74
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
75
|
+
version: '0.9'
|
90
76
|
type: :development
|
91
77
|
prerelease: false
|
92
78
|
version_requirements: !ruby/object:Gem::Requirement
|
93
79
|
requirements:
|
94
|
-
- -
|
80
|
+
- - ~>
|
95
81
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
82
|
+
version: '0.9'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: fix-protocol
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
100
86
|
requirements:
|
101
|
-
- -
|
87
|
+
- - ~>
|
102
88
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
89
|
+
version: 0.0.64
|
104
90
|
type: :runtime
|
105
91
|
prerelease: false
|
106
92
|
version_requirements: !ruby/object:Gem::Requirement
|
107
93
|
requirements:
|
108
|
-
- -
|
94
|
+
- - ~>
|
109
95
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
96
|
+
version: 0.0.64
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: eventmachine
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
|
-
- -
|
101
|
+
- - ~>
|
116
102
|
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
103
|
+
version: '1.0'
|
118
104
|
type: :runtime
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
107
|
requirements:
|
122
|
-
- -
|
108
|
+
- - ~>
|
123
109
|
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
description: FIX engine
|
110
|
+
version: '1.0'
|
111
|
+
description: " The FIX engine library allows one to easily connect to a FIX acceptor
|
112
|
+
and establish\n a session, it will handle the administrative messages such as logons,
|
113
|
+
hearbeats, gap fills and\n allow custom handling of business level messages.\n\n
|
114
|
+
\ Likewise, an acceptor may be easily implemented by defining callbacks for business
|
115
|
+
level messages.\n\n FIX protocol message parsing capabilities are provided by the
|
116
|
+
fix-protocol gem, which\n currently supports the administrative subset (and a few
|
117
|
+
business level messages) of the FIX 4.4\n message specification. \n"
|
126
118
|
email:
|
127
119
|
- david.francois@paymium.com
|
128
120
|
executables: []
|
@@ -134,13 +126,16 @@ files:
|
|
134
126
|
- bin/fix-engine
|
135
127
|
- lib/fix/engine.rb
|
136
128
|
- lib/fix/engine/client.rb
|
129
|
+
- lib/fix/engine/client_connection.rb
|
137
130
|
- lib/fix/engine/connection.rb
|
138
131
|
- lib/fix/engine/logger.rb
|
139
132
|
- lib/fix/engine/message_buffer.rb
|
140
133
|
- lib/fix/engine/server.rb
|
134
|
+
- lib/fix/engine/server_connection.rb
|
141
135
|
- lib/fix/engine/version.rb
|
142
136
|
homepage: https://github.com/paymium/fix-engine
|
143
|
-
licenses:
|
137
|
+
licenses:
|
138
|
+
- MIT
|
144
139
|
metadata: {}
|
145
140
|
post_install_message:
|
146
141
|
rdoc_options: []
|