244 lines
8.1 KiB
Ruby
244 lines
8.1 KiB
Ruby
# encoding: UTF-8
|
|
# frozen_string_literal: true
|
|
|
|
describe Matching::OrderBook do
|
|
context '#find' do
|
|
subject { Matching::OrderBook.new('btcusd', :ask) }
|
|
|
|
it 'should find specific order' do
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
subject.add o1
|
|
subject.add o2
|
|
|
|
expect(subject.find(o1.dup).object_id).to eq o1.object_id
|
|
expect(subject.find(o2.dup).object_id).to eq o2.object_id
|
|
end
|
|
end
|
|
|
|
context '#add' do
|
|
subject { Matching::OrderBook.new('btcusd', :ask) }
|
|
|
|
it 'should reject invalid order whose volume is zero' do
|
|
expect do
|
|
subject.add Matching.mock_limit_order(type: :ask, volume: '0.0'.to_d)
|
|
end.to raise_error(Matching::OrderError)
|
|
end
|
|
|
|
it 'should add market order' do
|
|
subject.add Matching.mock_limit_order(type: :ask)
|
|
|
|
o1 = Matching.mock_market_order(type: :ask)
|
|
expect{ subject.add o1 }.to raise_error(Matching::MarketOrderbookError)
|
|
end
|
|
|
|
it 'should create price level for order with new price' do
|
|
order = Matching.mock_limit_order(type: :ask)
|
|
subject.add order
|
|
expect(subject.limit_orders.keys.first).to eq order.price
|
|
expect(subject.limit_orders.values.first).to eq [order]
|
|
end
|
|
|
|
it 'should add order with same price to same price level' do
|
|
o1 = Matching.mock_limit_order(type: :ask)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: o1.price)
|
|
subject.add o1
|
|
subject.add o2
|
|
|
|
expect(subject.limit_orders.keys.size).to eq 1
|
|
expect(subject.limit_orders.values.first).to eq [o1, o2]
|
|
end
|
|
|
|
it 'should not broadcast add event' do
|
|
order = Matching.mock_limit_order(type: :ask)
|
|
|
|
AMQP::Queue.expects(:enqueue).with(:slave_book, { action: 'add', order: order.attributes }, persistent: false).never
|
|
Matching::OrderBook.new('btcusd', :ask, broadcast: false).add order
|
|
end
|
|
end
|
|
|
|
context '#remove' do
|
|
subject { Matching::OrderBook.new('btcusd', :ask) }
|
|
|
|
it 'should remove market order' do
|
|
subject.define_singleton_method(:legacy_add) do |order|
|
|
raise Matching::InvalidOrderError, "volume is zero" if order.volume <= 0.to_d
|
|
|
|
case order
|
|
when Matching::LimitOrder
|
|
@limit_orders[order.price] ||= PriceLevel.new(order.price)
|
|
@limit_orders[order.price].add order
|
|
when Matching::MarketOrder
|
|
@market_orders[order.id] = order
|
|
else
|
|
raise ArgumentError, "Unknown order type"
|
|
end
|
|
end
|
|
|
|
subject.add Matching.mock_limit_order(type: :ask)
|
|
order = Matching.mock_market_order(type: :ask)
|
|
subject.legacy_add order
|
|
subject.remove order
|
|
expect(subject.market_orders).to be_empty
|
|
end
|
|
|
|
it 'should remove limit order' do
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
subject.add o1
|
|
subject.add o2
|
|
subject.remove o1.dup # dup so it's not the same object, but has same id
|
|
|
|
expect(subject.limit_orders.values.first.size).to eq 1
|
|
end
|
|
|
|
it 'should remove price level if its only limit order removed' do
|
|
order = Matching.mock_limit_order(type: :ask)
|
|
subject.add order
|
|
subject.remove order.dup
|
|
expect(subject.limit_orders).to be_empty
|
|
end
|
|
|
|
it 'should return nil if order is not found' do
|
|
order = Matching.mock_limit_order(type: :ask)
|
|
expect(subject.remove(order)).to be_nil
|
|
end
|
|
|
|
it 'should return order in book' do
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = o1.dup
|
|
o1.instance_variable_set(:@volume, '12345'.to_d)
|
|
subject.add o1
|
|
o = subject.remove o2
|
|
expect(o.volume).to eq '12345'.to_d
|
|
end
|
|
end
|
|
|
|
context '#best_limit_price' do
|
|
it 'should return highest bid price' do
|
|
book = Matching::OrderBook.new('btcusd', :bid)
|
|
o1 = Matching.mock_limit_order(type: :bid, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :bid, price: '2.0'.to_d)
|
|
book.add o1
|
|
book.add o2
|
|
|
|
expect(book.best_limit_price).to eq o2.price
|
|
end
|
|
|
|
it 'should return lowest ask price' do
|
|
book = Matching::OrderBook.new('btcusd', :ask)
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: '2.0'.to_d)
|
|
book.add o1
|
|
book.add o2
|
|
|
|
expect(book.best_limit_price).to eq o1.price
|
|
end
|
|
|
|
it 'should return nil if there\'s no limit order' do
|
|
book = Matching::OrderBook.new('btcusd', :ask)
|
|
expect(book.best_limit_price).to be_nil
|
|
end
|
|
end
|
|
|
|
context '#top' do
|
|
it 'should return nil for empty book' do
|
|
book = Matching::OrderBook.new('btcusd', :ask)
|
|
expect(book.top).to be_nil
|
|
end
|
|
|
|
it 'should find ask order with lowest price' do
|
|
book = Matching::OrderBook.new('btcusd', :ask)
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: '2.0'.to_d)
|
|
book.add o1
|
|
book.add o2
|
|
|
|
expect(book.top).to eq o1
|
|
end
|
|
|
|
it 'should find bid order with highest price' do
|
|
book = Matching::OrderBook.new('btcusd', :bid)
|
|
o1 = Matching.mock_limit_order(type: :bid, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :bid, price: '2.0'.to_d)
|
|
book.add o1
|
|
book.add o2
|
|
|
|
expect(book.top).to eq o2
|
|
end
|
|
|
|
it 'should favor earlier order if orders have same price' do
|
|
book = Matching::OrderBook.new('btcusd', :ask)
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
book.add o1
|
|
book.add o2
|
|
|
|
expect(book.top).to eq o1
|
|
end
|
|
end
|
|
|
|
context '#fill_top' do
|
|
subject { Matching::OrderBook.new('btcusd', :ask) }
|
|
|
|
it 'should raise error if there is no top order' do
|
|
expect do
|
|
subject.fill_top('1.0'.to_d, '1.0'.to_d, '1.0'.to_d)
|
|
end.to raise_error(RuntimeError, 'No top order in empty book.')
|
|
end
|
|
|
|
it 'should remove the price level if top order is the only order in level' do
|
|
subject.add Matching.mock_limit_order(type: :ask, volume: '1.0'.to_d)
|
|
subject.fill_top '1.0'.to_d, '1.0'.to_d, '1.0'.to_d
|
|
expect(subject.limit_orders).to be_empty
|
|
end
|
|
|
|
it 'should remove order from level' do
|
|
subject.add Matching.mock_limit_order(type: :ask, volume: '1.0'.to_d)
|
|
subject.add Matching.mock_limit_order(type: :ask, volume: '1.0'.to_d)
|
|
subject.fill_top '1.0'.to_d, '1.0'.to_d, '1.0'.to_d
|
|
expect(subject.limit_orders.values.first.size).to eq 1
|
|
end
|
|
|
|
it 'should fill top order with volume' do
|
|
subject.add Matching.mock_limit_order(type: :ask, volume: '2.0'.to_d)
|
|
subject.fill_top '1.0'.to_d, '0.5'.to_d, '0.5'.to_d
|
|
expect(subject.top.volume).to eq '1.5'.to_d
|
|
end
|
|
end
|
|
|
|
context 'on_change callback provided' do
|
|
let(:callback) { Object.new }
|
|
subject { Matching::OrderBook.new('btcusd', :ask, on_change: callback) }
|
|
|
|
it 'should notify add limit order' do
|
|
order = Matching.mock_limit_order(type: :ask)
|
|
callback.expects(:call).with('btcusd', :ask, order.price, order.volume)
|
|
subject.add order
|
|
end
|
|
|
|
it 'should notify remove limit order' do
|
|
o1 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
o2 = Matching.mock_limit_order(type: :ask, price: '1.0'.to_d)
|
|
|
|
callback.expects(:call).with('btcusd', :ask, '1.0'.to_d, o1.volume)
|
|
callback.expects(:call).with('btcusd', :ask, '1.0'.to_d, o2.volume + o1.volume)
|
|
callback.expects(:call).with('btcusd', :ask, '1.0'.to_d, o2.volume)
|
|
|
|
subject.add o1
|
|
subject.add o2
|
|
subject.remove o1.dup # dup so it's not the same object, but has same id
|
|
|
|
expect(subject.limit_orders.values.first.size).to eq 1
|
|
end
|
|
|
|
it 'should notify fill top order with volume' do
|
|
o1 = Matching.mock_limit_order(type: :ask, volume: '2.0'.to_d)
|
|
callback.expects(:call).with('btcusd', :ask, o1.price, '2.0'.to_d)
|
|
callback.expects(:call).with('btcusd', :ask, o1.price, '1.5'.to_d)
|
|
subject.add(o1)
|
|
subject.fill_top(o1.price, '0.5'.to_d, '0.5'.to_d)
|
|
end
|
|
end
|
|
end
|