Implement matching engine
This commit is contained in:
parent
7278b77702
commit
f29f429c3c
@ -6,6 +6,8 @@ require_relative "server/messages"
|
|||||||
|
|
||||||
require_relative "server/request_schemas"
|
require_relative "server/request_schemas"
|
||||||
require_relative "server/message_parser"
|
require_relative "server/message_parser"
|
||||||
|
|
||||||
|
require_relative "server/matching_engine"
|
||||||
require_relative "server/message_broker"
|
require_relative "server/message_broker"
|
||||||
module Rex
|
module Rex
|
||||||
module Server
|
module Server
|
||||||
|
195
lib/rex/server/matching_engine.rb
Normal file
195
lib/rex/server/matching_engine.rb
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
module Rex
|
||||||
|
module Server
|
||||||
|
class MatchingEngine
|
||||||
|
def initialize(inbox, outbox,
|
||||||
|
order_book: Rex::Book::LimitOrderBook.new,
|
||||||
|
limit_volume_tracker: Rex::Book::LimitVolumeTracker.new,
|
||||||
|
trade_tracker: Rex::Book::TradeTracker.new)
|
||||||
|
@inbox = inbox
|
||||||
|
@outbox = outbox
|
||||||
|
@order_book = order_book
|
||||||
|
@limit_volume_tracker = limit_volume_tracker
|
||||||
|
@trade_tracker = trade_tracker
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
@subscription_id = @inbox.subscribe do |message|
|
||||||
|
process_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop
|
||||||
|
return unless @subscription_id
|
||||||
|
|
||||||
|
@inbox.unsubscribe(@subscription_id)
|
||||||
|
@subscription_id = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_message(message)
|
||||||
|
case message
|
||||||
|
when Rex::Server::Messages::CreateOrderRequest
|
||||||
|
create_order(
|
||||||
|
Rex::Book::Order.new(
|
||||||
|
user_id: message.user_id,
|
||||||
|
price: message.price,
|
||||||
|
quantity: message.quantity,
|
||||||
|
is_buy: message.side == :buy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
when Rex::Server::Messages::CancelOrderRequest
|
||||||
|
cancel_order(message.order_id)
|
||||||
|
when Rex::Server::Messages::FetchOrderBookRequest
|
||||||
|
fetch_orderbook(message.user_id)
|
||||||
|
when Rex::Server::Messages::FetchTradesRequest
|
||||||
|
fetch_trades(message.user_id)
|
||||||
|
when Rex::Server::Messages::FetchOrdersRequest
|
||||||
|
fetch_orders(message.user_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_trades(user_id)
|
||||||
|
@trade_tracker.fetch_trades(50).each do |trade|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::TradeFetchEvent.new(
|
||||||
|
trade.id,
|
||||||
|
user_id,
|
||||||
|
trade.price,
|
||||||
|
trade.quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_orders(user_id)
|
||||||
|
@order_book.orders_for_user(user_id).each do |order|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderFetchEvent.new(
|
||||||
|
order.id,
|
||||||
|
order.user_id,
|
||||||
|
order_side(order),
|
||||||
|
order.quantity,
|
||||||
|
order.remaining_quantity,
|
||||||
|
order.price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_orderbook(user_id)
|
||||||
|
@limit_volume_tracker.each do |limit_volume_change|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderBookFetchEvent.new(
|
||||||
|
user_id,
|
||||||
|
limit_volume_change.side,
|
||||||
|
limit_volume_change.price,
|
||||||
|
limit_volume_change.quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_order(order)
|
||||||
|
@order_book.add_order(order)
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderCreatedEvent.new(
|
||||||
|
order.id,
|
||||||
|
order.user_id,
|
||||||
|
order_side(order),
|
||||||
|
order.quantity,
|
||||||
|
order.remaining_quantity,
|
||||||
|
order.price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
changes = @limit_volume_tracker.add_order(order)
|
||||||
|
|
||||||
|
changes.each do |limit_volume_change|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderBookUpdateEvent.new(
|
||||||
|
limit_volume_change.side,
|
||||||
|
limit_volume_change.price,
|
||||||
|
limit_volume_change.quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
trades = @order_book.match
|
||||||
|
|
||||||
|
trades.each do |trade|
|
||||||
|
@trade_tracker.add(trade)
|
||||||
|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::TradeEvent.new(
|
||||||
|
trade.id,
|
||||||
|
trade.price,
|
||||||
|
trade.quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderFillEvent.new(
|
||||||
|
trade.buy_order.id,
|
||||||
|
trade.buy_order.user_id,
|
||||||
|
trade.buy_order.price,
|
||||||
|
order_side(trade.buy_order),
|
||||||
|
trade.buy_order.remaining_quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderFillEvent.new(
|
||||||
|
trade.sell_order.id,
|
||||||
|
trade.sell_order.user_id,
|
||||||
|
trade.sell_order.price,
|
||||||
|
order_side(trade.sell_order),
|
||||||
|
trade.sell_order.remaining_quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@limit_volume_tracker.process_trade(trade).each do |limit_volume_change|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderBookUpdateEvent.new(
|
||||||
|
limit_volume_change.side,
|
||||||
|
limit_volume_change.price,
|
||||||
|
limit_volume_change.quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cancel_order(order_id)
|
||||||
|
order = @order_book.remove_order(order_id)
|
||||||
|
return unless order
|
||||||
|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderCancelledEvent.new(
|
||||||
|
order.id,
|
||||||
|
order.user_id,
|
||||||
|
order_side(order),
|
||||||
|
order.remaining_quantity,
|
||||||
|
order.price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
changes = @limit_volume_tracker.remove_order(order)
|
||||||
|
|
||||||
|
changes.each do |limit_volume_change|
|
||||||
|
@outbox.push(
|
||||||
|
Messages::OrderBookUpdateEvent.new(
|
||||||
|
limit_volume_change.side,
|
||||||
|
limit_volume_change.price,
|
||||||
|
limit_volume_change.quantity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def order_side(order)
|
||||||
|
order.is_buy ? :buy : :sell
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
223
spec/server/matching_engine_spec.rb
Normal file
223
spec/server/matching_engine_spec.rb
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
# require "eventmachine"
|
||||||
|
RSpec.describe Rex::Server::MatchingEngine do
|
||||||
|
let(:inbox) { MockQueue.new }
|
||||||
|
let(:outbox) { MockQueue.new }
|
||||||
|
|
||||||
|
subject(:instance) { described_class.new(inbox, outbox) }
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
instance.start
|
||||||
|
example.run
|
||||||
|
instance.stop
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
messages.each do |message|
|
||||||
|
inbox.push(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "order creation message" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 20,
|
||||||
|
side: :buy
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sends order created event followed by order book update event" do
|
||||||
|
expect(outbox.messages).to eq([
|
||||||
|
Rex::Server::Messages::OrderCreatedEvent.new(1, 1, :buy, 20, 20, 10),
|
||||||
|
Rex::Server::Messages::OrderBookUpdateEvent.new(:buy, 10, 20)
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when my order matches an order" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 20,
|
||||||
|
side: :buy
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 2,
|
||||||
|
price: 9,
|
||||||
|
quantity: 22,
|
||||||
|
side: :sell
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns order book updates, order fill updates and trades" do
|
||||||
|
expect(outbox.messages).to eq([
|
||||||
|
Rex::Server::Messages::OrderCreatedEvent.new(id: 1, user_id: 1, side: :buy, quantity: 20, remaining_quantity: 20, price: 10),
|
||||||
|
Rex::Server::Messages::OrderBookUpdateEvent.new(side: :buy, price: 10, quantity: 20),
|
||||||
|
Rex::Server::Messages::OrderCreatedEvent.new(id: 2, user_id: 2, side: :sell, quantity: 22, remaining_quantity: 22, price: 9),
|
||||||
|
Rex::Server::Messages::OrderBookUpdateEvent.new(side: :sell, price: 9, quantity: 22),
|
||||||
|
Rex::Server::Messages::TradeEvent.new(id: 1, price: 9, quantity: 20),
|
||||||
|
Rex::Server::Messages::OrderFillEvent.new(id: 1, user_id: 1, price: 10, side: :buy, remaining_quantity: 0),
|
||||||
|
Rex::Server::Messages::OrderFillEvent.new(id: 2, user_id: 2, price: 9, side: :sell, remaining_quantity: 2),
|
||||||
|
Rex::Server::Messages::OrderBookUpdateEvent.new(side: :buy, price: 10, quantity: 0),
|
||||||
|
Rex::Server::Messages::OrderBookUpdateEvent.new(side: :sell, price: 9, quantity: 2)
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "cancel order request" do
|
||||||
|
context "when order is not existent" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CancelOrderRequest.new(1, 1)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nothing" do
|
||||||
|
expect(outbox.messages).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when order exists" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 20,
|
||||||
|
side: :buy
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::CancelOrderRequest.new(1, 1)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a cancelled order event and updates the order book" do
|
||||||
|
expect(outbox.messages.last(2)).to eq([
|
||||||
|
Rex::Server::Messages::OrderCancelledEvent.new(id: 1, user_id: 1, side: :buy, remaining_quantity: 20, price: 10),
|
||||||
|
Rex::Server::Messages::OrderBookUpdateEvent.new(side: :buy, price: 10, quantity: 0)
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "fetch order book request" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 20,
|
||||||
|
side: :buy
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 2,
|
||||||
|
price: 11,
|
||||||
|
quantity: 22,
|
||||||
|
side: :sell
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::FetchOrderBookRequest.new(3)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a list limit order book updates" do
|
||||||
|
expect(outbox.messages.last(2)).to eq([
|
||||||
|
Rex::Server::Messages::OrderBookFetchEvent.new(
|
||||||
|
3,
|
||||||
|
:buy,
|
||||||
|
10,
|
||||||
|
20
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::OrderBookFetchEvent.new(
|
||||||
|
3,
|
||||||
|
:sell,
|
||||||
|
11,
|
||||||
|
22
|
||||||
|
)
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "fetch trades request" do
|
||||||
|
context "when no trades exist" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::FetchTradesRequest.new(1)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nothing" do
|
||||||
|
expect(outbox.messages).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trades exist" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 15,
|
||||||
|
side: :buy
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 2,
|
||||||
|
price: 10,
|
||||||
|
quantity: 15,
|
||||||
|
side: :sell
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::FetchTradesRequest.new(1)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns trade fetch events" do
|
||||||
|
expect(outbox.messages.last(1)).to eq([
|
||||||
|
Rex::Server::Messages::TradeFetchEvent.new(
|
||||||
|
id: 1,
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 15
|
||||||
|
)
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "fetch orders request" do
|
||||||
|
context "when user does not have any orders" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::FetchOrdersRequest.new(1)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nothing" do
|
||||||
|
expect(outbox.messages).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user has orders" do
|
||||||
|
let(:messages) do
|
||||||
|
[
|
||||||
|
Rex::Server::Messages::CreateOrderRequest.new(
|
||||||
|
user_id: 1,
|
||||||
|
price: 10,
|
||||||
|
quantity: 15,
|
||||||
|
side: :sell
|
||||||
|
),
|
||||||
|
Rex::Server::Messages::FetchOrdersRequest.new(1)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the orders" do
|
||||||
|
expect(outbox.messages.last).to eq(
|
||||||
|
Rex::Server::Messages::OrderFetchEvent.new(id: 1, user_id: 1, side: :sell, quantity: 15, remaining_quantity: 15, price: 10)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user