Initial commit

This commit is contained in:
Yaser
2026-08-13 19:50:53 +03:30
commit 38084458fe
879 changed files with 95198 additions and 0 deletions

377
spec/models/order_spec.rb Normal file
View File

@@ -0,0 +1,377 @@
# 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