ruby-trade 0.2 → 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.
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: