Move order book to Rex::Book namespace
This commit is contained in:
parent
0efb6b0e88
commit
509b00bb14
10
lib/rex.rb
10
lib/rex.rb
@ -1,11 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "rex/version"
|
||||
require_relative "rex/limit"
|
||||
require_relative "rex/order"
|
||||
require_relative "rex/trade"
|
||||
require_relative "rex/order_book"
|
||||
require_relative "rex/matcher"
|
||||
require_relative "rex/book/limit"
|
||||
require_relative "rex/book/order"
|
||||
require_relative "rex/book/trade"
|
||||
require_relative "rex/book/order_book"
|
||||
require_relative "rex/book/matcher"
|
||||
|
||||
module Rex
|
||||
class Error < StandardError; end
|
||||
|
81
lib/rex/book/limit.rb
Normal file
81
lib/rex/book/limit.rb
Normal file
@ -0,0 +1,81 @@
|
||||
module Rex
|
||||
module Book
|
||||
class Limit
|
||||
attr_reader(
|
||||
:first_order,
|
||||
:last_order,
|
||||
:price
|
||||
)
|
||||
def initialize(price)
|
||||
@price = price
|
||||
@first_order = nil
|
||||
@last_order = nil
|
||||
@order_count = 0
|
||||
end
|
||||
|
||||
def peek_first_order
|
||||
@first_order
|
||||
end
|
||||
|
||||
def count
|
||||
@order_count
|
||||
end
|
||||
|
||||
def pop_first_order
|
||||
return nil if empty?
|
||||
|
||||
order = @first_order
|
||||
@first_order = @first_order.next_order
|
||||
@first_order&.previous_order = nil
|
||||
|
||||
if @first_order.nil?
|
||||
@last_order = nil
|
||||
end
|
||||
|
||||
order.next_order = nil
|
||||
order.previous_order = nil
|
||||
|
||||
@order_count -= 1
|
||||
order
|
||||
end
|
||||
|
||||
def add_order(order)
|
||||
if empty?
|
||||
@first_order = order
|
||||
else
|
||||
@last_order.next_order = order
|
||||
order.previous_order = @last_order
|
||||
end
|
||||
@last_order = order
|
||||
@order_count += 1
|
||||
end
|
||||
|
||||
# Assumption when calling: the order is part of the limit
|
||||
def remove_order(order)
|
||||
if @first_order == order
|
||||
@first_order = order.next_order
|
||||
end
|
||||
|
||||
if @last_order == order
|
||||
@last_order = order.previous_order
|
||||
end
|
||||
|
||||
previous_order = order.previous_order
|
||||
next_order = order.next_order
|
||||
|
||||
previous_order&.next_order = next_order
|
||||
next_order&.previous_order = previous_order
|
||||
|
||||
order.previous_order = nil
|
||||
order.next_order = nil
|
||||
@order_count -= 1
|
||||
|
||||
order
|
||||
end
|
||||
|
||||
def empty?
|
||||
@order_count == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
lib/rex/book/matcher.rb
Normal file
41
lib/rex/book/matcher.rb
Normal file
@ -0,0 +1,41 @@
|
||||
module Rex
|
||||
module Book
|
||||
class Matcher
|
||||
def match(order_book)
|
||||
trades = []
|
||||
highest_buy_order = order_book.highest_buy_order
|
||||
lowest_sell_order = order_book.lowest_sell_order
|
||||
return trades if highest_buy_order.nil? || lowest_sell_order.nil?
|
||||
|
||||
while highest_buy_order.price >= lowest_sell_order.price
|
||||
max_quantity = min(highest_buy_order.remaining_quantity, lowest_sell_order.remaining_quantity)
|
||||
trade = Trade.new(
|
||||
id: order_book.next_trade_id,
|
||||
buy_order: highest_buy_order,
|
||||
sell_order: lowest_sell_order,
|
||||
quantity: max_quantity,
|
||||
price: lowest_sell_order.price
|
||||
)
|
||||
|
||||
order_book.process_trade(trade)
|
||||
trades.push(trade)
|
||||
|
||||
# Go for the next run
|
||||
highest_buy_order = order_book.highest_buy_order
|
||||
lowest_sell_order = order_book.lowest_sell_order
|
||||
|
||||
return trades if highest_buy_order.nil? || lowest_sell_order.nil?
|
||||
end
|
||||
|
||||
trades
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def min(a, b)
|
||||
return a if a < b
|
||||
b
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
33
lib/rex/book/order.rb
Normal file
33
lib/rex/book/order.rb
Normal file
@ -0,0 +1,33 @@
|
||||
module Rex
|
||||
module Book
|
||||
class Order
|
||||
attr_accessor(
|
||||
:id,
|
||||
:is_buy,
|
||||
:price,
|
||||
:user_id,
|
||||
:quantity,
|
||||
:previous_order,
|
||||
:next_order,
|
||||
:remaining_quantity
|
||||
)
|
||||
|
||||
def initialize(attrs = {})
|
||||
@id = attrs[:id]
|
||||
|
||||
@user_id = attrs[:user_id]
|
||||
@price = attrs[:price]
|
||||
@is_buy = attrs[:is_buy]
|
||||
@quantity = attrs[:quantity]
|
||||
@remaining_quantity = attrs[:quantity]
|
||||
|
||||
@next_order = nil
|
||||
@previous_order = nil
|
||||
end
|
||||
|
||||
def filled?
|
||||
remaining_quantity == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
96
lib/rex/book/order_book.rb
Normal file
96
lib/rex/book/order_book.rb
Normal file
@ -0,0 +1,96 @@
|
||||
require "rbtree"
|
||||
|
||||
module Rex
|
||||
module Book
|
||||
class OrderBook
|
||||
def initialize(matcher: Matcher.new)
|
||||
@matcher = matcher
|
||||
@sell_side = RBTree.new
|
||||
@buy_side = RBTree.new
|
||||
@order_ids = {} # order_id => order
|
||||
@current_trade_id = 0
|
||||
@current_order_id = 0
|
||||
end
|
||||
|
||||
def add_order(order)
|
||||
side = side_for_order(order)
|
||||
order.id = next_order_id
|
||||
|
||||
side[order.price] ||= Limit.new(order.price)
|
||||
side[order.price].add_order(order)
|
||||
|
||||
order_ids[order.id] = order
|
||||
end
|
||||
|
||||
def add_and_match_order(order)
|
||||
add_order(order)
|
||||
@matcher.match(self)
|
||||
end
|
||||
|
||||
def remove_order(order_id)
|
||||
order = order_ids[order_id]
|
||||
return nil if order.nil?
|
||||
|
||||
side = side_for_order(order)
|
||||
limit = side[order.price]
|
||||
limit.remove_order(order)
|
||||
|
||||
if limit.empty?
|
||||
side.delete(limit.price)
|
||||
end
|
||||
|
||||
order_ids.delete(order.id)
|
||||
end
|
||||
|
||||
alias_method :cancel_order, :remove_order
|
||||
|
||||
def process_trade(trade)
|
||||
trade.buy_order.remaining_quantity -= trade.quantity
|
||||
trade.sell_order.remaining_quantity -= trade.quantity
|
||||
|
||||
remove_order(trade.buy_order.id) if trade.buy_order.filled?
|
||||
remove_order(trade.sell_order.id) if trade.sell_order.filled?
|
||||
end
|
||||
|
||||
def highest_buy_order
|
||||
buy_side.last&.[](1)&.peek_first_order
|
||||
end
|
||||
|
||||
def lowest_sell_order
|
||||
sell_side.first&.[](1)&.peek_first_order
|
||||
end
|
||||
|
||||
def best_buy_price
|
||||
buy_side.last&.[](0)
|
||||
end
|
||||
|
||||
def best_sell_price
|
||||
sell_side.first&.[](0)
|
||||
end
|
||||
|
||||
def next_trade_id
|
||||
@current_trade_id += 1
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader(
|
||||
:order_ids,
|
||||
:buy_side,
|
||||
:sell_side
|
||||
)
|
||||
|
||||
def next_order_id
|
||||
@current_order_id += 1
|
||||
end
|
||||
|
||||
def side_for_order(order)
|
||||
if order.is_buy
|
||||
buy_side
|
||||
else
|
||||
sell_side
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
lib/rex/book/trade.rb
Normal file
21
lib/rex/book/trade.rb
Normal file
@ -0,0 +1,21 @@
|
||||
module Rex
|
||||
module Book
|
||||
class Trade
|
||||
attr_accessor(
|
||||
:id,
|
||||
:buy_order,
|
||||
:sell_order,
|
||||
:quantity,
|
||||
:price
|
||||
)
|
||||
|
||||
def initialize(attributes = {})
|
||||
@id = attributes[:id]
|
||||
@buy_order = attributes[:buy_order]
|
||||
@sell_order = attributes[:sell_order]
|
||||
@quantity = attributes[:quantity]
|
||||
@price = attributes[:price]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,79 +0,0 @@
|
||||
module Rex
|
||||
class Limit
|
||||
attr_reader(
|
||||
:first_order,
|
||||
:last_order,
|
||||
:price
|
||||
)
|
||||
def initialize(price)
|
||||
@price = price
|
||||
@first_order = nil
|
||||
@last_order = nil
|
||||
@order_count = 0
|
||||
end
|
||||
|
||||
def peek_first_order
|
||||
@first_order
|
||||
end
|
||||
|
||||
def count
|
||||
@order_count
|
||||
end
|
||||
|
||||
def pop_first_order
|
||||
return nil if empty?
|
||||
|
||||
order = @first_order
|
||||
@first_order = @first_order.next_order
|
||||
@first_order&.previous_order = nil
|
||||
|
||||
if @first_order.nil?
|
||||
@last_order = nil
|
||||
end
|
||||
|
||||
order.next_order = nil
|
||||
order.previous_order = nil
|
||||
|
||||
@order_count -= 1
|
||||
order
|
||||
end
|
||||
|
||||
def add_order(order)
|
||||
if empty?
|
||||
@first_order = order
|
||||
else
|
||||
@last_order.next_order = order
|
||||
order.previous_order = @last_order
|
||||
end
|
||||
@last_order = order
|
||||
@order_count += 1
|
||||
end
|
||||
|
||||
# Assumption when calling: the order is part of the limit
|
||||
def remove_order(order)
|
||||
if @first_order == order
|
||||
@first_order = order.next_order
|
||||
end
|
||||
|
||||
if @last_order == order
|
||||
@last_order = order.previous_order
|
||||
end
|
||||
|
||||
previous_order = order.previous_order
|
||||
next_order = order.next_order
|
||||
|
||||
previous_order&.next_order = next_order
|
||||
next_order&.previous_order = previous_order
|
||||
|
||||
order.previous_order = nil
|
||||
order.next_order = nil
|
||||
@order_count -= 1
|
||||
|
||||
order
|
||||
end
|
||||
|
||||
def empty?
|
||||
@order_count == 0
|
||||
end
|
||||
end
|
||||
end
|
@ -1,39 +0,0 @@
|
||||
module Rex
|
||||
class Matcher
|
||||
def match(order_book)
|
||||
trades = []
|
||||
highest_buy_order = order_book.highest_buy_order
|
||||
lowest_sell_order = order_book.lowest_sell_order
|
||||
return trades if highest_buy_order.nil? || lowest_sell_order.nil?
|
||||
|
||||
while highest_buy_order.price >= lowest_sell_order.price
|
||||
max_quantity = min(highest_buy_order.remaining_quantity, lowest_sell_order.remaining_quantity)
|
||||
trade = Trade.new(
|
||||
id: order_book.next_trade_id,
|
||||
buy_order: highest_buy_order,
|
||||
sell_order: lowest_sell_order,
|
||||
quantity: max_quantity,
|
||||
price: lowest_sell_order.price
|
||||
)
|
||||
|
||||
order_book.process_trade(trade)
|
||||
trades.push(trade)
|
||||
|
||||
# Go for the next run
|
||||
highest_buy_order = order_book.highest_buy_order
|
||||
lowest_sell_order = order_book.lowest_sell_order
|
||||
|
||||
return trades if highest_buy_order.nil? || lowest_sell_order.nil?
|
||||
end
|
||||
|
||||
trades
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def min(a, b)
|
||||
return a if a < b
|
||||
b
|
||||
end
|
||||
end
|
||||
end
|
@ -1,31 +0,0 @@
|
||||
module Rex
|
||||
class Order
|
||||
attr_accessor(
|
||||
:id,
|
||||
:is_buy,
|
||||
:price,
|
||||
:user_id,
|
||||
:quantity,
|
||||
:previous_order,
|
||||
:next_order,
|
||||
:remaining_quantity
|
||||
)
|
||||
|
||||
def initialize(attrs = {})
|
||||
@id = attrs[:id]
|
||||
|
||||
@user_id = attrs[:user_id]
|
||||
@price = attrs[:price]
|
||||
@is_buy = attrs[:is_buy]
|
||||
@quantity = attrs[:quantity]
|
||||
@remaining_quantity = attrs[:quantity]
|
||||
|
||||
@next_order = nil
|
||||
@previous_order = nil
|
||||
end
|
||||
|
||||
def filled?
|
||||
remaining_quantity == 0
|
||||
end
|
||||
end
|
||||
end
|
@ -1,94 +0,0 @@
|
||||
require "rbtree"
|
||||
|
||||
module Rex
|
||||
class OrderBook
|
||||
def initialize(matcher: Matcher.new)
|
||||
@matcher = matcher
|
||||
@sell_side = RBTree.new
|
||||
@buy_side = RBTree.new
|
||||
@order_ids = {} # order_id => order
|
||||
@current_trade_id = 0
|
||||
@current_order_id = 0
|
||||
end
|
||||
|
||||
def add_order(order)
|
||||
side = side_for_order(order)
|
||||
order.id = next_order_id
|
||||
|
||||
side[order.price] ||= Limit.new(order.price)
|
||||
side[order.price].add_order(order)
|
||||
|
||||
order_ids[order.id] = order
|
||||
end
|
||||
|
||||
def add_and_match_order(order)
|
||||
add_order(order)
|
||||
@matcher.match(self)
|
||||
end
|
||||
|
||||
def remove_order(order_id)
|
||||
order = order_ids[order_id]
|
||||
return nil if order.nil?
|
||||
|
||||
side = side_for_order(order)
|
||||
limit = side[order.price]
|
||||
limit.remove_order(order)
|
||||
|
||||
if limit.empty?
|
||||
side.delete(limit.price)
|
||||
end
|
||||
|
||||
order_ids.delete(order.id)
|
||||
end
|
||||
|
||||
alias_method :cancel_order, :remove_order
|
||||
|
||||
def process_trade(trade)
|
||||
trade.buy_order.remaining_quantity -= trade.quantity
|
||||
trade.sell_order.remaining_quantity -= trade.quantity
|
||||
|
||||
remove_order(trade.buy_order.id) if trade.buy_order.filled?
|
||||
remove_order(trade.sell_order.id) if trade.sell_order.filled?
|
||||
end
|
||||
|
||||
def highest_buy_order
|
||||
buy_side.last&.[](1)&.peek_first_order
|
||||
end
|
||||
|
||||
def lowest_sell_order
|
||||
sell_side.first&.[](1)&.peek_first_order
|
||||
end
|
||||
|
||||
def best_buy_price
|
||||
buy_side.last&.[](0)
|
||||
end
|
||||
|
||||
def best_sell_price
|
||||
sell_side.first&.[](0)
|
||||
end
|
||||
|
||||
def next_trade_id
|
||||
@current_trade_id += 1
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader(
|
||||
:order_ids,
|
||||
:buy_side,
|
||||
:sell_side
|
||||
)
|
||||
|
||||
def next_order_id
|
||||
@current_order_id += 1
|
||||
end
|
||||
|
||||
def side_for_order(order)
|
||||
if order.is_buy
|
||||
buy_side
|
||||
else
|
||||
sell_side
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,19 +0,0 @@
|
||||
module Rex
|
||||
class Trade
|
||||
attr_accessor(
|
||||
:id,
|
||||
:buy_order,
|
||||
:sell_order,
|
||||
:quantity,
|
||||
:price
|
||||
)
|
||||
|
||||
def initialize(attributes = {})
|
||||
@id = attributes[:id]
|
||||
@buy_order = attributes[:buy_order]
|
||||
@sell_order = attributes[:sell_order]
|
||||
@quantity = attributes[:quantity]
|
||||
@price = attributes[:price]
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Rex::Limit do
|
||||
RSpec.describe Rex::Book::Limit do
|
||||
let(:instance) { described_class.new(100) }
|
||||
let(:order_a) { build(:order) }
|
||||
let(:order_b) { build(:order) }
|
@ -1,10 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Rex::Matcher do
|
||||
RSpec.describe Rex::Book::Matcher do
|
||||
let(:instance) { described_class.new }
|
||||
|
||||
describe "#match" do
|
||||
let(:order_book) { Rex::OrderBook.new }
|
||||
let(:order_book) { Rex::Book::OrderBook.new }
|
||||
let(:buy_order) { build(:order, price: 100, is_buy: true, quantity: 100, remaining_quantity: 100) }
|
||||
let(:cheaper_sell_order) { build(:order, price: 99, is_buy: false, quantity: 50, remaining_quantity: 50) }
|
||||
let(:pricier_sell_order) { build(:order, price: 100, is_buy: false, quantity: 70, remaining_quantity: 70) }
|
||||
@ -45,7 +45,7 @@ RSpec.describe Rex::Matcher do
|
||||
|
||||
context "when order book is empty" do
|
||||
it "returns an empty list" do
|
||||
expect(instance.match(Rex::OrderBook.new)).to eq([])
|
||||
expect(instance.match(Rex::Book::OrderBook.new)).to eq([])
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Rex::OrderBook do
|
||||
RSpec.describe Rex::Book::OrderBook do
|
||||
let(:instance) { described_class.new }
|
||||
|
||||
describe "#add_order" do
|
||||
@ -57,7 +57,7 @@ RSpec.describe Rex::OrderBook do
|
||||
|
||||
describe "add_and_match_order" do
|
||||
context "when matcher is given" do
|
||||
let(:matcher) { instance_double(Rex::Matcher) }
|
||||
let(:matcher) { instance_double(Rex::Book::Matcher) }
|
||||
let(:instance) { described_class.new(matcher: matcher) }
|
||||
let(:order) { build(:order, is_buy: true, price: 100) }
|
||||
|
||||
@ -93,7 +93,7 @@ RSpec.describe Rex::OrderBook do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#lowest_sell_Order" do
|
||||
describe "#lowest_sell_order" do
|
||||
context "when there is nothing in the book" do
|
||||
it "returns nil " do
|
||||
expect(instance.highest_buy_order).to eq(nil)
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Rex::Order do
|
||||
RSpec.describe Rex::Book::Order do
|
||||
describe "#filled?" do
|
||||
let(:order) do
|
||||
instance = build(:order)
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :limit, class: Rex::Limit do
|
||||
factory :limit, class: Rex::Book::Limit do
|
||||
transient do
|
||||
price { 100 }
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :order, class: Rex::Order do
|
||||
factory :order, class: Rex::Book::Order do
|
||||
sequence(:id) { |i| i }
|
||||
sequence(:user_id) { |i| i }
|
||||
sequence(:is_buy) { |i| (i % 2).zero? }
|
||||
|
Loading…
Reference in New Issue
Block a user