-
-
Notifications
You must be signed in to change notification settings - Fork 7.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
liqui: rounding in amountToPrecision() leads to InsufficientFunds exception #393
Comments
Hi, @mkutny ! Thx for opening this issue!
In general, we need proper values for these entities:
Unfortunately, some exchanges round these values and some exchanges truncate them inside their engines. So, any of those values upon any stage of fee calculation can be either truncated or rounded by the exchange. And we don't know beforehand what an exchange will do, because those guys never document their exact formulas for calculating fee precisions. This is very frustrating, because they make millions on traders, and they don't even bother to document their engines in a financially-sound way. So we had to reverse-engineer it. We did that by placing market-making orders (to guarantee the exact price for which an order is closed). Then we waited for the order to close. And after that we did calculations on our side based on expected fees and rounding/truncation rules and compared the balance differences. If a balance difference was the same as expected, we carried that we have the calculation done right and fees were applied in correct amounts to correct sides. The balance difference has to be compared with respect to truncation or rounding of amount/price/cost and fee. There's even more to it: some exchanges apply fees to this or that side of your balance (base or quote currency of the pair), depending on the side of your trade (Poloniex), whereas other exchanges always apply fees to quote side (Bittrex, Liqui), or always to the base side of the pair. And some of them allow to specify the desired fee side (Kraken). So, we had to test all cases, namely both sell and buy orders to compare balances in all these cases. We did that for several exchanges, and Liqui was among them. This is why some exchanges use the rounding in *toPrecision methods (like bittrex and Liqui) and the other use truncation (like Kraken).
As the user might pass a string, we do the parsing/type-conversion as well. That is, until we obtain a fixed-point solution. I hope it does answer your question... Let us know what you think of it, plz. |
The source of the test is: const amount = exchange.amountToPrecision (symbol, 0.030)
const price_precision = market['precision']['price']
const amount_precision = market['precision']['amount']
const price_step = Math.pow (10, -price_precision)
const type = 'limit'
const side = (oldBalance[base] < (oldBalance[quote] / ticker['bid'])) ? 'buy' : 'sell'
// find the mid-spread price for placing a market-making order
const spread = ticker['ask'] - ticker['bid']
const price = exchange.priceToPrecision (symbol, (side == 'sell') ? (ticker['ask'] - spread / 2) : (ticker['bid'] + spread / 2))
const cost = exchange.costToPrecision (symbol, price * amount)
const fee = exchange.calculateFee (symbol, type, side, amount, price, 'maker')
log.green.bright (side.toUpperCase (), symbol)
log.green ('\t', 'Price:', price)
log.green ('\t', 'Amount:', amount)
log.green ('\t', 'Cost:', cost)
log.yellow ('\t', 'Fee:', fee)
let expectedBaseBalance = undefined
let expectedQuoteBalance = undefined
let expectedBaseDifference = undefined
let expectedQuoteDifference = undefined
let baseFeeCost = (fee.currency == base) ? fee.cost : 0
let quoteFeeCost = (fee.currency == quote) ? fee.cost : 0
if (side == 'sell') {
expectedBaseDifference = 0 - amount - baseFeeCost
expectedQuoteDifference = 0 + cost - quoteFeeCost
} else {
expectedBaseDifference = 0 + amount - baseFeeCost
expectedQuoteDifference = 0 - cost - quoteFeeCost
}
expectedBaseBalance = oldBalance[base] + expectedBaseDifference
expectedQuoteBalance = oldBalance[quote] + expectedQuoteDifference
log ('------------------------------------------------'.dim)
log ('expected balance:')
log (base, exchange.amountToPrecision (symbol, expectedBaseBalance).green)
log (quote, exchange.priceToPrecision (symbol, expectedQuoteBalance).green)
log ('------------------------------------------------'.dim)
log ('expected difference:')
log (base, ((expectedBaseDifference >= 0) ? '+' : '').green + expectedBaseDifference.toFixed (amount_precision).green)
log (quote, ((expectedQuoteDifference >= 0) ? '+' : '').green + expectedQuoteDifference.toFixed (price_precision).green)
log ('------------------------------------------------'.dim)
log ('Placing', side, amount, base, 'for', price, quote)
const reply = await exchange.createOrder ('ETH/BTC', 'limit', side, amount, price)
log ('reply:', reply)
log ('------------------------------------------------'.dim)
const orderId = reply['id']
// re-check open orders in a continuous loop
// until it's either closed or a timeout of 2 minutes expires
// (makers are not guaranteed to close immediately)
const order = await untilOrderIsClosed (exchange, orderId, symbol, params)
log ('------------------------------------------------'.dim)
log ('order:', order)
log ('------------------------------------------------'.dim)
log ('old balance:')
printBalance2 (base, oldBalance[base], quote, oldBalance[quote])
log ('------------------------------------------------'.dim)
log ('new balance:')
const newBalance = await exchange.fetchFreeBalance ()
printBalance2 (base, newBalance[base], quote, newBalance[quote])
log ('------------------------------------------------'.dim)
log ('balance difference:')
const actualBaseDifference = newBalance[base] - oldBalance[base]
const actualQuoteDifference = newBalance[quote] - oldBalance[quote]
log (base, ((actualBaseDifference >= 0) ? '+' : '').green + exchange.amountToPrecision (symbol, actualBaseDifference).toString ().green)
log (quote, ((actualQuoteDifference >= 0) ? '+' : '').green + exchange.priceToPrecision (symbol, actualQuoteDifference).toString ().green)
log ('------------------------------------------------'.dim)
const actualCostToPrecision = exchange.priceToPrecision (symbol, order.cost)
const expectedCostToPrecision = exchange.priceToPrecision (symbol, cost)
const actualPriceToPrecision = exchange.priceToPrecision (symbol, order.price)
const expectedPriceToPrecision = exchange.priceToPrecision (symbol, price)
// if our order landed as a maker, the price is guaranteed
// therefore both the cost and the price should be equal to what we expect
const orderCostIsMaker = (actualCostToPrecision === expectedCostToPrecision)
const orderPriceIsMaker = (actualPriceToPrecision === expectedPriceToPrecision)
// check basic order props
assert.property (order, 'filled')
assert.property (order, 'status')
assert.property (order, 'cost')
assert.property (order, 'fee')
// check if our order ended up being a maker indeed
assert.equal (orderCostIsMaker, true)
assert.equal (orderPriceIsMaker, true)
assert.equal (order.status, 'closed')
const expectedBaseDifferenceToPrecision = exchange.amountToPrecision (symbol, expectedBaseDifference)
const actualBaseDifferenceToPrecision = exchange.amountToPrecision (symbol, actualBaseDifference)
const expectedQuoteDifferenceToPrecision = exchange.priceToPrecision (symbol, expectedQuoteDifference)
const actualQuoteDifferenceToPrecision = exchange.priceToPrecision (symbol, actualQuoteDifference)
// check expected balances on both sides against current balance received from the exchange
assert.equal (actualBaseDifferenceToPrecision, expectedBaseDifferenceToPrecision)
assert.equal (actualQuoteDifferenceToPrecision, expectedQuoteDifferenceToPrecision)
const filledToPrecision = exchange.amountToPrecision (symbol, order.filled)
const amountToPrecision = exchange.amountToPrecision (symbol, amount)
assert.equal (filledToPrecision, amountToPrecision) The above code snippet may be missing some non-copied methods, but the general structure of it should be clear. Standing by to hear more from you on this. |
Hmm, that's quite elaborate, it took me some time to read it (and ccxt code) thoughtfully. (Just a side note: it might be just more convenient to calculate mid spread as Now I see that the problem is actually more complex than I previously thought so if you could elaborate a bit more on it that would be greatly appreciated:
|
Because we had a fixed order amount of 0.03 ETH there, we were sure that we will avoid amount precision problems in the test.
After reviewing the above code I can actually see now, that you are totally correct. So, I guess, we can switch to amount truncation, I don't foresee any problems there with respect to arithmetics.
Actually, I think we'll be fine with truncation, and if it turns out to be a problem, then we can fall back any moment to a solution that does throw a "lot"-modulo exception. So, if you feel like you want to give it a try, go ahead and switch it to truncation, I'll merge it right away. Thx! |
…395 amountToPrecision(): truncate, not round up
I know my code above isn't very clean %)) Your formula is much shorter, of course ) |
Basically,
createLimitSellOrder('BTC/USDT', 0.00206404549, 10000)
results inInsufficientFunds
exception asamountToPrecision
rounds amount up to 0.00206405.The following in the base class fixes the issue:
At the same time I'm a bit unsure to submit a PR because I'm bothered with the following questions:
I see several places in code where
feeToPrecision()
andcostToPrecision()
were overridden in a child's class with a similar fix. At the same time, it looks more natural to me if the base class contains the code with truncation so there is no need to override. Why it's not the case?What's the convention for passing
amount
? Those overrides doparseFloat(amount)
before truncation. Is it expected thatamount
might be a String as well? I see thattruncate
convertsamount
to a String anyway down the road.The text was updated successfully, but these errors were encountered: