ruby-trade 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -8,10 +8,77 @@ into the market to buy or sell shares.
8
8
 
9
9
  ## Installation
10
10
 
11
- Just install the gem (not working just yet)
11
+ Just install the gem:
12
12
 
13
13
  gem install ruby-trade
14
14
 
15
+ Mac users: some people are having some issues installing the ZeroMQ libraries
16
+ on a Mac. If you install an older version of ZeroMQ it should work:
17
+
18
+ brew install zeromq22
19
+ gem install ruby-trade
20
+
21
+ ## Mechanics
22
+
23
+ The mechanics of the market are for the most part like a real stock market. In
24
+ ruby-trade there is only one stock, and everybody buys and sells that stock from
25
+ other players within the market.
26
+
27
+ ### Orders
28
+
29
+ The only way to buy or sell shares is through orders. The client script sends
30
+ a orders to the market to buy or sell a number of shares at a specified price.
31
+ When you send an order, the server checks to see if your order can be matched
32
+ with any of the other orders and if it can be, it will execute a trade and your
33
+ script will receive a notification.
34
+
35
+ The server is real-time, there is no time interval between when things happen.
36
+ If your script sends an order, it is sent to the market immediately.
37
+
38
+ ### Matching Example
39
+
40
+ Here's an example of how the server will attempt to match a new order into the
41
+ market. Suppose here are the existing orders:
42
+
43
+ * Trader A has a buy order for 5k shares at $9.00
44
+ * Trader B has a sell order for 10k shares at $10.00
45
+ * Trader C has:
46
+ * a sell order for 2k shares at $10.00 but it was placed after trader B's order
47
+ * a sell order for 10k shares at $10.50
48
+ * a buy order for 10k shares at $8.00.
49
+
50
+ In this case the "best" buy order is trader A's order at $9 because it has the
51
+ highest price (picture yourself in the position of a seller, would you rather
52
+ sell your shares to someone at $9 or at $8?). This best price is called the "bid".
53
+ The best sell order on the other hand is trader B's sell order at $10, and this
54
+ is called the "ask".
55
+
56
+ Now Trader D comes along and sends a buy order for 15k shares at $12. Here's how
57
+ the server will match up the orders:
58
+
59
+ 1. Trader D will buy 10k shares from trader B at $10.00 (it starts at the best
60
+ sell price).
61
+ 2. Trader D will then buy 2k shares from trader C at $10.00 (it resolves ties at
62
+ a certain price level using a first-come-first-serve algorithm).
63
+ 3. Trader D will finally buy 3k shares from trader C at $10.50. The first two
64
+ orders at $10.00 will be gone, and trader C's order at $10.50 will be updated
65
+ to only have 7k shares left.
66
+
67
+ When this is over, the "bid" will still be $9.00 from trader A's order, but the
68
+ "ask" will have gone up to $10.50 because all the orders at $10.00 are now gone.
69
+ The "last" price (the price that the last trade was at) would be $10.50.
70
+
71
+ Next, trader E sends a sell order for 10k shares at $8.50. The matching is like
72
+ this:
73
+
74
+ 1. Trader E will sell 5k shares to trader A at $9.00 (the best buy price).
75
+ 2. Since there are no more orders left that are greater than or equal to $8.50,
76
+ trader E's order will enter the market as a sell order for 5k shares at $8.50.
77
+
78
+ The "bid" gets updated to be $8.00 (for trader C's buy order) and the "ask" gets
79
+ updated to be $8.50 (trader E's new sell order). The "last" will be $9.00.
80
+
81
+
15
82
  ## Lingo
16
83
 
17
84
  Before getting started, there are a few definitions that you should know about:
@@ -74,6 +141,21 @@ Here is an example client:
74
141
  # Connect to the server
75
142
  MyApp.connect_to "127.0.0.1", as: "Jim"
76
143
 
144
+ ### Hooks
145
+
146
+ The following hooks are available:
147
+
148
+ * `on_connect` - Called when the client connects to the server.
149
+ * `on_tick level` - Called whenever something happens in the exchange. `level1`
150
+ is a hash containing `"bid"`, `"ask"`, and `"last"`.
151
+ * `on_fill order, amount, price` - Called when `order` is filled. `amount` is
152
+ the amount (usually the size of the order, but will be less if the order was
153
+ partially filled before), and `price` is the price that it was filled at.
154
+ * `on_partial_fill order, amount, price` - Same as `on_fill`, but this order is
155
+ still live in the market.
156
+ * `on_dividend amount` - Called when a dividend is received, `amount` is the
157
+ cash value of the dividend (which will be negative for short positions).
158
+
77
159
  ## Events
78
160
 
79
161
  ### Dividend
@@ -0,0 +1,37 @@
1
+ require 'ruby-trade'
2
+
3
+ TradeAmount = 20_000
4
+ NumOrders = 2000
5
+ InitialPrice = 10.0
6
+
7
+ class Slammer
8
+ include RubyTrade::Client
9
+
10
+ def self.on_connect *args
11
+ puts "Connected."
12
+
13
+ hit_it
14
+ end
15
+
16
+ def self.hit_it
17
+ @orders = (1..NumOrders).map do
18
+ buy 100, at: InitialPrice
19
+ end
20
+
21
+ EM.add_timer 1 do
22
+ @orders.each do |order|
23
+ order.cancel!
24
+ end
25
+
26
+ EM.add_timer 0.5 do
27
+ hit_it
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.on_tick level1
33
+ @level1 = level1
34
+ end
35
+ end
36
+
37
+ Slammer.connect_to "127.0.0.1", as: "Slammer", ai: true
data/lib/client.rb CHANGED
@@ -3,12 +3,15 @@ require 'json'
3
3
  require 'em-zeromq'
4
4
 
5
5
  require_relative "order"
6
+ require_relative "../server/common"
6
7
 
7
8
  DEFAULT_FEED_PORT = 9000
8
9
  DEFAULT_ORDER_PORT = 9001
9
10
 
10
11
  module RubyTrade
11
12
  module ConnectionClient
13
+ include LineCleaner
14
+
12
15
  def self.setup args, parent
13
16
  @@username = args[:as]
14
17
  @@ai = args[:ai] || false
@@ -25,6 +28,7 @@ module RubyTrade
25
28
 
26
29
  send_data_f data
27
30
 
31
+ @buffer = ""
28
32
  @order_no = 0
29
33
  @orders = {}
30
34
  @cash, @stock = 0, 0
@@ -83,15 +87,6 @@ module RubyTrade
83
87
  send_data "\x02#{data}\x03"
84
88
  end
85
89
 
86
- # Strip off begin/end transmission tokens
87
- def clean data
88
- if data.length > 2
89
- data[1..-2].split("\x03\x02")
90
- else
91
- []
92
- end
93
- end
94
-
95
90
  # Called by EM when we receive data
96
91
  def receive_data data
97
92
  clean(data).each do |msg|
@@ -132,6 +127,9 @@ module RubyTrade
132
127
  @connect_triggered = true
133
128
  @@parent.on_connect
134
129
  end
130
+ when "dividend"
131
+ @cash += data["value"]
132
+ @@parent.on_dividend data["value"]
135
133
  end
136
134
  end
137
135
  end
@@ -142,6 +140,7 @@ module RubyTrade
142
140
  def on_tick *args; end
143
141
  def on_fill *args; end
144
142
  def on_partial_fill *args; end
143
+ def on_dividend *args; end
145
144
 
146
145
  # hook so we can call child methods
147
146
  def child= child
data/lib/order.rb CHANGED
@@ -3,7 +3,7 @@ require 'observer'
3
3
  class Order
4
4
  include Observable
5
5
 
6
- attr_reader :id, :local_id, :side, :size, :sent_at, :status
6
+ attr_reader :id, :side, :size, :sent_at, :status
7
7
  attr_accessor :price, :status
8
8
 
9
9
  def initialize id, side, price, size
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "ruby-trade"
3
- s.version = "0.2"
3
+ s.version = "0.3"
4
4
  s.date = "2013-11-09"
5
5
  s.summary = "A stock market simulation game."
6
6
  s.description = ""
data/server/account.rb CHANGED
@@ -11,11 +11,20 @@ class Account
11
11
  @ai = false
12
12
  end
13
13
 
14
+ def process_dividend amount
15
+ value = stock * amount
16
+
17
+ @cash += value
18
+
19
+ changed
20
+ notify_observers :dividend, {amount: amount, value: value}
21
+ end
22
+
14
23
  def update_name name
15
24
  if @name != name
16
25
  @name = name
17
26
  changed
18
- notify_observers
27
+ notify_observers :name_change, name
19
28
  end
20
29
  end
21
30
 
data/server/common.rb ADDED
@@ -0,0 +1,22 @@
1
+ module LineCleaner
2
+ def clean data
3
+ @buffer ||= ""
4
+ @buffer += data
5
+
6
+ msgs = []
7
+
8
+ while @buffer.length > 0
9
+ next_close = @buffer.index "\x03"
10
+
11
+ break if next_close.nil?
12
+
13
+ next_piece = @buffer[1...next_close]
14
+
15
+ msgs << next_piece
16
+
17
+ @buffer = @buffer[(next_close + 1)..-1]
18
+ end
19
+
20
+ msgs
21
+ end
22
+ end
data/server/exchange.rb CHANGED
@@ -3,8 +3,10 @@ require_relative 'order-book'
3
3
  require_relative 'order'
4
4
  require_relative 'server'
5
5
 
6
- STARTING_EQUITY = 0
7
- STARTING_CASH = 10_000
6
+ StartingEquity = 0
7
+ StartingCash = 10_000
8
+ DividendAmount = 0.25
9
+ DividendFrequency = 600
8
10
 
9
11
  class Exchange
10
12
  def initialize
@@ -14,8 +16,19 @@ class Exchange
14
16
  @book = OrderBook.new
15
17
  end
16
18
 
19
+ def accounts
20
+ @accounts.values
21
+ end
22
+
23
+ # Pay dividends to all accounts
24
+ def pay_dividends
25
+ @accounts.values.each do |account|
26
+ account.process_dividend DividendAmount
27
+ end
28
+ end
29
+
17
30
  def identify data
18
- account = @accounts[data["peer_name"]] || Account.new(data["peer_name"], data["name"], STARTING_EQUITY, STARTING_CASH)
31
+ account = @accounts[data["peer_name"]] || Account.new(data["peer_name"], data["name"], StartingEquity, StartingCash)
19
32
 
20
33
  account.ai = data["ai"]
21
34
  account.update_name data["name"]
data/server/public/app.js CHANGED
@@ -26,6 +26,13 @@ $(function () {
26
26
  $(".ask").html(data.level1.ask.toFixed(2));
27
27
  $(".last").html(data.level1.last.toFixed(2));
28
28
  break;
29
+ case "accounts":
30
+ $("#leaderboard tbody").html(
31
+ $.map(data.accounts, function (obj) {
32
+ return "<tr><td>" + obj.join("</td><td>") + "</td></tr>";
33
+ }).join("")
34
+ );
35
+ break;
29
36
  }
30
37
  };
31
38
 
@@ -37,11 +37,19 @@
37
37
  </div>
38
38
  </div>
39
39
  <div class = "row-fluid">
40
- <div class = "col-md-6">
41
- <div class = "pull-right">
42
- <label>Last</label>
43
- <div class = "last">0.00</div>
44
- </div>
40
+ <div class = "col-md-2">&nbsp;</div>
41
+ <div class = "col-md-8">
42
+ <table class = "table table-bordered table-striped" id = "leaderboard">
43
+ <thead>
44
+ <tr>
45
+ <th>Name</th>
46
+ <th>Value</th>
47
+ <th>Cash</th>
48
+ <th>Stock</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody></tbody>
52
+ </table>
45
53
  </div>
46
54
  </div>
47
55
  </div>
data/server/server.rb CHANGED
@@ -3,14 +3,36 @@ require 'json'
3
3
 
4
4
  require_relative 'exchange'
5
5
  require_relative 'web_server'
6
+ require_relative 'common'
7
+
8
+ AccountUpdateFrequency = 30
6
9
 
7
10
  class OrderServer < EM::Connection
11
+ include LineCleaner
12
+
8
13
  def self.setup parent
9
14
  @@exchange = Exchange.new
10
15
  @@parent = parent
16
+
17
+ EM.add_periodic_timer DividendFrequency do
18
+ @@exchange.pay_dividends
19
+ end
20
+
21
+ EM.add_periodic_timer AccountUpdateFrequency do
22
+ level1 = @@exchange.level1
23
+ Webapp.update_accounts(@@exchange.accounts.select { |account|
24
+ #not account.ai?
25
+ true
26
+ }.map { |account|
27
+ [account.name, account.net_value(level1[:last]), account.stock, account.cash]
28
+ }.sort_by { |row|
29
+ row[3]
30
+ })
31
+ end
11
32
  end
12
33
 
13
34
  def post_init
35
+ @buffer = ""
14
36
  @my_orders = {}
15
37
  end
16
38
 
@@ -52,20 +74,17 @@ class OrderServer < EM::Connection
52
74
  local_id: order.local_id
53
75
  }.to_json)
54
76
  @@parent.tick @@exchange
77
+ when :dividend
78
+ send_data_f({
79
+ action: "dividend",
80
+ value: args[0][:value]
81
+ }.to_json)
55
82
  end
56
83
  end
57
84
 
58
85
  def send_data_f data
59
86
  send_data "\x02#{data}\x03"
60
87
  end
61
-
62
- def clean data
63
- if data.length > 2
64
- data[1..-2].split("\x03\x02")
65
- else
66
- []
67
- end
68
- end
69
88
 
70
89
  def handle_message data
71
90
  case data["action"]
@@ -73,15 +92,17 @@ class OrderServer < EM::Connection
73
92
  _, ip = Socket.unpack_sockaddr_in get_peername
74
93
  data["peer_name"] = ip
75
94
  puts "User #{data['name']}@#{data["peer_name"]} connected."
95
+
96
+ @account.delete_observer self if @account
76
97
  @account = @@exchange.identify data
98
+ @account.add_observer self
99
+
77
100
  send_data_f({
78
101
  action: "account_update",
79
102
  cash: @account.cash,
80
103
  stock: @account.stock
81
104
  }.to_json)
82
105
  when "new_order"
83
- puts "new order"
84
-
85
106
  error, order = @@exchange.new_order @account, data
86
107
 
87
108
  if error
@@ -102,14 +123,13 @@ class OrderServer < EM::Connection
102
123
  price: order.price
103
124
  }.to_json)
104
125
 
105
- @my_orders[order.id] = order
126
+ @my_orders[order.local_id] = order
106
127
  @@exchange.send_order order
107
128
  @@parent.tick @@exchange
108
129
  end
109
130
  when "cancel_order"
110
131
  order = @my_orders[data["id"]]
111
132
  if order
112
- puts "cancelling #{data["id"]}"
113
133
  @@exchange.cancel_order order
114
134
  @@parent.tick @@exchange
115
135
  else
@@ -127,15 +147,13 @@ end
127
147
 
128
148
  class Server
129
149
  def tick exchange
130
- puts "sending tick"
131
150
  msg = {
132
151
  action: "tick",
133
152
  level1: exchange.level1
134
153
  }
135
154
  Webapp.level1_update exchange.level1
136
155
 
137
- puts msg.inspect
138
- puts @feed_socket.send_msg(msg.to_json)
156
+ @feed_socket.send_msg(msg.to_json)
139
157
  end
140
158
 
141
159
  def start args = {}
@@ -145,9 +163,9 @@ class Server
145
163
 
146
164
  @context = EM::ZeroMQ::Context.new 1
147
165
 
148
- OrderServer.setup self
149
-
150
166
  EM.run do
167
+ OrderServer.setup self
168
+
151
169
  puts "Listening for clients on #{order_port}"
152
170
  EM.start_server "0.0.0.0", order_port, OrderServer
153
171
 
@@ -0,0 +1,76 @@
1
+ require_relative "../common"
2
+
3
+ class Cleaner
4
+ include LineCleaner
5
+ end
6
+
7
+ describe LineCleaner do
8
+ before :each do
9
+ @cleaner = Cleaner.new
10
+ end
11
+
12
+ it "should work with a basic message" do
13
+ res = @cleaner.clean "\x02this is a message\x03"
14
+
15
+ res.length.should == 1
16
+ res[0].should == "this is a message"
17
+ end
18
+
19
+ it "should split multiple messages" do
20
+ res = @cleaner.clean "\x02this is a message\x03\x02this is another message\x03\x02this is a third message\x03"
21
+
22
+ res.length.should == 3
23
+ res[0].should == "this is a message"
24
+ res[1].should == "this is another message"
25
+ res[2].should == "this is a third message"
26
+ end
27
+
28
+ it "should carry through different transmissions" do
29
+ res = @cleaner.clean "\x02this is a message\x03\x02this is a second"
30
+
31
+ res.length.should == 1
32
+ res[0].should == "this is a message"
33
+
34
+ res = @cleaner.clean " message that has been chopped in half\x03\x02this is a final message\x03"
35
+
36
+ res.length.should == 2
37
+ res[0].should == "this is a second message that has been chopped in half"
38
+ res[1].should == "this is a final message"
39
+ end
40
+
41
+ it "should buffer a big message" do
42
+ res = @cleaner.clean "\x02this is a message that does not"
43
+
44
+ res.length.should == 0
45
+
46
+ res = @cleaner.clean " end until later\x03"
47
+
48
+ res.length.should == 1
49
+ res[0].should == "this is a message that does not end until later"
50
+ end
51
+
52
+ it "should not split on end of string" do
53
+ res = @cleaner.clean "\x02this is a message with a split at the end\x03\x02"
54
+
55
+ res.length.should == 1
56
+ res[0].should == "this is a message with a split at the end"
57
+
58
+ res = @cleaner.clean "and here is the end\x03"
59
+
60
+ res.length.should == 1
61
+ res[0].should == "and here is the end"
62
+ end
63
+
64
+ it "should not split at the start of the string" do
65
+ res = @cleaner.clean "\x02this is the message with the split at the start"
66
+
67
+ res.length.should == 0
68
+
69
+ res = @cleaner.clean "\x03\x02and here is the second bit.\x03"
70
+
71
+ res.length.should == 2
72
+ res[0].should == "this is the message with the split at the start"
73
+ res[1].should == "and here is the second bit."
74
+ end
75
+ end
76
+
data/server/web_server.rb CHANGED
@@ -2,14 +2,13 @@ require "sinatra/base"
2
2
  require "sinatra-websocket"
3
3
  require "thin"
4
4
  require "rack"
5
- require "rack/sockjs"
6
5
  require 'observer'
7
6
  require 'json'
8
7
 
9
8
  class Level1
10
9
  include Observable
11
10
 
12
- attr_reader :level1
11
+ attr_reader :level1, :accounts
13
12
 
14
13
  def initialize
15
14
  @level1 = {
@@ -24,6 +23,12 @@ class Level1
24
23
  changed
25
24
  notify_observers :level1, level1
26
25
  end
26
+
27
+ def update_accounts accounts
28
+ @accounts = accounts
29
+ changed
30
+ notify_observers :accounts, accounts
31
+ end
27
32
  end
28
33
 
29
34
  class SocketWrapper
@@ -44,6 +49,11 @@ class SocketWrapper
44
49
  action: action,
45
50
  level1: data[0]
46
51
  }.to_json)
52
+ when :accounts
53
+ @socket.send({
54
+ action: action,
55
+ accounts: data[0]
56
+ }.to_json)
47
57
  end
48
58
  end
49
59
  end
@@ -88,6 +98,10 @@ class Webapp < Sinatra::Base
88
98
  def self.level1_update level1
89
99
  @@level1.update_level1 level1
90
100
  end
101
+
102
+ def self.update_accounts accounts
103
+ @@level1.update_accounts accounts
104
+ end
91
105
  end
92
106
 
93
107
  def run_webserver opts
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-trade
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -21,16 +21,18 @@ files:
21
21
  - README.md
22
22
  - examples/market_maker.rb
23
23
  - examples/slammer.rb
24
+ - examples/stress.rb
24
25
  - lib/Gemfile
25
26
  - lib/Gemfile.lock
26
27
  - lib/client.rb
27
28
  - lib/order.rb
28
29
  - lib/ruby-trade.rb
29
- - rubytrade.gemspec
30
+ - ruby-trade.gemspec
30
31
  - server/Gemfile
31
32
  - server/Gemfile.lock
32
33
  - server/account.rb
33
34
  - server/app.rb
35
+ - server/common.rb
34
36
  - server/exchange.rb
35
37
  - server/order-book.rb
36
38
  - server/order.rb
@@ -159,6 +161,7 @@ files:
159
161
  - server/public/jquery-2.0.3.min.js
160
162
  - server/public/sockjs-0.2.1.min.js
161
163
  - server/server.rb
164
+ - server/test/test_common.rb
162
165
  - server/test/test_order_book.rb
163
166
  - server/web_server.rb
164
167
  homepage: