diff --git a/lib/rex.rb b/lib/rex.rb index d46dd06..3460e1c 100644 --- a/lib/rex.rb +++ b/lib/rex.rb @@ -3,7 +3,9 @@ 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" module Rex class Error < StandardError; end diff --git a/lib/rex/matcher.rb b/lib/rex/matcher.rb new file mode 100644 index 0000000..1dc39ff --- /dev/null +++ b/lib/rex/matcher.rb @@ -0,0 +1,40 @@ +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_amount = [highest_buy_order.remaining_amount, lowest_sell_order.remaining_amount].min + highest_buy_order.remaining_amount -= max_amount + lowest_sell_order.remaining_amount -= max_amount + + trades << Trade.new( + id: order_book.next_trade_id, + buy_order: highest_buy_order, + sell_order: lowest_sell_order, + amount: max_amount, + price: lowest_sell_order.price + ) + + remove_if_filled(highest_buy_order, order_book) + remove_if_filled(lowest_sell_order, order_book) + + # 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 + end + + private + + def remove_if_filled(order, order_book) + return unless order.filled? + order_book.remove_order(order.id) + end + end +end diff --git a/lib/rex/order_book.rb b/lib/rex/order_book.rb index ed2c124..2b38845 100644 --- a/lib/rex/order_book.rb +++ b/lib/rex/order_book.rb @@ -6,6 +6,7 @@ module Rex @sell_side = RBTree.new @buy_side = RBTree.new @order_ids = {} # order_id => order + @current_trade_id = 0 end def add_order(order) @@ -49,6 +50,10 @@ module Rex sell_side.first&.[](0) end + def next_trade_id + @current_trade_id += 1 + end + private attr_reader :order_ids, :buy_side, :sell_side diff --git a/lib/rex/trade.rb b/lib/rex/trade.rb new file mode 100644 index 0000000..6c9db0d --- /dev/null +++ b/lib/rex/trade.rb @@ -0,0 +1,19 @@ +module Rex + class Trade + attr_accessor( + :id, + :buy_order, + :sell_order, + :amount, + :price + ) + + def initialize(attributes = {}) + @id = attributes[:id] + @buy_order = attributes[:buy_order] + @sell_order = attributes[:sell_order] + @amount = attributes[:amount] + @price = attributes[:price] + end + end +end diff --git a/spec/matcher_spec.rb b/spec/matcher_spec.rb new file mode 100644 index 0000000..65a2ca5 --- /dev/null +++ b/spec/matcher_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe Rex::Matcher do + let(:instance) { described_class.new } + + describe "#match" do + let(:order_book) { Rex::OrderBook.new } + let(:buy_order) { build(:order, price: 100, is_buy: true, amount: 100, remaining_amount: 100) } + let(:cheaper_sell_order) { build(:order, price: 99, is_buy: false, amount: 50, remaining_amount: 50) } + let(:pricier_sell_order) { build(:order, price: 100, is_buy: false, amount: 70, remaining_amount: 70) } + + + context "when order book has unmatched orders" do + before do + order_book.add_order(buy_order) + order_book.add_order(cheaper_sell_order) + order_book.add_order(pricier_sell_order) + end + + it "returns proper trades" do + trades = instance.match(order_book) + + expect(trades.length).to eq(2) + + expect(trades[0].id).to eq(1) + expect(trades[0].buy_order).to eq(buy_order) + expect(trades[0].sell_order).to eq(cheaper_sell_order) + expect(trades[0].price).to eq(99) + expect(trades[0].amount).to eq(50) + + expect(trades[1].id).to eq(2) + expect(trades[1].buy_order).to eq(buy_order) + expect(trades[1].sell_order).to eq(pricier_sell_order) + expect(trades[1].price).to eq(100) + expect(trades[1].amount).to eq(50) + end + + it "removes filled orders from the order book" do + instance.match(order_book) + + expect(order_book.highest_buy_order).to eq(nil) + expect(order_book.lowest_sell_order).to eq(pricier_sell_order) + expect(order_book.lowest_sell_order.remaining_amount).to eq(20) + end + end + + context "when order book is empty" do + it "returns an empty list" do + expect(instance.match(Rex::OrderBook.new)).to eq([]) + end + end + end +end diff --git a/spec/order_book_spec.rb b/spec/order_book_spec.rb index cdafc0f..f7f869e 100644 --- a/spec/order_book_spec.rb +++ b/spec/order_book_spec.rb @@ -94,4 +94,12 @@ RSpec.describe Rex::OrderBook do end end end + + describe "#next_trade_id" do + it 'returns an increasing trade id' do + expect(instance.next_trade_id).to eq(1) + expect(instance.next_trade_id).to eq(2) + expect(instance.next_trade_id).to eq(3) + end + end end