378 lines
12 KiB
Ruby
378 lines
12 KiB
Ruby
# encoding: UTF-8
|
|
# frozen_string_literal: true
|
|
|
|
describe Order, 'validations', type: :model do
|
|
context 'validations' do
|
|
subject do
|
|
Order.validators
|
|
.select { |v| v.is_a? ActiveRecord::Validations::PresenceValidator }
|
|
.map(&:attributes)
|
|
.flatten
|
|
end
|
|
|
|
it do
|
|
is_expected.to include :ord_type
|
|
is_expected.to include :volume
|
|
is_expected.to include :origin_volume
|
|
is_expected.to include :locked
|
|
is_expected.to include :origin_locked
|
|
end
|
|
end
|
|
|
|
context 'limit order' do
|
|
it 'should make sure price is present' do
|
|
order = OrderAsk.new(market_id: 'btcusd', price: nil, ord_type: 'limit')
|
|
expect(order).not_to be_valid
|
|
expect(order.errors[:price]).to include 'is not a number'
|
|
end
|
|
end
|
|
|
|
context 'market order' do
|
|
it 'should make sure price is not present' do
|
|
order = OrderAsk.new(market_id: 'btcusd', price: '0.0'.to_d, ord_type: 'market')
|
|
expect(order).not_to be_valid
|
|
expect(order.errors[:price]).to include 'must not be present'
|
|
end
|
|
end
|
|
|
|
context 'attr_readonly' do
|
|
let!(:order) { create(:order_bid, :btcusd) }
|
|
|
|
it "does not allow updating readonly attributes" do
|
|
expect { order.update_attribute(:member_id, 1) }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'member_id is marked as readonly')
|
|
|
|
expect { order.update_attribute(:bid, 'xyz') }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'bid is marked as readonly')
|
|
|
|
expect { order.update_attribute(:ask, 'abc') }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'ask is marked as readonly')
|
|
|
|
expect { order.update_attribute(:market_id, 'abcxyz') }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'market_id is marked as readonly')
|
|
|
|
expect { order.update_attribute(:ord_type, 'market') }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'ord_type is marked as readonly')
|
|
|
|
expect { order.update_attribute(:origin_volume, 1) }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'origin_volume is marked as readonly')
|
|
|
|
expect { order.update_attribute(:origin_locked, 1) }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'origin_locked is marked as readonly')
|
|
|
|
expect { order.update_attribute(:created_at, '2009-01-03') }.to \
|
|
raise_error(ActiveRecord::ActiveRecordError, 'created_at is marked as readonly')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe Order, '#submit' do
|
|
let(:order) { create(:order_bid, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
|
let(:rejected_order) { create(:order_bid, :with_deposit_liability, state: 'reject', price: '12.32'.to_d, volume: '123.12345678') }
|
|
let(:order_bid) { create(:order_bid, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
|
let(:order_ask) { create(:order_ask, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
|
|
|
before do
|
|
Order.submit(order_bid.id)
|
|
Order.submit(order_ask.id)
|
|
end
|
|
|
|
it do
|
|
expect(order_bid.reload.state).to eq 'wait'
|
|
expect(Operations::Liability.where(reference: order_ask).count).to eq 2
|
|
expect(Operations::Liability.where(reference: order_bid).count).to eq 2
|
|
end
|
|
|
|
context 'validations' do
|
|
before do
|
|
order.member.accounts.find_by_currency_id(order.currency).update(balance: 0)
|
|
end
|
|
|
|
it 'insufficient balance' do
|
|
expect {
|
|
Order.submit(order.id)
|
|
}.to raise_error(Account::AccountError)
|
|
expect(order.reload.state).to eq('reject')
|
|
end
|
|
|
|
it 'rejected order' do
|
|
Order.submit(rejected_order.id)
|
|
expect(rejected_order.reload.state).to eq('reject')
|
|
end
|
|
end
|
|
|
|
it 'mysql connection error' do
|
|
ActiveRecord::Base.stubs(:transaction).raises(Mysql2::Error::ConnectionError.new(''))
|
|
expect { Order.submit(order.id) }.to raise_error(Mysql2::Error::ConnectionError)
|
|
end
|
|
end
|
|
|
|
describe Order, '#cancel' do
|
|
let(:order) { create(:order_bid, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
|
|
|
it 'mysql connection error' do
|
|
ActiveRecord::Base.stubs(:transaction).raises(Mysql2::Error::ConnectionError.new(''))
|
|
expect { Order.cancel(order.id) }.to raise_error(Mysql2::Error::ConnectionError)
|
|
end
|
|
end
|
|
|
|
describe Order, 'precision validations', type: :model do
|
|
let(:order_bid) { build(:order_bid, :btcusd, price: '12.32'.to_d, volume: '123.123456789') }
|
|
let(:order_ask) { build(:order_ask, :btcusd, price: '12.326'.to_d, volume: '123.12345678') }
|
|
|
|
it 'validates origin_volume precision' do
|
|
record = order_bid
|
|
expect(record.save).to eq false
|
|
expect(record.errors[:origin_volume]).to include(/precision must be less than or equal to 8/i)
|
|
end
|
|
|
|
it 'validates price precision' do
|
|
record = order_ask
|
|
expect(record.save).to eq false
|
|
expect(record.errors[:price]).to include(/precision must be less than or equal to 2/i)
|
|
end
|
|
end
|
|
|
|
describe Order, '#done', type: :model do
|
|
let(:ask_fee) { '0.003'.to_d }
|
|
let(:bid_fee) { '0.001'.to_d }
|
|
let(:order) { order_bid }
|
|
let(:order_bid) { create(:order_bid, :btcusd, price: '1.2'.to_d, volume: '10.0'.to_d) }
|
|
let(:order_ask) { create(:order_ask, :btcusd, price: '1.2'.to_d, volume: '10.0'.to_d) }
|
|
let(:hold_account) { create_account(:usd, locked: '100.0'.to_d) }
|
|
let(:expect_account) { create_account(:btc) }
|
|
|
|
before do
|
|
order_bid.stubs(:hold_account!).returns(hold_account.lock!)
|
|
order_bid.stubs(:expect_account!).returns(expect_account.lock!)
|
|
order_ask.stubs(:hold_account!).returns(hold_account.lock!)
|
|
order_ask.stubs(:expect_account!).returns(expect_account.lock!)
|
|
OrderBid.any_instance.stubs(:fee).returns(bid_fee)
|
|
OrderAsk.any_instance.stubs(:fee).returns(ask_fee)
|
|
end
|
|
|
|
def mock_trade(volume, price)
|
|
build(:trade, volume: volume, price: price, id: rand(10))
|
|
end
|
|
|
|
shared_examples 'trade done' do
|
|
before do
|
|
hold_account.reload
|
|
expect_account.reload
|
|
end
|
|
end
|
|
end
|
|
|
|
describe Order, '#kind' do
|
|
it 'should be ask for ask order' do
|
|
expect(OrderAsk.new.kind).to eq 'ask'
|
|
end
|
|
|
|
it 'should be bid for bid order' do
|
|
expect(OrderBid.new.kind).to eq 'bid'
|
|
end
|
|
end
|
|
|
|
describe Order, 'related accounts' do
|
|
let(:alice) { who_is_billionaire }
|
|
let(:bob) { who_is_billionaire }
|
|
|
|
context OrderAsk do
|
|
it 'should hold btc and expect usd' do
|
|
ask = create(:order_ask, :btcusd, member: alice)
|
|
expect(ask.hold_account).to eq alice.get_account(:btc)
|
|
expect(ask.expect_account).to eq alice.get_account(:usd)
|
|
end
|
|
end
|
|
|
|
context OrderBid do
|
|
it 'should hold usd and expect btc' do
|
|
bid = create(:order_bid, :btcusd, member: bob)
|
|
expect(bid.hold_account).to eq bob.get_account(:usd)
|
|
expect(bid.expect_account).to eq bob.get_account(:btc)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe Order, '#avg_price' do
|
|
it 'should be zero if not filled yet' do
|
|
expect(
|
|
OrderAsk.new(
|
|
locked: '1.0',
|
|
origin_locked: '1.0',
|
|
volume: '1.0',
|
|
origin_volume: '1.0',
|
|
funds_received: '0'
|
|
).avg_price
|
|
).to eq '0'.to_d
|
|
|
|
expect(
|
|
OrderBid.new(
|
|
locked: '1.0',
|
|
origin_locked: '1.0',
|
|
volume: '1.0',
|
|
origin_volume: '1.0',
|
|
funds_received: '0'
|
|
).avg_price
|
|
).to eq '0'.to_d
|
|
end
|
|
|
|
it 'should calculate average price of bid order' do
|
|
expect(
|
|
OrderBid.new(
|
|
market_id: 'btcusd',
|
|
locked: '10.0',
|
|
origin_locked: '20.0',
|
|
volume: '1.0',
|
|
origin_volume: '3.0',
|
|
funds_received: '2.0'
|
|
).avg_price
|
|
).to eq '5'.to_d
|
|
end
|
|
|
|
it 'should calculate average price of ask order' do
|
|
expect(
|
|
OrderAsk.new(
|
|
market_id: 'btcusd',
|
|
locked: '1.0',
|
|
origin_locked: '2.0',
|
|
volume: '1.0',
|
|
origin_volume: '2.0',
|
|
funds_received: '10.0'
|
|
).avg_price
|
|
).to eq '10'.to_d
|
|
end
|
|
end
|
|
|
|
describe Order, '#record_submit_operations!' do
|
|
# Persist Order in database.
|
|
let!(:order){ create(:order_ask, :btcusd, :with_deposit_liability) }
|
|
|
|
subject { order }
|
|
|
|
it 'creates two liability operations' do
|
|
expect{ subject.record_submit_operations! }.to change{ Operations::Liability.count }.by(2)
|
|
end
|
|
|
|
it 'doesn\'t create asset operations' do
|
|
expect{ subject.record_submit_operations! }.to_not change{ Operations::Asset.count }
|
|
end
|
|
|
|
it 'debits main liabilities for member' do
|
|
expect{ subject.record_submit_operations! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :main)
|
|
}.by(-subject.locked)
|
|
end
|
|
|
|
it 'credits locked liabilities for member' do
|
|
expect{ subject.record_submit_operations! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
|
}.by(subject.locked)
|
|
end
|
|
end
|
|
|
|
describe Order, '#record_cancel_operations!' do
|
|
# Persist Order in database.
|
|
let!(:order){ create(:order_ask, :with_deposit_liability) }
|
|
|
|
subject { order }
|
|
before { subject.record_submit_operations! }
|
|
|
|
it 'creates two liability operations' do
|
|
expect{ subject.record_cancel_operations! }.to change{ Operations::Liability.count }.by(2)
|
|
end
|
|
|
|
it 'doesn\'t create asset operations' do
|
|
expect{ subject.record_cancel_operations! }.to_not change{ Operations::Asset.count }
|
|
end
|
|
|
|
it 'credits main liabilities for member' do
|
|
expect{ subject.record_cancel_operations! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :main)
|
|
}.by(subject.locked)
|
|
end
|
|
|
|
it 'debits locked liabilities for member' do
|
|
expect{ subject.record_cancel_operations! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
|
}.by(-subject.locked)
|
|
end
|
|
end
|
|
|
|
describe Order, '#trigger_event' do
|
|
|
|
context 'trigger pusher event for limit order' do
|
|
let!(:order){ create(:order_ask, :with_deposit_liability) }
|
|
|
|
subject { order }
|
|
|
|
let(:data) do
|
|
{
|
|
id: subject.id,
|
|
market: subject.market_id,
|
|
kind: subject.kind,
|
|
side: subject.side,
|
|
ord_type: subject.ord_type,
|
|
price: subject.price&.to_s('F'),
|
|
avg_price: subject.avg_price&.to_s('F'),
|
|
state: subject.state,
|
|
origin_volume: subject.origin_volume.to_s('F'),
|
|
remaining_volume: subject.volume.to_s('F'),
|
|
executed_volume: (subject.origin_volume - subject.volume).to_s('F'),
|
|
at: subject.created_at.to_i,
|
|
created_at: subject.created_at.to_i,
|
|
updated_at: subject.updated_at.to_i,
|
|
trades_count: subject.trades_count,
|
|
}
|
|
end
|
|
|
|
before { ::AMQP::Queue.expects(:enqueue_event).with('private', subject.member.uid, 'order', data) }
|
|
|
|
it { subject.trigger_event }
|
|
end
|
|
|
|
context 'trigger pusher event for market order' do
|
|
let!(:order) { create(:order_ask, :with_deposit_liability, ord_type: 'market', price: nil) }
|
|
|
|
subject { order }
|
|
|
|
let(:data) do
|
|
{
|
|
id: subject.id,
|
|
market: subject.market_id,
|
|
kind: subject.kind,
|
|
side: subject.side,
|
|
ord_type: subject.ord_type,
|
|
price: subject.price&.to_s('F'),
|
|
avg_price: subject.avg_price&.to_s('F'),
|
|
state: subject.state,
|
|
origin_volume: subject.origin_volume.to_s('F'),
|
|
remaining_volume: subject.volume.to_s('F'),
|
|
executed_volume: (subject.origin_volume - subject.volume).to_s('F'),
|
|
at: subject.created_at.to_i,
|
|
created_at: subject.created_at.to_i,
|
|
updated_at: subject.updated_at.to_i,
|
|
trades_count: subject.trades_count
|
|
}
|
|
end
|
|
|
|
it 'doesnt push event for active market order' do
|
|
::AMQP::Queue.expects(:enqueue_event).with(:order, data).never
|
|
subject.trigger_event
|
|
end
|
|
|
|
it 'pushes event for completed market order' do
|
|
subject.expects(:trigger_event)
|
|
subject.update!(state: 'done')
|
|
end
|
|
|
|
context do
|
|
it do
|
|
subject.update!(state: 'done')
|
|
::AMQP::Queue.expects(:enqueue_event).with('private', subject.member.uid, 'order', data)
|
|
subject.trigger_event
|
|
end
|
|
end
|
|
end
|
|
end
|