852 lines
37 KiB
Ruby
852 lines
37 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe Jobs::Cron::StatsMemberPnl do
|
|
let!(:member_platform) { create(:member, :level_3) }
|
|
let!(:member) { create(:member, :level_3) }
|
|
let!(:maker) { create(:member, role: 'maker') }
|
|
let(:maker2) { create(:member, role: 'maker') }
|
|
|
|
include ::API::V2::Management::Helpers
|
|
|
|
def create_transfer(transfer_attrs)
|
|
Transfer.transaction do
|
|
attrs = transfer_attrs.slice(:key, :category, :description)
|
|
transfer_attrs[:operations].each do |op_pair|
|
|
currency = Currency.find(op_pair[:currency])
|
|
|
|
debit_op = op_pair[:account_src].merge(debit: op_pair[:amount], credit: 0.0, currency: currency)
|
|
credit_op = op_pair[:account_dst].merge(credit: op_pair[:amount], debit: 0.0, currency: currency)
|
|
|
|
[debit_op, credit_op].each do |op|
|
|
klass = ::Operations.klass_for(code: op[:code])
|
|
|
|
uid = op.delete(:uid)
|
|
op.merge!(member: Member.find_by!(uid: uid)) if uid.present?
|
|
|
|
type = ::Operations::Account.find_by(code: op[:code]).type
|
|
type_plural = type.pluralize
|
|
if attrs[type_plural].present?
|
|
attrs[type_plural].push(klass.new(op))
|
|
else
|
|
attrs[type_plural] = [klass.new(op)]
|
|
end
|
|
end
|
|
end
|
|
|
|
Transfer.create!(attrs)
|
|
end
|
|
end
|
|
|
|
before(:each) do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:exclude_roles).returns(['maker'])
|
|
end
|
|
|
|
context 'conversion_market' do
|
|
it 'when there is no market' do
|
|
expect do
|
|
Jobs::Cron::StatsMemberPnl.conversion_market('test1', 'btc')
|
|
end.to raise_error('There is no market test1/btc')
|
|
end
|
|
|
|
it 'when market exists' do
|
|
market = Market.first
|
|
expect(Jobs::Cron::StatsMemberPnl.conversion_market(market.base_unit, market.quote_unit)).to eq market.id
|
|
end
|
|
end
|
|
|
|
context 'price_at' do
|
|
after { delete_measurments('trades') }
|
|
|
|
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
|
|
let!(:liability) { create(:liability, member: member, credit: 0.4, reference_type: 'Deposit', reference_id: coin_deposit.id) }
|
|
|
|
it 'when there is no trades' do
|
|
market = Market.find_by(base_unit: 'btc', quote_unit: 'usd')
|
|
expect do
|
|
Jobs::Cron::StatsMemberPnl.price_at(coin_deposit.currency_id, market.quote_unit, liability.created_at)
|
|
end.to raise_error("There is no trades on market #{coin_deposit.currency_id}#{market.quote_unit}")
|
|
end
|
|
|
|
context 'when trade exist' do
|
|
let(:trade) { create(:trade, :btceth, price: '5.0'.to_d, amount: '1.9'.to_d, total: '5.5'.to_d) }
|
|
|
|
before do
|
|
trade.write_to_influx
|
|
end
|
|
|
|
it 'return trade price' do
|
|
res = Jobs::Cron::StatsMemberPnl.price_at(coin_deposit.currency_id, trade.market.quote_unit, trade.created_at + 3.hours)
|
|
expect(res).to eq trade.price
|
|
end
|
|
|
|
it 'when pnl currency id equal to currency id' do
|
|
res = Jobs::Cron::StatsMemberPnl.price_at(coin_deposit.currency_id, coin_deposit.currency_id, trade.created_at + 3.hours)
|
|
expect(res).to eq 1.0
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'process currency' do
|
|
before(:each) do
|
|
StatsMemberPnl.delete_all
|
|
end
|
|
|
|
context 'reference type withdraw' do
|
|
before do
|
|
[member, maker].each do |m|
|
|
m.touch_accounts
|
|
m.accounts.map { |a| a.update(balance: 500) }
|
|
end
|
|
end
|
|
|
|
context 'creates one pnl' do
|
|
let!(:coin_withdraw) { create(:btc_withdraw, sum: 0.3.to_d, amount: 0.2.to_d, aasm_state: 'succeed', member: member) }
|
|
let!(:coin_withdraw_maker) { create(:btc_withdraw, sum: 0.3.to_d, amount: 0.2.to_d, aasm_state: 'succeed', member: maker) }
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(123)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process() }.to change { StatsMemberPnl.count }.by(1)
|
|
|
|
expect(StatsMemberPnl.last.member_id).to eq coin_withdraw.member_id
|
|
expect(StatsMemberPnl.last.currency_id).to eq coin_withdraw.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.total_credit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit).to eq coin_withdraw.amount
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq((coin_withdraw.amount + coin_withdraw.fee) * 123)
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq coin_withdraw.fee
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq 0
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq 0
|
|
end
|
|
end
|
|
|
|
context 'calculation on existing pnl' do
|
|
let!(:coin_withdraw) { create(:btc_withdraw, amount: 0.2.to_d, aasm_state: 'succeed', member: member) }
|
|
let!(:pnl) do
|
|
create(:stats_member_pnl, currency_id: coin_withdraw.currency_id, pnl_currency_id: 'eth', total_debit: 0.1,
|
|
total_debit_fees: 0.01, total_debit_value: 0.3,
|
|
member_id: coin_withdraw.member_id)
|
|
end
|
|
let!(:liability) do
|
|
create(:liability, id: 2, member_id: coin_withdraw.member_id, currency_id: coin_withdraw.currency_id,
|
|
debit: 0.3, reference_type: 'Withdraw', code: 212, reference_id: coin_withdraw.id)
|
|
end
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
|
|
|
|
expect(StatsMemberPnl.last.member_id).to eq coin_withdraw.member_id
|
|
expect(StatsMemberPnl.last.currency_id).to eq coin_withdraw.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.total_credit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit).to eq coin_withdraw.amount + pnl.total_debit
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq (coin_withdraw.amount + coin_withdraw.fee) * 1.0 + pnl.total_debit_value
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq coin_withdraw.fee + pnl.total_debit_fees
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq 0
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq 0
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'reference type adjustments' do
|
|
context 'creates one pnl with positive adjustment' do
|
|
let!(:member) { create(:member) }
|
|
let!(:adjustment) { create(:adjustment, currency_id: 'btc', amount: 1.0, receiving_account_number: "btc-202-#{member.uid}") }
|
|
let!(:adjustment_maker) { create(:adjustment, currency_id: 'btc', amount: 1.0, receiving_account_number: "btc-202-#{maker.uid}") }
|
|
let(:btceth_price) { 100.0 }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(btceth_price)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
adjustment.accept!(validator: member)
|
|
adjustment_maker.accept!(validator: member)
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(1)
|
|
expect(StatsMemberPnl.last.member_id).to eq member.id
|
|
expect(StatsMemberPnl.last.currency_id).to eq adjustment.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.total_credit).to eq adjustment.amount
|
|
expect(StatsMemberPnl.last.total_debit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq adjustment.amount * btceth_price
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq adjustment.amount * btceth_price
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq btceth_price
|
|
end
|
|
end
|
|
|
|
context 'creates one pnl with positive and negative adjustments' do
|
|
let(:member) { create(:member) }
|
|
let(:adjustment) { create(:adjustment, currency_id: 'btc', amount: 1.0, receiving_account_number: "btc-202-#{member.uid}") }
|
|
let(:btceth_price) { 100.0 }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(btceth_price)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
adjustment.accept!(validator: member)
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(1)
|
|
expect(StatsMemberPnl.last.member_id).to eq member.id
|
|
expect(StatsMemberPnl.last.currency_id).to eq adjustment.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.total_credit).to eq adjustment.amount
|
|
expect(StatsMemberPnl.last.total_debit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq adjustment.amount * btceth_price
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq adjustment.amount * btceth_price
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq btceth_price
|
|
|
|
half = 1.to_d / 2
|
|
adjustment2 = create(:adjustment, currency_id: 'btc', amount: -half, receiving_account_number: "btc-202-#{member.uid}")
|
|
adjustment2.accept!(validator: member)
|
|
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
|
|
|
|
expect(StatsMemberPnl.last.member_id).to eq member.id
|
|
expect(StatsMemberPnl.last.currency_id).to eq adjustment.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.total_credit).to eq adjustment.amount
|
|
expect(StatsMemberPnl.last.total_debit).to eq 0.5
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq 50
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq 1.0 * btceth_price
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq half * btceth_price
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq btceth_price
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'reference type deposit' do
|
|
context 'creates one pnl' do
|
|
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
|
|
let!(:coin_deposit_maker) { create(:deposit, :deposit_btc, member: maker) }
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
[coin_deposit, coin_deposit_maker].each do |d|
|
|
d.accept!
|
|
d.process!
|
|
end
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(1)
|
|
expect(StatsMemberPnl.last.member_id).to eq coin_deposit.member_id
|
|
expect(StatsMemberPnl.last.currency_id).to eq coin_deposit.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.total_credit).to eq coin_deposit.amount
|
|
expect(StatsMemberPnl.last.total_debit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq coin_deposit.fee
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq coin_deposit.amount * 1.0
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq coin_deposit.amount * 1.0
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq 1.0
|
|
end
|
|
end
|
|
|
|
context 'creates several pnls' do
|
|
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
|
|
let!(:fiat_deposit) { create(:deposit_usd, amount: 190.0) }
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
fiat_deposit.accept!
|
|
coin_deposit.accept!
|
|
coin_deposit.process!
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(2)
|
|
|
|
stats_member_pnl_btc = StatsMemberPnl.find_by(currency_id: coin_deposit.currency_id, member: coin_deposit.member)
|
|
stats_member_pnl_usd = StatsMemberPnl.find_by(currency_id: fiat_deposit.currency_id, member: fiat_deposit.member)
|
|
expect(stats_member_pnl_btc.member_id).to eq coin_deposit.member_id
|
|
expect(stats_member_pnl_btc.pnl_currency_id).to eq 'eth'
|
|
expect(stats_member_pnl_btc.currency_id).to eq coin_deposit.currency_id
|
|
expect(stats_member_pnl_btc.total_credit).to eq coin_deposit.amount
|
|
expect(stats_member_pnl_btc.total_debit).to eq 0
|
|
expect(stats_member_pnl_btc.total_debit_value).to eq 0
|
|
expect(stats_member_pnl_btc.total_debit_fees).to eq 0
|
|
expect(stats_member_pnl_btc.total_credit_fees).to eq coin_deposit.fee
|
|
expect(stats_member_pnl_btc.total_credit_value).to eq coin_deposit.amount * 1.0
|
|
expect(stats_member_pnl_btc.total_balance_value).to eq coin_deposit.amount * 1.0
|
|
expect(stats_member_pnl_btc.average_balance_price).to eq 1.0
|
|
|
|
expect(stats_member_pnl_usd.member_id).to eq fiat_deposit.member_id
|
|
expect(stats_member_pnl_usd.pnl_currency_id).to eq 'eth'
|
|
expect(stats_member_pnl_usd.currency_id).to eq fiat_deposit.currency_id
|
|
expect(stats_member_pnl_usd.total_credit).to eq fiat_deposit.amount
|
|
expect(stats_member_pnl_usd.total_debit).to eq 0
|
|
expect(stats_member_pnl_usd.total_debit_value).to eq 0
|
|
expect(stats_member_pnl_usd.total_debit_fees).to eq 0
|
|
expect(stats_member_pnl_usd.total_credit_fees).to eq fiat_deposit.fee
|
|
expect(stats_member_pnl_usd.total_credit_value).to eq fiat_deposit.amount * 1.0
|
|
expect(stats_member_pnl_usd.total_balance_value).to eq fiat_deposit.amount * 1.0
|
|
expect(stats_member_pnl_usd.average_balance_price).to eq 1.0
|
|
end
|
|
end
|
|
|
|
context 'calculation on existing pnl' do
|
|
let!(:coin_deposit) { create(:deposit, :deposit_btc, amount: '0.1'.to_d) }
|
|
let!(:pnl) do
|
|
create(:stats_member_pnl, currency_id: coin_deposit.currency_id, pnl_currency_id: 'eth', total_credit: 0.1,
|
|
total_credit_fees: '0.01'.to_d, total_credit_value: '0.3'.to_d, total_balance_value: '0.3'.to_d, member_id: coin_deposit.member_id)
|
|
end
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
coin_deposit.accept!
|
|
coin_deposit.process!
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
|
|
expect(StatsMemberPnl.last.member_id).to eq coin_deposit.member_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.last.currency_id).to eq coin_deposit.currency_id
|
|
expect(StatsMemberPnl.last.total_credit).to eq(coin_deposit.amount + pnl.total_credit)
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq(coin_deposit.fee + pnl.total_credit_fees)
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq(pnl.total_credit_value + coin_deposit.amount * 1.0)
|
|
expect(StatsMemberPnl.last.total_debit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_balance_value).to eq(pnl.total_credit_value + coin_deposit.amount * 1.0)
|
|
expect(StatsMemberPnl.last.average_balance_price).to eq((pnl.total_balance_value + coin_deposit.amount * 1.0) / (coin_deposit.amount + pnl.total_credit - pnl.total_debit))
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'reference type trade' do
|
|
context 'calculation on existing pnl' do
|
|
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d) }
|
|
let(:btceth_price) { 123 }
|
|
|
|
let!(:pnl1) do
|
|
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'btc',
|
|
total_credit: 2.0, total_credit_fees: 0.2, total_credit_value: 11.0, total_balance_value: 11.0, total_debit: 0.02,
|
|
average_balance_price: 5.5,
|
|
total_debit_value: 1.0, member_id: trade.maker_order.member.id)
|
|
end
|
|
|
|
let!(:pnl2) do
|
|
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'usd',
|
|
total_credit: 0.1, total_credit_fees: 0.01, total_credit_value: 0.3, total_debit: 0.2,
|
|
total_debit_value: 10.0, member_id: trade.maker_order.member.id)
|
|
end
|
|
|
|
let!(:pnl3) do
|
|
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'usd',
|
|
total_credit: 0.4, total_credit_fees: 0.01, total_credit_value: 0.3, total_debit: 0.2,
|
|
average_balance_price: 0.1,
|
|
total_debit_value: 10.0, member_id: trade.taker_order.member.id)
|
|
end
|
|
|
|
let!(:pnl4) do
|
|
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'btc',
|
|
total_credit: 0.4, total_credit_fees: 0.01, total_credit_value: 0.3,
|
|
total_debit: 0.2, total_debit_value: 10.0, member_id: trade.taker_order.member.id)
|
|
end
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(btceth_price)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
|
|
|
|
total_fees = trade.total * trade.order_fee(trade.maker_order)
|
|
expect(StatsMemberPnl.all[0].member_id).to eq trade.maker_order.member.id
|
|
expect(StatsMemberPnl.all[0].pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.all[0].currency_id).to eq pnl1.currency_id
|
|
expect(StatsMemberPnl.all[0].total_credit).to eq pnl1.total_credit
|
|
expect(StatsMemberPnl.all[0].total_credit_fees).to eq pnl1.total_credit_fees
|
|
expect(StatsMemberPnl.all[0].total_debit).to eq trade.amount + pnl1.total_debit
|
|
expect(StatsMemberPnl.all[0].total_debit_value).to eq pnl1.total_debit_value + trade.amount * btceth_price
|
|
expect(StatsMemberPnl.all[0].total_credit_value).to eq pnl1.total_credit_value
|
|
expect(StatsMemberPnl.all[0].total_balance_value).to eq(pnl1.total_balance_value - trade.amount * pnl1.average_balance_price)
|
|
|
|
expect(StatsMemberPnl.all[1].member_id).to eq trade.maker_order.member.id
|
|
expect(StatsMemberPnl.all[1].pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.all[1].currency_id).to eq pnl2.currency_id
|
|
expect(StatsMemberPnl.all[1].total_credit).to eq trade.total - total_fees + pnl2.total_credit
|
|
expect(StatsMemberPnl.all[1].total_credit_fees).to eq total_fees + + pnl2.total_credit_fees
|
|
expect(StatsMemberPnl.all[1].total_debit).to eq pnl2.total_debit
|
|
expect(StatsMemberPnl.all[1].total_debit_value).to eq pnl2.total_debit_value
|
|
expect(StatsMemberPnl.all[1].total_credit_value).to eq pnl2.total_credit_value + (trade.total - total_fees) * btceth_price
|
|
expect(StatsMemberPnl.all[1].total_balance_value).to eq pnl2.total_balance_value + (trade.total - total_fees) * btceth_price
|
|
|
|
total_fees = trade.amount * trade.order_fee(trade.taker_order)
|
|
expect(StatsMemberPnl.all[2].member_id).to eq trade.taker_order.member.id
|
|
expect(StatsMemberPnl.all[2].pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.all[2].currency_id).to eq pnl3.currency_id
|
|
expect(StatsMemberPnl.all[2].total_credit).to eq pnl3.total_credit
|
|
expect(StatsMemberPnl.all[2].total_debit).to eq trade.total + pnl3.total_debit
|
|
expect(StatsMemberPnl.all[2].total_debit_value).to eq trade.total * btceth_price + pnl3.total_debit_value
|
|
expect(StatsMemberPnl.all[2].total_credit_fees).to eq pnl3.total_credit_fees
|
|
expect(StatsMemberPnl.all[2].total_credit_value).to eq pnl3.total_credit_value
|
|
expect(StatsMemberPnl.all[2].total_balance_value).to eq(0)
|
|
|
|
expect(StatsMemberPnl.all[3].member_id).to eq trade.taker_order.member.id
|
|
expect(StatsMemberPnl.all[3].pnl_currency_id).to eq 'eth'
|
|
expect(StatsMemberPnl.all[3].currency_id).to eq pnl4.currency_id
|
|
expect(StatsMemberPnl.all[3].total_credit).to eq trade.amount - total_fees + pnl4.total_credit
|
|
expect(StatsMemberPnl.all[3].total_debit).to eq pnl4.total_debit
|
|
expect(StatsMemberPnl.all[3].total_debit_value).to eq pnl4.total_debit_value
|
|
expect(StatsMemberPnl.all[3].total_credit_fees).to eq total_fees + pnl4.total_credit_fees
|
|
expect(StatsMemberPnl.all[3].total_credit_value).to eq pnl4.total_credit_value + (trade.amount - total_fees) * btceth_price
|
|
expect(StatsMemberPnl.all[3].total_balance_value).to eq pnl4.total_balance_value + (trade.amount - total_fees) * btceth_price
|
|
end
|
|
end
|
|
|
|
context 'creates pnls while executing 1 trade' do
|
|
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d) }
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(4)
|
|
total_fees = trade.total * trade.order_fee(trade.maker_order)
|
|
|
|
pnl1 = StatsMemberPnl.find_by(member_id: trade.maker_order.member.id, currency_id: trade.maker_order.income_currency.id, pnl_currency_id: 'eth')
|
|
expect(pnl1.total_credit).to eq trade.total - total_fees
|
|
expect(pnl1.total_debit).to eq 0
|
|
expect(pnl1.total_debit_value).to eq 0
|
|
expect(pnl1.total_credit_fees).to eq total_fees
|
|
expect(pnl1.total_credit_value).to eq (trade.total - total_fees) * 1.0
|
|
|
|
pnl2 = StatsMemberPnl.find_by(member_id: trade.maker_order.member.id, currency_id: trade.maker_order.outcome_currency.id, pnl_currency_id: 'eth')
|
|
expect(pnl2.total_credit).to eq 0
|
|
expect(pnl2.total_debit).to eq trade.amount
|
|
expect(pnl2.total_debit_value).to eq trade.amount * 1.0
|
|
expect(pnl2.total_credit_fees).to eq 0
|
|
expect(pnl2.total_credit_value).to eq 0
|
|
|
|
total_fees = trade.amount * trade.order_fee(trade.taker_order)
|
|
pnl3 = StatsMemberPnl.find_by(member_id: trade.taker_order.member.id, currency_id: trade.taker_order.income_currency.id, pnl_currency_id: 'eth')
|
|
expect(pnl3.total_credit).to eq trade.amount - total_fees
|
|
expect(pnl3.total_debit).to eq 0
|
|
expect(pnl3.total_debit_value).to eq 0
|
|
expect(pnl3.total_credit_fees).to eq total_fees
|
|
expect(pnl3.total_credit_value).to eq (trade.amount - total_fees) * 1.0
|
|
|
|
pnl4 = StatsMemberPnl.find_by(member_id: trade.taker_order.member.id, currency_id: trade.taker_order.outcome_currency.id, pnl_currency_id: 'eth')
|
|
expect(pnl4.total_credit).to eq 0
|
|
expect(pnl4.total_debit).to eq trade.total
|
|
expect(pnl4.total_debit_value).to eq trade.total * 1.0
|
|
expect(pnl4.total_credit_fees).to eq 0
|
|
expect(pnl4.total_credit_value).to eq 0
|
|
end
|
|
end
|
|
|
|
context 'trades of makers should not create pnls' do
|
|
let!(:trade) do
|
|
create(
|
|
:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d,
|
|
maker_order: create(:order_bid, :btceth, member: maker),
|
|
taker_order: create(:order_ask, :btceth, member: maker)
|
|
)
|
|
|
|
create(
|
|
:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d,
|
|
maker_order: create(:order_bid, :btceth, member: maker),
|
|
taker_order: create(:order_ask, :btceth, member: maker2)
|
|
)
|
|
end
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
context 'process' do
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Market.first.quote_unit, Market.second.quote_unit].map{|id| Currency.find(id)})
|
|
end
|
|
|
|
context 'no liabilities' do
|
|
it do
|
|
Jobs::Cron::StatsMemberPnl.process
|
|
expect(StatsMemberPnl.count).to eq 0
|
|
end
|
|
end
|
|
|
|
context 'liability for reference type deposit' do
|
|
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
|
|
coin_deposit.accept!
|
|
coin_deposit.process!
|
|
coin_deposit.dispatch!
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(2)
|
|
|
|
expect(StatsMemberPnl.second.member_id).to eq coin_deposit.member_id
|
|
expect(StatsMemberPnl.second.currency_id).to eq coin_deposit.currency_id
|
|
expect(StatsMemberPnl.second.pnl_currency_id).to eq Market.first.quote_unit
|
|
expect(StatsMemberPnl.second.total_credit).to eq coin_deposit.amount
|
|
expect(StatsMemberPnl.second.total_debit).to eq 0
|
|
expect(StatsMemberPnl.second.total_debit_value).to eq 0
|
|
expect(StatsMemberPnl.second.total_credit_fees).to eq coin_deposit.fee
|
|
expect(StatsMemberPnl.second.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.second.total_credit_value).to eq coin_deposit.amount * 1.0
|
|
|
|
expect(StatsMemberPnl.last.member_id).to eq coin_deposit.member_id
|
|
expect(StatsMemberPnl.last.currency_id).to eq coin_deposit.currency_id
|
|
expect(StatsMemberPnl.last.pnl_currency_id).to eq Market.second.quote_unit
|
|
expect(StatsMemberPnl.last.total_credit).to eq coin_deposit.amount
|
|
expect(StatsMemberPnl.last.total_debit).to eq 0
|
|
expect(StatsMemberPnl.last.total_debit_value).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_fees).to eq coin_deposit.fee
|
|
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
|
|
expect(StatsMemberPnl.last.total_credit_value).to eq coin_deposit.amount * 1.0
|
|
end
|
|
end
|
|
|
|
context 'liability for reference type deposit (not yet collected)' do
|
|
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
|
|
let!(:pnl) { create(:stats_member_pnl) }
|
|
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
|
|
coin_deposit.accept!
|
|
coin_deposit.process!
|
|
end
|
|
|
|
it do
|
|
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(2)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'scenario1_internal_sell' do
|
|
before do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('usd')])
|
|
end
|
|
|
|
def scenario1_internal_sell_with_partial_refund(msrc, mdst)
|
|
key = (Time.now.to_f * 1000).to_i
|
|
create(:deposit_usd, member: mdst, amount: 100).accept!
|
|
d = create(:deposit_btc, member: msrc, amount: 0.09)
|
|
d.accept!
|
|
d.process!
|
|
d.dispatch!
|
|
|
|
transfers_attr = [
|
|
{
|
|
key: key,
|
|
category: Transfer::CATEGORIES_MAPPING[:purchases],
|
|
operations: [
|
|
{
|
|
currency: :usd,
|
|
amount: 100,
|
|
account_src: {
|
|
code: 201,
|
|
uid: mdst.uid
|
|
},
|
|
account_dst: {
|
|
code: 211,
|
|
uid: mdst.uid
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: key + 1,
|
|
category: Transfer::CATEGORIES_MAPPING[:purchases],
|
|
operations: [
|
|
{
|
|
# Refund (unlock) user 10 usd
|
|
currency: :usd,
|
|
amount: 10,
|
|
account_src: {
|
|
code: 211,
|
|
uid: mdst.uid
|
|
},
|
|
account_dst: {
|
|
code: 201,
|
|
uid: mdst.uid
|
|
}
|
|
},
|
|
{
|
|
# Transfer 89 usd from user to the platform
|
|
currency: :usd,
|
|
amount: 89,
|
|
account_src: {
|
|
code: 211,
|
|
uid: mdst.uid
|
|
},
|
|
account_dst: {
|
|
code: 201,
|
|
uid: msrc.uid
|
|
}
|
|
},
|
|
{
|
|
# Transfer 1 usd from user to the platform fees
|
|
currency: :usd,
|
|
amount: 1,
|
|
account_src: {
|
|
code: 211,
|
|
uid: mdst.uid
|
|
},
|
|
account_dst: {
|
|
code: 301,
|
|
uid: mdst.uid
|
|
}
|
|
},
|
|
{
|
|
# Transfer 0.09 btc from the platform to the user
|
|
currency: :btc,
|
|
amount: 0.09,
|
|
account_src: {
|
|
code: 202,
|
|
uid: msrc.uid
|
|
},
|
|
account_dst: {
|
|
code: 202,
|
|
uid: mdst.uid
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
transfers_attr.each do |transfer_attrs|
|
|
create_transfer(transfer_attrs)
|
|
end
|
|
end
|
|
|
|
it 'excludes makers' do
|
|
scenario1_internal_sell_with_partial_refund(maker2, maker)
|
|
Jobs::Cron::StatsMemberPnl.process
|
|
expect(StatsMemberPnl.count).to eq(0)
|
|
end
|
|
|
|
it do
|
|
scenario1_internal_sell_with_partial_refund(member_platform, member)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('usd', 'usd', anything).returns(1)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('btc', 'usd', anything).returns(10_000)
|
|
Jobs::Cron::StatsMemberPnl.process
|
|
expect(StatsMemberPnl.count).to eq(4)
|
|
|
|
musd = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'usd')
|
|
expect(musd.total_credit).to eq(100)
|
|
expect(musd.total_credit_fees).to eq(0)
|
|
expect(musd.total_debit_fees).to eq(1)
|
|
expect(musd.total_debit).to eq(89)
|
|
expect(musd.total_credit_value).to eq(100)
|
|
expect(musd.total_debit_value).to eq(89)
|
|
expect(musd.total_balance_value).to eq(10)
|
|
expect(musd.average_balance_price).to eq(1)
|
|
|
|
mbtc = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'btc')
|
|
expect(mbtc.total_credit).to eq(0.09)
|
|
expect(mbtc.total_credit_fees).to eq(0)
|
|
expect(mbtc.total_debit_fees).to eq(0)
|
|
expect(mbtc.total_debit).to eq(0)
|
|
expect(mbtc.total_credit_value).to eq(90)
|
|
expect(mbtc.total_debit_value).to eq(0)
|
|
expect(mbtc.total_balance_value).to eq(90)
|
|
expect(mbtc.average_balance_price).to eq(1000)
|
|
end
|
|
|
|
def scenario2_internal_sell
|
|
key = (Time.now.to_f * 1000).to_i
|
|
create(:deposit_usd, member: member, amount: 100).accept!
|
|
d = create(:deposit_btc, member: member_platform, amount: 0.09)
|
|
d.accept!
|
|
d.process!
|
|
d.dispatch!
|
|
|
|
transfers_attr = [
|
|
{
|
|
key: key,
|
|
category: Transfer::CATEGORIES_MAPPING[:purchases],
|
|
operations: [
|
|
{
|
|
currency: :usd,
|
|
amount: 100,
|
|
account_src: {
|
|
code: 201,
|
|
uid: member.uid
|
|
},
|
|
account_dst: {
|
|
code: 211,
|
|
uid: member.uid
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: key + 1,
|
|
category: Transfer::CATEGORIES_MAPPING[:purchases],
|
|
operations: [
|
|
{
|
|
# Transfer 99 usd from user to the platform
|
|
currency: :usd,
|
|
amount: 99,
|
|
account_src: {
|
|
code: 211,
|
|
uid: member.uid
|
|
},
|
|
account_dst: {
|
|
code: 201,
|
|
uid: member_platform.uid
|
|
}
|
|
},
|
|
{
|
|
# Transfer 1 usd from user to the platform fees
|
|
currency: :usd,
|
|
amount: 1,
|
|
account_src: {
|
|
code: 211,
|
|
uid: member.uid
|
|
},
|
|
account_dst: {
|
|
code: 301,
|
|
uid: member.uid
|
|
}
|
|
},
|
|
{
|
|
# Transfer 0.09 btc from the platform to the user
|
|
currency: :btc,
|
|
amount: 0.09,
|
|
account_src: {
|
|
code: 202,
|
|
uid: member_platform.uid
|
|
},
|
|
account_dst: {
|
|
code: 202,
|
|
uid: member.uid
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
transfers_attr.each do |transfer_attrs|
|
|
create_transfer(transfer_attrs)
|
|
end
|
|
end
|
|
|
|
it do
|
|
scenario2_internal_sell
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('usd', 'usd', anything).returns(1)
|
|
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('btc', 'usd', anything).returns(10_000)
|
|
Jobs::Cron::StatsMemberPnl.process
|
|
|
|
expect(StatsMemberPnl.count).to eq(4)
|
|
musd = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'usd')
|
|
expect(musd.total_credit).to eq(100)
|
|
expect(musd.total_credit_fees).to eq(0)
|
|
expect(musd.total_debit_fees).to eq(1)
|
|
expect(musd.total_debit).to eq(99)
|
|
expect(musd.total_credit_value).to eq(100)
|
|
expect(musd.total_debit_value).to eq(99)
|
|
expect(musd.total_balance_value).to eq(0)
|
|
expect(musd.average_balance_price).to eq(1)
|
|
|
|
mbtc = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'btc')
|
|
expect(mbtc.total_credit).to eq(0.09)
|
|
expect(mbtc.total_credit_fees).to eq(0)
|
|
expect(mbtc.total_debit_fees).to eq(0)
|
|
expect(mbtc.total_debit).to eq(0)
|
|
expect(mbtc.total_credit_value).to be_within(0.0001).of(100)
|
|
expect(mbtc.total_debit_value).to eq(0)
|
|
expect(mbtc.total_balance_value).to be_within(0.0001).of(100)
|
|
expect(mbtc.average_balance_price).to be_within(0.01).of(1111.11)
|
|
end
|
|
end
|
|
|
|
context 'parse_conversion_paths' do
|
|
it do
|
|
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths(nil)).to eq({})
|
|
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('')).to eq({})
|
|
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:usdt/usd,usd/abc')).to eq(
|
|
'usdt/abc' => [['usdt', 'usd', false], ['usd', 'abc', false]],
|
|
)
|
|
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:usdt/usd,usd/abc;usdt/def:usdt/usd,def/abc,abc/usd')).to eq(
|
|
'usdt/abc' => [['usdt', 'usd', false], ['usd', 'abc', false]],
|
|
'usdt/def' => [['usdt', 'usd', false], ['def', 'abc', false], ['abc', 'usd', false]]
|
|
)
|
|
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:_usd/usdt,usd/abc')).to eq(
|
|
'usdt/abc' => [['usd', 'usdt', true], ['usd', 'abc', false]],
|
|
)
|
|
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc,abc/usd') }.to raise_error(StandardError)
|
|
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths(':usdt/abc,abc/usd') }.to raise_error(StandardError)
|
|
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdtabc:usdt/usd,usd/abc') }.to raise_error(StandardError)
|
|
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:usdtusd,usdabc') }.to raise_error(StandardError)
|
|
end
|
|
end
|
|
|
|
context 'conversion path' do
|
|
before(:each) do
|
|
Trade.stubs(:nearest_trade_from_influx).with('btceth', anything).returns(price: 0.95)
|
|
Trade.stubs(:nearest_trade_from_influx).with('btcusd', anything).returns(price: 10_000)
|
|
end
|
|
|
|
it do
|
|
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'eth', 0)).to eq(0.95)
|
|
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'usd', 0)).to eq(10_000)
|
|
end
|
|
|
|
it 'uses direct markets prices' do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:conversion_paths).returns(
|
|
'btc/abc' => [['btc', 'eth', false], ['btc', 'usd', false]]
|
|
)
|
|
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'abc', 0)).to eq(9500)
|
|
end
|
|
|
|
it 'reverses a market price' do
|
|
Jobs::Cron::StatsMemberPnl.stubs(:conversion_paths).returns(
|
|
'btc/abc' => [['btc', 'eth', true], ['btc', 'usd', false]]
|
|
)
|
|
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'abc', 0)).to be_within(0.0001).of(10526.3157)
|
|
end
|
|
end
|
|
end
|