Files
Dena/spec/trading/matching/order_book_spec.rb
2026-08-13 19:50:53 +03:30

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