-
Notifications
You must be signed in to change notification settings - Fork 100
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
client/{core, btc}: MultiTrade #2362
Conversation
client/asset/interface.go
Outdated
// Version is the asset version of the "from" asset with the init | ||
// transaction (this wallet). Most backends only support one version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wallets don't really have a concept of "from" and "to" assets. Saying (this wallet)
is curious too. What other wallet would we be talking about? The docstring for Swaps.Version
just says Version is the asset version. Most backends only support one version.
, which is better, but I'd drop the second sentence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree the comment is a bit strange... I just copied it from the Order
type. Should we just change Version
to SwapVersion
? The Order
type also has a RedeemVersion
. We have fromAsset
and toAsset
on assetSet
and fromWallet
and toWallet
on walletSet
btw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As soon as we started including information about the redeem asset, this wording became less redundant. It's unfortunate, but there is ambiguity to resolve with these comments.
client/asset/btc/btc.go
Outdated
@@ -761,6 +761,11 @@ func (s *swapOptions) feeBump() (float64, error) { | |||
return bump, nil | |||
} | |||
|
|||
type fundMultiOptions struct { | |||
Split *bool `ini:"swapsplit"` | |||
SplitBuffer *uint64 `ini:"splitbuffer"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think SplitBuffer
should be an asset config option in the WalletDefinition
. It can be here too, if you still think it would be necessary, and it would take precedence here, but if we can set the option for using splits at the asset level, we should expose this buffer too. I would definitely use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have this implemented for the regular split transactions, only for these MultiTrade splits. I wasn't planning on exposing the MultiTrade functionality through the UI. Why would you use the buffer with a regular trade?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't planning on exposing the MultiTrade functionality through the UI.
Where are users going to set these values?
Why would you use the buffer with a regular trade?
What do you mean? We're not dealing with "regular trades" here. Just bot stuff, right? Isn't the point of the buffer to enable utxo reuse without additional splits or excessive overlock.
client/asset/btc/btc.go
Outdated
type fundMultiOptions struct { | ||
Split *bool `ini:"swapsplit"` | ||
SplitBuffer *uint64 `ini:"splitbuffer"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are these options being exposed to the user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't planning to add this to the UI, this would just be available by programming against Core
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So only people who are writing their own software can benefit from these settings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, or people running market makers. There will be settings in the BotConfig
which tell the market maker whether or not to set these.
client/asset/btc/btc.go
Outdated
|
||
outputAddresses := make([]btcutil.Address, len(values)) | ||
for i, req := range requiredForOrders { | ||
changeAddr, err := btc.node.changeAddress() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a change address, so the variable name is misleading. The changeAddress
method should really be named internalAddress
.
client/asset/btc/btc.go
Outdated
// fundMultiBestEffors makes a best effort to fund every order. If it is not | ||
// possible, it returns coins for the orders that could be funded. The coins | ||
// that fund each order are returned in the same order as the values that were | ||
// passed in. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning partial results feels icky. We usually return an error if the caller requests more than is available. What's different here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're not really requesting more than is available, it's just that without a split the wallet would not be able to fund all of the orders without leaving keep
as available funds.
There is a check a few lines down:
if bal.Available < totalRequiredForOrders+keep {
return nil, nil, 0, fmt.Errorf("insufficient funds. %d < %d + %d",
bal.Available, totalRequiredForOrders, keep)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the caller asks to fund 5 orders, but we return partial results for 3, is the caller going to place those 3 orders? Do they have any say in which orders get funded?
It's kinda confusing because for normal order funding, if we can't do it without a split, we definitely can't do it with a split. I don't think that's the case with FundMulti
, because overlocks stack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had roughly the same question: #2362 (comment)
It stops on the first one that fails. I agree that's a difficult method to use effectively. I think it would need a flag indicate if you want to allow that, or return the coins and fail entirely if you can't get them all.
Related, in caller, it returns and error even if some orders were submitted: #2362 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea was that the caller passes the orders in order of priority. I can definitely add a flag. I was considering adding an allOrNothing
flag for this, but I didn't because I didn't see any of the bots passing true for this.
I think it would be a nice setting in the bot to only allow splitting once every X amount of time.. and in these cases the partial results at least allow funding of some orders until splitting is once again allowed.
client/asset/btc/btc.go
Outdated
return coins, redeemScripts, 0, nil | ||
} | ||
|
||
return btc.fundMultiWithSplit(keep+reserves, values, splitTxFeeRate, maxFeeRate, splitBuffer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something feels backwards. If I want a split, and you don't split, that would be wrong. I guess you're changing the verbiage to "allow" a split, rather than to actually request a split? I would definitely want the option to split by default. Not offering that option would potentially limit my ability to trade on multiple markets simultaneously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something feels backwards. If I want a split, and you don't split, that would be wrong.
I believe this has been the behavior since the beginning, at least when it is decided the split tx adds more "baggage" than the split would save by avoiding overlock. Perhaps the context is different here? Is the bool even "more optional" in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MultiTrade
will never lock more the balance.Available - keep
funds. The bot will pass keep
as the amount that has been reserved by bots trading on other markets. If splitting is not allowed, then MultiTrade
will only fund the orders that it is able to without dipping into the other bots' allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something feels backwards. If I want a split, and you don't split, that would be wrong.
I believe this has been the behavior since the beginning, at least when it is decided the split tx adds more "baggage" than the split would save by avoiding overlock. Perhaps the context is different here? Is the bool even "more optional" in this case?
For normal trades, we check both the split and the non-split and compare them. In MultiTrade
, if we can do it without splitting we do, regardless of the amount of overlock.
@martonp please check my understanding. Ignoring fees, if we want to fund 3 1-lot orders with a lot size of 1, and we have three utxos of 1, 1, and 1000, we would end up locking up all of it because fundMultiBestEffort
would succeed without error, resulting in an overlock of 999 and precluding any additional orders until the overlocked order matches and a swap is generated. And that is regardless of what the user has set for fundMultiOptions.Split
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@martonp please check my understanding. Ignoring fees, if we want to fund 3 1-lot orders with a lot size of 1, and we have three utxos of 1, 1, and 1000, we would end up locking up all of it because fundMultiBestEffort would succeed without error, resulting in an overlock of 999 and precluding any additional orders until the overlocked order matches and a swap is generated. And that is regardless of what the user has set for fundMultiOptions.Split.
Yes, this is correct, as long as maxLock
is greater than or equal to 1002. I'm thinking maybe the confusion is coming from the fact that the goal if splitting in FundOrder
is avoiding overlock, but the goal of splitting in FundMultiOrder
is to be able to fund multiple orders, and maxLock
is what deals with the overlocking. Maybe splitting in FundMultiOrder
should be called something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding to the confusion, the default for allowSplit is btc.useSplitTx() which is set in configuration and has a known definition.
True, I'll remove that default.
How would a caller calculate maxLock? They won't know how to calculate fees.
maxLock
is set to the balance that is allocated to the bot that is placing the orders. You can see this logic in #2332
Splitting should really be a default behavior for a bot, we just need to be smarter about how we do it. On what reasoning do you think a bot operator may want to not enable splitting?
I think splitting should be the default, but potentially someone would want to turn on the bot and play around with it without automatically having to spend money.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maxLock
is set to the balance that is allocated to the bot that is placing the orders. You can see this logic in #2332
Ah. I see form.MaxLock = c.balanceHandler.botBalance(c.botID, fromAsset)
now.
..
maxLock
is what deals with the overlocking.
If maxLock
is being used to indicate the balance available to the bot, can it simultaneously be used to limit overlock?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bot code could potentially calculate the amount required for the orders, add a buffer on top, and pass that for max lock, if that's desired. The main thing I was concerned with here is not using the other bots' funds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another simple approach would be to set maxLock to the smaller of the bot balance or some multiple of the total-value of the multi-trade. e.g. in the (1,1,1000) UTXO case where the total order value is just 3, if you set maxLock to 9 and avoid insane levels of overlock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding default to split, always doing a split is not great for several reasons, the top being the fees, but also the greatly slowed order submission because server is then waiting on network tx propagation to verify the fresh 0-conf funding UTXO.
client/asset/btc/btc.go
Outdated
totalOutputRequired += req | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be useful to check existing utxos to see if we already have one that satisfies our needs here? Bots will likely cancel and replace a lot of fully unmatched orders, If these orders change by a rate that keeps the old funding coins withing the split buffer, we can reuse the utxos. If we can maximize our reuse of those outputs and minimize the number of inputs that go into the split tx (by reducing the total funding needed), we could potentially see significant fee savings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fundMultiBestEffort
handles this. It tries to fund all of the orders with existing UTXOs. If it is not able to, only then is the split done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example: Ignoring fees, orders of (10, 10, 10), utxos of (10, 10, 15), maxLock = 30
, allowSplits = true
, fundMultiBestEffort
would fail because of maxLock
. fundMultiWithSplit
would consume all three inputs to create outputs of (10, 10, 10, 5), even though we already had two outputs of 10. The ideal split would just consume the utxo of value 15 and create outputs at (10, 5). Inputs are much more expensive than outputs. So the current algo almost triples the fees associated with the split transaction compared to the ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good point. I'll combine the best effort logic with splitting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a swing at a hybrid approach here, but the diff is kinda messy, so might be easier to look at this first. (Not tested and tests not updated)
The primary differences
- Separates available funding (
maxLock
) from allowable overfunding (overfundBuffer
) - Only creates splits when existing outputs don't work
A notable consequence
- Orders can be funded out of order in partial results. For instance, if we're funding values (1, 3, 1), and we have utxos (1, 1, 50), low overfund allowance, and
allowSplit = false
, the first and last orders will be funded, but not the middle, since that would require an high overlock
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've also been working on it, I've pushed the change. I think overfundBuffer
is probably a good idea to add.
My implementation will be O(n^2)
in the number of orders when a split is needed, and yours will always be O(n)
, but I think sometimes an order that requires less funds may take the UTXO allocated for the larger one in your implementation. But both are better than the original in that they will only create the split for the orders that actually need it. I think if the splits are done very rarely, better to be more accurate though.
leastOverFund
could also be modified to favor the single UTXO instead of the set.
i think as long as we have a correct implementation, we could go with that for now. Joe is working on a testing framework for the bots, when running those we can see how many splits are being done and how the performance is, and adjust based on the results.
This adds a `core.MultiTrade` function. It allows callers to fund multiple trades in one go. It is only implemented for BTC currently, but will be implemented for other assets later. In order to fund multiple orders in one go, the BTC wallet first attempts to fund each of the orders with the existing UTXOs. If this was not possible and splitting is not allowed, the orders that are able to be funded are returned. If splitting is allowed, a split transaction with one output per order will be created. This also updates `FundOrder` to return the fees spent on any split transactions, and this is stored in the database.
I've been testing using #2409 and this has been working well. |
Great. I'll do a few manual mult-trades with the RPC client, but I have no more comments or requests on the code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All working as expected via the RPC server's multitrade
command. I'm excited to see how this comes together in the final bot project.
This adds a
core.MultiTrade
function. It allows callers to fund multiple trades in one go. It is only implemented for BTC currently, but will be implemented for other assets later, after this is reviewed. In order to fund multiple orders in one go, the BTC wallet first attempts to fund each of the orders with the existing UTXOs. If this was not possible and splitting is not allowed, the orders that are able to be funded are returned. If splitting is allowed, a split transaction with one output per order will be created.This also updates
FundOrder
to return the fees spent on any split transactions, and this is stored in the database.