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

235 lines
8.2 KiB
Ruby

# encoding: UTF-8
# frozen_string_literal: true
describe Matching::Executor do
let(:alice) { who_is_billionaire }
let(:bob) { who_is_billionaire }
let(:market) { Market.find('btcusd') }
let(:price) { 10.to_d }
let(:volume) { 5.to_d }
subject do
Matching::Executor.new(
action: 'execute',
trade: {
market_id: market.id,
maker_order_id: ask.id,
taker_order_id: bid.id,
strike_price: price.to_s('F'),
amount: volume.to_s('F'),
total: (price * volume).to_s('F')
}
)
end
context 'invalid volume' do
let(:ask) { ::Matching::LimitOrder.new create(:order_ask, :btcusd, price: price, volume: volume, member: alice).to_matching_attributes }
let(:bid) { ::Matching::LimitOrder.new create(:order_bid, :btcusd, price: price, volume: 3.to_d, member: bob).to_matching_attributes }
it 'should raise error' do
expect { subject.execute! }.to raise_error(Matching::TradeExecutionError)
end
end
context 'invalid bid price' do
let(:ask) { ::Matching::LimitOrder.new create(:order_ask, :btcusd, price: price, volume: volume, member: alice).to_matching_attributes }
let(:bid) { ::Matching::LimitOrder.new create(:order_bid, :btcusd, price: price - 1, volume: volume, member: bob).to_matching_attributes }
it 'should raise error' do
expect { subject.execute! }.to raise_error(Matching::TradeExecutionError)
end
end
context 'invalid ask price' do
let(:ask) { ::Matching::LimitOrder.new create(:order_ask, :btcusd, price: price + 1, volume: volume, member: alice).to_matching_attributes }
let(:bid) { ::Matching::LimitOrder.new create(:order_bid, :btcusd, price: price, volume: volume, member: bob).to_matching_attributes }
it 'should raise error' do
expect { subject.execute! }.to raise_error(Matching::TradeExecutionError)
end
end
context 'full execution' do
let(:ask) { ::Matching::LimitOrder.new create(:order_ask, :btcusd, price: price, volume: volume, member: alice).to_matching_attributes }
let(:bid) { ::Matching::LimitOrder.new create(:order_bid, :btcusd, price: price, volume: volume, member: bob).to_matching_attributes }
it 'should create trade' do
expect do
trade = subject.execute!
expect(trade.price).to eq price
expect(trade.amount).to eq volume
expect(trade.maker_order_id).to eq ask.id
expect(trade.taker_order_id).to eq bid.id
end.to change(Trade, :count).by(1)
end
it 'should set trade used funds' do
trade = subject.execute!
expect(trade.total).to eq price * volume
end
it 'should increase order\'s trades count' do
subject.execute!
expect(Order.find(ask.id).trades_count).to eq 1
expect(Order.find(bid.id).trades_count).to eq 1
end
it 'should mark both orders as done' do
subject.execute!
expect(Order.find(ask.id).state).to eq Order::DONE
expect(Order.find(bid.id).state).to eq Order::DONE
end
it 'should publish trade through amqp' do
AMQP::Queue.expects(:publish)
subject.execute!
end
end
context 'partial ask execution' do
let(:ask) { create(:order_ask, :btcusd, price: price, volume: 7.to_d, member: alice) }
let(:bid) { create(:order_bid, :btcusd, price: price, volume: 5.to_d, member: bob) }
it 'should set bid to done only' do
subject.execute!
expect(ask.reload.state).not_to eq Order::DONE
expect(bid.reload.state).to eq Order::DONE
end
end
context 'partial bid execution' do
let(:ask) { create(:order_ask, :btcusd, price: price, volume: 5.to_d, member: alice) }
let(:bid) { create(:order_bid, :btcusd, price: price, volume: 7.to_d, member: bob) }
it 'should set ask to done only' do
subject.execute!
expect(ask.reload.state).to eq Order::DONE
expect(bid.reload.state).not_to eq Order::DONE
end
end
context 'partially filled market order whose locked fund run out' do
let(:ask) { create(:order_ask, :btcusd, price: '2.0'.to_d, volume: '3.0'.to_d, member: alice) }
let(:bid) { create(:order_bid, :btcusd, price: nil, ord_type: 'market', volume: '2.0'.to_d, locked: '3.0'.to_d, member: bob) }
it 'should cancel the market order' do
executor = Matching::Executor.new(
action: 'execute',
trade: {
market_id: market.id,
maker_order_id: ask.id,
taker_order_id: bid.id,
strike_price: '2.0'.to_d,
amount: '1.5'.to_d,
total: '3.0'.to_d
}
)
executor.execute!
expect(bid.reload.state).to eq Order::CANCEL
end
end
context 'unlock not used funds' do
let(:ask) { create(:order_ask, :btcusd, price: price - 1, volume: 7.to_d, member: alice) }
let(:bid) { create(:order_bid, :btcusd, price: price, volume: volume, member: bob) }
subject do
Matching::Executor.new(
action: 'execute',
trade: {
market_id: market.id,
maker_order_id: ask.id,
taker_order_id: bid.id,
strike_price: price - 1, # so bid order only used (price-1)*volume
amount: volume.to_s('F'),
total: ((price - 1) * volume).to_s('F')
}
)
end
it 'should unlock funds not used by bid order' do
locked_before = bid.hold_account.reload.locked
subject.execute!
locked_after = bid.hold_account.reload.locked
expect(locked_after).to eq locked_before - (price * volume)
end
it 'should save unused amount in order locked attribute' do
subject.execute!
expect(bid.reload.locked).to eq price * volume - (price - 1) * volume
end
end
context 'maker/taker fee' do
# Create two limit orders:
# Ask: member_id: alice, market: btcusd, price: 10, volume: 5 - Maker;
# Bid: member_id: bob, market: btcusd, price: 10, volume: 5 - Taker;
context 'maker_fee: 1%, taker_fee: 2%' do
# Maker_fee: 1%;
# Taker_fee: 2%;
# Result after exeuction:
# Alice get 49.5 usd;
# Bob get 4.9 btc;
let(:ask) { create(:order_ask, :btcusd, price: price, volume: volume, member: alice) }
let(:bid) { create(:order_bid, :btcusd, price: price, volume: volume, member: bob) }
before do
TradingFee.last.update!(maker: 0.01, taker: 0.02)
subject.execute!
end
it do
expect(ask.member.balance_for(currency: bid.currency, kind: :main)).to eq(49.5)
expect(bid.member.balance_for(currency: ask.currency, kind: :main)).to eq(4.9)
expect(Operations::Revenue.find_by(currency: bid.currency, member: ask.member).credit).to eq(0.5)
expect(Operations::Revenue.find_by(currency: ask.currency, member: bid.member).credit).to eq(0.1)
end
end
context 'maker_fee: 0%, taker_fee: 2%' do
# Maker_fee: 0%;
# Taker_fee: 2%;
# Result after exeuction:
# Alice get 50.0 usd;
# Bob get 4.9 btc;
let(:ask) { create(:order_ask, :btcusd, price: price, volume: volume, member: alice) }
let(:bid) { create(:order_bid, :btcusd, price: price, volume: volume, member: bob) }
before do
TradingFee.last.update!(maker: 0.0, taker: 0.02)
subject.execute!
end
it do
expect(ask.member.balance_for(currency: bid.currency, kind: :main)).to eq(50)
expect(bid.member.balance_for(currency: ask.currency, kind: :main)).to eq(4.9)
expect(Operations::Revenue.find_by(currency: bid.currency, member: ask.member).blank?).to eq(true)
expect(Operations::Revenue.find_by(currency: ask.currency, member: bid.member).credit).to eq(0.1)
end
end
end
context 'execution fail' do
let(:ask) { ::Matching::LimitOrder.new create(:order_ask, :btcusd, price: price, volume: volume, member: alice).to_matching_attributes }
let(:bid) { ::Matching::LimitOrder.new create(:order_bid, :btcusd, price: price, volume: volume, member: bob).to_matching_attributes }
it 'should not create trade' do
# set locked funds to 0 so strike will fail
alice.get_account(:btc).update_attributes(locked: ::Trade::ZERO)
expect do
expect { subject.execute! }.to raise_error(Account::AccountError)
end.not_to change(Trade, :count)
end
end
end